@fml-inc/fml 0.8.6-rc.2 → 0.8.6-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fml",
3
- "version": "0.8.6-rc.2",
3
+ "version": "0.8.6-rc.3",
4
4
  "description": "FML agent tools for Claude Code",
5
5
  "mcpServers": {
6
6
  "fml": {
package/README.md CHANGED
@@ -25,19 +25,24 @@ npm install --global @fml-inc/fml
25
25
  fml setup
26
26
  ```
27
27
 
28
- For a customer-hosted deployment, pass its public backend (Convex HTTP
29
- actions) origin directly to login. The CLI validates and saves the URL as the
30
- active `custom` environment before authenticating, so later CLI and observer
31
- traffic uses the same deployment:
28
+ For a customer-hosted deployment, pass the address you use to open FML in the
29
+ browser to setup instead. The CLI asks that app for its backend (Convex HTTP
30
+ actions) origin, signs in against it, and only then saves both as the active
31
+ `custom` environment and restarts the observer, so later CLI and observer
32
+ traffic uses the same deployment and a failed login never switches
33
+ environments:
32
34
 
33
35
  ```bash
34
- fml login https://actions.fml.example.com
35
- # Headless alternative:
36
- fml login https://actions.fml.example.com --device
36
+ fml setup https://fml.example.com
37
+ # Sign in again later, or headless:
38
+ fml login https://fml.example.com
39
+ fml login https://fml.example.com --device
37
40
  ```
38
41
 
39
- The URL must be an HTTPS origin with no path, query, credentials, or fragment.
40
- Plain HTTP is accepted only for loopback development servers.
42
+ The backend origin itself (for example `https://actions.fml.example.com`) is
43
+ also accepted. Either URL must be an HTTPS origin with no path, query,
44
+ credentials, or fragment. Plain HTTP is accepted only for loopback development
45
+ servers.
41
46
 
42
47
  Run `fml update` to check for and install a newer global package with npm. It
43
48
  reports the version installed and refreshes FML setup automatically. Stable
@@ -308,7 +313,7 @@ fml tools call <tool-name> --args '{"key":"value"}'
308
313
  ## CLI overview
309
314
 
310
315
  ```text
311
- fml setup Install integrations, sign in if needed, and start FML
316
+ fml setup [backend-url] Install integrations, sign in if needed, and start FML
312
317
  fml install Register FML agent integrations and the MCP server
313
318
  fml uninstall Remove agent integration; add --purge to remove local state
314
319
  fml update Update @fml-inc/fml with npm (stable: latest; prerelease: next)
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  FML_HOOK_CAPTURE_DISABLED_FILE
3
- } from "./chunk-KWCFT54L.js";
3
+ } from "./chunk-B3H2YWPJ.js";
4
4
 
5
5
  // src/json-file.ts
6
6
  import { randomUUID } from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  FML_HOOK_INBOX_DIR
3
- } from "./chunk-KWCFT54L.js";
3
+ } from "./chunk-B3H2YWPJ.js";
4
4
 
5
5
  // ../../packages/panopticon/src/observer/auth.ts
6
6
  import crypto from "crypto";
@@ -192,12 +192,20 @@ function ensureDirs() {
192
192
  }
193
193
 
194
194
  // src/config.ts
195
- var DEFAULT_PROD_URL = "https://trustworthy-chihuahua-382.convex.cloud";
196
- var DEFAULT_SYNC_URL = DEFAULT_PROD_URL.replace(
197
- ".convex.cloud",
198
- ".convex.site"
199
- );
195
+ var DEFAULT_PROD_URL = "https://api.fml.inc";
200
196
  var DEFAULT_TARGET_NAME = "fml";
197
+ var LEGACY_HOSTED_BACKEND_URLS = [
198
+ "https://trustworthy-chihuahua-382.convex.site"
199
+ ];
200
+ function isHostedBackendUrl(url) {
201
+ let normalized;
202
+ try {
203
+ normalized = normalizeConvexUrl(url);
204
+ } catch {
205
+ return false;
206
+ }
207
+ return normalized === DEFAULT_PROD_URL || LEGACY_HOSTED_BACKEND_URLS.includes(normalized);
208
+ }
201
209
  var ENV_CONFIG_PATH = path2.join(FML_DATA_DIR, "env.json");
202
210
  var ENV_NAME_RE = /^[A-Za-z0-9_-]+$/;
203
211
  function isValidEnvName(envName) {
@@ -211,7 +219,10 @@ function normalizeConvexUrl(value) {
211
219
  "FML environment URL must be HTTPS (HTTP is allowed only for loopback) with no path, credentials, query, or fragment"
212
220
  );
213
221
  }
214
- return value.replace(/\/$/, "").replace(".convex.site", ".convex.cloud");
222
+ return url.origin;
223
+ }
224
+ function normalizeAppUrl(value) {
225
+ return normalizeConvexUrl(value);
215
226
  }
216
227
  function readEnvConfig() {
217
228
  try {
@@ -233,14 +244,19 @@ function readEnvConfig() {
233
244
  if (!isValidEnvName(name) || !environment || typeof environment !== "object" || Array.isArray(environment)) {
234
245
  continue;
235
246
  }
236
- const convexUrl = environment.convexUrl;
237
- if (typeof convexUrl !== "string") continue;
247
+ const entry = environment;
248
+ if (typeof entry.convexUrl !== "string") continue;
238
249
  try {
239
- environments[name] = { convexUrl: normalizeConvexUrl(convexUrl) };
250
+ const appUrl = typeof entry.appUrl === "string" ? normalizeAppUrl(entry.appUrl) : void 0;
251
+ environments[name] = {
252
+ convexUrl: normalizeConvexUrl(entry.convexUrl),
253
+ ...appUrl ? { appUrl } : {}
254
+ };
240
255
  } catch {
241
256
  }
242
257
  }
243
258
  }
259
+ environments[DEFAULT_TARGET_NAME] = { convexUrl: DEFAULT_PROD_URL };
244
260
  return {
245
261
  schemaVersion: 1,
246
262
  active: config.active,
@@ -275,7 +291,7 @@ function resolveEnvConvexUrl(envName) {
275
291
  if (envName === DEFAULT_TARGET_NAME) return DEFAULT_PROD_URL;
276
292
  return null;
277
293
  }
278
- function setActiveEnvironment(name, convexUrl) {
294
+ function setActiveEnvironment(name, convexUrl, appUrl) {
279
295
  if (!isValidEnvName(name)) {
280
296
  throw new Error(`Invalid FML environment name: ${name}`);
281
297
  }
@@ -285,19 +301,23 @@ function setActiveEnvironment(name, convexUrl) {
285
301
  active: name,
286
302
  environments: {
287
303
  ...current.environments ?? {},
288
- [name]: { convexUrl: normalizeConvexUrl(convexUrl) }
304
+ [name]: {
305
+ convexUrl: normalizeConvexUrl(convexUrl),
306
+ ...appUrl ? { appUrl: normalizeAppUrl(appUrl) } : {}
307
+ }
289
308
  }
290
309
  };
291
310
  writeEnvConfig(next);
292
311
  return next;
293
312
  }
294
313
  function getActiveEnv() {
295
- const name = readEnvConfig().active;
314
+ const config = readEnvConfig();
315
+ const name = config.active;
316
+ const stored = config.environments?.[name];
296
317
  const override = process.env.FML_CONVEX_URL;
297
- return {
298
- name,
299
- convexUrl: override ? normalizeConvexUrl(override) : resolveEnvConvexUrl(name)
300
- };
318
+ const convexUrl = override ? normalizeConvexUrl(override) : resolveEnvConvexUrl(name);
319
+ const appUrl = stored?.appUrl && stored.convexUrl === convexUrl ? stored.appUrl : null;
320
+ return { name, convexUrl, appUrl };
301
321
  }
302
322
  function authStorePathFor(envName) {
303
323
  return path2.join(FML_DATA_DIR, `auth.${envName}.json`);
@@ -311,7 +331,7 @@ function getSiteUrl() {
311
331
  function getSiteUrlForConvexUrl(convexUrl) {
312
332
  const explicit = process.env.CONVEX_SITE_URL;
313
333
  if (explicit) return explicit.replace(/\/$/, "");
314
- return (convexUrl ?? DEFAULT_PROD_URL).replace(".convex.cloud", ".convex.site").replace(/\/$/, "");
334
+ return (convexUrl ?? DEFAULT_PROD_URL).replace(/\/$/, "");
315
335
  }
316
336
 
317
337
  // src/sentry.ts
@@ -1587,8 +1607,10 @@ export {
1587
1607
  ensureDirs,
1588
1608
  DEFAULT_PROD_URL,
1589
1609
  DEFAULT_TARGET_NAME,
1610
+ isHostedBackendUrl,
1590
1611
  isValidEnvName,
1591
1612
  normalizeConvexUrl,
1613
+ readEnvConfig,
1592
1614
  writeEnvConfig,
1593
1615
  envConfigExists,
1594
1616
  resolveEnvConvexUrl,
@@ -7,7 +7,7 @@ import {
7
7
  getSelectedOrg,
8
8
  getSiteUrlForConvexUrl,
9
9
  getValidToken
10
- } from "./chunk-KWCFT54L.js";
10
+ } from "./chunk-B3H2YWPJ.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-TEPDSK2A.js");
82
+ const { readTokens, SERVICE_TOKEN_LOGIN_USER_ID } = await import("./token-store-ORR5MIJL.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;
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  OBSERVER_SERVICE_NAME
3
- } from "./chunk-3T5UCGBR.js";
3
+ } from "./chunk-5TOOBX7I.js";
4
4
  import {
5
5
  FML_OBSERVER_LOCK_FILE,
6
6
  FML_OBSERVER_PORT,
7
7
  FML_OBSERVER_STATE_FILE,
8
8
  Sentry
9
- } from "./chunk-KWCFT54L.js";
9
+ } from "./chunk-B3H2YWPJ.js";
10
10
 
11
11
  // src/observer/state.ts
12
12
  import { randomUUID } from "crypto";
@@ -990,7 +990,7 @@ var PACKAGE_NAME = "@fml-inc/fml";
990
990
  var UPDATE_TIMEOUT_MS = 12e4;
991
991
  var SETUP_TIMEOUT_MS = 3e5;
992
992
  function getCurrentVersion() {
993
- return true ? "0.8.6-rc.2+bda7cc0a" : "unknown";
993
+ return true ? "0.8.6-rc.3+f7c7191d" : "unknown";
994
994
  }
995
995
  function parseVersion(version) {
996
996
  const match = SEMVER_PATTERN.exec(version);
@@ -6,7 +6,7 @@ import {
6
6
  getSiteUrlForConvexUrl,
7
7
  getSyncDestinationIdentity,
8
8
  getValidToken
9
- } from "./chunk-KWCFT54L.js";
9
+ } from "./chunk-B3H2YWPJ.js";
10
10
 
11
11
  // src/hooks/sync-loop.ts
12
12
  import { createHash } from "crypto";
@@ -6,7 +6,7 @@ import {
6
6
  getSiteUrlForConvexUrl,
7
7
  getSyncDestinationIdentity,
8
8
  getValidToken
9
- } from "./chunk-KWCFT54L.js";
9
+ } from "./chunk-B3H2YWPJ.js";
10
10
 
11
11
  // ../../packages/panopticon/src/artifact.ts
12
12
  import {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  resolvePackageRoot,
3
3
  runRawSyncOnce
4
- } from "./chunk-Z2LWDZ6Z.js";
4
+ } from "./chunk-V2XC7JTB.js";
5
5
 
6
6
  // src/observer/sync-cycle.ts
7
7
  import { fork } from "child_process";
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createFmlClient,
4
4
  getAuthenticatedClient
5
- } from "./chunk-RZ5LWWRO.js";
5
+ } from "./chunk-LOJPIJZW.js";
6
6
  import {
7
7
  OBSERVER_HEARTBEAT_STALE_MS,
8
8
  execBinSync,
@@ -13,13 +13,13 @@ import {
13
13
  readObserverStateStatus,
14
14
  removeObserverStateIfOwned,
15
15
  resolveBin
16
- } from "./chunk-GQ2WVOGY.js";
16
+ } from "./chunk-ORYHJUC7.js";
17
17
  import {
18
18
  readJsonFile,
19
19
  setHookCaptureDisabled,
20
20
  writeJsonFile,
21
21
  writeTextFileAtomic
22
- } from "./chunk-FGR7SHV7.js";
22
+ } from "./chunk-2DQQXBWM.js";
23
23
  import {
24
24
  CODEX_HOOK_EVENTS,
25
25
  GEMINI_HOOK_EVENTS,
@@ -27,7 +27,7 @@ import {
27
27
  isObserverHealth,
28
28
  readHookInboxStatus,
29
29
  readObserverAuthToken
30
- } from "./chunk-3T5UCGBR.js";
30
+ } from "./chunk-5TOOBX7I.js";
31
31
  import "./chunk-7V7G7NVF.js";
32
32
  import {
33
33
  claudeConfigDirectory,
@@ -37,7 +37,7 @@ import {
37
37
  initializeSyncSettings,
38
38
  resolvePackageRoot,
39
39
  runRawSyncOnce
40
- } from "./chunk-Z2LWDZ6Z.js";
40
+ } from "./chunk-V2XC7JTB.js";
41
41
  import {
42
42
  DEFAULT_PROD_URL,
43
43
  DEFAULT_TARGET_NAME,
@@ -68,9 +68,11 @@ import {
68
68
  getSiteUrlForConvexUrl,
69
69
  getValidToken,
70
70
  initSentry,
71
+ isHostedBackendUrl,
71
72
  isValidEnvName,
72
73
  normalizeConvexUrl,
73
74
  observerOnlyOverrideWarning,
75
+ readEnvConfig,
74
76
  readTokens,
75
77
  resolveEnvConvexUrl,
76
78
  resolveFmlDefaultDirectories,
@@ -79,7 +81,7 @@ import {
79
81
  storeServiceRefreshToken,
80
82
  writeEnvConfig,
81
83
  writeTokens
82
- } from "./chunk-KWCFT54L.js";
84
+ } from "./chunk-B3H2YWPJ.js";
83
85
  import "./chunk-MLKGABMK.js";
84
86
 
85
87
  // src/cli.ts
@@ -127,7 +129,7 @@ var POLL_INTERVAL_MS = 50;
127
129
  var LOG_ROTATE_BYTES = 10 * 1024 * 1024;
128
130
  var LOG_ROTATE_FILES = 3;
129
131
  function expectedObserverVersion() {
130
- return true ? "0.8.6-rc.2+bda7cc0a" : "dev";
132
+ return true ? "0.8.6-rc.3+f7c7191d" : "dev";
131
133
  }
132
134
  function delay(ms) {
133
135
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -6828,8 +6830,16 @@ async function handleFmlRestart() {
6828
6830
  import crypto from "crypto";
6829
6831
  import http from "http";
6830
6832
  async function fetchWorkosLoginConfig(siteUrl) {
6831
- const response = await fetch(`${siteUrl}/api/auth/config`);
6832
- const data = await response.json();
6833
+ const configUrl = `${siteUrl}/api/auth/config`;
6834
+ const response = await fetch(configUrl);
6835
+ let data;
6836
+ try {
6837
+ data = await response.json();
6838
+ } catch {
6839
+ throw new Error(
6840
+ `${configUrl} did not return JSON (HTTP ${response.status}). Use the FML backend HTTP-actions origin, not the web app URL.`
6841
+ );
6842
+ }
6833
6843
  if (!response.ok || data.ok === false || !data.workosClientId) {
6834
6844
  throw new Error(data.error ?? `HTTP ${response.status}`);
6835
6845
  }
@@ -7001,14 +7011,6 @@ function startCallbackServer(expectedState) {
7001
7011
  function sleep(ms) {
7002
7012
  return new Promise((resolve) => setTimeout(resolve, ms));
7003
7013
  }
7004
- async function fetchWorkosClientId(siteUrl) {
7005
- const response = await fetch(`${siteUrl}/api/auth/config`);
7006
- const data = await response.json();
7007
- if (!response.ok || data.ok === false || !data.workosClientId) {
7008
- throw new Error(data.error ?? `HTTP ${response.status}`);
7009
- }
7010
- return data.workosClientId;
7011
- }
7012
7014
  function userName(user) {
7013
7015
  return [user.first_name, user.last_name].filter(Boolean).join(" ");
7014
7016
  }
@@ -7086,7 +7088,7 @@ async function pollForTokens(clientId, authorization) {
7086
7088
  throw new Error("Device authorization timed out. Run `fml login` again.");
7087
7089
  }
7088
7090
  async function deviceLogin(options) {
7089
- const clientId = await fetchWorkosClientId(options.siteUrl);
7091
+ const { clientId } = await fetchWorkosLoginConfig(options.siteUrl);
7090
7092
  const authorization = await requestDeviceAuthorization(clientId);
7091
7093
  console.log("");
7092
7094
  console.log(" Sign in with this code:");
@@ -7153,14 +7155,78 @@ function resolveGitHubToken() {
7153
7155
  }
7154
7156
  }
7155
7157
 
7158
+ // src/backend-discovery.ts
7159
+ var DISCOVERY_TIMEOUT_MS = 3e4;
7160
+ var defaultFetch = (input) => fetch(input, { signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS) });
7161
+ async function fetchJsonObject(url, fetchImpl) {
7162
+ let response;
7163
+ try {
7164
+ response = await fetchImpl(url);
7165
+ } catch (error) {
7166
+ const message = error instanceof Error && error.name === "TimeoutError" ? `no response within ${DISCOVERY_TIMEOUT_MS / 1e3}s` : error instanceof Error ? error.message : String(error);
7167
+ throw new Error(`Could not reach ${url}: ${message}`);
7168
+ }
7169
+ let parsed;
7170
+ try {
7171
+ parsed = await response.json();
7172
+ } catch {
7173
+ return null;
7174
+ }
7175
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
7176
+ return null;
7177
+ }
7178
+ return parsed;
7179
+ }
7180
+ async function discoverBackend(inputUrl, fetchImpl = defaultFetch) {
7181
+ const origin = normalizeConvexUrl(inputUrl);
7182
+ const appConfig = await fetchJsonObject(
7183
+ `${origin}/api/cli/config`,
7184
+ fetchImpl
7185
+ );
7186
+ if (appConfig?.ok === true && typeof appConfig.backendUrl === "string") {
7187
+ let backendUrl;
7188
+ try {
7189
+ backendUrl = normalizeConvexUrl(appConfig.backendUrl);
7190
+ } catch {
7191
+ throw new Error(
7192
+ `${origin} advertised an invalid FML backend URL: ${appConfig.backendUrl}`
7193
+ );
7194
+ }
7195
+ return { backendUrl, appUrl: origin };
7196
+ }
7197
+ if (appConfig && (appConfig.service === "fml-app" || appConfig.ok === false) && typeof appConfig.error === "string") {
7198
+ throw new Error(
7199
+ `${origin} could not provide its backend: ${appConfig.error}`
7200
+ );
7201
+ }
7202
+ const authConfig = await fetchJsonObject(
7203
+ `${origin}/api/auth/config`,
7204
+ fetchImpl
7205
+ );
7206
+ if (authConfig?.ok === true && typeof authConfig.workosClientId === "string") {
7207
+ return { backendUrl: origin };
7208
+ }
7209
+ throw new Error(
7210
+ `${origin} is not an FML web app or backend URL. Pass the address you use to open FML in the browser. If that app was deployed before CLI discovery existed, pass its backend HTTP origin instead.`
7211
+ );
7212
+ }
7213
+
7156
7214
  // src/commands/login.ts
7157
- function prepareLoginEnvironment(backendUrl) {
7158
- if (backendUrl) {
7159
- const convexUrl = normalizeConvexUrl(backendUrl);
7160
- setActiveEnvironment("custom", convexUrl);
7215
+ async function resolveLoginEnvironment(deploymentUrl) {
7216
+ if (deploymentUrl) {
7217
+ const discovered = await discoverBackend(deploymentUrl);
7218
+ if (isHostedBackendUrl(discovered.backendUrl)) {
7219
+ return {
7220
+ envName: DEFAULT_TARGET_NAME,
7221
+ siteUrl: getSiteUrlForConvexUrl(DEFAULT_PROD_URL),
7222
+ pendingConvexUrl: DEFAULT_PROD_URL
7223
+ };
7224
+ }
7161
7225
  return {
7162
7226
  envName: "custom",
7163
- siteUrl: getSiteUrlForConvexUrl(convexUrl)
7227
+ siteUrl: getSiteUrlForConvexUrl(discovered.backendUrl),
7228
+ pendingConvexUrl: discovered.backendUrl,
7229
+ ...discovered.appUrl ? { pendingAppUrl: discovered.appUrl } : {}
7164
7230
  };
7165
7231
  }
7166
7232
  const environment = getActiveEnv();
@@ -7169,6 +7235,22 @@ function prepareLoginEnvironment(backendUrl) {
7169
7235
  siteUrl: getSiteUrlForConvexUrl(environment.convexUrl)
7170
7236
  };
7171
7237
  }
7238
+ function commitLoginEnvironment(environment) {
7239
+ if (environment.pendingConvexUrl) {
7240
+ setActiveEnvironment(
7241
+ environment.envName,
7242
+ environment.pendingConvexUrl,
7243
+ environment.pendingAppUrl
7244
+ );
7245
+ }
7246
+ }
7247
+ function canReuseStoredLogin(stored, environment) {
7248
+ if (stored?.siteUrl) {
7249
+ if (stored.siteUrl === environment.siteUrl) return true;
7250
+ return environment.envName === DEFAULT_TARGET_NAME && isHostedBackendUrl(stored.siteUrl);
7251
+ }
7252
+ return !environment.pendingConvexUrl;
7253
+ }
7172
7254
  async function linkGitHubIdentity(environment) {
7173
7255
  const token = resolveGitHubToken();
7174
7256
  if (!token) {
@@ -7302,7 +7384,7 @@ async function selectOrg(environment) {
7302
7384
  }
7303
7385
  }
7304
7386
  async function runServiceTokenLogin(refreshToken, backendUrl) {
7305
- const environment = prepareLoginEnvironment(backendUrl);
7387
+ const environment = await resolveLoginEnvironment(backendUrl);
7306
7388
  const envName = environment.envName;
7307
7389
  if (!isValidEnvName(envName)) {
7308
7390
  throw new Error(
@@ -7318,6 +7400,7 @@ async function runServiceTokenLogin(refreshToken, backendUrl) {
7318
7400
  if (!ok) {
7319
7401
  throw new Error("Service token login failed.");
7320
7402
  }
7403
+ commitLoginEnvironment(environment);
7321
7404
  await selectOrg(environment);
7322
7405
  console.log("Logged in with FML service token.");
7323
7406
  console.log(
@@ -7340,11 +7423,14 @@ async function handleServiceTokenLogin(refreshToken, backendUrl) {
7340
7423
  }
7341
7424
  }
7342
7425
  async function runInteractiveLogin(opts = {}) {
7343
- const environment = prepareLoginEnvironment(opts.backendUrl);
7426
+ const environment = await resolveLoginEnvironment(opts.backendUrl);
7344
7427
  const envName = environment.envName;
7345
7428
  const stored = readTokens(envName);
7346
- const canReuseStoredLogin = !opts.backendUrl || stored?.siteUrl === environment.siteUrl;
7347
- const existingToken = canReuseStoredLogin ? await getValidToken({ env: envName }) : null;
7429
+ let existingToken = null;
7430
+ if (canReuseStoredLogin(stored, environment)) {
7431
+ commitLoginEnvironment(environment);
7432
+ existingToken = await getValidToken({ env: envName });
7433
+ }
7348
7434
  if (existingToken) {
7349
7435
  if (stored?.tokenType === "service") {
7350
7436
  const label = stored.user.id === SERVICE_TOKEN_LOGIN_USER_ID ? "a service token" : `${stored.user.name} (${stored.user.email})`;
@@ -7366,6 +7452,7 @@ async function runInteractiveLogin(opts = {}) {
7366
7452
  } else {
7367
7453
  result = await login(environment);
7368
7454
  }
7455
+ commitLoginEnvironment(environment);
7369
7456
  console.log(`Logged in as ${result.name} (${result.email})`);
7370
7457
  await linkGitHubIdentity(environment);
7371
7458
  await selectOrg(environment);
@@ -7393,7 +7480,7 @@ async function handleLogin(opts) {
7393
7480
  var defaultDependencies = {
7394
7481
  install: () => handleInstall(),
7395
7482
  getActiveToken: () => getValidToken({ env: getActiveEnv().name }),
7396
- login: () => runInteractiveLogin(),
7483
+ login: (options) => runInteractiveLogin(options),
7397
7484
  stop: () => handleFmlStop(),
7398
7485
  start: () => handleFmlStart()
7399
7486
  };
@@ -7408,15 +7495,25 @@ async function handleSetup(dependencies = defaultDependencies, options = {}) {
7408
7495
  if (process.exitCode) {
7409
7496
  throw new Error("FML setup cannot run after an earlier command failure.");
7410
7497
  }
7498
+ if (options.backendUrl && options.nonInteractive) {
7499
+ throw new Error(
7500
+ "A backend URL requires interactive login; omit --non-interactive."
7501
+ );
7502
+ }
7411
7503
  console.log("Setting up FML...");
7412
7504
  await runStep("integration installation", dependencies.install);
7413
- const token = await dependencies.getActiveToken();
7414
- if (token) {
7415
- console.log("Already signed in.");
7416
- } else if (options.nonInteractive) {
7417
- console.log("Not signed in; skipping login (non-interactive).");
7505
+ if (options.backendUrl) {
7506
+ const backendUrl = options.backendUrl;
7507
+ await runStep("login", () => dependencies.login({ backendUrl }));
7418
7508
  } else {
7419
- await runStep("login", dependencies.login);
7509
+ const token = await dependencies.getActiveToken();
7510
+ if (token) {
7511
+ console.log("Already signed in.");
7512
+ } else if (options.nonInteractive) {
7513
+ console.log("Not signed in; skipping login (non-interactive).");
7514
+ } else {
7515
+ await runStep("login", () => dependencies.login());
7516
+ }
7420
7517
  }
7421
7518
  await runStep("observer stop", dependencies.stop);
7422
7519
  await runStep("observer start", dependencies.start);
@@ -7927,9 +8024,12 @@ async function handleOrg(slug) {
7927
8024
 
7928
8025
  // src/commands/open.ts
7929
8026
  var DEFAULT_APP_BASE_URL = "https://app.fml.inc";
7930
- function appBaseUrl() {
8027
+ function appBaseUrl(environmentAppUrl) {
7931
8028
  const configured = process.env.FML_APP_URL?.trim();
7932
- return (configured || DEFAULT_APP_BASE_URL).replace(/\/+$/, "");
8029
+ return (configured || environmentAppUrl || DEFAULT_APP_BASE_URL).replace(
8030
+ /\/+$/,
8031
+ ""
8032
+ );
7933
8033
  }
7934
8034
  async function handleOpen(opts) {
7935
8035
  const environment = getActiveEnv();
@@ -7957,7 +8057,7 @@ async function handleOpen(opts) {
7957
8057
  );
7958
8058
  slug = "";
7959
8059
  }
7960
- const baseUrl = appBaseUrl();
8060
+ const baseUrl = appBaseUrl(environment.appUrl);
7961
8061
  const url = slug ? `${baseUrl}/${slug}` : baseUrl;
7962
8062
  if (opts.json) {
7963
8063
  console.log(JSON.stringify({ url }));
@@ -8246,22 +8346,35 @@ async function handleStatus() {
8246
8346
 
8247
8347
  // src/commands/env.ts
8248
8348
  function handleEnvShow() {
8249
- const { name, convexUrl } = getActiveEnv();
8349
+ const { name, convexUrl, appUrl } = getActiveEnv();
8250
8350
  console.log(`Environment: ${name}`);
8251
8351
  console.log(` Service URL: ${convexUrl ?? "not configured"}`);
8352
+ if (appUrl) console.log(` App URL: ${appUrl}`);
8252
8353
  }
8253
- function handleEnvSwitch(target2) {
8354
+ async function handleEnvSwitch(target2) {
8254
8355
  let name = target2;
8255
8356
  let convexUrl = null;
8357
+ let appUrl;
8256
8358
  try {
8257
8359
  if (/^https?:\/\//.test(target2)) {
8258
- name = "custom";
8259
- convexUrl = normalizeConvexUrl(target2);
8360
+ const discovered = await discoverBackend(target2);
8361
+ if (isHostedBackendUrl(discovered.backendUrl)) {
8362
+ name = DEFAULT_TARGET_NAME;
8363
+ convexUrl = DEFAULT_PROD_URL;
8364
+ } else {
8365
+ name = "custom";
8366
+ convexUrl = discovered.backendUrl;
8367
+ appUrl = discovered.appUrl;
8368
+ }
8260
8369
  } else {
8261
8370
  if (!isValidEnvName(target2)) {
8262
8371
  throw new Error(`Invalid FML environment name: ${target2}`);
8263
8372
  }
8264
8373
  convexUrl = process.env.FML_CONVEX_URL ? normalizeConvexUrl(process.env.FML_CONVEX_URL) : resolveEnvConvexUrl(target2);
8374
+ const stored = readEnvConfig().environments?.[target2];
8375
+ if (stored?.appUrl && stored.convexUrl === convexUrl) {
8376
+ appUrl = stored.appUrl;
8377
+ }
8265
8378
  }
8266
8379
  } catch (error) {
8267
8380
  console.error(
@@ -8273,14 +8386,15 @@ function handleEnvSwitch(target2) {
8273
8386
  if (!convexUrl) {
8274
8387
  console.error(`Environment ${name} has no configured service URL.`);
8275
8388
  console.error(
8276
- `Set FML_CONVEX_URL=https://<deployment>.convex.cloud and run \`fml env ${name}\` again.`
8389
+ `Set FML_CONVEX_URL=https://<backend-origin> and run \`fml env ${name}\` again.`
8277
8390
  );
8278
8391
  process.exitCode = 1;
8279
8392
  return;
8280
8393
  }
8281
- setActiveEnvironment(name, convexUrl);
8394
+ setActiveEnvironment(name, convexUrl, appUrl);
8282
8395
  console.log(`Environment switched to ${name}.`);
8283
8396
  console.log(`Service URL: ${convexUrl}`);
8397
+ if (appUrl) console.log(`App URL: ${appUrl}`);
8284
8398
  console.log("CLI commands and agent activity sync now use this environment.");
8285
8399
  console.log(
8286
8400
  "Start a new agent session to use the new environment and credentials."
@@ -8841,7 +8955,7 @@ async function handleConfigDetail(opts) {
8841
8955
  // src/commands/tools.ts
8842
8956
  import { readFileSync } from "fs";
8843
8957
  import path28 from "path";
8844
- var pluginVersion = true ? "0.8.6-rc.2+bda7cc0a" : void 0;
8958
+ var pluginVersion = true ? "0.8.6-rc.3+f7c7191d" : void 0;
8845
8959
  async function getApi() {
8846
8960
  const api = await getAuthenticatedClient();
8847
8961
  if (!api) {
@@ -8998,7 +9112,7 @@ if (process.argv.length <= 2 || process.argv.includes("--help") || process.argv.
8998
9112
  printBanner();
8999
9113
  }
9000
9114
  var program = new Command().name("fml").description("FML CLI and agent tools").version(
9001
- true ? "0.8.6-rc.2+bda7cc0a" : "dev"
9115
+ true ? "0.8.6-rc.3+f7c7191d" : "dev"
9002
9116
  );
9003
9117
  function formatCommandInventory(cmd, prefix = "") {
9004
9118
  const name = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
@@ -9027,13 +9141,19 @@ tools.command("call").description("Invoke a backend tool by name with JSON args"
9027
9141
  program.command("mcp-server", { hidden: true }).description("Run the FML MCP server on stdio").action(async () => {
9028
9142
  await import("./mcp/server.js");
9029
9143
  });
9030
- program.command("setup").description("Install integrations, sign in if needed, and start FML").addOption(
9144
+ program.command("setup").description("Install integrations, sign in if needed, and start FML").argument(
9145
+ "[backend-url]",
9146
+ "Self-hosted FML app or backend URL (saved as the active custom environment)"
9147
+ ).addOption(
9031
9148
  new Option(
9032
9149
  "--non-interactive",
9033
9150
  "Skip interactive login (internal)"
9034
9151
  ).hideHelp()
9035
9152
  ).action(
9036
- (opts) => handleSetup(void 0, { nonInteractive: Boolean(opts.nonInteractive) })
9153
+ (backendUrl, opts) => handleSetup(void 0, {
9154
+ nonInteractive: Boolean(opts.nonInteractive),
9155
+ backendUrl
9156
+ })
9037
9157
  );
9038
9158
  program.command("install").description("Register FML agent integrations and the MCP server").action(() => handleInstall());
9039
9159
  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(
@@ -9049,7 +9169,7 @@ program.command("update").description("Update fml when a newer release is availa
9049
9169
  );
9050
9170
  program.command("login").description("Sign in to your FML account").argument(
9051
9171
  "[backend-url]",
9052
- "Self-hosted FML backend URL (saved as the active custom environment)"
9172
+ "Self-hosted FML app or backend URL (saved as the active custom environment)"
9053
9173
  ).option("--device", "Use device authorization flow (no browser needed)").option(
9054
9174
  "--service-token",
9055
9175
  "Sign in with an org service refresh token (prompts for fml_srt_*)"
@@ -9061,8 +9181,8 @@ program.command("org").description("Show or select organization").argument("[slu
9061
9181
  program.command("status").description("Show auth, environment, and raw sync watermarks").action(() => handleStatus());
9062
9182
  program.command("doctor").description("Check FML configuration, agent activity sync, and connectivity").option("--json", "Output as JSON").action((opts) => handleDoctor(opts));
9063
9183
  program.command("open").description("Open FML dashboard in browser").option("--json", "Output URL as JSON instead of opening browser").action((opts) => handleOpen(opts));
9064
- program.command("env").description("Show or switch FML backend environment").argument("[target]", "Environment name or backend URL").action((target2) => {
9065
- if (target2) handleEnvSwitch(target2);
9184
+ program.command("env").description("Show or switch FML backend environment").argument("[target]", "Environment name, FML app URL, or backend URL").action(async (target2) => {
9185
+ if (target2) await handleEnvSwitch(target2);
9066
9186
  else handleEnvShow();
9067
9187
  });
9068
9188
  program.command("start").description("Start the local observer and continuous agent activity sync").action(() => handleFmlStart());
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  hookCaptureIsDisabled
4
- } from "./chunk-FGR7SHV7.js";
4
+ } from "./chunk-2DQQXBWM.js";
5
5
  import {
6
6
  getActiveHookDestinationBinding
7
- } from "./chunk-GST2GTOC.js";
7
+ } from "./chunk-PJWMSHHZ.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-3T5UCGBR.js";
15
+ } from "./chunk-5TOOBX7I.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-KWCFT54L.js";
26
+ } from "./chunk-B3H2YWPJ.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-RZ5LWWRO.js";
5
+ } from "../chunk-LOJPIJZW.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-KWCFT54L.js";
20
+ } from "../chunk-B3H2YWPJ.js";
21
21
  import "../chunk-MLKGABMK.js";
22
22
 
23
23
  // src/mcp/server.ts
@@ -11,11 +11,11 @@ import {
11
11
  removeObserverStateIfOwned,
12
12
  resolveBin,
13
13
  updateObserverState
14
- } from "./chunk-GQ2WVOGY.js";
14
+ } from "./chunk-ORYHJUC7.js";
15
15
  import {
16
16
  createHookSyncLoop,
17
17
  getActiveHookDestinationBinding
18
- } from "./chunk-GST2GTOC.js";
18
+ } from "./chunk-PJWMSHHZ.js";
19
19
  import {
20
20
  OBSERVER_PROTOCOL_VERSION,
21
21
  OBSERVER_SERVICE_NAME,
@@ -30,15 +30,15 @@ import {
30
30
  parseHookInboxMetadata,
31
31
  readHookInboxStatusAsync,
32
32
  retireHookInboxEntry
33
- } from "./chunk-3T5UCGBR.js";
33
+ } from "./chunk-5TOOBX7I.js";
34
34
  import "./chunk-7V7G7NVF.js";
35
35
  import {
36
36
  ObserverSyncCycleAbortedError,
37
37
  runObserverSyncCycleSubprocess
38
- } from "./chunk-JEZPS7F5.js";
38
+ } from "./chunk-YFHZJPTC.js";
39
39
  import {
40
40
  createCliRawSync
41
- } from "./chunk-Z2LWDZ6Z.js";
41
+ } from "./chunk-V2XC7JTB.js";
42
42
  import {
43
43
  FML_OBSERVER_AUTH_TOKEN_FILE,
44
44
  FML_OBSERVER_ENDPOINT_FILE,
@@ -56,7 +56,7 @@ import {
56
56
  getSyncDestinationAuthBinding,
57
57
  getValidToken,
58
58
  observerOnlyOverrideWarning
59
- } from "./chunk-KWCFT54L.js";
59
+ } from "./chunk-B3H2YWPJ.js";
60
60
  import "./chunk-MLKGABMK.js";
61
61
 
62
62
  // src/observer-server.ts
@@ -1480,7 +1480,7 @@ var hookRejectionStatus = () => ({
1480
1480
  ...lastRejectedReason !== void 0 ? { lastRejectedReason } : {},
1481
1481
  ...lastRejectedTarget !== void 0 ? { lastRejectedTarget } : {}
1482
1482
  });
1483
- var packageVersion = true ? "0.8.6-rc.2+bda7cc0a" : "dev";
1483
+ var packageVersion = true ? "0.8.6-rc.3+f7c7191d" : "dev";
1484
1484
  var health = () => ({
1485
1485
  status: "ok",
1486
1486
  service: OBSERVER_SERVICE_NAME,
@@ -853,11 +853,7 @@ var FML_HOOK_CAPTURE_DISABLED_FILE = path3.join(
853
853
  );
854
854
 
855
855
  // src/config.ts
856
- var DEFAULT_PROD_URL = "https://trustworthy-chihuahua-382.convex.cloud";
857
- var DEFAULT_SYNC_URL = DEFAULT_PROD_URL.replace(
858
- ".convex.cloud",
859
- ".convex.site"
860
- );
856
+ var DEFAULT_PROD_URL = "https://api.fml.inc";
861
857
  var DEFAULT_TARGET_NAME = "fml";
862
858
  var ENV_CONFIG_PATH = path4.join(FML_DATA_DIR, "env.json");
863
859
  var ENV_NAME_RE = /^[A-Za-z0-9_-]+$/;
@@ -872,7 +868,10 @@ function normalizeConvexUrl(value) {
872
868
  "FML environment URL must be HTTPS (HTTP is allowed only for loopback) with no path, credentials, query, or fragment"
873
869
  );
874
870
  }
875
- return value.replace(/\/$/, "").replace(".convex.site", ".convex.cloud");
871
+ return url.origin;
872
+ }
873
+ function normalizeAppUrl(value) {
874
+ return normalizeConvexUrl(value);
876
875
  }
877
876
  function readEnvConfig() {
878
877
  try {
@@ -894,14 +893,19 @@ function readEnvConfig() {
894
893
  if (!isValidEnvName(name) || !environment || typeof environment !== "object" || Array.isArray(environment)) {
895
894
  continue;
896
895
  }
897
- const convexUrl = environment.convexUrl;
898
- if (typeof convexUrl !== "string") continue;
896
+ const entry = environment;
897
+ if (typeof entry.convexUrl !== "string") continue;
899
898
  try {
900
- environments[name] = { convexUrl: normalizeConvexUrl(convexUrl) };
899
+ const appUrl = typeof entry.appUrl === "string" ? normalizeAppUrl(entry.appUrl) : void 0;
900
+ environments[name] = {
901
+ convexUrl: normalizeConvexUrl(entry.convexUrl),
902
+ ...appUrl ? { appUrl } : {}
903
+ };
901
904
  } catch {
902
905
  }
903
906
  }
904
907
  }
908
+ environments[DEFAULT_TARGET_NAME] = { convexUrl: DEFAULT_PROD_URL };
905
909
  return {
906
910
  schemaVersion: 1,
907
911
  active: config.active,
@@ -924,12 +928,13 @@ function resolveEnvConvexUrl(envName) {
924
928
  return null;
925
929
  }
926
930
  function getActiveEnv() {
927
- const name = readEnvConfig().active;
931
+ const config = readEnvConfig();
932
+ const name = config.active;
933
+ const stored = config.environments?.[name];
928
934
  const override = process.env.FML_CONVEX_URL;
929
- return {
930
- name,
931
- convexUrl: override ? normalizeConvexUrl(override) : resolveEnvConvexUrl(name)
932
- };
935
+ const convexUrl = override ? normalizeConvexUrl(override) : resolveEnvConvexUrl(name);
936
+ const appUrl = stored?.appUrl && stored.convexUrl === convexUrl ? stored.appUrl : null;
937
+ return { name, convexUrl, appUrl };
933
938
  }
934
939
  function authStorePathFor(envName) {
935
940
  return path4.join(FML_DATA_DIR, `auth.${envName}.json`);
@@ -939,7 +944,7 @@ var WORKOS_API_URL = "https://api.workos.com";
939
944
  function getSiteUrlForConvexUrl(convexUrl) {
940
945
  const explicit = process.env.CONVEX_SITE_URL;
941
946
  if (explicit) return explicit.replace(/\/$/, "");
942
- return (convexUrl ?? DEFAULT_PROD_URL).replace(".convex.cloud", ".convex.site").replace(/\/$/, "");
947
+ return (convexUrl ?? DEFAULT_PROD_URL).replace(/\/$/, "");
943
948
  }
944
949
 
945
950
  // src/sentry.ts
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runObserverSyncCycle
4
- } from "./chunk-JEZPS7F5.js";
5
- import "./chunk-Z2LWDZ6Z.js";
6
- import "./chunk-KWCFT54L.js";
4
+ } from "./chunk-YFHZJPTC.js";
5
+ import "./chunk-V2XC7JTB.js";
6
+ import "./chunk-B3H2YWPJ.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-KWCFT54L.js";
13
+ } from "./chunk-B3H2YWPJ.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.6-rc.2",
3
+ "version": "0.8.6-rc.3",
4
4
  "description": "FML CLI and agent tools",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",