@fml-inc/fml 0.8.3 → 0.8.4-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fml",
3
- "version": "0.8.3",
3
+ "version": "0.8.4-rc.2",
4
4
  "description": "FML agent tools for Claude Code",
5
5
  "mcpServers": {
6
6
  "fml": {
package/README.md CHANGED
@@ -240,6 +240,25 @@ directories request owner-only permissions, and the SQLite database and
240
240
  sidecars are tightened to owner-only modes on platforms that support POSIX
241
241
  modes. An existing caller-supplied state directory retains its directory mode.
242
242
 
243
+ ## Automatic updates
244
+
245
+ While the observer is running it checks npm for a newer FML release about once
246
+ an hour (with jitter) and, when one exists, applies it by running `fml update`
247
+ in the background: `npm install --global` followed by `fml setup`, which
248
+ restarts the observer onto the new version. Output from that background update
249
+ is written to `update.log` in the FML log directory.
250
+
251
+ Auto-update is on by default. To disable it, set `FML_AUTO_UPDATE=off` and
252
+ restart the observer so it picks up the change:
253
+
254
+ ```bash
255
+ FML_AUTO_UPDATE=off fml restart
256
+ ```
257
+
258
+ The variable is read from the observer's environment at startup, so exporting
259
+ it in a shell has no effect until the observer is relaunched. Development
260
+ builds and non-release versions never auto-update.
261
+
243
262
  ## MCP tools
244
263
 
245
264
  The MCP server exposes FML tools for session analytics, cost tracking,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  FML_HOOK_INBOX_DIR
3
- } from "./chunk-TNJVAET3.js";
3
+ } from "./chunk-ZBSC57CW.js";
4
4
 
5
5
  // ../../packages/panopticon/src/observer/auth.ts
6
6
  import crypto from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  FML_HOOK_CAPTURE_DISABLED_FILE
3
- } from "./chunk-TNJVAET3.js";
3
+ } from "./chunk-ZBSC57CW.js";
4
4
 
5
5
  // src/json-file.ts
6
6
  import { randomUUID } from "crypto";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  resolvePackageRoot,
3
3
  runRawSyncOnce
4
- } from "./chunk-UGUYQVUW.js";
4
+ } from "./chunk-T3ZFT3QP.js";
5
5
 
6
6
  // src/observer/sync-cycle.ts
7
7
  import { fork } from "child_process";
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  OBSERVER_SERVICE_NAME
3
- } from "./chunk-F4A2POJX.js";
3
+ } from "./chunk-732O7ISN.js";
4
4
  import {
5
5
  FML_OBSERVER_LOCK_FILE,
6
6
  FML_OBSERVER_PORT,
7
- FML_OBSERVER_STATE_FILE
8
- } from "./chunk-TNJVAET3.js";
7
+ FML_OBSERVER_STATE_FILE,
8
+ Sentry
9
+ } from "./chunk-ZBSC57CW.js";
9
10
 
10
11
  // src/observer/state.ts
11
12
  import { randomUUID } from "crypto";
@@ -938,6 +939,270 @@ function removeObserverStateIfOwned(instanceId, opts = {}) {
938
939
  }
939
940
  }
940
941
 
942
+ // src/bin-utils.ts
943
+ import fs2 from "fs";
944
+ import path2 from "path";
945
+ import { execFileSync as execFileSync2, execSync } from "child_process";
946
+ var IS_WIN = process.platform === "win32";
947
+ var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
948
+ function resolveBin(name) {
949
+ const pathDirs = (process.env.PATH ?? "").split(path2.delimiter).filter(Boolean);
950
+ const hasExt = path2.extname(name) !== "";
951
+ const exts = !IS_WIN || hasExt ? [""] : (process.env.PATHEXT ?? DEFAULT_PATHEXT).split(";").filter(Boolean);
952
+ for (const dir of pathDirs) {
953
+ for (const ext of exts) {
954
+ const candidate = path2.join(dir, name + ext);
955
+ try {
956
+ if (fs2.statSync(candidate).isFile()) return candidate;
957
+ } catch {
958
+ }
959
+ }
960
+ }
961
+ return null;
962
+ }
963
+ function isWinShim(bin) {
964
+ return IS_WIN && /\.(cmd|bat)$/i.test(bin);
965
+ }
966
+ function quoteWinArg(arg) {
967
+ if (arg.length === 0) return '""';
968
+ if (/^[A-Za-z0-9@\-_+=:/.,]+$/.test(arg)) return arg;
969
+ const crt = arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, "$1$1");
970
+ const quoted = `"${crt}"`;
971
+ return quoted.replace(/([()%!^<>&|"])/g, "^$1");
972
+ }
973
+ function execBinSync(bin, args, opts = {}) {
974
+ const {
975
+ timeout = 1e4,
976
+ encoding = "utf-8",
977
+ stdio = "pipe",
978
+ env = process.env
979
+ } = opts;
980
+ if (isWinShim(bin)) {
981
+ const command = [bin, ...args].map(quoteWinArg).join(" ");
982
+ return execSync(command, { timeout, encoding, stdio, env });
983
+ }
984
+ return execFileSync2(bin, args, { timeout, encoding, stdio, env });
985
+ }
986
+
987
+ // src/commands/update.ts
988
+ var SEMVER_PATTERN = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
989
+ var PACKAGE_NAME = "@fml-inc/fml";
990
+ var UPDATE_TIMEOUT_MS = 12e4;
991
+ var SETUP_TIMEOUT_MS = 3e5;
992
+ function getCurrentVersion() {
993
+ return true ? "0.8.4-rc.2+f282df41" : "unknown";
994
+ }
995
+ function parseVersion(version) {
996
+ const match = SEMVER_PATTERN.exec(version);
997
+ if (!match) return null;
998
+ return {
999
+ major: match[1],
1000
+ minor: match[2],
1001
+ patch: match[3],
1002
+ prerelease: match[4]?.split(".") ?? []
1003
+ };
1004
+ }
1005
+ function compareNumericIdentifiers(left, right) {
1006
+ if (left.length !== right.length) return left.length - right.length;
1007
+ if (left === right) return 0;
1008
+ return left < right ? -1 : 1;
1009
+ }
1010
+ function compareIdentifiers(left, right) {
1011
+ const leftIsNumeric = /^[0-9]+$/u.test(left);
1012
+ const rightIsNumeric = /^[0-9]+$/u.test(right);
1013
+ if (leftIsNumeric && rightIsNumeric) {
1014
+ return compareNumericIdentifiers(left, right);
1015
+ }
1016
+ if (leftIsNumeric) return -1;
1017
+ if (rightIsNumeric) return 1;
1018
+ if (left === right) return 0;
1019
+ return left < right ? -1 : 1;
1020
+ }
1021
+ function compareVersions(left, right) {
1022
+ const leftVersion = parseVersion(left);
1023
+ const rightVersion = parseVersion(right);
1024
+ if (!leftVersion || !rightVersion) {
1025
+ throw new Error("FML update requires valid semantic versions.");
1026
+ }
1027
+ for (const part of ["major", "minor", "patch"]) {
1028
+ const difference = compareNumericIdentifiers(
1029
+ leftVersion[part],
1030
+ rightVersion[part]
1031
+ );
1032
+ if (difference) return difference;
1033
+ }
1034
+ if (!leftVersion.prerelease.length && !rightVersion.prerelease.length) {
1035
+ return 0;
1036
+ }
1037
+ if (!leftVersion.prerelease.length) return 1;
1038
+ if (!rightVersion.prerelease.length) return -1;
1039
+ const length = Math.max(
1040
+ leftVersion.prerelease.length,
1041
+ rightVersion.prerelease.length
1042
+ );
1043
+ for (let index = 0; index < length; index += 1) {
1044
+ const leftIdentifier = leftVersion.prerelease[index];
1045
+ const rightIdentifier = rightVersion.prerelease[index];
1046
+ if (leftIdentifier === void 0) return -1;
1047
+ if (rightIdentifier === void 0) return 1;
1048
+ const difference = compareIdentifiers(leftIdentifier, rightIdentifier);
1049
+ if (difference) return difference;
1050
+ }
1051
+ return 0;
1052
+ }
1053
+ function isUpdatableVersion(version) {
1054
+ if (version === "dev" || version === "unknown") return false;
1055
+ const parsed = parseVersion(version);
1056
+ if (!parsed) return false;
1057
+ if (parsed.prerelease.includes("dev")) return false;
1058
+ return true;
1059
+ }
1060
+ function resolveUpdateChannel(version) {
1061
+ const match = SEMVER_PATTERN.exec(version);
1062
+ if (!match) return "latest";
1063
+ const prerelease = match[4];
1064
+ if (!prerelease) return "latest";
1065
+ return prerelease.split(".").every((identifier) => {
1066
+ return !(identifier.length > 1 && identifier.startsWith("0") && /^[0-9]+$/u.test(identifier));
1067
+ }) ? "next" : "latest";
1068
+ }
1069
+ function resolveUpdateTarget(version) {
1070
+ return `${PACKAGE_NAME}@${resolveUpdateChannel(version)}`;
1071
+ }
1072
+ function createUpdateEnvironment(env) {
1073
+ const updateEnv = { ...env };
1074
+ for (const key of Object.keys(updateEnv)) {
1075
+ if (key.toLowerCase() === "npm_config_registry") {
1076
+ delete updateEnv[key];
1077
+ }
1078
+ }
1079
+ return updateEnv;
1080
+ }
1081
+ function parseAvailableVersion(output) {
1082
+ let response;
1083
+ try {
1084
+ response = JSON.parse(output);
1085
+ } catch {
1086
+ throw new Error("Could not read the available FML version from npm.");
1087
+ }
1088
+ const version = Array.isArray(response) && response.length === 1 ? response[0] : response;
1089
+ if (typeof version !== "string" || !parseVersion(version)) {
1090
+ throw new Error("Could not read the available FML version from npm.");
1091
+ }
1092
+ return version;
1093
+ }
1094
+ async function checkForUpdate(dependencies) {
1095
+ if (!isUpdatableVersion(dependencies.currentVersion)) {
1096
+ throw new Error("FML update requires a released semantic version.");
1097
+ }
1098
+ const channel = resolveUpdateChannel(dependencies.currentVersion);
1099
+ const target = resolveUpdateTarget(dependencies.currentVersion);
1100
+ const updateEnv = createUpdateEnvironment(dependencies.env);
1101
+ let output;
1102
+ if (dependencies.runNpmView) {
1103
+ output = await dependencies.runNpmView(target, updateEnv);
1104
+ } else {
1105
+ const npm = dependencies.resolveBin?.("npm");
1106
+ if (!npm) throw new Error("npm was not found on PATH");
1107
+ if (!dependencies.execBinSync) {
1108
+ throw new Error("FML update check requires an npm view runner.");
1109
+ }
1110
+ output = dependencies.execBinSync(
1111
+ npm,
1112
+ ["view", target, "version", "--json"],
1113
+ {
1114
+ timeout: UPDATE_TIMEOUT_MS,
1115
+ env: updateEnv
1116
+ }
1117
+ );
1118
+ }
1119
+ const latest = parseAvailableVersion(output);
1120
+ return {
1121
+ current: dependencies.currentVersion,
1122
+ latest,
1123
+ channel,
1124
+ newer: compareVersions(latest, dependencies.currentVersion) > 0
1125
+ };
1126
+ }
1127
+ function formatUpdateError(err) {
1128
+ if (!(err instanceof Error)) return String(err);
1129
+ const stderr = err.stderr;
1130
+ if (typeof stderr === "string") return stderr;
1131
+ if (Buffer.isBuffer(stderr)) return stderr.toString();
1132
+ return err.message;
1133
+ }
1134
+ async function handleUpdate(overrides = {}) {
1135
+ const dependencies = {
1136
+ currentVersion: getCurrentVersion(),
1137
+ env: process.env,
1138
+ resolveBin,
1139
+ execBinSync,
1140
+ captureException: Sentry.captureException,
1141
+ log: console.log,
1142
+ error: console.error,
1143
+ exit: process.exit,
1144
+ nonInteractive: false,
1145
+ ...overrides
1146
+ };
1147
+ dependencies.log(`Current version: ${dependencies.currentVersion}`);
1148
+ if (!isUpdatableVersion(dependencies.currentVersion)) {
1149
+ dependencies.log("This is a development build; `fml update` is disabled.");
1150
+ return;
1151
+ }
1152
+ try {
1153
+ const npm = dependencies.resolveBin("npm");
1154
+ if (!npm) throw new Error("npm was not found on PATH");
1155
+ const check = await checkForUpdate({
1156
+ ...dependencies,
1157
+ resolveBin: () => npm
1158
+ });
1159
+ if (!check.newer) {
1160
+ dependencies.log(
1161
+ `FML is already up to date (${dependencies.currentVersion}).`
1162
+ );
1163
+ return;
1164
+ }
1165
+ const updateEnv = createUpdateEnvironment(dependencies.env);
1166
+ const availableVersion = check.latest;
1167
+ const channel = check.channel;
1168
+ dependencies.log(
1169
+ `Updating FML from ${dependencies.currentVersion} to ${availableVersion} on the ${channel} channel...
1170
+ `
1171
+ );
1172
+ dependencies.execBinSync(
1173
+ npm,
1174
+ ["install", "--global", `${PACKAGE_NAME}@${availableVersion}`],
1175
+ { timeout: UPDATE_TIMEOUT_MS, env: updateEnv }
1176
+ );
1177
+ dependencies.log(
1178
+ `
1179
+ FML updated from ${dependencies.currentVersion} to ${availableVersion}. Refreshing setup...`
1180
+ );
1181
+ try {
1182
+ const fml = dependencies.resolveBin("fml");
1183
+ if (!fml) throw new Error("Updated FML was not found on PATH");
1184
+ dependencies.execBinSync(
1185
+ fml,
1186
+ dependencies.nonInteractive ? ["setup", "--non-interactive"] : ["setup"],
1187
+ { timeout: SETUP_TIMEOUT_MS, stdio: "inherit" }
1188
+ );
1189
+ dependencies.log("FML setup complete.");
1190
+ } catch (err) {
1191
+ dependencies.captureException(err);
1192
+ dependencies.error(
1193
+ "FML updated, but setup failed:",
1194
+ formatUpdateError(err)
1195
+ );
1196
+ dependencies.error("Run `fml setup` to finish refreshing FML.");
1197
+ dependencies.exit(1);
1198
+ }
1199
+ } catch (err) {
1200
+ dependencies.captureException(err);
1201
+ dependencies.error("Update failed:", formatUpdateError(err));
1202
+ dependencies.exit(1);
1203
+ }
1204
+ }
1205
+
941
1206
  export {
942
1207
  OBSERVER_HEARTBEAT_STALE_MS,
943
1208
  isPidAlive,
@@ -949,5 +1214,12 @@ export {
949
1214
  getProcessIdentity,
950
1215
  claimObserverState,
951
1216
  updateObserverState,
952
- removeObserverStateIfOwned
1217
+ removeObserverStateIfOwned,
1218
+ resolveBin,
1219
+ isWinShim,
1220
+ quoteWinArg,
1221
+ execBinSync,
1222
+ isUpdatableVersion,
1223
+ checkForUpdate,
1224
+ handleUpdate
953
1225
  };
@@ -7,7 +7,7 @@ import {
7
7
  getSelectedOrg,
8
8
  getSiteUrlForConvexUrl,
9
9
  getValidToken
10
- } from "./chunk-TNJVAET3.js";
10
+ } from "./chunk-ZBSC57CW.js";
11
11
 
12
12
  // src/fml-client.ts
13
13
  var LOGIN_EXPIRED_MESSAGE = "Authentication expired. Run `fml login` to sign in again, then restart Claude Code.";
@@ -79,7 +79,7 @@ function createFmlClient(token, options = {}) {
79
79
  const repo = resolveRepoFromCwd(process.cwd());
80
80
  if (repo) body.repo = repo.repo;
81
81
  if (isServiceToken) {
82
- const { readTokens, SERVICE_TOKEN_LOGIN_USER_ID } = await import("./token-store-7OI3CMM4.js");
82
+ const { readTokens, SERVICE_TOKEN_LOGIN_USER_ID } = await import("./token-store-QYXUL4NG.js");
83
83
  const stored = readTokens(options.envName);
84
84
  const storedUserExternalId = stored?.tokenType === "service" && stored.user.id !== SERVICE_TOKEN_LOGIN_USER_ID ? stored.user.id : void 0;
85
85
  const userExternalId = process.env.FML_USER_EXTERNAL_ID ?? storedUserExternalId;
@@ -6,7 +6,7 @@ import {
6
6
  getSiteUrlForConvexUrl,
7
7
  getSyncDestinationIdentity,
8
8
  getValidToken
9
- } from "./chunk-TNJVAET3.js";
9
+ } from "./chunk-ZBSC57CW.js";
10
10
 
11
11
  // ../../packages/panopticon/src/artifact.ts
12
12
  import {
@@ -235,7 +235,7 @@ function walkFiles(input) {
235
235
  const root = normalizeFilePath(input.root);
236
236
  const files = [];
237
237
  const seen = /* @__PURE__ */ new Set();
238
- const skipDirs = input.skipDirs ?? DEFAULT_SKIP_DIRS;
238
+ const skipDirs = /* @__PURE__ */ new Set([...DEFAULT_SKIP_DIRS, ...input.skipDirs ?? []]);
239
239
  function walk(dir, depth) {
240
240
  if (depth > input.maxDepth) return;
241
241
  for (const entry of safeReaddir(dir)) {
@@ -741,6 +741,7 @@ var grokArtifactDiscovery = Object.freeze({
741
741
 
742
742
  // ../../packages/agent-targets/src/artifacts/targets/pi.ts
743
743
  import path15 from "path";
744
+ var PI_SKIP_DIRS = /* @__PURE__ */ new Set(["subagent-artifacts"]);
744
745
  function piTranscriptCandidate(sourcePath, context) {
745
746
  const fallbackObservedAtMs = (context.nowMs ?? Date.now)();
746
747
  for (const record of readJsonlContextRecords(sourcePath)) {
@@ -779,7 +780,8 @@ var piArtifactDiscovery = Object.freeze({
779
780
  return walkFiles({
780
781
  root: path15.join(homeDir, ".pi", "agent", "sessions"),
781
782
  maxDepth: 8,
782
- includeFile: (filePath) => filePath.endsWith(".jsonl")
783
+ includeFile: (filePath) => filePath.endsWith(".jsonl"),
784
+ skipDirs: PI_SKIP_DIRS
783
785
  }).map((sourcePath) => piTranscriptCandidate(sourcePath, context));
784
786
  }
785
787
  });
@@ -150,6 +150,7 @@ var FML_OBSERVER_ENDPOINT_FILE = path.join(
150
150
  "endpoint.json"
151
151
  );
152
152
  var FML_OBSERVER_LOG_FILE = path.join(FML_LOG_DIR, "observer.log");
153
+ var FML_UPDATE_LOG_FILE = path.join(FML_LOG_DIR, "update.log");
153
154
  var FML_INSTALLATION_ID_FILE = path.join(
154
155
  FML_STATE_DIR,
155
156
  "installation_id"
@@ -1572,6 +1573,7 @@ export {
1572
1573
  FML_OBSERVER_AUTH_TOKEN_FILE,
1573
1574
  FML_OBSERVER_ENDPOINT_FILE,
1574
1575
  FML_OBSERVER_LOG_FILE,
1576
+ FML_UPDATE_LOG_FILE,
1575
1577
  FML_INSTALLATION_ID_FILE,
1576
1578
  FML_PANOPTICON_DB_FILE,
1577
1579
  FML_PANOPTICON_DB_OBSERVER_STATE_FILE,
@@ -6,7 +6,7 @@ import {
6
6
  getSiteUrlForConvexUrl,
7
7
  getSyncDestinationIdentity,
8
8
  getValidToken
9
- } from "./chunk-TNJVAET3.js";
9
+ } from "./chunk-ZBSC57CW.js";
10
10
 
11
11
  // src/hooks/sync-loop.ts
12
12
  import { createHash } from "crypto";
@@ -449,17 +449,27 @@ function hookSessionRow(events) {
449
449
  const cwds = [...cwdsByPath.values()].sort(
450
450
  (left, right) => left.firstSeenMs - right.firstSeenMs || left.order - right.order
451
451
  ).map(({ order: _order, ...relationship }) => relationship);
452
+ const timezone = machineTimezone();
452
453
  return {
453
454
  sessionId: first.sessionId,
454
455
  target: first.target,
455
456
  startedAtMs,
456
457
  createdAt: startedAtMs,
457
458
  machine: "local",
459
+ ...timezone ? { timezone } : {},
458
460
  ...firstPrompt ? { firstPrompt } : {},
459
461
  ...repositories.length > 0 ? { repositories } : {},
460
462
  ...cwds.length > 0 ? { cwds } : {}
461
463
  };
462
464
  }
465
+ function machineTimezone() {
466
+ try {
467
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
468
+ return timezone && timezone.length > 0 ? timezone : void 0;
469
+ } catch {
470
+ return void 0;
471
+ }
472
+ }
463
473
  function batchesForSession(events) {
464
474
  const batches = [];
465
475
  let batch = [];