@fml-inc/fml 0.8.0-rc.1 → 0.8.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-WZLLO563.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-56TPYMNK.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.2+bed6eaed" : "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,6 +1056,9 @@ 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
@@ -1264,24 +1305,6 @@ 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]));
1281
- }
1282
- console.log("");
1283
- }
1284
-
1285
1308
  // src/bin-utils.ts
1286
1309
  import fs3 from "fs";
1287
1310
  import path4 from "path";
@@ -4132,7 +4155,7 @@ function rollbackMutation2(mutation) {
4132
4155
  }
4133
4156
  function warnSkippedPlan(plan, error, warn) {
4134
4157
  warn(
4135
- `warn: skipped ${plan.displayName} MCP config at ${plan.configPath}: ${error}`
4158
+ `Warning: skipped ${plan.displayName} MCP config at ${plan.configPath}: ${error}`
4136
4159
  );
4137
4160
  }
4138
4161
  function applyPlan2(plan, warn) {
@@ -4176,14 +4199,17 @@ function reportPlan(plan, warn, log) {
4176
4199
  for (const note of plan.notes) log(note);
4177
4200
  }
4178
4201
  function configureFmlDirectMcp(pluginRoot, options = {}) {
4179
- const warn = options.warn ?? console.warn;
4202
+ const emitWarning = (message) => {
4203
+ (options.warn ?? console.warn)(message);
4204
+ options.onFailure?.(message);
4205
+ };
4180
4206
  const log = options.log ?? console.log;
4181
4207
  const context = plannerContext2(pluginRoot, options);
4182
4208
  const changes = [];
4183
4209
  for (const capability of DIRECT_MCP_CAPABILITIES) {
4184
4210
  const plan = capability.planInstall(context);
4185
- reportPlan(plan, warn, log);
4186
- if (!applyPlan2(plan, warn)) continue;
4211
+ reportPlan(plan, emitWarning, log);
4212
+ if (!applyPlan2(plan, emitWarning)) continue;
4187
4213
  changes.push({
4188
4214
  targetId: plan.targetId,
4189
4215
  displayName: plan.displayName,
@@ -4867,14 +4893,13 @@ async function handleInstall(opts = {}) {
4867
4893
  homeDir: os5.homedir(),
4868
4894
  packageRoot: pluginRoot
4869
4895
  });
4870
- console.log("Installing fml...\n");
4871
- console.log("[1/5] Creating FML directories...");
4896
+ console.log("Configuring FML integrations...");
4872
4897
  for (const root of configuredManagedRoots()) {
4873
4898
  const preparation = prepareManagedDirectory(root);
4874
4899
  if (!preparation.managed) {
4875
4900
  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
4901
  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\``
4902
+ `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
4903
  );
4879
4904
  }
4880
4905
  }
@@ -4885,11 +4910,8 @@ async function handleInstall(opts = {}) {
4885
4910
  FML_OBSERVER_DIR
4886
4911
  ])) {
4887
4912
  fs9.mkdirSync(directory, { recursive: true, mode: 448 });
4888
- console.log(` ${directory}`);
4889
4913
  }
4890
4914
  initializeSyncSettings(path21.join(FML_OBSERVER_DIR, "sync.json"));
4891
- console.log();
4892
- console.log("[2/5] Installing agent-native /fml skills...");
4893
4915
  let skillReady = false;
4894
4916
  try {
4895
4917
  const skillResults = installManagedAgentSkills(pluginRoot);
@@ -4897,23 +4919,17 @@ async function handleInstall(opts = {}) {
4897
4919
  for (const result of skillResults) {
4898
4920
  const targets = result.displayNames.join(", ");
4899
4921
  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})`
4922
+ console.warn(
4923
+ `Warning: preserved ${result.destinationPath} for ${targets}: ${result.reason}`
4906
4924
  );
4907
4925
  }
4908
4926
  }
4909
4927
  } catch (error) {
4910
4928
  const message = error instanceof Error ? error.message : String(error);
4911
- console.log(
4912
- ` warn: agent-native skill setup could not be completed: ${message}`
4929
+ console.warn(
4930
+ `Warning: agent-native skill setup could not be completed: ${message}`
4913
4931
  );
4914
4932
  }
4915
- console.log();
4916
- console.log("[3/5] Writing plugin manifest...");
4917
4933
  const packageJson = readJsonFile(path21.join(pluginRoot, "package.json"));
4918
4934
  const generatedManifest = readJsonFile(paths.pluginManifestPath);
4919
4935
  const version = typeof generatedManifest?.version === "string" ? generatedManifest.version : typeof packageJson?.version === "string" ? packageJson.version : "0.0.0-dev";
@@ -4921,17 +4937,14 @@ async function handleInstall(opts = {}) {
4921
4937
  paths.pluginManifestPath,
4922
4938
  CLAUDE_PLUGIN_INSTALLER.pluginManifest(version)
4923
4939
  );
4924
- console.log(` Version: ${version}
4925
- `);
4926
- console.log("[4/5] Registering the FML plugin...");
4927
4940
  const claudeBin = resolveBin("claude");
4928
4941
  const marketplaceResult = readJsonFileForUpdate(
4929
4942
  paths.marketplaceManifestPath
4930
4943
  );
4931
4944
  let marketplaceReady = false;
4932
4945
  if (!marketplaceResult.ok) {
4933
- console.log(
4934
- ` warn: preserved unreadable marketplace config ${paths.marketplaceManifestPath}: ${marketplaceResult.error}`
4946
+ console.warn(
4947
+ `Warning: preserved unreadable marketplace config ${paths.marketplaceManifestPath}: ${marketplaceResult.error}`
4935
4948
  );
4936
4949
  } else {
4937
4950
  fs9.mkdirSync(paths.marketplaceDir, { recursive: true, mode: 448 });
@@ -4941,8 +4954,8 @@ async function handleInstall(opts = {}) {
4941
4954
  marketplaceSymlinkType()
4942
4955
  );
4943
4956
  if (linkUpdate === "preserved") {
4944
- console.log(
4945
- ` warn: preserved non-symlink marketplace path ${paths.marketplaceLinkPath}; remove it manually to enable the FML Claude plugin`
4957
+ console.warn(
4958
+ `Warning: preserved non-symlink marketplace path ${paths.marketplaceLinkPath}; remove it manually to enable the FML Claude plugin`
4946
4959
  );
4947
4960
  } else {
4948
4961
  const marketplace = CLAUDE_PLUGIN_INSTALLER.addMarketplaceEntry(
@@ -4955,8 +4968,8 @@ async function handleInstall(opts = {}) {
4955
4968
  const settingsResult = readJsonFileForUpdate(paths.settingsPath);
4956
4969
  let settingsReady = false;
4957
4970
  if (!settingsResult.ok) {
4958
- console.log(
4959
- ` warn: preserved unreadable Claude settings ${paths.settingsPath}: ${settingsResult.error}`
4971
+ console.warn(
4972
+ `Warning: preserved unreadable Claude settings ${paths.settingsPath}: ${settingsResult.error}`
4960
4973
  );
4961
4974
  } else if (marketplaceReady) {
4962
4975
  const settings = CLAUDE_PLUGIN_INSTALLER.enableInSettings(
@@ -4966,21 +4979,23 @@ async function handleInstall(opts = {}) {
4966
4979
  writeJsonFile(paths.settingsPath, settings);
4967
4980
  settingsReady = true;
4968
4981
  }
4969
- for (const target2 of configureFmlDirectMcp(pluginRoot)) {
4970
- console.log(` MCP: ${target2.displayName} -> fml`);
4971
- }
4982
+ let mcpReady = true;
4983
+ let claudePluginReady = true;
4984
+ configureFmlDirectMcp(pluginRoot, {
4985
+ warn: (message) => console.warn(message.replace(/^warn:/u, "Warning:")),
4986
+ onFailure: () => {
4987
+ mcpReady = false;
4988
+ }
4989
+ });
4972
4990
  let hooksReady = true;
4973
- const hookTargets = configureFmlHooks(pluginRoot, {
4991
+ configureFmlHooks(pluginRoot, {
4974
4992
  onFailure: () => {
4975
4993
  hooksReady = false;
4976
4994
  }
4977
4995
  });
4978
- for (const target2 of hookTargets) {
4979
- console.log(` Hooks: ${target2.displayName} -> fml`);
4980
- }
4981
4996
  if (!marketplaceReady || !settingsReady) {
4982
- console.log(
4983
- " warn: skipped Claude plugin registration until its existing config is repaired"
4997
+ console.warn(
4998
+ "Warning: skipped Claude plugin registration until its existing config is repaired"
4984
4999
  );
4985
5000
  } else if (claudeBin) {
4986
5001
  try {
@@ -4995,16 +5010,14 @@ async function handleInstall(opts = {}) {
4995
5010
  timeout: command.timeoutMs
4996
5011
  });
4997
5012
  }
4998
- console.log(" Plugin registered via Claude Code CLI");
4999
5013
  } catch (error) {
5014
+ claudePluginReady = false;
5000
5015
  const message = error instanceof Error ? error.message : String(error);
5001
- console.log(` warn: Claude plugin registration failed: ${message}`);
5016
+ console.warn(`Warning: Claude plugin registration failed: ${message}`);
5002
5017
  }
5003
5018
  } else {
5004
- console.log(" warn: claude CLI not found; plugin files are ready");
5019
+ console.warn("Warning: claude CLI not found; plugin files are ready");
5005
5020
  }
5006
- console.log();
5007
- console.log("[5/5] Initializing the FML environment...");
5008
5021
  if (!envConfigExists()) {
5009
5022
  writeEnvConfig({
5010
5023
  schemaVersion: 1,
@@ -5013,9 +5026,6 @@ async function handleInstall(opts = {}) {
5013
5026
  [DEFAULT_TARGET_NAME]: { convexUrl: DEFAULT_PROD_URL }
5014
5027
  }
5015
5028
  });
5016
- console.log(` ${DEFAULT_TARGET_NAME}: ${DEFAULT_PROD_URL}`);
5017
- } else {
5018
- console.log(" Preserved existing environment selection");
5019
5029
  }
5020
5030
  if (!skillReady || !marketplaceReady || !settingsReady || !hooksReady) {
5021
5031
  throw new Error(
@@ -5030,21 +5040,20 @@ async function handleInstall(opts = {}) {
5030
5040
  `FML install is incomplete: hook capture remains paused because its state could not be cleared: ${message}`
5031
5041
  );
5032
5042
  }
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");
5043
+ console.log("\u2713 Agent skills configured");
5044
+ if (mcpReady && claudePluginReady) {
5045
+ console.log("\u2713 Coding-agent integrations configured");
5046
+ console.log("FML integrations configured.");
5047
+ } else {
5048
+ console.warn(
5049
+ "Warning: Coding-agent integrations were not fully configured. See warnings above."
5050
+ );
5051
+ console.log("FML integration configuration finished with warnings.");
5052
+ }
5041
5053
  }
5042
5054
 
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";
5055
+ // src/commands/sync.ts
5056
+ import { statSync } from "fs";
5048
5057
 
5049
5058
  // src/commands/otel-config.ts
5050
5059
  import fs11 from "fs";
@@ -6040,7 +6049,7 @@ function applyPlan3(plan, warn, commitOwnership) {
6040
6049
  } catch (error) {
6041
6050
  const detail = error instanceof Error ? error.message : String(error);
6042
6051
  warn(
6043
- `warn: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: ${detail}`
6052
+ `Warning: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: ${detail}`
6044
6053
  );
6045
6054
  return null;
6046
6055
  }
@@ -6049,7 +6058,7 @@ function applyPlan3(plan, warn, commitOwnership) {
6049
6058
  } catch (error) {
6050
6059
  const detail = error instanceof Error ? error.message : String(error);
6051
6060
  warn(
6052
- `warn: could not record ${plan.displayName} OpenTelemetry ownership at ${plan.configPath}: ${detail}`
6061
+ `Warning: could not record ${plan.displayName} OpenTelemetry ownership at ${plan.configPath}: ${detail}`
6053
6062
  );
6054
6063
  }
6055
6064
  }
@@ -6058,7 +6067,7 @@ function applyPlan3(plan, warn, commitOwnership) {
6058
6067
  const uniquePaths2 = new Set(plan.mutations.map((mutation) => mutation.path));
6059
6068
  if (uniquePaths2.size !== plan.mutations.length) {
6060
6069
  warn(
6061
- `warn: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: plan contains duplicate paths`
6070
+ `Warning: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: plan contains duplicate paths`
6062
6071
  );
6063
6072
  return null;
6064
6073
  }
@@ -6089,7 +6098,7 @@ function applyPlan3(plan, warn, commitOwnership) {
6089
6098
  const detail = error instanceof Error ? error.message : String(error);
6090
6099
  const rollbackDetail = rollbackErrors.length === 0 ? "" : `; rollback failed: ${rollbackErrors.join("; ")}`;
6091
6100
  warn(
6092
- `warn: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: ${detail}${rollbackDetail}`
6101
+ `Warning: skipped ${plan.displayName} OpenTelemetry at ${plan.configPath}: ${detail}${rollbackDetail}`
6093
6102
  );
6094
6103
  return null;
6095
6104
  }
@@ -6124,7 +6133,7 @@ function loadOtelConfigState(options, warn) {
6124
6133
  )
6125
6134
  };
6126
6135
  } catch (error) {
6127
- warn(`warn: ${error instanceof Error ? error.message : String(error)}`);
6136
+ warn(`Warning: ${error instanceof Error ? error.message : String(error)}`);
6128
6137
  return null;
6129
6138
  }
6130
6139
  }
@@ -6171,7 +6180,7 @@ function planSafely(capability, operation, context, warn) {
6171
6180
  return operation === "install" ? capability.planInstall(context) : capability.planUninstall(context);
6172
6181
  } catch (error) {
6173
6182
  warn(
6174
- `warn: skipped ${capability.displayName} OpenTelemetry: ${error instanceof Error ? error.message : String(error)}`
6183
+ `Warning: skipped ${capability.displayName} OpenTelemetry: ${error instanceof Error ? error.message : String(error)}`
6175
6184
  );
6176
6185
  return null;
6177
6186
  }
@@ -6181,7 +6190,7 @@ function pruneMissingRecordedEntry(state, key, entry, warn) {
6181
6190
  commitStateEntry(state, key, null);
6182
6191
  } catch (error) {
6183
6192
  warn(
6184
- `warn: could not prune missing ${entry.targetId} OpenTelemetry ownership at ${entry.path}: ${error instanceof Error ? error.message : String(error)}`
6193
+ `Warning: could not prune missing ${entry.targetId} OpenTelemetry ownership at ${entry.path}: ${error instanceof Error ? error.message : String(error)}`
6185
6194
  );
6186
6195
  }
6187
6196
  }
@@ -6283,13 +6292,13 @@ function removeFmlOtel(observer, options = {}) {
6283
6292
  }
6284
6293
  if (candidate.recorded && candidate.plan.mutations.length === 0 && candidate.plan.warnings.length === 0 && state) {
6285
6294
  warn(
6286
- `warn: preserved ${candidate.plan.displayName} OpenTelemetry at ${candidate.plan.configPath}: managed fields were modified or removed after installation`
6295
+ `Warning: preserved ${candidate.plan.displayName} OpenTelemetry at ${candidate.plan.configPath}: managed fields were modified or removed after installation`
6287
6296
  );
6288
6297
  try {
6289
6298
  commitStateEntry(state, key, null);
6290
6299
  } catch (error) {
6291
6300
  warn(
6292
- `warn: could not relinquish ${candidate.plan.displayName} OpenTelemetry ownership at ${candidate.plan.configPath}: ${error instanceof Error ? error.message : String(error)}`
6301
+ `Warning: could not relinquish ${candidate.plan.displayName} OpenTelemetry ownership at ${candidate.plan.configPath}: ${error instanceof Error ? error.message : String(error)}`
6293
6302
  );
6294
6303
  }
6295
6304
  }
@@ -6297,420 +6306,390 @@ function removeFmlOtel(observer, options = {}) {
6297
6306
  return changes;
6298
6307
  }
6299
6308
 
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
- }
6309
+ // src/commands/sync.ts
6310
+ function rawSync() {
6311
+ return createCliRawSync();
6313
6312
  }
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];
6313
+ function formatBytes(bytes) {
6314
+ if (bytes < 1024) return `${bytes} B`;
6315
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
6316
+ if (bytes < 1024 * 1024 * 1024) {
6317
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
6321
6318
  }
6319
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
6322
6320
  }
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)
6336
- );
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
- )
6321
+ function formatDuration(durationMs) {
6322
+ if (durationMs < 1e3) return `${Math.round(durationMs)} ms`;
6323
+ return `${(durationMs / 1e3).toFixed(2)} s`;
6324
+ }
6325
+ function printRepositoryResolution(metrics) {
6326
+ if (metrics.attempted === 0 && metrics.budgetSkipped === 0) return;
6327
+ console.log(
6328
+ ` Repo checks: ${metrics.attempted} (${metrics.resolved} resolved, ${metrics.unresolved} unresolved, ${metrics.cacheHits} cached, ${metrics.budgetSkipped} skipped by budget)`
6355
6329
  );
6356
- if (!containsPreservedPath) {
6357
- fs12.rmSync(directory, { recursive: true, force: true });
6358
- return true;
6359
- }
6360
- const isPreservedPath = directoryPaths.some(
6361
- (directoryPath) => preservedPaths.some(
6362
- (preservedPath) => pathsAreSame(directoryPath, path27.resolve(preservedPath))
6363
- )
6330
+ console.log(
6331
+ ` Repo check time: ${formatDuration(metrics.elapsedMs)} total, ${formatDuration(metrics.slowestMs)} slowest`
6364
6332
  );
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
6333
  }
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
6391
- );
6334
+ function writeJson(value) {
6335
+ console.log(JSON.stringify(value, null, 2));
6392
6336
  }
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;
6337
+ function warnRetainedLegacyDatabase(retained) {
6338
+ if (retained.pendingEvents !== null && retained.pendingHookEvents !== null && retained.pendingOtlpEnvelopes !== null) {
6339
+ console.warn(
6340
+ `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.`
6341
+ );
6342
+ return;
6417
6343
  }
6344
+ console.warn(
6345
+ `Warning: Retained legacy observer database at ${retained.databaseFile}; pending row count unavailable. FML will not sync that database automatically.`
6346
+ );
6418
6347
  }
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
6348
+ function printStatus(status) {
6349
+ const { queue } = status;
6350
+ console.log(
6351
+ `Raw artifact sync: ${status.settings.enabled ? "enabled" : "disabled"}`
6425
6352
  );
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");
6353
+ console.log(` State: ${FML_OBSERVER_DIR}`);
6354
+ console.log(` Tracked: ${queue.tracked}`);
6355
+ console.log(` Pending: ${queue.pending}`);
6356
+ console.log(` Raw work: ${queue.rawPending}`);
6357
+ console.log(` Metadata: ${queue.metadataPending}`);
6358
+ console.log(` Uploaded: ${queue.uploaded}`);
6359
+ console.log(` Failed: ${queue.failed}`);
6360
+ console.log(
6361
+ ` Watermark: ${formatBytes(queue.uploadedBytes)} / ${formatBytes(queue.totalBytes)}`
6362
+ );
6363
+ console.log(` Remaining: ${formatBytes(queue.remainingBytes)}`);
6440
6364
  }
6441
- function purgeWarning(roots) {
6442
- return `${PURGE_WARNING}
6443
- Purge roots:
6444
- ${roots.map((root) => ` - ${root.directory}`).join("\n")}`;
6365
+ function syncExitCode(result) {
6366
+ if (result.outcome === "failed" || result.outcome === "exhausted") return 1;
6367
+ if (result.outcome === "busy") return 1;
6368
+ return 0;
6445
6369
  }
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;
6479
- }
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}`);
6370
+ async function handleSyncRun(opts = {}) {
6371
+ const sync2 = rawSync();
6372
+ if (!sync2.getSettings().enabled) {
6373
+ const result2 = {
6374
+ ok: true,
6375
+ outcome: "disabled",
6376
+ status: sync2.status()
6377
+ };
6378
+ if (opts.json) writeJson(result2);
6379
+ else printStatus(result2.status);
6380
+ return;
6486
6381
  }
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;
6382
+ if (opts.dryRun) {
6383
+ const discovery = sync2.discover();
6384
+ const bySource = {};
6385
+ let totalBytes = 0;
6386
+ for (const candidate of discovery.candidates) {
6387
+ bySource[candidate.source] = (bySource[candidate.source] ?? 0) + 1;
6388
+ try {
6389
+ totalBytes += statSync(candidate.sourcePath).size;
6390
+ } catch {
6391
+ }
6500
6392
  }
6501
- for (const root of purgeRoots) {
6502
- if (!fs12.existsSync(root.directory) && pathContains(root.directory, FML_OBSERVER_DIR)) {
6503
- prepareManagedDirectory(root);
6393
+ const result2 = {
6394
+ ok: true,
6395
+ dryRun: true,
6396
+ targetUrl: getSiteUrl(),
6397
+ discovered: {
6398
+ count: discovery.candidates.length,
6399
+ totalBytes,
6400
+ bySource
6401
+ },
6402
+ repositoryResolution: discovery.repositoryResolution,
6403
+ oversized: discovery.oversized,
6404
+ status: sync2.status()
6405
+ };
6406
+ if (opts.json) writeJson(result2);
6407
+ else {
6408
+ console.log("FML raw artifact sync dry-run");
6409
+ console.log(` Target: ${result2.targetUrl}`);
6410
+ console.log(` Discovered: ${result2.discovered.count}`);
6411
+ console.log(` Bytes: ${formatBytes(result2.discovered.totalBytes)}`);
6412
+ for (const [source, count] of Object.entries(bySource).sort()) {
6413
+ console.log(` ${source}: ${count}`);
6414
+ }
6415
+ printRepositoryResolution(discovery.repositoryResolution);
6416
+ console.log(` Oversized: ${discovery.oversized.length}`);
6417
+ if (discovery.compressedArtifactsUnsupportedReason) {
6418
+ console.log(
6419
+ ` Compressed: skipped \u2014 ${discovery.compressedArtifactsUnsupportedReason}`
6420
+ );
6504
6421
  }
6505
6422
  }
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
6423
  return;
6523
6424
  }
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
- );
6425
+ const attempt = await runRawSyncOnce();
6426
+ if (attempt.kind === "unauthenticated") {
6427
+ console.error("Raw artifact sync requires an FML login.");
6428
+ console.error("Run `fml login` or set FML_TOKEN.");
6528
6429
  process.exitCode = 1;
6529
6430
  return;
6530
6431
  }
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 {
6432
+ if (attempt.kind === "disabled") {
6433
+ if (opts.json) {
6434
+ writeJson({ ok: true, outcome: "disabled", status: attempt.status });
6435
+ } else {
6436
+ printStatus(attempt.status);
6546
6437
  }
6438
+ return;
6547
6439
  }
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)
6440
+ const { result, targetUrl } = attempt;
6441
+ if (opts.json) {
6442
+ writeJson({ ok: syncExitCode(result) === 0, targetUrl, result });
6443
+ } else {
6444
+ console.log("FML raw artifact sync");
6445
+ console.log(` Target: ${targetUrl}`);
6446
+ console.log(` Outcome: ${result.outcome}`);
6447
+ console.log(` Sync passes: ${result.syncPasses}`);
6448
+ console.log(` Discovered: ${result.discovery.candidates.length}`);
6449
+ if (result.discovery.compressedArtifactsUnsupportedReason) {
6450
+ console.log(
6451
+ ` Compressed: skipped \u2014 ${result.discovery.compressedArtifactsUnsupportedReason}`
6452
+ );
6453
+ }
6454
+ printRepositoryResolution(result.discovery.repositoryResolution);
6455
+ console.log(` Uploaded files: ${result.upload.uploaded}`);
6456
+ console.log(` Uploaded chunks: ${result.upload.uploadedChunks}`);
6457
+ console.log(
6458
+ ` Uploaded bytes: ${formatBytes(result.upload.uploadedBytes)}`
6559
6459
  );
6460
+ console.log(` Failed: ${result.upload.failed}`);
6461
+ console.log(` Exhausted: ${result.upload.exhausted}`);
6462
+ if (result.upload.retryAtMs !== void 0) {
6463
+ console.log(
6464
+ ` Retry at: ${new Date(result.upload.retryAtMs).toISOString()}`
6465
+ );
6466
+ }
6467
+ console.log(` Drained: ${result.drained ? "yes" : "no"}`);
6468
+ if (result.outcome === "pending") {
6469
+ console.log(
6470
+ " Next: More data remains; run `fml sync` again or `fml start` for continuous syncing."
6471
+ );
6472
+ } else if (result.outcome === "backing_off") {
6473
+ const nextRetryAtMs = result.upload.nextRetryAtMs;
6474
+ console.log(
6475
+ ` Next: Remaining items are backing off after upload errors${nextRetryAtMs !== void 0 ? `; next retry due ${new Date(nextRetryAtMs).toISOString()}` : ""}. \`fml start\` retries automatically.`
6476
+ );
6477
+ }
6478
+ console.log("");
6479
+ printStatus(result.status);
6560
6480
  }
6561
- console.log(" Plugin settings removed");
6562
- console.log("[2/7] Removing agent-native /fml skills...");
6481
+ process.exitCode = syncExitCode(result);
6482
+ }
6483
+ function setRawSyncEnabled(enabled) {
6484
+ return rawSync().setEnabled(enabled);
6485
+ }
6486
+ async function handleSyncEnable(opts = {}) {
6487
+ const sync2 = rawSync();
6488
+ const settings = sync2.setEnabled(true);
6489
+ const isolationWarning = observerOnlyOverrideWarning();
6490
+ if (isolationWarning) console.warn(isolationWarning);
6491
+ let retainedLegacyDatabase;
6563
6492
  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}`
6493
+ const observer = await startObserver({
6494
+ onLegacyDatabaseRetained: (retained) => {
6495
+ retainedLegacyDatabase = retained;
6496
+ if (!opts.json) warnRetainedLegacyDatabase(retained);
6497
+ }
6498
+ });
6499
+ setHookCaptureDisabled(false);
6500
+ if (observer.status === "already_running") {
6501
+ await wakeObserverSync();
6502
+ }
6503
+ const port = observer.runtime.health?.port;
6504
+ const authToken = port ? readObserverAuthToken(FML_OBSERVER_AUTH_TOKEN_FILE) : null;
6505
+ if (port && authToken) {
6506
+ configureFmlOtel({
6507
+ baseUrl: `http://127.0.0.1:${port}`,
6508
+ authToken
6509
+ });
6510
+ } else if (!opts.json) {
6511
+ console.warn(
6512
+ "Warning: Could not configure native OpenTelemetry: the healthy observer endpoint or local token is unavailable."
6513
+ );
6514
+ }
6515
+ if (opts.json) {
6516
+ writeJson({
6517
+ ok: true,
6518
+ sync: settings,
6519
+ observer,
6520
+ ...observer.retainedLegacyDatabase ? { retainedLegacyDatabase: observer.retainedLegacyDatabase } : {}
6521
+ });
6522
+ } else {
6523
+ const pid = observer.runtime.health?.pid;
6524
+ if (observer.status === "started") {
6525
+ console.log(
6526
+ pid ? `FML observer started (PID ${pid}).` : "FML observer started."
6573
6527
  );
6528
+ console.log("Agent activity will sync automatically.");
6574
6529
  } else {
6575
6530
  console.log(
6576
- ` ${targets}: ${result.destinationPath} (${result.status})`
6531
+ pid ? `FML observer is already running (PID ${pid}).` : "FML observer is already running."
6577
6532
  );
6533
+ console.log("Agent activity sync is on.");
6578
6534
  }
6579
6535
  }
6580
6536
  } catch (error) {
6581
6537
  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
- );
6538
+ if (opts.json) {
6539
+ writeJson({
6540
+ ok: false,
6541
+ sync: settings,
6542
+ error: message,
6543
+ ...retainedLegacyDatabase ? { retainedLegacyDatabase } : {}
6544
+ });
6545
+ } else console.error(message);
6546
+ process.exitCode = 1;
6591
6547
  }
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;
6548
+ }
6549
+ async function handleSyncDisable(opts = {}) {
6550
+ const sync2 = rawSync();
6551
+ const settings = sync2.setEnabled(false);
6552
+ const observerWoken = await wakeObserverSync();
6553
+ if (opts.json) {
6554
+ writeJson({ ok: true, sync: settings, observerWoken });
6555
+ } else {
6556
+ console.log("Agent activity sync is paused.");
6557
+ const observer = await readObserverRuntimeStatus();
6558
+ if (observer.running) {
6559
+ console.log("FML observer remains available for local integrations");
6609
6560
  }
6610
- })) {
6611
- console.log(` Removed FML hooks from ${target2.displayName}`);
6612
- }
6613
- if (!hookRemovalComplete) {
6614
- process.exitCode = 1;
6615
- console.warn(
6616
- " Some FML hook registrations were preserved; repair the paths reported above and rerun `fml uninstall`."
6617
- );
6618
- }
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}`);
6622
6561
  }
6623
- if (!otelDestination) {
6624
- console.warn(
6625
- " The local observer endpoint or token is unavailable; only recorded OpenTelemetry profiles could be cleaned"
6562
+ }
6563
+ async function handleSyncStatus(opts = {}) {
6564
+ const status = rawSync().status();
6565
+ const observer = await readObserverRuntimeStatus();
6566
+ if (opts.json) writeJson({ ok: true, status, observer });
6567
+ else {
6568
+ printStatus(status);
6569
+ const state = observer.stateStatus.state;
6570
+ console.log(
6571
+ ` Observer: ${observer.healthy ? "running" : observer.running ? "unhealthy" : "stopped"}`
6626
6572
  );
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
6573
+ if (state) {
6574
+ console.log(` PID: ${state.pid}`);
6575
+ console.log(` Phase: ${state.phase}`);
6576
+ if (state.lastOutcome) {
6577
+ console.log(` Last run: ${state.lastOutcome}`);
6578
+ }
6579
+ if (state.nextAttemptAtMs) {
6580
+ console.log(
6581
+ ` Next run: ${new Date(state.nextAttemptAtMs).toISOString()}`
6646
6582
  );
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
6583
  }
6672
6584
  }
6673
- if (!hookCapturePaused) hookCapturePaused = tryDisableHookCapture();
6585
+ console.log(` Log: ${observer.logFile}`);
6586
+ }
6587
+ }
6588
+ async function handleSyncReset(opts = {}) {
6589
+ const result = rawSync().reset();
6590
+ if (opts.json) writeJson({ ok: !result.skipped, reset: result });
6591
+ else if (result.skipped) {
6592
+ console.error(
6593
+ "Raw artifact reset skipped: another sync owns the queue lock"
6594
+ );
6674
6595
  } else {
6675
6596
  console.log(
6676
- " Preserved auth, logs, raw-sync watermarks, and the bounded OTLP spool"
6597
+ `Reset raw upload watermarks for ${result.reset} artifact${result.reset === 1 ? "" : "s"}.`
6677
6598
  );
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."
6599
+ if (result.removed > 0) {
6600
+ console.log(`Removed ${result.removed} missing artifact entries.`);
6601
+ }
6602
+ console.log(
6603
+ "The next enabled sync will replay each artifact from byte zero."
6683
6604
  );
6684
6605
  }
6685
- if (opts.purge && !purgeComplete) {
6606
+ if (result.skipped) {
6686
6607
  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
- );
6608
+ } else {
6609
+ await wakeObserverSync();
6690
6610
  }
6691
- console.log("\nDone! FML has been uninstalled.");
6692
6611
  }
6693
6612
 
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);
6613
+ // src/commands/daemon.ts
6614
+ async function handleFmlStart() {
6615
+ await handleSyncEnable();
6616
+ }
6617
+ async function stopFml(opts = {}) {
6618
+ setRawSyncEnabled(false);
6619
+ const result = await stopObserver({ force: opts.force });
6620
+ let stopped = result.status === "stopped" || result.status === "killed" || result.status === "not_running" || result.status === "stale_state";
6621
+ if (stopped) {
6622
+ try {
6623
+ setHookCaptureDisabled(true);
6624
+ } catch (error) {
6625
+ const message = error instanceof Error ? error.message : String(error);
6626
+ console.error(
6627
+ `FML observer stopped, but the hook-capture pause could not be persisted: ${message}`
6628
+ );
6629
+ process.exitCode = 1;
6630
+ stopped = false;
6631
+ }
6632
+ }
6633
+ switch (result.status) {
6634
+ case "stopped":
6635
+ console.log(`FML observer stopped (PID ${result.pid}).`);
6636
+ console.log("Agent activity sync is paused.");
6637
+ break;
6638
+ case "killed":
6639
+ console.log(`FML observer stopped (PID ${result.pid}).`);
6640
+ console.log("Agent activity sync is paused.");
6641
+ break;
6642
+ case "not_running":
6643
+ console.log("FML observer is already stopped.");
6644
+ console.log("Agent activity sync is paused.");
6645
+ break;
6646
+ case "stale_state":
6647
+ console.log(
6648
+ "FML observer is stopped. Old process state will be cleaned on the next start."
6649
+ );
6650
+ console.log("Agent activity sync is paused.");
6651
+ break;
6652
+ case "permission_denied":
6653
+ console.error(
6654
+ `Could not stop FML observer PID ${result.pid}: permission denied`
6655
+ );
6656
+ process.exitCode = 1;
6657
+ break;
6658
+ case "foreign_listener":
6659
+ console.error("Refused to stop a different local observer instance");
6660
+ process.exitCode = 1;
6661
+ break;
6662
+ case "ambiguous_live_process":
6663
+ console.error(
6664
+ `Refused to treat unverifiable live PID ${result.pid} as a stopped FML observer`
6665
+ );
6666
+ process.exitCode = 1;
6667
+ break;
6668
+ case "unresponsive":
6669
+ console.error(
6670
+ `Refused to signal unverifiable FML observer PID ${result.pid}; retry with \`fml stop --force\` to escalate`
6671
+ );
6672
+ process.exitCode = 1;
6673
+ break;
6674
+ case "signal_sent":
6675
+ console.error(`FML observer PID ${result.pid} did not stop`);
6676
+ process.exitCode = 1;
6677
+ break;
6678
+ case "force_stop_unverified":
6679
+ console.error(
6680
+ `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.`
6681
+ );
6682
+ process.exitCode = 1;
6683
+ break;
6684
+ }
6685
+ return stopped;
6686
+ }
6687
+ async function handleFmlStop(opts = {}) {
6688
+ await stopFml(opts);
6689
+ }
6690
+ async function handleFmlRestart() {
6691
+ if (await stopFml()) {
6692
+ await handleFmlStart();
6714
6693
  }
6715
6694
  }
6716
6695
 
@@ -7194,8 +7173,9 @@ async function runServiceTokenLogin(refreshToken) {
7194
7173
  }
7195
7174
  await selectOrg(environment);
7196
7175
  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.");
7176
+ console.log(
7177
+ "Queries and agent activity sync will use the stored service token."
7178
+ );
7199
7179
  }
7200
7180
  async function handleServiceTokenLogin(refreshToken) {
7201
7181
  try {
@@ -7212,16 +7192,12 @@ async function handleServiceTokenLogin(refreshToken) {
7212
7192
  process.exit(1);
7213
7193
  }
7214
7194
  }
7215
- async function handleLogin(opts) {
7216
- if (opts?.serviceToken) {
7217
- await handleServiceTokenLogin();
7218
- return;
7219
- }
7195
+ async function runInteractiveLogin(opts = {}) {
7220
7196
  const environment = captureLoginEnvironment();
7221
7197
  const envName = environment.envName;
7222
7198
  const existingToken = await getValidToken({ env: envName });
7223
7199
  if (existingToken) {
7224
- const { readTokens: readTokens2 } = await import("./token-store-R7WXEPLI.js");
7200
+ const { readTokens: readTokens2 } = await import("./token-store-BT5THIAJ.js");
7225
7201
  const stored = readTokens2(envName);
7226
7202
  if (stored?.tokenType === "service") {
7227
7203
  const label = stored.user.id === SERVICE_TOKEN_LOGIN_USER_ID ? "a service token" : `${stored.user.name} (${stored.user.email})`;
@@ -7233,23 +7209,27 @@ async function handleLogin(opts) {
7233
7209
  }
7234
7210
  await linkGitHubIdentity(environment);
7235
7211
  await selectOrg(environment);
7236
- console.log("You're all set! Restart Claude Code to use FML tools.");
7237
- process.exit(0);
7212
+ return;
7238
7213
  }
7239
7214
  console.log(`Signing in to FML (${envName})...`);
7215
+ const useDeviceFlow = opts.device || !await canOpenBrowser();
7216
+ let result;
7217
+ if (useDeviceFlow) {
7218
+ result = await deviceLogin(environment);
7219
+ } else {
7220
+ result = await login(environment);
7221
+ }
7222
+ console.log(`Logged in as ${result.name} (${result.email})`);
7223
+ await linkGitHubIdentity(environment);
7224
+ await selectOrg(environment);
7225
+ }
7226
+ async function handleLogin(opts) {
7227
+ if (opts?.serviceToken) {
7228
+ await handleServiceTokenLogin();
7229
+ return;
7230
+ }
7240
7231
  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.");
7232
+ await runInteractiveLogin({ device: opts?.device });
7253
7233
  process.exit(0);
7254
7234
  } catch (err) {
7255
7235
  Sentry.captureException(err);
@@ -7259,742 +7239,891 @@ Logged in as ${result.name} (${result.email})`);
7259
7239
  }
7260
7240
  }
7261
7241
 
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.`);
7242
+ // src/commands/setup.ts
7243
+ var defaultDependencies = {
7244
+ install: () => handleInstall(),
7245
+ getActiveToken: () => getValidToken({ env: getActiveEnv().name }),
7246
+ login: () => runInteractiveLogin(),
7247
+ stop: () => handleFmlStop(),
7248
+ start: () => handleFmlStart()
7249
+ };
7250
+ async function runStep(name, operation) {
7251
+ const priorExitCode = process.exitCode;
7252
+ await operation();
7253
+ if (process.exitCode && process.exitCode !== priorExitCode) {
7254
+ throw new Error(`FML setup could not complete ${name}.`);
7255
+ }
7256
+ }
7257
+ async function handleSetup(dependencies = defaultDependencies) {
7258
+ if (process.exitCode) {
7259
+ throw new Error("FML setup cannot run after an earlier command failure.");
7260
+ }
7261
+ console.log("Setting up FML...");
7262
+ await runStep("integration installation", dependencies.install);
7263
+ const token = await dependencies.getActiveToken();
7264
+ if (token) {
7265
+ console.log("Already signed in.");
7266
+ } else {
7267
+ await runStep("login", dependencies.login);
7268
+ }
7269
+ await runStep("observer stop", dependencies.stop);
7270
+ await runStep("observer start", dependencies.start);
7271
+ console.log("FML is ready.");
7272
+ console.log("Start a new agent session to load FML tools.");
7273
+ console.log("Run `fml doctor` to verify setup.");
7267
7274
  }
7268
7275
 
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);
7276
+ // src/commands/uninstall.ts
7277
+ import fs12 from "fs";
7278
+ import os7 from "os";
7279
+ import path27 from "path";
7280
+ import readline from "readline/promises";
7281
+ var PURGE_WARNING = "This deletes FML credentials for ALL environments, logs, sync state, and every pending best-effort OTLP envelope.";
7282
+ function tryDisableHookCapture() {
7283
+ try {
7284
+ setHookCaptureDisabled(true);
7285
+ return true;
7286
+ } catch (error) {
7287
+ const message = error instanceof Error ? error.message : String(error);
7288
+ console.warn(
7289
+ `Warning: Could not persist the hook-capture pause at ${FML_HOOK_CAPTURE_DISABLED_FILE}: ${message}`
7290
+ );
7291
+ return false;
7276
7292
  }
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);
7293
+ }
7294
+ function resolvePreservedPaths(preservedFile) {
7295
+ const logicalPath = path27.resolve(preservedFile);
7296
+ try {
7297
+ const canonicalPath = fs12.realpathSync(preservedFile);
7298
+ return pathsAreSame(canonicalPath, logicalPath) ? [logicalPath] : [logicalPath, canonicalPath];
7299
+ } catch {
7300
+ return [logicalPath];
7285
7301
  }
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}`);
7294
- }
7295
- if (!currentId && !current) {
7296
- console.log("\nNo org selected. Run `fml org <slug>` to select one.");
7297
- }
7298
- return;
7302
+ }
7303
+ function purgeDirectoryPreservingPaths(directory, preservedPaths, configuredRoot = false) {
7304
+ const resolvedDirectory = path27.resolve(directory);
7305
+ let directoryStats;
7306
+ try {
7307
+ directoryStats = fs12.lstatSync(directory);
7308
+ } catch (error) {
7309
+ if (error.code === "ENOENT") return true;
7310
+ throw error;
7299
7311
  }
7300
- const match = orgs.find(
7301
- (o) => (o.slug ?? o.name) === slug || o.name === slug
7302
- );
7303
- if (!match) {
7304
- console.error(
7305
- `Organization "${slug}" not found. Available: ${orgs.map((o) => o.slug ?? o.name).join(", ")}`
7312
+ if (directoryStats.isSymbolicLink()) {
7313
+ if (configuredRoot) return false;
7314
+ const containsLogicalMarker = preservedPaths.some(
7315
+ (preservedPath) => pathContains(resolvedDirectory, preservedPath)
7306
7316
  );
7307
- process.exit(1);
7308
- }
7309
- const matchSlug = match.slug ?? match.name;
7310
- await setSelectedOrg(matchSlug, match._id, environment.name);
7311
- console.log(`Selected org: ${match.name} (${matchSlug})`);
7312
- }
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(/\/+$/, "");
7319
- }
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);
7317
+ if (!containsLogicalMarker) {
7318
+ fs12.rmSync(directory, { force: true });
7319
+ return true;
7320
+ }
7321
+ return false;
7326
7322
  }
7327
- let slug;
7323
+ const directoryPaths = [resolvedDirectory];
7328
7324
  try {
7329
- const orgs = await api.queryOrgs();
7330
- if (orgs.length === 0) {
7331
- console.error("No organizations found.");
7332
- process.exit(1);
7325
+ const canonicalDirectory = fs12.realpathSync(directory);
7326
+ if (!pathsAreSame(canonicalDirectory, resolvedDirectory)) {
7327
+ directoryPaths.push(canonicalDirectory);
7333
7328
  }
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
7329
  } catch {
7341
- console.error("Could not fetch organizations. Opening base URL.");
7342
- slug = "";
7343
7330
  }
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);
7331
+ const containsPreservedPath = directoryPaths.some(
7332
+ (directoryPath) => preservedPaths.some(
7333
+ (preservedPath) => pathContains(directoryPath, preservedPath)
7334
+ )
7335
+ );
7336
+ if (!containsPreservedPath) {
7337
+ fs12.rmSync(directory, { recursive: true, force: true });
7338
+ return true;
7351
7339
  }
7352
- }
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`;
7340
+ const isPreservedPath = directoryPaths.some(
7341
+ (directoryPath) => preservedPaths.some(
7342
+ (preservedPath) => pathsAreSame(directoryPath, path27.resolve(preservedPath))
7343
+ )
7344
+ );
7345
+ if (isPreservedPath) return true;
7346
+ if (!directoryStats.isDirectory()) return false;
7347
+ let entries;
7348
+ try {
7349
+ entries = fs12.readdirSync(directory);
7350
+ } catch (error) {
7351
+ if (error.code === "ENOENT") return true;
7352
+ throw error;
7360
7353
  }
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;
7354
+ let complete = true;
7355
+ for (const entry of entries) {
7356
+ const entryPath = path27.join(directory, entry);
7357
+ if (!purgeDirectoryPreservingPaths(entryPath, preservedPaths, false)) {
7358
+ complete = false;
7359
+ }
7366
7360
  }
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"
7361
+ return complete;
7362
+ }
7363
+ function purgeDirectoryPreservingFiles(directory, preservedFiles) {
7364
+ if (hasSymlinkedParentBelowTrustedAnchor(directory)) return false;
7365
+ return purgeDirectoryPreservingPaths(
7366
+ directory,
7367
+ preservedFiles.flatMap(
7368
+ (preservedFile) => resolvePreservedPaths(preservedFile)
7369
+ ),
7370
+ true
7378
7371
  );
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;
7383
- }
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
- };
7396
7372
  }
7397
- function otlpSpoolStatus(value) {
7398
- if (!value || typeof value !== "object" || Array.isArray(value)) {
7399
- return void 0;
7400
- }
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;
7373
+ function finishCustomRootPurge(directory) {
7374
+ const marker = managedDirectoryMarker(directory);
7375
+ fs12.rmSync(marker, { force: true });
7376
+ try {
7377
+ fs12.rmdirSync(directory);
7378
+ } catch (error) {
7379
+ if (error.code === "ENOENT") return;
7380
+ try {
7381
+ fs12.writeFileSync(marker, FML_MANAGED_DIRECTORY_MARKER_CONTENT, {
7382
+ encoding: "utf8",
7383
+ flag: "wx",
7384
+ mode: 384
7385
+ });
7386
+ } catch (markerError) {
7387
+ if (markerError.code === "EEXIST" && hasManagedDirectoryMarker(directory)) {
7388
+ throw error;
7389
+ }
7390
+ const removalMessage = error instanceof Error ? error.message : String(error);
7391
+ const markerMessage = markerError instanceof Error ? markerError.message : String(markerError);
7392
+ throw new Error(
7393
+ `could not remove ${directory} (${removalMessage}) or restore its FML ownership marker (${markerMessage})`
7394
+ );
7395
+ }
7396
+ throw error;
7414
7397
  }
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
7398
  }
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;
7399
+ function cleanInstalledPluginScopes(paths) {
7400
+ const installedPlugins = readJsonFile(paths.installedPluginsPath);
7401
+ if (!installedPlugins) return;
7402
+ const update = CLAUDE_PLUGIN_INSTALLER.removeInstalledPlugin(
7403
+ installedPlugins,
7404
+ CLAUDE_PLUGIN_INSTALLER.pluginId
7405
+ );
7406
+ if (!update.changed) return;
7407
+ for (const settingsPath2 of CLAUDE_PLUGIN_INSTALLER.projectSettingsPaths(
7408
+ installedPlugins,
7409
+ CLAUDE_PLUGIN_INSTALLER.pluginId
7410
+ )) {
7411
+ removeEnabledPluginFromSettings(
7412
+ settingsPath2,
7413
+ CLAUDE_PLUGIN_INSTALLER.pluginId
7414
+ );
7433
7415
  }
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
- };
7416
+ writeJsonFile(paths.installedPluginsPath, update.installedPlugins);
7442
7417
  }
7443
- function hookStatus(value) {
7444
- const status = observerQueueStatus(value, "hooks");
7445
- if (!status || !value || typeof value !== "object" || Array.isArray(value)) {
7446
- return null;
7447
- }
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
- };
7418
+ function purgeWarning(roots) {
7419
+ return `${PURGE_WARNING}
7420
+ Purge roots:
7421
+ ${roots.map((root) => ` - ${root.directory}`).join("\n")}`;
7459
7422
  }
7460
- function otlpStatus(value) {
7461
- const status = observerQueueStatus(value, "otlp");
7462
- if (!status || !value || typeof value !== "object" || Array.isArray(value)) {
7463
- return null;
7423
+ async function confirmPurge(opts, roots) {
7424
+ const warning = purgeWarning(roots);
7425
+ if (opts.assumeYes) {
7426
+ console.log(warning);
7427
+ return true;
7464
7428
  }
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})`);
7489
- }
7490
- console.log(
7491
- ` Token: ${tokenValid ? "valid" : "expired (needs refresh/re-login)"}`
7492
- );
7493
- console.log(
7494
- ` Org: ${tokens.orgSlug ?? "(none \u2014 run `fml org <slug>`)"}`
7429
+ const input = opts.input ?? process.stdin;
7430
+ if (input.isTTY !== true) {
7431
+ console.error(
7432
+ `${warning}
7433
+ Re-run with --yes to confirm in a non-interactive session.`
7495
7434
  );
7435
+ return false;
7496
7436
  }
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}`);
7524
- }
7437
+ const rl = readline.createInterface({
7438
+ input,
7439
+ output: opts.output ?? process.stdout
7440
+ });
7441
+ try {
7442
+ const closed = new Promise((resolve) => {
7443
+ rl.once("close", () => resolve(null));
7444
+ });
7445
+ const answer = await Promise.race([
7446
+ rl.question(`${warning}
7447
+ Type y to continue: `).catch(() => null),
7448
+ closed
7449
+ ]);
7450
+ if (answer !== null && answer.trim().toLowerCase() === "y") return true;
7451
+ } finally {
7452
+ rl.close();
7525
7453
  }
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
- }
7454
+ console.error("Uninstall canceled; nothing was removed.");
7455
+ return false;
7456
+ }
7457
+ function refuseUnsafePurge(roots) {
7458
+ const unsafeRoots = unsafePurgeRoots(roots);
7459
+ if (unsafeRoots.length === 0) return false;
7460
+ console.error("Refusing to purge unsafe FML directories:");
7461
+ for (const root of unsafeRoots) {
7462
+ console.error(` - ${root.directory}: ${root.reason}`);
7463
+ }
7464
+ console.error(
7465
+ "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."
7466
+ );
7467
+ process.exitCode = 1;
7468
+ return true;
7469
+ }
7470
+ async function handleUninstall(opts) {
7471
+ const purgeRoots = opts.purge ? configuredManagedRoots() : [];
7472
+ if (opts.purge) {
7473
+ if (refuseUnsafePurge(purgeRoots)) return;
7474
+ if (!await confirmPurge(opts, purgeRoots)) {
7475
+ process.exitCode = 1;
7476
+ return;
7545
7477
  }
7546
- if (hooks.spool) {
7547
- console.log(
7548
- ` Spool: ${formatBytes(hooks.spool.logicalBytes)} / ${hooks.spool.maxBytes === null ? "unbounded" : formatBytes(hooks.spool.maxBytes)} (logical content)`
7549
- );
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
- }
7478
+ for (const root of purgeRoots) {
7479
+ if (!fs12.existsSync(root.directory) && pathContains(root.directory, FML_OBSERVER_DIR)) {
7480
+ prepareManagedDirectory(root);
7564
7481
  }
7565
7482
  }
7566
- if (hooks.lastError) console.log(` Error: ${hooks.lastError}`);
7483
+ if (refuseUnsafePurge(purgeRoots)) return;
7567
7484
  }
7568
- if (hookInbox.unreadable) {
7569
- console.log(" Inbox: unavailable");
7570
- } else {
7571
- console.log(
7572
- ` Inbox: ${hookInbox.pendingEntries} pending (${formatBytes(hookInbox.pendingBytes)})`
7485
+ console.log("Removing FML integrations...\n");
7486
+ const otelDestination = readLocalObserverOtelDestination({
7487
+ endpointFile: FML_OBSERVER_ENDPOINT_FILE,
7488
+ authTokenFile: FML_OBSERVER_AUTH_TOKEN_FILE
7489
+ });
7490
+ let observer;
7491
+ try {
7492
+ observer = await stopObserver();
7493
+ } catch (error) {
7494
+ console.error(error instanceof Error ? error.message : String(error));
7495
+ console.error(
7496
+ "Could not safely stop the FML observer; uninstall was aborted. Repair the reported files and rerun `fml uninstall`."
7573
7497
  );
7574
- console.log(
7575
- ` Inbox limits: ${hookInbox.maxBytes === null ? "unbounded bytes" : formatBytes(hookInbox.maxBytes)}, ${hookInbox.maxEntries === null ? "unbounded entries" : `${hookInbox.maxEntries} entries`}`
7498
+ process.exitCode = 1;
7499
+ return;
7500
+ }
7501
+ if (observer.status === "permission_denied" || observer.status === "foreign_listener" || observer.status === "ambiguous_live_process" || observer.status === "unresponsive" || observer.status === "signal_sent") {
7502
+ console.error(
7503
+ `Could not safely stop FML observer PID ${observer.pid}; uninstall was aborted.`
7576
7504
  );
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
- }
7505
+ process.exitCode = 1;
7506
+ return;
7507
+ }
7508
+ let hookCapturePaused = tryDisableHookCapture();
7509
+ createRawArtifactSync({ stateDir: FML_OBSERVER_DIR }).setEnabled(false);
7510
+ const paths = CLAUDE_PLUGIN_INSTALLER.resolvePaths({
7511
+ env: process.env,
7512
+ homeDir: os7.homedir()
7513
+ });
7514
+ const claudeBin = resolveBin("claude");
7515
+ if (claudeBin) {
7516
+ try {
7517
+ const command = CLAUDE_PLUGIN_INSTALLER.uninstallCommand;
7518
+ execBinSync(claudeBin, [...command.args], {
7519
+ timeout: command.timeoutMs
7520
+ });
7521
+ } catch {
7586
7522
  }
7587
- if (hookInbox.retiredEntries > 0) {
7588
- console.log(
7589
- ` Inbox retired: ${hookInbox.retiredEntries} record(s), ${formatBytes(hookInbox.retiredBytes)}`
7523
+ }
7524
+ cleanInstalledPluginScopes(paths);
7525
+ const settings = readJsonFile(paths.settingsPath);
7526
+ if (settings) {
7527
+ const marketplace2 = readJsonFile(paths.marketplaceManifestPath);
7528
+ const keepLocalMarketplace = CLAUDE_PLUGIN_INSTALLER.hasOtherLocalMarketplacePlugins(
7529
+ settings,
7530
+ marketplace2
7531
+ );
7532
+ writeJsonFile(
7533
+ paths.settingsPath,
7534
+ CLAUDE_PLUGIN_INSTALLER.disableInSettings(settings, keepLocalMarketplace)
7535
+ );
7536
+ }
7537
+ let skillsRemoved = true;
7538
+ try {
7539
+ for (const result of removeManagedAgentSkills(resolvePackageRoot())) {
7540
+ if (result.status !== "preserved" && !result.reason) continue;
7541
+ skillsRemoved = false;
7542
+ const targets = result.displayNames.join(", ");
7543
+ console.warn(
7544
+ `Warning: Preserved ${result.destinationPath} for ${targets}: ${result.reason ?? "cleanup could not be verified"}`
7590
7545
  );
7591
7546
  }
7547
+ } catch (error) {
7548
+ skillsRemoved = false;
7549
+ const message = error instanceof Error ? error.message : String(error);
7550
+ console.warn(
7551
+ `Warning: Agent skill cleanup could not be completed: ${message}`
7552
+ );
7592
7553
  }
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)`
7554
+ let integrationsRemoved = true;
7555
+ if (removeMarketplaceLink(paths.marketplaceLinkPath) === "preserved") {
7556
+ integrationsRemoved = false;
7557
+ console.warn(
7558
+ `Warning: Preserved non-symlink marketplace path ${paths.marketplaceLinkPath}`
7559
+ );
7560
+ }
7561
+ const marketplace = readJsonFile(paths.marketplaceManifestPath);
7562
+ if (marketplace && Array.isArray(marketplace.plugins)) {
7563
+ writeJsonFile(
7564
+ paths.marketplaceManifestPath,
7565
+ CLAUDE_PLUGIN_INSTALLER.removeMarketplaceEntry(
7566
+ marketplace,
7567
+ CLAUDE_PLUGIN_INSTALLER.pluginName
7568
+ )
7569
+ );
7570
+ }
7571
+ fs12.rmSync(paths.pluginCacheDir, { recursive: true, force: true });
7572
+ const reportIntegrationWarning = (message) => {
7573
+ integrationsRemoved = false;
7574
+ console.warn(message.replace(/^warn:/u, "Warning:"));
7575
+ };
7576
+ let hookRemovalComplete = true;
7577
+ removeFmlHooks({
7578
+ onFailure: (target2) => {
7579
+ hookRemovalComplete = false;
7580
+ reportIntegrationWarning(
7581
+ `Warning: Preserved FML hook registration at ${target2.configPath}: ${target2.reason}`
7604
7582
  );
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`
7613
- );
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()}`}`
7617
- );
7618
- }
7619
- }
7620
7583
  }
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
- );
7584
+ });
7585
+ if (!hookRemovalComplete) {
7586
+ integrationsRemoved = false;
7587
+ process.exitCode = 1;
7588
+ console.warn(
7589
+ "Warning: Some FML hook registrations were preserved; repair the paths reported above and rerun `fml uninstall`."
7590
+ );
7591
+ }
7592
+ removeFmlOtel(otelDestination, { warn: reportIntegrationWarning });
7593
+ if (!otelDestination) {
7594
+ integrationsRemoved = false;
7595
+ console.warn(
7596
+ "Warning: The local observer endpoint or token is unavailable; only recorded OpenTelemetry profiles could be cleaned"
7597
+ );
7598
+ }
7599
+ removeFmlDirectMcp("all", {
7600
+ log: () => void 0,
7601
+ warn: reportIntegrationWarning
7602
+ });
7603
+ let purgeComplete = true;
7604
+ if (opts.purge) {
7605
+ if (refuseUnsafePurge(purgeRoots)) {
7606
+ purgeComplete = false;
7607
+ } else {
7608
+ for (const root of purgeRoots) {
7609
+ if (refuseUnsafePurge([root])) {
7610
+ purgeComplete = false;
7611
+ continue;
7612
+ }
7613
+ const containsObserverDirectory = pathContains(
7614
+ root.directory,
7615
+ FML_OBSERVER_DIR
7616
+ );
7617
+ const customManagedRoot = !root.nativeDefault;
7618
+ try {
7619
+ const purged = purgeDirectoryPreservingFiles(root.directory, [
7620
+ ...customManagedRoot ? [managedDirectoryMarker(root.directory)] : [],
7621
+ ...containsObserverDirectory && hookCapturePaused ? [FML_HOOK_CAPTURE_DISABLED_FILE] : []
7622
+ ]);
7623
+ if (!purged) {
7624
+ purgeComplete = false;
7625
+ console.warn(
7626
+ `Warning: Preserved ${root.directory}: a symbolic link prevents a complete safe purge`
7627
+ );
7628
+ continue;
7629
+ }
7630
+ if (customManagedRoot && !containsObserverDirectory) {
7631
+ finishCustomRootPurge(root.directory);
7632
+ }
7633
+ } catch (error) {
7634
+ purgeComplete = false;
7635
+ const message = error instanceof Error ? error.message : String(error);
7636
+ console.warn(
7637
+ `Warning: Preserved ${root.directory}: the safe purge was incomplete: ${message}`
7638
+ );
7639
+ }
7640
+ }
7627
7641
  }
7642
+ if (!hookCapturePaused) hookCapturePaused = tryDisableHookCapture();
7643
+ }
7644
+ if (!hookCapturePaused) {
7645
+ process.exitCode = 1;
7646
+ integrationsRemoved = false;
7647
+ console.warn(
7648
+ "Warning: FML cleanup is incomplete; cached agent processes may keep invoking hooks until they exit."
7649
+ );
7650
+ }
7651
+ if (opts.purge && !purgeComplete) {
7652
+ process.exitCode = 1;
7653
+ console.warn(
7654
+ "Warning: Some local data remains because a purge target could not be removed safely. Repair the reported path and rerun `fml uninstall --purge`."
7655
+ );
7656
+ }
7657
+ if (skillsRemoved) {
7658
+ console.log("\u2713 Agent skills removed");
7659
+ } else {
7660
+ console.warn(
7661
+ "Warning: Agent skills were not fully removed. See warnings above."
7662
+ );
7663
+ }
7664
+ if (integrationsRemoved) {
7665
+ console.log("\u2713 Coding-agent integrations removed");
7666
+ } else {
7667
+ console.warn(
7668
+ "Warning: Coding-agent integrations were not fully removed. See warnings above."
7669
+ );
7670
+ }
7671
+ if (!opts.purge) {
7672
+ console.log("\u2713 Local data preserved");
7673
+ } else if (purgeComplete && hookCapturePaused) {
7674
+ console.log("\u2713 Local data deleted");
7675
+ } else {
7676
+ console.warn(
7677
+ "Warning: Local data was not fully deleted. See warnings above."
7678
+ );
7679
+ }
7680
+ const removalComplete = skillsRemoved && integrationsRemoved && (!opts.purge || purgeComplete && hookCapturePaused);
7681
+ if (removalComplete) {
7682
+ console.log("\nFML integrations removed.");
7683
+ } else {
7684
+ console.warn(
7685
+ "FML integration removal is incomplete; review the warnings above."
7686
+ );
7628
7687
  }
7629
7688
  }
7630
7689
 
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`;
7690
+ // src/commands/update.ts
7691
+ 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-]+)*)?$/;
7692
+ function getCurrentVersion() {
7693
+ return true ? "0.8.0-rc.2+bed6eaed" : "unknown";
7694
+ }
7695
+ function resolveUpdateChannel(version) {
7696
+ const match = SEMVER_PATTERN.exec(version);
7697
+ if (!match) return "latest";
7698
+ const prerelease = match[1];
7699
+ if (!prerelease) return "latest";
7700
+ return prerelease.split(".").every((identifier) => {
7701
+ return !(identifier.length > 1 && identifier.startsWith("0") && /^[0-9]+$/u.test(identifier));
7702
+ }) ? "next" : "latest";
7703
+ }
7704
+ function resolveUpdateTarget(version) {
7705
+ return `@fml-inc/fml@${resolveUpdateChannel(version)}`;
7706
+ }
7707
+ function createUpdateEnvironment(env) {
7708
+ const updateEnv = { ...env };
7709
+ for (const key of Object.keys(updateEnv)) {
7710
+ if (key.toLowerCase() === "npm_config_registry") {
7711
+ delete updateEnv[key];
7712
+ }
7713
+ }
7714
+ return updateEnv;
7715
+ }
7716
+ function formatUpdateError(err) {
7717
+ if (!(err instanceof Error)) return String(err);
7718
+ const stderr = err.stderr;
7719
+ if (typeof stderr === "string") return stderr;
7720
+ if (Buffer.isBuffer(stderr)) return stderr.toString();
7721
+ return err.message;
7722
+ }
7723
+ async function handleUpdate(overrides = {}) {
7724
+ const dependencies = {
7725
+ currentVersion: getCurrentVersion(),
7726
+ env: process.env,
7727
+ resolveBin,
7728
+ execBinSync,
7729
+ captureException: Sentry.captureException,
7730
+ log: console.log,
7731
+ error: console.error,
7732
+ exit: process.exit,
7733
+ ...overrides
7734
+ };
7735
+ const channel = resolveUpdateChannel(dependencies.currentVersion);
7736
+ const target2 = resolveUpdateTarget(dependencies.currentVersion);
7737
+ dependencies.log(`Current version: ${dependencies.currentVersion}`);
7738
+ dependencies.log(`Updating FML from the ${channel} channel...
7739
+ `);
7740
+ try {
7741
+ const npm = dependencies.resolveBin("npm");
7742
+ if (!npm) throw new Error("npm was not found on PATH");
7743
+ dependencies.execBinSync(npm, ["install", "--global", target2], {
7744
+ timeout: 12e4,
7745
+ env: createUpdateEnvironment(dependencies.env)
7746
+ });
7747
+ dependencies.log(`
7748
+ FML update complete (${channel} channel).`);
7749
+ dependencies.log("Run `fml setup` to refresh integrations.");
7750
+ } catch (err) {
7751
+ dependencies.captureException(err);
7752
+ dependencies.error("Update failed:", formatUpdateError(err));
7753
+ dependencies.exit(1);
7641
7754
  }
7642
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
7643
7755
  }
7644
- function formatDuration(durationMs) {
7645
- if (durationMs < 1e3) return `${Math.round(durationMs)} ms`;
7646
- return `${(durationMs / 1e3).toFixed(2)} s`;
7647
- }
7648
- function printRepositoryResolution(metrics) {
7649
- if (metrics.attempted === 0 && metrics.budgetSkipped === 0) return;
7756
+
7757
+ // src/commands/logout.ts
7758
+ async function handleLogout() {
7759
+ const { name } = getActiveEnv();
7760
+ await clearStoredCredentials(name);
7650
7761
  console.log(
7651
- ` Repo checks: ${metrics.attempted} (${metrics.resolved} resolved, ${metrics.unresolved} unresolved, ${metrics.cacheHits} cached, ${metrics.budgetSkipped} skipped by budget)`
7762
+ `Logged out. Stored credentials for the ${name} environment were cleared.`
7652
7763
  );
7653
- console.log(
7654
- ` Repo check time: ${formatDuration(metrics.elapsedMs)} total, ${formatDuration(metrics.slowestMs)} slowest`
7764
+ }
7765
+
7766
+ // src/commands/org.ts
7767
+ async function handleOrg(slug) {
7768
+ const environment = getActiveEnv();
7769
+ const token = await getValidToken({ env: environment.name });
7770
+ if (!token) {
7771
+ console.error("Not logged in. Run `fml login` first.");
7772
+ process.exit(1);
7773
+ }
7774
+ const api = createFmlClient(token, {
7775
+ envName: environment.name,
7776
+ siteUrl: getSiteUrlForConvexUrl(environment.convexUrl)
7777
+ });
7778
+ const orgs = await api.queryOrgs();
7779
+ if (orgs.length === 0) {
7780
+ console.error("No organizations found.");
7781
+ process.exit(1);
7782
+ }
7783
+ const current = getSelectedOrg(environment.name);
7784
+ const currentId = getSelectedOrgId(environment.name);
7785
+ if (!slug) {
7786
+ console.log("Organizations:\n");
7787
+ for (const org of orgs) {
7788
+ const orgSlug = org.slug ?? org.name;
7789
+ const marker = org._id === currentId || !currentId && orgSlug === current ? " (selected)" : "";
7790
+ console.log(` ${orgSlug}${marker}`);
7791
+ }
7792
+ if (!currentId && !current) {
7793
+ console.log("\nNo org selected. Run `fml org <slug>` to select one.");
7794
+ }
7795
+ return;
7796
+ }
7797
+ const match = orgs.find(
7798
+ (o) => (o.slug ?? o.name) === slug || o.name === slug
7655
7799
  );
7800
+ if (!match) {
7801
+ console.error(
7802
+ `Organization "${slug}" not found. Available: ${orgs.map((o) => o.slug ?? o.name).join(", ")}`
7803
+ );
7804
+ process.exit(1);
7805
+ }
7806
+ const matchSlug = match.slug ?? match.name;
7807
+ await setSelectedOrg(matchSlug, match._id, environment.name);
7808
+ console.log(`Selected org: ${match.name} (${matchSlug})`);
7656
7809
  }
7657
- function writeJson(value) {
7658
- console.log(JSON.stringify(value, null, 2));
7810
+
7811
+ // src/commands/open.ts
7812
+ var DEFAULT_APP_BASE_URL = "https://app.fml.inc";
7813
+ function appBaseUrl() {
7814
+ const configured = process.env.FML_APP_URL?.trim();
7815
+ return (configured || DEFAULT_APP_BASE_URL).replace(/\/+$/, "");
7659
7816
  }
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.`
7817
+ async function handleOpen(opts) {
7818
+ const environment = getActiveEnv();
7819
+ const api = await getAuthenticatedClient(environment);
7820
+ if (!api) {
7821
+ console.error("Not signed in. Run `fml login` first.");
7822
+ process.exit(1);
7823
+ }
7824
+ let slug;
7825
+ try {
7826
+ const orgs = await api.queryOrgs();
7827
+ if (orgs.length === 0) {
7828
+ console.error("No organizations are available for this account.");
7829
+ process.exit(1);
7830
+ }
7831
+ const selected = getSelectedOrg(environment.name);
7832
+ const org = selected && orgs.find((candidate) => {
7833
+ const candidateSlug = candidate.slug ?? candidate.name;
7834
+ return candidateSlug === selected || candidate.name === selected;
7835
+ }) || orgs[0];
7836
+ slug = org.slug ?? org.name;
7837
+ } catch {
7838
+ console.error(
7839
+ "Could not load your organizations. Opening the FML dashboard."
7664
7840
  );
7665
- return;
7841
+ slug = "";
7842
+ }
7843
+ const baseUrl = appBaseUrl();
7844
+ const url = slug ? `${baseUrl}/${slug}` : baseUrl;
7845
+ if (opts.json) {
7846
+ console.log(JSON.stringify({ url }));
7847
+ } else {
7848
+ const openBrowser = (await import("open")).default;
7849
+ await openBrowser(url);
7666
7850
  }
7667
- console.warn(
7668
- `Retained legacy observer database at ${retained.databaseFile}; pending row count unavailable. FML will not sync that database automatically.`
7669
- );
7670
7851
  }
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)}`);
7852
+
7853
+ // src/commands/status.ts
7854
+ function formatBytes2(bytes) {
7855
+ if (bytes < 1024) return `${bytes} B`;
7856
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
7857
+ if (bytes < 1024 * 1024 * 1024) {
7858
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
7859
+ }
7860
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
7687
7861
  }
7688
- function syncExitCode(result) {
7689
- if (result.outcome === "failed" || result.outcome === "exhausted") return 1;
7690
- if (result.outcome === "busy") return 1;
7691
- return 0;
7862
+ function hookSpoolStatus(value) {
7863
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7864
+ return void 0;
7865
+ }
7866
+ const record = value;
7867
+ const nonNegativeInteger = (field) => {
7868
+ const candidate = record[field];
7869
+ return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0 ? candidate : void 0;
7870
+ };
7871
+ const logicalBytes = nonNegativeInteger("logicalBytes");
7872
+ const evictedRecords = nonNegativeInteger("evictedRecords");
7873
+ const evictedBytes = nonNegativeInteger("evictedBytes");
7874
+ const evictedPendingRecords = nonNegativeInteger("evictedPendingRecords");
7875
+ const evictedQuarantinedRecords = nonNegativeInteger(
7876
+ "evictedQuarantinedRecords"
7877
+ );
7878
+ const maxBytes = record.maxBytes;
7879
+ const quarantineMaxAgeMs = record.quarantineMaxAgeMs;
7880
+ 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)) {
7881
+ return void 0;
7882
+ }
7883
+ const lastEvictedAtMsValue = nonNegativeInteger("lastEvictedAtMs");
7884
+ const lastEvictedAtMs = lastEvictedAtMsValue !== void 0 && lastEvictedAtMsValue <= 864e13 ? lastEvictedAtMsValue : void 0;
7885
+ return {
7886
+ logicalBytes,
7887
+ maxBytes,
7888
+ ...quarantineMaxAgeMs === null ? { quarantineMaxAgeMs: null } : typeof quarantineMaxAgeMs === "number" && Number.isSafeInteger(quarantineMaxAgeMs) && quarantineMaxAgeMs > 0 ? { quarantineMaxAgeMs } : {},
7889
+ evictedRecords,
7890
+ evictedBytes,
7891
+ evictedPendingRecords,
7892
+ evictedQuarantinedRecords,
7893
+ ...lastEvictedAtMs === void 0 ? {} : { lastEvictedAtMs }
7894
+ };
7692
7895
  }
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;
7896
+ function otlpSpoolStatus(value) {
7897
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7898
+ return void 0;
7704
7899
  }
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
- }
7715
- }
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
- }
7745
- }
7746
- return;
7900
+ const record = value;
7901
+ const nonNegativeInteger = (field) => {
7902
+ const candidate = record[field];
7903
+ return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0 ? candidate : void 0;
7904
+ };
7905
+ const logicalBytes = nonNegativeInteger("logicalBytes");
7906
+ const bodyBytes = nonNegativeInteger("bodyBytes");
7907
+ const maxBytes = nonNegativeInteger("maxBytes");
7908
+ const droppedRecords = nonNegativeInteger("droppedRecords");
7909
+ const droppedLogicalBytes = nonNegativeInteger("droppedLogicalBytes");
7910
+ const droppedBodyBytes = nonNegativeInteger("droppedBodyBytes");
7911
+ 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) {
7912
+ return void 0;
7747
7913
  }
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;
7914
+ return {
7915
+ logicalBytes,
7916
+ bodyBytes,
7917
+ maxBytes,
7918
+ droppedRecords,
7919
+ droppedLogicalBytes,
7920
+ droppedBodyBytes,
7921
+ ...typeof record.lastDroppedAtMs === "number" && Number.isSafeInteger(record.lastDroppedAtMs) && record.lastDroppedAtMs >= 0 && Number.isFinite(new Date(record.lastDroppedAtMs).getTime()) ? { lastDroppedAtMs: record.lastDroppedAtMs } : {},
7922
+ ...typeof record.lastDropReason === "string" ? { lastDropReason: record.lastDropReason } : {}
7923
+ };
7924
+ }
7925
+ function observerQueueStatus(value, key) {
7926
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
7927
+ const queue = value[key];
7928
+ if (!queue || typeof queue !== "object" || Array.isArray(queue)) return null;
7929
+ const record = queue;
7930
+ if (typeof record.phase !== "string" || typeof record.pending !== "number" || !Number.isSafeInteger(record.pending) || record.pending < 0) {
7931
+ return null;
7754
7932
  }
7755
- if (attempt.kind === "disabled") {
7756
- if (opts.json) {
7757
- writeJson({ ok: true, outcome: "disabled", status: attempt.status });
7758
- } else {
7759
- printStatus(attempt.status);
7760
- }
7761
- return;
7933
+ return {
7934
+ phase: record.phase,
7935
+ pending: record.pending,
7936
+ ...typeof record.quarantined === "number" && Number.isSafeInteger(record.quarantined) && record.quarantined >= 0 ? { quarantined: record.quarantined } : {},
7937
+ ...typeof record.lastError === "string" ? { lastError: record.lastError } : {},
7938
+ ...typeof record.lastWarning === "string" ? { lastWarning: record.lastWarning } : {},
7939
+ ...typeof record.nextAttemptAtMs === "number" && Number.isSafeInteger(record.nextAttemptAtMs) && record.nextAttemptAtMs >= 0 && Number.isFinite(new Date(record.nextAttemptAtMs).getTime()) ? { nextAttemptAtMs: record.nextAttemptAtMs } : {}
7940
+ };
7941
+ }
7942
+ function hookStatus(value) {
7943
+ const status = observerQueueStatus(value, "hooks");
7944
+ if (!status || !value || typeof value !== "object" || Array.isArray(value)) {
7945
+ return null;
7762
7946
  }
7763
- const { result, targetUrl } = attempt;
7764
- if (opts.json) {
7765
- writeJson({ ok: syncExitCode(result) === 0, targetUrl, result });
7947
+ const hooks = value.hooks;
7948
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return null;
7949
+ const record = hooks;
7950
+ const spool = hookSpoolStatus(record.spool);
7951
+ return {
7952
+ ...status,
7953
+ ...typeof record.rejected === "number" && Number.isSafeInteger(record.rejected) && record.rejected >= 0 ? { rejected: record.rejected } : {},
7954
+ ...typeof record.lastRejectedReason === "string" ? { lastRejectedReason: record.lastRejectedReason } : {},
7955
+ ...typeof record.lastRejectedTarget === "string" ? { lastRejectedTarget: record.lastRejectedTarget } : {},
7956
+ ...spool === void 0 ? {} : { spool }
7957
+ };
7958
+ }
7959
+ function otlpStatus(value) {
7960
+ const status = observerQueueStatus(value, "otlp");
7961
+ if (!status || !value || typeof value !== "object" || Array.isArray(value)) {
7962
+ return null;
7963
+ }
7964
+ const otlp = value.otlp;
7965
+ if (!otlp || typeof otlp !== "object" || Array.isArray(otlp)) return null;
7966
+ const spool = otlpSpoolStatus(otlp.spool);
7967
+ return {
7968
+ ...status,
7969
+ ...spool === void 0 ? {} : { spool }
7970
+ };
7971
+ }
7972
+ async function handleStatus() {
7973
+ const { name, convexUrl } = getActiveEnv();
7974
+ console.log(`Environment: ${name}`);
7975
+ console.log(` Convex: ${convexUrl ?? "not configured"}
7976
+ `);
7977
+ const tokens = readTokens(name);
7978
+ if (!tokens) {
7979
+ console.log("Authentication:");
7980
+ console.log(" Not authenticated. Run `fml login` to sign in.");
7766
7981
  } 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
- );
7982
+ const tokenValid = await getValidToken({ env: name });
7983
+ console.log("Authentication:");
7984
+ if (tokens.tokenType === "service" && tokens.user.id === SERVICE_TOKEN_LOGIN_USER_ID) {
7985
+ console.log(" Auth: service token");
7986
+ } else {
7987
+ console.log(` User: ${tokens.user.name} (${tokens.user.email})`);
7776
7988
  }
7777
- printRepositoryResolution(result.discovery.repositoryResolution);
7778
- console.log(` Uploaded files: ${result.upload.uploaded}`);
7779
- console.log(` Uploaded chunks: ${result.upload.uploadedChunks}`);
7780
7989
  console.log(
7781
- ` Uploaded bytes: ${formatBytes2(result.upload.uploadedBytes)}`
7990
+ ` Token: ${tokenValid ? "valid" : "expired (needs refresh/re-login)"}`
7782
7991
  );
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
- );
7992
+ console.log(
7993
+ ` Org: ${tokens.orgSlug ?? "(none \u2014 run `fml org <slug>`)"}`
7994
+ );
7995
+ }
7996
+ const syncStatus = createRawArtifactSync({
7997
+ stateDir: FML_OBSERVER_DIR
7998
+ }).status();
7999
+ console.log("\nRaw artifact sync:");
8000
+ console.log(` Enabled: ${syncStatus.settings.enabled ? "yes" : "no"}`);
8001
+ console.log(` Tracked: ${syncStatus.queue.tracked}`);
8002
+ console.log(` Pending: ${syncStatus.queue.pending}`);
8003
+ console.log(` Raw work: ${syncStatus.queue.rawPending}`);
8004
+ console.log(` Metadata: ${syncStatus.queue.metadataPending}`);
8005
+ console.log(` Uploaded: ${syncStatus.queue.uploaded}`);
8006
+ console.log(` Failed: ${syncStatus.queue.failed}`);
8007
+ console.log(
8008
+ ` Watermark: ${formatBytes2(syncStatus.queue.uploadedBytes)} / ${formatBytes2(syncStatus.queue.totalBytes)}`
8009
+ );
8010
+ console.log(` Remaining: ${formatBytes2(syncStatus.queue.remainingBytes)}`);
8011
+ const observer = await readObserverRuntimeStatus();
8012
+ const protectedStatus = observer.healthy ? await readObserverProtectedStatus() : null;
8013
+ const observerState = observer.stateStatus.state;
8014
+ console.log("\nLocal observer:");
8015
+ console.log(
8016
+ ` Status: ${observer.healthy ? "running" : observer.running ? "unhealthy" : "stopped"}`
8017
+ );
8018
+ if (observerState) {
8019
+ console.log(` PID: ${observerState.pid}`);
8020
+ console.log(` Phase: ${observerState.phase}`);
8021
+ if (observerState.lastOutcome) {
8022
+ console.log(` Last: ${observerState.lastOutcome}`);
7800
8023
  }
7801
- console.log("");
7802
- printStatus(result.status);
7803
8024
  }
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();
8025
+ console.log(` Log: ${observer.logFile}`);
8026
+ const hooks = hookStatus(protectedStatus);
8027
+ const hookInbox = readHookInboxStatus();
8028
+ console.log("\nHook capture:");
8029
+ if (!hooks) {
8030
+ console.log(` Status: ${observer.healthy ? "unavailable" : "stopped"}`);
8031
+ } else {
8032
+ console.log(` Status: ${hooks.phase}`);
8033
+ console.log(` Pending: ${hooks.pending}`);
8034
+ if (hooks.quarantined && hooks.quarantined > 0) {
8035
+ console.log(` Quarantined: ${hooks.quarantined}`);
7825
8036
  }
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
- }
8037
+ if (hooks.rejected && hooks.rejected > 0) {
8038
+ console.log(` Rejected: ${hooks.rejected}`);
8039
+ if (hooks.lastRejectedReason) {
8040
+ console.log(
8041
+ ` Last rejection: ${hooks.lastRejectedReason}${hooks.lastRejectedTarget ? ` (${hooks.lastRejectedTarget})` : ""}`
8042
+ );
7836
8043
  }
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
8044
  }
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;
8045
+ if (hooks.spool) {
7852
8046
  console.log(
7853
- observer.status === "started" ? `FML observer started${pid ? ` (PID ${pid})` : ""}` : `FML observer is already running${pid ? ` (PID ${pid})` : ""}`
8047
+ ` Spool: ${formatBytes2(hooks.spool.logicalBytes)} / ${hooks.spool.maxBytes === null ? "unbounded" : formatBytes2(hooks.spool.maxBytes)} (logical content)`
7854
8048
  );
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}`);
8049
+ if (hooks.spool.quarantineMaxAgeMs !== void 0) {
8050
+ console.log(
8051
+ ` Quarantine retention: ${hooks.spool.quarantineMaxAgeMs === null ? "unbounded" : `${Math.round(hooks.spool.quarantineMaxAgeMs / 864e5)} day(s)`}`
8052
+ );
7898
8053
  }
7899
- if (state.nextAttemptAtMs) {
8054
+ if (hooks.spool.evictedRecords > 0) {
7900
8055
  console.log(
7901
- ` Next run: ${new Date(state.nextAttemptAtMs).toISOString()}`
8056
+ ` Evicted: ${hooks.spool.evictedRecords} record(s), ${formatBytes2(hooks.spool.evictedBytes)} (${hooks.spool.evictedQuarantinedRecords} quarantined, ${hooks.spool.evictedPendingRecords} pending)`
7902
8057
  );
8058
+ if (hooks.spool.lastEvictedAtMs !== void 0) {
8059
+ console.log(
8060
+ ` Last eviction: ${new Date(hooks.spool.lastEvictedAtMs).toISOString()}`
8061
+ );
8062
+ }
7903
8063
  }
7904
8064
  }
7905
- console.log(` Log: ${observer.logFile}`);
8065
+ if (hooks.lastError) console.log(` Error: ${hooks.lastError}`);
7906
8066
  }
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
- );
8067
+ if (hookInbox.unreadable) {
8068
+ console.log(" Inbox: unavailable");
7915
8069
  } else {
7916
8070
  console.log(
7917
- `Reset raw upload watermarks for ${result.reset} artifact${result.reset === 1 ? "" : "s"}.`
8071
+ ` Inbox: ${hookInbox.pendingEntries} pending (${formatBytes2(hookInbox.pendingBytes)})`
7918
8072
  );
7919
- if (result.removed > 0) {
7920
- console.log(`Removed ${result.removed} missing artifact entries.`);
7921
- }
7922
8073
  console.log(
7923
- "The next enabled sync will replay each artifact from byte zero."
8074
+ ` Inbox limits: ${hookInbox.maxBytes === null ? "unbounded bytes" : formatBytes2(hookInbox.maxBytes)}, ${hookInbox.maxEntries === null ? "unbounded entries" : `${hookInbox.maxEntries} entries`}`
7924
8075
  );
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}`
8076
+ if (hookInbox.capacityRejectedEntries > 0) {
8077
+ console.log(
8078
+ ` Inbox capacity refusals: ${hookInbox.capacityRejectedEntries} record(s), ${formatBytes2(hookInbox.capacityRejectedBytes)}`
7947
8079
  );
7948
- process.exitCode = 1;
8080
+ if (hookInbox.lastCapacityRejectedAtMs !== void 0) {
8081
+ console.log(
8082
+ ` Last inbox refusal: ${new Date(hookInbox.lastCapacityRejectedAtMs).toISOString()}${hookInbox.lastCapacityRejectionReason ? ` (${hookInbox.lastCapacityRejectionReason})` : ""}`
8083
+ );
8084
+ }
7949
8085
  }
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":
8086
+ if (hookInbox.retiredEntries > 0) {
7962
8087
  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`
8088
+ ` Inbox retired: ${hookInbox.retiredEntries} record(s), ${formatBytes2(hookInbox.retiredBytes)}`
7979
8089
  );
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`
8090
+ }
8091
+ }
8092
+ const otlp = otlpStatus(protectedStatus);
8093
+ console.log("\nNative OpenTelemetry:");
8094
+ if (!otlp) {
8095
+ console.log(` Status: ${observer.healthy ? "unavailable" : "stopped"}`);
8096
+ } else {
8097
+ console.log(` Status: ${otlp.phase}`);
8098
+ console.log(` Pending: ${otlp.pending}`);
8099
+ if (otlp.spool) {
8100
+ const usedPercent = otlp.spool.logicalBytes / otlp.spool.maxBytes * 100;
8101
+ console.log(
8102
+ ` Spool: ${formatBytes2(otlp.spool.logicalBytes)} / ${formatBytes2(otlp.spool.maxBytes)} (${usedPercent.toFixed(1)}%; ${formatBytes2(otlp.spool.bodyBytes)} encoded bodies)`
7985
8103
  );
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.`
8104
+ if (usedPercent >= 80) {
8105
+ console.log(
8106
+ ` Attention: the oldest pending telemetry is dropped when the spool reaches ${formatBytes2(otlp.spool.maxBytes)}`
8107
+ );
8108
+ }
8109
+ if (otlp.spool.droppedRecords > 0) {
8110
+ console.log(
8111
+ ` Dropped: ${otlp.spool.droppedRecords} envelope(s), ${formatBytes2(otlp.spool.droppedLogicalBytes)} logical; ${formatBytes2(otlp.spool.droppedBodyBytes)} encoded bodies`
8112
+ );
8113
+ if (otlp.spool.lastDropReason) {
8114
+ console.log(
8115
+ ` Last drop: ${otlp.spool.lastDropReason}${otlp.spool.lastDroppedAtMs === void 0 ? "" : ` at ${new Date(otlp.spool.lastDroppedAtMs).toISOString()}`}`
8116
+ );
8117
+ }
8118
+ }
8119
+ }
8120
+ if (otlp.lastError) console.log(` Error: ${otlp.lastError}`);
8121
+ if (otlp.lastWarning) console.log(` Warning: ${otlp.lastWarning}`);
8122
+ if (otlp.nextAttemptAtMs !== void 0) {
8123
+ console.log(
8124
+ ` Next attempt: ${new Date(otlp.nextAttemptAtMs).toISOString()}`
7995
8125
  );
7996
- process.exitCode = 1;
7997
- break;
8126
+ }
7998
8127
  }
7999
8128
  }
8000
8129
 
@@ -8002,7 +8131,7 @@ async function handleFmlStop(opts = {}) {
8002
8131
  function handleEnvShow() {
8003
8132
  const { name, convexUrl } = getActiveEnv();
8004
8133
  console.log(`Environment: ${name}`);
8005
- console.log(` Convex URL: ${convexUrl ?? "not configured"}`);
8134
+ console.log(` Service URL: ${convexUrl ?? "not configured"}`);
8006
8135
  }
8007
8136
  function handleEnvSwitch(target2) {
8008
8137
  let name = target2;
@@ -8025,17 +8154,20 @@ function handleEnvSwitch(target2) {
8025
8154
  return;
8026
8155
  }
8027
8156
  if (!convexUrl) {
8028
- console.error(`FML environment "${name}" has no configured backend URL.`);
8157
+ console.error(`Environment ${name} has no configured service URL.`);
8029
8158
  console.error(
8030
- `Run FML_CONVEX_URL=https://<deployment>.convex.cloud fml env ${name}`
8159
+ `Set FML_CONVEX_URL=https://<deployment>.convex.cloud and run \`fml env ${name}\` again.`
8031
8160
  );
8032
8161
  process.exitCode = 1;
8033
8162
  return;
8034
8163
  }
8035
8164
  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.");
8165
+ console.log(`Environment switched to ${name}.`);
8166
+ console.log(`Service URL: ${convexUrl}`);
8167
+ console.log("CLI commands and agent activity sync now use this environment.");
8168
+ console.log(
8169
+ "Start a new agent session to use the new environment and credentials."
8170
+ );
8039
8171
  }
8040
8172
 
8041
8173
  // src/commands/data.ts
@@ -8592,7 +8724,7 @@ async function handleConfigDetail(opts) {
8592
8724
  // src/commands/tools.ts
8593
8725
  import { readFileSync } from "fs";
8594
8726
  import path28 from "path";
8595
- var pluginVersion = true ? "0.8.0-rc.1+60c9b7e1" : void 0;
8727
+ var pluginVersion = true ? "0.8.0-rc.2+bed6eaed" : void 0;
8596
8728
  async function getApi() {
8597
8729
  const api = await getAuthenticatedClient();
8598
8730
  if (!api) {
@@ -8725,13 +8857,31 @@ async function handleToolsCall(name, opts) {
8725
8857
  console.log(JSON.stringify(result.result, null, 2));
8726
8858
  }
8727
8859
 
8860
+ // src/banner.ts
8861
+ import pc2 from "picocolors";
8862
+ var LOGO_LINES = [
8863
+ " \u2584\u2588\u2588\u2580 \u2588\u2588\u2584\u2588\u2588\u2584\u2588\u2588 \u2588\u2588",
8864
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
8865
+ "\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
8866
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588",
8867
+ " \u2580\u2580 \u2580\u2580 \u2580\u2580 \u2580\u2580\u2580\u2580"
8868
+ ];
8869
+ function printBanner() {
8870
+ console.log("");
8871
+ for (let i = 0; i < LOGO_LINES.length; i++) {
8872
+ const color = i < 3 ? pc2.blue : pc2.cyan;
8873
+ console.log(" " + color(LOGO_LINES[i]));
8874
+ }
8875
+ console.log("");
8876
+ }
8877
+
8728
8878
  // src/cli.ts
8729
8879
  await initSentry();
8730
8880
  if (process.argv.length <= 2 || process.argv.includes("--help") || process.argv.includes("-h")) {
8731
8881
  printBanner();
8732
8882
  }
8733
8883
  var program = new Command().name("fml").description("FML CLI and agent tools").version(
8734
- true ? "0.8.0-rc.1+60c9b7e1" : "dev"
8884
+ true ? "0.8.0-rc.2+bed6eaed" : "dev"
8735
8885
  );
8736
8886
  function formatCommandInventory(cmd, prefix = "") {
8737
8887
  const name = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
@@ -8760,11 +8910,12 @@ tools.command("call").description("Invoke a backend tool by name with JSON args"
8760
8910
  program.command("mcp-server", { hidden: true }).description("Run the FML MCP server on stdio").action(async () => {
8761
8911
  await import("./mcp/server.js");
8762
8912
  });
8763
- program.command("install").description("Set up FML for local agent use").action(() => handleInstall());
8913
+ program.command("setup").description("Install integrations, sign in if needed, and start FML").action(() => handleSetup());
8914
+ program.command("install").description("Register FML agent integrations and the MCP server").action(() => handleInstall());
8764
8915
  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
8916
  (opts) => handleUninstall({ purge: opts.purge, assumeYes: opts.yes })
8766
8917
  );
8767
- program.command("update").description("Update fml to the latest version").action(() => handleUpdate());
8918
+ program.command("update").description("Update fml from its release channel").action(() => handleUpdate());
8768
8919
  program.command("login").description("Sign in to your FML account").option("--device", "Use device authorization flow (no browser needed)").option(
8769
8920
  "--service-token",
8770
8921
  "Sign in with an org service refresh token (prompts for fml_srt_*)"
@@ -8774,28 +8925,29 @@ program.command("login").description("Sign in to your FML account").option("--de
8774
8925
  program.command("logout").description("Sign out and clear stored credentials").action(() => handleLogout());
8775
8926
  program.command("org").description("Show or select organization").argument("[slug]", "Org slug to select").action((slug) => handleOrg(slug));
8776
8927
  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));
8928
+ program.command("doctor").description("Check FML configuration, agent activity sync, and connectivity").option("--json", "Output as JSON").action((opts) => handleDoctor(opts));
8778
8929
  program.command("open").description("Open FML dashboard in browser").option("--json", "Output URL as JSON instead of opening browser").action((opts) => handleOpen(opts));
8779
8930
  program.command("env").description("Show or switch FML backend environment").argument("[target]", "Environment name or backend URL").action((target2) => {
8780
8931
  if (target2) handleEnvSwitch(target2);
8781
8932
  else handleEnvShow();
8782
8933
  });
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(
8934
+ program.command("start").description("Start the local observer and continuous agent activity sync").action(() => handleFmlStart());
8935
+ program.command("restart").description("Restart the local observer and continuous agent activity sync").action(() => handleFmlRestart());
8936
+ 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 }));
8937
+ 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));
8938
+ sync.command("enable").description("Enable Agent activity sync").option("--json", "Output as JSON").action(
8787
8939
  (_opts, command) => handleSyncEnable(command.optsWithGlobals())
8788
8940
  );
8789
- sync.command("disable").description("Disable raw artifact sync").option("--json", "Output as JSON").action(
8941
+ sync.command("disable").description("Pause Agent activity sync").option("--json", "Output as JSON").action(
8790
8942
  (_opts, command) => handleSyncDisable(command.optsWithGlobals())
8791
8943
  );
8792
- sync.command("status").description("Show raw artifact queue watermarks").option("--json", "Output as JSON").action(
8944
+ sync.command("status").description("Show Agent activity queue watermarks").option("--json", "Output as JSON").action(
8793
8945
  (_opts, command) => handleSyncStatus(command.optsWithGlobals())
8794
8946
  );
8795
- sync.command("reset").description("Replay all raw artifacts from byte zero").option("--json", "Output as JSON").action(
8947
+ sync.command("reset").description("Replay Agent activity uploads from byte zero").option("--json", "Output as JSON").action(
8796
8948
  (_opts, command) => handleSyncReset(command.optsWithGlobals())
8797
8949
  );
8798
- var artifacts = program.command("artifacts", { hidden: true }).description("Compatibility alias for raw artifact sync");
8950
+ var artifacts = program.command("artifacts", { hidden: true }).description("Compatibility alias for Agent activity sync");
8799
8951
  artifacts.command("sync").option("--dry-run", "Discover artifacts without changing queue state").option("--json", "Output as JSON").action((opts) => handleSyncRun(opts));
8800
8952
  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
8953
  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));