@fml-inc/fml 0.0.27 → 0.0.29

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
@@ -8,25 +8,26 @@ import {
8
8
  createApiClient,
9
9
  ensureNativeModules,
10
10
  getAuthenticatedClient
11
- } from "./chunk-ND2YSPQ6.js";
11
+ } from "./chunk-RJOFOTNH.js";
12
12
  import "./chunk-UALBZA5X.js";
13
13
  import {
14
14
  AUTH_STORE_PATH,
15
15
  CONVEX_URL,
16
- DEFAULT_PROD_URL,
16
+ DEFAULT_SYNC_URL,
17
17
  DEFAULT_TARGET_NAME,
18
18
  Sentry,
19
19
  WORKOS_API_URL,
20
20
  WORKOS_AUTH_URL,
21
21
  getActiveEnv,
22
22
  getSelectedOrg,
23
+ getSiteUrl,
23
24
  getValidToken,
24
25
  initSentry,
25
26
  readTokens,
26
27
  setSelectedOrg,
27
28
  writeEnvConfig,
28
29
  writeTokens
29
- } from "./chunk-FS55W24B.js";
30
+ } from "./chunk-TXQRKU2Y.js";
30
31
  import {
31
32
  handleStart,
32
33
  handleStop,
@@ -2063,6 +2064,11 @@ function resolveGitHubToken() {
2063
2064
  return null;
2064
2065
  }
2065
2066
  }
2067
+ function resolveSyncTokenCommand() {
2068
+ if (resolveGitHubToken()) return "gh auth token";
2069
+ if (readTokens()) return "fml sync-token";
2070
+ return void 0;
2071
+ }
2066
2072
 
2067
2073
  // src/commands/install.ts
2068
2074
  var CLAUDE_DIR = path2.join(os2.homedir(), ".claude");
@@ -2224,24 +2230,23 @@ async function handleInstall() {
2224
2230
  }
2225
2231
  console.log();
2226
2232
  console.log("[5/5] Configuring sync target...");
2227
- const prodSyncUrl = DEFAULT_PROD_URL.replace(".cloud", ".site");
2228
2233
  const existingTargets = listTargets();
2229
- const existingProd = existingTargets.find((t) => t.url === prodSyncUrl);
2234
+ const existingProd = existingTargets.find((t) => t.url === DEFAULT_SYNC_URL);
2230
2235
  if (existingProd) {
2231
2236
  console.log(` Production target already configured`);
2232
2237
  } else {
2233
- const ghToken = resolveGitHubToken();
2234
- if (ghToken) {
2235
- addTarget({
2236
- name: DEFAULT_TARGET_NAME,
2237
- url: prodSyncUrl,
2238
- tokenCommand: "gh auth token"
2239
- });
2240
- console.log(` Target "${DEFAULT_TARGET_NAME}": ${prodSyncUrl}`);
2241
- console.log(" Auth: gh auth token");
2238
+ const tokenCommand = resolveSyncTokenCommand();
2239
+ addTarget({
2240
+ name: DEFAULT_TARGET_NAME,
2241
+ url: DEFAULT_SYNC_URL,
2242
+ tokenCommand
2243
+ });
2244
+ console.log(` Target "${DEFAULT_TARGET_NAME}": ${DEFAULT_SYNC_URL}`);
2245
+ if (tokenCommand) {
2246
+ console.log(` Auth: ${tokenCommand}`);
2242
2247
  } else {
2243
2248
  console.log(
2244
- " Skipped (no GitHub token). Run `fml sync setup` later."
2249
+ " Auth: not configured \u2014 run `fml login` to enable sync."
2245
2250
  );
2246
2251
  }
2247
2252
  }
@@ -2415,7 +2420,7 @@ function handleUninstall(opts) {
2415
2420
  // src/commands/update.ts
2416
2421
  import { execSync as execSync2 } from "child_process";
2417
2422
  async function handleUpdate() {
2418
- const currentVersion = true ? "0.0.27+924614a" : "unknown";
2423
+ const currentVersion = true ? "0.0.29+df7d11e" : "unknown";
2419
2424
  console.log(`Current: ${currentVersion}`);
2420
2425
  console.log("Updating via npm...\n");
2421
2426
  try {
@@ -2445,6 +2450,22 @@ async function fetchWorkosClientId() {
2445
2450
  const result = await client.query(ref, {});
2446
2451
  return result.workosClientId;
2447
2452
  }
2453
+ async function canOpenBrowser() {
2454
+ if (process.env.FML_DEVICE_FLOW === "1") return false;
2455
+ if (process.env.FML_NO_BROWSER === "1") return false;
2456
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
2457
+ return false;
2458
+ }
2459
+ if (process.env.SSH_CONNECTION) return false;
2460
+ if (process.env.CODESPACES) return false;
2461
+ if (process.env.REMOTE_CONTAINERS) return false;
2462
+ try {
2463
+ const fs4 = await import("fs");
2464
+ if (fs4.existsSync("/.dockerenv")) return false;
2465
+ } catch {
2466
+ }
2467
+ return true;
2468
+ }
2448
2469
  async function login() {
2449
2470
  const clientId = await fetchWorkosClientId();
2450
2471
  const codeVerifier = crypto.randomBytes(32).toString("base64url");
@@ -2568,6 +2589,93 @@ function startCallbackServer(expectedState) {
2568
2589
  });
2569
2590
  }
2570
2591
 
2592
+ // src/auth/device-flow.ts
2593
+ import { createInterface } from "readline";
2594
+ function sleep(ms) {
2595
+ return new Promise((resolve) => setTimeout(resolve, ms));
2596
+ }
2597
+ function prompt(question) {
2598
+ const rl = createInterface({
2599
+ input: process.stdin,
2600
+ output: process.stderr
2601
+ // Use stderr so stdout stays clean for MCP
2602
+ });
2603
+ return new Promise((resolve) => {
2604
+ rl.question(question, (answer) => {
2605
+ rl.close();
2606
+ resolve(answer.trim());
2607
+ });
2608
+ });
2609
+ }
2610
+ async function deviceLogin() {
2611
+ const siteUrl = getSiteUrl();
2612
+ const codeRes = await fetch(`${siteUrl}/api/auth/device/code`, {
2613
+ method: "POST",
2614
+ headers: { "Content-Type": "application/json" }
2615
+ });
2616
+ if (!codeRes.ok) {
2617
+ const body = await codeRes.text();
2618
+ throw new Error(`Failed to start device flow: ${body}`);
2619
+ }
2620
+ const codeData = await codeRes.json();
2621
+ console.log("");
2622
+ console.log(" Paste this URL in your browser to sign in:");
2623
+ console.log("");
2624
+ console.log(` ${codeData.verificationUri}`);
2625
+ console.log("");
2626
+ const confirmationCode = await prompt(" Enter the code from your browser: ");
2627
+ if (!confirmationCode) {
2628
+ throw new Error("No confirmation code entered.");
2629
+ }
2630
+ const normalizedCode = confirmationCode.toUpperCase().replace(/[\s-]/g, "").replace(/^(.{4})/, "$1-");
2631
+ console.log(" Verifying...");
2632
+ let pollInterval = codeData.interval * 1e3;
2633
+ while (Date.now() < codeData.expiresAt) {
2634
+ const pollRes = await fetch(`${siteUrl}/api/auth/device/poll`, {
2635
+ method: "POST",
2636
+ headers: { "Content-Type": "application/json" },
2637
+ body: JSON.stringify({
2638
+ deviceCode: codeData.deviceCode,
2639
+ confirmationCode: normalizedCode
2640
+ })
2641
+ });
2642
+ if (pollRes.status === 429) {
2643
+ const data2 = await pollRes.json();
2644
+ pollInterval = (data2.interval ?? codeData.interval * 2) * 1e3;
2645
+ await sleep(pollInterval);
2646
+ continue;
2647
+ }
2648
+ const data = await pollRes.json();
2649
+ if (data.status === "pending") {
2650
+ await sleep(pollInterval);
2651
+ continue;
2652
+ }
2653
+ if (data.status === "authorized") {
2654
+ writeTokens({
2655
+ accessToken: data.accessToken,
2656
+ refreshToken: data.refreshToken,
2657
+ expiresAt: data.expiresAt,
2658
+ user: data.user,
2659
+ orgSlug: data.orgSlug ?? void 0,
2660
+ tokenType: "service"
2661
+ });
2662
+ return { email: data.user.email, name: data.user.name };
2663
+ }
2664
+ if (data.status === "invalid_code") {
2665
+ throw new Error(
2666
+ "Invalid confirmation code. Make sure you copied the code from the browser correctly."
2667
+ );
2668
+ }
2669
+ if (data.status === "expired") {
2670
+ throw new Error("Device authorization expired. Run `fml login` again.");
2671
+ }
2672
+ throw new Error(`Device authorization failed: ${data.status}`);
2673
+ }
2674
+ throw new Error(
2675
+ "Device authorization timed out (15 minutes). Run `fml login` again."
2676
+ );
2677
+ }
2678
+
2571
2679
  // src/commands/login.ts
2572
2680
  async function linkGitHubIdentity() {
2573
2681
  const token = resolveGitHubToken();
@@ -2594,10 +2702,7 @@ async function linkGitHubIdentity() {
2594
2702
  console.warn("[fml] No FML token available \u2014 skipping identity link");
2595
2703
  return;
2596
2704
  }
2597
- if (!CONVEX_URL) {
2598
- console.warn(
2599
- "[fml] No Convex URL configured \u2014 skipping identity link (run fml install first)"
2600
- );
2705
+ if (fmlToken.startsWith("fml_st_")) {
2601
2706
  return;
2602
2707
  }
2603
2708
  const { ConvexHttpClient } = await import("convex/browser");
@@ -2615,6 +2720,33 @@ async function linkGitHubIdentity() {
2615
2720
  console.warn(`[fml] Failed to link GitHub identity: ${msg}`);
2616
2721
  }
2617
2722
  }
2723
+ function upgradeSyncTargetAfterLogin() {
2724
+ try {
2725
+ const config3 = loadSyncConfig();
2726
+ const existing = config3.targets.find((t) => t.name === DEFAULT_TARGET_NAME);
2727
+ if (!existing) {
2728
+ addTarget({
2729
+ name: DEFAULT_TARGET_NAME,
2730
+ url: DEFAULT_SYNC_URL,
2731
+ tokenCommand: "fml sync-token"
2732
+ });
2733
+ console.log(
2734
+ `Sync target "${DEFAULT_TARGET_NAME}" configured with fml sync-token.`
2735
+ );
2736
+ console.log("Restart panopticon to apply: fml stop && fml start");
2737
+ return;
2738
+ }
2739
+ if (!existing.tokenCommand && !existing.token) {
2740
+ existing.tokenCommand = "fml sync-token";
2741
+ saveSyncConfig(config3);
2742
+ console.log(`Sync target "${existing.name}" now using fml sync-token.`);
2743
+ console.log("Restart panopticon to apply: fml stop && fml start");
2744
+ }
2745
+ } catch (err) {
2746
+ const msg = err instanceof Error ? err.message : String(err);
2747
+ console.warn(`[fml] Could not update sync target: ${msg}`);
2748
+ }
2749
+ }
2618
2750
  async function selectOrg() {
2619
2751
  try {
2620
2752
  const token = await getValidToken();
@@ -2629,27 +2761,35 @@ async function selectOrg() {
2629
2761
  } catch {
2630
2762
  }
2631
2763
  }
2632
- async function handleLogin() {
2764
+ async function handleLogin(opts) {
2633
2765
  const { name: envName } = getActiveEnv();
2634
2766
  const existingToken = await getValidToken();
2635
2767
  if (existingToken) {
2636
- const { readTokens: readTokens2 } = await import("./token-store-KQSMR6XA.js");
2768
+ const { readTokens: readTokens2 } = await import("./token-store-NE7AM2PH.js");
2637
2769
  const stored = readTokens2();
2638
2770
  console.log(
2639
2771
  `Already logged in as ${stored?.user.name} (${stored?.user.email}) on ${envName}.`
2640
2772
  );
2641
2773
  await linkGitHubIdentity();
2642
2774
  await selectOrg();
2775
+ upgradeSyncTargetAfterLogin();
2643
2776
  console.log("You're all set! Restart Claude Code to use FML tools.");
2644
2777
  process.exit(0);
2645
2778
  }
2646
2779
  console.log(`Signing in to FML (${envName})...`);
2647
2780
  try {
2648
- const { email, name } = await login();
2781
+ const useDeviceFlow = opts?.device || !await canOpenBrowser();
2782
+ let result;
2783
+ if (useDeviceFlow) {
2784
+ result = await deviceLogin();
2785
+ } else {
2786
+ result = await login();
2787
+ }
2649
2788
  console.log(`
2650
- Logged in as ${name} (${email})`);
2789
+ Logged in as ${result.name} (${result.email})`);
2651
2790
  await linkGitHubIdentity();
2652
2791
  await selectOrg();
2792
+ upgradeSyncTargetAfterLogin();
2653
2793
  console.log("You're all set! Restart Claude Code to use FML tools.");
2654
2794
  process.exit(0);
2655
2795
  } catch (err) {
@@ -2760,26 +2900,24 @@ async function handleStatus() {
2760
2900
  }
2761
2901
 
2762
2902
  // src/commands/sync.ts
2763
- var FML_SYNC_URL = CONVEX_URL.replace(".cloud", ".site");
2764
2903
  async function handleSyncSetup() {
2765
2904
  console.log("FML Sync Setup\n");
2766
- const token = resolveGitHubToken();
2767
- if (!token) {
2905
+ const tokenCommand = resolveSyncTokenCommand();
2906
+ if (!tokenCommand) {
2768
2907
  console.error(
2769
- "No GitHub token found. Set PANOPTICON_GITHUB_TOKEN or install gh CLI."
2908
+ "No credential available. Run `fml login` first, or set PANOPTICON_GITHUB_TOKEN / install gh CLI."
2770
2909
  );
2771
2910
  process.exit(1);
2772
2911
  }
2773
- console.log("GitHub token: found");
2912
+ removeTarget(DEFAULT_TARGET_NAME);
2774
2913
  addTarget({
2775
2914
  name: DEFAULT_TARGET_NAME,
2776
- url: FML_SYNC_URL,
2777
- tokenCommand: "gh auth token"
2915
+ url: DEFAULT_SYNC_URL,
2916
+ tokenCommand
2778
2917
  });
2779
- console.log(`
2780
- Sync target "${DEFAULT_TARGET_NAME}" configured:`);
2781
- console.log(` URL: ${FML_SYNC_URL}`);
2782
- console.log(` Auth: gh auth token (refreshed every 5 min)`);
2918
+ console.log(`Sync target "${DEFAULT_TARGET_NAME}" configured:`);
2919
+ console.log(` URL: ${DEFAULT_SYNC_URL}`);
2920
+ console.log(` Auth: ${tokenCommand}`);
2783
2921
  console.log(`
2784
2922
  Restart panopticon to activate: fml stop && fml start`);
2785
2923
  }
@@ -2890,6 +3028,20 @@ async function handleSyncReset(targetName) {
2890
3028
  );
2891
3029
  }
2892
3030
 
3031
+ // src/commands/sync-token.ts
3032
+ import { once } from "events";
3033
+ async function handleSyncToken() {
3034
+ const token = await getValidToken();
3035
+ if (!token) {
3036
+ console.error("fml: not logged in. Run `fml login` to enable sync.");
3037
+ process.exit(1);
3038
+ }
3039
+ if (!process.stdout.write(token)) {
3040
+ await once(process.stdout, "drain");
3041
+ }
3042
+ process.exit(0);
3043
+ }
3044
+
2893
3045
  // src/commands/env.ts
2894
3046
  function handleEnvShow() {
2895
3047
  const { name, convexUrl } = getActiveEnv();
@@ -3467,7 +3619,7 @@ if (process.argv.length <= 2 || process.argv.includes("--help") || process.argv.
3467
3619
  printBanner();
3468
3620
  }
3469
3621
  var program = new Command().name("fml").description("FML agent tools for Claude Code").version(
3470
- true ? "0.0.27+924614a" : "dev"
3622
+ true ? "0.0.29+df7d11e" : "dev"
3471
3623
  );
3472
3624
  program.command("tools", { hidden: true }).description("List all available CLI commands (including hidden agent tools)").action(() => {
3473
3625
  function formatCmd(cmd, prefix = "") {
@@ -3494,7 +3646,7 @@ program.command("uninstall").description("Remove fml plugin, hooks, and optional
3494
3646
  "Target CLI: claude, gemini, codex, claude-desktop, all"
3495
3647
  ).option("--purge", "Also remove all data, logs, and auth tokens").action((opts) => handleUninstall(opts));
3496
3648
  program.command("update").description("Update fml to the latest version").action(() => handleUpdate());
3497
- program.command("login").description("Sign in to your FML account").action(() => handleLogin());
3649
+ program.command("login").description("Sign in to your FML account").option("--device", "Use device authorization flow (no browser needed)").action((opts) => handleLogin(opts));
3498
3650
  program.command("logout").description("Sign out and clear stored credentials").action(() => handleLogout());
3499
3651
  program.command("org").description("Show or select organization").argument("[slug]", "Org slug to select").action((slug) => handleOrg(slug));
3500
3652
  program.command("status").description("Show current auth and daemon status").action(() => handleStatus());
@@ -3518,6 +3670,9 @@ sync.command("edit").description("Edit a sync target").argument("<name>", "Targe
3518
3670
  );
3519
3671
  sync.command("status").description("Show sync config and watermarks").action(() => handleSyncStatus());
3520
3672
  sync.command("reset").description("Reset sync watermarks").argument("[name]", "Target name (resets all if omitted)").action((name) => handleSyncReset(name));
3673
+ program.command("sync-token", { hidden: true }).description(
3674
+ "Print a refreshed FML access token (for panopticon tokenCommand)"
3675
+ ).action(() => handleSyncToken());
3521
3676
  program.command("activity").description("Activity summary \u2014 sessions, prompts, tools, costs").option("--since <duration>", 'Time window, e.g. "24h", "7d"').action((opts) => handleActivity(opts));
3522
3677
  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));
3523
3678
  program.command("timeline").description("Get events for a session").argument("<session-id>", "Session ID").option("--limit <n>", "Max events to return").option("--offset <n>", "Events to skip").action((sessionId, opts) => handleTimeline(sessionId, opts));