@fml-inc/fml 0.4.0 → 0.6.0

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.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "FML agent tools for Claude Code",
5
5
  "mcpServers": {
6
6
  "fml": {
@@ -6,9 +6,6 @@ import {
6
6
 
7
7
  // src/fml-client.ts
8
8
  import { resolveRepoFromCwd } from "@fml-inc/panopticon/repo";
9
- function ref(path) {
10
- return path;
11
- }
12
9
  var LOGIN_EXPIRED_MESSAGE = "Authentication expired. Run `fml login` to sign in again, then restart Claude Code.";
13
10
  function isAuthError(data) {
14
11
  return data.code === "UNAUTHENTICATED" || data.code === "TOKEN_EXPIRED" || data.error?.includes("Unauthorized") === true || data.error?.includes("not authenticated") === true;
@@ -19,19 +16,14 @@ function normalizeToolResult(data) {
19
16
  }
20
17
  return data;
21
18
  }
19
+ function requireToolResult(result) {
20
+ if (!result.ok) {
21
+ throw new Error(result.error ?? "Backend request failed");
22
+ }
23
+ return result.result;
24
+ }
22
25
  function createFmlClient(token) {
23
26
  const isServiceToken = token.startsWith("fml_st_");
24
- let clientPromise = null;
25
- function getClient() {
26
- if (!clientPromise) {
27
- clientPromise = import("convex/browser").then(({ ConvexHttpClient }) => {
28
- const client = new ConvexHttpClient(CONVEX_URL);
29
- client.setAuth(token);
30
- return client;
31
- });
32
- }
33
- return clientPromise;
34
- }
35
27
  function getSiteUrl() {
36
28
  return CONVEX_URL.replace(".convex.cloud", ".convex.site").replace(
37
29
  /\/$/,
@@ -70,11 +62,8 @@ function createFmlClient(token) {
70
62
  return {
71
63
  // ── Orgs ──────────────────────────────────────────────────────────────
72
64
  async queryOrgs() {
73
- if (isServiceToken) {
74
- return [];
75
- }
76
- const client = await getClient();
77
- return await client.query(ref("user/plugin:getMyOrgsAndRepos"), {});
65
+ const result = await this.callBackend("list-orgs", {});
66
+ return requireToolResult(result) ?? [];
78
67
  },
79
68
  // ── Tool gateway ─────────────────────────────────────────────────────
80
69
  async callBackend(toolName, args, opts) {
@@ -173,42 +162,45 @@ function createFmlClient(token) {
173
162
  },
174
163
  // ── Repo resolution ──────────────────────────────────────────────────
175
164
  async resolveRepo(orgSlug, repoFullName) {
176
- const client = await getClient();
177
- return await client.query(ref("user/cli:resolveRepo"), {
178
- orgSlug,
179
- repoFullName
180
- });
165
+ const result = await this.callBackend(
166
+ "resolve-repo",
167
+ { orgSlug, repoFullName },
168
+ { org: orgSlug }
169
+ );
170
+ return requireToolResult(result);
181
171
  },
182
172
  // ── Config snapshots ─────────────────────────────────────────────────
183
173
  async listUserConfigSnapshots(orgSlug) {
184
- const client = await getClient();
185
- const result = await client.query(
186
- ref("user/config_snapshots:listUserSnapshots"),
187
- { orgSlug }
174
+ const result = await this.callBackend(
175
+ "list-user-config-snapshots",
176
+ { orgSlug },
177
+ { org: orgSlug }
188
178
  );
189
- return result ?? [];
179
+ return requireToolResult(result) ?? [];
190
180
  },
191
181
  async getUserConfigDetail(orgSlug, githubUsername) {
192
- const client = await getClient();
193
- return await client.query(ref("user/config_snapshots:getUserDetail"), {
194
- orgSlug,
195
- githubUsername
196
- });
182
+ const result = await this.callBackend(
183
+ "get-user-config-snapshot",
184
+ { orgSlug, githubUsername },
185
+ { org: orgSlug }
186
+ );
187
+ return requireToolResult(result);
197
188
  },
198
189
  async listRepoConfigSnapshots(orgSlug, repository) {
199
- const client = await getClient();
200
- const result = await client.query(
201
- ref("user/config_snapshots:listRepoSnapshots"),
202
- { orgSlug, repository }
190
+ const result = await this.callBackend(
191
+ "list-repo-config-snapshots",
192
+ { orgSlug, repository },
193
+ { org: orgSlug }
203
194
  );
204
- return result ?? [];
195
+ return requireToolResult(result) ?? [];
205
196
  },
206
197
  async getRepoConfigDetail(orgSlug, repository) {
207
- const client = await getClient();
208
- return await client.query(ref("user/config_snapshots:getRepoDetail"), {
209
- orgSlug,
210
- repository
211
- });
198
+ const result = await this.callBackend(
199
+ "get-repo-config-snapshot",
200
+ { orgSlug, repository },
201
+ { org: orgSlug }
202
+ );
203
+ return requireToolResult(result);
212
204
  }
213
205
  };
214
206
  }
@@ -222,4 +214,4 @@ export {
222
214
  createFmlClient,
223
215
  getAuthenticatedClient
224
216
  };
225
- //# sourceMappingURL=chunk-H2VOISTZ.js.map
217
+ //# sourceMappingURL=chunk-OI562MUP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fml-client.ts"],"sourcesContent":["import { resolveRepoFromCwd } from \"@fml-inc/panopticon/repo\";\nimport { getSelectedOrg, getValidToken } from \"./auth/token-store.js\";\nimport { CONVEX_URL } from \"./config.js\";\nimport type {\n RepoConfigSnapshotDetail,\n RepoConfigSnapshotSummary,\n ResolvedRepo,\n UserConfigSnapshotDetail,\n UserConfigSnapshotSummary,\n} from \"./types.js\";\n\n// ── Shared plumbing ─────────────────────────────────────────────────────────\n\nexport type ToolCategory =\n | \"messages\"\n | \"slack\"\n | \"skills\"\n | \"analysis\"\n | \"integrations\"\n | \"engineering\"\n | \"memory\"\n | \"automations\"\n | \"amplitude\"\n | \"posthog\"\n | \"meta-ads\";\n\nexport interface PublicToolDescriptor {\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n category?: ToolCategory;\n experimental?: boolean;\n}\n\nexport type KnownToolErrorCode =\n | \"UNAUTHENTICATED\"\n | \"TOKEN_EXPIRED\"\n | \"ACCESS_DENIED\"\n | \"ORG_REQUIRED\"\n | \"ORG_NOT_FOUND\"\n | \"REPO_NOT_FOUND\"\n | \"UNKNOWN_TOOL\"\n | \"INVALID_ARGS\"\n | \"RATE_LIMITED\"\n | \"INTERNAL_ERROR\";\n\nexport type ToolErrorCode = KnownToolErrorCode | (string & {});\n\nexport interface ToolResult {\n ok: boolean;\n result?: unknown;\n error?: string;\n code?: ToolErrorCode;\n}\n\nexport interface OrgInfo {\n _id: string;\n name: string;\n slug?: string;\n repos?: Array<{\n _id: string;\n fullName: string;\n owner: string;\n name: string;\n private: boolean;\n }>;\n}\n\nconst LOGIN_EXPIRED_MESSAGE =\n \"Authentication expired. Run `fml login` to sign in again, then restart Claude Code.\";\n\nfunction isAuthError(data: { error?: string; code?: string }): boolean {\n return (\n data.code === \"UNAUTHENTICATED\" ||\n data.code === \"TOKEN_EXPIRED\" ||\n data.error?.includes(\"Unauthorized\") === true ||\n data.error?.includes(\"not authenticated\") === true\n );\n}\n\nfunction normalizeToolResult(data: ToolResult): ToolResult {\n if (!data.ok && isAuthError(data)) {\n return { ok: false, error: LOGIN_EXPIRED_MESSAGE, code: data.code };\n }\n return data;\n}\n\nfunction requireToolResult<T>(result: ToolResult): T {\n if (!result.ok) {\n throw new Error(result.error ?? \"Backend request failed\");\n }\n return result.result as T;\n}\n\n// ── API client factory ──────────────────────────────────────────────────────\n\nexport function createFmlClient(token: string) {\n const isServiceToken = token.startsWith(\"fml_st_\");\n\n /**\n * Derive the Convex site URL (HTTP actions) from the cloud URL.\n * Convex uses paired domains: *.convex.cloud for client APIs,\n * *.convex.site for HTTP actions. This is a stable Convex convention.\n */\n function getSiteUrl(): string {\n return CONVEX_URL.replace(\".convex.cloud\", \".convex.site\").replace(\n /\\/$/,\n \"\",\n );\n }\n\n /** Authed fetch to a fml-be HTTP action, parsing the JSON envelope. */\n async function fetchContext(\n pathAndQuery: string,\n init: RequestInit,\n ): Promise<ToolResult> {\n try {\n const res = await fetch(`${getSiteUrl()}${pathAndQuery}`, {\n ...init,\n headers: {\n Authorization: `Bearer ${token}`,\n ...(init.body ? { \"Content-Type\": \"application/json\" } : {}),\n },\n });\n const text = await res.text();\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch {\n return { ok: false, error: `HTTP ${res.status}: ${text.slice(0, 200)}` };\n }\n if (!res.ok) {\n const envelope = data as { error?: string; code?: ToolErrorCode };\n return normalizeToolResult({\n ok: false,\n error: envelope.error ?? `HTTP ${res.status}`,\n code: envelope.code,\n });\n }\n return { ok: true, result: data };\n } catch (err) {\n return { ok: false, error: err instanceof Error ? err.message : String(err) };\n }\n }\n\n return {\n // ── Orgs ──────────────────────────────────────────────────────────────\n\n async queryOrgs(): Promise<OrgInfo[]> {\n const result = await this.callBackend(\"list-orgs\", {});\n return requireToolResult<OrgInfo[] | null>(result) ?? [];\n },\n\n // ── Tool gateway ─────────────────────────────────────────────────────\n\n async callBackend(\n toolName: string,\n args: Record<string, unknown>,\n opts?: { org?: string },\n ): Promise<ToolResult> {\n try {\n // Unified CLI/MCP path: POST to the dual-auth HTTP endpoint for both\n // service tokens and WorkOS/JWT user tokens. This keeps command\n // handlers independent of token class and matches the backend's agent\n // tool contract.\n const body: Record<string, unknown> = { toolName, args };\n\n // Explicit org > stored org selection > repo-based inference.\n const org = opts?.org ?? getSelectedOrg();\n if (org) body.org = org;\n const repo = resolveRepoFromCwd(process.cwd());\n if (repo) body.repo = repo.repo;\n\n // Thread user identity for service-token-backed sandbox agents. For\n // JWT callers this is ignored by the backend; for service tokens the\n // backend validates membership before honoring the override.\n if (isServiceToken) {\n const { readTokens } = await import(\"./auth/token-store.js\");\n const userExternalId =\n process.env.FML_USER_EXTERNAL_ID ?? readTokens()?.user.id;\n if (userExternalId) body.userExternalId = userExternalId;\n }\n\n const res = await fetch(`${getSiteUrl()}/api/tools/execute`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify(body),\n });\n const text = await res.text();\n let data: ToolResult;\n try {\n data = JSON.parse(text) as ToolResult;\n } catch {\n return {\n ok: false,\n error: `HTTP ${res.status}: ${text.slice(0, 200)}`,\n };\n }\n if (!res.ok && !data.error) {\n return { ok: false, error: `HTTP ${res.status}` };\n }\n return normalizeToolResult(data);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes(\"Unauthorized\") || msg.includes(\"not authenticated\")) {\n return { ok: false, error: LOGIN_EXPIRED_MESSAGE };\n }\n return { ok: false, error: msg };\n }\n },\n\n // ── Tool catalog ─────────────────────────────────────────────────────\n\n async listTools(pluginVersion?: string): Promise<PublicToolDescriptor[]> {\n try {\n // Always use the HTTP catalog endpoint — it accepts both a service\n // token and a user WorkOS JWT. A raw client.query rejects the user\n // JWT (no `aud` claim, which Convex's client-protocol auth requires),\n // so the httpAction path is the only one that works for both.\n const url = new URL(`${getSiteUrl()}/api/tools/list`);\n if (pluginVersion) url.searchParams.set(\"pluginVersion\", pluginVersion);\n const res = await fetch(url.toString(), {\n headers: { Authorization: `Bearer ${token}` },\n });\n const text = await res.text();\n let data: {\n ok: boolean;\n descriptors?: PublicToolDescriptor[];\n error?: string;\n code?: ToolErrorCode;\n };\n try {\n data = JSON.parse(text) as typeof data;\n } catch {\n throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);\n }\n if (!data.ok || !data.descriptors) {\n if (isAuthError(data)) {\n throw new Error(LOGIN_EXPIRED_MESSAGE);\n }\n throw new Error(data.error ?? `HTTP ${res.status}`);\n }\n return data.descriptors;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes(\"Unauthorized\") || msg.includes(\"not authenticated\")) {\n throw new Error(LOGIN_EXPIRED_MESSAGE);\n }\n throw err;\n }\n },\n\n // ── Anamnesis ground-truth context ───────────────────────────────────\n\n /**\n * Call a read-only anamnesis context endpoint\n * (/v1/anamnesis/context/{path,commit,pr}). Sends the current token\n * (service token or user JWT) — the endpoint accepts either.\n */\n async anamnesisContext(\n kind: \"path\" | \"commit\" | \"pr\",\n params: Record<string, string | number | undefined>,\n ): Promise<ToolResult> {\n const qs = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) {\n if (v != null) qs.set(k, String(v));\n }\n return fetchContext(`/v1/anamnesis/context/${kind}?${qs.toString()}`, {\n method: \"GET\",\n });\n },\n\n /** POST /v1/anamnesis/context/query — generic predicate query. */\n async anamnesisQuery(body: Record<string, unknown>): Promise<ToolResult> {\n return fetchContext(\"/v1/anamnesis/context/query\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n },\n\n // ── Repo resolution ──────────────────────────────────────────────────\n\n async resolveRepo(\n orgSlug: string,\n repoFullName: string,\n ): Promise<ResolvedRepo | null> {\n const result = await this.callBackend(\n \"resolve-repo\",\n { orgSlug, repoFullName },\n { org: orgSlug },\n );\n return requireToolResult<ResolvedRepo | null>(result);\n },\n\n // ── Config snapshots ─────────────────────────────────────────────────\n\n async listUserConfigSnapshots(\n orgSlug: string,\n ): Promise<UserConfigSnapshotSummary[]> {\n const result = await this.callBackend(\n \"list-user-config-snapshots\",\n { orgSlug },\n { org: orgSlug },\n );\n return requireToolResult<UserConfigSnapshotSummary[] | null>(result) ?? [];\n },\n\n async getUserConfigDetail(\n orgSlug: string,\n githubUsername: string,\n ): Promise<UserConfigSnapshotDetail | null> {\n const result = await this.callBackend(\n \"get-user-config-snapshot\",\n { orgSlug, githubUsername },\n { org: orgSlug },\n );\n return requireToolResult<UserConfigSnapshotDetail | null>(result);\n },\n\n async listRepoConfigSnapshots(\n orgSlug: string,\n repository?: string,\n ): Promise<RepoConfigSnapshotSummary[]> {\n const result = await this.callBackend(\n \"list-repo-config-snapshots\",\n { orgSlug, repository },\n { org: orgSlug },\n );\n return requireToolResult<RepoConfigSnapshotSummary[] | null>(result) ?? [];\n },\n\n async getRepoConfigDetail(\n orgSlug: string,\n repository: string,\n ): Promise<RepoConfigSnapshotDetail | null> {\n const result = await this.callBackend(\n \"get-repo-config-snapshot\",\n { orgSlug, repository },\n { org: orgSlug },\n );\n return requireToolResult<RepoConfigSnapshotDetail | null>(result);\n },\n };\n}\n\n// ── Convenience: auto-authenticated client ──────────────────────────────────\n\n/**\n * Create an API client using the stored auth token.\n * Returns null if not authenticated.\n */\nexport async function getAuthenticatedClient() {\n const token = await getValidToken();\n if (!token) return null;\n return createFmlClient(token);\n}\n"],"mappings":"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","names":[]}
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createFmlClient,
4
4
  getAuthenticatedClient
5
- } from "./chunk-H2VOISTZ.js";
5
+ } from "./chunk-OI562MUP.js";
6
6
  import {
7
7
  CONVEX_URL,
8
8
  DEFAULT_SYNC_URL,
@@ -705,7 +705,7 @@ function handleUninstall(opts) {
705
705
  // src/commands/update.ts
706
706
  import { execSync as execSync2 } from "child_process";
707
707
  async function handleUpdate() {
708
- const currentVersion = true ? "0.4.0+ef00cc0" : "unknown";
708
+ const currentVersion = true ? "0.6.0+c1e84c1" : "unknown";
709
709
  console.log(`Current: ${currentVersion}`);
710
710
  console.log("Updating via npm...\n");
711
711
  try {
@@ -736,11 +736,12 @@ import {
736
736
  import crypto from "crypto";
737
737
  import http from "http";
738
738
  async function fetchWorkosClientId() {
739
- const { ConvexHttpClient } = await import("convex/browser");
740
- const client = new ConvexHttpClient(CONVEX_URL);
741
- const ref = "public_config:getAuthConfig";
742
- const result = await client.query(ref, {});
743
- return result.workosClientId;
739
+ const response = await fetch(`${getSiteUrl()}/api/auth/config`);
740
+ const data = await response.json();
741
+ if (!response.ok || data.ok === false || !data.workosClientId) {
742
+ throw new Error(data.error ?? `HTTP ${response.status}`);
743
+ }
744
+ return data.workosClientId;
744
745
  }
745
746
  async function canOpenBrowser() {
746
747
  if (process.env.FML_DEVICE_FLOW === "1") return false;
@@ -799,7 +800,7 @@ async function login() {
799
800
  throw new Error(`Token exchange failed: ${error}`);
800
801
  }
801
802
  const data = await tokenResponse.json();
802
- const userName = [data.user.first_name, data.user.last_name].filter(Boolean).join(" ");
803
+ const userName2 = [data.user.first_name, data.user.last_name].filter(Boolean).join(" ");
803
804
  writeTokens({
804
805
  accessToken: data.access_token,
805
806
  refreshToken: data.refresh_token,
@@ -807,11 +808,11 @@ async function login() {
807
808
  user: {
808
809
  id: data.user.id,
809
810
  email: data.user.email,
810
- name: userName
811
+ name: userName2
811
812
  },
812
813
  workosClientId: clientId
813
814
  });
814
- return { email: data.user.email, name: userName };
815
+ return { email: data.user.email, name: userName2 };
815
816
  }
816
817
  function startCallbackServer(expectedState) {
817
818
  return new Promise((resolve, reject) => {
@@ -882,90 +883,109 @@ function startCallbackServer(expectedState) {
882
883
  }
883
884
 
884
885
  // src/auth/device-flow.ts
885
- import { createInterface } from "readline";
886
886
  function sleep(ms) {
887
887
  return new Promise((resolve) => setTimeout(resolve, ms));
888
888
  }
889
- function prompt(question) {
890
- const rl = createInterface({
891
- input: process.stdin,
892
- output: process.stderr
893
- // Use stderr so stdout stays clean for MCP
894
- });
895
- return new Promise((resolve) => {
896
- rl.question(question, (answer) => {
897
- rl.close();
898
- resolve(answer.trim());
899
- });
900
- });
889
+ async function fetchWorkosClientId2() {
890
+ const response = await fetch(`${getSiteUrl()}/api/auth/config`);
891
+ const data = await response.json();
892
+ if (!response.ok || data.ok === false || !data.workosClientId) {
893
+ throw new Error(data.error ?? `HTTP ${response.status}`);
894
+ }
895
+ return data.workosClientId;
901
896
  }
902
- async function deviceLogin() {
903
- const siteUrl = getSiteUrl();
904
- const codeRes = await fetch(`${siteUrl}/api/auth/device/code`, {
905
- method: "POST",
906
- headers: { "Content-Type": "application/json" }
907
- });
908
- if (!codeRes.ok) {
909
- const body = await codeRes.text();
910
- throw new Error(`Failed to start device flow: ${body}`);
897
+ function userName(user) {
898
+ return [user.first_name, user.last_name].filter(Boolean).join(" ");
899
+ }
900
+ async function requestDeviceAuthorization(clientId) {
901
+ const response = await fetch(
902
+ `${WORKOS_API_URL}/user_management/authorize/device`,
903
+ {
904
+ method: "POST",
905
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
906
+ body: new URLSearchParams({ client_id: clientId })
907
+ }
908
+ );
909
+ const data = await response.json();
910
+ if (!response.ok || !data.device_code || !data.user_code || !data.verification_uri) {
911
+ throw new Error(
912
+ data.error_description ?? data.error ?? `HTTP ${response.status}`
913
+ );
911
914
  }
912
- const codeData = await codeRes.json();
913
- console.log("");
914
- console.log(" Paste this URL in your browser to sign in:");
915
- console.log("");
916
- console.log(` ${codeData.verificationUri}`);
917
- console.log("");
918
- const confirmationCode = await prompt(" Enter the code from your browser: ");
919
- if (!confirmationCode) {
920
- throw new Error("No confirmation code entered.");
921
- }
922
- const normalizedCode = confirmationCode.toUpperCase().replace(/[\s-]/g, "").replace(/^(.{4})/, "$1-");
923
- console.log(" Verifying...");
924
- let pollInterval = codeData.interval * 1e3;
925
- while (Date.now() < codeData.expiresAt) {
926
- const pollRes = await fetch(`${siteUrl}/api/auth/device/poll`, {
915
+ return data;
916
+ }
917
+ async function pollForTokens(clientId, authorization) {
918
+ let intervalMs = (authorization.interval ?? 5) * 1e3;
919
+ const deadline = Date.now() + (authorization.expires_in ?? 300) * 1e3;
920
+ while (Date.now() < deadline) {
921
+ const response = await fetch(`${WORKOS_API_URL}/user_management/authenticate`, {
927
922
  method: "POST",
928
- headers: { "Content-Type": "application/json" },
929
- body: JSON.stringify({
930
- deviceCode: codeData.deviceCode,
931
- confirmationCode: normalizedCode
923
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
924
+ body: new URLSearchParams({
925
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
926
+ device_code: authorization.device_code,
927
+ client_id: clientId
932
928
  })
933
929
  });
934
- if (pollRes.status === 429) {
935
- const data2 = await pollRes.json();
936
- pollInterval = (data2.interval ?? codeData.interval * 2) * 1e3;
937
- await sleep(pollInterval);
938
- continue;
930
+ const data = await response.json();
931
+ if (response.ok) {
932
+ const success = data;
933
+ if (!success.access_token || !success.refresh_token || !success.user) {
934
+ throw new Error("Device authorization returned an incomplete token response.");
935
+ }
936
+ return success;
939
937
  }
940
- const data = await pollRes.json();
941
- if (data.status === "pending") {
942
- await sleep(pollInterval);
938
+ const error = data.error;
939
+ if (error === "authorization_pending") {
940
+ await sleep(intervalMs);
943
941
  continue;
944
942
  }
945
- if (data.status === "authorized") {
946
- writeTokens({
947
- accessToken: data.accessToken,
948
- refreshToken: data.refreshToken,
949
- expiresAt: data.expiresAt,
950
- user: data.user,
951
- orgSlug: data.orgSlug ?? void 0,
952
- tokenType: "service"
953
- });
954
- return { email: data.user.email, name: data.user.name };
943
+ if (error === "slow_down") {
944
+ intervalMs += 5e3;
945
+ await sleep(intervalMs);
946
+ continue;
955
947
  }
956
- if (data.status === "invalid_code") {
957
- throw new Error(
958
- "Invalid confirmation code. Make sure you copied the code from the browser correctly."
959
- );
948
+ if (error === "access_denied") {
949
+ throw new Error("Device authorization was denied.");
960
950
  }
961
- if (data.status === "expired") {
951
+ if (error === "expired_token") {
962
952
  throw new Error("Device authorization expired. Run `fml login` again.");
963
953
  }
964
- throw new Error(`Device authorization failed: ${data.status}`);
954
+ throw new Error(
955
+ data.error_description ?? error ?? `HTTP ${response.status}`
956
+ );
965
957
  }
966
- throw new Error(
967
- "Device authorization timed out (15 minutes). Run `fml login` again."
958
+ throw new Error("Device authorization timed out. Run `fml login` again.");
959
+ }
960
+ async function deviceLogin() {
961
+ const clientId = await fetchWorkosClientId2();
962
+ const authorization = await requestDeviceAuthorization(clientId);
963
+ console.log("");
964
+ console.log(" Sign in with this code:");
965
+ console.log("");
966
+ console.log(` ${authorization.user_code}`);
967
+ console.log("");
968
+ console.log(" Open this URL in your browser:");
969
+ console.log("");
970
+ console.log(
971
+ ` ${authorization.verification_uri_complete ?? authorization.verification_uri}`
968
972
  );
973
+ console.log("");
974
+ console.log(" Waiting for authorization...");
975
+ const data = await pollForTokens(clientId, authorization);
976
+ const name = userName(data.user);
977
+ writeTokens({
978
+ accessToken: data.access_token,
979
+ refreshToken: data.refresh_token,
980
+ expiresAt: Date.now() + (data.expires_in ?? 300) * 1e3,
981
+ user: {
982
+ id: data.user.id,
983
+ email: data.user.email,
984
+ name
985
+ },
986
+ workosClientId: clientId
987
+ });
988
+ return { email: data.user.email, name };
969
989
  }
970
990
 
971
991
  // src/commands/login.ts
@@ -976,19 +996,6 @@ async function linkGitHubIdentity() {
976
996
  return;
977
997
  }
978
998
  try {
979
- const response = await fetch("https://api.github.com/user", {
980
- headers: {
981
- Authorization: `Bearer ${token}`,
982
- Accept: "application/vnd.github+json"
983
- }
984
- });
985
- if (!response.ok) {
986
- console.warn(
987
- `[fml] GitHub API returned ${response.status} \u2014 skipping identity link`
988
- );
989
- return;
990
- }
991
- const user = await response.json();
992
999
  const fmlToken = await getValidToken();
993
1000
  if (!fmlToken) {
994
1001
  console.warn("[fml] No FML token available \u2014 skipping identity link");
@@ -997,16 +1004,19 @@ async function linkGitHubIdentity() {
997
1004
  if (fmlToken.startsWith("fml_st_")) {
998
1005
  return;
999
1006
  }
1000
- const { ConvexHttpClient } = await import("convex/browser");
1001
- const client = new ConvexHttpClient(CONVEX_URL);
1002
- client.setAuth(fmlToken);
1003
- const ref = "user/panopticon:linkGitHubIdentity";
1004
- await client.mutation(ref, {
1005
- githubUsername: user.login,
1006
- githubId: user.id,
1007
- githubEmail: user.email ?? void 0
1007
+ const linkResponse = await fetch(`${getSiteUrl()}/api/auth/link-github-identity`, {
1008
+ method: "POST",
1009
+ headers: {
1010
+ "Content-Type": "application/json",
1011
+ Authorization: `Bearer ${fmlToken}`
1012
+ },
1013
+ body: JSON.stringify({ githubAccessToken: token })
1008
1014
  });
1009
- console.log(`Linked GitHub account: ${user.login}`);
1015
+ const linkResult = await linkResponse.json();
1016
+ if (!linkResponse.ok || linkResult.ok === false) {
1017
+ throw new Error(linkResult.error ?? `HTTP ${linkResponse.status}`);
1018
+ }
1019
+ console.log("Linked GitHub account");
1010
1020
  } catch (err) {
1011
1021
  const msg = err instanceof Error ? err.message : String(err);
1012
1022
  console.warn(`[fml] Failed to link GitHub identity: ${msg}`);
@@ -1951,7 +1961,7 @@ async function handleConfigDetail(opts) {
1951
1961
  // src/commands/tools.ts
1952
1962
  import { readFileSync } from "fs";
1953
1963
  import path3 from "path";
1954
- var pluginVersion = true ? "0.4.0+ef00cc0" : void 0;
1964
+ var pluginVersion = true ? "0.6.0+c1e84c1" : void 0;
1955
1965
  async function getApi() {
1956
1966
  const api = await getAuthenticatedClient();
1957
1967
  if (!api) {
@@ -2082,7 +2092,7 @@ if (process.argv.length <= 2 || process.argv.includes("--help") || process.argv.
2082
2092
  printBanner();
2083
2093
  }
2084
2094
  var program = new Command().name("fml").description("FML agent tools for Claude Code").version(
2085
- true ? "0.4.0+ef00cc0" : "dev"
2095
+ true ? "0.6.0+c1e84c1" : "dev"
2086
2096
  );
2087
2097
  var tools = program.command("tools", { hidden: true }).description("List all available CLI commands (including hidden agent tools)").action(() => {
2088
2098
  function formatCmd(cmd, prefix = "") {