@fml-inc/fml 0.8.0-rc.1 → 0.8.0-rc.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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createFmlClient,
4
4
  getAuthenticatedClient
5
- } from "./chunk-P3QLARXE.js";
5
+ } from "./chunk-R45CWWZO.js";
6
6
  import {
7
7
  OBSERVER_HEARTBEAT_STALE_MS,
8
8
  getProcessIdentity,
@@ -10,13 +10,13 @@ import {
10
10
  isPidAlive,
11
11
  readObserverStateStatus,
12
12
  removeObserverStateIfOwned
13
- } from "./chunk-HUR2HCUG.js";
13
+ } from "./chunk-7DWFJENY.js";
14
14
  import {
15
15
  readJsonFile,
16
16
  setHookCaptureDisabled,
17
17
  writeJsonFile,
18
18
  writeTextFileAtomic
19
- } from "./chunk-I4GZPTBM.js";
19
+ } from "./chunk-ENW2GJNO.js";
20
20
  import {
21
21
  CODEX_HOOK_EVENTS,
22
22
  GEMINI_HOOK_EVENTS,
@@ -24,8 +24,8 @@ import {
24
24
  isObserverHealth,
25
25
  readHookInboxStatus,
26
26
  readObserverAuthToken
27
- } from "./chunk-DEMVOVVY.js";
28
- import "./chunk-T5YVLSUC.js";
27
+ } from "./chunk-KBT5RMAU.js";
28
+ import "./chunk-7V7G7NVF.js";
29
29
  import {
30
30
  claudeConfigDirectory,
31
31
  createCliRawSync,
@@ -34,7 +34,7 @@ import {
34
34
  initializeSyncSettings,
35
35
  resolvePackageRoot,
36
36
  runRawSyncOnce
37
- } from "./chunk-JXSSPQ5B.js";
37
+ } from "./chunk-YSU5CS2V.js";
38
38
  import {
39
39
  DEFAULT_PROD_URL,
40
40
  DEFAULT_TARGET_NAME,
@@ -76,7 +76,7 @@ import {
76
76
  storeServiceRefreshToken,
77
77
  writeEnvConfig,
78
78
  writeTokens
79
- } from "./chunk-7DR5X7XI.js";
79
+ } from "./chunk-A6HAEVJX.js";
80
80
  import "./chunk-MLKGABMK.js";
81
81
 
82
82
  // src/cli.ts
@@ -124,7 +124,7 @@ var POLL_INTERVAL_MS = 50;
124
124
  var LOG_ROTATE_BYTES = 10 * 1024 * 1024;
125
125
  var LOG_ROTATE_FILES = 3;
126
126
  function expectedObserverVersion() {
127
- return true ? "0.8.0-rc.1+60c9b7e1" : "dev";
127
+ return true ? "0.8.0-rc.3+246af775" : "dev";
128
128
  }
129
129
  function delay(ms) {
130
130
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -776,20 +776,58 @@ function probeLocalStorage() {
776
776
  });
777
777
  }
778
778
  }
779
+ function humanCheckResult(result) {
780
+ switch (result.label) {
781
+ case "Auth":
782
+ return { label: "Account", detail: result.detail };
783
+ case "Environment": {
784
+ const environmentName = result.detail.replace(
785
+ / has no backend URL$/u,
786
+ ""
787
+ );
788
+ return {
789
+ label: "Environment",
790
+ detail: result.status === "ok" ? result.detail.split(" \u2192 ")[0] ?? result.detail : `No service URL is configured for ${environmentName}. Run \`fml env <name-or-url>\`.`
791
+ };
792
+ }
793
+ case "Organization":
794
+ return { label: "Organization", detail: result.detail };
795
+ case "Local data":
796
+ return { label: "Local storage", detail: result.detail };
797
+ case "Database lease":
798
+ return {
799
+ label: "Runtime state",
800
+ detail: result.detail.replace("observer", "FML service")
801
+ };
802
+ case "Raw sync":
803
+ return {
804
+ label: "Agent activity sync",
805
+ detail: result.detail.replace("observer state", "FML service state").replace("raw sync", "agent activity sync")
806
+ };
807
+ case "API":
808
+ return { label: "FML API", detail: result.detail };
809
+ case "Observer":
810
+ return {
811
+ label: "FML service",
812
+ detail: result.detail.replace("observer", "FML service").replace("continuous sync", "agent activity sync").replace("raw sync", "agent activity sync")
813
+ };
814
+ }
815
+ return null;
816
+ }
779
817
  function reportResult(result, spinner) {
780
- const text = `${result.label} \u2014 ${result.detail}`;
781
818
  if (spinner) {
782
- if (result.status === "ok") spinner.succeed(text);
783
- if (result.status === "warn") spinner.warn(text);
784
- if (result.status === "fail") spinner.fail(text);
785
- return;
819
+ spinner.indent = 0;
820
+ spinner.stop();
786
821
  }
822
+ const humanResult = humanCheckResult(result);
823
+ if (!humanResult) return;
787
824
  const icon = result.status === "ok" ? pc.green("\u2713") : result.status === "warn" ? pc.yellow("!") : pc.red("\u2717");
788
- console.log(` ${icon} ${text}`);
825
+ console.log(` ${icon} ${humanResult.label} \u2014 ${humanResult.detail}`);
789
826
  }
790
827
  async function handleDoctor(opts) {
791
828
  const environment = getActiveEnv();
792
829
  const isTTY = !opts.json && process.stdout.isTTY === true;
830
+ if (!opts.json) console.log("FML doctor\n");
793
831
  const checks = [];
794
832
  const startSpinner = (label) => isTTY ? ora({ text: `Checking ${label}...`, indent: 2 }).start() : null;
795
833
  const record = (result, spinner) => {
@@ -909,7 +947,7 @@ async function handleDoctor(opts) {
909
947
  }
910
948
  }
911
949
  {
912
- const spinner = startSpinner("raw sync");
950
+ const spinner = startSpinner("agent activity sync");
913
951
  try {
914
952
  try {
915
953
  probeDurableDirectory(FML_OBSERVER_DIR, {
@@ -977,7 +1015,7 @@ async function handleDoctor(opts) {
977
1015
  }
978
1016
  }
979
1017
  {
980
- const spinner = startSpinner("local observer");
1018
+ const spinner = startSpinner("FML service");
981
1019
  const rawSync2 = createRawArtifactSync({ stateDir: FML_OBSERVER_DIR });
982
1020
  const syncEnabled = rawSync2.getSettings().enabled;
983
1021
  const observer = await readObserverRuntimeStatus();
@@ -1018,12 +1056,15 @@ async function handleDoctor(opts) {
1018
1056
  console.log(`
1019
1057
  ${parts.join(", ")}
1020
1058
  `);
1059
+ if (failed > 0) {
1060
+ console.log("Fix the failed checks above, then run `fml doctor` again.");
1061
+ }
1021
1062
  }
1022
1063
 
1023
1064
  // src/commands/install.ts
1024
- import fs9 from "fs";
1065
+ import fs10 from "fs";
1025
1066
  import os5 from "os";
1026
- import path21 from "path";
1067
+ import path22 from "path";
1027
1068
  import { fileURLToPath } from "url";
1028
1069
 
1029
1070
  // ../../packages/agent-targets/src/install/claude/index.ts
@@ -1264,39 +1305,166 @@ var CLAUDE_PLUGIN_INSTALLER = Object.freeze({
1264
1305
  })
1265
1306
  });
1266
1307
 
1267
- // src/banner.ts
1268
- import pc2 from "picocolors";
1269
- var LOGO_LINES = [
1270
- " \u2584\u2588\u2588\u2580 \u2588\u2588\u2584\u2588\u2588\u2584\u2588\u2588 \u2588\u2588",
1271
- " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
1272
- "\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
1273
- " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588",
1274
- " \u2580\u2580 \u2580\u2580 \u2580\u2580 \u2580\u2580\u2580\u2580"
1275
- ];
1276
- function printBanner() {
1277
- console.log("");
1278
- for (let i = 0; i < LOGO_LINES.length; i++) {
1279
- const color = i < 3 ? pc2.blue : pc2.cyan;
1280
- console.log(" " + color(LOGO_LINES[i]));
1308
+ // ../../packages/agent-targets/src/install/pi/index.ts
1309
+ import fs3 from "fs";
1310
+ import path4 from "path";
1311
+ import { randomUUID as randomUUID2 } from "crypto";
1312
+ var FML_PI_EXTENSION_MARKER = "__FML_PI_EXTENSION_V1__";
1313
+ var LEGACY_PI_EXTENSION_MARKER = "__panopticonPiExtensionTest";
1314
+ var LEGACY_PI_EXTENSION_SIGNATURES = Object.freeze([
1315
+ "PANOPTICON_PORT",
1316
+ '"/hooks"',
1317
+ "UserPromptSubmit",
1318
+ "PreToolUse",
1319
+ "PostToolUse",
1320
+ "SessionEnd"
1321
+ ]);
1322
+ function pathApiFor(platform) {
1323
+ return platform === "win32" ? path4.win32 : path4.posix;
1324
+ }
1325
+ function resolvePiAgentDir(context, pathApi) {
1326
+ const configured = context.env.PI_CODING_AGENT_DIR?.trim();
1327
+ if (!configured) {
1328
+ return pathApi.join(context.homeDir, ".pi", "agent");
1329
+ }
1330
+ if (configured === "~") return context.homeDir;
1331
+ if (configured.startsWith("~/") || configured.startsWith("~\\")) {
1332
+ return pathApi.join(context.homeDir, configured.slice(2));
1333
+ }
1334
+ return pathApi.normalize(configured);
1335
+ }
1336
+ function readFileIfPresent(filePath) {
1337
+ try {
1338
+ return fs3.readFileSync(filePath, "utf8");
1339
+ } catch (error) {
1340
+ if (error.code === "ENOENT") return null;
1341
+ throw error;
1342
+ }
1343
+ }
1344
+ function isOwnedExtension(source, legacy = false) {
1345
+ return source.includes(FML_PI_EXTENSION_MARKER) || legacy && (source.includes(LEGACY_PI_EXTENSION_MARKER) || LEGACY_PI_EXTENSION_SIGNATURES.every(
1346
+ (signature) => source.includes(signature)
1347
+ ));
1348
+ }
1349
+ function removeLegacyExtension(filePath) {
1350
+ const source = readFileIfPresent(filePath);
1351
+ if (source === null) return { removed: false, preserved: false };
1352
+ if (!isOwnedExtension(source, true)) {
1353
+ return { removed: false, preserved: true };
1354
+ }
1355
+ fs3.rmSync(filePath);
1356
+ return { removed: true, preserved: false };
1357
+ }
1358
+ function writeExtensionAtomically(filePath, source) {
1359
+ fs3.mkdirSync(path4.dirname(filePath), { recursive: true, mode: 448 });
1360
+ const temporaryPath = path4.join(
1361
+ path4.dirname(filePath),
1362
+ `.${path4.basename(filePath)}.${process.pid}.${randomUUID2()}.tmp`
1363
+ );
1364
+ try {
1365
+ fs3.writeFileSync(temporaryPath, source, { encoding: "utf8", mode: 384 });
1366
+ if (process.platform === "win32" && fs3.existsSync(filePath)) {
1367
+ fs3.rmSync(filePath);
1368
+ }
1369
+ fs3.renameSync(temporaryPath, filePath);
1370
+ } finally {
1371
+ fs3.rmSync(temporaryPath, { force: true });
1281
1372
  }
1282
- console.log("");
1283
1373
  }
1374
+ var PI_EXTENSION_INSTALLER = Object.freeze({
1375
+ resolvePaths(context) {
1376
+ const pathApi = pathApiFor(context.platform);
1377
+ const extensionsDir = pathApi.join(
1378
+ resolvePiAgentDir(context, pathApi),
1379
+ "extensions"
1380
+ );
1381
+ return {
1382
+ sourcePath: pathApi.join(
1383
+ context.packageRoot,
1384
+ "dist",
1385
+ "pi",
1386
+ "extension.js"
1387
+ ),
1388
+ extensionsDir,
1389
+ destinationPath: pathApi.join(extensionsDir, "fml.js"),
1390
+ legacyDestinationPath: pathApi.join(extensionsDir, "panopticon.js")
1391
+ };
1392
+ },
1393
+ install(paths) {
1394
+ const source = fs3.readFileSync(paths.sourcePath, "utf8");
1395
+ if (!source.includes(FML_PI_EXTENSION_MARKER)) {
1396
+ throw new Error(
1397
+ `Packaged Pi extension is invalid: ${paths.sourcePath} does not contain the FML ownership marker`
1398
+ );
1399
+ }
1400
+ const existing = readFileIfPresent(paths.destinationPath);
1401
+ if (existing !== null && !isOwnedExtension(existing)) {
1402
+ const legacy2 = removeLegacyExtension(paths.legacyDestinationPath);
1403
+ return {
1404
+ status: "preserved",
1405
+ destinationPath: paths.destinationPath,
1406
+ reason: "an extension not managed by FML already exists at this path",
1407
+ legacyRemoved: legacy2.removed,
1408
+ legacyPreserved: legacy2.preserved
1409
+ };
1410
+ }
1411
+ const status = existing === null ? "installed" : existing === source ? "unchanged" : "updated";
1412
+ if (status !== "unchanged") {
1413
+ writeExtensionAtomically(paths.destinationPath, source);
1414
+ }
1415
+ const legacy = removeLegacyExtension(paths.legacyDestinationPath);
1416
+ return {
1417
+ status,
1418
+ destinationPath: paths.destinationPath,
1419
+ legacyRemoved: legacy.removed,
1420
+ legacyPreserved: legacy.preserved
1421
+ };
1422
+ },
1423
+ uninstall(paths) {
1424
+ const existing = readFileIfPresent(paths.destinationPath);
1425
+ const legacy = removeLegacyExtension(paths.legacyDestinationPath);
1426
+ if (existing === null) {
1427
+ return {
1428
+ status: "absent",
1429
+ destinationPath: paths.destinationPath,
1430
+ legacyRemoved: legacy.removed,
1431
+ legacyPreserved: legacy.preserved
1432
+ };
1433
+ }
1434
+ if (!isOwnedExtension(existing)) {
1435
+ return {
1436
+ status: "preserved",
1437
+ destinationPath: paths.destinationPath,
1438
+ reason: "the existing extension is not managed by FML",
1439
+ legacyRemoved: legacy.removed,
1440
+ legacyPreserved: legacy.preserved
1441
+ };
1442
+ }
1443
+ fs3.rmSync(paths.destinationPath);
1444
+ return {
1445
+ status: "removed",
1446
+ destinationPath: paths.destinationPath,
1447
+ legacyRemoved: legacy.removed,
1448
+ legacyPreserved: legacy.preserved
1449
+ };
1450
+ }
1451
+ });
1284
1452
 
1285
1453
  // src/bin-utils.ts
1286
- import fs3 from "fs";
1287
- import path4 from "path";
1454
+ import fs4 from "fs";
1455
+ import path5 from "path";
1288
1456
  import { execFileSync, execSync } from "child_process";
1289
1457
  var IS_WIN = process.platform === "win32";
1290
1458
  var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
1291
1459
  function resolveBin(name) {
1292
- const pathDirs = (process.env.PATH ?? "").split(path4.delimiter).filter(Boolean);
1293
- const hasExt = path4.extname(name) !== "";
1460
+ const pathDirs = (process.env.PATH ?? "").split(path5.delimiter).filter(Boolean);
1461
+ const hasExt = path5.extname(name) !== "";
1294
1462
  const exts = !IS_WIN || hasExt ? [""] : (process.env.PATHEXT ?? DEFAULT_PATHEXT).split(";").filter(Boolean);
1295
1463
  for (const dir of pathDirs) {
1296
1464
  for (const ext of exts) {
1297
- const candidate = path4.join(dir, name + ext);
1465
+ const candidate = path5.join(dir, name + ext);
1298
1466
  try {
1299
- if (fs3.statSync(candidate).isFile()) return candidate;
1467
+ if (fs4.statSync(candidate).isFile()) return candidate;
1300
1468
  } catch {
1301
1469
  }
1302
1470
  }
@@ -1328,18 +1496,18 @@ function execBinSync(bin, args, opts = {}) {
1328
1496
  }
1329
1497
 
1330
1498
  // src/managed-directories.ts
1331
- import fs4 from "fs";
1499
+ import fs5 from "fs";
1332
1500
  import os from "os";
1333
- import path5 from "path";
1501
+ import path6 from "path";
1334
1502
  var FML_MANAGED_DIRECTORY_MARKER = ".fml-managed-root";
1335
1503
  var FML_MANAGED_DIRECTORY_MARKER_CONTENT = "fml-managed-directory-v1\n";
1336
1504
  function comparisonPathApi(platform, targetPaths) {
1337
1505
  if (platform === "win32" && targetPaths.some(
1338
1506
  (targetPath) => /^[A-Za-z]:[\\/]/u.test(targetPath) || targetPath.startsWith("\\\\") || targetPath.startsWith("//")
1339
1507
  )) {
1340
- return path5.win32;
1508
+ return path6.win32;
1341
1509
  }
1342
- return path5;
1510
+ return path6;
1343
1511
  }
1344
1512
  function resolveComparisonPath(targetPath, platform) {
1345
1513
  return comparisonPathApi(platform, [targetPath]).resolve(targetPath);
@@ -1412,7 +1580,7 @@ function configuredManagedRoots(platform = process.platform) {
1412
1580
  );
1413
1581
  }
1414
1582
  function managedDirectoryMarker(directory) {
1415
- return path5.join(directory, FML_MANAGED_DIRECTORY_MARKER);
1583
+ return path6.join(directory, FML_MANAGED_DIRECTORY_MARKER);
1416
1584
  }
1417
1585
  function ownershipStatus(stats, platform) {
1418
1586
  if (platform === "win32") return "unverifiable";
@@ -1423,8 +1591,8 @@ function ownershipStatus(stats, platform) {
1423
1591
  function hasManagedDirectoryMarkerContents(directory) {
1424
1592
  const marker = managedDirectoryMarker(directory);
1425
1593
  try {
1426
- const stats = fs4.lstatSync(marker);
1427
- return stats.isFile() && fs4.readFileSync(marker, "utf8") === FML_MANAGED_DIRECTORY_MARKER_CONTENT;
1594
+ const stats = fs5.lstatSync(marker);
1595
+ return stats.isFile() && fs5.readFileSync(marker, "utf8") === FML_MANAGED_DIRECTORY_MARKER_CONTENT;
1428
1596
  } catch {
1429
1597
  return false;
1430
1598
  }
@@ -1432,11 +1600,11 @@ function hasManagedDirectoryMarkerContents(directory) {
1432
1600
  function hasManagedDirectoryMarker(directory, platform = process.platform) {
1433
1601
  const marker = managedDirectoryMarker(directory);
1434
1602
  try {
1435
- const stats = fs4.lstatSync(marker);
1603
+ const stats = fs5.lstatSync(marker);
1436
1604
  if (!stats.isFile() || ownershipStatus(stats, platform) !== "owned") {
1437
1605
  return false;
1438
1606
  }
1439
- return fs4.readFileSync(marker, "utf8") === FML_MANAGED_DIRECTORY_MARKER_CONTENT;
1607
+ return fs5.readFileSync(marker, "utf8") === FML_MANAGED_DIRECTORY_MARKER_CONTENT;
1440
1608
  } catch {
1441
1609
  return false;
1442
1610
  }
@@ -1444,7 +1612,7 @@ function hasManagedDirectoryMarker(directory, platform = process.platform) {
1444
1612
  function writeManagedDirectoryMarker(directory, platform) {
1445
1613
  const marker = managedDirectoryMarker(directory);
1446
1614
  try {
1447
- fs4.writeFileSync(marker, FML_MANAGED_DIRECTORY_MARKER_CONTENT, {
1615
+ fs5.writeFileSync(marker, FML_MANAGED_DIRECTORY_MARKER_CONTENT, {
1448
1616
  encoding: "utf8",
1449
1617
  flag: "wx",
1450
1618
  mode: 384
@@ -1469,23 +1637,23 @@ function prepareManagedDirectory(root, platform = process.platform) {
1469
1637
  let created = false;
1470
1638
  let existingStats = null;
1471
1639
  try {
1472
- existingStats = fs4.lstatSync(root.directory);
1640
+ existingStats = fs5.lstatSync(root.directory);
1473
1641
  } catch (error) {
1474
1642
  if (error.code !== "ENOENT") throw error;
1475
1643
  existed = false;
1476
1644
  }
1477
1645
  if (!existed) {
1478
- fs4.mkdirSync(path5.dirname(root.directory), {
1646
+ fs5.mkdirSync(path6.dirname(root.directory), {
1479
1647
  recursive: true,
1480
1648
  mode: 448
1481
1649
  });
1482
1650
  try {
1483
- fs4.mkdirSync(root.directory, { mode: 448 });
1651
+ fs5.mkdirSync(root.directory, { mode: 448 });
1484
1652
  created = true;
1485
1653
  } catch (error) {
1486
1654
  if (error.code !== "EEXIST") throw error;
1487
1655
  existed = true;
1488
- existingStats = fs4.lstatSync(root.directory);
1656
+ existingStats = fs5.lstatSync(root.directory);
1489
1657
  }
1490
1658
  }
1491
1659
  if (hasSymlinkedParentBelowTrustedAnchor(root.directory, platform)) {
@@ -1495,7 +1663,7 @@ function prepareManagedDirectory(root, platform = process.platform) {
1495
1663
  markerCreated: false
1496
1664
  };
1497
1665
  }
1498
- const directoryStats = existingStats ?? fs4.lstatSync(root.directory);
1666
+ const directoryStats = existingStats ?? fs5.lstatSync(root.directory);
1499
1667
  const ownership = ownershipStatus(directoryStats, platform);
1500
1668
  const recognizedWindowsMarker = platform === "win32" && hasManagedDirectoryMarkerContents(root.directory);
1501
1669
  if (directoryStats.isSymbolicLink() || !directoryStats.isDirectory() || ownership === "foreign" || !root.nativeDefault && ownership !== "owned" && !(created && ownership === "unverifiable") && !recognizedWindowsMarker) {
@@ -1519,7 +1687,7 @@ function prepareManagedDirectory(root, platform = process.platform) {
1519
1687
  markerCreated: false
1520
1688
  };
1521
1689
  }
1522
- const mayClaim = fs4.readdirSync(root.directory).length === 0;
1690
+ const mayClaim = fs5.readdirSync(root.directory).length === 0;
1523
1691
  if (!mayClaim) {
1524
1692
  return {
1525
1693
  directory: root.directory,
@@ -1578,7 +1746,7 @@ function hasSymlinkedParentBelowTrustedAnchor(targetPath, platform = process.pla
1578
1746
  for (const component of relativeParent.split(pathApi.sep)) {
1579
1747
  current = pathApi.join(current, component);
1580
1748
  try {
1581
- if (fs4.lstatSync(current).isSymbolicLink()) return true;
1749
+ if (fs5.lstatSync(current).isSymbolicLink()) return true;
1582
1750
  } catch (error) {
1583
1751
  if (error.code === "ENOENT") return false;
1584
1752
  return true;
@@ -1603,7 +1771,7 @@ function unsafePurgeRoots(roots, platform = process.platform) {
1603
1771
  }
1604
1772
  let stats;
1605
1773
  try {
1606
- stats = fs4.lstatSync(root.directory);
1774
+ stats = fs5.lstatSync(root.directory);
1607
1775
  } catch (error) {
1608
1776
  if (error.code === "ENOENT") continue;
1609
1777
  unsafe.push({
@@ -1652,14 +1820,14 @@ function unsafePurgeRoots(roots, platform = process.platform) {
1652
1820
  }
1653
1821
 
1654
1822
  // src/commands/hook-config.ts
1655
- import fs5 from "fs";
1823
+ import fs6 from "fs";
1656
1824
  import os2 from "os";
1657
- import path10 from "path";
1825
+ import path11 from "path";
1658
1826
 
1659
1827
  // ../../packages/agent-targets/src/install/hooks/codex.ts
1660
1828
  import { createHash } from "crypto";
1661
1829
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
1662
- import path7 from "path";
1830
+ import path8 from "path";
1663
1831
  import { parse as parse2 } from "smol-toml";
1664
1832
 
1665
1833
  // ../../packages/agent-targets/src/toml.ts
@@ -1669,7 +1837,7 @@ function asRecord2(value) {
1669
1837
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1670
1838
  }
1671
1839
  function singlePath(value) {
1672
- const path29 = [];
1840
+ const path30 = [];
1673
1841
  let current = value;
1674
1842
  while (true) {
1675
1843
  if (Array.isArray(current) && current.length === 1) {
@@ -1677,13 +1845,13 @@ function singlePath(value) {
1677
1845
  continue;
1678
1846
  }
1679
1847
  const record = asRecord2(current);
1680
- if (!record) return path29;
1848
+ if (!record) return path30;
1681
1849
  const keys = Object.keys(record);
1682
- if (keys.length === 0) return path29;
1850
+ if (keys.length === 0) return path30;
1683
1851
  if (keys.length !== 1) throw new Error("ambiguous TOML statement path");
1684
1852
  const key = keys[0];
1685
1853
  if (key === void 0) throw new Error("missing TOML statement key");
1686
- path29.push(key);
1854
+ path30.push(key);
1687
1855
  current = record[key];
1688
1856
  }
1689
1857
  }
@@ -1765,8 +1933,8 @@ function scanStatements(source) {
1765
1933
  }
1766
1934
  return statements;
1767
1935
  }
1768
- function startsWithPath(path29, prefix) {
1769
- return prefix.every((part, index) => path29[index] === part);
1936
+ function startsWithPath(path30, prefix) {
1937
+ return prefix.every((part, index) => path30[index] === part);
1770
1938
  }
1771
1939
  function pathsEqual(left, right) {
1772
1940
  return left.length === right.length && left.every((part, index) => part === right[index]);
@@ -1792,9 +1960,9 @@ function appendToml(source, addition) {
1792
1960
  function hasOwn(record, key) {
1793
1961
  return Object.prototype.hasOwnProperty.call(record, key);
1794
1962
  }
1795
- function updatedValueAtPath(updated, path29) {
1963
+ function updatedValueAtPath(updated, path30) {
1796
1964
  let current = updated;
1797
- for (const part of path29) {
1965
+ for (const part of path30) {
1798
1966
  const record = asRecord2(current);
1799
1967
  if (!record || !hasOwn(record, part)) return { exists: false };
1800
1968
  current = record[part];
@@ -1808,22 +1976,22 @@ function normalizedPaths(updates) {
1808
1976
  throw new Error("TOML update path cannot be empty");
1809
1977
  }
1810
1978
  if (paths.some(
1811
- (path29) => pathsEqual(path29, update.path) || startsWithPath(update.path, path29)
1979
+ (path30) => pathsEqual(path30, update.path) || startsWithPath(update.path, path30)
1812
1980
  )) {
1813
1981
  continue;
1814
1982
  }
1815
1983
  for (let index = paths.length - 1; index >= 0; index -= 1) {
1816
- const path29 = paths[index];
1817
- if (path29 && startsWithPath(path29, update.path)) paths.splice(index, 1);
1984
+ const path30 = paths[index];
1985
+ if (path30 && startsWithPath(path30, update.path)) paths.splice(index, 1);
1818
1986
  }
1819
1987
  paths.push([...update.path]);
1820
1988
  }
1821
1989
  return paths;
1822
1990
  }
1823
- function valueDocument(path29, value) {
1991
+ function valueDocument(path30, value) {
1824
1992
  let nested = value;
1825
- for (let index = path29.length - 1; index >= 0; index -= 1) {
1826
- const part = path29[index];
1993
+ for (let index = path30.length - 1; index >= 0; index -= 1) {
1994
+ const part = path30[index];
1827
1995
  if (part === void 0) throw new Error("TOML update path is invalid");
1828
1996
  nested = { [part]: nested };
1829
1997
  }
@@ -1842,12 +2010,12 @@ function insertAfterStatement(source, statement, addition) {
1842
2010
  const separator = before.endsWith(eol) ? "" : eol;
1843
2011
  return `${before}${separator}${normalized}${eol}${source.slice(statement.end)}`;
1844
2012
  }
1845
- function insertUpdatedValue(source, path29, value) {
1846
- const key = path29.at(-1);
2013
+ function insertUpdatedValue(source, path30, value) {
2014
+ const key = path30.at(-1);
1847
2015
  if (key === void 0) throw new Error("TOML update path cannot be empty");
1848
2016
  const assignment = localAssignment(key, value);
1849
2017
  if (assignment !== null) {
1850
- const parent = path29.slice(0, -1);
2018
+ const parent = path30.slice(0, -1);
1851
2019
  const parentTables = scanStatements(source).filter(
1852
2020
  (statement) => statement.isTable && pathsEqual(statement.path, parent)
1853
2021
  );
@@ -1861,17 +2029,17 @@ function insertUpdatedValue(source, path29, value) {
1861
2029
  return insertAfterStatement(source, parentTable, assignment);
1862
2030
  }
1863
2031
  }
1864
- return appendToml(source, stringify(valueDocument(path29, value)));
2032
+ return appendToml(source, stringify(valueDocument(path30, value)));
1865
2033
  }
1866
2034
  function updateTomlPaths(source, updated, updates) {
1867
2035
  const paths = normalizedPaths(updates);
1868
2036
  if (paths.length === 0) return source;
1869
2037
  const stripped = removeStatements(source, scanStatements(source), paths);
1870
2038
  let result = stripped;
1871
- for (const path29 of paths) {
1872
- const next = updatedValueAtPath(updated, path29);
2039
+ for (const path30 of paths) {
2040
+ const next = updatedValueAtPath(updated, path30);
1873
2041
  if (!next.exists) continue;
1874
- result = insertUpdatedValue(result, path29, next.value);
2042
+ result = insertUpdatedValue(result, path30, next.value);
1875
2043
  }
1876
2044
  let reparsed;
1877
2045
  try {
@@ -1890,7 +2058,7 @@ function updateTomlPaths(source, updated, updates) {
1890
2058
  }
1891
2059
 
1892
2060
  // ../../packages/agent-targets/src/install/hooks/shared.ts
1893
- import path6 from "path";
2061
+ import path7 from "path";
1894
2062
  function asRecord3(value) {
1895
2063
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1896
2064
  }
@@ -1950,7 +2118,7 @@ function changedMutation(snapshot, after) {
1950
2118
  return before === after ? null : { path: snapshot.path, before, after };
1951
2119
  }
1952
2120
  function fmlHookCommand(pluginRoot, target2) {
1953
- const handlerPath = path6.join(pluginRoot, "bin", "hook-handler");
2121
+ const handlerPath = path7.join(pluginRoot, "bin", "hook-handler");
1954
2122
  const encodedPath = Buffer.from(handlerPath, "utf8").toString("base64url");
1955
2123
  return `node -e "${FML_HOOK_LOADER}" ${encodedPath} ${target2}`;
1956
2124
  }
@@ -2063,11 +2231,11 @@ function uniquePaths(paths) {
2063
2231
  function codexPaths(context) {
2064
2232
  const configuredHome = context.env.CODEX_HOME;
2065
2233
  const trimmedHome = typeof configuredHome === "string" ? configuredHome.trim() : "";
2066
- const dir = path7.resolve(
2067
- trimmedHome.length > 0 ? trimmedHome : path7.join(context.homeDir, ".codex")
2234
+ const dir = path8.resolve(
2235
+ trimmedHome.length > 0 ? trimmedHome : path8.join(context.homeDir, ".codex")
2068
2236
  );
2069
- const configPath4 = path7.join(dir, "config.toml");
2070
- const hooksPath = path7.join(dir, "hooks.json");
2237
+ const configPath4 = path8.join(dir, "config.toml");
2238
+ const hooksPath = path8.join(dir, "hooks.json");
2071
2239
  return {
2072
2240
  configPath: configPath4,
2073
2241
  hooksPath,
@@ -2499,7 +2667,7 @@ function codexConfigUpdatePaths(original, updated) {
2499
2667
  }
2500
2668
  }
2501
2669
  }
2502
- return paths.map((path29) => ({ path: path29 }));
2670
+ return paths.map((path30) => ({ path: path30 }));
2503
2671
  }
2504
2672
  function updateCodexConfigToml(source, original, updated) {
2505
2673
  return updateTomlPaths(
@@ -2949,7 +3117,7 @@ var CODEX_HOOK_INSTALLER = Object.freeze({
2949
3117
 
2950
3118
  // ../../packages/agent-targets/src/install/hooks/gemini.ts
2951
3119
  import { isDeepStrictEqual as isDeepStrictEqual4 } from "util";
2952
- import path8 from "path";
3120
+ import path9 from "path";
2953
3121
 
2954
3122
  // ../../packages/agent-targets/src/mcp/jsonc.ts
2955
3123
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
@@ -3005,7 +3173,7 @@ function longestCommonArraySubsequence(original, updated) {
3005
3173
  }
3006
3174
  return matches;
3007
3175
  }
3008
- function arrayDiff(original, updated, path29) {
3176
+ function arrayDiff(original, updated, path30) {
3009
3177
  const updates = [];
3010
3178
  const matches = longestCommonArraySubsequence(original, updated);
3011
3179
  let originalStart = 0;
@@ -3023,19 +3191,19 @@ function arrayDiff(original, updated, path29) {
3023
3191
  ...jsoncUpdatesForObjectDiff(
3024
3192
  original[originalStart + offset],
3025
3193
  updated[updatedStart + offset],
3026
- [...path29, currentIndex + offset]
3194
+ [...path30, currentIndex + offset]
3027
3195
  )
3028
3196
  );
3029
3197
  }
3030
3198
  for (let offset = pairedCount; offset < originalCount; offset += 1) {
3031
3199
  updates.push({
3032
- path: [...path29, currentIndex + pairedCount],
3200
+ path: [...path30, currentIndex + pairedCount],
3033
3201
  value: void 0
3034
3202
  });
3035
3203
  }
3036
3204
  for (let offset = pairedCount; offset < updatedCount; offset += 1) {
3037
3205
  updates.push({
3038
- path: [...path29, currentIndex + offset],
3206
+ path: [...path30, currentIndex + offset],
3039
3207
  value: updated[updatedStart + offset],
3040
3208
  isArrayInsertion: true
3041
3209
  });
@@ -3048,21 +3216,21 @@ function arrayDiff(original, updated, path29) {
3048
3216
  }
3049
3217
  return updates;
3050
3218
  }
3051
- function jsoncUpdatesForObjectDiff(original, updated, path29 = []) {
3219
+ function jsoncUpdatesForObjectDiff(original, updated, path30 = []) {
3052
3220
  if (isDeepStrictEqual3(original, updated)) return [];
3053
3221
  if (Array.isArray(original) && Array.isArray(updated)) {
3054
- return arrayDiff(original, updated, path29);
3222
+ return arrayDiff(original, updated, path30);
3055
3223
  }
3056
3224
  const originalObject = asObject(original);
3057
3225
  const updatedObject = asObject(updated);
3058
- if (!originalObject || !updatedObject) return [{ path: path29, value: updated }];
3226
+ if (!originalObject || !updatedObject) return [{ path: path30, value: updated }];
3059
3227
  const keys = /* @__PURE__ */ new Set([
3060
3228
  ...Object.keys(originalObject),
3061
3229
  ...Object.keys(updatedObject)
3062
3230
  ]);
3063
3231
  return [...keys].flatMap(
3064
3232
  (key) => jsoncUpdatesForObjectDiff(originalObject[key], updatedObject[key], [
3065
- ...path29,
3233
+ ...path30,
3066
3234
  key
3067
3235
  ])
3068
3236
  );
@@ -3110,7 +3278,7 @@ var TARGET_ID3 = TARGET3.id;
3110
3278
  var DISPLAY_NAME3 = TARGET3.displayName;
3111
3279
  var MANAGED_HOOK_NAME = "fml-capture";
3112
3280
  function settingsPath(context) {
3113
- return path8.join(geminiConfigDirectory(context), "settings.json");
3281
+ return path9.join(geminiConfigDirectory(context), "settings.json");
3114
3282
  }
3115
3283
  function readGeminiSettings(snapshot) {
3116
3284
  const failure = snapshotError(snapshot);
@@ -3343,15 +3511,15 @@ var GEMINI_HOOK_INSTALLER = Object.freeze({
3343
3511
  });
3344
3512
 
3345
3513
  // ../../packages/agent-targets/src/install/hooks/grok.ts
3346
- import path9 from "path";
3514
+ import path10 from "path";
3347
3515
  var TARGET4 = agentTarget("grok");
3348
3516
  var TARGET_ID4 = TARGET4.id;
3349
3517
  var DISPLAY_NAME4 = TARGET4.displayName;
3350
3518
  var HOOK_TIMEOUT_SECONDS2 = 10;
3351
3519
  function grokHooksPath(context) {
3352
3520
  const configuredHome = context.env.GROK_HOME?.trim();
3353
- const grokHome = configuredHome ? path9.resolve(configuredHome) : path9.join(context.homeDir, ".grok");
3354
- return path9.join(grokHome, "hooks", "fml.json");
3521
+ const grokHome = configuredHome ? path10.resolve(configuredHome) : path10.join(context.homeDir, ".grok");
3522
+ return path10.join(grokHome, "hooks", "fml.json");
3355
3523
  }
3356
3524
  function planInstall3(context) {
3357
3525
  const configPath4 = grokHooksPath(context);
@@ -3470,14 +3638,14 @@ var HOOK_REGISTRATION_CAPABILITIES = Object.freeze([
3470
3638
  ]);
3471
3639
 
3472
3640
  // src/commands/hook-config.ts
3473
- var DEFAULT_CODEX_HOOK_OWNERSHIP_STATE_PATH = path10.join(
3641
+ var DEFAULT_CODEX_HOOK_OWNERSHIP_STATE_PATH = path11.join(
3474
3642
  FML_DATA_DIR,
3475
3643
  "codex-hook-install.json"
3476
3644
  );
3477
3645
  function readHookFile(filePath) {
3478
3646
  let stat;
3479
3647
  try {
3480
- stat = fs5.lstatSync(filePath);
3648
+ stat = fs6.lstatSync(filePath);
3481
3649
  } catch (error) {
3482
3650
  if (error.code === "ENOENT") {
3483
3651
  return { path: filePath, kind: "missing" };
@@ -3506,7 +3674,7 @@ function readHookFile(filePath) {
3506
3674
  return {
3507
3675
  path: filePath,
3508
3676
  kind: "file",
3509
- contents: fs5.readFileSync(filePath, "utf8")
3677
+ contents: fs6.readFileSync(filePath, "utf8")
3510
3678
  };
3511
3679
  } catch (error) {
3512
3680
  return {
@@ -3518,17 +3686,17 @@ function readHookFile(filePath) {
3518
3686
  }
3519
3687
  function canonicalizePath(filePath) {
3520
3688
  try {
3521
- return fs5.realpathSync(filePath);
3689
+ return fs6.realpathSync(filePath);
3522
3690
  } catch {
3523
3691
  const missingSegments = [];
3524
3692
  let ancestor = filePath;
3525
3693
  while (true) {
3526
- const parent = path10.dirname(ancestor);
3694
+ const parent = path11.dirname(ancestor);
3527
3695
  if (parent === ancestor) return filePath;
3528
- missingSegments.unshift(path10.basename(ancestor));
3696
+ missingSegments.unshift(path11.basename(ancestor));
3529
3697
  ancestor = parent;
3530
3698
  try {
3531
- return path10.join(fs5.realpathSync(ancestor), ...missingSegments);
3699
+ return path11.join(fs6.realpathSync(ancestor), ...missingSegments);
3532
3700
  } catch {
3533
3701
  }
3534
3702
  }
@@ -3547,7 +3715,7 @@ function assertMutationPrecondition(mutation) {
3547
3715
  }
3548
3716
  function applyMutation(mutation) {
3549
3717
  if (mutation.after === null) {
3550
- fs5.rmSync(mutation.path);
3718
+ fs6.rmSync(mutation.path);
3551
3719
  } else {
3552
3720
  writeTextFileAtomic(mutation.path, mutation.after);
3553
3721
  }
@@ -3560,7 +3728,7 @@ function rollbackMutation(mutation) {
3560
3728
  };
3561
3729
  assertMutationPrecondition(inverse);
3562
3730
  if (inverse.after === null) {
3563
- fs5.rmSync(inverse.path);
3731
+ fs6.rmSync(inverse.path);
3564
3732
  } else {
3565
3733
  writeTextFileAtomic(inverse.path, inverse.after);
3566
3734
  }
@@ -3628,7 +3796,7 @@ function applyPlan(plan, warn, onFailure) {
3628
3796
  };
3629
3797
  }
3630
3798
  function plannerContext(packageRoot, options) {
3631
- const ownershipStatePath = options.ownershipStatePath ?? (options.homeDir ? path10.join(options.homeDir, ".fml", "codex-hook-install.json") : DEFAULT_CODEX_HOOK_OWNERSHIP_STATE_PATH);
3799
+ const ownershipStatePath = options.ownershipStatePath ?? (options.homeDir ? path11.join(options.homeDir, ".fml", "codex-hook-install.json") : DEFAULT_CODEX_HOOK_OWNERSHIP_STATE_PATH);
3632
3800
  return {
3633
3801
  packageRoot,
3634
3802
  homeDir: options.homeDir ?? os2.homedir(),
@@ -3659,10 +3827,10 @@ function removeFmlHooks(options = {}) {
3659
3827
  }
3660
3828
 
3661
3829
  // src/commands/marketplace-link.ts
3662
- import fs6 from "fs";
3830
+ import fs7 from "fs";
3663
3831
  function existingPathKind(linkPath) {
3664
3832
  try {
3665
- return fs6.lstatSync(linkPath).isSymbolicLink() ? "symlink" : "other";
3833
+ return fs7.lstatSync(linkPath).isSymbolicLink() ? "symlink" : "other";
3666
3834
  } catch (error) {
3667
3835
  if (error.code === "ENOENT") return "missing";
3668
3836
  throw error;
@@ -3671,15 +3839,15 @@ function existingPathKind(linkPath) {
3671
3839
  function replaceMarketplaceLink(linkPath, targetPath, type) {
3672
3840
  const existing = existingPathKind(linkPath);
3673
3841
  if (existing === "other") return "preserved";
3674
- if (existing === "symlink") fs6.unlinkSync(linkPath);
3675
- fs6.symlinkSync(targetPath, linkPath, type);
3842
+ if (existing === "symlink") fs7.unlinkSync(linkPath);
3843
+ fs7.symlinkSync(targetPath, linkPath, type);
3676
3844
  return existing === "symlink" ? "replaced" : "created";
3677
3845
  }
3678
3846
  function removeMarketplaceLink(linkPath) {
3679
3847
  const existing = existingPathKind(linkPath);
3680
3848
  if (existing === "missing") return "missing";
3681
3849
  if (existing === "other") return "preserved";
3682
- fs6.unlinkSync(linkPath);
3850
+ fs7.unlinkSync(linkPath);
3683
3851
  return "removed";
3684
3852
  }
3685
3853
 
@@ -3774,10 +3942,10 @@ function configRecord(value) {
3774
3942
  }
3775
3943
 
3776
3944
  // ../../packages/agent-targets/src/mcp/targets/claude-desktop.ts
3777
- import path12 from "path";
3945
+ import path13 from "path";
3778
3946
 
3779
3947
  // ../../packages/agent-targets/src/mcp/capability.ts
3780
- import path11 from "path";
3948
+ import path12 from "path";
3781
3949
  import { parse as parse4 } from "smol-toml";
3782
3950
 
3783
3951
  // ../../packages/agent-targets/src/mcp/toml.ts
@@ -3883,7 +4051,7 @@ function changedConfigPlan(target2, current, before, data) {
3883
4051
  function generatedServer(target2, context) {
3884
4052
  return fmlMcpServerConfig(
3885
4053
  target2.command,
3886
- path11.join(context.packageRoot, "bin", "mcp-server")
4054
+ path12.join(context.packageRoot, "bin", "mcp-server")
3887
4055
  );
3888
4056
  }
3889
4057
  function planInstall4(target2, context) {
@@ -3963,7 +4131,7 @@ var CLAUDE_DESKTOP_DIRECT_MCP = createDirectMcpCapability({
3963
4131
  configKey: "mcpServers",
3964
4132
  configPath: ({ env, homeDir, platform }) => {
3965
4133
  if (platform === "darwin") {
3966
- return path12.join(
4134
+ return path13.join(
3967
4135
  homeDir,
3968
4136
  "Library",
3969
4137
  "Application Support",
@@ -3972,14 +4140,14 @@ var CLAUDE_DESKTOP_DIRECT_MCP = createDirectMcpCapability({
3972
4140
  );
3973
4141
  }
3974
4142
  if (platform === "win32") {
3975
- return path12.join(
3976
- env.APPDATA ?? path12.join(homeDir, "AppData", "Roaming"),
4143
+ return path13.join(
4144
+ env.APPDATA ?? path13.join(homeDir, "AppData", "Roaming"),
3977
4145
  "Claude",
3978
4146
  "claude_desktop_config.json"
3979
4147
  );
3980
4148
  }
3981
- return path12.join(
3982
- env.XDG_CONFIG_HOME ?? path12.join(homeDir, ".config"),
4149
+ return path13.join(
4150
+ env.XDG_CONFIG_HOME ?? path13.join(homeDir, ".config"),
3983
4151
  "Claude",
3984
4152
  "claude_desktop_config.json"
3985
4153
  );
@@ -3988,7 +4156,7 @@ var CLAUDE_DESKTOP_DIRECT_MCP = createDirectMcpCapability({
3988
4156
  });
3989
4157
 
3990
4158
  // ../../packages/agent-targets/src/mcp/targets/codex.ts
3991
- import path13 from "path";
4159
+ import path14 from "path";
3992
4160
  var TARGET6 = agentTarget("codex");
3993
4161
  var CODEX_DIRECT_MCP = createDirectMcpCapability({
3994
4162
  targetId: TARGET6.id,
@@ -3997,26 +4165,26 @@ var CODEX_DIRECT_MCP = createDirectMcpCapability({
3997
4165
  configKey: "mcp_servers",
3998
4166
  configPath: ({ env, homeDir }) => {
3999
4167
  const configuredHome = env.CODEX_HOME;
4000
- const codexHome = typeof configuredHome === "string" && configuredHome.length > 0 ? path13.resolve(configuredHome) : path13.join(homeDir, ".codex");
4001
- return path13.join(codexHome, "config.toml");
4168
+ const codexHome = typeof configuredHome === "string" && configuredHome.length > 0 ? path14.resolve(configuredHome) : path14.join(homeDir, ".codex");
4169
+ return path14.join(codexHome, "config.toml");
4002
4170
  },
4003
4171
  command: () => "node"
4004
4172
  });
4005
4173
 
4006
4174
  // ../../packages/agent-targets/src/mcp/targets/gemini.ts
4007
- import path14 from "path";
4175
+ import path15 from "path";
4008
4176
  var TARGET7 = agentTarget("gemini");
4009
4177
  var GEMINI_DIRECT_MCP = createDirectMcpCapability({
4010
4178
  targetId: TARGET7.id,
4011
4179
  displayName: TARGET7.displayName,
4012
4180
  configFormat: "json",
4013
4181
  configKey: "mcpServers",
4014
- configPath: (context) => path14.join(geminiConfigDirectory(context), "settings.json"),
4182
+ configPath: (context) => path15.join(geminiConfigDirectory(context), "settings.json"),
4015
4183
  command: () => "node"
4016
4184
  });
4017
4185
 
4018
4186
  // ../../packages/agent-targets/src/mcp/targets/grok.ts
4019
- import path15 from "path";
4187
+ import path16 from "path";
4020
4188
  var TARGET8 = agentTarget("grok");
4021
4189
  var GROK_DIRECT_MCP = createDirectMcpCapability({
4022
4190
  targetId: TARGET8.id,
@@ -4025,8 +4193,8 @@ var GROK_DIRECT_MCP = createDirectMcpCapability({
4025
4193
  configKey: "mcp_servers",
4026
4194
  configPath: ({ env, homeDir }) => {
4027
4195
  const configuredHome = env.GROK_HOME;
4028
- const grokHome = typeof configuredHome === "string" && configuredHome.length > 0 ? path15.resolve(configuredHome) : path15.join(homeDir, ".grok");
4029
- return path15.join(grokHome, "config.toml");
4196
+ const grokHome = typeof configuredHome === "string" && configuredHome.length > 0 ? path16.resolve(configuredHome) : path16.join(homeDir, ".grok");
4197
+ return path16.join(grokHome, "config.toml");
4030
4198
  },
4031
4199
  command: () => "node"
4032
4200
  });
@@ -4040,7 +4208,7 @@ var DIRECT_MCP_CAPABILITIES = Object.freeze([
4040
4208
  ]);
4041
4209
 
4042
4210
  // src/commands/mcp-config.ts
4043
- import fs7 from "fs";
4211
+ import fs8 from "fs";
4044
4212
  import os3 from "os";
4045
4213
  function resolutionContext(options) {
4046
4214
  return {
@@ -4053,7 +4221,7 @@ function resolutionContext(options) {
4053
4221
  function readMcpFile(filePath) {
4054
4222
  let stat;
4055
4223
  try {
4056
- stat = fs7.lstatSync(filePath);
4224
+ stat = fs8.lstatSync(filePath);
4057
4225
  } catch (error) {
4058
4226
  if (error.code === "ENOENT") {
4059
4227
  return { path: filePath, kind: "missing" };
@@ -4082,7 +4250,7 @@ function readMcpFile(filePath) {
4082
4250
  return {
4083
4251
  path: filePath,
4084
4252
  kind: "file",
4085
- contents: fs7.readFileSync(filePath, "utf8")
4253
+ contents: fs8.readFileSync(filePath, "utf8")
4086
4254
  };
4087
4255
  } catch (error) {
4088
4256
  return {
@@ -4118,21 +4286,21 @@ function preflightMutation(mutation) {
4118
4286
  }
4119
4287
  function writeMutation(mutation) {
4120
4288
  if (mutation.after === null) {
4121
- fs7.rmSync(mutation.path, { force: true });
4289
+ fs8.rmSync(mutation.path, { force: true });
4122
4290
  return;
4123
4291
  }
4124
4292
  writeTextFileAtomic(mutation.path, mutation.after);
4125
4293
  }
4126
4294
  function rollbackMutation2(mutation) {
4127
4295
  if (mutation.before === null) {
4128
- fs7.rmSync(mutation.path, { force: true });
4296
+ fs8.rmSync(mutation.path, { force: true });
4129
4297
  return;
4130
4298
  }
4131
4299
  writeTextFileAtomic(mutation.path, mutation.before);
4132
4300
  }
4133
4301
  function warnSkippedPlan(plan, error, warn) {
4134
4302
  warn(
4135
- `warn: skipped ${plan.displayName} MCP config at ${plan.configPath}: ${error}`
4303
+ `Warning: skipped ${plan.displayName} MCP config at ${plan.configPath}: ${error}`
4136
4304
  );
4137
4305
  }
4138
4306
  function applyPlan2(plan, warn) {
@@ -4176,14 +4344,17 @@ function reportPlan(plan, warn, log) {
4176
4344
  for (const note of plan.notes) log(note);
4177
4345
  }
4178
4346
  function configureFmlDirectMcp(pluginRoot, options = {}) {
4179
- const warn = options.warn ?? console.warn;
4347
+ const emitWarning = (message) => {
4348
+ (options.warn ?? console.warn)(message);
4349
+ options.onFailure?.(message);
4350
+ };
4180
4351
  const log = options.log ?? console.log;
4181
4352
  const context = plannerContext2(pluginRoot, options);
4182
4353
  const changes = [];
4183
4354
  for (const capability of DIRECT_MCP_CAPABILITIES) {
4184
4355
  const plan = capability.planInstall(context);
4185
- reportPlan(plan, warn, log);
4186
- if (!applyPlan2(plan, warn)) continue;
4356
+ reportPlan(plan, emitWarning, log);
4357
+ if (!applyPlan2(plan, emitWarning)) continue;
4187
4358
  changes.push({
4188
4359
  targetId: plan.targetId,
4189
4360
  displayName: plan.displayName,
@@ -4212,13 +4383,13 @@ function removeFmlDirectMcp(target2 = "all", options = {}) {
4212
4383
  }
4213
4384
 
4214
4385
  // src/commands/agent-skills.ts
4215
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
4216
- import fs8 from "fs";
4386
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
4387
+ import fs9 from "fs";
4217
4388
  import os4 from "os";
4218
- import path20 from "path";
4389
+ import path21 from "path";
4219
4390
 
4220
4391
  // ../../packages/agent-targets/src/install/surfaces/capability.ts
4221
- import path16 from "path";
4392
+ import path17 from "path";
4222
4393
  var SOURCE_PATH_SEGMENTS = ["agent-assets", "fml", "SKILL.md"];
4223
4394
  var DESTINATION_PATH_SEGMENTS = ["skills", "fml", "SKILL.md"];
4224
4395
  function createAgentSkillCapability(definition) {
@@ -4231,8 +4402,8 @@ function createAgentSkillCapability(definition) {
4231
4402
  resolve: (context) => Object.freeze({
4232
4403
  targetId: definition.targetId,
4233
4404
  displayName: definition.displayName,
4234
- sourcePath: path16.join(context.packageRoot, ...SOURCE_PATH_SEGMENTS),
4235
- destinationPath: path16.join(
4405
+ sourcePath: path17.join(context.packageRoot, ...SOURCE_PATH_SEGMENTS),
4406
+ destinationPath: path17.join(
4236
4407
  definition.destinationRoot(context),
4237
4408
  ...DESTINATION_PATH_SEGMENTS
4238
4409
  ),
@@ -4255,7 +4426,7 @@ var CLAUDE_AGENT_SKILL = createAgentSkillCapability({
4255
4426
  });
4256
4427
 
4257
4428
  // ../../packages/agent-targets/src/install/surfaces/targets/codex.ts
4258
- import path17 from "path";
4429
+ import path18 from "path";
4259
4430
  var TARGET10 = agentTarget("codex");
4260
4431
  var CODEX_AGENT_SKILL = createAgentSkillCapability({
4261
4432
  targetId: TARGET10.id,
@@ -4263,11 +4434,11 @@ var CODEX_AGENT_SKILL = createAgentSkillCapability({
4263
4434
  invocation: "$fml",
4264
4435
  deliveryMode: "instruction-read",
4265
4436
  argumentMode: "user-input",
4266
- destinationRoot: ({ homeDir }) => path17.join(homeDir, ".agents")
4437
+ destinationRoot: ({ homeDir }) => path18.join(homeDir, ".agents")
4267
4438
  });
4268
4439
 
4269
4440
  // ../../packages/agent-targets/src/install/surfaces/targets/gemini.ts
4270
- import path18 from "path";
4441
+ import path19 from "path";
4271
4442
  var TARGET11 = agentTarget("gemini");
4272
4443
  var GEMINI_AGENT_SKILL = createAgentSkillCapability({
4273
4444
  targetId: TARGET11.id,
@@ -4277,13 +4448,13 @@ var GEMINI_AGENT_SKILL = createAgentSkillCapability({
4277
4448
  argumentMode: "post-submit-prompt",
4278
4449
  destinationRoot: ({ env, homeDir }) => {
4279
4450
  const configuredHome = env.GEMINI_CLI_HOME?.trim();
4280
- const homeRoot = configuredHome ? path18.resolve(configuredHome) : homeDir;
4281
- return path18.join(homeRoot, ".agents");
4451
+ const homeRoot = configuredHome ? path19.resolve(configuredHome) : homeDir;
4452
+ return path19.join(homeRoot, ".agents");
4282
4453
  }
4283
4454
  });
4284
4455
 
4285
4456
  // ../../packages/agent-targets/src/install/surfaces/targets/grok.ts
4286
- import path19 from "path";
4457
+ import path20 from "path";
4287
4458
  var TARGET12 = agentTarget("grok");
4288
4459
  var GROK_AGENT_SKILL = createAgentSkillCapability({
4289
4460
  targetId: TARGET12.id,
@@ -4291,7 +4462,7 @@ var GROK_AGENT_SKILL = createAgentSkillCapability({
4291
4462
  invocation: "/fml",
4292
4463
  deliveryMode: "prompt-expansion",
4293
4464
  argumentMode: "template-or-append",
4294
- destinationRoot: ({ homeDir }) => path19.join(homeDir, ".agents")
4465
+ destinationRoot: ({ homeDir }) => path20.join(homeDir, ".agents")
4295
4466
  });
4296
4467
 
4297
4468
  // ../../packages/agent-targets/src/install/surfaces/registry.ts
@@ -4305,7 +4476,7 @@ var AGENT_SKILL_CAPABILITIES = Object.freeze([
4305
4476
  // src/commands/agent-skills.ts
4306
4477
  var FML_AGENT_SKILL_MARKER = "<!-- fml-managed-agent-surface:v1 -->";
4307
4478
  var AGENT_SKILL_STATE_SCHEMA_VERSION = 1;
4308
- var DEFAULT_STATE_PATH = path20.join(FML_DATA_DIR, "agent-skills.json");
4479
+ var DEFAULT_STATE_PATH = path21.join(FML_DATA_DIR, "agent-skills.json");
4309
4480
  function emptyState() {
4310
4481
  return Object.freeze({
4311
4482
  schemaVersion: AGENT_SKILL_STATE_SCHEMA_VERSION,
@@ -4318,7 +4489,7 @@ function sha256(contents) {
4318
4489
  function readSkillFile(filePath) {
4319
4490
  let stat;
4320
4491
  try {
4321
- stat = fs8.lstatSync(filePath);
4492
+ stat = fs9.lstatSync(filePath);
4322
4493
  } catch (error) {
4323
4494
  if (error.code === "ENOENT") {
4324
4495
  return { kind: "missing" };
@@ -4341,7 +4512,7 @@ function readSkillFile(filePath) {
4341
4512
  };
4342
4513
  }
4343
4514
  try {
4344
- const contents = fs8.readFileSync(filePath, "utf8");
4515
+ const contents = fs9.readFileSync(filePath, "utf8");
4345
4516
  return { kind: "file", contents, sha256: sha256(contents) };
4346
4517
  } catch (error) {
4347
4518
  return {
@@ -4410,7 +4581,7 @@ function parseState(source, statePath) {
4410
4581
  }
4411
4582
  entries.push(
4412
4583
  Object.freeze({
4413
- path: path20.resolve(entry.path),
4584
+ path: path21.resolve(entry.path),
4414
4585
  sha256: entry.sha256,
4415
4586
  targetIds: Object.freeze([...entry.targetIds])
4416
4587
  })
@@ -4436,7 +4607,7 @@ function loadState(statePath) {
4436
4607
  }
4437
4608
  function serializeState(entries) {
4438
4609
  const sorted = [...entries].map((entry) => ({
4439
- path: path20.resolve(entry.path),
4610
+ path: path21.resolve(entry.path),
4440
4611
  sha256: entry.sha256,
4441
4612
  targetIds: [...entry.targetIds].sort()
4442
4613
  })).sort((left, right) => left.path.localeCompare(right.path));
@@ -4475,7 +4646,7 @@ function resolvedGroups(pluginRoot, options) {
4475
4646
  const groups = /* @__PURE__ */ new Map();
4476
4647
  for (const capability of AGENT_SKILL_CAPABILITIES) {
4477
4648
  const resolved = capability.resolve(context);
4478
- const destinationPath = path20.resolve(resolved.destinationPath);
4649
+ const destinationPath = path21.resolve(resolved.destinationPath);
4479
4650
  const key = process.platform === "win32" ? destinationPath.toLowerCase() : destinationPath;
4480
4651
  const current = groups.get(key) ?? [];
4481
4652
  current.push({ ...resolved, destinationPath });
@@ -4512,10 +4683,10 @@ function resultFor(group, status, reason) {
4512
4683
  });
4513
4684
  }
4514
4685
  function destinationDirectoryIssue(destinationPath, requireEmpty) {
4515
- const skillDir = path20.dirname(destinationPath);
4686
+ const skillDir = path21.dirname(destinationPath);
4516
4687
  let stat;
4517
4688
  try {
4518
- stat = fs8.lstatSync(skillDir);
4689
+ stat = fs9.lstatSync(skillDir);
4519
4690
  } catch (error) {
4520
4691
  if (error.code === "ENOENT") return null;
4521
4692
  return error instanceof Error ? error.message : String(error);
@@ -4523,7 +4694,7 @@ function destinationDirectoryIssue(destinationPath, requireEmpty) {
4523
4694
  if (stat.isSymbolicLink()) return "skill directory is a symbolic link";
4524
4695
  if (!stat.isDirectory()) return "skill directory is not a directory";
4525
4696
  try {
4526
- if (!requireEmpty || fs8.readdirSync(skillDir).length === 0) return null;
4697
+ if (!requireEmpty || fs9.readdirSync(skillDir).length === 0) return null;
4527
4698
  return "skill directory contains unmanaged files";
4528
4699
  } catch (error) {
4529
4700
  return error instanceof Error ? error.message : String(error);
@@ -4540,9 +4711,9 @@ function rollbackSkillWrite(destinationPath, before) {
4540
4711
  writeTextFileAtomic(destinationPath, before.contents);
4541
4712
  return;
4542
4713
  }
4543
- fs8.rmSync(destinationPath, { force: true });
4714
+ fs9.rmSync(destinationPath, { force: true });
4544
4715
  try {
4545
- fs8.rmdirSync(path20.dirname(destinationPath));
4716
+ fs9.rmdirSync(path21.dirname(destinationPath));
4546
4717
  } catch {
4547
4718
  }
4548
4719
  }
@@ -4585,12 +4756,12 @@ function installManagedAgentSkills(pluginRoot, options = {}) {
4585
4756
  const loaded = loadState(statePath);
4586
4757
  let stateSource = loaded.source;
4587
4758
  const entries = new Map(
4588
- loaded.state.entries.map((entry) => [path20.resolve(entry.path), entry])
4759
+ loaded.state.entries.map((entry) => [path21.resolve(entry.path), entry])
4589
4760
  );
4590
4761
  const results = [];
4591
4762
  for (const group of resolvedGroups(pluginRoot, options)) {
4592
4763
  const source = sourceSkill(group.sourcePath);
4593
- const destinationPath = path20.resolve(group.destinationPath);
4764
+ const destinationPath = path21.resolve(group.destinationPath);
4594
4765
  const current = readSkillFile(destinationPath);
4595
4766
  const previous = entries.get(destinationPath);
4596
4767
  const nextEntry = stateEntryFor(
@@ -4675,11 +4846,11 @@ function removeSkillFile(args) {
4675
4846
  `Agent skill changed before ${args.destinationPath} could be removed.`
4676
4847
  );
4677
4848
  }
4678
- const backupPath = path20.join(
4679
- path20.dirname(args.destinationPath),
4680
- `.${path20.basename(args.destinationPath)}.${process.pid}.${randomUUID2()}.bak`
4849
+ const backupPath = path21.join(
4850
+ path21.dirname(args.destinationPath),
4851
+ `.${path21.basename(args.destinationPath)}.${process.pid}.${randomUUID3()}.bak`
4681
4852
  );
4682
- fs8.renameSync(args.destinationPath, backupPath);
4853
+ fs9.renameSync(args.destinationPath, backupPath);
4683
4854
  let stateSource;
4684
4855
  try {
4685
4856
  stateSource = writeState(
@@ -4689,7 +4860,7 @@ function removeSkillFile(args) {
4689
4860
  );
4690
4861
  } catch (error) {
4691
4862
  try {
4692
- fs8.renameSync(backupPath, args.destinationPath);
4863
+ fs9.renameSync(backupPath, args.destinationPath);
4693
4864
  } catch (rollbackError) {
4694
4865
  const message = error instanceof Error ? error.message : String(error);
4695
4866
  const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
@@ -4701,12 +4872,12 @@ function removeSkillFile(args) {
4701
4872
  }
4702
4873
  let warning;
4703
4874
  try {
4704
- fs8.rmSync(backupPath, { force: true });
4875
+ fs9.rmSync(backupPath, { force: true });
4705
4876
  } catch (error) {
4706
4877
  warning = `agent skill removal was committed, but backup cleanup failed at ${backupPath}: ${error instanceof Error ? error.message : String(error)}`;
4707
4878
  }
4708
4879
  try {
4709
- fs8.rmdirSync(path20.dirname(args.destinationPath));
4880
+ fs9.rmdirSync(path21.dirname(args.destinationPath));
4710
4881
  } catch {
4711
4882
  }
4712
4883
  return { stateSource, ...warning ? { warning } : {} };
@@ -4716,16 +4887,16 @@ function removeManagedAgentSkills(pluginRoot, options = {}) {
4716
4887
  const loaded = loadState(statePath);
4717
4888
  let stateSource = loaded.source;
4718
4889
  const entries = new Map(
4719
- loaded.state.entries.map((entry) => [path20.resolve(entry.path), entry])
4890
+ loaded.state.entries.map((entry) => [path21.resolve(entry.path), entry])
4720
4891
  );
4721
4892
  const candidates = /* @__PURE__ */ new Map();
4722
4893
  const currentGroups = resolvedGroups(pluginRoot, options);
4723
- const packagedSourcePath = currentGroups[0]?.sourcePath ?? path20.join(pluginRoot, "agent-assets", "fml", "SKILL.md");
4894
+ const packagedSourcePath = currentGroups[0]?.sourcePath ?? path21.join(pluginRoot, "agent-assets", "fml", "SKILL.md");
4724
4895
  for (const group of currentGroups) {
4725
- candidates.set(path20.resolve(group.destinationPath), group);
4896
+ candidates.set(path21.resolve(group.destinationPath), group);
4726
4897
  }
4727
4898
  for (const entry of entries.values()) {
4728
- const destinationPath = path20.resolve(entry.path);
4899
+ const destinationPath = path21.resolve(entry.path);
4729
4900
  if (candidates.has(destinationPath)) continue;
4730
4901
  const targets = entry.targetIds.filter(isAgentSkillTargetId).map(targetForId);
4731
4902
  if (targets.length === 0) continue;
@@ -4740,7 +4911,7 @@ function removeManagedAgentSkills(pluginRoot, options = {}) {
4740
4911
  for (const group of [...candidates.values()].sort(
4741
4912
  (left, right) => left.destinationPath.localeCompare(right.destinationPath)
4742
4913
  )) {
4743
- const destinationPath = path20.resolve(group.destinationPath);
4914
+ const destinationPath = path21.resolve(group.destinationPath);
4744
4915
  const current = readSkillFile(destinationPath);
4745
4916
  const previous = entries.get(destinationPath);
4746
4917
  if (current.kind === "missing") {
@@ -4798,13 +4969,13 @@ function removeManagedAgentSkills(pluginRoot, options = {}) {
4798
4969
  }
4799
4970
 
4800
4971
  // src/commands/install.ts
4801
- function resolvePluginRoot(startDir = path21.dirname(fileURLToPath(import.meta.url))) {
4972
+ function resolvePluginRoot(startDir = path22.dirname(fileURLToPath(import.meta.url))) {
4802
4973
  let directory = startDir;
4803
4974
  while (true) {
4804
- if (fs9.existsSync(path21.join(directory, "package.json")) && fs9.existsSync(path21.join(directory, "bin"))) {
4975
+ if (fs10.existsSync(path22.join(directory, "package.json")) && fs10.existsSync(path22.join(directory, "bin"))) {
4805
4976
  return directory;
4806
4977
  }
4807
- const parent = path21.dirname(directory);
4978
+ const parent = path22.dirname(directory);
4808
4979
  if (parent === directory) {
4809
4980
  throw new Error(
4810
4981
  `Could not locate the FML package root: no ancestor of ${startDir} contains both package.json and bin/. Reinstall @fml-inc/fml and retry.`
@@ -4819,7 +4990,7 @@ function getPluginRoot() {
4819
4990
  function readJsonFileForUpdate(filePath) {
4820
4991
  let source;
4821
4992
  try {
4822
- source = fs9.readFileSync(filePath, "utf8");
4993
+ source = fs10.readFileSync(filePath, "utf8");
4823
4994
  } catch (error) {
4824
4995
  if (error.code === "ENOENT") {
4825
4996
  return { ok: true, exists: false, data: {} };
@@ -4867,14 +5038,13 @@ async function handleInstall(opts = {}) {
4867
5038
  homeDir: os5.homedir(),
4868
5039
  packageRoot: pluginRoot
4869
5040
  });
4870
- console.log("Installing fml...\n");
4871
- console.log("[1/5] Creating FML directories...");
5041
+ console.log("Configuring FML integrations...");
4872
5042
  for (const root of configuredManagedRoots()) {
4873
5043
  const preparation = prepareManagedDirectory(root);
4874
5044
  if (!preparation.managed) {
4875
5045
  const recovery = process.platform === "win32" ? "use a dedicated FML directory in a native location; custom Windows roots require manual cleanup" : "use a dedicated FML directory or follow the inspected custom-root adoption steps in https://github.com/fml-inc/fml/blob/main/docs/plugin-cleanup.md#adopt-an-existing-custom-root";
4876
5046
  console.warn(
4877
- ` warn: ${root.directory} could not be safely marked as FML-owned because it is non-empty, symlinked, or not owned by the current user; ${recovery} before using \`fml uninstall --purge\``
5047
+ `Warning: ${root.directory} could not be safely marked as FML-owned because it is non-empty, symlinked, or not owned by the current user; ${recovery} before using \`fml uninstall --purge\`.`
4878
5048
  );
4879
5049
  }
4880
5050
  }
@@ -4884,12 +5054,9 @@ async function handleInstall(opts = {}) {
4884
5054
  FML_LOG_DIR,
4885
5055
  FML_OBSERVER_DIR
4886
5056
  ])) {
4887
- fs9.mkdirSync(directory, { recursive: true, mode: 448 });
4888
- console.log(` ${directory}`);
5057
+ fs10.mkdirSync(directory, { recursive: true, mode: 448 });
4889
5058
  }
4890
- initializeSyncSettings(path21.join(FML_OBSERVER_DIR, "sync.json"));
4891
- console.log();
4892
- console.log("[2/5] Installing agent-native /fml skills...");
5059
+ initializeSyncSettings(path22.join(FML_OBSERVER_DIR, "sync.json"));
4893
5060
  let skillReady = false;
4894
5061
  try {
4895
5062
  const skillResults = installManagedAgentSkills(pluginRoot);
@@ -4897,52 +5064,71 @@ async function handleInstall(opts = {}) {
4897
5064
  for (const result of skillResults) {
4898
5065
  const targets = result.displayNames.join(", ");
4899
5066
  if (result.status === "preserved") {
4900
- console.log(
4901
- ` warn: preserved ${result.destinationPath} for ${targets}: ${result.reason}`
4902
- );
4903
- } else {
4904
- console.log(
4905
- ` ${targets}: ${result.destinationPath} (${result.status})`
5067
+ console.warn(
5068
+ `Warning: preserved ${result.destinationPath} for ${targets}: ${result.reason}`
4906
5069
  );
4907
5070
  }
4908
5071
  }
5072
+ } catch (error) {
5073
+ const message = error instanceof Error ? error.message : String(error);
5074
+ console.warn(
5075
+ `Warning: agent-native skill setup could not be completed: ${message}`
5076
+ );
5077
+ }
5078
+ let piExtensionReady = false;
5079
+ try {
5080
+ const piPaths = PI_EXTENSION_INSTALLER.resolvePaths({
5081
+ env: process.env,
5082
+ homeDir: os5.homedir(),
5083
+ packageRoot: pluginRoot,
5084
+ platform: process.platform
5085
+ });
5086
+ const piResult = PI_EXTENSION_INSTALLER.install(piPaths);
5087
+ piExtensionReady = piResult.status !== "preserved";
5088
+ if (piResult.status === "preserved") {
5089
+ console.log(
5090
+ ` warn: preserved ${piResult.destinationPath} for Pi: ${piResult.reason}`
5091
+ );
5092
+ }
5093
+ if (piResult.legacyRemoved) {
5094
+ console.log(" Removed the legacy Panopticon Pi extension");
5095
+ } else if (piResult.legacyPreserved) {
5096
+ console.log(
5097
+ ` warn: preserved unrecognized Pi extension ${piPaths.legacyDestinationPath}; inspect and remove it if it belongs to Panopticon`
5098
+ );
5099
+ }
4909
5100
  } catch (error) {
4910
5101
  const message = error instanceof Error ? error.message : String(error);
4911
5102
  console.log(
4912
- ` warn: agent-native skill setup could not be completed: ${message}`
5103
+ ` warn: Pi extension setup could not be completed: ${message}`
4913
5104
  );
4914
5105
  }
4915
- console.log();
4916
- console.log("[3/5] Writing plugin manifest...");
4917
- const packageJson = readJsonFile(path21.join(pluginRoot, "package.json"));
5106
+ const packageJson = readJsonFile(path22.join(pluginRoot, "package.json"));
4918
5107
  const generatedManifest = readJsonFile(paths.pluginManifestPath);
4919
5108
  const version = typeof generatedManifest?.version === "string" ? generatedManifest.version : typeof packageJson?.version === "string" ? packageJson.version : "0.0.0-dev";
4920
5109
  writeJsonFile(
4921
5110
  paths.pluginManifestPath,
4922
5111
  CLAUDE_PLUGIN_INSTALLER.pluginManifest(version)
4923
5112
  );
4924
- console.log(` Version: ${version}
4925
- `);
4926
- console.log("[4/5] Registering the FML plugin...");
4927
5113
  const claudeBin = resolveBin("claude");
4928
5114
  const marketplaceResult = readJsonFileForUpdate(
4929
5115
  paths.marketplaceManifestPath
4930
5116
  );
4931
5117
  let marketplaceReady = false;
4932
5118
  if (!marketplaceResult.ok) {
4933
- console.log(
4934
- ` warn: preserved unreadable marketplace config ${paths.marketplaceManifestPath}: ${marketplaceResult.error}`
5119
+ console.warn(
5120
+ `Warning: preserved unreadable marketplace config ${paths.marketplaceManifestPath}: ${marketplaceResult.error}`
4935
5121
  );
4936
5122
  } else {
4937
- fs9.mkdirSync(paths.marketplaceDir, { recursive: true, mode: 448 });
5123
+ fs10.mkdirSync(paths.marketplaceDir, { recursive: true, mode: 448 });
4938
5124
  const linkUpdate = replaceMarketplaceLink(
4939
5125
  paths.marketplaceLinkPath,
4940
5126
  pluginRoot,
4941
5127
  marketplaceSymlinkType()
4942
5128
  );
4943
5129
  if (linkUpdate === "preserved") {
4944
- console.log(
4945
- ` warn: preserved non-symlink marketplace path ${paths.marketplaceLinkPath}; remove it manually to enable the FML Claude plugin`
5130
+ console.warn(
5131
+ `Warning: preserved non-symlink marketplace path ${paths.marketplaceLinkPath}; remove it manually to enable the FML Claude plugin`
4946
5132
  );
4947
5133
  } else {
4948
5134
  const marketplace = CLAUDE_PLUGIN_INSTALLER.addMarketplaceEntry(
@@ -4955,8 +5141,8 @@ async function handleInstall(opts = {}) {
4955
5141
  const settingsResult = readJsonFileForUpdate(paths.settingsPath);
4956
5142
  let settingsReady = false;
4957
5143
  if (!settingsResult.ok) {
4958
- console.log(
4959
- ` warn: preserved unreadable Claude settings ${paths.settingsPath}: ${settingsResult.error}`
5144
+ console.warn(
5145
+ `Warning: preserved unreadable Claude settings ${paths.settingsPath}: ${settingsResult.error}`
4960
5146
  );
4961
5147
  } else if (marketplaceReady) {
4962
5148
  const settings = CLAUDE_PLUGIN_INSTALLER.enableInSettings(
@@ -4966,21 +5152,23 @@ async function handleInstall(opts = {}) {
4966
5152
  writeJsonFile(paths.settingsPath, settings);
4967
5153
  settingsReady = true;
4968
5154
  }
4969
- for (const target2 of configureFmlDirectMcp(pluginRoot)) {
4970
- console.log(` MCP: ${target2.displayName} -> fml`);
4971
- }
5155
+ let mcpReady = true;
5156
+ let claudePluginReady = true;
5157
+ configureFmlDirectMcp(pluginRoot, {
5158
+ warn: (message) => console.warn(message.replace(/^warn:/u, "Warning:")),
5159
+ onFailure: () => {
5160
+ mcpReady = false;
5161
+ }
5162
+ });
4972
5163
  let hooksReady = true;
4973
- const hookTargets = configureFmlHooks(pluginRoot, {
5164
+ configureFmlHooks(pluginRoot, {
4974
5165
  onFailure: () => {
4975
5166
  hooksReady = false;
4976
5167
  }
4977
5168
  });
4978
- for (const target2 of hookTargets) {
4979
- console.log(` Hooks: ${target2.displayName} -> fml`);
4980
- }
4981
5169
  if (!marketplaceReady || !settingsReady) {
4982
- console.log(
4983
- " warn: skipped Claude plugin registration until its existing config is repaired"
5170
+ console.warn(
5171
+ "Warning: skipped Claude plugin registration until its existing config is repaired"
4984
5172
  );
4985
5173
  } else if (claudeBin) {
4986
5174
  try {
@@ -4995,16 +5183,14 @@ async function handleInstall(opts = {}) {
4995
5183
  timeout: command.timeoutMs
4996
5184
  });
4997
5185
  }
4998
- console.log(" Plugin registered via Claude Code CLI");
4999
5186
  } catch (error) {
5187
+ claudePluginReady = false;
5000
5188
  const message = error instanceof Error ? error.message : String(error);
5001
- console.log(` warn: Claude plugin registration failed: ${message}`);
5189
+ console.warn(`Warning: Claude plugin registration failed: ${message}`);
5002
5190
  }
5003
5191
  } else {
5004
- console.log(" warn: claude CLI not found; plugin files are ready");
5192
+ console.warn("Warning: claude CLI not found; plugin files are ready");
5005
5193
  }
5006
- console.log();
5007
- console.log("[5/5] Initializing the FML environment...");
5008
5194
  if (!envConfigExists()) {
5009
5195
  writeEnvConfig({
5010
5196
  schemaVersion: 1,
@@ -5013,13 +5199,10 @@ async function handleInstall(opts = {}) {
5013
5199
  [DEFAULT_TARGET_NAME]: { convexUrl: DEFAULT_PROD_URL }
5014
5200
  }
5015
5201
  });
5016
- console.log(` ${DEFAULT_TARGET_NAME}: ${DEFAULT_PROD_URL}`);
5017
- } else {
5018
- console.log(" Preserved existing environment selection");
5019
5202
  }
5020
- if (!skillReady || !marketplaceReady || !settingsReady || !hooksReady) {
5203
+ if (!skillReady || !piExtensionReady || !marketplaceReady || !settingsReady || !hooksReady) {
5021
5204
  throw new Error(
5022
- "FML install is incomplete: repair the preserved agent skill, hook configuration, Claude plugin path, or configuration reported above, then run `fml install` again."
5205
+ "FML install is incomplete: repair the preserved agent skill, Pi extension, hook configuration, Claude plugin path, or configuration reported above, then run `fml install` again."
5023
5206
  );
5024
5207
  }
5025
5208
  try {
@@ -5030,29 +5213,28 @@ async function handleInstall(opts = {}) {
5030
5213
  `FML install is incomplete: hook capture remains paused because its state could not be cleared: ${message}`
5031
5214
  );
5032
5215
  }
5033
- console.log("");
5034
- printBanner();
5035
- console.log("Done! Start a new coding session to activate.\n");
5036
- console.log("Next steps:");
5037
- console.log(" fml login Sign in to your FML account");
5038
- console.log(" fml org Select organization");
5039
- console.log(" fml start Start hook capture and raw artifact sync");
5040
- console.log(" fml doctor Verify setup");
5216
+ console.log("\u2713 Agent skills configured");
5217
+ if (mcpReady && claudePluginReady) {
5218
+ console.log("\u2713 Coding-agent integrations configured");
5219
+ console.log("FML integrations configured.");
5220
+ } else {
5221
+ console.warn(
5222
+ "Warning: Coding-agent integrations were not fully configured. See warnings above."
5223
+ );
5224
+ console.log("FML integration configuration finished with warnings.");
5225
+ }
5041
5226
  }
5042
5227
 
5043
- // src/commands/uninstall.ts
5044
- import fs12 from "fs";
5045
- import os7 from "os";
5046
- import path27 from "path";
5047
- import readline from "readline/promises";
5228
+ // src/commands/sync.ts
5229
+ import { statSync } from "fs";
5048
5230
 
5049
5231
  // src/commands/otel-config.ts
5050
- import fs11 from "fs";
5232
+ import fs12 from "fs";
5051
5233
  import os6 from "os";
5052
- import path26 from "path";
5234
+ import path27 from "path";
5053
5235
 
5054
5236
  // ../../packages/agent-targets/src/install/otel/claude.ts
5055
- import path22 from "path";
5237
+ import path23 from "path";
5056
5238
 
5057
5239
  // ../../packages/agent-targets/src/install/otel/shared.ts
5058
5240
  import { isDeepStrictEqual as isDeepStrictEqual7 } from "util";
@@ -5189,9 +5371,9 @@ var MANAGED_MARKER = "FML_OTEL_MANAGED";
5189
5371
  var CLAUDE_OTEL_SOURCE_LIMITATION = "Claude Code may truncate inline raw API bodies and tool content at 60 KiB and redact extended-thinking content before export";
5190
5372
  function configPath(context) {
5191
5373
  if (context.configPathOverride) {
5192
- return path22.resolve(context.configPathOverride);
5374
+ return path23.resolve(context.configPathOverride);
5193
5375
  }
5194
- return path22.join(
5376
+ return path23.join(
5195
5377
  claudeConfigDirectory({ env: context.env, homeDir: context.homeDir }),
5196
5378
  "settings.json"
5197
5379
  );
@@ -5362,7 +5544,7 @@ var CLAUDE_OTEL_INSTALLER = Object.freeze({
5362
5544
 
5363
5545
  // ../../packages/agent-targets/src/install/otel/codex.ts
5364
5546
  import { isDeepStrictEqual as isDeepStrictEqual8 } from "util";
5365
- import path23 from "path";
5547
+ import path24 from "path";
5366
5548
  import { parse as parse5 } from "smol-toml";
5367
5549
  var TARGET14 = agentTarget("codex");
5368
5550
  var TARGET_ID6 = TARGET14.id;
@@ -5371,13 +5553,13 @@ var AUTHENTICATION2 = "bearer-header";
5371
5553
  var CODEX_OTEL_SOURCE_LIMITATION = "Codex OTel has no documented raw API body or full tool-content export switch; tool-result content is an output snippet";
5372
5554
  function configPath2(context) {
5373
5555
  if (context.configPathOverride) {
5374
- return path23.resolve(context.configPathOverride);
5556
+ return path24.resolve(context.configPathOverride);
5375
5557
  }
5376
5558
  const configuredHome = context.env.CODEX_HOME;
5377
- const codexHome = path23.resolve(
5378
- typeof configuredHome === "string" && configuredHome.trim().length > 0 ? configuredHome : path23.join(context.homeDir, ".codex")
5559
+ const codexHome = path24.resolve(
5560
+ typeof configuredHome === "string" && configuredHome.trim().length > 0 ? configuredHome : path24.join(context.homeDir, ".codex")
5379
5561
  );
5380
- return path23.join(codexHome, "config.toml");
5562
+ return path24.join(codexHome, "config.toml");
5381
5563
  }
5382
5564
  function readToml(snapshot) {
5383
5565
  const failure = snapshotError2(snapshot);
@@ -5597,7 +5779,7 @@ var CODEX_OTEL_INSTALLER = Object.freeze({
5597
5779
  });
5598
5780
 
5599
5781
  // ../../packages/agent-targets/src/install/otel/gemini.ts
5600
- import path24 from "path";
5782
+ import path25 from "path";
5601
5783
  var TARGET15 = agentTarget("gemini");
5602
5784
  var TARGET_ID7 = TARGET15.id;
5603
5785
  var DISPLAY_NAME7 = TARGET15.displayName;
@@ -5605,9 +5787,9 @@ var AUTHENTICATION3 = "path-token";
5605
5787
  var GEMINI_OTEL_SOURCE_LIMITATION = "Gemini CLI has no documented OTLP header setting; FML uses an observer-local tokenized loopback path";
5606
5788
  function configPath3(context) {
5607
5789
  if (context.configPathOverride) {
5608
- return path24.resolve(context.configPathOverride);
5790
+ return path25.resolve(context.configPathOverride);
5609
5791
  }
5610
- return path24.join(geminiConfigDirectory(context), "settings.json");
5792
+ return path25.join(geminiConfigDirectory(context), "settings.json");
5611
5793
  }
5612
5794
  function managedTelemetry(context) {
5613
5795
  const destination = resolveDestination(context.observer);
@@ -5761,8 +5943,8 @@ var OTEL_INSTALLER_CAPABILITIES = Object.freeze([
5761
5943
  ]);
5762
5944
 
5763
5945
  // src/commands/otel-config-state.ts
5764
- import fs10 from "fs";
5765
- import path25 from "path";
5946
+ import fs11 from "fs";
5947
+ import path26 from "path";
5766
5948
  var OTEL_CONFIG_STATE_SCHEMA_VERSION = 1;
5767
5949
  function emptyState2() {
5768
5950
  return Object.freeze({
@@ -5776,12 +5958,12 @@ function invalidState2(statePath, reason) {
5776
5958
  );
5777
5959
  }
5778
5960
  function enforceOwnerOnlyMode(statePath) {
5779
- if (process.platform !== "win32") fs10.chmodSync(statePath, 384);
5961
+ if (process.platform !== "win32") fs11.chmodSync(statePath, 384);
5780
5962
  }
5781
5963
  function readStateFile(statePath) {
5782
5964
  let stat;
5783
5965
  try {
5784
- stat = fs10.lstatSync(statePath);
5966
+ stat = fs11.lstatSync(statePath);
5785
5967
  } catch (error) {
5786
5968
  if (error.code === "ENOENT") {
5787
5969
  return { kind: "missing" };
@@ -5799,7 +5981,7 @@ function readStateFile(statePath) {
5799
5981
  }
5800
5982
  try {
5801
5983
  enforceOwnerOnlyMode(statePath);
5802
- return { kind: "file", contents: fs10.readFileSync(statePath, "utf8") };
5984
+ return { kind: "file", contents: fs11.readFileSync(statePath, "utf8") };
5803
5985
  } catch (error) {
5804
5986
  return {
5805
5987
  kind: "invalid",
@@ -5808,7 +5990,7 @@ function readStateFile(statePath) {
5808
5990
  }
5809
5991
  }
5810
5992
  function otelConfigStateEntryKey(targetId, configPath4) {
5811
- const resolved = path25.resolve(configPath4);
5993
+ const resolved = path26.resolve(configPath4);
5812
5994
  const pathKey = process.platform === "win32" ? resolved.toLowerCase() : resolved;
5813
5995
  return `${targetId}\0${pathKey}`;
5814
5996
  }
@@ -5844,7 +6026,7 @@ function parseState2(source, statePath) {
5844
6026
  if (typeof observerRecord.baseUrl !== "string" || observerRecord.baseUrl.length === 0 || typeof observerRecord.authToken !== "string" || observerRecord.authToken.length === 0) {
5845
6027
  throw invalidState2(statePath, "contains an invalid observer destination");
5846
6028
  }
5847
- const resolvedPath = path25.resolve(entry.path);
6029
+ const resolvedPath = path26.resolve(entry.path);
5848
6030
  const key = otelConfigStateEntryKey(entry.targetId, resolvedPath);
5849
6031
  if (keys.has(key)) {
5850
6032
  throw invalidState2(statePath, "contains duplicate entries");
@@ -5882,7 +6064,7 @@ function loadFmlOtelConfigState(statePath) {
5882
6064
  function serializeState2(entries) {
5883
6065
  const sorted = [...entries].map((entry) => ({
5884
6066
  targetId: entry.targetId,
5885
- path: path25.resolve(entry.path),
6067
+ path: path26.resolve(entry.path),
5886
6068
  observer: {
5887
6069
  baseUrl: entry.observer.baseUrl,
5888
6070
  authToken: entry.observer.authToken
@@ -5919,7 +6101,7 @@ function isKnownOtelTargetId(targetId) {
5919
6101
  }
5920
6102
 
5921
6103
  // src/commands/otel-config.ts
5922
- var DEFAULT_OTEL_CONFIG_STATE_PATH = path26.join(
6104
+ var DEFAULT_OTEL_CONFIG_STATE_PATH = path27.join(
5923
6105
  FML_DATA_DIR,
5924
6106
  "otel-config.json"
5925
6107
  );
@@ -5928,7 +6110,7 @@ function readLocalObserverOtelDestination(options) {
5928
6110
  if (!authToken) return null;
5929
6111
  try {
5930
6112
  const parsed = JSON.parse(
5931
- fs11.readFileSync(options.endpointFile, "utf8")
6113
+ fs12.readFileSync(options.endpointFile, "utf8")
5932
6114
  );
5933
6115
  const endpoint = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5934
6116
  const host = endpoint?.host;
@@ -5945,7 +6127,7 @@ function readLocalObserverOtelDestination(options) {
5945
6127
  function readOtelFile(filePath) {
5946
6128
  let stat;
5947
6129
  try {
5948
- stat = fs11.lstatSync(filePath);
6130
+ stat = fs12.lstatSync(filePath);
5949
6131
  } catch (error) {
5950
6132
  if (error.code === "ENOENT") {
5951
6133
  return { path: filePath, kind: "missing" };
@@ -5974,7 +6156,7 @@ function readOtelFile(filePath) {
5974
6156
  return {
5975
6157
  path: filePath,
5976
6158
  kind: "file",
5977
- contents: fs11.readFileSync(filePath, "utf8")
6159
+ contents: fs12.readFileSync(filePath, "utf8")
5978
6160
  };
5979
6161
  } catch (error) {
5980
6162
  return {
@@ -5996,7 +6178,7 @@ function assertMutationPrecondition2(mutation) {
5996
6178
  throw new Error(`configuration changed while planning ${mutation.path}`);
5997
6179
  }
5998
6180
  function applyMutation2(mutation) {
5999
- if (mutation.after === null) fs11.rmSync(mutation.path);
6181
+ if (mutation.after === null) fs12.rmSync(mutation.path);
6000
6182
  else writeTextFileAtomic(mutation.path, mutation.after);
6001
6183
  }
6002
6184
  function enforceOwnerOnlyConfigPermissions(filePath) {
@@ -6009,7 +6191,7 @@ function enforceOwnerOnlyConfigPermissions(filePath) {
6009
6191
  );
6010
6192
  }
6011
6193
  try {
6012
- fs11.chmodSync(filePath, 384);
6194
+ fs12.chmodSync(filePath, 384);
6013
6195
  } catch (error) {
6014
6196
  const detail = error instanceof Error ? error.message : String(error);
6015
6197
  throw new Error(
@@ -6040,7 +6222,7 @@ function applyPlan3(plan, warn, commitOwnership) {
6040
6222
  } catch (error) {
6041
6223
  const detail = error instanceof Error ? error.message : String(error);
6042
6224
  warn(
6043
- `warn: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: ${detail}`
6225
+ `Warning: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: ${detail}`
6044
6226
  );
6045
6227
  return null;
6046
6228
  }
@@ -6049,7 +6231,7 @@ function applyPlan3(plan, warn, commitOwnership) {
6049
6231
  } catch (error) {
6050
6232
  const detail = error instanceof Error ? error.message : String(error);
6051
6233
  warn(
6052
- `warn: could not record ${plan.displayName} OpenTelemetry ownership at ${plan.configPath}: ${detail}`
6234
+ `Warning: could not record ${plan.displayName} OpenTelemetry ownership at ${plan.configPath}: ${detail}`
6053
6235
  );
6054
6236
  }
6055
6237
  }
@@ -6058,7 +6240,7 @@ function applyPlan3(plan, warn, commitOwnership) {
6058
6240
  const uniquePaths2 = new Set(plan.mutations.map((mutation) => mutation.path));
6059
6241
  if (uniquePaths2.size !== plan.mutations.length) {
6060
6242
  warn(
6061
- `warn: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: plan contains duplicate paths`
6243
+ `Warning: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: plan contains duplicate paths`
6062
6244
  );
6063
6245
  return null;
6064
6246
  }
@@ -6089,7 +6271,7 @@ function applyPlan3(plan, warn, commitOwnership) {
6089
6271
  const detail = error instanceof Error ? error.message : String(error);
6090
6272
  const rollbackDetail = rollbackErrors.length === 0 ? "" : `; rollback failed: ${rollbackErrors.join("; ")}`;
6091
6273
  warn(
6092
- `warn: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: ${detail}${rollbackDetail}`
6274
+ `Warning: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: ${detail}${rollbackDetail}`
6093
6275
  );
6094
6276
  return null;
6095
6277
  }
@@ -6104,7 +6286,7 @@ function plannerContext3(observer, options, configPathOverride, configOwnershipR
6104
6286
  observer,
6105
6287
  homeDir: options.homeDir ?? os6.homedir(),
6106
6288
  env: options.env ?? process.env,
6107
- ...configPathOverride ? { configPathOverride: path26.resolve(configPathOverride) } : {},
6289
+ ...configPathOverride ? { configPathOverride: path27.resolve(configPathOverride) } : {},
6108
6290
  ...configOwnershipRecorded ? { configOwnershipRecorded: true } : {},
6109
6291
  readFile: readOtelFile
6110
6292
  };
@@ -6124,7 +6306,7 @@ function loadOtelConfigState(options, warn) {
6124
6306
  )
6125
6307
  };
6126
6308
  } catch (error) {
6127
- warn(`warn: ${error instanceof Error ? error.message : String(error)}`);
6309
+ warn(`Warning: ${error instanceof Error ? error.message : String(error)}`);
6128
6310
  return null;
6129
6311
  }
6130
6312
  }
@@ -6147,7 +6329,7 @@ function commitStateEntry(state, key, next) {
6147
6329
  function stateEntry(plan, observer) {
6148
6330
  return Object.freeze({
6149
6331
  targetId: plan.targetId,
6150
- path: path26.resolve(plan.configPath),
6332
+ path: path27.resolve(plan.configPath),
6151
6333
  observer: Object.freeze({
6152
6334
  baseUrl: observer.baseUrl,
6153
6335
  authToken: observer.authToken
@@ -6171,7 +6353,7 @@ function planSafely(capability, operation, context, warn) {
6171
6353
  return operation === "install" ? capability.planInstall(context) : capability.planUninstall(context);
6172
6354
  } catch (error) {
6173
6355
  warn(
6174
- `warn: skipped ${capability.displayName} OpenTelemetry: ${error instanceof Error ? error.message : String(error)}`
6356
+ `Warning: skipped ${capability.displayName} OpenTelemetry: ${error instanceof Error ? error.message : String(error)}`
6175
6357
  );
6176
6358
  return null;
6177
6359
  }
@@ -6181,7 +6363,7 @@ function pruneMissingRecordedEntry(state, key, entry, warn) {
6181
6363
  commitStateEntry(state, key, null);
6182
6364
  } catch (error) {
6183
6365
  warn(
6184
- `warn: could not prune missing ${entry.targetId} OpenTelemetry ownership at ${entry.path}: ${error instanceof Error ? error.message : String(error)}`
6366
+ `Warning: could not prune missing ${entry.targetId} OpenTelemetry ownership at ${entry.path}: ${error instanceof Error ? error.message : String(error)}`
6185
6367
  );
6186
6368
  }
6187
6369
  }
@@ -6283,13 +6465,13 @@ function removeFmlOtel(observer, options = {}) {
6283
6465
  }
6284
6466
  if (candidate.recorded && candidate.plan.mutations.length === 0 && candidate.plan.warnings.length === 0 && state) {
6285
6467
  warn(
6286
- `warn: preserved ${candidate.plan.displayName} OpenTelemetry at ${candidate.plan.configPath}: managed fields were modified or removed after installation`
6468
+ `Warning: preserved ${candidate.plan.displayName} OpenTelemetry at ${candidate.plan.configPath}: managed fields were modified or removed after installation`
6287
6469
  );
6288
6470
  try {
6289
6471
  commitStateEntry(state, key, null);
6290
6472
  } catch (error) {
6291
6473
  warn(
6292
- `warn: could not relinquish ${candidate.plan.displayName} OpenTelemetry ownership at ${candidate.plan.configPath}: ${error instanceof Error ? error.message : String(error)}`
6474
+ `Warning: could not relinquish ${candidate.plan.displayName} OpenTelemetry ownership at ${candidate.plan.configPath}: ${error instanceof Error ? error.message : String(error)}`
6293
6475
  );
6294
6476
  }
6295
6477
  }
@@ -6297,437 +6479,407 @@ function removeFmlOtel(observer, options = {}) {
6297
6479
  return changes;
6298
6480
  }
6299
6481
 
6300
- // src/commands/uninstall.ts
6301
- var PURGE_WARNING = "This deletes FML credentials for ALL environments, logs, sync state, and every pending best-effort OTLP envelope.";
6302
- function tryDisableHookCapture() {
6303
- try {
6304
- setHookCaptureDisabled(true);
6305
- return true;
6306
- } catch (error) {
6307
- const message = error instanceof Error ? error.message : String(error);
6308
- console.warn(
6309
- `Could not persist the hook-capture pause at ${FML_HOOK_CAPTURE_DISABLED_FILE}: ${message}`
6310
- );
6311
- return false;
6312
- }
6482
+ // src/commands/sync.ts
6483
+ function rawSync() {
6484
+ return createCliRawSync();
6313
6485
  }
6314
- function resolvePreservedPaths(preservedFile) {
6315
- const logicalPath = path27.resolve(preservedFile);
6316
- try {
6317
- const canonicalPath = fs12.realpathSync(preservedFile);
6318
- return pathsAreSame(canonicalPath, logicalPath) ? [logicalPath] : [logicalPath, canonicalPath];
6319
- } catch {
6320
- return [logicalPath];
6486
+ function formatBytes(bytes) {
6487
+ if (bytes < 1024) return `${bytes} B`;
6488
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
6489
+ if (bytes < 1024 * 1024 * 1024) {
6490
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
6321
6491
  }
6492
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
6322
6493
  }
6323
- function purgeDirectoryPreservingPaths(directory, preservedPaths, configuredRoot = false) {
6324
- const resolvedDirectory = path27.resolve(directory);
6325
- let directoryStats;
6326
- try {
6327
- directoryStats = fs12.lstatSync(directory);
6328
- } catch (error) {
6329
- if (error.code === "ENOENT") return true;
6330
- throw error;
6331
- }
6332
- if (directoryStats.isSymbolicLink()) {
6333
- if (configuredRoot) return false;
6334
- const containsLogicalMarker = preservedPaths.some(
6335
- (preservedPath) => pathContains(resolvedDirectory, preservedPath)
6494
+ function formatDuration(durationMs) {
6495
+ if (durationMs < 1e3) return `${Math.round(durationMs)} ms`;
6496
+ return `${(durationMs / 1e3).toFixed(2)} s`;
6497
+ }
6498
+ function printRepositoryResolution(metrics) {
6499
+ if (metrics.attempted === 0 && metrics.budgetSkipped === 0) return;
6500
+ console.log(
6501
+ ` Repo checks: ${metrics.attempted} (${metrics.resolved} resolved, ${metrics.unresolved} unresolved, ${metrics.cacheHits} cached, ${metrics.budgetSkipped} skipped by budget)`
6502
+ );
6503
+ console.log(
6504
+ ` Repo check time: ${formatDuration(metrics.elapsedMs)} total, ${formatDuration(metrics.slowestMs)} slowest`
6505
+ );
6506
+ }
6507
+ function writeJson(value) {
6508
+ console.log(JSON.stringify(value, null, 2));
6509
+ }
6510
+ function warnRetainedLegacyDatabase(retained) {
6511
+ if (retained.pendingEvents !== null && retained.pendingHookEvents !== null && retained.pendingOtlpEnvelopes !== null) {
6512
+ console.warn(
6513
+ `Warning: Retained legacy observer database at ${retained.databaseFile} with ${retained.pendingEvents} pending rows (${retained.pendingHookEvents} hook, ${retained.pendingOtlpEnvelopes} OTLP); FML will not sync those rows automatically.`
6336
6514
  );
6337
- if (!containsLogicalMarker) {
6338
- fs12.rmSync(directory, { force: true });
6339
- return true;
6340
- }
6341
- return false;
6342
- }
6343
- const directoryPaths = [resolvedDirectory];
6344
- try {
6345
- const canonicalDirectory = fs12.realpathSync(directory);
6346
- if (!pathsAreSame(canonicalDirectory, resolvedDirectory)) {
6347
- directoryPaths.push(canonicalDirectory);
6348
- }
6349
- } catch {
6350
- }
6351
- const containsPreservedPath = directoryPaths.some(
6352
- (directoryPath) => preservedPaths.some(
6353
- (preservedPath) => pathContains(directoryPath, preservedPath)
6354
- )
6355
- );
6356
- if (!containsPreservedPath) {
6357
- fs12.rmSync(directory, { recursive: true, force: true });
6358
- return true;
6515
+ return;
6359
6516
  }
6360
- const isPreservedPath = directoryPaths.some(
6361
- (directoryPath) => preservedPaths.some(
6362
- (preservedPath) => pathsAreSame(directoryPath, path27.resolve(preservedPath))
6363
- )
6517
+ console.warn(
6518
+ `Warning: Retained legacy observer database at ${retained.databaseFile}; pending row count unavailable. FML will not sync that database automatically.`
6364
6519
  );
6365
- if (isPreservedPath) return true;
6366
- if (!directoryStats.isDirectory()) return false;
6367
- let entries;
6368
- try {
6369
- entries = fs12.readdirSync(directory);
6370
- } catch (error) {
6371
- if (error.code === "ENOENT") return true;
6372
- throw error;
6373
- }
6374
- let complete = true;
6375
- for (const entry of entries) {
6376
- const entryPath = path27.join(directory, entry);
6377
- if (!purgeDirectoryPreservingPaths(entryPath, preservedPaths, false)) {
6378
- complete = false;
6379
- }
6380
- }
6381
- return complete;
6382
6520
  }
6383
- function purgeDirectoryPreservingFiles(directory, preservedFiles) {
6384
- if (hasSymlinkedParentBelowTrustedAnchor(directory)) return false;
6385
- return purgeDirectoryPreservingPaths(
6386
- directory,
6387
- preservedFiles.flatMap(
6388
- (preservedFile) => resolvePreservedPaths(preservedFile)
6389
- ),
6390
- true
6521
+ function printStatus(status) {
6522
+ const { queue } = status;
6523
+ console.log(
6524
+ `Raw artifact sync: ${status.settings.enabled ? "enabled" : "disabled"}`
6391
6525
  );
6392
- }
6393
- function finishCustomRootPurge(directory) {
6394
- const marker = managedDirectoryMarker(directory);
6395
- fs12.rmSync(marker, { force: true });
6396
- try {
6397
- fs12.rmdirSync(directory);
6398
- } catch (error) {
6399
- if (error.code === "ENOENT") return;
6400
- try {
6401
- fs12.writeFileSync(marker, FML_MANAGED_DIRECTORY_MARKER_CONTENT, {
6402
- encoding: "utf8",
6403
- flag: "wx",
6404
- mode: 384
6405
- });
6406
- } catch (markerError) {
6407
- if (markerError.code === "EEXIST" && hasManagedDirectoryMarker(directory)) {
6408
- throw error;
6409
- }
6410
- const removalMessage = error instanceof Error ? error.message : String(error);
6411
- const markerMessage = markerError instanceof Error ? markerError.message : String(markerError);
6412
- throw new Error(
6413
- `could not remove ${directory} (${removalMessage}) or restore its FML ownership marker (${markerMessage})`
6414
- );
6415
- }
6416
- throw error;
6417
- }
6418
- }
6419
- function cleanInstalledPluginScopes(paths) {
6420
- const installedPlugins = readJsonFile(paths.installedPluginsPath);
6421
- if (!installedPlugins) return;
6422
- const update = CLAUDE_PLUGIN_INSTALLER.removeInstalledPlugin(
6423
- installedPlugins,
6424
- CLAUDE_PLUGIN_INSTALLER.pluginId
6526
+ console.log(` State: ${FML_OBSERVER_DIR}`);
6527
+ console.log(` Tracked: ${queue.tracked}`);
6528
+ console.log(` Pending: ${queue.pending}`);
6529
+ console.log(` Raw work: ${queue.rawPending}`);
6530
+ console.log(` Metadata: ${queue.metadataPending}`);
6531
+ console.log(` Uploaded: ${queue.uploaded}`);
6532
+ console.log(` Failed: ${queue.failed}`);
6533
+ console.log(
6534
+ ` Watermark: ${formatBytes(queue.uploadedBytes)} / ${formatBytes(queue.totalBytes)}`
6425
6535
  );
6426
- if (!update.changed) return;
6427
- for (const settingsPath2 of CLAUDE_PLUGIN_INSTALLER.projectSettingsPaths(
6428
- installedPlugins,
6429
- CLAUDE_PLUGIN_INSTALLER.pluginId
6430
- )) {
6431
- if (removeEnabledPluginFromSettings(
6432
- settingsPath2,
6433
- CLAUDE_PLUGIN_INSTALLER.pluginId
6434
- )) {
6435
- console.log(` Cleaned ${settingsPath2}`);
6436
- }
6437
- }
6438
- writeJsonFile(paths.installedPluginsPath, update.installedPlugins);
6439
- console.log(" Cleaned installed_plugins.json");
6440
- }
6441
- function purgeWarning(roots) {
6442
- return `${PURGE_WARNING}
6443
- Purge roots:
6444
- ${roots.map((root) => ` - ${root.directory}`).join("\n")}`;
6536
+ console.log(` Remaining: ${formatBytes(queue.remainingBytes)}`);
6445
6537
  }
6446
- async function confirmPurge(opts, roots) {
6447
- const warning = purgeWarning(roots);
6448
- if (opts.assumeYes) {
6449
- console.log(warning);
6450
- return true;
6451
- }
6452
- const input = opts.input ?? process.stdin;
6453
- if (input.isTTY !== true) {
6454
- console.error(
6455
- `${warning}
6456
- Re-run with --yes to confirm in a non-interactive session.`
6457
- );
6458
- return false;
6459
- }
6460
- const rl = readline.createInterface({
6461
- input,
6462
- output: opts.output ?? process.stdout
6463
- });
6464
- try {
6465
- const closed = new Promise((resolve) => {
6466
- rl.once("close", () => resolve(null));
6467
- });
6468
- const answer = await Promise.race([
6469
- rl.question(`${warning}
6470
- Type y to continue: `).catch(() => null),
6471
- closed
6472
- ]);
6473
- if (answer !== null && answer.trim().toLowerCase() === "y") return true;
6474
- } finally {
6475
- rl.close();
6476
- }
6477
- console.error("Uninstall canceled; nothing was removed.");
6478
- return false;
6538
+ function syncExitCode(result) {
6539
+ if (result.outcome === "failed" || result.outcome === "exhausted") return 1;
6540
+ if (result.outcome === "busy") return 1;
6541
+ return 0;
6479
6542
  }
6480
- function refuseUnsafePurge(roots) {
6481
- const unsafeRoots = unsafePurgeRoots(roots);
6482
- if (unsafeRoots.length === 0) return false;
6483
- console.error("Refusing to purge unsafe FML directories:");
6484
- for (const root of unsafeRoots) {
6485
- console.error(` - ${root.directory}: ${root.reason}`);
6543
+ async function handleSyncRun(opts = {}) {
6544
+ const sync2 = rawSync();
6545
+ if (!sync2.getSettings().enabled) {
6546
+ const result2 = {
6547
+ ok: true,
6548
+ outcome: "disabled",
6549
+ status: sync2.status()
6550
+ };
6551
+ if (opts.json) writeJson(result2);
6552
+ else printStatus(result2.status);
6553
+ return;
6486
6554
  }
6487
- console.error(
6488
- "Inspect and remove those roots manually, or reconfigure FML to fresh dedicated empty data, state, log, and observer directories and run `fml install` before retrying."
6489
- );
6490
- process.exitCode = 1;
6491
- return true;
6492
- }
6493
- async function handleUninstall(opts) {
6494
- const purgeRoots = opts.purge ? configuredManagedRoots() : [];
6495
- if (opts.purge) {
6496
- if (refuseUnsafePurge(purgeRoots)) return;
6497
- if (!await confirmPurge(opts, purgeRoots)) {
6498
- process.exitCode = 1;
6499
- return;
6555
+ if (opts.dryRun) {
6556
+ const discovery = sync2.discover();
6557
+ const bySource = {};
6558
+ let totalBytes = 0;
6559
+ for (const candidate of discovery.candidates) {
6560
+ bySource[candidate.source] = (bySource[candidate.source] ?? 0) + 1;
6561
+ try {
6562
+ totalBytes += statSync(candidate.sourcePath).size;
6563
+ } catch {
6564
+ }
6500
6565
  }
6501
- for (const root of purgeRoots) {
6502
- if (!fs12.existsSync(root.directory) && pathContains(root.directory, FML_OBSERVER_DIR)) {
6503
- prepareManagedDirectory(root);
6566
+ const result2 = {
6567
+ ok: true,
6568
+ dryRun: true,
6569
+ targetUrl: getSiteUrl(),
6570
+ discovered: {
6571
+ count: discovery.candidates.length,
6572
+ totalBytes,
6573
+ bySource
6574
+ },
6575
+ repositoryResolution: discovery.repositoryResolution,
6576
+ oversized: discovery.oversized,
6577
+ status: sync2.status()
6578
+ };
6579
+ if (opts.json) writeJson(result2);
6580
+ else {
6581
+ console.log("FML raw artifact sync dry-run");
6582
+ console.log(` Target: ${result2.targetUrl}`);
6583
+ console.log(` Discovered: ${result2.discovered.count}`);
6584
+ console.log(` Bytes: ${formatBytes(result2.discovered.totalBytes)}`);
6585
+ for (const [source, count] of Object.entries(bySource).sort()) {
6586
+ console.log(` ${source}: ${count}`);
6587
+ }
6588
+ printRepositoryResolution(discovery.repositoryResolution);
6589
+ console.log(` Oversized: ${discovery.oversized.length}`);
6590
+ if (discovery.compressedArtifactsUnsupportedReason) {
6591
+ console.log(
6592
+ ` Compressed: skipped \u2014 ${discovery.compressedArtifactsUnsupportedReason}`
6593
+ );
6504
6594
  }
6505
6595
  }
6506
- if (refuseUnsafePurge(purgeRoots)) return;
6507
- }
6508
- console.log("Uninstalling fml...\n");
6509
- const otelDestination = readLocalObserverOtelDestination({
6510
- endpointFile: FML_OBSERVER_ENDPOINT_FILE,
6511
- authTokenFile: FML_OBSERVER_AUTH_TOKEN_FILE
6512
- });
6513
- let observer;
6514
- try {
6515
- observer = await stopObserver();
6516
- } catch (error) {
6517
- console.error(error instanceof Error ? error.message : String(error));
6518
- console.error(
6519
- "Could not safely stop the FML observer; uninstall was aborted. Repair the reported files and rerun `fml uninstall`."
6520
- );
6521
- process.exitCode = 1;
6522
6596
  return;
6523
6597
  }
6524
- if (observer.status === "permission_denied" || observer.status === "foreign_listener" || observer.status === "ambiguous_live_process" || observer.status === "unresponsive" || observer.status === "signal_sent") {
6525
- console.error(
6526
- `Could not safely stop FML observer PID ${observer.pid}; uninstall was aborted.`
6527
- );
6598
+ const attempt = await runRawSyncOnce();
6599
+ if (attempt.kind === "unauthenticated") {
6600
+ console.error("Raw artifact sync requires an FML login.");
6601
+ console.error("Run `fml login` or set FML_TOKEN.");
6528
6602
  process.exitCode = 1;
6529
6603
  return;
6530
6604
  }
6531
- let hookCapturePaused = tryDisableHookCapture();
6532
- createRawArtifactSync({ stateDir: FML_OBSERVER_DIR }).setEnabled(false);
6533
- const paths = CLAUDE_PLUGIN_INSTALLER.resolvePaths({
6534
- env: process.env,
6535
- homeDir: os7.homedir()
6536
- });
6537
- console.log("[1/7] Unregistering the Claude Code plugin...");
6538
- const claudeBin = resolveBin("claude");
6539
- if (claudeBin) {
6540
- try {
6541
- const command = CLAUDE_PLUGIN_INSTALLER.uninstallCommand;
6542
- execBinSync(claudeBin, [...command.args], {
6543
- timeout: command.timeoutMs
6544
- });
6545
- } catch {
6605
+ if (attempt.kind === "disabled") {
6606
+ if (opts.json) {
6607
+ writeJson({ ok: true, outcome: "disabled", status: attempt.status });
6608
+ } else {
6609
+ printStatus(attempt.status);
6546
6610
  }
6611
+ return;
6547
6612
  }
6548
- cleanInstalledPluginScopes(paths);
6549
- const settings = readJsonFile(paths.settingsPath);
6550
- if (settings) {
6551
- const marketplace2 = readJsonFile(paths.marketplaceManifestPath);
6552
- const keepLocalMarketplace = CLAUDE_PLUGIN_INSTALLER.hasOtherLocalMarketplacePlugins(
6553
- settings,
6554
- marketplace2
6555
- );
6556
- writeJsonFile(
6557
- paths.settingsPath,
6558
- CLAUDE_PLUGIN_INSTALLER.disableInSettings(settings, keepLocalMarketplace)
6613
+ const { result, targetUrl } = attempt;
6614
+ if (opts.json) {
6615
+ writeJson({ ok: syncExitCode(result) === 0, targetUrl, result });
6616
+ } else {
6617
+ console.log("FML raw artifact sync");
6618
+ console.log(` Target: ${targetUrl}`);
6619
+ console.log(` Outcome: ${result.outcome}`);
6620
+ console.log(` Sync passes: ${result.syncPasses}`);
6621
+ console.log(` Discovered: ${result.discovery.candidates.length}`);
6622
+ if (result.discovery.compressedArtifactsUnsupportedReason) {
6623
+ console.log(
6624
+ ` Compressed: skipped \u2014 ${result.discovery.compressedArtifactsUnsupportedReason}`
6625
+ );
6626
+ }
6627
+ printRepositoryResolution(result.discovery.repositoryResolution);
6628
+ console.log(` Uploaded files: ${result.upload.uploaded}`);
6629
+ console.log(` Uploaded chunks: ${result.upload.uploadedChunks}`);
6630
+ console.log(
6631
+ ` Uploaded bytes: ${formatBytes(result.upload.uploadedBytes)}`
6559
6632
  );
6633
+ console.log(` Failed: ${result.upload.failed}`);
6634
+ console.log(` Exhausted: ${result.upload.exhausted}`);
6635
+ if (result.upload.retryAtMs !== void 0) {
6636
+ console.log(
6637
+ ` Retry at: ${new Date(result.upload.retryAtMs).toISOString()}`
6638
+ );
6639
+ }
6640
+ console.log(` Drained: ${result.drained ? "yes" : "no"}`);
6641
+ if (result.outcome === "pending") {
6642
+ console.log(
6643
+ " Next: More data remains; run `fml sync` again or `fml start` for continuous syncing."
6644
+ );
6645
+ } else if (result.outcome === "backing_off") {
6646
+ const nextRetryAtMs = result.upload.nextRetryAtMs;
6647
+ console.log(
6648
+ ` Next: Remaining items are backing off after upload errors${nextRetryAtMs !== void 0 ? `; next retry due ${new Date(nextRetryAtMs).toISOString()}` : ""}. \`fml start\` retries automatically.`
6649
+ );
6650
+ }
6651
+ console.log("");
6652
+ printStatus(result.status);
6560
6653
  }
6561
- console.log(" Plugin settings removed");
6562
- console.log("[2/7] Removing agent-native /fml skills...");
6654
+ process.exitCode = syncExitCode(result);
6655
+ }
6656
+ function setRawSyncEnabled(enabled) {
6657
+ return rawSync().setEnabled(enabled);
6658
+ }
6659
+ async function handleSyncEnable(opts = {}) {
6660
+ const sync2 = rawSync();
6661
+ const settings = sync2.setEnabled(true);
6662
+ const isolationWarning = observerOnlyOverrideWarning();
6663
+ if (isolationWarning) console.warn(isolationWarning);
6664
+ let retainedLegacyDatabase;
6563
6665
  try {
6564
- for (const result of removeManagedAgentSkills(resolvePackageRoot())) {
6565
- const targets = result.displayNames.join(", ");
6566
- if (result.status === "preserved") {
6567
- console.warn(
6568
- ` Preserved ${result.destinationPath} for ${targets}: ${result.reason}`
6569
- );
6570
- } else if (result.reason) {
6571
- console.warn(
6572
- ` ${targets}: ${result.destinationPath} (${result.status}); ${result.reason}`
6666
+ const observer = await startObserver({
6667
+ onLegacyDatabaseRetained: (retained) => {
6668
+ retainedLegacyDatabase = retained;
6669
+ if (!opts.json) warnRetainedLegacyDatabase(retained);
6670
+ }
6671
+ });
6672
+ setHookCaptureDisabled(false);
6673
+ if (observer.status === "already_running") {
6674
+ await wakeObserverSync();
6675
+ }
6676
+ const port = observer.runtime.health?.port;
6677
+ const authToken = port ? readObserverAuthToken(FML_OBSERVER_AUTH_TOKEN_FILE) : null;
6678
+ if (port && authToken) {
6679
+ configureFmlOtel({
6680
+ baseUrl: `http://127.0.0.1:${port}`,
6681
+ authToken
6682
+ });
6683
+ } else if (!opts.json) {
6684
+ console.warn(
6685
+ "Warning: Could not configure native OpenTelemetry: the healthy observer endpoint or local token is unavailable."
6686
+ );
6687
+ }
6688
+ if (opts.json) {
6689
+ writeJson({
6690
+ ok: true,
6691
+ sync: settings,
6692
+ observer,
6693
+ ...observer.retainedLegacyDatabase ? { retainedLegacyDatabase: observer.retainedLegacyDatabase } : {}
6694
+ });
6695
+ } else {
6696
+ const pid = observer.runtime.health?.pid;
6697
+ if (observer.status === "started") {
6698
+ console.log(
6699
+ pid ? `FML observer started (PID ${pid}).` : "FML observer started."
6573
6700
  );
6701
+ console.log("Agent activity will sync automatically.");
6574
6702
  } else {
6575
6703
  console.log(
6576
- ` ${targets}: ${result.destinationPath} (${result.status})`
6704
+ pid ? `FML observer is already running (PID ${pid}).` : "FML observer is already running."
6577
6705
  );
6706
+ console.log("Agent activity sync is on.");
6578
6707
  }
6579
6708
  }
6580
6709
  } catch (error) {
6581
6710
  const message = error instanceof Error ? error.message : String(error);
6582
- console.warn(
6583
- ` Agent-native skill cleanup could not be completed: ${message}`
6584
- );
6585
- }
6586
- console.log("[3/7] Removing the local marketplace entry...");
6587
- if (removeMarketplaceLink(paths.marketplaceLinkPath) === "preserved") {
6588
- console.warn(
6589
- ` Preserved non-symlink marketplace path ${paths.marketplaceLinkPath}`
6590
- );
6591
- }
6592
- const marketplace = readJsonFile(paths.marketplaceManifestPath);
6593
- if (marketplace && Array.isArray(marketplace.plugins)) {
6594
- writeJsonFile(
6595
- paths.marketplaceManifestPath,
6596
- CLAUDE_PLUGIN_INSTALLER.removeMarketplaceEntry(
6597
- marketplace,
6598
- CLAUDE_PLUGIN_INSTALLER.pluginName
6599
- )
6600
- );
6601
- }
6602
- fs12.rmSync(paths.pluginCacheDir, { recursive: true, force: true });
6603
- console.log(" Marketplace and plugin cache removed");
6604
- console.log("[4/7] Removing FML hook registrations...");
6605
- let hookRemovalComplete = true;
6606
- for (const target2 of removeFmlHooks({
6607
- onFailure: () => {
6608
- hookRemovalComplete = false;
6609
- }
6610
- })) {
6611
- console.log(` Removed FML hooks from ${target2.displayName}`);
6612
- }
6613
- if (!hookRemovalComplete) {
6711
+ if (opts.json) {
6712
+ writeJson({
6713
+ ok: false,
6714
+ sync: settings,
6715
+ error: message,
6716
+ ...retainedLegacyDatabase ? { retainedLegacyDatabase } : {}
6717
+ });
6718
+ } else console.error(message);
6614
6719
  process.exitCode = 1;
6615
- console.warn(
6616
- " Some FML hook registrations were preserved; repair the paths reported above and rerun `fml uninstall`."
6617
- );
6618
6720
  }
6619
- console.log("[5/7] Removing native OpenTelemetry configuration...");
6620
- for (const target2 of removeFmlOtel(otelDestination)) {
6621
- console.log(` Removed FML OpenTelemetry from ${target2.displayName}`);
6721
+ }
6722
+ async function handleSyncDisable(opts = {}) {
6723
+ const sync2 = rawSync();
6724
+ const settings = sync2.setEnabled(false);
6725
+ const observerWoken = await wakeObserverSync();
6726
+ if (opts.json) {
6727
+ writeJson({ ok: true, sync: settings, observerWoken });
6728
+ } else {
6729
+ console.log("Agent activity sync is paused.");
6730
+ const observer = await readObserverRuntimeStatus();
6731
+ if (observer.running) {
6732
+ console.log("FML observer remains available for local integrations");
6733
+ }
6622
6734
  }
6623
- if (!otelDestination) {
6624
- console.warn(
6625
- " The local observer endpoint or token is unavailable; only recorded OpenTelemetry profiles could be cleaned"
6735
+ }
6736
+ async function handleSyncStatus(opts = {}) {
6737
+ const status = rawSync().status();
6738
+ const observer = await readObserverRuntimeStatus();
6739
+ if (opts.json) writeJson({ ok: true, status, observer });
6740
+ else {
6741
+ printStatus(status);
6742
+ const state = observer.stateStatus.state;
6743
+ console.log(
6744
+ ` Observer: ${observer.healthy ? "running" : observer.running ? "unhealthy" : "stopped"}`
6626
6745
  );
6627
- }
6628
- console.log("[6/7] Removing direct MCP registrations...");
6629
- for (const target2 of removeFmlDirectMcp("all")) {
6630
- console.log(` Removed FML MCP from ${target2.displayName}`);
6631
- }
6632
- console.log("[7/7] Handling local data...");
6633
- let purgeComplete = true;
6634
- if (opts.purge) {
6635
- if (refuseUnsafePurge(purgeRoots)) {
6636
- purgeComplete = false;
6637
- } else {
6638
- for (const root of purgeRoots) {
6639
- if (refuseUnsafePurge([root])) {
6640
- purgeComplete = false;
6641
- continue;
6642
- }
6643
- const containsObserverDirectory = pathContains(
6644
- root.directory,
6645
- FML_OBSERVER_DIR
6746
+ if (state) {
6747
+ console.log(` PID: ${state.pid}`);
6748
+ console.log(` Phase: ${state.phase}`);
6749
+ if (state.lastOutcome) {
6750
+ console.log(` Last run: ${state.lastOutcome}`);
6751
+ }
6752
+ if (state.nextAttemptAtMs) {
6753
+ console.log(
6754
+ ` Next run: ${new Date(state.nextAttemptAtMs).toISOString()}`
6646
6755
  );
6647
- const customManagedRoot = !root.nativeDefault;
6648
- try {
6649
- const purged = purgeDirectoryPreservingFiles(root.directory, [
6650
- ...customManagedRoot ? [managedDirectoryMarker(root.directory)] : [],
6651
- ...containsObserverDirectory && hookCapturePaused ? [FML_HOOK_CAPTURE_DISABLED_FILE] : []
6652
- ]);
6653
- if (!purged) {
6654
- purgeComplete = false;
6655
- console.warn(
6656
- ` Preserved ${root.directory}: a symbolic link prevents a complete safe purge`
6657
- );
6658
- continue;
6659
- }
6660
- if (customManagedRoot && !containsObserverDirectory) {
6661
- finishCustomRootPurge(root.directory);
6662
- }
6663
- console.log(` Purged ${root.directory}`);
6664
- } catch (error) {
6665
- purgeComplete = false;
6666
- const message = error instanceof Error ? error.message : String(error);
6667
- console.warn(
6668
- ` Preserved ${root.directory}: the safe purge was incomplete: ${message}`
6669
- );
6670
- }
6671
6756
  }
6672
6757
  }
6673
- if (!hookCapturePaused) hookCapturePaused = tryDisableHookCapture();
6758
+ console.log(` Log: ${observer.logFile}`);
6759
+ }
6760
+ }
6761
+ async function handleSyncReset(opts = {}) {
6762
+ const result = rawSync().reset();
6763
+ if (opts.json) writeJson({ ok: !result.skipped, reset: result });
6764
+ else if (result.skipped) {
6765
+ console.error(
6766
+ "Raw artifact reset skipped: another sync owns the queue lock"
6767
+ );
6674
6768
  } else {
6675
6769
  console.log(
6676
- " Preserved auth, logs, raw-sync watermarks, and the bounded OTLP spool"
6770
+ `Reset raw upload watermarks for ${result.reset} artifact${result.reset === 1 ? "" : "s"}.`
6677
6771
  );
6678
- }
6679
- if (!hookCapturePaused) {
6680
- process.exitCode = 1;
6681
- console.warn(
6682
- "FML integrations were removed, but cached agent processes may keep invoking hooks until they exit."
6772
+ if (result.removed > 0) {
6773
+ console.log(`Removed ${result.removed} missing artifact entries.`);
6774
+ }
6775
+ console.log(
6776
+ "The next enabled sync will replay each artifact from byte zero."
6683
6777
  );
6684
6778
  }
6685
- if (opts.purge && !purgeComplete) {
6779
+ if (result.skipped) {
6686
6780
  process.exitCode = 1;
6687
- console.warn(
6688
- "FML integrations were removed, but local data remains because a purge target could not be removed safely. Repair the reported path and rerun `fml uninstall --purge`."
6689
- );
6781
+ } else {
6782
+ await wakeObserverSync();
6690
6783
  }
6691
- console.log("\nDone! FML has been uninstalled.");
6692
6784
  }
6693
6785
 
6694
- // src/commands/update.ts
6695
- async function handleUpdate() {
6696
- const currentVersion = true ? "0.8.0-rc.1+60c9b7e1" : "unknown";
6697
- console.log(`Current: ${currentVersion}`);
6698
- console.log("Updating via pnpm...\n");
6699
- try {
6700
- const pnpm = resolveBin("pnpm");
6701
- if (!pnpm) throw new Error("pnpm was not found on PATH");
6702
- const env = { ...process.env };
6703
- delete env.npm_config_registry;
6704
- execBinSync(pnpm, ["add", "--global", "@fml-inc/fml@latest"], {
6705
- timeout: 12e4,
6706
- env
6707
- });
6708
- console.log("\nUpdated. Run `fml install` to reconfigure.");
6709
- } catch (err) {
6710
- Sentry.captureException(err);
6711
- const msg = err instanceof Error ? err.stderr?.toString() ?? err.message : String(err);
6712
- console.error("Update failed:", msg);
6713
- process.exit(1);
6714
- }
6786
+ // src/commands/daemon.ts
6787
+ async function handleFmlStart() {
6788
+ await handleSyncEnable();
6715
6789
  }
6716
-
6717
- // src/auth/oauth.ts
6718
- import crypto from "crypto";
6719
- import http from "http";
6720
- async function fetchWorkosClientId(siteUrl) {
6721
- const response = await fetch(`${siteUrl}/api/auth/config`);
6722
- const data = await response.json();
6723
- if (!response.ok || data.ok === false || !data.workosClientId) {
6724
- throw new Error(data.error ?? `HTTP ${response.status}`);
6790
+ async function stopFml(opts = {}) {
6791
+ setRawSyncEnabled(false);
6792
+ const result = await stopObserver({ force: opts.force });
6793
+ let stopped = result.status === "stopped" || result.status === "killed" || result.status === "not_running" || result.status === "stale_state";
6794
+ if (stopped) {
6795
+ try {
6796
+ setHookCaptureDisabled(true);
6797
+ } catch (error) {
6798
+ const message = error instanceof Error ? error.message : String(error);
6799
+ console.error(
6800
+ `FML observer stopped, but the hook-capture pause could not be persisted: ${message}`
6801
+ );
6802
+ process.exitCode = 1;
6803
+ stopped = false;
6804
+ }
6725
6805
  }
6726
- return data.workosClientId;
6727
- }
6728
- async function canOpenBrowser() {
6729
- if (process.env.FML_DEVICE_FLOW === "1") return false;
6730
- if (process.env.FML_NO_BROWSER === "1") return false;
6806
+ switch (result.status) {
6807
+ case "stopped":
6808
+ console.log(`FML observer stopped (PID ${result.pid}).`);
6809
+ console.log("Agent activity sync is paused.");
6810
+ break;
6811
+ case "killed":
6812
+ console.log(`FML observer stopped (PID ${result.pid}).`);
6813
+ console.log("Agent activity sync is paused.");
6814
+ break;
6815
+ case "not_running":
6816
+ console.log("FML observer is already stopped.");
6817
+ console.log("Agent activity sync is paused.");
6818
+ break;
6819
+ case "stale_state":
6820
+ console.log(
6821
+ "FML observer is stopped. Old process state will be cleaned on the next start."
6822
+ );
6823
+ console.log("Agent activity sync is paused.");
6824
+ break;
6825
+ case "permission_denied":
6826
+ console.error(
6827
+ `Could not stop FML observer PID ${result.pid}: permission denied`
6828
+ );
6829
+ process.exitCode = 1;
6830
+ break;
6831
+ case "foreign_listener":
6832
+ console.error("Refused to stop a different local observer instance");
6833
+ process.exitCode = 1;
6834
+ break;
6835
+ case "ambiguous_live_process":
6836
+ console.error(
6837
+ `Refused to treat unverifiable live PID ${result.pid} as a stopped FML observer`
6838
+ );
6839
+ process.exitCode = 1;
6840
+ break;
6841
+ case "unresponsive":
6842
+ console.error(
6843
+ `Refused to signal unverifiable FML observer PID ${result.pid}; retry with \`fml stop --force\` to escalate`
6844
+ );
6845
+ process.exitCode = 1;
6846
+ break;
6847
+ case "signal_sent":
6848
+ console.error(`FML observer PID ${result.pid} did not stop`);
6849
+ process.exitCode = 1;
6850
+ break;
6851
+ case "force_stop_unverified":
6852
+ console.error(
6853
+ `Could not corroborate that PID ${result.pid} is the FML observer; refusing to signal it. If the observer is not running, remove ${result.stateFile}, ${result.lockFile}, ${result.databaseStateFile}, and ${result.databaseLockFile} manually.`
6854
+ );
6855
+ process.exitCode = 1;
6856
+ break;
6857
+ }
6858
+ return stopped;
6859
+ }
6860
+ async function handleFmlStop(opts = {}) {
6861
+ await stopFml(opts);
6862
+ }
6863
+ async function handleFmlRestart() {
6864
+ if (await stopFml()) {
6865
+ await handleFmlStart();
6866
+ }
6867
+ }
6868
+
6869
+ // src/auth/oauth.ts
6870
+ import crypto from "crypto";
6871
+ import http from "http";
6872
+ async function fetchWorkosClientId(siteUrl) {
6873
+ const response = await fetch(`${siteUrl}/api/auth/config`);
6874
+ const data = await response.json();
6875
+ if (!response.ok || data.ok === false || !data.workosClientId) {
6876
+ throw new Error(data.error ?? `HTTP ${response.status}`);
6877
+ }
6878
+ return data.workosClientId;
6879
+ }
6880
+ async function canOpenBrowser() {
6881
+ if (process.env.FML_DEVICE_FLOW === "1") return false;
6882
+ if (process.env.FML_NO_BROWSER === "1") return false;
6731
6883
  if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
6732
6884
  return false;
6733
6885
  }
@@ -6735,8 +6887,8 @@ async function canOpenBrowser() {
6735
6887
  if (process.env.CODESPACES) return false;
6736
6888
  if (process.env.REMOTE_CONTAINERS) return false;
6737
6889
  try {
6738
- const fs13 = await import("fs");
6739
- if (fs13.existsSync("/.dockerenv")) return false;
6890
+ const fs14 = await import("fs");
6891
+ if (fs14.existsSync("/.dockerenv")) return false;
6740
6892
  } catch {
6741
6893
  }
6742
6894
  return true;
@@ -7194,8 +7346,9 @@ async function runServiceTokenLogin(refreshToken) {
7194
7346
  }
7195
7347
  await selectOrg(environment);
7196
7348
  console.log("Logged in with FML service token.");
7197
- console.log("Queries and sync will use the stored service token.");
7198
- console.log("You're all set! Restart Claude Code to use FML tools.");
7349
+ console.log(
7350
+ "Queries and agent activity sync will use the stored service token."
7351
+ );
7199
7352
  }
7200
7353
  async function handleServiceTokenLogin(refreshToken) {
7201
7354
  try {
@@ -7212,16 +7365,12 @@ async function handleServiceTokenLogin(refreshToken) {
7212
7365
  process.exit(1);
7213
7366
  }
7214
7367
  }
7215
- async function handleLogin(opts) {
7216
- if (opts?.serviceToken) {
7217
- await handleServiceTokenLogin();
7218
- return;
7219
- }
7368
+ async function runInteractiveLogin(opts = {}) {
7220
7369
  const environment = captureLoginEnvironment();
7221
7370
  const envName = environment.envName;
7222
7371
  const existingToken = await getValidToken({ env: envName });
7223
7372
  if (existingToken) {
7224
- const { readTokens: readTokens2 } = await import("./token-store-R7WXEPLI.js");
7373
+ const { readTokens: readTokens2 } = await import("./token-store-BT5THIAJ.js");
7225
7374
  const stored = readTokens2(envName);
7226
7375
  if (stored?.tokenType === "service") {
7227
7376
  const label = stored.user.id === SERVICE_TOKEN_LOGIN_USER_ID ? "a service token" : `${stored.user.name} (${stored.user.email})`;
@@ -7233,23 +7382,27 @@ async function handleLogin(opts) {
7233
7382
  }
7234
7383
  await linkGitHubIdentity(environment);
7235
7384
  await selectOrg(environment);
7236
- console.log("You're all set! Restart Claude Code to use FML tools.");
7237
- process.exit(0);
7385
+ return;
7238
7386
  }
7239
7387
  console.log(`Signing in to FML (${envName})...`);
7388
+ const useDeviceFlow = opts.device || !await canOpenBrowser();
7389
+ let result;
7390
+ if (useDeviceFlow) {
7391
+ result = await deviceLogin(environment);
7392
+ } else {
7393
+ result = await login(environment);
7394
+ }
7395
+ console.log(`Logged in as ${result.name} (${result.email})`);
7396
+ await linkGitHubIdentity(environment);
7397
+ await selectOrg(environment);
7398
+ }
7399
+ async function handleLogin(opts) {
7400
+ if (opts?.serviceToken) {
7401
+ await handleServiceTokenLogin();
7402
+ return;
7403
+ }
7240
7404
  try {
7241
- const useDeviceFlow = opts?.device || !await canOpenBrowser();
7242
- let result;
7243
- if (useDeviceFlow) {
7244
- result = await deviceLogin(environment);
7245
- } else {
7246
- result = await login(environment);
7247
- }
7248
- console.log(`
7249
- Logged in as ${result.name} (${result.email})`);
7250
- await linkGitHubIdentity(environment);
7251
- await selectOrg(environment);
7252
- console.log("You're all set! Restart Claude Code to use FML tools.");
7405
+ await runInteractiveLogin({ device: opts?.device });
7253
7406
  process.exit(0);
7254
7407
  } catch (err) {
7255
7408
  Sentry.captureException(err);
@@ -7259,742 +7412,923 @@ Logged in as ${result.name} (${result.email})`);
7259
7412
  }
7260
7413
  }
7261
7414
 
7262
- // src/commands/logout.ts
7263
- async function handleLogout() {
7264
- const { name } = getActiveEnv();
7265
- await clearStoredCredentials(name);
7266
- console.log(`Logged out of "${name}". Stored credentials cleared.`);
7415
+ // src/commands/setup.ts
7416
+ var defaultDependencies = {
7417
+ install: () => handleInstall(),
7418
+ getActiveToken: () => getValidToken({ env: getActiveEnv().name }),
7419
+ login: () => runInteractiveLogin(),
7420
+ stop: () => handleFmlStop(),
7421
+ start: () => handleFmlStart()
7422
+ };
7423
+ async function runStep(name, operation) {
7424
+ const priorExitCode = process.exitCode;
7425
+ await operation();
7426
+ if (process.exitCode && process.exitCode !== priorExitCode) {
7427
+ throw new Error(`FML setup could not complete ${name}.`);
7428
+ }
7429
+ }
7430
+ async function handleSetup(dependencies = defaultDependencies) {
7431
+ if (process.exitCode) {
7432
+ throw new Error("FML setup cannot run after an earlier command failure.");
7433
+ }
7434
+ console.log("Setting up FML...");
7435
+ await runStep("integration installation", dependencies.install);
7436
+ const token = await dependencies.getActiveToken();
7437
+ if (token) {
7438
+ console.log("Already signed in.");
7439
+ } else {
7440
+ await runStep("login", dependencies.login);
7441
+ }
7442
+ await runStep("observer stop", dependencies.stop);
7443
+ await runStep("observer start", dependencies.start);
7444
+ console.log("FML is ready.");
7445
+ console.log("Start a new agent session to load FML tools.");
7446
+ console.log("Run `fml doctor` to verify setup.");
7267
7447
  }
7268
7448
 
7269
- // src/commands/org.ts
7270
- async function handleOrg(slug) {
7271
- const environment = getActiveEnv();
7272
- const token = await getValidToken({ env: environment.name });
7273
- if (!token) {
7274
- console.error("Not logged in. Run `fml login` first.");
7275
- process.exit(1);
7449
+ // src/commands/uninstall.ts
7450
+ import fs13 from "fs";
7451
+ import os7 from "os";
7452
+ import path28 from "path";
7453
+ import readline from "readline/promises";
7454
+ var PURGE_WARNING = "This deletes FML credentials for ALL environments, logs, sync state, and every pending best-effort OTLP envelope.";
7455
+ function tryDisableHookCapture() {
7456
+ try {
7457
+ setHookCaptureDisabled(true);
7458
+ return true;
7459
+ } catch (error) {
7460
+ const message = error instanceof Error ? error.message : String(error);
7461
+ console.warn(
7462
+ `Warning: Could not persist the hook-capture pause at ${FML_HOOK_CAPTURE_DISABLED_FILE}: ${message}`
7463
+ );
7464
+ return false;
7276
7465
  }
7277
- const api = createFmlClient(token, {
7278
- envName: environment.name,
7279
- siteUrl: getSiteUrlForConvexUrl(environment.convexUrl)
7280
- });
7281
- const orgs = await api.queryOrgs();
7282
- if (orgs.length === 0) {
7283
- console.error("No organizations found.");
7284
- process.exit(1);
7466
+ }
7467
+ function resolvePreservedPaths(preservedFile) {
7468
+ const logicalPath = path28.resolve(preservedFile);
7469
+ try {
7470
+ const canonicalPath = fs13.realpathSync(preservedFile);
7471
+ return pathsAreSame(canonicalPath, logicalPath) ? [logicalPath] : [logicalPath, canonicalPath];
7472
+ } catch {
7473
+ return [logicalPath];
7285
7474
  }
7286
- const current = getSelectedOrg(environment.name);
7287
- const currentId = getSelectedOrgId(environment.name);
7288
- if (!slug) {
7289
- console.log("Organizations:\n");
7290
- for (const org of orgs) {
7291
- const orgSlug = org.slug ?? org.name;
7292
- const marker = org._id === currentId || !currentId && orgSlug === current ? " (selected)" : "";
7293
- console.log(` ${orgSlug}${marker}`);
7475
+ }
7476
+ function purgeDirectoryPreservingPaths(directory, preservedPaths, configuredRoot = false) {
7477
+ const resolvedDirectory = path28.resolve(directory);
7478
+ let directoryStats;
7479
+ try {
7480
+ directoryStats = fs13.lstatSync(directory);
7481
+ } catch (error) {
7482
+ if (error.code === "ENOENT") return true;
7483
+ throw error;
7484
+ }
7485
+ if (directoryStats.isSymbolicLink()) {
7486
+ if (configuredRoot) return false;
7487
+ const containsLogicalMarker = preservedPaths.some(
7488
+ (preservedPath) => pathContains(resolvedDirectory, preservedPath)
7489
+ );
7490
+ if (!containsLogicalMarker) {
7491
+ fs13.rmSync(directory, { force: true });
7492
+ return true;
7294
7493
  }
7295
- if (!currentId && !current) {
7296
- console.log("\nNo org selected. Run `fml org <slug>` to select one.");
7494
+ return false;
7495
+ }
7496
+ const directoryPaths = [resolvedDirectory];
7497
+ try {
7498
+ const canonicalDirectory = fs13.realpathSync(directory);
7499
+ if (!pathsAreSame(canonicalDirectory, resolvedDirectory)) {
7500
+ directoryPaths.push(canonicalDirectory);
7297
7501
  }
7298
- return;
7502
+ } catch {
7299
7503
  }
7300
- const match = orgs.find(
7301
- (o) => (o.slug ?? o.name) === slug || o.name === slug
7504
+ const containsPreservedPath = directoryPaths.some(
7505
+ (directoryPath) => preservedPaths.some(
7506
+ (preservedPath) => pathContains(directoryPath, preservedPath)
7507
+ )
7302
7508
  );
7303
- if (!match) {
7304
- console.error(
7305
- `Organization "${slug}" not found. Available: ${orgs.map((o) => o.slug ?? o.name).join(", ")}`
7306
- );
7307
- process.exit(1);
7509
+ if (!containsPreservedPath) {
7510
+ fs13.rmSync(directory, { recursive: true, force: true });
7511
+ return true;
7308
7512
  }
7309
- const matchSlug = match.slug ?? match.name;
7310
- await setSelectedOrg(matchSlug, match._id, environment.name);
7311
- console.log(`Selected org: ${match.name} (${matchSlug})`);
7513
+ const isPreservedPath = directoryPaths.some(
7514
+ (directoryPath) => preservedPaths.some(
7515
+ (preservedPath) => pathsAreSame(directoryPath, path28.resolve(preservedPath))
7516
+ )
7517
+ );
7518
+ if (isPreservedPath) return true;
7519
+ if (!directoryStats.isDirectory()) return false;
7520
+ let entries;
7521
+ try {
7522
+ entries = fs13.readdirSync(directory);
7523
+ } catch (error) {
7524
+ if (error.code === "ENOENT") return true;
7525
+ throw error;
7526
+ }
7527
+ let complete = true;
7528
+ for (const entry of entries) {
7529
+ const entryPath = path28.join(directory, entry);
7530
+ if (!purgeDirectoryPreservingPaths(entryPath, preservedPaths, false)) {
7531
+ complete = false;
7532
+ }
7533
+ }
7534
+ return complete;
7312
7535
  }
7313
-
7314
- // src/commands/open.ts
7315
- var DEFAULT_APP_BASE_URL = "https://app.fml.app";
7316
- function appBaseUrl() {
7317
- const configured = process.env.FML_APP_URL?.trim();
7318
- return (configured || DEFAULT_APP_BASE_URL).replace(/\/+$/, "");
7536
+ function purgeDirectoryPreservingFiles(directory, preservedFiles) {
7537
+ if (hasSymlinkedParentBelowTrustedAnchor(directory)) return false;
7538
+ return purgeDirectoryPreservingPaths(
7539
+ directory,
7540
+ preservedFiles.flatMap(
7541
+ (preservedFile) => resolvePreservedPaths(preservedFile)
7542
+ ),
7543
+ true
7544
+ );
7319
7545
  }
7320
- async function handleOpen(opts) {
7321
- const environment = getActiveEnv();
7322
- const api = await getAuthenticatedClient(environment);
7323
- if (!api) {
7324
- console.error("Not authenticated. Run `fml login` first.");
7325
- process.exit(1);
7326
- }
7327
- let slug;
7546
+ function finishCustomRootPurge(directory) {
7547
+ const marker = managedDirectoryMarker(directory);
7548
+ fs13.rmSync(marker, { force: true });
7328
7549
  try {
7329
- const orgs = await api.queryOrgs();
7330
- if (orgs.length === 0) {
7331
- console.error("No organizations found.");
7332
- process.exit(1);
7550
+ fs13.rmdirSync(directory);
7551
+ } catch (error) {
7552
+ if (error.code === "ENOENT") return;
7553
+ try {
7554
+ fs13.writeFileSync(marker, FML_MANAGED_DIRECTORY_MARKER_CONTENT, {
7555
+ encoding: "utf8",
7556
+ flag: "wx",
7557
+ mode: 384
7558
+ });
7559
+ } catch (markerError) {
7560
+ if (markerError.code === "EEXIST" && hasManagedDirectoryMarker(directory)) {
7561
+ throw error;
7562
+ }
7563
+ const removalMessage = error instanceof Error ? error.message : String(error);
7564
+ const markerMessage = markerError instanceof Error ? markerError.message : String(markerError);
7565
+ throw new Error(
7566
+ `could not remove ${directory} (${removalMessage}) or restore its FML ownership marker (${markerMessage})`
7567
+ );
7333
7568
  }
7334
- const selected = getSelectedOrg(environment.name);
7335
- const org = selected && orgs.find((candidate) => {
7336
- const candidateSlug = candidate.slug ?? candidate.name;
7337
- return candidateSlug === selected || candidate.name === selected;
7338
- }) || orgs[0];
7339
- slug = org.slug ?? org.name;
7340
- } catch {
7341
- console.error("Could not fetch organizations. Opening base URL.");
7342
- slug = "";
7343
- }
7344
- const baseUrl = appBaseUrl();
7345
- const url = slug ? `${baseUrl}/${slug}` : baseUrl;
7346
- if (opts.json) {
7347
- console.log(JSON.stringify({ url }));
7348
- } else {
7349
- const openBrowser = (await import("open")).default;
7350
- await openBrowser(url);
7569
+ throw error;
7351
7570
  }
7352
7571
  }
7353
-
7354
- // src/commands/status.ts
7355
- function formatBytes(bytes) {
7356
- if (bytes < 1024) return `${bytes} B`;
7357
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
7358
- if (bytes < 1024 * 1024 * 1024) {
7359
- return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
7360
- }
7361
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
7362
- }
7363
- function hookSpoolStatus(value) {
7364
- if (!value || typeof value !== "object" || Array.isArray(value)) {
7365
- return void 0;
7366
- }
7367
- const record = value;
7368
- const nonNegativeInteger = (field) => {
7369
- const candidate = record[field];
7370
- return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0 ? candidate : void 0;
7371
- };
7372
- const logicalBytes = nonNegativeInteger("logicalBytes");
7373
- const evictedRecords = nonNegativeInteger("evictedRecords");
7374
- const evictedBytes = nonNegativeInteger("evictedBytes");
7375
- const evictedPendingRecords = nonNegativeInteger("evictedPendingRecords");
7376
- const evictedQuarantinedRecords = nonNegativeInteger(
7377
- "evictedQuarantinedRecords"
7572
+ function cleanInstalledPluginScopes(paths) {
7573
+ const installedPlugins = readJsonFile(paths.installedPluginsPath);
7574
+ if (!installedPlugins) return;
7575
+ const update = CLAUDE_PLUGIN_INSTALLER.removeInstalledPlugin(
7576
+ installedPlugins,
7577
+ CLAUDE_PLUGIN_INSTALLER.pluginId
7378
7578
  );
7379
- const maxBytes = record.maxBytes;
7380
- const quarantineMaxAgeMs = record.quarantineMaxAgeMs;
7381
- if (record.unit !== "logical_bytes" || logicalBytes === void 0 || evictedRecords === void 0 || evictedBytes === void 0 || evictedPendingRecords === void 0 || evictedQuarantinedRecords === void 0 || maxBytes !== null && (typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes < 1)) {
7382
- return void 0;
7579
+ if (!update.changed) return;
7580
+ for (const settingsPath2 of CLAUDE_PLUGIN_INSTALLER.projectSettingsPaths(
7581
+ installedPlugins,
7582
+ CLAUDE_PLUGIN_INSTALLER.pluginId
7583
+ )) {
7584
+ removeEnabledPluginFromSettings(
7585
+ settingsPath2,
7586
+ CLAUDE_PLUGIN_INSTALLER.pluginId
7587
+ );
7383
7588
  }
7384
- const lastEvictedAtMsValue = nonNegativeInteger("lastEvictedAtMs");
7385
- const lastEvictedAtMs = lastEvictedAtMsValue !== void 0 && lastEvictedAtMsValue <= 864e13 ? lastEvictedAtMsValue : void 0;
7386
- return {
7387
- logicalBytes,
7388
- maxBytes,
7389
- ...quarantineMaxAgeMs === null ? { quarantineMaxAgeMs: null } : typeof quarantineMaxAgeMs === "number" && Number.isSafeInteger(quarantineMaxAgeMs) && quarantineMaxAgeMs > 0 ? { quarantineMaxAgeMs } : {},
7390
- evictedRecords,
7391
- evictedBytes,
7392
- evictedPendingRecords,
7393
- evictedQuarantinedRecords,
7394
- ...lastEvictedAtMs === void 0 ? {} : { lastEvictedAtMs }
7395
- };
7589
+ writeJsonFile(paths.installedPluginsPath, update.installedPlugins);
7396
7590
  }
7397
- function otlpSpoolStatus(value) {
7398
- if (!value || typeof value !== "object" || Array.isArray(value)) {
7399
- return void 0;
7591
+ function purgeWarning(roots) {
7592
+ return `${PURGE_WARNING}
7593
+ Purge roots:
7594
+ ${roots.map((root) => ` - ${root.directory}`).join("\n")}`;
7595
+ }
7596
+ async function confirmPurge(opts, roots) {
7597
+ const warning = purgeWarning(roots);
7598
+ if (opts.assumeYes) {
7599
+ console.log(warning);
7600
+ return true;
7400
7601
  }
7401
- const record = value;
7402
- const nonNegativeInteger = (field) => {
7403
- const candidate = record[field];
7404
- return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0 ? candidate : void 0;
7405
- };
7406
- const logicalBytes = nonNegativeInteger("logicalBytes");
7407
- const bodyBytes = nonNegativeInteger("bodyBytes");
7408
- const maxBytes = nonNegativeInteger("maxBytes");
7409
- const droppedRecords = nonNegativeInteger("droppedRecords");
7410
- const droppedLogicalBytes = nonNegativeInteger("droppedLogicalBytes");
7411
- const droppedBodyBytes = nonNegativeInteger("droppedBodyBytes");
7412
- if (record.unit !== "logical_bytes" || logicalBytes === void 0 || bodyBytes === void 0 || maxBytes === void 0 || maxBytes < 1 || droppedRecords === void 0 || droppedLogicalBytes === void 0 || droppedBodyBytes === void 0 || bodyBytes > logicalBytes || droppedBodyBytes > droppedLogicalBytes) {
7413
- return void 0;
7602
+ const input = opts.input ?? process.stdin;
7603
+ if (input.isTTY !== true) {
7604
+ console.error(
7605
+ `${warning}
7606
+ Re-run with --yes to confirm in a non-interactive session.`
7607
+ );
7608
+ return false;
7414
7609
  }
7415
- return {
7416
- logicalBytes,
7417
- bodyBytes,
7418
- maxBytes,
7419
- droppedRecords,
7420
- droppedLogicalBytes,
7421
- droppedBodyBytes,
7422
- ...typeof record.lastDroppedAtMs === "number" && Number.isSafeInteger(record.lastDroppedAtMs) && record.lastDroppedAtMs >= 0 && Number.isFinite(new Date(record.lastDroppedAtMs).getTime()) ? { lastDroppedAtMs: record.lastDroppedAtMs } : {},
7423
- ...typeof record.lastDropReason === "string" ? { lastDropReason: record.lastDropReason } : {}
7424
- };
7425
- }
7426
- function observerQueueStatus(value, key) {
7427
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
7428
- const queue = value[key];
7429
- if (!queue || typeof queue !== "object" || Array.isArray(queue)) return null;
7430
- const record = queue;
7431
- if (typeof record.phase !== "string" || typeof record.pending !== "number" || !Number.isSafeInteger(record.pending) || record.pending < 0) {
7432
- return null;
7610
+ const rl = readline.createInterface({
7611
+ input,
7612
+ output: opts.output ?? process.stdout
7613
+ });
7614
+ try {
7615
+ const closed = new Promise((resolve) => {
7616
+ rl.once("close", () => resolve(null));
7617
+ });
7618
+ const answer = await Promise.race([
7619
+ rl.question(`${warning}
7620
+ Type y to continue: `).catch(() => null),
7621
+ closed
7622
+ ]);
7623
+ if (answer !== null && answer.trim().toLowerCase() === "y") return true;
7624
+ } finally {
7625
+ rl.close();
7433
7626
  }
7434
- return {
7435
- phase: record.phase,
7436
- pending: record.pending,
7437
- ...typeof record.quarantined === "number" && Number.isSafeInteger(record.quarantined) && record.quarantined >= 0 ? { quarantined: record.quarantined } : {},
7438
- ...typeof record.lastError === "string" ? { lastError: record.lastError } : {},
7439
- ...typeof record.lastWarning === "string" ? { lastWarning: record.lastWarning } : {},
7440
- ...typeof record.nextAttemptAtMs === "number" && Number.isSafeInteger(record.nextAttemptAtMs) && record.nextAttemptAtMs >= 0 && Number.isFinite(new Date(record.nextAttemptAtMs).getTime()) ? { nextAttemptAtMs: record.nextAttemptAtMs } : {}
7441
- };
7627
+ console.error("Uninstall canceled; nothing was removed.");
7628
+ return false;
7442
7629
  }
7443
- function hookStatus(value) {
7444
- const status = observerQueueStatus(value, "hooks");
7445
- if (!status || !value || typeof value !== "object" || Array.isArray(value)) {
7446
- return null;
7630
+ function refuseUnsafePurge(roots) {
7631
+ const unsafeRoots = unsafePurgeRoots(roots);
7632
+ if (unsafeRoots.length === 0) return false;
7633
+ console.error("Refusing to purge unsafe FML directories:");
7634
+ for (const root of unsafeRoots) {
7635
+ console.error(` - ${root.directory}: ${root.reason}`);
7447
7636
  }
7448
- const hooks = value.hooks;
7449
- if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return null;
7450
- const record = hooks;
7451
- const spool = hookSpoolStatus(record.spool);
7452
- return {
7453
- ...status,
7454
- ...typeof record.rejected === "number" && Number.isSafeInteger(record.rejected) && record.rejected >= 0 ? { rejected: record.rejected } : {},
7455
- ...typeof record.lastRejectedReason === "string" ? { lastRejectedReason: record.lastRejectedReason } : {},
7456
- ...typeof record.lastRejectedTarget === "string" ? { lastRejectedTarget: record.lastRejectedTarget } : {},
7457
- ...spool === void 0 ? {} : { spool }
7458
- };
7637
+ console.error(
7638
+ "Inspect and remove those roots manually, or reconfigure FML to fresh dedicated empty data, state, log, and observer directories and run `fml install` before retrying."
7639
+ );
7640
+ process.exitCode = 1;
7641
+ return true;
7459
7642
  }
7460
- function otlpStatus(value) {
7461
- const status = observerQueueStatus(value, "otlp");
7462
- if (!status || !value || typeof value !== "object" || Array.isArray(value)) {
7463
- return null;
7643
+ async function handleUninstall(opts) {
7644
+ const purgeRoots = opts.purge ? configuredManagedRoots() : [];
7645
+ if (opts.purge) {
7646
+ if (refuseUnsafePurge(purgeRoots)) return;
7647
+ if (!await confirmPurge(opts, purgeRoots)) {
7648
+ process.exitCode = 1;
7649
+ return;
7650
+ }
7651
+ for (const root of purgeRoots) {
7652
+ if (!fs13.existsSync(root.directory) && pathContains(root.directory, FML_OBSERVER_DIR)) {
7653
+ prepareManagedDirectory(root);
7654
+ }
7655
+ }
7656
+ if (refuseUnsafePurge(purgeRoots)) return;
7464
7657
  }
7465
- const otlp = value.otlp;
7466
- if (!otlp || typeof otlp !== "object" || Array.isArray(otlp)) return null;
7467
- const spool = otlpSpoolStatus(otlp.spool);
7468
- return {
7469
- ...status,
7470
- ...spool === void 0 ? {} : { spool }
7471
- };
7472
- }
7473
- async function handleStatus() {
7474
- const { name, convexUrl } = getActiveEnv();
7475
- console.log(`Environment: ${name}`);
7476
- console.log(` Convex: ${convexUrl ?? "not configured"}
7477
- `);
7478
- const tokens = readTokens(name);
7479
- if (!tokens) {
7480
- console.log("Authentication:");
7481
- console.log(" Not authenticated. Run `fml login` to sign in.");
7482
- } else {
7483
- const tokenValid = await getValidToken({ env: name });
7484
- console.log("Authentication:");
7485
- if (tokens.tokenType === "service" && tokens.user.id === SERVICE_TOKEN_LOGIN_USER_ID) {
7486
- console.log(" Auth: service token");
7487
- } else {
7488
- console.log(` User: ${tokens.user.name} (${tokens.user.email})`);
7658
+ console.log("Removing FML integrations...\n");
7659
+ const otelDestination = readLocalObserverOtelDestination({
7660
+ endpointFile: FML_OBSERVER_ENDPOINT_FILE,
7661
+ authTokenFile: FML_OBSERVER_AUTH_TOKEN_FILE
7662
+ });
7663
+ let observer;
7664
+ try {
7665
+ observer = await stopObserver();
7666
+ } catch (error) {
7667
+ console.error(error instanceof Error ? error.message : String(error));
7668
+ console.error(
7669
+ "Could not safely stop the FML observer; uninstall was aborted. Repair the reported files and rerun `fml uninstall`."
7670
+ );
7671
+ process.exitCode = 1;
7672
+ return;
7673
+ }
7674
+ if (observer.status === "permission_denied" || observer.status === "foreign_listener" || observer.status === "ambiguous_live_process" || observer.status === "unresponsive" || observer.status === "signal_sent") {
7675
+ console.error(
7676
+ `Could not safely stop FML observer PID ${observer.pid}; uninstall was aborted.`
7677
+ );
7678
+ process.exitCode = 1;
7679
+ return;
7680
+ }
7681
+ let hookCapturePaused = tryDisableHookCapture();
7682
+ createRawArtifactSync({ stateDir: FML_OBSERVER_DIR }).setEnabled(false);
7683
+ const paths = CLAUDE_PLUGIN_INSTALLER.resolvePaths({
7684
+ env: process.env,
7685
+ homeDir: os7.homedir()
7686
+ });
7687
+ const claudeBin = resolveBin("claude");
7688
+ if (claudeBin) {
7689
+ try {
7690
+ const command = CLAUDE_PLUGIN_INSTALLER.uninstallCommand;
7691
+ execBinSync(claudeBin, [...command.args], {
7692
+ timeout: command.timeoutMs
7693
+ });
7694
+ } catch {
7489
7695
  }
7490
- console.log(
7491
- ` Token: ${tokenValid ? "valid" : "expired (needs refresh/re-login)"}`
7696
+ }
7697
+ cleanInstalledPluginScopes(paths);
7698
+ const settings = readJsonFile(paths.settingsPath);
7699
+ if (settings) {
7700
+ const marketplace2 = readJsonFile(paths.marketplaceManifestPath);
7701
+ const keepLocalMarketplace = CLAUDE_PLUGIN_INSTALLER.hasOtherLocalMarketplacePlugins(
7702
+ settings,
7703
+ marketplace2
7492
7704
  );
7493
- console.log(
7494
- ` Org: ${tokens.orgSlug ?? "(none \u2014 run `fml org <slug>`)"}`
7705
+ writeJsonFile(
7706
+ paths.settingsPath,
7707
+ CLAUDE_PLUGIN_INSTALLER.disableInSettings(settings, keepLocalMarketplace)
7495
7708
  );
7496
7709
  }
7497
- const syncStatus = createRawArtifactSync({
7498
- stateDir: FML_OBSERVER_DIR
7499
- }).status();
7500
- console.log("\nRaw artifact sync:");
7501
- console.log(` Enabled: ${syncStatus.settings.enabled ? "yes" : "no"}`);
7502
- console.log(` Tracked: ${syncStatus.queue.tracked}`);
7503
- console.log(` Pending: ${syncStatus.queue.pending}`);
7504
- console.log(` Raw work: ${syncStatus.queue.rawPending}`);
7505
- console.log(` Metadata: ${syncStatus.queue.metadataPending}`);
7506
- console.log(` Uploaded: ${syncStatus.queue.uploaded}`);
7507
- console.log(` Failed: ${syncStatus.queue.failed}`);
7508
- console.log(
7509
- ` Watermark: ${formatBytes(syncStatus.queue.uploadedBytes)} / ${formatBytes(syncStatus.queue.totalBytes)}`
7510
- );
7511
- console.log(` Remaining: ${formatBytes(syncStatus.queue.remainingBytes)}`);
7512
- const observer = await readObserverRuntimeStatus();
7513
- const protectedStatus = observer.healthy ? await readObserverProtectedStatus() : null;
7514
- const observerState = observer.stateStatus.state;
7515
- console.log("\nLocal observer:");
7516
- console.log(
7517
- ` Status: ${observer.healthy ? "running" : observer.running ? "unhealthy" : "stopped"}`
7518
- );
7519
- if (observerState) {
7520
- console.log(` PID: ${observerState.pid}`);
7521
- console.log(` Phase: ${observerState.phase}`);
7522
- if (observerState.lastOutcome) {
7523
- console.log(` Last: ${observerState.lastOutcome}`);
7710
+ let skillsRemoved = true;
7711
+ try {
7712
+ for (const result of removeManagedAgentSkills(resolvePackageRoot())) {
7713
+ if (result.status !== "preserved" && !result.reason) continue;
7714
+ skillsRemoved = false;
7715
+ const targets = result.displayNames.join(", ");
7716
+ console.warn(
7717
+ `Warning: Preserved ${result.destinationPath} for ${targets}: ${result.reason ?? "cleanup could not be verified"}`
7718
+ );
7524
7719
  }
7720
+ } catch (error) {
7721
+ skillsRemoved = false;
7722
+ const message = error instanceof Error ? error.message : String(error);
7723
+ console.warn(
7724
+ `Warning: Agent skill cleanup could not be completed: ${message}`
7725
+ );
7525
7726
  }
7526
- console.log(` Log: ${observer.logFile}`);
7527
- const hooks = hookStatus(protectedStatus);
7528
- const hookInbox = readHookInboxStatus();
7529
- console.log("\nHook capture:");
7530
- if (!hooks) {
7531
- console.log(` Status: ${observer.healthy ? "unavailable" : "stopped"}`);
7532
- } else {
7533
- console.log(` Status: ${hooks.phase}`);
7534
- console.log(` Pending: ${hooks.pending}`);
7535
- if (hooks.quarantined && hooks.quarantined > 0) {
7536
- console.log(` Quarantined: ${hooks.quarantined}`);
7537
- }
7538
- if (hooks.rejected && hooks.rejected > 0) {
7539
- console.log(` Rejected: ${hooks.rejected}`);
7540
- if (hooks.lastRejectedReason) {
7541
- console.log(
7542
- ` Last rejection: ${hooks.lastRejectedReason}${hooks.lastRejectedTarget ? ` (${hooks.lastRejectedTarget})` : ""}`
7543
- );
7544
- }
7727
+ let integrationsRemoved = true;
7728
+ try {
7729
+ const piPaths = PI_EXTENSION_INSTALLER.resolvePaths({
7730
+ env: process.env,
7731
+ homeDir: os7.homedir(),
7732
+ packageRoot: resolvePackageRoot(),
7733
+ platform: process.platform
7734
+ });
7735
+ const piResult = PI_EXTENSION_INSTALLER.uninstall(piPaths);
7736
+ if (piResult.status === "preserved") {
7737
+ integrationsRemoved = false;
7738
+ process.exitCode = 1;
7739
+ console.warn(
7740
+ `Warning: Preserved ${piResult.destinationPath} for Pi: ${piResult.reason}`
7741
+ );
7545
7742
  }
7546
- if (hooks.spool) {
7547
- console.log(
7548
- ` Spool: ${formatBytes(hooks.spool.logicalBytes)} / ${hooks.spool.maxBytes === null ? "unbounded" : formatBytes(hooks.spool.maxBytes)} (logical content)`
7743
+ if (piResult.legacyRemoved) {
7744
+ console.log("Removed the legacy Panopticon Pi extension");
7745
+ } else if (piResult.legacyPreserved) {
7746
+ integrationsRemoved = false;
7747
+ process.exitCode = 1;
7748
+ console.warn(
7749
+ `Warning: Preserved unrecognized Pi extension ${piPaths.legacyDestinationPath}; inspect and remove it if it belongs to Panopticon`
7549
7750
  );
7550
- if (hooks.spool.quarantineMaxAgeMs !== void 0) {
7551
- console.log(
7552
- ` Quarantine retention: ${hooks.spool.quarantineMaxAgeMs === null ? "unbounded" : `${Math.round(hooks.spool.quarantineMaxAgeMs / 864e5)} day(s)`}`
7553
- );
7554
- }
7555
- if (hooks.spool.evictedRecords > 0) {
7556
- console.log(
7557
- ` Evicted: ${hooks.spool.evictedRecords} record(s), ${formatBytes(hooks.spool.evictedBytes)} (${hooks.spool.evictedQuarantinedRecords} quarantined, ${hooks.spool.evictedPendingRecords} pending)`
7558
- );
7559
- if (hooks.spool.lastEvictedAtMs !== void 0) {
7560
- console.log(
7561
- ` Last eviction: ${new Date(hooks.spool.lastEvictedAtMs).toISOString()}`
7562
- );
7563
- }
7564
- }
7565
7751
  }
7566
- if (hooks.lastError) console.log(` Error: ${hooks.lastError}`);
7752
+ } catch (error) {
7753
+ integrationsRemoved = false;
7754
+ process.exitCode = 1;
7755
+ const message = error instanceof Error ? error.message : String(error);
7756
+ console.warn(
7757
+ `Warning: Pi extension cleanup could not be completed: ${message}`
7758
+ );
7567
7759
  }
7568
- if (hookInbox.unreadable) {
7569
- console.log(" Inbox: unavailable");
7570
- } else {
7571
- console.log(
7572
- ` Inbox: ${hookInbox.pendingEntries} pending (${formatBytes(hookInbox.pendingBytes)})`
7760
+ if (removeMarketplaceLink(paths.marketplaceLinkPath) === "preserved") {
7761
+ integrationsRemoved = false;
7762
+ console.warn(
7763
+ `Warning: Preserved non-symlink marketplace path ${paths.marketplaceLinkPath}`
7573
7764
  );
7574
- console.log(
7575
- ` Inbox limits: ${hookInbox.maxBytes === null ? "unbounded bytes" : formatBytes(hookInbox.maxBytes)}, ${hookInbox.maxEntries === null ? "unbounded entries" : `${hookInbox.maxEntries} entries`}`
7765
+ }
7766
+ const marketplace = readJsonFile(paths.marketplaceManifestPath);
7767
+ if (marketplace && Array.isArray(marketplace.plugins)) {
7768
+ writeJsonFile(
7769
+ paths.marketplaceManifestPath,
7770
+ CLAUDE_PLUGIN_INSTALLER.removeMarketplaceEntry(
7771
+ marketplace,
7772
+ CLAUDE_PLUGIN_INSTALLER.pluginName
7773
+ )
7576
7774
  );
7577
- if (hookInbox.capacityRejectedEntries > 0) {
7578
- console.log(
7579
- ` Inbox capacity refusals: ${hookInbox.capacityRejectedEntries} record(s), ${formatBytes(hookInbox.capacityRejectedBytes)}`
7580
- );
7581
- if (hookInbox.lastCapacityRejectedAtMs !== void 0) {
7582
- console.log(
7583
- ` Last inbox refusal: ${new Date(hookInbox.lastCapacityRejectedAtMs).toISOString()}${hookInbox.lastCapacityRejectionReason ? ` (${hookInbox.lastCapacityRejectionReason})` : ""}`
7584
- );
7585
- }
7586
- }
7587
- if (hookInbox.retiredEntries > 0) {
7588
- console.log(
7589
- ` Inbox retired: ${hookInbox.retiredEntries} record(s), ${formatBytes(hookInbox.retiredBytes)}`
7775
+ }
7776
+ fs13.rmSync(paths.pluginCacheDir, { recursive: true, force: true });
7777
+ const reportIntegrationWarning = (message) => {
7778
+ integrationsRemoved = false;
7779
+ console.warn(message.replace(/^warn:/u, "Warning:"));
7780
+ };
7781
+ let hookRemovalComplete = true;
7782
+ removeFmlHooks({
7783
+ onFailure: (target2) => {
7784
+ hookRemovalComplete = false;
7785
+ reportIntegrationWarning(
7786
+ `Warning: Preserved FML hook registration at ${target2.configPath}: ${target2.reason}`
7590
7787
  );
7591
7788
  }
7789
+ });
7790
+ if (!hookRemovalComplete) {
7791
+ integrationsRemoved = false;
7792
+ process.exitCode = 1;
7793
+ console.warn(
7794
+ "Warning: Some FML hook registrations were preserved; repair the paths reported above and rerun `fml uninstall`."
7795
+ );
7592
7796
  }
7593
- const otlp = otlpStatus(protectedStatus);
7594
- console.log("\nNative OpenTelemetry:");
7595
- if (!otlp) {
7596
- console.log(` Status: ${observer.healthy ? "unavailable" : "stopped"}`);
7597
- } else {
7598
- console.log(` Status: ${otlp.phase}`);
7599
- console.log(` Pending: ${otlp.pending}`);
7600
- if (otlp.spool) {
7601
- const usedPercent = otlp.spool.logicalBytes / otlp.spool.maxBytes * 100;
7602
- console.log(
7603
- ` Spool: ${formatBytes(otlp.spool.logicalBytes)} / ${formatBytes(otlp.spool.maxBytes)} (${usedPercent.toFixed(1)}%; ${formatBytes(otlp.spool.bodyBytes)} encoded bodies)`
7604
- );
7605
- if (usedPercent >= 80) {
7606
- console.log(
7607
- ` Attention: the oldest pending telemetry is dropped when the spool reaches ${formatBytes(otlp.spool.maxBytes)}`
7608
- );
7609
- }
7610
- if (otlp.spool.droppedRecords > 0) {
7611
- console.log(
7612
- ` Dropped: ${otlp.spool.droppedRecords} envelope(s), ${formatBytes(otlp.spool.droppedLogicalBytes)} logical; ${formatBytes(otlp.spool.droppedBodyBytes)} encoded bodies`
7797
+ removeFmlOtel(otelDestination, { warn: reportIntegrationWarning });
7798
+ if (!otelDestination) {
7799
+ integrationsRemoved = false;
7800
+ console.warn(
7801
+ "Warning: The local observer endpoint or token is unavailable; only recorded OpenTelemetry profiles could be cleaned"
7802
+ );
7803
+ }
7804
+ removeFmlDirectMcp("all", {
7805
+ log: () => void 0,
7806
+ warn: reportIntegrationWarning
7807
+ });
7808
+ let purgeComplete = true;
7809
+ if (opts.purge) {
7810
+ if (refuseUnsafePurge(purgeRoots)) {
7811
+ purgeComplete = false;
7812
+ } else {
7813
+ for (const root of purgeRoots) {
7814
+ if (refuseUnsafePurge([root])) {
7815
+ purgeComplete = false;
7816
+ continue;
7817
+ }
7818
+ const containsObserverDirectory = pathContains(
7819
+ root.directory,
7820
+ FML_OBSERVER_DIR
7613
7821
  );
7614
- if (otlp.spool.lastDropReason) {
7615
- console.log(
7616
- ` Last drop: ${otlp.spool.lastDropReason}${otlp.spool.lastDroppedAtMs === void 0 ? "" : ` at ${new Date(otlp.spool.lastDroppedAtMs).toISOString()}`}`
7822
+ const customManagedRoot = !root.nativeDefault;
7823
+ try {
7824
+ const purged = purgeDirectoryPreservingFiles(root.directory, [
7825
+ ...customManagedRoot ? [managedDirectoryMarker(root.directory)] : [],
7826
+ ...containsObserverDirectory && hookCapturePaused ? [FML_HOOK_CAPTURE_DISABLED_FILE] : []
7827
+ ]);
7828
+ if (!purged) {
7829
+ purgeComplete = false;
7830
+ console.warn(
7831
+ `Warning: Preserved ${root.directory}: a symbolic link prevents a complete safe purge`
7832
+ );
7833
+ continue;
7834
+ }
7835
+ if (customManagedRoot && !containsObserverDirectory) {
7836
+ finishCustomRootPurge(root.directory);
7837
+ }
7838
+ } catch (error) {
7839
+ purgeComplete = false;
7840
+ const message = error instanceof Error ? error.message : String(error);
7841
+ console.warn(
7842
+ `Warning: Preserved ${root.directory}: the safe purge was incomplete: ${message}`
7617
7843
  );
7618
7844
  }
7619
7845
  }
7620
7846
  }
7621
- if (otlp.lastError) console.log(` Error: ${otlp.lastError}`);
7622
- if (otlp.lastWarning) console.log(` Warning: ${otlp.lastWarning}`);
7623
- if (otlp.nextAttemptAtMs !== void 0) {
7624
- console.log(
7625
- ` Next attempt: ${new Date(otlp.nextAttemptAtMs).toISOString()}`
7626
- );
7627
- }
7847
+ if (!hookCapturePaused) hookCapturePaused = tryDisableHookCapture();
7848
+ }
7849
+ if (!hookCapturePaused) {
7850
+ process.exitCode = 1;
7851
+ integrationsRemoved = false;
7852
+ console.warn(
7853
+ "Warning: FML cleanup is incomplete; cached agent processes may keep invoking hooks until they exit."
7854
+ );
7855
+ }
7856
+ if (opts.purge && !purgeComplete) {
7857
+ process.exitCode = 1;
7858
+ console.warn(
7859
+ "Warning: Some local data remains because a purge target could not be removed safely. Repair the reported path and rerun `fml uninstall --purge`."
7860
+ );
7861
+ }
7862
+ if (skillsRemoved) {
7863
+ console.log("\u2713 Agent skills removed");
7864
+ } else {
7865
+ console.warn(
7866
+ "Warning: Agent skills were not fully removed. See warnings above."
7867
+ );
7868
+ }
7869
+ if (integrationsRemoved) {
7870
+ console.log("\u2713 Coding-agent integrations removed");
7871
+ } else {
7872
+ console.warn(
7873
+ "Warning: Coding-agent integrations were not fully removed. See warnings above."
7874
+ );
7875
+ }
7876
+ if (!opts.purge) {
7877
+ console.log("\u2713 Local data preserved");
7878
+ } else if (purgeComplete && hookCapturePaused) {
7879
+ console.log("\u2713 Local data deleted");
7880
+ } else {
7881
+ console.warn(
7882
+ "Warning: Local data was not fully deleted. See warnings above."
7883
+ );
7884
+ }
7885
+ const removalComplete = skillsRemoved && integrationsRemoved && (!opts.purge || purgeComplete && hookCapturePaused);
7886
+ if (removalComplete) {
7887
+ console.log("\nFML integrations removed.");
7888
+ } else {
7889
+ console.warn(
7890
+ "FML integration removal is incomplete; review the warnings above."
7891
+ );
7628
7892
  }
7629
7893
  }
7630
7894
 
7631
- // src/commands/sync.ts
7632
- import { statSync } from "fs";
7633
- function rawSync() {
7634
- return createCliRawSync();
7635
- }
7636
- function formatBytes2(bytes) {
7637
- if (bytes < 1024) return `${bytes} B`;
7638
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
7639
- if (bytes < 1024 * 1024 * 1024) {
7640
- return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
7895
+ // src/commands/update.ts
7896
+ var SEMVER_PATTERN = /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
7897
+ function getCurrentVersion() {
7898
+ return true ? "0.8.0-rc.3+246af775" : "unknown";
7899
+ }
7900
+ function resolveUpdateChannel(version) {
7901
+ const match = SEMVER_PATTERN.exec(version);
7902
+ if (!match) return "latest";
7903
+ const prerelease = match[1];
7904
+ if (!prerelease) return "latest";
7905
+ return prerelease.split(".").every((identifier) => {
7906
+ return !(identifier.length > 1 && identifier.startsWith("0") && /^[0-9]+$/u.test(identifier));
7907
+ }) ? "next" : "latest";
7908
+ }
7909
+ function resolveUpdateTarget(version) {
7910
+ return `@fml-inc/fml@${resolveUpdateChannel(version)}`;
7911
+ }
7912
+ function createUpdateEnvironment(env) {
7913
+ const updateEnv = { ...env };
7914
+ for (const key of Object.keys(updateEnv)) {
7915
+ if (key.toLowerCase() === "npm_config_registry") {
7916
+ delete updateEnv[key];
7917
+ }
7918
+ }
7919
+ return updateEnv;
7920
+ }
7921
+ function formatUpdateError(err) {
7922
+ if (!(err instanceof Error)) return String(err);
7923
+ const stderr = err.stderr;
7924
+ if (typeof stderr === "string") return stderr;
7925
+ if (Buffer.isBuffer(stderr)) return stderr.toString();
7926
+ return err.message;
7927
+ }
7928
+ async function handleUpdate(overrides = {}) {
7929
+ const dependencies = {
7930
+ currentVersion: getCurrentVersion(),
7931
+ env: process.env,
7932
+ resolveBin,
7933
+ execBinSync,
7934
+ captureException: Sentry.captureException,
7935
+ log: console.log,
7936
+ error: console.error,
7937
+ exit: process.exit,
7938
+ ...overrides
7939
+ };
7940
+ const channel = resolveUpdateChannel(dependencies.currentVersion);
7941
+ const target2 = resolveUpdateTarget(dependencies.currentVersion);
7942
+ dependencies.log(`Current version: ${dependencies.currentVersion}`);
7943
+ dependencies.log(`Updating FML from the ${channel} channel...
7944
+ `);
7945
+ try {
7946
+ const npm = dependencies.resolveBin("npm");
7947
+ if (!npm) throw new Error("npm was not found on PATH");
7948
+ dependencies.execBinSync(npm, ["install", "--global", target2], {
7949
+ timeout: 12e4,
7950
+ env: createUpdateEnvironment(dependencies.env)
7951
+ });
7952
+ dependencies.log(`
7953
+ FML update complete (${channel} channel).`);
7954
+ dependencies.log("Run `fml setup` to refresh integrations.");
7955
+ } catch (err) {
7956
+ dependencies.captureException(err);
7957
+ dependencies.error("Update failed:", formatUpdateError(err));
7958
+ dependencies.exit(1);
7641
7959
  }
7642
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
7643
- }
7644
- function formatDuration(durationMs) {
7645
- if (durationMs < 1e3) return `${Math.round(durationMs)} ms`;
7646
- return `${(durationMs / 1e3).toFixed(2)} s`;
7647
7960
  }
7648
- function printRepositoryResolution(metrics) {
7649
- if (metrics.attempted === 0 && metrics.budgetSkipped === 0) return;
7650
- console.log(
7651
- ` Repo checks: ${metrics.attempted} (${metrics.resolved} resolved, ${metrics.unresolved} unresolved, ${metrics.cacheHits} cached, ${metrics.budgetSkipped} skipped by budget)`
7652
- );
7961
+
7962
+ // src/commands/logout.ts
7963
+ async function handleLogout() {
7964
+ const { name } = getActiveEnv();
7965
+ await clearStoredCredentials(name);
7653
7966
  console.log(
7654
- ` Repo check time: ${formatDuration(metrics.elapsedMs)} total, ${formatDuration(metrics.slowestMs)} slowest`
7967
+ `Logged out. Stored credentials for the ${name} environment were cleared.`
7655
7968
  );
7656
7969
  }
7657
- function writeJson(value) {
7658
- console.log(JSON.stringify(value, null, 2));
7659
- }
7660
- function warnRetainedLegacyDatabase(retained) {
7661
- if (retained.pendingEvents !== null && retained.pendingHookEvents !== null && retained.pendingOtlpEnvelopes !== null) {
7662
- console.warn(
7663
- `Retained legacy observer database at ${retained.databaseFile} with ${retained.pendingEvents} pending rows (${retained.pendingHookEvents} hook, ${retained.pendingOtlpEnvelopes} OTLP); FML will not sync those rows automatically.`
7664
- );
7665
- return;
7970
+
7971
+ // src/commands/org.ts
7972
+ async function handleOrg(slug) {
7973
+ const environment = getActiveEnv();
7974
+ const token = await getValidToken({ env: environment.name });
7975
+ if (!token) {
7976
+ console.error("Not logged in. Run `fml login` first.");
7977
+ process.exit(1);
7666
7978
  }
7667
- console.warn(
7668
- `Retained legacy observer database at ${retained.databaseFile}; pending row count unavailable. FML will not sync that database automatically.`
7669
- );
7670
- }
7671
- function printStatus(status) {
7672
- const { queue } = status;
7673
- console.log(
7674
- `Raw artifact sync: ${status.settings.enabled ? "enabled" : "disabled"}`
7675
- );
7676
- console.log(` State: ${FML_OBSERVER_DIR}`);
7677
- console.log(` Tracked: ${queue.tracked}`);
7678
- console.log(` Pending: ${queue.pending}`);
7679
- console.log(` Raw work: ${queue.rawPending}`);
7680
- console.log(` Metadata: ${queue.metadataPending}`);
7681
- console.log(` Uploaded: ${queue.uploaded}`);
7682
- console.log(` Failed: ${queue.failed}`);
7683
- console.log(
7684
- ` Watermark: ${formatBytes2(queue.uploadedBytes)} / ${formatBytes2(queue.totalBytes)}`
7685
- );
7686
- console.log(` Remaining: ${formatBytes2(queue.remainingBytes)}`);
7687
- }
7688
- function syncExitCode(result) {
7689
- if (result.outcome === "failed" || result.outcome === "exhausted") return 1;
7690
- if (result.outcome === "busy") return 1;
7691
- return 0;
7692
- }
7693
- async function handleSyncRun(opts = {}) {
7694
- const sync2 = rawSync();
7695
- if (!sync2.getSettings().enabled) {
7696
- const result2 = {
7697
- ok: true,
7698
- outcome: "disabled",
7699
- status: sync2.status()
7700
- };
7701
- if (opts.json) writeJson(result2);
7702
- else printStatus(result2.status);
7703
- return;
7979
+ const api = createFmlClient(token, {
7980
+ envName: environment.name,
7981
+ siteUrl: getSiteUrlForConvexUrl(environment.convexUrl)
7982
+ });
7983
+ const orgs = await api.queryOrgs();
7984
+ if (orgs.length === 0) {
7985
+ console.error("No organizations found.");
7986
+ process.exit(1);
7704
7987
  }
7705
- if (opts.dryRun) {
7706
- const discovery = sync2.discover();
7707
- const bySource = {};
7708
- let totalBytes = 0;
7709
- for (const candidate of discovery.candidates) {
7710
- bySource[candidate.source] = (bySource[candidate.source] ?? 0) + 1;
7711
- try {
7712
- totalBytes += statSync(candidate.sourcePath).size;
7713
- } catch {
7714
- }
7988
+ const current = getSelectedOrg(environment.name);
7989
+ const currentId = getSelectedOrgId(environment.name);
7990
+ if (!slug) {
7991
+ console.log("Organizations:\n");
7992
+ for (const org of orgs) {
7993
+ const orgSlug = org.slug ?? org.name;
7994
+ const marker = org._id === currentId || !currentId && orgSlug === current ? " (selected)" : "";
7995
+ console.log(` ${orgSlug}${marker}`);
7715
7996
  }
7716
- const result2 = {
7717
- ok: true,
7718
- dryRun: true,
7719
- targetUrl: getSiteUrl(),
7720
- discovered: {
7721
- count: discovery.candidates.length,
7722
- totalBytes,
7723
- bySource
7724
- },
7725
- repositoryResolution: discovery.repositoryResolution,
7726
- oversized: discovery.oversized,
7727
- status: sync2.status()
7728
- };
7729
- if (opts.json) writeJson(result2);
7730
- else {
7731
- console.log("FML raw artifact sync dry-run");
7732
- console.log(` Target: ${result2.targetUrl}`);
7733
- console.log(` Discovered: ${result2.discovered.count}`);
7734
- console.log(` Bytes: ${formatBytes2(result2.discovered.totalBytes)}`);
7735
- for (const [source, count] of Object.entries(bySource).sort()) {
7736
- console.log(` ${source}: ${count}`);
7737
- }
7738
- printRepositoryResolution(discovery.repositoryResolution);
7739
- console.log(` Oversized: ${discovery.oversized.length}`);
7740
- if (discovery.compressedArtifactsUnsupportedReason) {
7741
- console.log(
7742
- ` Compressed: skipped \u2014 ${discovery.compressedArtifactsUnsupportedReason}`
7743
- );
7744
- }
7997
+ if (!currentId && !current) {
7998
+ console.log("\nNo org selected. Run `fml org <slug>` to select one.");
7745
7999
  }
7746
8000
  return;
7747
8001
  }
7748
- const attempt = await runRawSyncOnce();
7749
- if (attempt.kind === "unauthenticated") {
7750
- console.error("Raw artifact sync requires an FML login.");
7751
- console.error("Run `fml login` or set FML_TOKEN.");
7752
- process.exitCode = 1;
7753
- return;
8002
+ const match = orgs.find(
8003
+ (o) => (o.slug ?? o.name) === slug || o.name === slug
8004
+ );
8005
+ if (!match) {
8006
+ console.error(
8007
+ `Organization "${slug}" not found. Available: ${orgs.map((o) => o.slug ?? o.name).join(", ")}`
8008
+ );
8009
+ process.exit(1);
7754
8010
  }
7755
- if (attempt.kind === "disabled") {
7756
- if (opts.json) {
7757
- writeJson({ ok: true, outcome: "disabled", status: attempt.status });
7758
- } else {
7759
- printStatus(attempt.status);
8011
+ const matchSlug = match.slug ?? match.name;
8012
+ await setSelectedOrg(matchSlug, match._id, environment.name);
8013
+ console.log(`Selected org: ${match.name} (${matchSlug})`);
8014
+ }
8015
+
8016
+ // src/commands/open.ts
8017
+ var DEFAULT_APP_BASE_URL = "https://app.fml.inc";
8018
+ function appBaseUrl() {
8019
+ const configured = process.env.FML_APP_URL?.trim();
8020
+ return (configured || DEFAULT_APP_BASE_URL).replace(/\/+$/, "");
8021
+ }
8022
+ async function handleOpen(opts) {
8023
+ const environment = getActiveEnv();
8024
+ const api = await getAuthenticatedClient(environment);
8025
+ if (!api) {
8026
+ console.error("Not signed in. Run `fml login` first.");
8027
+ process.exit(1);
8028
+ }
8029
+ let slug;
8030
+ try {
8031
+ const orgs = await api.queryOrgs();
8032
+ if (orgs.length === 0) {
8033
+ console.error("No organizations are available for this account.");
8034
+ process.exit(1);
7760
8035
  }
7761
- return;
8036
+ const selected = getSelectedOrg(environment.name);
8037
+ const org = selected && orgs.find((candidate) => {
8038
+ const candidateSlug = candidate.slug ?? candidate.name;
8039
+ return candidateSlug === selected || candidate.name === selected;
8040
+ }) || orgs[0];
8041
+ slug = org.slug ?? org.name;
8042
+ } catch {
8043
+ console.error(
8044
+ "Could not load your organizations. Opening the FML dashboard."
8045
+ );
8046
+ slug = "";
7762
8047
  }
7763
- const { result, targetUrl } = attempt;
8048
+ const baseUrl = appBaseUrl();
8049
+ const url = slug ? `${baseUrl}/${slug}` : baseUrl;
7764
8050
  if (opts.json) {
7765
- writeJson({ ok: syncExitCode(result) === 0, targetUrl, result });
8051
+ console.log(JSON.stringify({ url }));
7766
8052
  } else {
7767
- console.log("FML raw artifact sync");
7768
- console.log(` Target: ${targetUrl}`);
7769
- console.log(` Outcome: ${result.outcome}`);
7770
- console.log(` Sync passes: ${result.syncPasses}`);
7771
- console.log(` Discovered: ${result.discovery.candidates.length}`);
7772
- if (result.discovery.compressedArtifactsUnsupportedReason) {
7773
- console.log(
7774
- ` Compressed: skipped \u2014 ${result.discovery.compressedArtifactsUnsupportedReason}`
7775
- );
8053
+ const openBrowser = (await import("open")).default;
8054
+ await openBrowser(url);
8055
+ }
8056
+ }
8057
+
8058
+ // src/commands/status.ts
8059
+ function formatBytes2(bytes) {
8060
+ if (bytes < 1024) return `${bytes} B`;
8061
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
8062
+ if (bytes < 1024 * 1024 * 1024) {
8063
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
8064
+ }
8065
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
8066
+ }
8067
+ function hookSpoolStatus(value) {
8068
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
8069
+ return void 0;
8070
+ }
8071
+ const record = value;
8072
+ const nonNegativeInteger = (field) => {
8073
+ const candidate = record[field];
8074
+ return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0 ? candidate : void 0;
8075
+ };
8076
+ const logicalBytes = nonNegativeInteger("logicalBytes");
8077
+ const evictedRecords = nonNegativeInteger("evictedRecords");
8078
+ const evictedBytes = nonNegativeInteger("evictedBytes");
8079
+ const evictedPendingRecords = nonNegativeInteger("evictedPendingRecords");
8080
+ const evictedQuarantinedRecords = nonNegativeInteger(
8081
+ "evictedQuarantinedRecords"
8082
+ );
8083
+ const maxBytes = record.maxBytes;
8084
+ const quarantineMaxAgeMs = record.quarantineMaxAgeMs;
8085
+ if (record.unit !== "logical_bytes" || logicalBytes === void 0 || evictedRecords === void 0 || evictedBytes === void 0 || evictedPendingRecords === void 0 || evictedQuarantinedRecords === void 0 || maxBytes !== null && (typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes < 1)) {
8086
+ return void 0;
8087
+ }
8088
+ const lastEvictedAtMsValue = nonNegativeInteger("lastEvictedAtMs");
8089
+ const lastEvictedAtMs = lastEvictedAtMsValue !== void 0 && lastEvictedAtMsValue <= 864e13 ? lastEvictedAtMsValue : void 0;
8090
+ return {
8091
+ logicalBytes,
8092
+ maxBytes,
8093
+ ...quarantineMaxAgeMs === null ? { quarantineMaxAgeMs: null } : typeof quarantineMaxAgeMs === "number" && Number.isSafeInteger(quarantineMaxAgeMs) && quarantineMaxAgeMs > 0 ? { quarantineMaxAgeMs } : {},
8094
+ evictedRecords,
8095
+ evictedBytes,
8096
+ evictedPendingRecords,
8097
+ evictedQuarantinedRecords,
8098
+ ...lastEvictedAtMs === void 0 ? {} : { lastEvictedAtMs }
8099
+ };
8100
+ }
8101
+ function otlpSpoolStatus(value) {
8102
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
8103
+ return void 0;
8104
+ }
8105
+ const record = value;
8106
+ const nonNegativeInteger = (field) => {
8107
+ const candidate = record[field];
8108
+ return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0 ? candidate : void 0;
8109
+ };
8110
+ const logicalBytes = nonNegativeInteger("logicalBytes");
8111
+ const bodyBytes = nonNegativeInteger("bodyBytes");
8112
+ const maxBytes = nonNegativeInteger("maxBytes");
8113
+ const droppedRecords = nonNegativeInteger("droppedRecords");
8114
+ const droppedLogicalBytes = nonNegativeInteger("droppedLogicalBytes");
8115
+ const droppedBodyBytes = nonNegativeInteger("droppedBodyBytes");
8116
+ if (record.unit !== "logical_bytes" || logicalBytes === void 0 || bodyBytes === void 0 || maxBytes === void 0 || maxBytes < 1 || droppedRecords === void 0 || droppedLogicalBytes === void 0 || droppedBodyBytes === void 0 || bodyBytes > logicalBytes || droppedBodyBytes > droppedLogicalBytes) {
8117
+ return void 0;
8118
+ }
8119
+ return {
8120
+ logicalBytes,
8121
+ bodyBytes,
8122
+ maxBytes,
8123
+ droppedRecords,
8124
+ droppedLogicalBytes,
8125
+ droppedBodyBytes,
8126
+ ...typeof record.lastDroppedAtMs === "number" && Number.isSafeInteger(record.lastDroppedAtMs) && record.lastDroppedAtMs >= 0 && Number.isFinite(new Date(record.lastDroppedAtMs).getTime()) ? { lastDroppedAtMs: record.lastDroppedAtMs } : {},
8127
+ ...typeof record.lastDropReason === "string" ? { lastDropReason: record.lastDropReason } : {}
8128
+ };
8129
+ }
8130
+ function observerQueueStatus(value, key) {
8131
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
8132
+ const queue = value[key];
8133
+ if (!queue || typeof queue !== "object" || Array.isArray(queue)) return null;
8134
+ const record = queue;
8135
+ if (typeof record.phase !== "string" || typeof record.pending !== "number" || !Number.isSafeInteger(record.pending) || record.pending < 0) {
8136
+ return null;
8137
+ }
8138
+ return {
8139
+ phase: record.phase,
8140
+ pending: record.pending,
8141
+ ...typeof record.quarantined === "number" && Number.isSafeInteger(record.quarantined) && record.quarantined >= 0 ? { quarantined: record.quarantined } : {},
8142
+ ...typeof record.lastError === "string" ? { lastError: record.lastError } : {},
8143
+ ...typeof record.lastWarning === "string" ? { lastWarning: record.lastWarning } : {},
8144
+ ...typeof record.nextAttemptAtMs === "number" && Number.isSafeInteger(record.nextAttemptAtMs) && record.nextAttemptAtMs >= 0 && Number.isFinite(new Date(record.nextAttemptAtMs).getTime()) ? { nextAttemptAtMs: record.nextAttemptAtMs } : {}
8145
+ };
8146
+ }
8147
+ function hookStatus(value) {
8148
+ const status = observerQueueStatus(value, "hooks");
8149
+ if (!status || !value || typeof value !== "object" || Array.isArray(value)) {
8150
+ return null;
8151
+ }
8152
+ const hooks = value.hooks;
8153
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return null;
8154
+ const record = hooks;
8155
+ const spool = hookSpoolStatus(record.spool);
8156
+ return {
8157
+ ...status,
8158
+ ...typeof record.rejected === "number" && Number.isSafeInteger(record.rejected) && record.rejected >= 0 ? { rejected: record.rejected } : {},
8159
+ ...typeof record.lastRejectedReason === "string" ? { lastRejectedReason: record.lastRejectedReason } : {},
8160
+ ...typeof record.lastRejectedTarget === "string" ? { lastRejectedTarget: record.lastRejectedTarget } : {},
8161
+ ...spool === void 0 ? {} : { spool }
8162
+ };
8163
+ }
8164
+ function otlpStatus(value) {
8165
+ const status = observerQueueStatus(value, "otlp");
8166
+ if (!status || !value || typeof value !== "object" || Array.isArray(value)) {
8167
+ return null;
8168
+ }
8169
+ const otlp = value.otlp;
8170
+ if (!otlp || typeof otlp !== "object" || Array.isArray(otlp)) return null;
8171
+ const spool = otlpSpoolStatus(otlp.spool);
8172
+ return {
8173
+ ...status,
8174
+ ...spool === void 0 ? {} : { spool }
8175
+ };
8176
+ }
8177
+ async function handleStatus() {
8178
+ const { name, convexUrl } = getActiveEnv();
8179
+ console.log(`Environment: ${name}`);
8180
+ console.log(` Convex: ${convexUrl ?? "not configured"}
8181
+ `);
8182
+ const tokens = readTokens(name);
8183
+ if (!tokens) {
8184
+ console.log("Authentication:");
8185
+ console.log(" Not authenticated. Run `fml login` to sign in.");
8186
+ } else {
8187
+ const tokenValid = await getValidToken({ env: name });
8188
+ console.log("Authentication:");
8189
+ if (tokens.tokenType === "service" && tokens.user.id === SERVICE_TOKEN_LOGIN_USER_ID) {
8190
+ console.log(" Auth: service token");
8191
+ } else {
8192
+ console.log(` User: ${tokens.user.name} (${tokens.user.email})`);
7776
8193
  }
7777
- printRepositoryResolution(result.discovery.repositoryResolution);
7778
- console.log(` Uploaded files: ${result.upload.uploaded}`);
7779
- console.log(` Uploaded chunks: ${result.upload.uploadedChunks}`);
7780
8194
  console.log(
7781
- ` Uploaded bytes: ${formatBytes2(result.upload.uploadedBytes)}`
8195
+ ` Token: ${tokenValid ? "valid" : "expired (needs refresh/re-login)"}`
7782
8196
  );
7783
- console.log(` Failed: ${result.upload.failed}`);
7784
- console.log(` Exhausted: ${result.upload.exhausted}`);
7785
- if (result.upload.retryAtMs !== void 0) {
7786
- console.log(
7787
- ` Retry at: ${new Date(result.upload.retryAtMs).toISOString()}`
7788
- );
7789
- }
7790
- console.log(` Drained: ${result.drained ? "yes" : "no"}`);
7791
- if (result.outcome === "pending") {
7792
- console.log(
7793
- " Next: More data remains; run `fml sync` again or `fml start` for continuous syncing."
7794
- );
7795
- } else if (result.outcome === "backing_off") {
7796
- const nextRetryAtMs = result.upload.nextRetryAtMs;
7797
- console.log(
7798
- ` Next: Remaining items are backing off after upload errors${nextRetryAtMs !== void 0 ? `; next retry due ${new Date(nextRetryAtMs).toISOString()}` : ""}. \`fml start\` retries automatically.`
7799
- );
8197
+ console.log(
8198
+ ` Org: ${tokens.orgSlug ?? "(none \u2014 run `fml org <slug>`)"}`
8199
+ );
8200
+ }
8201
+ const syncStatus = createRawArtifactSync({
8202
+ stateDir: FML_OBSERVER_DIR
8203
+ }).status();
8204
+ console.log("\nRaw artifact sync:");
8205
+ console.log(` Enabled: ${syncStatus.settings.enabled ? "yes" : "no"}`);
8206
+ console.log(` Tracked: ${syncStatus.queue.tracked}`);
8207
+ console.log(` Pending: ${syncStatus.queue.pending}`);
8208
+ console.log(` Raw work: ${syncStatus.queue.rawPending}`);
8209
+ console.log(` Metadata: ${syncStatus.queue.metadataPending}`);
8210
+ console.log(` Uploaded: ${syncStatus.queue.uploaded}`);
8211
+ console.log(` Failed: ${syncStatus.queue.failed}`);
8212
+ console.log(
8213
+ ` Watermark: ${formatBytes2(syncStatus.queue.uploadedBytes)} / ${formatBytes2(syncStatus.queue.totalBytes)}`
8214
+ );
8215
+ console.log(` Remaining: ${formatBytes2(syncStatus.queue.remainingBytes)}`);
8216
+ const observer = await readObserverRuntimeStatus();
8217
+ const protectedStatus = observer.healthy ? await readObserverProtectedStatus() : null;
8218
+ const observerState = observer.stateStatus.state;
8219
+ console.log("\nLocal observer:");
8220
+ console.log(
8221
+ ` Status: ${observer.healthy ? "running" : observer.running ? "unhealthy" : "stopped"}`
8222
+ );
8223
+ if (observerState) {
8224
+ console.log(` PID: ${observerState.pid}`);
8225
+ console.log(` Phase: ${observerState.phase}`);
8226
+ if (observerState.lastOutcome) {
8227
+ console.log(` Last: ${observerState.lastOutcome}`);
7800
8228
  }
7801
- console.log("");
7802
- printStatus(result.status);
7803
8229
  }
7804
- process.exitCode = syncExitCode(result);
7805
- }
7806
- function setRawSyncEnabled(enabled) {
7807
- return rawSync().setEnabled(enabled);
7808
- }
7809
- async function handleSyncEnable(opts = {}) {
7810
- const sync2 = rawSync();
7811
- const settings = sync2.setEnabled(true);
7812
- const isolationWarning = observerOnlyOverrideWarning();
7813
- if (isolationWarning) console.warn(isolationWarning);
7814
- let retainedLegacyDatabase;
7815
- try {
7816
- const observer = await startObserver({
7817
- onLegacyDatabaseRetained: (retained) => {
7818
- retainedLegacyDatabase = retained;
7819
- if (!opts.json) warnRetainedLegacyDatabase(retained);
7820
- }
7821
- });
7822
- setHookCaptureDisabled(false);
7823
- if (observer.status === "already_running") {
7824
- await wakeObserverSync();
8230
+ console.log(` Log: ${observer.logFile}`);
8231
+ const hooks = hookStatus(protectedStatus);
8232
+ const hookInbox = readHookInboxStatus();
8233
+ console.log("\nHook capture:");
8234
+ if (!hooks) {
8235
+ console.log(` Status: ${observer.healthy ? "unavailable" : "stopped"}`);
8236
+ } else {
8237
+ console.log(` Status: ${hooks.phase}`);
8238
+ console.log(` Pending: ${hooks.pending}`);
8239
+ if (hooks.quarantined && hooks.quarantined > 0) {
8240
+ console.log(` Quarantined: ${hooks.quarantined}`);
7825
8241
  }
7826
- const port = observer.runtime.health?.port;
7827
- const authToken = port ? readObserverAuthToken(FML_OBSERVER_AUTH_TOKEN_FILE) : null;
7828
- if (port && authToken) {
7829
- for (const target2 of configureFmlOtel({
7830
- baseUrl: `http://127.0.0.1:${port}`,
7831
- authToken
7832
- })) {
7833
- if (!opts.json) {
7834
- console.log(`OpenTelemetry: ${target2.displayName} -> FML observer`);
7835
- }
8242
+ if (hooks.rejected && hooks.rejected > 0) {
8243
+ console.log(` Rejected: ${hooks.rejected}`);
8244
+ if (hooks.lastRejectedReason) {
8245
+ console.log(
8246
+ ` Last rejection: ${hooks.lastRejectedReason}${hooks.lastRejectedTarget ? ` (${hooks.lastRejectedTarget})` : ""}`
8247
+ );
7836
8248
  }
7837
- } else if (!opts.json) {
7838
- console.warn(
7839
- "Could not configure native OpenTelemetry: the healthy observer endpoint or local token is unavailable."
7840
- );
7841
8249
  }
7842
- if (opts.json) {
7843
- writeJson({
7844
- ok: true,
7845
- sync: settings,
7846
- observer,
7847
- ...observer.retainedLegacyDatabase ? { retainedLegacyDatabase: observer.retainedLegacyDatabase } : {}
7848
- });
7849
- } else {
7850
- console.log("FML raw artifact sync is enabled");
7851
- const pid = observer.runtime.health?.pid;
8250
+ if (hooks.spool) {
7852
8251
  console.log(
7853
- observer.status === "started" ? `FML observer started${pid ? ` (PID ${pid})` : ""}` : `FML observer is already running${pid ? ` (PID ${pid})` : ""}`
8252
+ ` Spool: ${formatBytes2(hooks.spool.logicalBytes)} / ${hooks.spool.maxBytes === null ? "unbounded" : formatBytes2(hooks.spool.maxBytes)} (logical content)`
7854
8253
  );
7855
- }
7856
- } catch (error) {
7857
- const message = error instanceof Error ? error.message : String(error);
7858
- if (opts.json) {
7859
- writeJson({
7860
- ok: false,
7861
- sync: settings,
7862
- error: message,
7863
- ...retainedLegacyDatabase ? { retainedLegacyDatabase } : {}
7864
- });
7865
- } else console.error(message);
7866
- process.exitCode = 1;
7867
- }
7868
- }
7869
- async function handleSyncDisable(opts = {}) {
7870
- const sync2 = rawSync();
7871
- const settings = sync2.setEnabled(false);
7872
- const observerWoken = await wakeObserverSync();
7873
- if (opts.json) {
7874
- writeJson({ ok: true, sync: settings, observerWoken });
7875
- } else {
7876
- console.log("FML raw artifact sync is disabled");
7877
- const observer = await readObserverRuntimeStatus();
7878
- if (observer.running) {
7879
- console.log("FML observer remains available for local integrations");
7880
- }
7881
- }
7882
- }
7883
- async function handleSyncStatus(opts = {}) {
7884
- const status = rawSync().status();
7885
- const observer = await readObserverRuntimeStatus();
7886
- if (opts.json) writeJson({ ok: true, status, observer });
7887
- else {
7888
- printStatus(status);
7889
- const state = observer.stateStatus.state;
7890
- console.log(
7891
- ` Observer: ${observer.healthy ? "running" : observer.running ? "unhealthy" : "stopped"}`
7892
- );
7893
- if (state) {
7894
- console.log(` PID: ${state.pid}`);
7895
- console.log(` Phase: ${state.phase}`);
7896
- if (state.lastOutcome) {
7897
- console.log(` Last run: ${state.lastOutcome}`);
8254
+ if (hooks.spool.quarantineMaxAgeMs !== void 0) {
8255
+ console.log(
8256
+ ` Quarantine retention: ${hooks.spool.quarantineMaxAgeMs === null ? "unbounded" : `${Math.round(hooks.spool.quarantineMaxAgeMs / 864e5)} day(s)`}`
8257
+ );
7898
8258
  }
7899
- if (state.nextAttemptAtMs) {
8259
+ if (hooks.spool.evictedRecords > 0) {
7900
8260
  console.log(
7901
- ` Next run: ${new Date(state.nextAttemptAtMs).toISOString()}`
8261
+ ` Evicted: ${hooks.spool.evictedRecords} record(s), ${formatBytes2(hooks.spool.evictedBytes)} (${hooks.spool.evictedQuarantinedRecords} quarantined, ${hooks.spool.evictedPendingRecords} pending)`
7902
8262
  );
8263
+ if (hooks.spool.lastEvictedAtMs !== void 0) {
8264
+ console.log(
8265
+ ` Last eviction: ${new Date(hooks.spool.lastEvictedAtMs).toISOString()}`
8266
+ );
8267
+ }
7903
8268
  }
7904
8269
  }
7905
- console.log(` Log: ${observer.logFile}`);
8270
+ if (hooks.lastError) console.log(` Error: ${hooks.lastError}`);
7906
8271
  }
7907
- }
7908
- async function handleSyncReset(opts = {}) {
7909
- const result = rawSync().reset();
7910
- if (opts.json) writeJson({ ok: !result.skipped, reset: result });
7911
- else if (result.skipped) {
7912
- console.error(
7913
- "Raw artifact reset skipped: another sync owns the queue lock"
7914
- );
8272
+ if (hookInbox.unreadable) {
8273
+ console.log(" Inbox: unavailable");
7915
8274
  } else {
7916
8275
  console.log(
7917
- `Reset raw upload watermarks for ${result.reset} artifact${result.reset === 1 ? "" : "s"}.`
8276
+ ` Inbox: ${hookInbox.pendingEntries} pending (${formatBytes2(hookInbox.pendingBytes)})`
7918
8277
  );
7919
- if (result.removed > 0) {
7920
- console.log(`Removed ${result.removed} missing artifact entries.`);
7921
- }
7922
8278
  console.log(
7923
- "The next enabled sync will replay each artifact from byte zero."
8279
+ ` Inbox limits: ${hookInbox.maxBytes === null ? "unbounded bytes" : formatBytes2(hookInbox.maxBytes)}, ${hookInbox.maxEntries === null ? "unbounded entries" : `${hookInbox.maxEntries} entries`}`
7924
8280
  );
7925
- }
7926
- if (result.skipped) {
7927
- process.exitCode = 1;
7928
- } else {
7929
- await wakeObserverSync();
7930
- }
7931
- }
7932
-
7933
- // src/commands/daemon.ts
7934
- async function handleFmlStart() {
7935
- await handleSyncEnable();
7936
- }
7937
- async function handleFmlStop(opts = {}) {
7938
- setRawSyncEnabled(false);
7939
- const result = await stopObserver({ force: opts.force });
7940
- if (result.status === "stopped" || result.status === "killed" || result.status === "not_running" || result.status === "stale_state") {
7941
- try {
7942
- setHookCaptureDisabled(true);
7943
- } catch (error) {
7944
- const message = error instanceof Error ? error.message : String(error);
7945
- console.error(
7946
- `FML observer stopped, but the hook-capture pause could not be persisted: ${message}`
8281
+ if (hookInbox.capacityRejectedEntries > 0) {
8282
+ console.log(
8283
+ ` Inbox capacity refusals: ${hookInbox.capacityRejectedEntries} record(s), ${formatBytes2(hookInbox.capacityRejectedBytes)}`
7947
8284
  );
7948
- process.exitCode = 1;
8285
+ if (hookInbox.lastCapacityRejectedAtMs !== void 0) {
8286
+ console.log(
8287
+ ` Last inbox refusal: ${new Date(hookInbox.lastCapacityRejectedAtMs).toISOString()}${hookInbox.lastCapacityRejectionReason ? ` (${hookInbox.lastCapacityRejectionReason})` : ""}`
8288
+ );
8289
+ }
7949
8290
  }
7950
- }
7951
- switch (result.status) {
7952
- case "stopped":
7953
- console.log(`FML observer stopped (PID ${result.pid})`);
7954
- break;
7955
- case "killed":
7956
- console.log(`FML observer force stopped (PID ${result.pid})`);
7957
- break;
7958
- case "not_running":
7959
- console.log("FML observer was not running");
7960
- break;
7961
- case "stale_state":
8291
+ if (hookInbox.retiredEntries > 0) {
7962
8292
  console.log(
7963
- "FML observer is not running; stale state will be reclaimed on the next start"
7964
- );
7965
- break;
7966
- case "permission_denied":
7967
- console.error(
7968
- `Could not stop FML observer PID ${result.pid}: permission denied`
7969
- );
7970
- process.exitCode = 1;
7971
- break;
7972
- case "foreign_listener":
7973
- console.error("Refused to stop a different local observer instance");
7974
- process.exitCode = 1;
7975
- break;
7976
- case "ambiguous_live_process":
7977
- console.error(
7978
- `Refused to treat unverifiable live PID ${result.pid} as a stopped FML observer`
8293
+ ` Inbox retired: ${hookInbox.retiredEntries} record(s), ${formatBytes2(hookInbox.retiredBytes)}`
7979
8294
  );
7980
- process.exitCode = 1;
7981
- break;
7982
- case "unresponsive":
7983
- console.error(
7984
- `Refused to signal unverifiable FML observer PID ${result.pid}; retry with \`fml stop --force\` to escalate`
8295
+ }
8296
+ }
8297
+ const otlp = otlpStatus(protectedStatus);
8298
+ console.log("\nNative OpenTelemetry:");
8299
+ if (!otlp) {
8300
+ console.log(` Status: ${observer.healthy ? "unavailable" : "stopped"}`);
8301
+ } else {
8302
+ console.log(` Status: ${otlp.phase}`);
8303
+ console.log(` Pending: ${otlp.pending}`);
8304
+ if (otlp.spool) {
8305
+ const usedPercent = otlp.spool.logicalBytes / otlp.spool.maxBytes * 100;
8306
+ console.log(
8307
+ ` Spool: ${formatBytes2(otlp.spool.logicalBytes)} / ${formatBytes2(otlp.spool.maxBytes)} (${usedPercent.toFixed(1)}%; ${formatBytes2(otlp.spool.bodyBytes)} encoded bodies)`
7985
8308
  );
7986
- process.exitCode = 1;
7987
- break;
7988
- case "signal_sent":
7989
- console.error(`FML observer PID ${result.pid} did not stop`);
7990
- process.exitCode = 1;
7991
- break;
7992
- case "force_stop_unverified":
7993
- console.error(
7994
- `Could not corroborate that PID ${result.pid} is the FML observer; refusing to signal it. If the observer is not running, remove ${result.stateFile}, ${result.lockFile}, ${result.databaseStateFile}, and ${result.databaseLockFile} manually.`
8309
+ if (usedPercent >= 80) {
8310
+ console.log(
8311
+ ` Attention: the oldest pending telemetry is dropped when the spool reaches ${formatBytes2(otlp.spool.maxBytes)}`
8312
+ );
8313
+ }
8314
+ if (otlp.spool.droppedRecords > 0) {
8315
+ console.log(
8316
+ ` Dropped: ${otlp.spool.droppedRecords} envelope(s), ${formatBytes2(otlp.spool.droppedLogicalBytes)} logical; ${formatBytes2(otlp.spool.droppedBodyBytes)} encoded bodies`
8317
+ );
8318
+ if (otlp.spool.lastDropReason) {
8319
+ console.log(
8320
+ ` Last drop: ${otlp.spool.lastDropReason}${otlp.spool.lastDroppedAtMs === void 0 ? "" : ` at ${new Date(otlp.spool.lastDroppedAtMs).toISOString()}`}`
8321
+ );
8322
+ }
8323
+ }
8324
+ }
8325
+ if (otlp.lastError) console.log(` Error: ${otlp.lastError}`);
8326
+ if (otlp.lastWarning) console.log(` Warning: ${otlp.lastWarning}`);
8327
+ if (otlp.nextAttemptAtMs !== void 0) {
8328
+ console.log(
8329
+ ` Next attempt: ${new Date(otlp.nextAttemptAtMs).toISOString()}`
7995
8330
  );
7996
- process.exitCode = 1;
7997
- break;
8331
+ }
7998
8332
  }
7999
8333
  }
8000
8334
 
@@ -8002,7 +8336,7 @@ async function handleFmlStop(opts = {}) {
8002
8336
  function handleEnvShow() {
8003
8337
  const { name, convexUrl } = getActiveEnv();
8004
8338
  console.log(`Environment: ${name}`);
8005
- console.log(` Convex URL: ${convexUrl ?? "not configured"}`);
8339
+ console.log(` Service URL: ${convexUrl ?? "not configured"}`);
8006
8340
  }
8007
8341
  function handleEnvSwitch(target2) {
8008
8342
  let name = target2;
@@ -8025,17 +8359,20 @@ function handleEnvSwitch(target2) {
8025
8359
  return;
8026
8360
  }
8027
8361
  if (!convexUrl) {
8028
- console.error(`FML environment "${name}" has no configured backend URL.`);
8362
+ console.error(`Environment ${name} has no configured service URL.`);
8029
8363
  console.error(
8030
- `Run FML_CONVEX_URL=https://<deployment>.convex.cloud fml env ${name}`
8364
+ `Set FML_CONVEX_URL=https://<deployment>.convex.cloud and run \`fml env ${name}\` again.`
8031
8365
  );
8032
8366
  process.exitCode = 1;
8033
8367
  return;
8034
8368
  }
8035
8369
  setActiveEnvironment(name, convexUrl);
8036
- console.log(`Switched to ${name}: ${convexUrl}`);
8037
- console.log("New CLI and sync operations will use this environment.");
8038
- console.log("Restart running MCP clients to switch their backend and auth.");
8370
+ console.log(`Environment switched to ${name}.`);
8371
+ console.log(`Service URL: ${convexUrl}`);
8372
+ console.log("CLI commands and agent activity sync now use this environment.");
8373
+ console.log(
8374
+ "Start a new agent session to use the new environment and credentials."
8375
+ );
8039
8376
  }
8040
8377
 
8041
8378
  // src/commands/data.ts
@@ -8591,8 +8928,8 @@ async function handleConfigDetail(opts) {
8591
8928
 
8592
8929
  // src/commands/tools.ts
8593
8930
  import { readFileSync } from "fs";
8594
- import path28 from "path";
8595
- var pluginVersion = true ? "0.8.0-rc.1+60c9b7e1" : void 0;
8931
+ import path29 from "path";
8932
+ var pluginVersion = true ? "0.8.0-rc.3+246af775" : void 0;
8596
8933
  async function getApi() {
8597
8934
  const api = await getAuthenticatedClient();
8598
8935
  if (!api) {
@@ -8699,7 +9036,7 @@ async function handleToolsCall(name, opts) {
8699
9036
  }
8700
9037
  const raw = opts.file ? (() => {
8701
9038
  try {
8702
- return readFileSync(path28.resolve(opts.file), "utf8");
9039
+ return readFileSync(path29.resolve(opts.file), "utf8");
8703
9040
  } catch (err) {
8704
9041
  const msg = err instanceof Error ? err.message : String(err);
8705
9042
  console.error(`Could not read args file: ${msg}`);
@@ -8725,13 +9062,31 @@ async function handleToolsCall(name, opts) {
8725
9062
  console.log(JSON.stringify(result.result, null, 2));
8726
9063
  }
8727
9064
 
9065
+ // src/banner.ts
9066
+ import pc2 from "picocolors";
9067
+ var LOGO_LINES = [
9068
+ " \u2584\u2588\u2588\u2580 \u2588\u2588\u2584\u2588\u2588\u2584\u2588\u2588 \u2588\u2588",
9069
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
9070
+ "\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
9071
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588",
9072
+ " \u2580\u2580 \u2580\u2580 \u2580\u2580 \u2580\u2580\u2580\u2580"
9073
+ ];
9074
+ function printBanner() {
9075
+ console.log("");
9076
+ for (let i = 0; i < LOGO_LINES.length; i++) {
9077
+ const color = i < 3 ? pc2.blue : pc2.cyan;
9078
+ console.log(" " + color(LOGO_LINES[i]));
9079
+ }
9080
+ console.log("");
9081
+ }
9082
+
8728
9083
  // src/cli.ts
8729
9084
  await initSentry();
8730
9085
  if (process.argv.length <= 2 || process.argv.includes("--help") || process.argv.includes("-h")) {
8731
9086
  printBanner();
8732
9087
  }
8733
9088
  var program = new Command().name("fml").description("FML CLI and agent tools").version(
8734
- true ? "0.8.0-rc.1+60c9b7e1" : "dev"
9089
+ true ? "0.8.0-rc.3+246af775" : "dev"
8735
9090
  );
8736
9091
  function formatCommandInventory(cmd, prefix = "") {
8737
9092
  const name = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
@@ -8760,11 +9115,12 @@ tools.command("call").description("Invoke a backend tool by name with JSON args"
8760
9115
  program.command("mcp-server", { hidden: true }).description("Run the FML MCP server on stdio").action(async () => {
8761
9116
  await import("./mcp/server.js");
8762
9117
  });
8763
- program.command("install").description("Set up FML for local agent use").action(() => handleInstall());
9118
+ program.command("setup").description("Install integrations, sign in if needed, and start FML").action(() => handleSetup());
9119
+ program.command("install").description("Register FML agent integrations and the MCP server").action(() => handleInstall());
8764
9120
  program.command("uninstall").description("Remove FML agent integrations and optionally local data").option("--purge", "Also remove all data, logs, and auth tokens").option("-y, --yes", "Skip the purge confirmation prompt").action(
8765
9121
  (opts) => handleUninstall({ purge: opts.purge, assumeYes: opts.yes })
8766
9122
  );
8767
- program.command("update").description("Update fml to the latest version").action(() => handleUpdate());
9123
+ program.command("update").description("Update fml from its release channel").action(() => handleUpdate());
8768
9124
  program.command("login").description("Sign in to your FML account").option("--device", "Use device authorization flow (no browser needed)").option(
8769
9125
  "--service-token",
8770
9126
  "Sign in with an org service refresh token (prompts for fml_srt_*)"
@@ -8774,28 +9130,29 @@ program.command("login").description("Sign in to your FML account").option("--de
8774
9130
  program.command("logout").description("Sign out and clear stored credentials").action(() => handleLogout());
8775
9131
  program.command("org").description("Show or select organization").argument("[slug]", "Org slug to select").action((slug) => handleOrg(slug));
8776
9132
  program.command("status").description("Show auth, environment, and raw sync watermarks").action(() => handleStatus());
8777
- program.command("doctor").description("Check FML configuration, raw sync, and connectivity").option("--json", "Output as JSON").action((opts) => handleDoctor(opts));
9133
+ program.command("doctor").description("Check FML configuration, agent activity sync, and connectivity").option("--json", "Output as JSON").action((opts) => handleDoctor(opts));
8778
9134
  program.command("open").description("Open FML dashboard in browser").option("--json", "Output URL as JSON instead of opening browser").action((opts) => handleOpen(opts));
8779
9135
  program.command("env").description("Show or switch FML backend environment").argument("[target]", "Environment name or backend URL").action((target2) => {
8780
9136
  if (target2) handleEnvSwitch(target2);
8781
9137
  else handleEnvShow();
8782
9138
  });
8783
- program.command("start").description("Start the local observer and continuous raw artifact sync").action(() => handleFmlStart());
8784
- program.command("stop").description("Stop the local observer and disable raw artifact sync").option("--force", "Escalate to SIGTERM/SIGKILL if the observer is hung").action((opts) => handleFmlStop({ force: opts.force }));
8785
- var sync = program.command("sync").description("Sync raw local artifacts to FML").option("--dry-run", "Discover artifacts without changing queue state").option("--json", "Output as JSON").action((opts) => handleSyncRun(opts));
8786
- sync.command("enable").description("Enable raw artifact sync").option("--json", "Output as JSON").action(
9139
+ program.command("start").description("Start the local observer and continuous agent activity sync").action(() => handleFmlStart());
9140
+ program.command("restart").description("Restart the local observer and continuous agent activity sync").action(() => handleFmlRestart());
9141
+ program.command("stop").description("Stop the local observer and pause agent activity sync").option("--force", "Escalate to SIGTERM/SIGKILL if the observer is hung").action((opts) => handleFmlStop({ force: opts.force }));
9142
+ var sync = program.command("sync").description("Sync Agent activity to FML").option("--dry-run", "Discover artifacts without changing queue state").option("--json", "Output as JSON").action((opts) => handleSyncRun(opts));
9143
+ sync.command("enable").description("Enable Agent activity sync").option("--json", "Output as JSON").action(
8787
9144
  (_opts, command) => handleSyncEnable(command.optsWithGlobals())
8788
9145
  );
8789
- sync.command("disable").description("Disable raw artifact sync").option("--json", "Output as JSON").action(
9146
+ sync.command("disable").description("Pause Agent activity sync").option("--json", "Output as JSON").action(
8790
9147
  (_opts, command) => handleSyncDisable(command.optsWithGlobals())
8791
9148
  );
8792
- sync.command("status").description("Show raw artifact queue watermarks").option("--json", "Output as JSON").action(
9149
+ sync.command("status").description("Show Agent activity queue watermarks").option("--json", "Output as JSON").action(
8793
9150
  (_opts, command) => handleSyncStatus(command.optsWithGlobals())
8794
9151
  );
8795
- sync.command("reset").description("Replay all raw artifacts from byte zero").option("--json", "Output as JSON").action(
9152
+ sync.command("reset").description("Replay Agent activity uploads from byte zero").option("--json", "Output as JSON").action(
8796
9153
  (_opts, command) => handleSyncReset(command.optsWithGlobals())
8797
9154
  );
8798
- var artifacts = program.command("artifacts", { hidden: true }).description("Compatibility alias for raw artifact sync");
9155
+ var artifacts = program.command("artifacts", { hidden: true }).description("Compatibility alias for Agent activity sync");
8799
9156
  artifacts.command("sync").option("--dry-run", "Discover artifacts without changing queue state").option("--json", "Output as JSON").action((opts) => handleSyncRun(opts));
8800
9157
  program.command("activity").description("Activity summary \u2014 sessions, prompts, tools, costs").option("--since <duration>", 'Time window, e.g. "24h", "7d"').action((opts) => handleActivity(opts));
8801
9158
  program.command("sessions").description("List recent sessions").option("--since <duration>", 'Time filter, e.g. "24h", "7d"').option("--limit <n>", "Max sessions to return").action((opts) => handleSessions(opts));