@fml-inc/fml 0.6.1 → 0.8.0-rc.1
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 +2 -4
- package/README.md +316 -70
- package/agent-assets/fml/SKILL.md +180 -0
- package/bin/fml +4 -18
- package/bin/hook-handler +7 -0
- package/bin/mcp-server +4 -18
- package/bin/observer +6 -0
- package/dist/chunk-7DR5X7XI.js +1522 -0
- package/dist/chunk-CK5MEL5J.js +140 -0
- package/dist/chunk-DEMVOVVY.js +4442 -0
- package/dist/chunk-F66IBM3Z.js +1037 -0
- package/dist/chunk-HUR2HCUG.js +953 -0
- package/dist/chunk-I4GZPTBM.js +109 -0
- package/dist/chunk-JXSSPQ5B.js +3331 -0
- package/dist/{chunk-PZ5AY32C.js → chunk-MLKGABMK.js} +0 -1
- package/dist/{chunk-2H6D7RUN.js → chunk-P3QLARXE.js} +36 -23
- package/dist/chunk-T5YVLSUC.js +138 -0
- package/dist/cli.js +7623 -990
- package/dist/{esm-2EUDFZWP.js → esm-4YKLKFRX.js} +546 -741
- package/dist/hook-handler.js +190 -0
- package/dist/mcp/server.js +112 -98
- package/dist/observer-server.js +1566 -0
- package/dist/sync-cycle.js +31 -0
- package/dist/token-store-R7WXEPLI.js +27 -0
- package/hooks/hooks.json +290 -0
- package/package.json +24 -27
- package/dist/chunk-2H6D7RUN.js.map +0 -1
- package/dist/chunk-PZ5AY32C.js.map +0 -1
- package/dist/chunk-VDW5L7OB.js +0 -405
- package/dist/chunk-VDW5L7OB.js.map +0 -1
- package/dist/chunk-YH5ROR7G.js +0 -69
- package/dist/chunk-YH5ROR7G.js.map +0 -1
- package/dist/chunk-ZFUJ5ZQP.js +0 -41
- package/dist/chunk-ZFUJ5ZQP.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/daemon-J27JBWLV.js +0 -17
- package/dist/daemon-J27JBWLV.js.map +0 -1
- package/dist/daemon-utils-OODBIYB7.js +0 -10
- package/dist/daemon-utils-OODBIYB7.js.map +0 -1
- package/dist/esm-2EUDFZWP.js.map +0 -1
- package/dist/mcp/server.js.map +0 -1
- package/dist/postinstall.js +0 -16
- package/dist/postinstall.js.map +0 -1
- package/dist/token-store-JZJ3FJHA.js +0 -16
- package/dist/token-store-JZJ3FJHA.js.map +0 -1
- package/scripts/postinstall.cjs +0 -25
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveRepoFromCwd
|
|
3
|
+
} from "./chunk-T5YVLSUC.js";
|
|
1
4
|
import {
|
|
2
5
|
CONVEX_URL,
|
|
6
|
+
getActiveEnv,
|
|
3
7
|
getSelectedOrg,
|
|
8
|
+
getSiteUrlForConvexUrl,
|
|
4
9
|
getValidToken
|
|
5
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-7DR5X7XI.js";
|
|
6
11
|
|
|
7
12
|
// src/fml-client.ts
|
|
8
|
-
import { resolveRepoFromCwd } from "@fml-inc/panopticon/repo";
|
|
9
13
|
var LOGIN_EXPIRED_MESSAGE = "Authentication expired. Run `fml login` to sign in again, then restart Claude Code.";
|
|
10
14
|
function isAuthError(data) {
|
|
11
15
|
return data.code === "UNAUTHENTICATED" || data.code === "TOKEN_EXPIRED" || data.error?.includes("Unauthorized") === true || data.error?.includes("not authenticated") === true;
|
|
@@ -22,17 +26,12 @@ function requireToolResult(result) {
|
|
|
22
26
|
}
|
|
23
27
|
return result.result;
|
|
24
28
|
}
|
|
25
|
-
function createFmlClient(token) {
|
|
29
|
+
function createFmlClient(token, options = {}) {
|
|
26
30
|
const isServiceToken = token.startsWith("fml_st_");
|
|
27
|
-
|
|
28
|
-
return CONVEX_URL.replace(".convex.cloud", ".convex.site").replace(
|
|
29
|
-
/\/$/,
|
|
30
|
-
""
|
|
31
|
-
);
|
|
32
|
-
}
|
|
31
|
+
const siteUrl = (options.siteUrl ?? getSiteUrlForConvexUrl(CONVEX_URL)).replace(/\/$/, "");
|
|
33
32
|
async function fetchContext(pathAndQuery, init) {
|
|
34
33
|
try {
|
|
35
|
-
const res = await fetch(`${
|
|
34
|
+
const res = await fetch(`${siteUrl}${pathAndQuery}`, {
|
|
36
35
|
...init,
|
|
37
36
|
headers: {
|
|
38
37
|
Authorization: `Bearer ${token}`,
|
|
@@ -44,7 +43,10 @@ function createFmlClient(token) {
|
|
|
44
43
|
try {
|
|
45
44
|
data = JSON.parse(text);
|
|
46
45
|
} catch {
|
|
47
|
-
return {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
error: `HTTP ${res.status}: ${text.slice(0, 200)}`
|
|
49
|
+
};
|
|
48
50
|
}
|
|
49
51
|
if (!res.ok) {
|
|
50
52
|
const envelope = data;
|
|
@@ -56,7 +58,10 @@ function createFmlClient(token) {
|
|
|
56
58
|
}
|
|
57
59
|
return { ok: true, result: data };
|
|
58
60
|
} catch (err) {
|
|
59
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: err instanceof Error ? err.message : String(err)
|
|
64
|
+
};
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
67
|
return {
|
|
@@ -69,16 +74,18 @@ function createFmlClient(token) {
|
|
|
69
74
|
async callBackend(toolName, args, opts) {
|
|
70
75
|
try {
|
|
71
76
|
const body = { toolName, args };
|
|
72
|
-
const org = opts?.org ?? getSelectedOrg();
|
|
77
|
+
const org = opts?.org ?? getSelectedOrg(options.envName);
|
|
73
78
|
if (org) body.org = org;
|
|
74
79
|
const repo = resolveRepoFromCwd(process.cwd());
|
|
75
80
|
if (repo) body.repo = repo.repo;
|
|
76
81
|
if (isServiceToken) {
|
|
77
|
-
const { readTokens } = await import("./token-store-
|
|
78
|
-
const
|
|
82
|
+
const { readTokens, SERVICE_TOKEN_LOGIN_USER_ID } = await import("./token-store-R7WXEPLI.js");
|
|
83
|
+
const stored = readTokens(options.envName);
|
|
84
|
+
const storedUserExternalId = stored?.tokenType === "service" && stored.user.id !== SERVICE_TOKEN_LOGIN_USER_ID ? stored.user.id : void 0;
|
|
85
|
+
const userExternalId = process.env.FML_USER_EXTERNAL_ID ?? storedUserExternalId;
|
|
79
86
|
if (userExternalId) body.userExternalId = userExternalId;
|
|
80
87
|
}
|
|
81
|
-
const res = await fetch(`${
|
|
88
|
+
const res = await fetch(`${siteUrl}/api/tools/execute`, {
|
|
82
89
|
method: "POST",
|
|
83
90
|
headers: {
|
|
84
91
|
"Content-Type": "application/json",
|
|
@@ -96,8 +103,12 @@ function createFmlClient(token) {
|
|
|
96
103
|
error: `HTTP ${res.status}: ${text.slice(0, 200)}`
|
|
97
104
|
};
|
|
98
105
|
}
|
|
99
|
-
if (!res.ok
|
|
100
|
-
return {
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
return normalizeToolResult({
|
|
108
|
+
ok: false,
|
|
109
|
+
error: data.error ?? `HTTP ${res.status}`,
|
|
110
|
+
code: data.code
|
|
111
|
+
});
|
|
101
112
|
}
|
|
102
113
|
return normalizeToolResult(data);
|
|
103
114
|
} catch (err) {
|
|
@@ -111,7 +122,7 @@ function createFmlClient(token) {
|
|
|
111
122
|
// ── Tool catalog ─────────────────────────────────────────────────────
|
|
112
123
|
async listTools(pluginVersion) {
|
|
113
124
|
try {
|
|
114
|
-
const url = new URL(`${
|
|
125
|
+
const url = new URL(`${siteUrl}/api/tools/list`);
|
|
115
126
|
if (pluginVersion) url.searchParams.set("pluginVersion", pluginVersion);
|
|
116
127
|
const res = await fetch(url.toString(), {
|
|
117
128
|
headers: { Authorization: `Bearer ${token}` }
|
|
@@ -204,14 +215,16 @@ function createFmlClient(token) {
|
|
|
204
215
|
}
|
|
205
216
|
};
|
|
206
217
|
}
|
|
207
|
-
async function getAuthenticatedClient() {
|
|
208
|
-
const token = await getValidToken();
|
|
218
|
+
async function getAuthenticatedClient(environment = getActiveEnv()) {
|
|
219
|
+
const token = await getValidToken({ env: environment.name });
|
|
209
220
|
if (!token) return null;
|
|
210
|
-
return createFmlClient(token
|
|
221
|
+
return createFmlClient(token, {
|
|
222
|
+
envName: environment.name,
|
|
223
|
+
siteUrl: getSiteUrlForConvexUrl(environment.convexUrl)
|
|
224
|
+
});
|
|
211
225
|
}
|
|
212
226
|
|
|
213
227
|
export {
|
|
214
228
|
createFmlClient,
|
|
215
229
|
getAuthenticatedClient
|
|
216
230
|
};
|
|
217
|
-
//# sourceMappingURL=chunk-2H6D7RUN.js.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// src/repo.ts
|
|
2
|
+
import { execFile, execFileSync } from "child_process";
|
|
3
|
+
var REPO_CACHE = /* @__PURE__ */ new Map();
|
|
4
|
+
var ASYNC_REPO_CACHE = /* @__PURE__ */ new Map();
|
|
5
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
6
|
+
var POSITIVE_REPO_CACHE_TTL_MS = 3e4;
|
|
7
|
+
var NEGATIVE_REPO_CACHE_TTL_MS = 1e3;
|
|
8
|
+
var MAX_REPO_CACHE_ENTRIES = 256;
|
|
9
|
+
function cachedRepo(cwd) {
|
|
10
|
+
const entry = REPO_CACHE.get(cwd);
|
|
11
|
+
if (!entry) return { hit: false };
|
|
12
|
+
if (entry.expiresAtMs <= Date.now()) {
|
|
13
|
+
REPO_CACHE.delete(cwd);
|
|
14
|
+
return { hit: false };
|
|
15
|
+
}
|
|
16
|
+
REPO_CACHE.delete(cwd);
|
|
17
|
+
REPO_CACHE.set(cwd, entry);
|
|
18
|
+
return { hit: true, value: entry.value };
|
|
19
|
+
}
|
|
20
|
+
function cacheRepo(cwd, value) {
|
|
21
|
+
REPO_CACHE.delete(cwd);
|
|
22
|
+
while (REPO_CACHE.size >= MAX_REPO_CACHE_ENTRIES) {
|
|
23
|
+
const oldest = REPO_CACHE.keys().next().value;
|
|
24
|
+
if (oldest === void 0) break;
|
|
25
|
+
REPO_CACHE.delete(oldest);
|
|
26
|
+
}
|
|
27
|
+
REPO_CACHE.set(cwd, {
|
|
28
|
+
value,
|
|
29
|
+
expiresAtMs: Date.now() + (value === null ? NEGATIVE_REPO_CACHE_TTL_MS : POSITIVE_REPO_CACHE_TTL_MS)
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function git(cwd, args, timeoutMs) {
|
|
33
|
+
try {
|
|
34
|
+
return execFileSync("git", ["-C", cwd, ...args], {
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
timeout: timeoutMs,
|
|
37
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
38
|
+
windowsHide: true
|
|
39
|
+
}).trim() || null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function gitAsync(cwd, args, signal) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
try {
|
|
47
|
+
execFile(
|
|
48
|
+
"git",
|
|
49
|
+
["-C", cwd, ...args],
|
|
50
|
+
{
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
timeout: GIT_TIMEOUT_MS,
|
|
53
|
+
windowsHide: true,
|
|
54
|
+
...signal ? { signal } : {}
|
|
55
|
+
},
|
|
56
|
+
(error, stdout) => {
|
|
57
|
+
if (signal?.aborted) {
|
|
58
|
+
reject(signal.reason ?? new Error("Repository lookup aborted"));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
resolve(error ? null : stdout.trim() || null);
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (signal?.aborted) {
|
|
66
|
+
reject(signal.reason ?? error);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
resolve(null);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function repositorySlug(remote) {
|
|
74
|
+
const ssh = remote.match(/^[^@]+@[^:]+:([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
75
|
+
if (ssh?.[1]) return ssh[1];
|
|
76
|
+
try {
|
|
77
|
+
const url = new URL(remote);
|
|
78
|
+
const slug = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
79
|
+
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(slug) ? slug : null;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function resolveRepoFromCwd(cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
85
|
+
const cached = cachedRepo(cwd);
|
|
86
|
+
if (cached.hit) return cached.value;
|
|
87
|
+
const remote = git(cwd, ["remote", "get-url", "origin"], timeoutMs);
|
|
88
|
+
const repo = remote ? repositorySlug(remote) : null;
|
|
89
|
+
const result = repo ? {
|
|
90
|
+
repo,
|
|
91
|
+
branch: git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], timeoutMs)
|
|
92
|
+
} : null;
|
|
93
|
+
cacheRepo(cwd, result);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
function resolveRepositorySlugFromCwd(cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
97
|
+
const cached = cachedRepo(cwd);
|
|
98
|
+
if (cached.hit) return cached.value?.repo ?? null;
|
|
99
|
+
const remote = git(cwd, ["remote", "get-url", "origin"], timeoutMs);
|
|
100
|
+
return remote ? repositorySlug(remote) : null;
|
|
101
|
+
}
|
|
102
|
+
async function resolveRepoFromCwdAsync(cwd, signal) {
|
|
103
|
+
if (signal?.aborted) {
|
|
104
|
+
throw signal.reason ?? new Error("Repository lookup aborted");
|
|
105
|
+
}
|
|
106
|
+
const cached = cachedRepo(cwd);
|
|
107
|
+
if (cached.hit) return cached.value;
|
|
108
|
+
const cachedResolution = ASYNC_REPO_CACHE.get(cwd);
|
|
109
|
+
if (cachedResolution) return await cachedResolution;
|
|
110
|
+
const resolution = (async () => {
|
|
111
|
+
const remote = await gitAsync(cwd, ["remote", "get-url", "origin"], signal);
|
|
112
|
+
const repo = remote ? repositorySlug(remote) : null;
|
|
113
|
+
const result = repo ? {
|
|
114
|
+
repo,
|
|
115
|
+
branch: await gitAsync(
|
|
116
|
+
cwd,
|
|
117
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
118
|
+
signal
|
|
119
|
+
)
|
|
120
|
+
} : null;
|
|
121
|
+
cacheRepo(cwd, result);
|
|
122
|
+
return result;
|
|
123
|
+
})();
|
|
124
|
+
ASYNC_REPO_CACHE.set(cwd, resolution);
|
|
125
|
+
try {
|
|
126
|
+
return await resolution;
|
|
127
|
+
} finally {
|
|
128
|
+
if (ASYNC_REPO_CACHE.get(cwd) === resolution) {
|
|
129
|
+
ASYNC_REPO_CACHE.delete(cwd);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
resolveRepoFromCwd,
|
|
136
|
+
resolveRepositorySlugFromCwd,
|
|
137
|
+
resolveRepoFromCwdAsync
|
|
138
|
+
};
|