@fml-inc/fml 0.8.4-rc.1 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +19 -0
- package/dist/{chunk-F4A2POJX.js → chunk-732O7ISN.js} +1 -1
- package/dist/{chunk-UZG65JUB.js → chunk-IIGTUGO5.js} +1 -1
- package/dist/{chunk-4U4XXSH2.js → chunk-LWOINL4K.js} +1 -1
- package/dist/{chunk-3WDAKM5B.js → chunk-NP7HI4UC.js} +276 -4
- package/dist/{chunk-26RJC76L.js → chunk-P4HOUZXC.js} +2 -2
- package/dist/{chunk-4LJVPXII.js → chunk-T3ZFT3QP.js} +1 -1
- package/dist/{chunk-TNJVAET3.js → chunk-ZBSC57CW.js} +2 -0
- package/dist/{chunk-G5HGOPWY.js → chunk-ZGA57JPY.js} +1 -1
- package/dist/cli.js +231 -432
- package/dist/hook-handler.js +4 -4
- package/dist/mcp/server.js +2 -2
- package/dist/observer-server.js +239 -14
- package/dist/pi/extension.js +1 -0
- package/dist/sync-cycle.js +3 -3
- package/dist/{token-store-7OI3CMM4.js → token-store-QYXUL4NG.js} +1 -1
- package/package.json +1 -1
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,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
OBSERVER_SERVICE_NAME
|
|
3
|
-
} from "./chunk-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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;
|
|
@@ -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,
|