@fml-inc/fml 0.3.0 → 0.4.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/bin/fml +17 -0
- package/bin/mcp-server +17 -0
- package/dist/{chunk-CUBBYHH7.js → chunk-5BHBYQ2F.js} +84 -30
- package/dist/chunk-5BHBYQ2F.js.map +1 -0
- package/dist/chunk-H2VOISTZ.js +225 -0
- package/dist/chunk-H2VOISTZ.js.map +1 -0
- package/dist/{chunk-MUKYS4IB.js → chunk-YH5ROR7G.js} +1 -25
- package/dist/chunk-YH5ROR7G.js.map +1 -0
- package/dist/{chunk-CFPXJT5Q.js → chunk-ZFUJ5ZQP.js} +17 -9
- package/dist/chunk-ZFUJ5ZQP.js.map +1 -0
- package/dist/cli.js +39 -29
- package/dist/cli.js.map +1 -1
- package/dist/daemon-J27JBWLV.js +17 -0
- package/dist/daemon-utils-OODBIYB7.js +10 -0
- package/dist/mcp/server.js +112 -13
- package/dist/mcp/server.js.map +1 -1
- package/dist/{token-store-HOFFHNNN.js → token-store-T2RNLXRM.js} +2 -3
- package/package.json +2 -8
- package/bin/sync-daemon +0 -3
- package/dist/chunk-4YEAQJPF.js +0 -47
- package/dist/chunk-4YEAQJPF.js.map +0 -1
- package/dist/chunk-BOREGHA6.js +0 -221
- package/dist/chunk-BOREGHA6.js.map +0 -1
- package/dist/chunk-CFPXJT5Q.js.map +0 -1
- package/dist/chunk-CUBBYHH7.js.map +0 -1
- package/dist/chunk-MUKYS4IB.js.map +0 -1
- package/dist/daemon-GM3WNS4Z.js +0 -16
- package/dist/daemon-utils-CJO4XVJQ.js +0 -17
- /package/dist/{daemon-GM3WNS4Z.js.map → daemon-J27JBWLV.js.map} +0 -0
- /package/dist/{daemon-utils-CJO4XVJQ.js.map → daemon-utils-OODBIYB7.js.map} +0 -0
- /package/dist/{token-store-HOFFHNNN.js.map → token-store-T2RNLXRM.js.map} +0 -0
package/bin/fml
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Suppress Node's experimental SQLite warning (emitted by node:sqlite, pulled in
|
|
4
|
+
// transitively via @fml-inc/panopticon) before the dynamic import below loads it.
|
|
5
|
+
// Scoped to the SQLite warning so other ExperimentalWarnings still surface.
|
|
6
|
+
const originalEmit = process.emit;
|
|
7
|
+
process.emit = function (name, data, ...args) {
|
|
8
|
+
if (
|
|
9
|
+
name === "warning" &&
|
|
10
|
+
data &&
|
|
11
|
+
data.name === "ExperimentalWarning" &&
|
|
12
|
+
typeof data.message === "string" &&
|
|
13
|
+
data.message.includes("SQLite")
|
|
14
|
+
) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return originalEmit.call(this, name, data, ...args);
|
|
18
|
+
};
|
|
19
|
+
|
|
3
20
|
import("../dist/cli.js");
|
package/bin/mcp-server
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Suppress Node's experimental SQLite warning (emitted by node:sqlite, pulled in
|
|
4
|
+
// transitively via @fml-inc/panopticon) before the dynamic import below loads it.
|
|
5
|
+
// Scoped to the SQLite warning so other ExperimentalWarnings still surface.
|
|
6
|
+
const originalEmit = process.emit;
|
|
7
|
+
process.emit = function (name, data, ...args) {
|
|
8
|
+
if (
|
|
9
|
+
name === "warning" &&
|
|
10
|
+
data &&
|
|
11
|
+
data.name === "ExperimentalWarning" &&
|
|
12
|
+
typeof data.message === "string" &&
|
|
13
|
+
data.message.includes("SQLite")
|
|
14
|
+
) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return originalEmit.call(this, name, data, ...args);
|
|
18
|
+
};
|
|
19
|
+
|
|
3
20
|
import("../dist/mcp/server.js");
|
|
@@ -1,52 +1,97 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FML_DATA_DIR
|
|
3
|
-
} from "./chunk-4YEAQJPF.js";
|
|
4
|
-
|
|
5
1
|
// src/auth/token-store.ts
|
|
2
|
+
import fs3 from "fs";
|
|
3
|
+
import path3 from "path";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
6
|
import fs2 from "fs";
|
|
7
|
+
import os2 from "os";
|
|
7
8
|
import path2 from "path";
|
|
8
9
|
|
|
9
|
-
// src/
|
|
10
|
+
// src/dirs.ts
|
|
10
11
|
import fs from "fs";
|
|
11
12
|
import os from "os";
|
|
12
13
|
import path from "path";
|
|
14
|
+
var home = os.homedir();
|
|
15
|
+
function dataDir() {
|
|
16
|
+
if (process.env.FML_DATA_DIR) return process.env.FML_DATA_DIR;
|
|
17
|
+
switch (process.platform) {
|
|
18
|
+
case "darwin":
|
|
19
|
+
return path.join(home, "Library", "Application Support", "fml");
|
|
20
|
+
case "win32":
|
|
21
|
+
return path.join(
|
|
22
|
+
process.env.APPDATA ?? path.join(home, "AppData", "Roaming"),
|
|
23
|
+
"fml"
|
|
24
|
+
);
|
|
25
|
+
default:
|
|
26
|
+
return path.join(
|
|
27
|
+
process.env.XDG_DATA_HOME ?? path.join(home, ".local", "share"),
|
|
28
|
+
"fml"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function logDir() {
|
|
33
|
+
if (process.env.FML_LOG_DIR) return process.env.FML_LOG_DIR;
|
|
34
|
+
switch (process.platform) {
|
|
35
|
+
case "darwin":
|
|
36
|
+
return path.join(home, "Library", "Logs", "fml");
|
|
37
|
+
case "win32":
|
|
38
|
+
return path.join(
|
|
39
|
+
process.env.APPDATA ?? path.join(home, "AppData", "Roaming"),
|
|
40
|
+
"fml",
|
|
41
|
+
"logs"
|
|
42
|
+
);
|
|
43
|
+
default:
|
|
44
|
+
return path.join(
|
|
45
|
+
process.env.XDG_STATE_HOME ?? path.join(home, ".local", "state"),
|
|
46
|
+
"fml",
|
|
47
|
+
"log"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
var FML_DATA_DIR = dataDir();
|
|
52
|
+
var FML_LOG_DIR = logDir();
|
|
53
|
+
|
|
54
|
+
// src/config.ts
|
|
13
55
|
var DEFAULT_PROD_URL = "https://trustworthy-chihuahua-382.convex.cloud";
|
|
14
56
|
var DEFAULT_SYNC_URL = DEFAULT_PROD_URL.replace(
|
|
15
57
|
".convex.cloud",
|
|
16
58
|
".convex.site"
|
|
17
59
|
);
|
|
18
60
|
var DEFAULT_TARGET_NAME = "fml";
|
|
19
|
-
var ENV_CONFIG_PATH =
|
|
61
|
+
var ENV_CONFIG_PATH = path2.join(FML_DATA_DIR, "env.json");
|
|
20
62
|
function readEnvConfig() {
|
|
21
63
|
try {
|
|
22
|
-
const raw =
|
|
64
|
+
const raw = fs2.readFileSync(ENV_CONFIG_PATH, "utf-8");
|
|
23
65
|
return JSON.parse(raw);
|
|
24
66
|
} catch {
|
|
25
67
|
return { active: DEFAULT_TARGET_NAME };
|
|
26
68
|
}
|
|
27
69
|
}
|
|
28
70
|
function writeEnvConfig(config) {
|
|
29
|
-
const dir =
|
|
30
|
-
if (!
|
|
31
|
-
|
|
71
|
+
const dir = path2.dirname(ENV_CONFIG_PATH);
|
|
72
|
+
if (!fs2.existsSync(dir)) {
|
|
73
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
32
74
|
}
|
|
33
|
-
|
|
75
|
+
fs2.writeFileSync(ENV_CONFIG_PATH, `${JSON.stringify(config, null, 2)}
|
|
34
76
|
`, {
|
|
35
77
|
mode: 384
|
|
36
78
|
});
|
|
37
79
|
}
|
|
80
|
+
function envConfigExists() {
|
|
81
|
+
return fs2.existsSync(ENV_CONFIG_PATH);
|
|
82
|
+
}
|
|
38
83
|
function resolveEnvConvexUrl(envName) {
|
|
39
84
|
try {
|
|
40
|
-
const panoDataDir = process.env.PANOPTICON_DATA_DIR ?? (process.platform === "darwin" ?
|
|
41
|
-
|
|
85
|
+
const panoDataDir = process.env.PANOPTICON_DATA_DIR ?? (process.platform === "darwin" ? path2.join(
|
|
86
|
+
os2.homedir(),
|
|
42
87
|
"Library",
|
|
43
88
|
"Application Support",
|
|
44
89
|
"panopticon"
|
|
45
|
-
) : process.platform === "win32" ?
|
|
46
|
-
process.env.APPDATA ??
|
|
90
|
+
) : process.platform === "win32" ? path2.join(
|
|
91
|
+
process.env.APPDATA ?? path2.join(os2.homedir(), "AppData", "Roaming"),
|
|
47
92
|
"panopticon"
|
|
48
|
-
) :
|
|
49
|
-
const raw =
|
|
93
|
+
) : path2.join(os2.homedir(), ".local", "share", "panopticon"));
|
|
94
|
+
const raw = fs2.readFileSync(path2.join(panoDataDir, "config.json"), "utf-8");
|
|
50
95
|
const panoConf = JSON.parse(raw);
|
|
51
96
|
const target = panoConf.sync?.targets?.find((t) => t.name === envName);
|
|
52
97
|
if (target) {
|
|
@@ -61,7 +106,7 @@ function getActiveEnv() {
|
|
|
61
106
|
return { name, convexUrl: resolveEnvConvexUrl(name) };
|
|
62
107
|
}
|
|
63
108
|
function authStorePathFor(envName) {
|
|
64
|
-
return
|
|
109
|
+
return path2.join(FML_DATA_DIR, `auth.${envName}.json`);
|
|
65
110
|
}
|
|
66
111
|
var ENV_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
67
112
|
function isValidEnvName(envName) {
|
|
@@ -124,14 +169,14 @@ function storePathFor(envName) {
|
|
|
124
169
|
return envName ? authStorePathFor(envName) : authStorePath();
|
|
125
170
|
}
|
|
126
171
|
function ensureDir(envName) {
|
|
127
|
-
const dir =
|
|
128
|
-
if (!
|
|
129
|
-
|
|
172
|
+
const dir = path3.dirname(storePathFor(envName));
|
|
173
|
+
if (!fs3.existsSync(dir)) {
|
|
174
|
+
fs3.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
130
175
|
}
|
|
131
176
|
}
|
|
132
177
|
function readTokens(envName) {
|
|
133
178
|
try {
|
|
134
|
-
const data =
|
|
179
|
+
const data = fs3.readFileSync(storePathFor(envName), "utf-8");
|
|
135
180
|
return JSON.parse(data);
|
|
136
181
|
} catch {
|
|
137
182
|
return null;
|
|
@@ -139,7 +184,7 @@ function readTokens(envName) {
|
|
|
139
184
|
}
|
|
140
185
|
function writeTokens(auth, envName) {
|
|
141
186
|
ensureDir(envName);
|
|
142
|
-
|
|
187
|
+
fs3.writeFileSync(storePathFor(envName), JSON.stringify(auth, null, 2), {
|
|
143
188
|
mode: 384
|
|
144
189
|
});
|
|
145
190
|
}
|
|
@@ -156,7 +201,7 @@ var serviceTokenCaches = /* @__PURE__ */ new Map();
|
|
|
156
201
|
var serviceRefreshPromises = /* @__PURE__ */ new Map();
|
|
157
202
|
var oauthRefreshPromises = /* @__PURE__ */ new Map();
|
|
158
203
|
var cacheKey = (envName) => envName ?? ACTIVE_KEY;
|
|
159
|
-
var SERVICE_ACCESS_TOKEN_PATH =
|
|
204
|
+
var SERVICE_ACCESS_TOKEN_PATH = path3.join(FML_DATA_DIR, "access_token");
|
|
160
205
|
function getSiteUrl2(envName) {
|
|
161
206
|
const explicit = process.env.CONVEX_SITE_URL;
|
|
162
207
|
if (explicit) return explicit.replace(/\/$/, "");
|
|
@@ -193,11 +238,11 @@ async function refreshServiceToken(refreshToken2, envName) {
|
|
|
193
238
|
expiresAt: data.expiresAt
|
|
194
239
|
});
|
|
195
240
|
try {
|
|
196
|
-
const dir =
|
|
197
|
-
if (!
|
|
198
|
-
|
|
241
|
+
const dir = path3.dirname(SERVICE_ACCESS_TOKEN_PATH);
|
|
242
|
+
if (!fs3.existsSync(dir)) {
|
|
243
|
+
fs3.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
199
244
|
}
|
|
200
|
-
|
|
245
|
+
fs3.writeFileSync(SERVICE_ACCESS_TOKEN_PATH, data.accessToken, {
|
|
201
246
|
mode: 384
|
|
202
247
|
});
|
|
203
248
|
} catch {
|
|
@@ -228,7 +273,13 @@ async function getValidToken(opts) {
|
|
|
228
273
|
serviceRefreshPromises.delete(key);
|
|
229
274
|
}
|
|
230
275
|
}
|
|
231
|
-
if (envToken) return envToken;
|
|
276
|
+
if (envToken?.startsWith("fml_st_")) return envToken;
|
|
277
|
+
if (envToken) {
|
|
278
|
+
console.error(
|
|
279
|
+
"[fml] Auth: unsupported FML_TOKEN format. Expected fml_srt_* or fml_st_*."
|
|
280
|
+
);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
232
283
|
const stored = readTokens(envName);
|
|
233
284
|
if (!stored) {
|
|
234
285
|
console.error(
|
|
@@ -332,9 +383,12 @@ async function refreshToken(stored, envName) {
|
|
|
332
383
|
export {
|
|
333
384
|
initSentry,
|
|
334
385
|
Sentry,
|
|
386
|
+
FML_DATA_DIR,
|
|
387
|
+
FML_LOG_DIR,
|
|
335
388
|
DEFAULT_SYNC_URL,
|
|
336
389
|
DEFAULT_TARGET_NAME,
|
|
337
390
|
writeEnvConfig,
|
|
391
|
+
envConfigExists,
|
|
338
392
|
getActiveEnv,
|
|
339
393
|
isValidEnvName,
|
|
340
394
|
CONVEX_URL,
|
|
@@ -348,4 +402,4 @@ export {
|
|
|
348
402
|
setSelectedOrg,
|
|
349
403
|
getValidToken
|
|
350
404
|
};
|
|
351
|
-
//# sourceMappingURL=chunk-
|
|
405
|
+
//# sourceMappingURL=chunk-5BHBYQ2F.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth/token-store.ts","../src/config.ts","../src/dirs.ts","../src/sentry.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport {\n CONVEX_URL,\n WORKOS_API_URL,\n authStorePath,\n authStorePathFor,\n resolveEnvConvexUrl,\n} from \"../config.js\";\nimport { FML_DATA_DIR } from \"../dirs.js\";\nimport { Sentry } from \"../sentry.js\";\n\ninterface StoredAuth {\n accessToken: string;\n refreshToken: string;\n expiresAt: number; // Unix timestamp in ms\n user: {\n id: string;\n email: string;\n name: string;\n };\n orgSlug?: string;\n /** WorkOS client ID used to obtain these tokens (needed for refresh) */\n workosClientId?: string;\n /** Token type: \"oauth\" (default) for WorkOS JWT, \"service\" for device flow tokens */\n tokenType?: \"oauth\" | \"service\";\n}\n\nfunction storePathFor(envName?: string): string {\n return envName ? authStorePathFor(envName) : authStorePath();\n}\n\nfunction ensureDir(envName?: string): void {\n const dir = path.dirname(storePathFor(envName));\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n}\n\nexport function readTokens(envName?: string): StoredAuth | null {\n try {\n const data = fs.readFileSync(storePathFor(envName), \"utf-8\");\n return JSON.parse(data) as StoredAuth;\n } catch {\n return null;\n }\n}\n\nexport function writeTokens(auth: StoredAuth, envName?: string): void {\n ensureDir(envName);\n fs.writeFileSync(storePathFor(envName), JSON.stringify(auth, null, 2), {\n mode: 0o600,\n });\n}\n\nexport function getSelectedOrg(): string | null {\n return readTokens()?.orgSlug ?? null;\n}\n\nexport function setSelectedOrg(orgSlug: string): void {\n const stored = readTokens();\n if (!stored) return;\n writeTokens({ ...stored, orgSlug });\n}\n\n// ── Service Token Refresh (sandbox fml_srt_* → fml_st_*) ────────────────────\n\ninterface ServiceTokenCache {\n accessToken: string;\n expiresAt: number;\n}\n\n/**\n * Caches and refresh-deduplication promises are keyed by env so in-process\n * callers that read multiple envs (`getValidToken({ env: \"a\" })` followed by\n * `getValidToken({ env: \"b\" })`) don't cross-contaminate. The sentinel\n * \"__active__\" stands in for calls with no explicit env.\n */\nconst ACTIVE_KEY = \"__active__\";\nconst serviceTokenCaches = new Map<string, ServiceTokenCache>();\nconst serviceRefreshPromises = new Map<string, Promise<string | null>>();\nconst oauthRefreshPromises = new Map<string, Promise<string | null>>();\nconst cacheKey = (envName?: string): string => envName ?? ACTIVE_KEY;\n\n/** File where the current access token is written for panopticon's tokenCommand */\nconst SERVICE_ACCESS_TOKEN_PATH = path.join(FML_DATA_DIR, \"access_token\");\n\nfunction getSiteUrl(envName?: string): string {\n const explicit = process.env.CONVEX_SITE_URL;\n if (explicit) return explicit.replace(/\\/$/, \"\");\n if (envName) {\n const envConvexUrl = resolveEnvConvexUrl(envName);\n if (envConvexUrl) {\n return envConvexUrl.replace(\".convex.cloud\", \".convex.site\");\n }\n }\n return CONVEX_URL.replace(\".convex.cloud\", \".convex.site\");\n}\n\nasync function refreshServiceToken(\n refreshToken: string,\n envName?: string,\n): Promise<string | null> {\n try {\n const response = await fetch(`${getSiteUrl(envName)}/api/tokens/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${refreshToken}`,\n },\n });\n\n if (!response.ok) {\n console.error(\n `[fml] Service token refresh failed: HTTP ${response.status}`,\n );\n return null;\n }\n\n const data = (await response.json()) as {\n ok: boolean;\n accessToken: string;\n expiresAt: number;\n error?: string;\n };\n\n if (!data.ok || !data.accessToken) {\n console.error(`[fml] Service token refresh error: ${data.error}`);\n return null;\n }\n\n serviceTokenCaches.set(cacheKey(envName), {\n accessToken: data.accessToken,\n expiresAt: data.expiresAt,\n });\n\n // Write to file for panopticon's tokenCommand\n try {\n const dir = path.dirname(SERVICE_ACCESS_TOKEN_PATH);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n fs.writeFileSync(SERVICE_ACCESS_TOKEN_PATH, data.accessToken, {\n mode: 0o600,\n });\n } catch {\n // Non-fatal — in-memory cache still works for the plugin itself\n }\n\n return data.accessToken;\n } catch (err: unknown) {\n Sentry.captureException(err);\n console.error(`[fml] Service token refresh error: ${err}`);\n return null;\n }\n}\n\n// ── Token Resolution ────────────────────────────────────────────────────────\n\n/**\n * Get a valid access token, refreshing if expired.\n *\n * Three paths:\n * 1. FML_TOKEN=fml_srt_* → exchange refresh token for short-lived access token\n * 2. FML_TOKEN=fml_st_* → use directly (legacy/CI)\n * 3. No FML_TOKEN → OAuth flow (interactive user)\n */\nexport async function getValidToken(opts?: {\n env?: string;\n}): Promise<string | null> {\n const envName = opts?.env;\n const key = cacheKey(envName);\n const envToken = process.env.FML_TOKEN;\n\n // Service refresh token → exchange for short-lived access token\n if (envToken?.startsWith(\"fml_srt_\")) {\n const cached = serviceTokenCaches.get(key);\n if (cached && cached.expiresAt > Date.now() + 60_000) {\n return cached.accessToken;\n }\n\n const inflight = serviceRefreshPromises.get(key);\n if (inflight) return inflight;\n const promise = refreshServiceToken(envToken, envName);\n serviceRefreshPromises.set(key, promise);\n try {\n return await promise;\n } finally {\n serviceRefreshPromises.delete(key);\n }\n }\n\n // Static service access token.\n if (envToken?.startsWith(\"fml_st_\")) return envToken;\n\n if (envToken) {\n console.error(\n \"[fml] Auth: unsupported FML_TOKEN format. Expected fml_srt_* or fml_st_*.\",\n );\n return null;\n }\n\n // Stored token path (OAuth or device flow)\n const stored = readTokens(envName);\n if (!stored) {\n console.error(\n envName\n ? `[fml] Auth: no stored tokens for env \"${envName}\"`\n : \"[fml] Auth: no stored tokens\",\n );\n return null;\n }\n\n // If token expires in more than 60 seconds, use it\n if (stored.expiresAt > Date.now() + 60_000) {\n return stored.accessToken;\n }\n\n // Device flow tokens: refresh via /api/tokens/refresh (same as sandbox tokens)\n if (stored.tokenType === \"service\") {\n const inflight = serviceRefreshPromises.get(key);\n if (inflight) return inflight;\n const promise = (async () => {\n const newAccessToken = await refreshServiceToken(\n stored.refreshToken,\n envName,\n );\n const cached = serviceTokenCaches.get(key);\n if (newAccessToken && cached) {\n writeTokens(\n {\n ...stored,\n accessToken: newAccessToken,\n expiresAt: cached.expiresAt,\n },\n envName,\n );\n }\n return newAccessToken;\n })();\n serviceRefreshPromises.set(key, promise);\n try {\n return await promise;\n } finally {\n serviceRefreshPromises.delete(key);\n }\n }\n\n // OAuth tokens: refresh via WorkOS\n const inflight = oauthRefreshPromises.get(key);\n if (inflight) return inflight;\n const promise = refreshToken(stored, envName);\n oauthRefreshPromises.set(key, promise);\n try {\n return await promise;\n } finally {\n oauthRefreshPromises.delete(key);\n }\n}\n\nasync function refreshToken(\n stored: StoredAuth,\n envName?: string,\n): Promise<string | null> {\n if (!stored.workosClientId) {\n console.error(\n \"[fml] Auth: token expired, missing workosClientId — run `fml login`\",\n );\n return null;\n }\n\n try {\n const response = await fetch(\n `${WORKOS_API_URL}/user_management/authenticate`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n grant_type: \"refresh_token\",\n client_id: stored.workosClientId,\n refresh_token: stored.refreshToken,\n }),\n },\n );\n\n if (!response.ok) {\n console.error(\n `[fml] Auth: refresh failed (HTTP ${response.status}) — token preserved`,\n );\n return null;\n }\n\n const data = (await response.json()) as {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n user: {\n id: string;\n email: string;\n first_name?: string;\n last_name?: string;\n };\n };\n\n const userName = [data.user.first_name, data.user.last_name]\n .filter(Boolean)\n .join(\" \");\n\n const refreshed: StoredAuth = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt: Date.now() + (data.expires_in ?? 300) * 1000,\n user: {\n id: data.user.id,\n email: data.user.email,\n name: userName || stored.user.name,\n },\n orgSlug: stored.orgSlug,\n workosClientId: stored.workosClientId,\n };\n\n writeTokens(refreshed, envName);\n return refreshed.accessToken;\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(\n `[fml] Auth: refresh failed (network error: ${msg}) — token preserved`,\n );\n Sentry.captureException(err);\n return null;\n }\n}\n","import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { FML_DATA_DIR } from \"./dirs.js\";\n\n// ── Default production deployment ───────────────────────────────────────────\n\nexport const DEFAULT_PROD_URL =\n \"https://trustworthy-chihuahua-382.convex.cloud\";\n\n/** Convex site URL for sync/HTTP actions — derived from DEFAULT_PROD_URL. */\nexport const DEFAULT_SYNC_URL = DEFAULT_PROD_URL.replace(\n \".convex.cloud\",\n \".convex.site\",\n);\n\n/** Default sync target name created by `fml install` */\nexport const DEFAULT_TARGET_NAME = \"fml\";\n\n// ── Persistent env selection ────────────────────────────────────────────────\n\ninterface EnvConfig {\n /** Name of the active sync target */\n active: string;\n}\n\nconst ENV_CONFIG_PATH = path.join(FML_DATA_DIR, \"env.json\");\n\nfunction readEnvConfig(): EnvConfig {\n try {\n const raw = fs.readFileSync(ENV_CONFIG_PATH, \"utf-8\");\n return JSON.parse(raw) as EnvConfig;\n } catch {\n return { active: DEFAULT_TARGET_NAME };\n }\n}\n\nexport function writeEnvConfig(config: EnvConfig): void {\n const dir = path.dirname(ENV_CONFIG_PATH);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n fs.writeFileSync(ENV_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\\n`, {\n mode: 0o600,\n });\n}\n\n/** Whether an env selection has already been persisted (not a fresh install). */\nexport function envConfigExists(): boolean {\n return fs.existsSync(ENV_CONFIG_PATH);\n}\n\n/**\n * Look up a panopticon sync target by name and return its Convex deployment\n * URL (`.convex.cloud`). Returns null if panopticon isn't installed or the\n * target isn't configured.\n */\nexport function resolveEnvConvexUrl(envName: string): string | null {\n try {\n const panoDataDir =\n process.env.PANOPTICON_DATA_DIR ??\n (process.platform === \"darwin\"\n ? path.join(\n os.homedir(),\n \"Library\",\n \"Application Support\",\n \"panopticon\",\n )\n : process.platform === \"win32\"\n ? path.join(\n process.env.APPDATA ??\n path.join(os.homedir(), \"AppData\", \"Roaming\"),\n \"panopticon\",\n )\n : path.join(os.homedir(), \".local\", \"share\", \"panopticon\"));\n const raw = fs.readFileSync(path.join(panoDataDir, \"config.json\"), \"utf-8\");\n const panoConf = JSON.parse(raw) as {\n sync?: { targets?: Array<{ name: string; url: string }> };\n };\n const target = panoConf.sync?.targets?.find((t) => t.name === envName);\n if (target) {\n return target.url.replace(\".convex.site\", \".convex.cloud\");\n }\n } catch {\n // Panopticon config not available\n }\n return null;\n}\n\n/**\n * Resolve the active environment by looking up the sync target.\n * Derives the .convex.cloud URL from the target's .convex.site URL.\n * Returns null convexUrl if the target is not found (panopticon not installed).\n */\nexport function getActiveEnv(): { name: string; convexUrl: string | null } {\n const name = readEnvConfig().active;\n return { name, convexUrl: resolveEnvConvexUrl(name) };\n}\n\n/** Auth store path for a specific env name (tokens are segregated per env). */\nexport function authStorePathFor(envName: string): string {\n return path.join(FML_DATA_DIR, `auth.${envName}.json`);\n}\n\n/**\n * Env names flow into `tokenCommand` strings that panopticon shells out, so\n * refuse anything that could carry shell metacharacters. Target names are\n * also written to panopticon's config by this rule (see `fml env use`).\n */\nconst ENV_NAME_RE = /^[A-Za-z0-9_-]+$/;\nexport function isValidEnvName(envName: string): boolean {\n return ENV_NAME_RE.test(envName);\n}\n\n/**\n * Require a resolved Convex URL. Exits with an error if the sync target\n * is not configured (panopticon not installed).\n */\nexport function requireConvexUrl(): string {\n const { name, convexUrl } = getActiveEnv();\n if (!convexUrl) {\n console.error(\n `Sync target \"${name}\" not found. Run \\`fml install\\` or \\`fml sync setup\\`.`,\n );\n process.exit(1);\n }\n return convexUrl;\n}\n\n// ── Exports ─────────────────────────────────────────────────────────────────\n\n/**\n * Convex deployment URL (switches with `fml env`).\n *\n * Resolution order:\n * 1. `FML_CONVEX_URL` env var (dev/preview overrides)\n * 2. Active panopticon sync target\n * 3. `DEFAULT_PROD_URL` — lets a fresh `npm install -g` log in against\n * prod before `fml install` has had a chance to seed a sync target.\n */\nexport const CONVEX_URL: string = (\n process.env.FML_CONVEX_URL ??\n getActiveEnv().convexUrl ??\n DEFAULT_PROD_URL\n).replace(/\\/$/, \"\");\n\n/** WorkOS API base URL */\nexport const WORKOS_API_URL = \"https://api.workos.com\";\n\n/** WorkOS authorization base URL */\nexport const WORKOS_AUTH_URL = \"https://auth.fml.inc\";\n\n/**\n * Path to the auth token store for the *currently* active env. Resolved\n * lazily so long-running processes that outlive an `fml env use` switch\n * still read the correct file on the next call.\n */\nexport function authStorePath(): string {\n return authStorePathFor(getActiveEnv().name);\n}\n\n/** Convex site URL (HTTP actions) — derived from CONVEX_URL */\nexport function getSiteUrl(): string {\n return CONVEX_URL.replace(\".convex.cloud\", \".convex.site\").replace(\n /:\\d+$/,\n \"\",\n );\n}\n","/**\n * Platform-native directories for FML runtime data.\n *\n * macOS: ~/Library/Application Support/fml + ~/Library/Logs/fml\n * Linux: ~/.local/share/fml + ~/.local/state/fml/log\n * Windows: %APPDATA%/fml + %APPDATA%/fml/logs\n */\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nconst home = os.homedir();\n\nfunction dataDir(): string {\n // Override (tests, eval replays, sandboxes) so they never touch a real\n // user's auth/config in the platform-default location.\n if (process.env.FML_DATA_DIR) return process.env.FML_DATA_DIR;\n switch (process.platform) {\n case \"darwin\":\n return path.join(home, \"Library\", \"Application Support\", \"fml\");\n case \"win32\":\n return path.join(\n process.env.APPDATA ?? path.join(home, \"AppData\", \"Roaming\"),\n \"fml\",\n );\n default:\n return path.join(\n process.env.XDG_DATA_HOME ?? path.join(home, \".local\", \"share\"),\n \"fml\",\n );\n }\n}\n\nfunction logDir(): string {\n if (process.env.FML_LOG_DIR) return process.env.FML_LOG_DIR;\n switch (process.platform) {\n case \"darwin\":\n return path.join(home, \"Library\", \"Logs\", \"fml\");\n case \"win32\":\n return path.join(\n process.env.APPDATA ?? path.join(home, \"AppData\", \"Roaming\"),\n \"fml\",\n \"logs\",\n );\n default:\n return path.join(\n process.env.XDG_STATE_HOME ?? path.join(home, \".local\", \"state\"),\n \"fml\",\n \"log\",\n );\n }\n}\n\n/** Runtime data — auth tokens, env config, PID files */\nexport const FML_DATA_DIR = dataDir();\n\n/** Log files — daemon logs, MCP logs */\nexport const FML_LOG_DIR = logDir();\n\n/** Ensure both directories exist (called once at startup). */\nexport function ensureDirs(): void {\n fs.mkdirSync(FML_DATA_DIR, { recursive: true, mode: 0o700 });\n fs.mkdirSync(FML_LOG_DIR, { recursive: true, mode: 0o700 });\n}\n","declare const __SENTRY_DSN__: string;\n\nlet initialized = false;\nlet sentry: typeof import(\"@sentry/core\") | null = null;\n\nasync function loadSentry(): Promise<typeof import(\"@sentry/core\") | null> {\n if (sentry) return sentry;\n try {\n sentry = await import(\"@sentry/core\");\n return sentry;\n } catch {\n return null;\n }\n}\n\nexport async function initSentry(): Promise<void> {\n if (initialized) return;\n initialized = true;\n\n const mod = await loadSentry();\n if (!mod) return;\n\n const client = new mod.ServerRuntimeClient({\n dsn: __SENTRY_DSN__,\n tracesSampleRate: 0,\n integrations: [],\n stackParser: mod.createStackParser(mod.nodeStackLineParser()),\n transport: (options) =>\n mod.createTransport(options, async (request) => {\n const res = await fetch(options.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-sentry-envelope\" },\n body: request.body as BodyInit,\n });\n return { statusCode: res.status };\n }),\n });\n\n mod.setCurrentClient(client);\n client.init();\n}\n\nexport const Sentry = {\n captureException: (err: unknown) => sentry?.captureException(err),\n captureMessage: (msg: string) => sentry?.captureMessage(msg),\n} as const;\n"],"mappings":"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","names":["fs","path","fs","os","path","path","fs","os","path","fs","getSiteUrl","refreshToken","inflight","promise"]}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONVEX_URL,
|
|
3
|
+
getSelectedOrg,
|
|
4
|
+
getValidToken
|
|
5
|
+
} from "./chunk-5BHBYQ2F.js";
|
|
6
|
+
|
|
7
|
+
// src/fml-client.ts
|
|
8
|
+
import { resolveRepoFromCwd } from "@fml-inc/panopticon/repo";
|
|
9
|
+
function ref(path) {
|
|
10
|
+
return path;
|
|
11
|
+
}
|
|
12
|
+
var LOGIN_EXPIRED_MESSAGE = "Authentication expired. Run `fml login` to sign in again, then restart Claude Code.";
|
|
13
|
+
function isAuthError(data) {
|
|
14
|
+
return data.code === "UNAUTHENTICATED" || data.code === "TOKEN_EXPIRED" || data.error?.includes("Unauthorized") === true || data.error?.includes("not authenticated") === true;
|
|
15
|
+
}
|
|
16
|
+
function normalizeToolResult(data) {
|
|
17
|
+
if (!data.ok && isAuthError(data)) {
|
|
18
|
+
return { ok: false, error: LOGIN_EXPIRED_MESSAGE, code: data.code };
|
|
19
|
+
}
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
function createFmlClient(token) {
|
|
23
|
+
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
|
+
function getSiteUrl() {
|
|
36
|
+
return CONVEX_URL.replace(".convex.cloud", ".convex.site").replace(
|
|
37
|
+
/\/$/,
|
|
38
|
+
""
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
async function fetchContext(pathAndQuery, init) {
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(`${getSiteUrl()}${pathAndQuery}`, {
|
|
44
|
+
...init,
|
|
45
|
+
headers: {
|
|
46
|
+
Authorization: `Bearer ${token}`,
|
|
47
|
+
...init.body ? { "Content-Type": "application/json" } : {}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const text = await res.text();
|
|
51
|
+
let data;
|
|
52
|
+
try {
|
|
53
|
+
data = JSON.parse(text);
|
|
54
|
+
} catch {
|
|
55
|
+
return { ok: false, error: `HTTP ${res.status}: ${text.slice(0, 200)}` };
|
|
56
|
+
}
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const envelope = data;
|
|
59
|
+
return normalizeToolResult({
|
|
60
|
+
ok: false,
|
|
61
|
+
error: envelope.error ?? `HTTP ${res.status}`,
|
|
62
|
+
code: envelope.code
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, result: data };
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
// ── Orgs ──────────────────────────────────────────────────────────────
|
|
72
|
+
async queryOrgs() {
|
|
73
|
+
if (isServiceToken) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const client = await getClient();
|
|
77
|
+
return await client.query(ref("user/plugin:getMyOrgsAndRepos"), {});
|
|
78
|
+
},
|
|
79
|
+
// ── Tool gateway ─────────────────────────────────────────────────────
|
|
80
|
+
async callBackend(toolName, args, opts) {
|
|
81
|
+
try {
|
|
82
|
+
const body = { toolName, args };
|
|
83
|
+
const org = opts?.org ?? getSelectedOrg();
|
|
84
|
+
if (org) body.org = org;
|
|
85
|
+
const repo = resolveRepoFromCwd(process.cwd());
|
|
86
|
+
if (repo) body.repo = repo.repo;
|
|
87
|
+
if (isServiceToken) {
|
|
88
|
+
const { readTokens } = await import("./token-store-T2RNLXRM.js");
|
|
89
|
+
const userExternalId = process.env.FML_USER_EXTERNAL_ID ?? readTokens()?.user.id;
|
|
90
|
+
if (userExternalId) body.userExternalId = userExternalId;
|
|
91
|
+
}
|
|
92
|
+
const res = await fetch(`${getSiteUrl()}/api/tools/execute`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: {
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
Authorization: `Bearer ${token}`
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify(body)
|
|
99
|
+
});
|
|
100
|
+
const text = await res.text();
|
|
101
|
+
let data;
|
|
102
|
+
try {
|
|
103
|
+
data = JSON.parse(text);
|
|
104
|
+
} catch {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: `HTTP ${res.status}: ${text.slice(0, 200)}`
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (!res.ok && !data.error) {
|
|
111
|
+
return { ok: false, error: `HTTP ${res.status}` };
|
|
112
|
+
}
|
|
113
|
+
return normalizeToolResult(data);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
+
if (msg.includes("Unauthorized") || msg.includes("not authenticated")) {
|
|
117
|
+
return { ok: false, error: LOGIN_EXPIRED_MESSAGE };
|
|
118
|
+
}
|
|
119
|
+
return { ok: false, error: msg };
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
// ── Tool catalog ─────────────────────────────────────────────────────
|
|
123
|
+
async listTools(pluginVersion) {
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(`${getSiteUrl()}/api/tools/list`);
|
|
126
|
+
if (pluginVersion) url.searchParams.set("pluginVersion", pluginVersion);
|
|
127
|
+
const res = await fetch(url.toString(), {
|
|
128
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
129
|
+
});
|
|
130
|
+
const text = await res.text();
|
|
131
|
+
let data;
|
|
132
|
+
try {
|
|
133
|
+
data = JSON.parse(text);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
136
|
+
}
|
|
137
|
+
if (!data.ok || !data.descriptors) {
|
|
138
|
+
if (isAuthError(data)) {
|
|
139
|
+
throw new Error(LOGIN_EXPIRED_MESSAGE);
|
|
140
|
+
}
|
|
141
|
+
throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
142
|
+
}
|
|
143
|
+
return data.descriptors;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
146
|
+
if (msg.includes("Unauthorized") || msg.includes("not authenticated")) {
|
|
147
|
+
throw new Error(LOGIN_EXPIRED_MESSAGE);
|
|
148
|
+
}
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
// ── Anamnesis ground-truth context ───────────────────────────────────
|
|
153
|
+
/**
|
|
154
|
+
* Call a read-only anamnesis context endpoint
|
|
155
|
+
* (/v1/anamnesis/context/{path,commit,pr}). Sends the current token
|
|
156
|
+
* (service token or user JWT) — the endpoint accepts either.
|
|
157
|
+
*/
|
|
158
|
+
async anamnesisContext(kind, params) {
|
|
159
|
+
const qs = new URLSearchParams();
|
|
160
|
+
for (const [k, v] of Object.entries(params)) {
|
|
161
|
+
if (v != null) qs.set(k, String(v));
|
|
162
|
+
}
|
|
163
|
+
return fetchContext(`/v1/anamnesis/context/${kind}?${qs.toString()}`, {
|
|
164
|
+
method: "GET"
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
/** POST /v1/anamnesis/context/query — generic predicate query. */
|
|
168
|
+
async anamnesisQuery(body) {
|
|
169
|
+
return fetchContext("/v1/anamnesis/context/query", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
body: JSON.stringify(body)
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
// ── Repo resolution ──────────────────────────────────────────────────
|
|
175
|
+
async resolveRepo(orgSlug, repoFullName) {
|
|
176
|
+
const client = await getClient();
|
|
177
|
+
return await client.query(ref("user/cli:resolveRepo"), {
|
|
178
|
+
orgSlug,
|
|
179
|
+
repoFullName
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
// ── Config snapshots ─────────────────────────────────────────────────
|
|
183
|
+
async listUserConfigSnapshots(orgSlug) {
|
|
184
|
+
const client = await getClient();
|
|
185
|
+
const result = await client.query(
|
|
186
|
+
ref("user/config_snapshots:listUserSnapshots"),
|
|
187
|
+
{ orgSlug }
|
|
188
|
+
);
|
|
189
|
+
return result ?? [];
|
|
190
|
+
},
|
|
191
|
+
async getUserConfigDetail(orgSlug, githubUsername) {
|
|
192
|
+
const client = await getClient();
|
|
193
|
+
return await client.query(ref("user/config_snapshots:getUserDetail"), {
|
|
194
|
+
orgSlug,
|
|
195
|
+
githubUsername
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
async listRepoConfigSnapshots(orgSlug, repository) {
|
|
199
|
+
const client = await getClient();
|
|
200
|
+
const result = await client.query(
|
|
201
|
+
ref("user/config_snapshots:listRepoSnapshots"),
|
|
202
|
+
{ orgSlug, repository }
|
|
203
|
+
);
|
|
204
|
+
return result ?? [];
|
|
205
|
+
},
|
|
206
|
+
async getRepoConfigDetail(orgSlug, repository) {
|
|
207
|
+
const client = await getClient();
|
|
208
|
+
return await client.query(ref("user/config_snapshots:getRepoDetail"), {
|
|
209
|
+
orgSlug,
|
|
210
|
+
repository
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async function getAuthenticatedClient() {
|
|
216
|
+
const token = await getValidToken();
|
|
217
|
+
if (!token) return null;
|
|
218
|
+
return createFmlClient(token);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export {
|
|
222
|
+
createFmlClient,
|
|
223
|
+
getAuthenticatedClient
|
|
224
|
+
};
|
|
225
|
+
//# sourceMappingURL=chunk-H2VOISTZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fml-client.ts"],"sourcesContent":["import type { FunctionReference } from \"convex/server\";\nimport { 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\nfunction ref<T extends \"query\" | \"mutation\" | \"action\">(\n path: string,\n): FunctionReference<T> {\n return path as unknown as FunctionReference<T>;\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\n// ── API client factory ──────────────────────────────────────────────────────\n\nexport function createFmlClient(token: string) {\n const isServiceToken = token.startsWith(\"fml_st_\");\n\n let clientPromise: Promise<{\n query: <T>(\n ref: FunctionReference<\"query\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n mutation: <T>(\n ref: FunctionReference<\"mutation\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n action: <T>(\n ref: FunctionReference<\"action\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n }> | null = null;\n\n function getClient() {\n if (!clientPromise) {\n clientPromise = import(\"convex/browser\").then(({ ConvexHttpClient }) => {\n const client = new ConvexHttpClient(CONVEX_URL);\n client.setAuth(token);\n return client;\n });\n }\n return clientPromise;\n }\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 if (isServiceToken) {\n // Service tokens can't call Convex actions directly — org is embedded in the token\n return [];\n }\n const client = await getClient();\n return await client.query(ref(\"user/plugin:getMyOrgsAndRepos\"), {});\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 client = await getClient();\n return await client.query(ref(\"user/cli:resolveRepo\"), {\n orgSlug,\n repoFullName,\n });\n },\n\n // ── Config snapshots ─────────────────────────────────────────────────\n\n async listUserConfigSnapshots(\n orgSlug: string,\n ): Promise<UserConfigSnapshotSummary[]> {\n const client = await getClient();\n const result = await client.query<UserConfigSnapshotSummary[] | null>(\n ref(\"user/config_snapshots:listUserSnapshots\"),\n { orgSlug },\n );\n return result ?? [];\n },\n\n async getUserConfigDetail(\n orgSlug: string,\n githubUsername: string,\n ): Promise<UserConfigSnapshotDetail | null> {\n const client = await getClient();\n return await client.query(ref(\"user/config_snapshots:getUserDetail\"), {\n orgSlug,\n githubUsername,\n });\n },\n\n async listRepoConfigSnapshots(\n orgSlug: string,\n repository?: string,\n ): Promise<RepoConfigSnapshotSummary[]> {\n const client = await getClient();\n const result = await client.query<RepoConfigSnapshotSummary[] | null>(\n ref(\"user/config_snapshots:listRepoSnapshots\"),\n { orgSlug, repository },\n );\n return result ?? [];\n },\n\n async getRepoConfigDetail(\n orgSlug: string,\n repository: string,\n ): Promise<RepoConfigSnapshotDetail | null> {\n const client = await getClient();\n return await client.query(ref(\"user/config_snapshots:getRepoDetail\"), {\n orgSlug,\n repository,\n });\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":[]}
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FML_DATA_DIR,
|
|
3
|
-
FML_LOG_DIR
|
|
4
|
-
} from "./chunk-4YEAQJPF.js";
|
|
5
|
-
|
|
6
|
-
// src/daemon-utils.ts
|
|
7
|
-
import fs2 from "fs";
|
|
8
|
-
import path2 from "path";
|
|
9
|
-
|
|
10
1
|
// src/bin-utils.ts
|
|
11
2
|
import fs from "fs";
|
|
12
3
|
import path from "path";
|
|
@@ -48,18 +39,6 @@ function execBinSync(bin, args, opts = {}) {
|
|
|
48
39
|
}
|
|
49
40
|
|
|
50
41
|
// src/daemon-utils.ts
|
|
51
|
-
var SYNC_PID_FILE = path2.join(FML_DATA_DIR, "sync.pid");
|
|
52
|
-
var SYNC_LOG_FILE = path2.join(FML_LOG_DIR, "sync.log");
|
|
53
|
-
function isDaemonRunning(pidFile) {
|
|
54
|
-
if (!fs2.existsSync(pidFile)) return { running: false, pid: null };
|
|
55
|
-
const pid = parseInt(fs2.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
56
|
-
try {
|
|
57
|
-
process.kill(pid, 0);
|
|
58
|
-
return { running: true, pid };
|
|
59
|
-
} catch {
|
|
60
|
-
return { running: false, pid };
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
42
|
function resolvePanopticonBin() {
|
|
64
43
|
return resolveBin("panopticon");
|
|
65
44
|
}
|
|
@@ -84,10 +63,7 @@ function panopticonExec(...argsAndOpts) {
|
|
|
84
63
|
export {
|
|
85
64
|
resolveBin,
|
|
86
65
|
execBinSync,
|
|
87
|
-
SYNC_PID_FILE,
|
|
88
|
-
SYNC_LOG_FILE,
|
|
89
|
-
isDaemonRunning,
|
|
90
66
|
resolvePanopticonBin,
|
|
91
67
|
panopticonExec
|
|
92
68
|
};
|
|
93
|
-
//# sourceMappingURL=chunk-
|
|
69
|
+
//# sourceMappingURL=chunk-YH5ROR7G.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bin-utils.ts","../src/daemon-utils.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { execFileSync, execSync } from \"node:child_process\";\n\nconst IS_WIN = process.platform === \"win32\";\nconst DEFAULT_PATHEXT = \".COM;.EXE;.BAT;.CMD\";\n\n/**\n * Resolve a CLI binary by walking process.env.PATH. On Windows, probe each\n * process.env.PATHEXT extension. Returns an absolute path or null.\n *\n * Pass the bare command name (e.g. \"npm\", not \"npm.cmd\"); the extension is\n * applied via PATHEXT on Windows. If the caller does include an extension,\n * it's used as-is.\n *\n * Replaces `which`/`where`: avoids spawning a child process and works the\n * same on macOS, Linux, and Windows.\n */\nexport function resolveBin(name: string): string | null {\n const pathDirs = (process.env.PATH ?? \"\").split(path.delimiter).filter(Boolean);\n const hasExt = path.extname(name) !== \"\";\n const exts = !IS_WIN || hasExt\n ? [\"\"]\n : (process.env.PATHEXT ?? DEFAULT_PATHEXT).split(\";\").filter(Boolean);\n\n for (const dir of pathDirs) {\n for (const ext of exts) {\n const candidate = path.join(dir, name + ext);\n try {\n if (fs.statSync(candidate).isFile()) return candidate;\n } catch {}\n }\n }\n return null;\n}\n\n/** True for Windows shell shims that execFile cannot run directly. */\nfunction isWinShim(bin: string): boolean {\n return IS_WIN && /\\.(cmd|bat)$/i.test(bin);\n}\n\n/**\n * Quote a single argument for safe inclusion in a Windows command line that\n * will be parsed by cmd.exe and then re-parsed by a CRT program.\n *\n * Two layers:\n * 1. CRT (CommandLineToArgvW): wrap in `\"`, escape internal `\"` as `\\\"`,\n * double any backslashes that precede a quote.\n * 2. cmd.exe: escape its metacharacters with `^` so they aren't expanded\n * before the CRT layer sees them. `%` and `!` (delayed expansion) are\n * the dangerous ones inside quotes; the rest are safest belt-and-braces.\n */\nexport function quoteWinArg(arg: string): string {\n if (arg.length === 0) return '\"\"';\n\n // Fast path: bare-word args that need no quoting at all.\n // Backslash is intentionally excluded so `path\\` and similar still go\n // through the trailing-backslash doubling logic below.\n if (/^[A-Za-z0-9@\\-_+=:/.,]+$/.test(arg)) return arg;\n\n // CRT layer: double backslashes before quotes, escape quotes, double trailing backslashes.\n const crt = arg\n .replace(/(\\\\*)\"/g, '$1$1\\\\\"')\n .replace(/(\\\\+)$/, \"$1$1\");\n const quoted = `\"${crt}\"`;\n\n // cmd.exe layer: escape metacharacters (including the surrounding quotes)\n // so cmd's first pass passes them through verbatim.\n return quoted.replace(/([()%!^<>&|\"])/g, \"^$1\");\n}\n\nexport interface ExecBinOptions {\n timeout?: number;\n encoding?: BufferEncoding;\n stdio?: \"pipe\" | \"inherit\" | \"ignore\";\n}\n\n/**\n * Run a resolved binary path with the given args, handling Windows .cmd/.bat\n * shims correctly. Throws on non-zero exit (matching execFileSync).\n */\nexport function execBinSync(\n bin: string,\n args: string[],\n opts: ExecBinOptions = {},\n): string {\n const { timeout = 10_000, encoding = \"utf-8\", stdio = \"pipe\" } = opts;\n\n if (isWinShim(bin)) {\n // CVE-2024-27980: Node refuses to spawn .cmd/.bat without shell: true.\n // Build the command string ourselves so we control the quoting.\n const command = [bin, ...args].map(quoteWinArg).join(\" \");\n return execSync(command, { timeout, encoding, stdio });\n }\n\n return execFileSync(bin, args, { timeout, encoding, stdio });\n}\n","import { execBinSync, resolveBin } from \"./bin-utils.js\";\n\n/** Resolve the panopticon CLI binary from PATH (globally installed). */\nexport function resolvePanopticonBin(): string | null {\n return resolveBin(\"panopticon\");\n}\n\n/** Run a panopticon CLI command and return stdout. */\nexport function panopticonExec(...args: string[]): {\n ok: boolean;\n stdout: string;\n};\nexport function panopticonExec(\n ...argsAndOpts: [...string[], { timeout?: number }]\n): { ok: boolean; stdout: string };\nexport function panopticonExec(\n ...argsAndOpts: Array<string | { timeout?: number }>\n): {\n ok: boolean;\n stdout: string;\n} {\n let timeout = 10_000;\n const args: string[] = [];\n for (const a of argsAndOpts) {\n if (typeof a === \"string\") args.push(a);\n else timeout = a.timeout ?? timeout;\n }\n const bin = resolvePanopticonBin();\n if (!bin) return { ok: false, stdout: \"panopticon binary not found\" };\n try {\n const stdout = execBinSync(bin, args, { timeout });\n return { ok: true, stdout };\n } catch (err: unknown) {\n const e = err as { stdout?: string; stderr?: string; status?: number };\n return { ok: false, stdout: (e.stdout ?? \"\") + (e.stderr ?? \"\") };\n }\n}\n"],"mappings":";AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,cAAc,gBAAgB;AAEvC,IAAM,SAAS,QAAQ,aAAa;AACpC,IAAM,kBAAkB;AAajB,SAAS,WAAW,MAA6B;AACtD,QAAM,YAAY,QAAQ,IAAI,QAAQ,IAAI,MAAM,KAAK,SAAS,EAAE,OAAO,OAAO;AAC9E,QAAM,SAAS,KAAK,QAAQ,IAAI,MAAM;AACtC,QAAM,OAAO,CAAC,UAAU,SACpB,CAAC,EAAE,KACF,QAAQ,IAAI,WAAW,iBAAiB,MAAM,GAAG,EAAE,OAAO,OAAO;AAEtE,aAAW,OAAO,UAAU;AAC1B,eAAW,OAAO,MAAM;AACtB,YAAM,YAAY,KAAK,KAAK,KAAK,OAAO,GAAG;AAC3C,UAAI;AACF,YAAI,GAAG,SAAS,SAAS,EAAE,OAAO,EAAG,QAAO;AAAA,MAC9C,QAAQ;AAAA,MAAC;AAAA,IACX;AAAA,EACF;AACA,SAAO;AACT;AAGA,SAAS,UAAU,KAAsB;AACvC,SAAO,UAAU,gBAAgB,KAAK,GAAG;AAC3C;AAaO,SAAS,YAAY,KAAqB;AAC/C,MAAI,IAAI,WAAW,EAAG,QAAO;AAK7B,MAAI,2BAA2B,KAAK,GAAG,EAAG,QAAO;AAGjD,QAAM,MAAM,IACT,QAAQ,WAAW,SAAS,EAC5B,QAAQ,UAAU,MAAM;AAC3B,QAAM,SAAS,IAAI,GAAG;AAItB,SAAO,OAAO,QAAQ,mBAAmB,KAAK;AAChD;AAYO,SAAS,YACd,KACA,MACA,OAAuB,CAAC,GAChB;AACR,QAAM,EAAE,UAAU,KAAQ,WAAW,SAAS,QAAQ,OAAO,IAAI;AAEjE,MAAI,UAAU,GAAG,GAAG;AAGlB,UAAM,UAAU,CAAC,KAAK,GAAG,IAAI,EAAE,IAAI,WAAW,EAAE,KAAK,GAAG;AACxD,WAAO,SAAS,SAAS,EAAE,SAAS,UAAU,MAAM,CAAC;AAAA,EACvD;AAEA,SAAO,aAAa,KAAK,MAAM,EAAE,SAAS,UAAU,MAAM,CAAC;AAC7D;;;AC7FO,SAAS,uBAAsC;AACpD,SAAO,WAAW,YAAY;AAChC;AAUO,SAAS,kBACX,aAIH;AACA,MAAI,UAAU;AACd,QAAM,OAAiB,CAAC;AACxB,aAAW,KAAK,aAAa;AAC3B,QAAI,OAAO,MAAM,SAAU,MAAK,KAAK,CAAC;AAAA,QACjC,WAAU,EAAE,WAAW;AAAA,EAC9B;AACA,QAAM,MAAM,qBAAqB;AACjC,MAAI,CAAC,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,8BAA8B;AACpE,MAAI;AACF,UAAM,SAAS,YAAY,KAAK,MAAM,EAAE,QAAQ,CAAC;AACjD,WAAO,EAAE,IAAI,MAAM,OAAO;AAAA,EAC5B,SAAS,KAAc;AACrB,UAAM,IAAI;AACV,WAAO,EAAE,IAAI,OAAO,SAAS,EAAE,UAAU,OAAO,EAAE,UAAU,IAAI;AAAA,EAClE;AACF;","names":[]}
|