@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,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  hookCaptureIsDisabled
4
- } from "./chunk-UZG65JUB.js";
4
+ } from "./chunk-IIGTUGO5.js";
5
5
  import {
6
6
  getActiveHookDestinationBinding
7
- } from "./chunk-E6YIJCIE.js";
7
+ } from "./chunk-ZGA57JPY.js";
8
8
  import {
9
9
  SHELL_TOOL_NAMES,
10
10
  createHookInboxEntry,
@@ -12,7 +12,7 @@ import {
12
12
  normalizeObserverHookCapture,
13
13
  persistHookInboxEntry,
14
14
  readObserverAuthToken
15
- } from "./chunk-F4A2POJX.js";
15
+ } from "./chunk-732O7ISN.js";
16
16
  import {
17
17
  resolveRepositoryRootFromCwd,
18
18
  resolveRepositorySlugFromCwd
@@ -23,7 +23,7 @@ import {
23
23
  FML_OBSERVER_AUTH_TOKEN_FILE,
24
24
  FML_OBSERVER_ENDPOINT_FILE,
25
25
  getValidToken
26
- } from "./chunk-TNJVAET3.js";
26
+ } from "./chunk-ZBSC57CW.js";
27
27
  import "./chunk-MLKGABMK.js";
28
28
 
29
29
  // src/hooks/handler.ts
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createFmlClient,
4
4
  getAuthenticatedClient
5
- } from "../chunk-26RJC76L.js";
5
+ } from "../chunk-P4HOUZXC.js";
6
6
  import {
7
7
  resolveRepoFromCwd
8
8
  } from "../chunk-7V7G7NVF.js";
@@ -17,7 +17,7 @@ import {
17
17
  getValidToken,
18
18
  initSentry,
19
19
  readTokens
20
- } from "../chunk-TNJVAET3.js";
20
+ } from "../chunk-ZBSC57CW.js";
21
21
  import "../chunk-MLKGABMK.js";
22
22
 
23
23
  // src/mcp/server.ts
@@ -342,13 +342,16 @@ function registerTools(server2, environment = getActiveEnv()) {
342
342
  );
343
343
  server2.tool(
344
344
  "get_ai_spending",
345
- "Get AI spending and token usage data. Groups by 'session' (default), 'model', or 'day'.",
345
+ "Get AI spending and token usage data. Groups by 'session' (default), 'model', 'day', or 'user'. Usage is event-time attributed: day groups reflect when tokens were actually consumed, in each author's local timezone, not session start times. Time windows have 15-minute bucket resolution.",
346
346
  {
347
347
  timeRange: z.enum(["1h", "6h", "24h", "7d", "30d"]).optional().describe("Default '7d'."),
348
348
  since: z.string().optional().describe("ISO 8601 start timestamp."),
349
349
  until: z.string().optional().describe("ISO 8601 end timestamp."),
350
350
  groupBy: z.enum(["session", "model", "day", "user"]).optional().describe("How to group results."),
351
351
  target: z.enum(["claude", "codex", "gemini", "mastra"]).optional().describe("Filter by agent type. Omit for all."),
352
+ timezone: z.string().optional().describe(
353
+ "IANA timezone fallback for day grouping when a session's author has no recorded timezone (author-recorded timezones win). Defaults to UTC."
354
+ ),
352
355
  limit: z.number().int().min(1).max(500).optional().describe("Maximum groups to return, default 100."),
353
356
  offset: z.number().int().min(0).optional().describe("Groups to skip; use nextOffset from the previous page.")
354
357
  },
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ObserverStateMutationBusyError,
4
+ checkForUpdate,
4
5
  claimObserverState,
5
6
  createInitialObserverState,
7
+ isUpdatableVersion,
8
+ isWinShim,
6
9
  observerLeaseIsOwned,
10
+ quoteWinArg,
7
11
  removeObserverStateIfOwned,
12
+ resolveBin,
8
13
  updateObserverState
9
- } from "./chunk-3WDAKM5B.js";
14
+ } from "./chunk-NP7HI4UC.js";
10
15
  import {
11
16
  createHookSyncLoop,
12
17
  getActiveHookDestinationBinding
13
- } from "./chunk-E6YIJCIE.js";
18
+ } from "./chunk-ZGA57JPY.js";
14
19
  import {
15
20
  OBSERVER_PROTOCOL_VERSION,
16
21
  OBSERVER_SERVICE_NAME,
@@ -25,15 +30,15 @@ import {
25
30
  parseHookInboxMetadata,
26
31
  readHookInboxStatusAsync,
27
32
  retireHookInboxEntry
28
- } from "./chunk-F4A2POJX.js";
33
+ } from "./chunk-732O7ISN.js";
29
34
  import "./chunk-7V7G7NVF.js";
30
35
  import {
31
36
  ObserverSyncCycleAbortedError,
32
37
  runObserverSyncCycleSubprocess
33
- } from "./chunk-IZH4MDS4.js";
38
+ } from "./chunk-LWOINL4K.js";
34
39
  import {
35
40
  createCliRawSync
36
- } from "./chunk-UGUYQVUW.js";
41
+ } from "./chunk-T3ZFT3QP.js";
37
42
  import {
38
43
  FML_OBSERVER_AUTH_TOKEN_FILE,
39
44
  FML_OBSERVER_ENDPOINT_FILE,
@@ -44,17 +49,167 @@ import {
44
49
  FML_PANOPTICON_DB_FILE,
45
50
  FML_PANOPTICON_DB_OBSERVER_LOCK_FILE,
46
51
  FML_PANOPTICON_DB_OBSERVER_STATE_FILE,
52
+ FML_UPDATE_LOG_FILE,
47
53
  ensureDirs,
48
54
  getActiveEnv,
49
55
  getSiteUrlForConvexUrl,
50
56
  getSyncDestinationAuthBinding,
51
57
  getValidToken,
52
58
  observerOnlyOverrideWarning
53
- } from "./chunk-TNJVAET3.js";
59
+ } from "./chunk-ZBSC57CW.js";
54
60
  import "./chunk-MLKGABMK.js";
55
61
 
56
62
  // src/observer-server.ts
63
+ import fs2 from "fs";
64
+
65
+ // src/observer/update-runner.ts
66
+ import { spawn } from "child_process";
57
67
  import fs from "fs";
68
+ import os from "os";
69
+ import path from "path";
70
+ var NPM_VIEW_TIMEOUT_MS = 6e4;
71
+ var LOG_ROTATE_BYTES = 10 * 1024 * 1024;
72
+ var LOG_ROTATE_FILES = 3;
73
+ function errorMessage(error) {
74
+ return error instanceof Error ? error.message : String(error);
75
+ }
76
+ function buildSpawnInvocation(bin, args, winShim) {
77
+ if (winShim) {
78
+ return {
79
+ command: [bin, ...args].map(quoteWinArg).join(" "),
80
+ args: [],
81
+ shell: true
82
+ };
83
+ }
84
+ return { command: bin, args, shell: false };
85
+ }
86
+ function runNpmView(target, env, deps = {}) {
87
+ const spawnFn = deps.spawn ?? spawn;
88
+ const resolveBinFn = deps.resolveBin ?? resolveBin;
89
+ const timeoutMs = deps.timeoutMs ?? NPM_VIEW_TIMEOUT_MS;
90
+ const npm = resolveBinFn("npm");
91
+ if (!npm) {
92
+ return Promise.reject(new Error("npm was not found on PATH"));
93
+ }
94
+ const invocation = buildSpawnInvocation(
95
+ npm,
96
+ ["view", target, "version", "--json"],
97
+ isWinShim(npm)
98
+ );
99
+ return new Promise((resolve, reject) => {
100
+ const child = spawnFn(invocation.command, invocation.args, {
101
+ cwd: os.homedir(),
102
+ env,
103
+ stdio: ["ignore", "pipe", "pipe"],
104
+ shell: invocation.shell,
105
+ windowsHide: true
106
+ });
107
+ let stdout = "";
108
+ let stderr = "";
109
+ let settled = false;
110
+ let timer;
111
+ const settle = (callback) => {
112
+ if (settled) return;
113
+ settled = true;
114
+ clearTimeout(timer);
115
+ callback();
116
+ };
117
+ timer = setTimeout(() => {
118
+ settle(() => {
119
+ child.kill("SIGKILL");
120
+ reject(new Error(`npm view timed out after ${timeoutMs}ms`));
121
+ });
122
+ }, timeoutMs);
123
+ timer.unref();
124
+ child.stdout?.setEncoding("utf8");
125
+ child.stdout?.on("data", (chunk) => {
126
+ stdout += chunk;
127
+ });
128
+ child.stderr?.setEncoding("utf8");
129
+ child.stderr?.on("data", (chunk) => {
130
+ stderr += chunk;
131
+ });
132
+ child.once("error", (error) => {
133
+ settle(() => reject(error));
134
+ });
135
+ child.once("close", (code, signal) => {
136
+ settle(() => {
137
+ if (code !== 0) {
138
+ reject(
139
+ new Error(
140
+ stderr.trim() || `npm view exited with ${signal ? `signal ${signal}` : `status ${code}`}`
141
+ )
142
+ );
143
+ return;
144
+ }
145
+ resolve(stdout);
146
+ });
147
+ });
148
+ });
149
+ }
150
+ function rotateUpdateLog() {
151
+ let size = 0;
152
+ try {
153
+ size = fs.statSync(FML_UPDATE_LOG_FILE).size;
154
+ } catch {
155
+ return;
156
+ }
157
+ if (size < LOG_ROTATE_BYTES) return;
158
+ try {
159
+ fs.rmSync(`${FML_UPDATE_LOG_FILE}.${LOG_ROTATE_FILES}`, { force: true });
160
+ for (let index = LOG_ROTATE_FILES - 1; index >= 1; index -= 1) {
161
+ const source = `${FML_UPDATE_LOG_FILE}.${index}`;
162
+ if (fs.existsSync(source)) {
163
+ fs.renameSync(source, `${FML_UPDATE_LOG_FILE}.${index + 1}`);
164
+ }
165
+ }
166
+ fs.renameSync(FML_UPDATE_LOG_FILE, `${FML_UPDATE_LOG_FILE}.1`);
167
+ } catch {
168
+ }
169
+ }
170
+ function openUpdateLog() {
171
+ try {
172
+ fs.mkdirSync(path.dirname(FML_UPDATE_LOG_FILE), {
173
+ recursive: true,
174
+ mode: 448
175
+ });
176
+ rotateUpdateLog();
177
+ return fs.openSync(FML_UPDATE_LOG_FILE, "a", 384);
178
+ } catch (error) {
179
+ console.warn(`[fml] Auto-update log unavailable: ${errorMessage(error)}`);
180
+ return "ignore";
181
+ }
182
+ }
183
+ function spawnObserverUpdate(deps = {}) {
184
+ const spawnFn = deps.spawn ?? spawn;
185
+ const resolve = deps.resolveBin ?? resolveBin;
186
+ const warn = deps.warn ?? console.warn;
187
+ const openLog = deps.openLog ?? openUpdateLog;
188
+ const closeLog = deps.closeLog ?? fs.closeSync;
189
+ const fml = resolve("fml");
190
+ if (!fml) {
191
+ warn("[fml] Auto-update skipped: fml was not found on PATH");
192
+ return;
193
+ }
194
+ const log = openLog();
195
+ const invocation = buildSpawnInvocation(
196
+ fml,
197
+ ["update", "--non-interactive"],
198
+ isWinShim(fml)
199
+ );
200
+ const child = spawnFn(invocation.command, invocation.args, {
201
+ cwd: os.homedir(),
202
+ detached: true,
203
+ stdio: ["ignore", log, log],
204
+ shell: invocation.shell,
205
+ windowsHide: true
206
+ });
207
+ child.once("error", (error) => {
208
+ warn(`[fml] Auto-update failed to start: ${error.message}`);
209
+ });
210
+ child.unref();
211
+ if (typeof log === "number") closeLog(log);
212
+ }
58
213
 
59
214
  // src/otel/sync-loop.ts
60
215
  import { createHash } from "crypto";
@@ -157,17 +312,17 @@ function validateProtobufPartialSuccess(body) {
157
312
  continue;
158
313
  }
159
314
  if (fieldNumber === 2n && wireType === 2) {
160
- const errorMessage = parseLengthDelimited(body, offset);
161
- if (!errorMessage) {
315
+ const errorMessage3 = parseLengthDelimited(body, offset);
316
+ if (!errorMessage3) {
162
317
  return {
163
318
  error: "OTLP partial-success response has a malformed error message"
164
319
  };
165
320
  }
166
- offset = errorMessage.nextOffset;
321
+ offset = errorMessage3.nextOffset;
167
322
  let decoded;
168
323
  try {
169
324
  decoded = new TextDecoder("utf-8", { fatal: true }).decode(
170
- errorMessage.value
325
+ errorMessage3.value
171
326
  );
172
327
  } catch {
173
328
  return {
@@ -1044,6 +1199,60 @@ function createObserverSyncLoop(opts) {
1044
1199
  };
1045
1200
  }
1046
1201
 
1202
+ // src/observer/auto-update.ts
1203
+ var AUTO_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
1204
+ var AUTO_UPDATE_JITTER_MS = 10 * 60 * 1e3;
1205
+ function errorMessage2(error) {
1206
+ return error instanceof Error ? error.message : String(error);
1207
+ }
1208
+ function createAutoUpdater(dependencies) {
1209
+ let timer;
1210
+ let stopped = true;
1211
+ let generation = 0;
1212
+ const nextDelay = () => {
1213
+ if (dependencies.jitterMs <= 0) return dependencies.intervalMs;
1214
+ const range = dependencies.jitterMs * 2 + 1;
1215
+ const offset = dependencies.nowMs() % range - dependencies.jitterMs;
1216
+ return Math.max(0, dependencies.intervalMs + offset);
1217
+ };
1218
+ const schedule = () => {
1219
+ if (stopped) return;
1220
+ timer = setTimeout(onTick, nextDelay());
1221
+ timer.unref();
1222
+ };
1223
+ const onTick = async () => {
1224
+ const tickGeneration = generation;
1225
+ timer = void 0;
1226
+ try {
1227
+ const result = await dependencies.runCheck();
1228
+ if (!stopped && result.newer) dependencies.spawnUpdate();
1229
+ } catch (error) {
1230
+ if (!stopped) {
1231
+ dependencies.log(
1232
+ `[fml] Auto-update check failed: ${errorMessage2(error)}`
1233
+ );
1234
+ }
1235
+ } finally {
1236
+ if (tickGeneration === generation) schedule();
1237
+ }
1238
+ };
1239
+ return {
1240
+ start() {
1241
+ if (!stopped || !isUpdatableVersion(dependencies.currentVersion) || process.env.FML_AUTO_UPDATE === "off") {
1242
+ return;
1243
+ }
1244
+ stopped = false;
1245
+ schedule();
1246
+ },
1247
+ stop() {
1248
+ stopped = true;
1249
+ generation += 1;
1250
+ clearTimeout(timer);
1251
+ timer = void 0;
1252
+ }
1253
+ };
1254
+ }
1255
+
1047
1256
  // src/observer-server.ts
1048
1257
  var HEARTBEAT_INTERVAL_MS = 5e3;
1049
1258
  var STATE_UPDATE_RETRY_MS = 50;
@@ -1073,7 +1282,7 @@ function removeInitialLeasesBestEffort(instanceId) {
1073
1282
  function persistedPort() {
1074
1283
  try {
1075
1284
  const parsed = JSON.parse(
1076
- fs.readFileSync(FML_OBSERVER_ENDPOINT_FILE, "utf8")
1285
+ fs2.readFileSync(FML_OBSERVER_ENDPOINT_FILE, "utf8")
1077
1286
  );
1078
1287
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1079
1288
  return null;
@@ -1086,7 +1295,7 @@ function persistedPort() {
1086
1295
  }
1087
1296
  function writeEndpoint(port) {
1088
1297
  const tempFile = `${FML_OBSERVER_ENDPOINT_FILE}.${process.pid}.tmp`;
1089
- fs.writeFileSync(
1298
+ fs2.writeFileSync(
1090
1299
  tempFile,
1091
1300
  `${JSON.stringify({
1092
1301
  service: OBSERVER_SERVICE_NAME,
@@ -1097,7 +1306,7 @@ function writeEndpoint(port) {
1097
1306
  `,
1098
1307
  { mode: 384 }
1099
1308
  );
1100
- fs.renameSync(tempFile, FML_OBSERVER_ENDPOINT_FILE);
1309
+ fs2.renameSync(tempFile, FML_OBSERVER_ENDPOINT_FILE);
1101
1310
  }
1102
1311
  async function tryListen(server2, port) {
1103
1312
  return await new Promise((resolve, reject) => {
@@ -1193,6 +1402,7 @@ var hookQuarantinePruneTimer = null;
1193
1402
  var stateUpdateRetryTimer = null;
1194
1403
  var syncLoop = null;
1195
1404
  var hookSyncLoop = null;
1405
+ var autoUpdater = null;
1196
1406
  var otlpSyncLoop = null;
1197
1407
  var pendingStateUpdate = null;
1198
1408
  var authToken;
@@ -1239,7 +1449,7 @@ var hookRejectionStatus = () => ({
1239
1449
  ...lastRejectedReason !== void 0 ? { lastRejectedReason } : {},
1240
1450
  ...lastRejectedTarget !== void 0 ? { lastRejectedTarget } : {}
1241
1451
  });
1242
- var packageVersion = true ? "0.8.3+499a546c" : "dev";
1452
+ var packageVersion = true ? "0.8.4-rc.2+f282df41" : "dev";
1243
1453
  var health = () => ({
1244
1454
  status: "ok",
1245
1455
  service: OBSERVER_SERVICE_NAME,
@@ -1305,6 +1515,7 @@ var shutdown = async () => {
1305
1515
  if (hookInboxDrainTimer) clearInterval(hookInboxDrainTimer);
1306
1516
  if (hookQuarantinePruneTimer) clearInterval(hookQuarantinePruneTimer);
1307
1517
  if (stateUpdateRetryTimer) clearTimeout(stateUpdateRetryTimer);
1518
+ autoUpdater?.stop();
1308
1519
  if (bothObserverLeasesAreOwned(state.instanceId)) {
1309
1520
  try {
1310
1521
  state = updateBothObserverStates(state, {
@@ -1511,6 +1722,20 @@ try {
1511
1722
  updateState({ heartbeatAtMs: Date.now() });
1512
1723
  }, HEARTBEAT_INTERVAL_MS);
1513
1724
  heartbeatTimer.unref();
1725
+ autoUpdater = createAutoUpdater({
1726
+ currentVersion: packageVersion,
1727
+ nowMs: Date.now,
1728
+ runCheck: () => checkForUpdate({
1729
+ currentVersion: packageVersion,
1730
+ env: process.env,
1731
+ runNpmView
1732
+ }),
1733
+ spawnUpdate: spawnObserverUpdate,
1734
+ intervalMs: AUTO_UPDATE_INTERVAL_MS,
1735
+ jitterMs: AUTO_UPDATE_JITTER_MS,
1736
+ log: console.warn
1737
+ });
1738
+ autoUpdater.start();
1514
1739
  await drainPendingHookInbox();
1515
1740
  hookInboxDrainTimer = setInterval(() => {
1516
1741
  void drainPendingHookInbox();
@@ -830,6 +830,7 @@ var FML_OBSERVER_ENDPOINT_FILE = path3.join(
830
830
  "endpoint.json"
831
831
  );
832
832
  var FML_OBSERVER_LOG_FILE = path3.join(FML_LOG_DIR, "observer.log");
833
+ var FML_UPDATE_LOG_FILE = path3.join(FML_LOG_DIR, "update.log");
833
834
  var FML_INSTALLATION_ID_FILE = path3.join(
834
835
  FML_STATE_DIR,
835
836
  "installation_id"
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runObserverSyncCycle
4
- } from "./chunk-IZH4MDS4.js";
5
- import "./chunk-UGUYQVUW.js";
6
- import "./chunk-TNJVAET3.js";
4
+ } from "./chunk-LWOINL4K.js";
5
+ import "./chunk-T3ZFT3QP.js";
6
+ import "./chunk-ZBSC57CW.js";
7
7
  import "./chunk-MLKGABMK.js";
8
8
 
9
9
  // src/sync-cycle.ts
@@ -10,7 +10,7 @@ import {
10
10
  setSelectedOrg,
11
11
  storeServiceRefreshToken,
12
12
  writeTokens
13
- } from "./chunk-TNJVAET3.js";
13
+ } from "./chunk-ZBSC57CW.js";
14
14
  import "./chunk-MLKGABMK.js";
15
15
  export {
16
16
  SERVICE_TOKEN_LOGIN_USER_ID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fml-inc/fml",
3
- "version": "0.8.3",
3
+ "version": "0.8.4-rc.2",
4
4
  "description": "FML CLI and agent tools",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",