@fml-inc/fml 0.0.25 → 0.0.27
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 -0
- package/README.md +101 -0
- package/dist/chunk-2CRFCFE4.js +11 -0
- package/dist/chunk-2CRFCFE4.js.map +1 -0
- package/dist/{chunk-WXDGVKXK.js → chunk-ND2YSPQ6.js} +138 -4
- package/dist/chunk-ND2YSPQ6.js.map +1 -0
- package/dist/chunk-NLFKYSN7.js +640 -0
- package/dist/chunk-NLFKYSN7.js.map +1 -0
- package/dist/chunk-UALBZA5X.js +123 -0
- package/dist/chunk-UALBZA5X.js.map +1 -0
- package/dist/chunk-YIWT7BWN.js +33 -0
- package/dist/chunk-YIWT7BWN.js.map +1 -0
- package/dist/{chunk-EL2WSQBR.js → chunk-ZA4RG25C.js} +6 -33
- package/dist/chunk-ZA4RG25C.js.map +1 -0
- package/dist/cli.js +1929 -77
- package/dist/cli.js.map +1 -1
- package/dist/{daemon-J4WSZXVY.js → daemon-2YGD43RJ.js} +3 -2
- package/dist/daemon-utils-LIG6K2MK.js +17 -0
- package/dist/daemon-utils-LIG6K2MK.js.map +1 -0
- package/dist/doctor-YV6KQEGW.js +2019 -0
- package/dist/doctor-YV6KQEGW.js.map +1 -0
- package/dist/esm-GTDP7OMQ-EP6UYLQG.js +14442 -0
- package/dist/esm-GTDP7OMQ-EP6UYLQG.js.map +1 -0
- package/dist/mcp/server.js +6 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/postinstall.js +0 -65
- package/dist/postinstall.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-EL2WSQBR.js.map +0 -1
- package/dist/chunk-WXDGVKXK.js.map +0 -1
- /package/dist/{daemon-J4WSZXVY.js.map → daemon-2YGD43RJ.js.map} +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# FML
|
|
2
|
+
|
|
3
|
+
Agent tools for AI coding assistants. Installs as a [Claude Code plugin](https://docs.anthropic.com/en/docs/claude-code/plugins) with MCP tools for session analytics, cost tracking, integrations, and team workflows — powered by [Panopticon](https://github.com/fml-inc/panopticon) for local data collection.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @fml-inc/fml
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This installs the plugin, registers hooks and MCP tools in Claude Code, and bootstraps Panopticon for local data collection. Start a new session to activate.
|
|
12
|
+
|
|
13
|
+
Then sign in:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
fml login
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
**Local observability** — Panopticon captures OpenTelemetry signals, hook events, session files, and API traffic from Claude Code, Gemini CLI, and Codex CLI into a local SQLite database.
|
|
22
|
+
|
|
23
|
+
**MCP tools for your agent** — Once installed, Claude Code gets tools to query your sessions, costs, activity, and connected integrations directly in conversation.
|
|
24
|
+
|
|
25
|
+
**Cloud sync** — Optionally sync local data to the FML dashboard for team-wide visibility, config snapshots, and automations.
|
|
26
|
+
|
|
27
|
+
## MCP tools
|
|
28
|
+
|
|
29
|
+
These tools are available to Claude Code via the plugin:
|
|
30
|
+
|
|
31
|
+
| Tool | Description |
|
|
32
|
+
|------|-------------|
|
|
33
|
+
| `get_engineering_activity` | Activity summary — sessions, prompts, tools, costs |
|
|
34
|
+
| `list_engineering_sessions` | List recent sessions with stats |
|
|
35
|
+
| `search_engineering_sessions` | Search across all sessions |
|
|
36
|
+
| `get_session_timeline` | Messages and tool calls for a session |
|
|
37
|
+
| `get_session_turns` | Per-turn token usage for a session |
|
|
38
|
+
| `get_ai_spending` | Token usage and cost breakdowns |
|
|
39
|
+
| `whoami` | Current auth and org status |
|
|
40
|
+
| `fml_list_integrations` | Connected integrations (Slack, GitHub, Linear, Sentry, etc.) |
|
|
41
|
+
| `fml_query_*` | Query connected integrations directly |
|
|
42
|
+
| `fml_list_messages` | Conversation messages |
|
|
43
|
+
| `fml_search_analysis` | Search codebase analysis results |
|
|
44
|
+
| `fml_run_analysis_workflow` | Run deep analysis workflows |
|
|
45
|
+
| `fml_list_skills` | Browse and load skills |
|
|
46
|
+
| `list_repo_configs` | Team config snapshots |
|
|
47
|
+
|
|
48
|
+
## CLI
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
fml install Register plugin, hooks, and daemons
|
|
52
|
+
fml uninstall Remove plugin and hooks
|
|
53
|
+
--target <t> Target: claude, gemini, codex, claude-desktop, all
|
|
54
|
+
--purge Also remove all data, logs, and auth tokens
|
|
55
|
+
fml update Update to the latest version
|
|
56
|
+
|
|
57
|
+
fml login Sign in to your FML account
|
|
58
|
+
fml logout Sign out and clear credentials
|
|
59
|
+
fml org [slug] Show or select organization
|
|
60
|
+
fml status Show auth and daemon status
|
|
61
|
+
fml doctor Check configuration and connectivity
|
|
62
|
+
|
|
63
|
+
fml open Open FML dashboard in browser
|
|
64
|
+
fml start Start panopticon server
|
|
65
|
+
fml stop Stop panopticon server
|
|
66
|
+
|
|
67
|
+
fml activity Activity summary
|
|
68
|
+
--since <duration> Time window (e.g. "24h", "7d")
|
|
69
|
+
fml sessions List recent sessions
|
|
70
|
+
fml timeline <id> Events for a session
|
|
71
|
+
fml spending Token usage and cost breakdown
|
|
72
|
+
fml search <query> Search across sessions
|
|
73
|
+
|
|
74
|
+
fml sync setup Configure sync targets
|
|
75
|
+
fml sync list List sync targets
|
|
76
|
+
fml sync add <n> <url> Add a sync target
|
|
77
|
+
fml sync remove <name> Remove a sync target
|
|
78
|
+
fml sync status Show sync status
|
|
79
|
+
fml sync reset [name] Reset sync watermarks
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pnpm install # Install dependencies
|
|
86
|
+
pnpm dev # Watch mode (tsup)
|
|
87
|
+
pnpm test # Run tests (Vitest)
|
|
88
|
+
pnpm type-check # Type check
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
To test the full install flow:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pnpm build && pnpm pack
|
|
95
|
+
npm install -g ./fml-inc-fml-*.tgz
|
|
96
|
+
fml install
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
Proprietary — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/dist/chunk-PZ5AY32C.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__export
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=chunk-2CRFCFE4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/dist/chunk-PZ5AY32C.js"],"sourcesContent":["var __defProp = Object.defineProperty;\nvar __export = (target, all) => {\n for (var name in all)\n __defProp(target, name, { get: all[name], enumerable: true });\n};\n\nexport {\n __export\n};\n//# sourceMappingURL=chunk-PZ5AY32C.js.map"],"mappings":";AAAA,IAAI,YAAY,OAAO;AACvB,IAAI,WAAW,CAAC,QAAQ,QAAQ;AAC9B,WAAS,QAAQ;AACf,cAAU,QAAQ,MAAM,EAAE,KAAK,IAAI,IAAI,GAAG,YAAY,KAAK,CAAC;AAChE;","names":[]}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Database
|
|
3
|
+
} from "./chunk-UALBZA5X.js";
|
|
1
4
|
import {
|
|
2
5
|
CONVEX_URL,
|
|
3
6
|
getSelectedOrg,
|
|
@@ -42,10 +45,141 @@ function ensureNativeModules() {
|
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
// node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/dist/chunk-Z4RRVQR6.js
|
|
49
|
+
import { execFileSync } from "child_process";
|
|
50
|
+
import os from "os";
|
|
51
|
+
import path from "path";
|
|
52
|
+
var SUPERSET_MARKER = `${path.sep}.superset${path.sep}`;
|
|
53
|
+
var SUPERSET_DB_PATH = path.join(os.homedir(), ".superset", "local.db");
|
|
54
|
+
var SupersetProvider = class {
|
|
55
|
+
id = "superset";
|
|
56
|
+
db = null;
|
|
57
|
+
dbFailed = false;
|
|
58
|
+
canResolve(cwd) {
|
|
59
|
+
return cwd.includes(SUPERSET_MARKER);
|
|
60
|
+
}
|
|
61
|
+
resolve(cwd) {
|
|
62
|
+
const db = this.getDb();
|
|
63
|
+
if (!db) return null;
|
|
64
|
+
try {
|
|
65
|
+
const wt = db.prepare(
|
|
66
|
+
`SELECT p.main_repo_path, w.branch, p.github_owner, p.name as project_name
|
|
67
|
+
FROM worktrees w
|
|
68
|
+
JOIN projects p ON w.project_id = p.id
|
|
69
|
+
WHERE ? LIKE w.path || '%'
|
|
70
|
+
ORDER BY LENGTH(w.path) DESC
|
|
71
|
+
LIMIT 1`
|
|
72
|
+
).get(cwd);
|
|
73
|
+
if (wt) return this.buildResult(wt);
|
|
74
|
+
const proj = db.prepare(
|
|
75
|
+
`SELECT main_repo_path, default_branch AS branch, github_owner, name as project_name
|
|
76
|
+
FROM projects
|
|
77
|
+
WHERE ? LIKE main_repo_path || '%'
|
|
78
|
+
ORDER BY LENGTH(main_repo_path) DESC
|
|
79
|
+
LIMIT 1`
|
|
80
|
+
).get(cwd);
|
|
81
|
+
if (proj) return this.buildResult(proj);
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
close() {
|
|
87
|
+
if (this.db) {
|
|
88
|
+
try {
|
|
89
|
+
this.db.close();
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
this.db = null;
|
|
93
|
+
}
|
|
94
|
+
this.dbFailed = false;
|
|
95
|
+
}
|
|
96
|
+
buildResult(row) {
|
|
97
|
+
if (row.github_owner) {
|
|
98
|
+
return {
|
|
99
|
+
repo: `${row.github_owner}/${row.project_name}`,
|
|
100
|
+
branch: row.branch
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { repoDir: row.main_repo_path, branch: row.branch };
|
|
104
|
+
}
|
|
105
|
+
getDb() {
|
|
106
|
+
if (this.dbFailed) return null;
|
|
107
|
+
if (this.db) return this.db;
|
|
108
|
+
try {
|
|
109
|
+
this.db = new Database(SUPERSET_DB_PATH, {
|
|
110
|
+
readonly: true,
|
|
111
|
+
fileMustExist: true
|
|
112
|
+
});
|
|
113
|
+
return this.db;
|
|
114
|
+
} catch {
|
|
115
|
+
this.dbFailed = true;
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var repoCache = /* @__PURE__ */ new Map();
|
|
121
|
+
var providers = [new SupersetProvider()];
|
|
122
|
+
function resolveGitRemote(dir) {
|
|
123
|
+
try {
|
|
124
|
+
const url = execFileSync(
|
|
125
|
+
"git",
|
|
126
|
+
["-C", dir, "remote", "get-url", "origin"],
|
|
127
|
+
{
|
|
128
|
+
encoding: "utf-8",
|
|
129
|
+
timeout: 5e3,
|
|
130
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
131
|
+
}
|
|
132
|
+
).trim();
|
|
133
|
+
const sshMatch = url.match(/git@[^:]+:([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
134
|
+
if (sshMatch) return sshMatch[1];
|
|
135
|
+
const httpsMatch = url.match(/\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
136
|
+
if (httpsMatch) return httpsMatch[1];
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function resolveGitBranch(dir) {
|
|
142
|
+
try {
|
|
143
|
+
return execFileSync("git", ["-C", dir, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
144
|
+
encoding: "utf-8",
|
|
145
|
+
timeout: 5e3,
|
|
146
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
147
|
+
}).trim() || null;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function resolveRepoFromCwd(cwd) {
|
|
153
|
+
if (repoCache.has(cwd)) return repoCache.get(cwd);
|
|
154
|
+
let result = null;
|
|
155
|
+
const repo = resolveGitRemote(cwd);
|
|
156
|
+
if (repo) {
|
|
157
|
+
result = { repo, branch: resolveGitBranch(cwd) };
|
|
158
|
+
} else {
|
|
159
|
+
for (const provider of providers) {
|
|
160
|
+
if (!provider.canResolve(cwd)) continue;
|
|
161
|
+
const resolved = provider.resolve(cwd);
|
|
162
|
+
if (!resolved) continue;
|
|
163
|
+
if (resolved.repo) {
|
|
164
|
+
result = { repo: resolved.repo, branch: resolved.branch };
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
if (resolved.repoDir) {
|
|
168
|
+
const fallbackRepo = resolveGitRemote(resolved.repoDir);
|
|
169
|
+
if (fallbackRepo) {
|
|
170
|
+
result = { repo: fallbackRepo, branch: resolved.branch };
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
repoCache.set(cwd, result);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
45
180
|
// src/convex-client.ts
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return path;
|
|
181
|
+
function ref(path2) {
|
|
182
|
+
return path2;
|
|
49
183
|
}
|
|
50
184
|
function createApiClient(token) {
|
|
51
185
|
const isServiceToken = token.startsWith("fml_st_");
|
|
@@ -176,4 +310,4 @@ export {
|
|
|
176
310
|
createApiClient,
|
|
177
311
|
getAuthenticatedClient
|
|
178
312
|
};
|
|
179
|
-
//# sourceMappingURL=chunk-
|
|
313
|
+
//# sourceMappingURL=chunk-ND2YSPQ6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/native-check.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/repo.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/workspaces/superset.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 { execFileSync } from \"node:child_process\";\nimport { SupersetProvider } from \"./workspaces/superset.js\";\nimport type { WorkspaceProvider } from \"./workspaces/types.js\";\n\nexport interface RepoInfo {\n repo: string;\n branch?: string | null;\n}\n\n// Cache: cwd → RepoInfo | null\nconst repoCache = new Map<string, RepoInfo | null>();\n\n// Registered workspace providers — checked in order when git fails.\nconst providers: WorkspaceProvider[] = [new SupersetProvider()];\n\n/** Resolve \"org/repo\" from a directory's git remote origin URL. */\nfunction resolveGitRemote(dir: string): string | null {\n try {\n const url = execFileSync(\n \"git\",\n [\"-C\", dir, \"remote\", \"get-url\", \"origin\"],\n {\n encoding: \"utf-8\",\n timeout: 5000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n },\n ).trim();\n\n // SSH: git@github.com:org/repo.git\n const sshMatch = url.match(/git@[^:]+:([^/]+\\/[^/]+?)(?:\\.git)?$/);\n if (sshMatch) return sshMatch[1];\n\n // HTTPS: https://github.com/org/repo.git\n const httpsMatch = url.match(/\\/([^/]+\\/[^/]+?)(?:\\.git)?$/);\n if (httpsMatch) return httpsMatch[1];\n } catch {\n // Not a git repo, no remote, etc.\n }\n return null;\n}\n\n/** Resolve the current git branch for a directory. */\nfunction resolveGitBranch(dir: string): string | null {\n try {\n return (\n execFileSync(\"git\", [\"-C\", dir, \"rev-parse\", \"--abbrev-ref\", \"HEAD\"], {\n encoding: \"utf-8\",\n timeout: 5000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null\n );\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the GitHub \"org/repo\" and branch for a working directory.\n * Results are cached for the lifetime of the process.\n *\n * 1. Try git directly on the CWD\n * 2. On failure, ask registered workspace providers (e.g. Superset)\n * for an alternative repo directory to resolve against\n */\nexport function resolveRepoFromCwd(cwd: string): RepoInfo | null {\n if (repoCache.has(cwd)) return repoCache.get(cwd)!;\n\n let result: RepoInfo | null = null;\n\n const repo = resolveGitRemote(cwd);\n if (repo) {\n result = { repo, branch: resolveGitBranch(cwd) };\n } else {\n // Ask workspace providers for a fallback\n for (const provider of providers) {\n if (!provider.canResolve(cwd)) continue;\n const resolved = provider.resolve(cwd);\n if (!resolved) continue;\n\n // Provider returned repo name directly (no git needed)\n if (resolved.repo) {\n result = { repo: resolved.repo, branch: resolved.branch };\n break;\n }\n\n // Provider returned a directory to resolve via git\n if (resolved.repoDir) {\n const fallbackRepo = resolveGitRemote(resolved.repoDir);\n if (fallbackRepo) {\n result = { repo: fallbackRepo, branch: resolved.branch };\n break;\n }\n }\n }\n }\n\n repoCache.set(cwd, result);\n return result;\n}\n\n// Cache: cwd → git identity\nconst gitIdentityCache = new Map<\n string,\n { name: string | null; email: string | null }\n>();\n\n/** Resolve git user.name / user.email for a directory. Cached per-process. */\nexport function resolveGitIdentity(cwd: string): {\n name: string | null;\n email: string | null;\n} {\n const cached = gitIdentityCache.get(cwd);\n if (cached) return cached;\n\n const result = { name: null as string | null, email: null as string | null };\n try {\n result.name =\n execFileSync(\"git\", [\"-C\", cwd, \"config\", \"user.name\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null;\n } catch {\n // no user.name configured\n }\n try {\n result.email =\n execFileSync(\"git\", [\"-C\", cwd, \"config\", \"user.email\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null;\n } catch {\n // no user.email configured\n }\n gitIdentityCache.set(cwd, result);\n return result;\n}\n\n/** Reset caches (for testing). */\nexport function _resetRepoCache(): void {\n repoCache.clear();\n gitIdentityCache.clear();\n for (const p of providers) p.close?.();\n}\n","import os from \"node:os\";\nimport path from \"node:path\";\nimport { Database } from \"../db/driver.js\";\nimport type { WorkspaceProvider } from \"./types.js\";\n\nconst SUPERSET_MARKER = `${path.sep}.superset${path.sep}`;\nconst SUPERSET_DB_PATH = path.join(os.homedir(), \".superset\", \"local.db\");\n\ninterface WorktreeRow {\n main_repo_path: string;\n branch: string | null;\n github_owner: string | null;\n project_name: string;\n}\n\nexport class SupersetProvider implements WorkspaceProvider {\n readonly id = \"superset\";\n\n private db: Database | null = null;\n private dbFailed = false;\n\n canResolve(cwd: string): boolean {\n return cwd.includes(SUPERSET_MARKER);\n }\n\n resolve(\n cwd: string,\n ): { repo?: string; repoDir?: string; branch?: string | null } | null {\n const db = this.getDb();\n if (!db) return null;\n\n try {\n // Try worktrees table first (for ~/.superset/worktrees/ paths)\n const wt = db\n .prepare(\n `SELECT p.main_repo_path, w.branch, p.github_owner, p.name as project_name\n FROM worktrees w\n JOIN projects p ON w.project_id = p.id\n WHERE ? LIKE w.path || '%'\n ORDER BY LENGTH(w.path) DESC\n LIMIT 1`,\n )\n .get(cwd) as WorktreeRow | undefined;\n if (wt) return this.buildResult(wt);\n\n // Fall back to projects table (for ~/.superset/projects/ paths)\n const proj = db\n .prepare(\n `SELECT main_repo_path, default_branch AS branch, github_owner, name as project_name\n FROM projects\n WHERE ? LIKE main_repo_path || '%'\n ORDER BY LENGTH(main_repo_path) DESC\n LIMIT 1`,\n )\n .get(cwd) as WorktreeRow | undefined;\n if (proj) return this.buildResult(proj);\n } catch {\n // Schema mismatch, corrupt DB, etc.\n }\n\n return null;\n }\n\n close(): void {\n if (this.db) {\n try {\n this.db.close();\n } catch {}\n this.db = null;\n }\n this.dbFailed = false;\n }\n\n private buildResult(row: WorktreeRow): {\n repo?: string;\n repoDir?: string;\n branch?: string | null;\n } {\n // Prefer deriving repo name directly from Superset DB (no git needed)\n if (row.github_owner) {\n return {\n repo: `${row.github_owner}/${row.project_name}`,\n branch: row.branch,\n };\n }\n // Fall back to git resolution on the main repo path\n return { repoDir: row.main_repo_path, branch: row.branch };\n }\n\n private getDb(): Database | null {\n if (this.dbFailed) return null;\n if (this.db) return this.db;\n try {\n this.db = new Database(SUPERSET_DB_PATH, {\n readonly: true,\n fileMustExist: true,\n });\n return this.db;\n } catch {\n this.dbFailed = true;\n return null;\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 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 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({ toolName, args }),\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 // ── 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","path"]}
|