@fml-inc/fml 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fml",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "FML agent tools for Claude Code",
5
5
  "mcpServers": {
6
6
  "fml": {
@@ -130,6 +130,42 @@ function createApiClient(token) {
130
130
  return { ok: false, error: msg };
131
131
  }
132
132
  },
133
+ // ── Tool catalog ─────────────────────────────────────────────────────
134
+ async listTools(pluginVersion) {
135
+ try {
136
+ if (isServiceToken) {
137
+ const url = new URL(`${getSiteUrl()}/api/tools/list`);
138
+ if (pluginVersion) url.searchParams.set("pluginVersion", pluginVersion);
139
+ const res = await fetch(url.toString(), {
140
+ headers: { Authorization: `Bearer ${token}` }
141
+ });
142
+ const text = await res.text();
143
+ let data;
144
+ try {
145
+ data = JSON.parse(text);
146
+ } catch {
147
+ throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
148
+ }
149
+ if (!data.ok || !data.descriptors) {
150
+ throw new Error(data.error ?? `HTTP ${res.status}`);
151
+ }
152
+ return data.descriptors;
153
+ }
154
+ const client = await getClient();
155
+ return await client.query(
156
+ ref("user/plugin_tools:listTools"),
157
+ pluginVersion ? { pluginVersion } : {}
158
+ );
159
+ } catch (err) {
160
+ const msg = err instanceof Error ? err.message : String(err);
161
+ if (msg.includes("Unauthorized") || msg.includes("not authenticated")) {
162
+ throw new Error(
163
+ "Authentication expired. Run `fml login` to sign in again, then restart Claude Code."
164
+ );
165
+ }
166
+ throw err;
167
+ }
168
+ },
133
169
  // ── Repo resolution ──────────────────────────────────────────────────
134
170
  async resolveRepo(orgSlug, repoFullName) {
135
171
  const client = await getClient();
@@ -182,4 +218,4 @@ export {
182
218
  createApiClient,
183
219
  getAuthenticatedClient
184
220
  };
185
- //# sourceMappingURL=chunk-I5BHNW37.js.map
221
+ //# sourceMappingURL=chunk-BOREGHA6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/native-check.ts","../src/convex-client.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { execSync } from \"node:child_process\";\nimport { rmSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Verify that better-sqlite3's native addon matches the running Node version.\n * If mismatched (e.g. user upgraded Node), attempt an automatic rebuild.\n * Call this before any code that imports better-sqlite3.\n */\nexport function ensureNativeModules(): void {\n const require = createRequire(import.meta.url);\n try {\n require(\"better-sqlite3\");\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"\";\n if (!msg.includes(\"NODE_MODULE_VERSION\")) {\n throw err;\n }\n\n console.error(\n \"[fml] better-sqlite3 was compiled for a different Node.js version, rebuilding...\",\n );\n\n const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), \"..\");\n\n // Remove stale build directory to avoid EEXIST errors from node-gyp\n const sqlitePath = dirname(require.resolve(\"better-sqlite3/package.json\"));\n rmSync(join(sqlitePath, \"build\"), { recursive: true, force: true });\n\n try {\n execSync(\"npm rebuild better-sqlite3\", {\n cwd: pkgRoot,\n stdio: \"inherit\",\n timeout: 60_000,\n });\n console.error(\"[fml] Rebuild complete.\");\n } catch {\n console.error(\n \"[fml] Automatic rebuild failed. Please run manually:\\n\" +\n ` cd ${pkgRoot} && rm -rf node_modules/better-sqlite3/build && npm rebuild better-sqlite3`,\n );\n process.exit(1);\n }\n }\n}\n","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 interface ToolResult {\n ok: boolean;\n result?: unknown;\n error?: string;\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\n// ── API client factory ──────────────────────────────────────────────────────\n\nexport function createApiClient(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 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 if (isServiceToken) {\n // Service token path: POST to HTTP endpoint (Convex can't validate non-JWT tokens)\n // Thread user identity: sandbox agents set FML_USER_EXTERNAL_ID;\n // device-flow logins fall back to the id stashed on the stored token\n // so tools can execute with the real user context even though the\n // token's actAsExternalId is the namespaced system:cli:* string.\n const { readTokens } = await import(\"./auth/token-store.js\");\n const userExternalId =\n process.env.FML_USER_EXTERNAL_ID ?? readTokens()?.user.id;\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({\n toolName,\n args,\n ...(userExternalId && { userExternalId }),\n }),\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 data;\n }\n\n // JWT path: standard Convex action\n const client = await getClient();\n const actionArgs: Record<string, unknown> = { toolName, args };\n // Explicit org > stored org selection > repo-based inference\n const org = opts?.org ?? getSelectedOrg();\n if (org) actionArgs.org = org;\n const repo = resolveRepoFromCwd(process.cwd());\n if (repo) actionArgs.repo = repo.repo;\n\n const result = await client.action(\n ref<\"action\">(\"user/tool_gateway:executeTool\"),\n actionArgs,\n );\n return { ok: true, result };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes(\"Unauthorized\") || msg.includes(\"not authenticated\")) {\n return {\n ok: false,\n error:\n \"Authentication expired. Run `fml login` to sign in again, then restart Claude Code.\",\n };\n }\n return { ok: false, error: msg };\n }\n },\n\n // ── Tool catalog ─────────────────────────────────────────────────────\n\n async listTools(pluginVersion?: string): Promise<PublicToolDescriptor[]> {\n try {\n if (isServiceToken) {\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: { ok: boolean; descriptors?: PublicToolDescriptor[]; error?: string };\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 throw new Error(data.error ?? `HTTP ${res.status}`);\n }\n return data.descriptors;\n }\n\n const client = await getClient();\n return await client.query<PublicToolDescriptor[]>(\n ref<\"query\">(\"user/plugin_tools:listTools\"),\n pluginVersion ? { pluginVersion } : {},\n );\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(\n \"Authentication expired. Run `fml login` to sign in again, then restart Claude Code.\",\n );\n }\n throw err;\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 createApiClient(token);\n}\n"],"mappings":"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","names":["require"]}
@@ -2,7 +2,7 @@ import {
2
2
  SYNC_PID_FILE,
3
3
  isDaemonRunning,
4
4
  panopticonExec
5
- } from "./chunk-ZA4RG25C.js";
5
+ } from "./chunk-MUKYS4IB.js";
6
6
 
7
7
  // src/commands/daemon.ts
8
8
  function parsePanopticonRunning() {
@@ -30,4 +30,4 @@ export {
30
30
  handleStart,
31
31
  handleStop
32
32
  };
33
- //# sourceMappingURL=chunk-YIWT7BWN.js.map
33
+ //# sourceMappingURL=chunk-CFPXJT5Q.js.map
@@ -0,0 +1,93 @@
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
+ // src/bin-utils.ts
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import { execFileSync, execSync } from "child_process";
14
+ var IS_WIN = process.platform === "win32";
15
+ var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
16
+ function resolveBin(name) {
17
+ const pathDirs = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
18
+ const hasExt = path.extname(name) !== "";
19
+ const exts = !IS_WIN || hasExt ? [""] : (process.env.PATHEXT ?? DEFAULT_PATHEXT).split(";").filter(Boolean);
20
+ for (const dir of pathDirs) {
21
+ for (const ext of exts) {
22
+ const candidate = path.join(dir, name + ext);
23
+ try {
24
+ if (fs.statSync(candidate).isFile()) return candidate;
25
+ } catch {
26
+ }
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+ function isWinShim(bin) {
32
+ return IS_WIN && /\.(cmd|bat)$/i.test(bin);
33
+ }
34
+ function quoteWinArg(arg) {
35
+ if (arg.length === 0) return '""';
36
+ if (/^[A-Za-z0-9@\-_+=:/.,]+$/.test(arg)) return arg;
37
+ const crt = arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, "$1$1");
38
+ const quoted = `"${crt}"`;
39
+ return quoted.replace(/([()%!^<>&|"])/g, "^$1");
40
+ }
41
+ function execBinSync(bin, args, opts = {}) {
42
+ const { timeout = 1e4, encoding = "utf-8", stdio = "pipe" } = opts;
43
+ if (isWinShim(bin)) {
44
+ const command = [bin, ...args].map(quoteWinArg).join(" ");
45
+ return execSync(command, { timeout, encoding, stdio });
46
+ }
47
+ return execFileSync(bin, args, { timeout, encoding, stdio });
48
+ }
49
+
50
+ // 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
+ function resolvePanopticonBin() {
64
+ return resolveBin("panopticon");
65
+ }
66
+ function panopticonExec(...argsAndOpts) {
67
+ let timeout = 1e4;
68
+ const args = [];
69
+ for (const a of argsAndOpts) {
70
+ if (typeof a === "string") args.push(a);
71
+ else timeout = a.timeout ?? timeout;
72
+ }
73
+ const bin = resolvePanopticonBin();
74
+ if (!bin) return { ok: false, stdout: "panopticon binary not found" };
75
+ try {
76
+ const stdout = execBinSync(bin, args, { timeout });
77
+ return { ok: true, stdout };
78
+ } catch (err) {
79
+ const e = err;
80
+ return { ok: false, stdout: (e.stdout ?? "") + (e.stderr ?? "") };
81
+ }
82
+ }
83
+
84
+ export {
85
+ resolveBin,
86
+ execBinSync,
87
+ SYNC_PID_FILE,
88
+ SYNC_LOG_FILE,
89
+ isDaemonRunning,
90
+ resolvePanopticonBin,
91
+ panopticonExec
92
+ };
93
+ //# sourceMappingURL=chunk-MUKYS4IB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/daemon-utils.ts","../src/bin-utils.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { execBinSync, resolveBin } from \"./bin-utils.js\";\nimport { FML_DATA_DIR, FML_LOG_DIR } from \"./dirs.js\";\n\nexport const SYNC_PID_FILE = path.join(FML_DATA_DIR, \"sync.pid\");\nexport const SYNC_LOG_FILE = path.join(FML_LOG_DIR, \"sync.log\");\n\nexport function isDaemonRunning(pidFile: string): {\n running: boolean;\n pid: number | null;\n} {\n if (!fs.existsSync(pidFile)) return { running: false, pid: null };\n const pid = parseInt(fs.readFileSync(pidFile, \"utf-8\").trim(), 10);\n try {\n process.kill(pid, 0);\n return { running: true, pid };\n } catch {\n return { running: false, pid };\n }\n}\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","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"],"mappings":";;;;;;AAAA,OAAOA,SAAQ;AACf,OAAOC,WAAU;;;ACDjB,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;;;AD3FO,IAAM,gBAAgBC,MAAK,KAAK,cAAc,UAAU;AACxD,IAAM,gBAAgBA,MAAK,KAAK,aAAa,UAAU;AAEvD,SAAS,gBAAgB,SAG9B;AACA,MAAI,CAACC,IAAG,WAAW,OAAO,EAAG,QAAO,EAAE,SAAS,OAAO,KAAK,KAAK;AAChE,QAAM,MAAM,SAASA,IAAG,aAAa,SAAS,OAAO,EAAE,KAAK,GAAG,EAAE;AACjE,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO,EAAE,SAAS,MAAM,IAAI;AAAA,EAC9B,QAAQ;AACN,WAAO,EAAE,SAAS,OAAO,IAAI;AAAA,EAC/B;AACF;AAGO,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":["fs","path","path","fs"]}
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createApiClient,
4
4
  ensureNativeModules,
5
5
  getAuthenticatedClient
6
- } from "./chunk-I5BHNW37.js";
6
+ } from "./chunk-BOREGHA6.js";
7
7
  import {
8
8
  CONVEX_URL,
9
9
  DEFAULT_SYNC_URL,
@@ -27,10 +27,12 @@ import {
27
27
  handleStart,
28
28
  handleStop,
29
29
  parsePanopticonRunning
30
- } from "./chunk-YIWT7BWN.js";
30
+ } from "./chunk-CFPXJT5Q.js";
31
31
  import {
32
- panopticonExec
33
- } from "./chunk-ZA4RG25C.js";
32
+ execBinSync,
33
+ panopticonExec,
34
+ resolveBin
35
+ } from "./chunk-MUKYS4IB.js";
34
36
  import {
35
37
  FML_DATA_DIR,
36
38
  FML_LOG_DIR
@@ -273,7 +275,6 @@ async function handleDoctor(opts) {
273
275
  }
274
276
 
275
277
  // src/commands/install.ts
276
- import { execFileSync } from "child_process";
277
278
  import fs from "fs";
278
279
  import os from "os";
279
280
  import path from "path";
@@ -369,7 +370,7 @@ async function handleInstall() {
369
370
  const pluginRoot = getPluginRoot();
370
371
  console.log("Installing fml...\n");
371
372
  console.log("[1/5] Setting up panopticon...");
372
- const { resolvePanopticonBin } = await import("./daemon-utils-LIG6K2MK.js");
373
+ const { resolvePanopticonBin } = await import("./daemon-utils-CJO4XVJQ.js");
373
374
  let freshInstall = false;
374
375
  const bin = resolvePanopticonBin();
375
376
  let needsInstall = !bin;
@@ -386,18 +387,22 @@ async function handleInstall() {
386
387
  console.log(
387
388
  bin ? " Upgrading @fml-inc/panopticon..." : " Installing @fml-inc/panopticon globally..."
388
389
  );
389
- try {
390
- execFileSync("npm", ["install", "-g", "@fml-inc/panopticon@latest"], {
391
- encoding: "utf-8",
392
- timeout: 12e4,
393
- stdio: "pipe"
394
- });
395
- freshInstall = true;
396
- console.log(" Installed @fml-inc/panopticon");
397
- } catch (err) {
398
- const msg = err instanceof Error ? err.message : String(err);
399
- console.error(` Failed to install panopticon: ${msg}`);
390
+ const npmBin = resolveBin("npm");
391
+ if (!npmBin) {
392
+ console.error(" Failed to install panopticon: npm not found on PATH");
400
393
  console.error(" Install manually: npm install -g @fml-inc/panopticon@latest");
394
+ } else {
395
+ try {
396
+ execBinSync(npmBin, ["install", "-g", "@fml-inc/panopticon@latest"], {
397
+ timeout: 12e4
398
+ });
399
+ freshInstall = true;
400
+ console.log(" Installed @fml-inc/panopticon");
401
+ } catch (err) {
402
+ const msg = err instanceof Error ? err.message : String(err);
403
+ console.error(` Failed to install panopticon: ${msg}`);
404
+ console.error(" Install manually: npm install -g @fml-inc/panopticon@latest");
405
+ }
401
406
  }
402
407
  }
403
408
  if (!freshInstall) {
@@ -481,21 +486,26 @@ async function handleInstall() {
481
486
  settings.enabledPlugins["fml@local-plugins"] = true;
482
487
  writeJsonFile(CLAUDE_SETTINGS_PATH, settings);
483
488
  console.log(` Claude settings: ${CLAUDE_SETTINGS_PATH}`);
484
- try {
489
+ const claudeBin = resolveBin("claude");
490
+ if (!claudeBin) {
491
+ console.log(" warn: claude CLI not found, run 'claude plugin install fml@local-plugins' manually");
492
+ } else {
485
493
  try {
486
- execFileSync("claude", ["plugin", "install", "fml@local-plugins"], {
487
- stdio: "pipe",
488
- timeout: 15e3
489
- });
490
- } catch {
491
- execFileSync("claude", ["plugin", "update", "fml@local-plugins"], {
492
- stdio: "pipe",
493
- timeout: 15e3
494
- });
494
+ try {
495
+ execBinSync(claudeBin, ["plugin", "install", "fml@local-plugins"], {
496
+ timeout: 15e3
497
+ });
498
+ } catch {
499
+ execBinSync(claudeBin, ["plugin", "update", "fml@local-plugins"], {
500
+ timeout: 15e3
501
+ });
502
+ }
503
+ console.log(" Plugin registered via Claude Code CLI");
504
+ } catch (err) {
505
+ const msg = err instanceof Error ? err.message : String(err);
506
+ console.log(` warn: claude plugin install/update failed: ${msg}`);
507
+ console.log(" Run 'claude plugin install fml@local-plugins' manually");
495
508
  }
496
- console.log(" Plugin registered via Claude Code CLI");
497
- } catch {
498
- console.log(" warn: claude CLI not found, run 'claude plugin install fml@local-plugins' manually");
499
509
  }
500
510
  console.log();
501
511
  console.log("[5/5] Configuring sync target...");
@@ -531,7 +541,6 @@ async function handleInstall() {
531
541
  }
532
542
 
533
543
  // src/commands/uninstall.ts
534
- import { execFileSync as execFileSync2 } from "child_process";
535
544
  import fs2 from "fs";
536
545
  import os2 from "os";
537
546
  import path2 from "path";
@@ -568,14 +577,17 @@ function handleUninstall(opts) {
568
577
  console.log("[1/5] Skipping plugin uninstall (target-specific uninstall)");
569
578
  } else {
570
579
  console.log("[1/5] Uninstalling MCP plugin...");
571
- try {
572
- execFileSync2(
573
- "claude",
574
- ["plugin", "uninstall", "fml@local-plugins", "--scope", "user"],
575
- { stdio: "pipe", timeout: 1e4 }
576
- );
577
- console.log(" Uninstalled (user scope)");
578
- } catch {
580
+ const claudeBin = resolveBin("claude");
581
+ if (claudeBin) {
582
+ try {
583
+ execBinSync(
584
+ claudeBin,
585
+ ["plugin", "uninstall", "fml@local-plugins", "--scope", "user"],
586
+ { timeout: 1e4 }
587
+ );
588
+ console.log(" Uninstalled (user scope)");
589
+ } catch {
590
+ }
579
591
  }
580
592
  const installedPluginsPath = path2.join(
581
593
  CLAUDE_DIR2,
@@ -689,7 +701,7 @@ function handleUninstall(opts) {
689
701
  // src/commands/update.ts
690
702
  import { execSync as execSync2 } from "child_process";
691
703
  async function handleUpdate() {
692
- const currentVersion = true ? "0.1.3+6d86365" : "unknown";
704
+ const currentVersion = true ? "0.3.0+3bdd3b3" : "unknown";
693
705
  console.log(`Current: ${currentVersion}`);
694
706
  console.log("Updating via npm...\n");
695
707
  try {
@@ -1931,6 +1943,134 @@ async function handleConfigDetail(opts) {
1931
1943
  }
1932
1944
  }
1933
1945
 
1946
+ // src/commands/tools.ts
1947
+ import { readFileSync } from "fs";
1948
+ import path3 from "path";
1949
+ var pluginVersion = true ? "0.3.0+3bdd3b3" : void 0;
1950
+ async function getApi() {
1951
+ const api = await getAuthenticatedClient();
1952
+ if (!api) {
1953
+ console.error("Not authenticated. Run `fml login`.");
1954
+ process.exit(1);
1955
+ }
1956
+ return api;
1957
+ }
1958
+ function levenshtein(a, b) {
1959
+ if (a === b) return 0;
1960
+ if (a.length === 0) return b.length;
1961
+ if (b.length === 0) return a.length;
1962
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
1963
+ for (let i = 0; i < a.length; i++) {
1964
+ const curr = [i + 1];
1965
+ for (let j = 0; j < b.length; j++) {
1966
+ const cost = a[i] === b[j] ? 0 : 1;
1967
+ curr.push(Math.min(curr[j] + 1, prev[j + 1] + 1, prev[j] + cost));
1968
+ }
1969
+ prev = curr;
1970
+ }
1971
+ return prev[b.length];
1972
+ }
1973
+ function closestMatches(name, descriptors) {
1974
+ const threshold = Math.max(2, Math.ceil(name.length * 0.4));
1975
+ return descriptors.map((d) => ({
1976
+ name: d.name,
1977
+ // Substring match always beats fuzzy match.
1978
+ score: d.name.includes(name) ? 0 : levenshtein(name, d.name)
1979
+ })).filter((m) => m.score <= threshold).sort((a, b) => a.score - b.score).slice(0, 3).map((m) => m.name);
1980
+ }
1981
+ function findOrSuggestExit(name, descriptors) {
1982
+ const descriptor = descriptors.find((d) => d.name === name);
1983
+ if (descriptor) return descriptor;
1984
+ console.error(`Unknown tool: ${name}`);
1985
+ const suggestions = closestMatches(name, descriptors);
1986
+ if (suggestions.length > 0) {
1987
+ console.error(`Did you mean: ${suggestions.join(", ")}?`);
1988
+ }
1989
+ process.exit(1);
1990
+ }
1991
+ async function handleToolsList(opts) {
1992
+ const api = await getApi();
1993
+ let descriptors = await api.listTools(pluginVersion);
1994
+ if (opts.category) {
1995
+ descriptors = descriptors.filter((d) => d.category === opts.category);
1996
+ }
1997
+ if (opts.json) {
1998
+ console.log(JSON.stringify(descriptors, null, 2));
1999
+ return;
2000
+ }
2001
+ if (descriptors.length === 0) {
2002
+ console.log("No tools found.");
2003
+ return;
2004
+ }
2005
+ const nameW = Math.min(
2006
+ 40,
2007
+ Math.max(...descriptors.map((d) => d.name.length), 4)
2008
+ );
2009
+ const catW = 14;
2010
+ const header = "name".padEnd(nameW) + " " + "category".padEnd(catW) + " description";
2011
+ const divider = "-".repeat(header.length);
2012
+ console.log(header);
2013
+ console.log(divider);
2014
+ for (const d of descriptors) {
2015
+ const cat = d.category ?? "";
2016
+ const suffix = d.experimental ? " (experimental)" : "";
2017
+ const desc = (d.description + suffix).slice(0, 80);
2018
+ console.log(d.name.padEnd(nameW) + " " + cat.padEnd(catW) + " " + desc);
2019
+ }
2020
+ }
2021
+ async function handleToolsDescribe(name, opts) {
2022
+ const api = await getApi();
2023
+ const descriptors = await api.listTools(pluginVersion);
2024
+ const descriptor = findOrSuggestExit(name, descriptors);
2025
+ if (opts.json) {
2026
+ console.log(JSON.stringify(descriptor, null, 2));
2027
+ return;
2028
+ }
2029
+ console.log(`Name: ${descriptor.name}`);
2030
+ if (descriptor.category) console.log(`Category: ${descriptor.category}`);
2031
+ if (descriptor.experimental) console.log(`Experimental: true`);
2032
+ console.log(`
2033
+ Description:
2034
+ ${descriptor.description}`);
2035
+ console.log(`
2036
+ Input schema:
2037
+ ${JSON.stringify(descriptor.inputSchema, null, 2)}`);
2038
+ }
2039
+ async function handleToolsCall(name, opts) {
2040
+ if (opts.args !== void 0 && opts.file !== void 0) {
2041
+ console.error("Error: --args and --file are mutually exclusive.");
2042
+ process.exit(1);
2043
+ }
2044
+ const raw = opts.file ? (() => {
2045
+ try {
2046
+ return readFileSync(path3.resolve(opts.file), "utf8");
2047
+ } catch (err) {
2048
+ const msg = err instanceof Error ? err.message : String(err);
2049
+ console.error(`Could not read args file: ${msg}`);
2050
+ process.exit(1);
2051
+ }
2052
+ })() : opts.args ?? "{}";
2053
+ let parsedArgs;
2054
+ try {
2055
+ parsedArgs = JSON.parse(raw);
2056
+ } catch (err) {
2057
+ const msg = err instanceof Error ? err.message : String(err);
2058
+ console.error(
2059
+ `Invalid JSON in ${opts.file ? "--file" : "--args"}: ${msg}`
2060
+ );
2061
+ process.exit(1);
2062
+ }
2063
+ const api = await getApi();
2064
+ const descriptors = await api.listTools(pluginVersion);
2065
+ findOrSuggestExit(name, descriptors);
2066
+ const result = await api.callBackend(name, parsedArgs);
2067
+ if (!result.ok) {
2068
+ console.error(result.error || "Request failed");
2069
+ process.exit(1);
2070
+ }
2071
+ console.log(JSON.stringify(result.result, null, 2));
2072
+ }
2073
+
1934
2074
  // src/cli.ts
1935
2075
  ensureNativeModules();
1936
2076
  await initSentry();
@@ -1938,9 +2078,9 @@ if (process.argv.length <= 2 || process.argv.includes("--help") || process.argv.
1938
2078
  printBanner();
1939
2079
  }
1940
2080
  var program = new Command().name("fml").description("FML agent tools for Claude Code").version(
1941
- true ? "0.1.3+6d86365" : "dev"
2081
+ true ? "0.3.0+3bdd3b3" : "dev"
1942
2082
  );
1943
- program.command("tools", { hidden: true }).description("List all available CLI commands (including hidden agent tools)").action(() => {
2083
+ var tools = program.command("tools", { hidden: true }).description("List all available CLI commands (including hidden agent tools)").action(() => {
1944
2084
  function formatCmd(cmd, prefix = "") {
1945
2085
  const name = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
1946
2086
  const args = cmd.registeredArguments.map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
@@ -1959,6 +2099,9 @@ program.command("tools", { hidden: true }).description("List all available CLI c
1959
2099
  });
1960
2100
  console.log(lines.join("\n\n"));
1961
2101
  });
2102
+ tools.command("list").description("List backend tools available via the dynamic catalog").option("--category <category>", "Filter by category").option("--json", "Output as JSON").action((opts) => handleToolsList(opts));
2103
+ tools.command("describe").description("Show description and input schema for a backend tool").argument("<name>", "Tool name (e.g. integration-github)").option("--json", "Output as JSON").action((name, opts) => handleToolsDescribe(name, opts));
2104
+ tools.command("call").description("Invoke a backend tool by name with JSON args").argument("<name>", "Tool name").option("--args <json>", "Args as a JSON string (defaults to {})").option("--file <path>", "Path to a JSON file containing args").action((name, opts) => handleToolsCall(name, opts));
1962
2105
  program.command("install").description("Register plugin, hooks, and daemons").action(() => handleInstall());
1963
2106
  program.command("uninstall").description("Remove fml plugin, hooks, and optionally all data").option(
1964
2107
  "--target <target>",