@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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/{chunk-H2VOISTZ.js → chunk-OI562MUP.js} +37 -45
- package/dist/chunk-OI562MUP.js.map +1 -0
- package/dist/cli.js +111 -101
- package/dist/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/package.json +1 -2
- package/dist/chunk-H2VOISTZ.js.map +0 -1
|
@@ -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
|
-
|
|
74
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
orgSlug,
|
|
179
|
-
|
|
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
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
193
|
-
|
|
194
|
-
orgSlug,
|
|
195
|
-
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
{ orgSlug
|
|
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
|
|
208
|
-
|
|
209
|
-
orgSlug,
|
|
210
|
-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
|
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:
|
|
811
|
+
name: userName2
|
|
811
812
|
},
|
|
812
813
|
workosClientId: clientId
|
|
813
814
|
});
|
|
814
|
-
return { email: data.user.email, name:
|
|
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
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
return
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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/
|
|
929
|
-
body:
|
|
930
|
-
|
|
931
|
-
|
|
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
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
|
941
|
-
if (
|
|
942
|
-
await sleep(
|
|
938
|
+
const error = data.error;
|
|
939
|
+
if (error === "authorization_pending") {
|
|
940
|
+
await sleep(intervalMs);
|
|
943
941
|
continue;
|
|
944
942
|
}
|
|
945
|
-
if (
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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 (
|
|
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 (
|
|
951
|
+
if (error === "expired_token") {
|
|
962
952
|
throw new Error("Device authorization expired. Run `fml login` again.");
|
|
963
953
|
}
|
|
964
|
-
throw new Error(
|
|
954
|
+
throw new Error(
|
|
955
|
+
data.error_description ?? error ?? `HTTP ${response.status}`
|
|
956
|
+
);
|
|
965
957
|
}
|
|
966
|
-
throw new Error(
|
|
967
|
-
|
|
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
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 = "") {
|