@costlens/mcp-server 0.1.0 → 0.4.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/dist/cli.js ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/cli.ts
5
+ var import_http = require("http");
6
+ var import_child_process = require("child_process");
7
+ var import_fs = require("fs");
8
+ var import_os = require("os");
9
+ var import_path = require("path");
10
+ var CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".costlens");
11
+ var CONFIG_FILE = (0, import_path.join)(CONFIG_DIR, "config.json");
12
+ var API_BASE = process.env.COSTLENS_API_URL || "https://api.costlens.dev";
13
+ var APP_URL = process.env.COSTLENS_APP_URL || "https://costlens.dev";
14
+ function readConfig() {
15
+ try {
16
+ return JSON.parse((0, import_fs.readFileSync)(CONFIG_FILE, "utf-8"));
17
+ } catch {
18
+ return {};
19
+ }
20
+ }
21
+ function writeConfig(config) {
22
+ if (!(0, import_fs.existsSync)(CONFIG_DIR)) (0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
23
+ (0, import_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2));
24
+ }
25
+ async function login() {
26
+ console.log("\u{1F511} CostLens \u2014 Authenticating...\n");
27
+ const port = 9876 + Math.floor(Math.random() * 100);
28
+ const server = (0, import_http.createServer)((req, res) => {
29
+ const url = new URL(req.url, `http://localhost:${port}`);
30
+ const key = url.searchParams.get("key");
31
+ if (key) {
32
+ writeConfig({ apiKey: key });
33
+ res.writeHead(200, { "Content-Type": "text/html" });
34
+ res.end('<html><body style="font-family:sans-serif;text-align:center;padding:60px"><h2>Authenticated</h2><p>You can close this tab and return to your terminal.</p></body></html>');
35
+ console.log("\u2713 Authenticated successfully");
36
+ console.log(` Key saved to ${CONFIG_FILE}
37
+ `);
38
+ setTimeout(() => {
39
+ server.close();
40
+ if (process.argv[2] === "setup") {
41
+ init();
42
+ } else {
43
+ process.exit(0);
44
+ }
45
+ }, 500);
46
+ } else {
47
+ res.writeHead(400);
48
+ res.end("Missing key");
49
+ }
50
+ });
51
+ server.listen(port, () => {
52
+ const authUrl = `${APP_URL}/cli-auth?port=${port}`;
53
+ console.log(` Opening browser: ${authUrl}
54
+ `);
55
+ const platform = process.platform;
56
+ try {
57
+ if (platform === "darwin") (0, import_child_process.execSync)(`open "${authUrl}"`);
58
+ else if (platform === "linux") (0, import_child_process.execSync)(`xdg-open "${authUrl}"`);
59
+ else if (platform === "win32") (0, import_child_process.execSync)(`start "${authUrl}"`);
60
+ else console.log(` Open this URL manually: ${authUrl}`);
61
+ } catch {
62
+ console.log(` Open this URL manually: ${authUrl}`);
63
+ }
64
+ console.log(" Waiting for authentication...");
65
+ });
66
+ setTimeout(() => {
67
+ console.log("\n\u2717 Timed out. Try again.");
68
+ server.close();
69
+ process.exit(1);
70
+ }, 12e4);
71
+ }
72
+ function init() {
73
+ const config = readConfig();
74
+ if (!config.apiKey) {
75
+ console.log("\u2717 Not authenticated. Run: npx @costlens/mcp-server login\n");
76
+ process.exit(1);
77
+ }
78
+ const mcpConfig = {
79
+ mcpServers: {
80
+ costlens: {
81
+ command: "npx",
82
+ args: ["-y", "@costlens/mcp-server"],
83
+ env: {
84
+ COSTLENS_MCP_KEY: config.apiKey
85
+ }
86
+ }
87
+ }
88
+ };
89
+ console.log("\u{1F4CB} MCP Configuration:\n");
90
+ console.log(JSON.stringify(mcpConfig, null, 2));
91
+ console.log("\n Add this to your agent config file:\n");
92
+ console.log(" Kiro: ~/.kiro/settings/mcp.json");
93
+ console.log(" Cursor: ~/.cursor/mcp.json");
94
+ console.log(" Claude Code: ~/.claude/mcp_servers.json");
95
+ console.log(" VS Code: .vscode/mcp.json");
96
+ console.log("");
97
+ const kiroPath = (0, import_path.join)((0, import_os.homedir)(), ".kiro", "settings", "mcp.json");
98
+ const cursorPath = (0, import_path.join)((0, import_os.homedir)(), ".cursor", "mcp.json");
99
+ const claudePath = (0, import_path.join)((0, import_os.homedir)(), ".claude", "mcp_servers.json");
100
+ const paths = [
101
+ { name: "Kiro", path: kiroPath },
102
+ { name: "Cursor", path: cursorPath },
103
+ { name: "Claude Code", path: claudePath }
104
+ ];
105
+ for (const { name, path } of paths) {
106
+ if ((0, import_fs.existsSync)(path)) {
107
+ try {
108
+ const existing = JSON.parse((0, import_fs.readFileSync)(path, "utf-8"));
109
+ if (!existing.mcpServers?.costlens) {
110
+ existing.mcpServers = { ...existing.mcpServers, ...mcpConfig.mcpServers };
111
+ (0, import_fs.writeFileSync)(path, JSON.stringify(existing, null, 2));
112
+ console.log(` \u2713 Auto-configured ${name} (${path})`);
113
+ } else {
114
+ console.log(` \xB7 ${name} already configured`);
115
+ }
116
+ } catch {
117
+ console.log(` \xB7 Could not auto-configure ${name}`);
118
+ }
119
+ }
120
+ }
121
+ console.log("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
122
+ console.log("\n \u2713 Setup complete. What's next:\n");
123
+ console.log(" 1. Restart your coding agent");
124
+ console.log(" 2. Start coding \u2014 sessions are tracked automatically");
125
+ console.log(" 3. View your dashboard: https://costlens.dev/dashboard");
126
+ console.log(" 4. Docs: https://costlens.dev/docs/mcp\n");
127
+ console.log(" Commands:");
128
+ console.log(" npx @costlens/mcp-server status \u2014 check connection");
129
+ console.log(" npx @costlens/mcp-server login \u2014 re-authenticate");
130
+ console.log("");
131
+ }
132
+ async function status() {
133
+ const config = readConfig();
134
+ if (!config.apiKey) {
135
+ console.log("\u2717 Not authenticated. Run: npx @costlens/mcp-server login\n");
136
+ process.exit(1);
137
+ }
138
+ console.log("\u{1F4CA} CostLens Status\n");
139
+ try {
140
+ const res = await fetch(`${API_BASE}/v1/spend`, {
141
+ headers: { Authorization: `Bearer ${config.apiKey}` },
142
+ signal: AbortSignal.timeout(5e3)
143
+ });
144
+ if (!res.ok) {
145
+ console.log(" \u2717 Connection failed (invalid key or server error)");
146
+ process.exit(1);
147
+ }
148
+ const data = await res.json();
149
+ console.log(` \u2713 Connected`);
150
+ console.log(` Key: ${config.apiKey.slice(0, 8)}...`);
151
+ console.log(` Today: $${(data.today || 0).toFixed(4)}`);
152
+ console.log(` This week: $${(data.week || 0).toFixed(4)}`);
153
+ console.log(` This month: $${(data.month || 0).toFixed(4)}`);
154
+ } catch (e) {
155
+ console.log(` \u2717 Could not connect: ${e.message}`);
156
+ process.exit(1);
157
+ }
158
+ }
159
+ var command = process.argv[2];
160
+ if (command === "login") login();
161
+ else if (command === "init") init();
162
+ else if (command === "status") status();
163
+ else if (command === "setup") setup();
164
+ else if (command) {
165
+ console.log(`Unknown command: ${command}`);
166
+ console.log("Available: setup, login, init, status");
167
+ process.exit(1);
168
+ }
169
+ async function setup() {
170
+ console.log("\u{1F527} CostLens \u2014 One-step setup\n");
171
+ const existing = readConfig();
172
+ if (existing.apiKey) {
173
+ console.log(" Already authenticated. Running init...\n");
174
+ init();
175
+ return;
176
+ }
177
+ await login();
178
+ }
package/dist/index.js CHANGED
@@ -1,37 +1,355 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- // @ts-nocheck — MCP SDK types cause deep instantiation issues with tsc
5
- const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
6
- const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
7
- const zod_1 = require("zod");
8
- const classifier_1 = require("@costlens/classifier");
9
- const server = new mcp_js_1.McpServer({
10
- name: '@costlens/mcp-server',
11
- version: '0.1.0',
3
+
4
+ // src/index.ts
5
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
6
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
7
+ var import_zod = require("zod");
8
+ var import_classifier = require("@lens360/classifier");
9
+ var import_fs = require("fs");
10
+ var import_path = require("path");
11
+ var import_os = require("os");
12
+ function resolveApiKey() {
13
+ const envKey = process.env.COSTLENS_API_KEY || process.env.COSTLENS_MCP_KEY || process.argv.find((a) => a.startsWith("--api-key="))?.split("=")[1];
14
+ if (envKey) return envKey;
15
+ try {
16
+ const config = JSON.parse((0, import_fs.readFileSync)((0, import_path.join)((0, import_os.homedir)(), ".costlens", "config.json"), "utf-8"));
17
+ return config.apiKey;
18
+ } catch {
19
+ return void 0;
20
+ }
21
+ }
22
+ var API_KEY = resolveApiKey();
23
+ var API_BASE = process.env.COSTLENS_API_URL || "https://api.costlens.dev";
24
+ async function loadPricing() {
25
+ try {
26
+ const headers = {};
27
+ if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`;
28
+ const res = await fetch(`${API_BASE}/v1/pricing`, { headers, signal: AbortSignal.timeout(5e3) });
29
+ if (res.ok) {
30
+ const data = await res.json();
31
+ for (const m of data.models) {
32
+ import_classifier.PRICING[m.model] = { input: m.input, output: m.output };
33
+ }
34
+ }
35
+ } catch {
36
+ }
37
+ }
38
+ var server = new import_mcp.McpServer({
39
+ name: "@lens360/mcp-server",
40
+ version: "0.3.1"
12
41
  });
13
- server.tool('costlens_suggest_model', 'Analyze a prompt and suggest the cheapest adequate model', {
14
- prompt: zod_1.z.string(),
15
- currentModel: zod_1.z.string(),
16
- provider: zod_1.z.string(),
17
- messageCount: zod_1.z.number().optional(),
18
- routingMode: zod_1.z.string().optional(),
19
- isFirstMessage: zod_1.z.boolean().optional(),
20
- }, async (params) => {
21
- const result = (0, classifier_1.classify)({
22
- prompt: params.prompt,
23
- currentModel: params.currentModel,
24
- provider: params.provider,
25
- messageCount: params.messageCount,
26
- routingMode: params.routingMode || undefined,
27
- isFirstMessage: params.isFirstMessage,
42
+ var activeSessionId = null;
43
+ var heartbeatInterval = null;
44
+ function startHeartbeat(sessionId) {
45
+ stopHeartbeat();
46
+ activeSessionId = sessionId;
47
+ heartbeatInterval = setInterval(async () => {
48
+ if (!activeSessionId || !API_KEY) return;
49
+ try {
50
+ const res = await fetch(`${API_BASE}/v1/productivity/heartbeat`, {
51
+ method: "POST",
52
+ headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
53
+ body: JSON.stringify({ session_id: activeSessionId }),
54
+ signal: AbortSignal.timeout(5e3)
55
+ });
56
+ if (res.status === 410) {
57
+ activeSessionId = null;
58
+ stopHeartbeat();
59
+ }
60
+ } catch {
61
+ }
62
+ }, 6e4);
63
+ }
64
+ function stopHeartbeat() {
65
+ if (heartbeatInterval) {
66
+ clearInterval(heartbeatInterval);
67
+ heartbeatInterval = null;
68
+ }
69
+ }
70
+ server.tool(
71
+ "get_spend_summary",
72
+ "Get current spend summary \u2014 daily, weekly, monthly, and session totals",
73
+ {},
74
+ async () => {
75
+ if (!API_KEY) {
76
+ return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
77
+ }
78
+ try {
79
+ const res = await fetch(`${API_BASE}/v1/spend`, {
80
+ headers: { "Authorization": `Bearer ${API_KEY}` },
81
+ signal: AbortSignal.timeout(5e3)
82
+ });
83
+ if (!res.ok) {
84
+ return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
85
+ }
86
+ const data = await res.json();
87
+ const lines = [
88
+ `\u{1F4B0} Spend Summary (${data.key})`,
89
+ ``,
90
+ `Today: $${data.daily.cost.toFixed(4)} (${data.daily.requests} requests)`,
91
+ `Week: $${data.weekly.cost.toFixed(4)} (${data.weekly.requests} requests)`,
92
+ `Month: $${data.monthly.cost.toFixed(4)} (${data.monthly.requests} requests)`
93
+ ];
94
+ if (data.session) {
95
+ lines.push(`Session: $${data.session.cost.toFixed(4)} (${data.session.requests} requests) [${data.session.correlationId}]`);
96
+ }
97
+ if (data.dailyBudget) {
98
+ lines.push(``, `Budget: $${data.budgetRemaining.toFixed(2)} remaining of $${data.dailyBudget.toFixed(2)}/day`);
99
+ }
100
+ return { content: [{ type: "text", text: lines.join("\n") }] };
101
+ } catch (err) {
102
+ return { content: [{ type: "text", text: `Error fetching spend: ${err.message}` }] };
103
+ }
104
+ }
105
+ );
106
+ server.tool(
107
+ "costlens_suggest_model",
108
+ "Analyze a prompt and suggest the cheapest adequate model",
109
+ {
110
+ prompt: import_zod.z.string(),
111
+ currentModel: import_zod.z.string(),
112
+ provider: import_zod.z.string(),
113
+ messageCount: import_zod.z.number().optional(),
114
+ routingMode: import_zod.z.string().optional(),
115
+ isFirstMessage: import_zod.z.boolean().optional()
116
+ },
117
+ async (params) => {
118
+ const result = (0, import_classifier.classify)({
119
+ prompt: params.prompt,
120
+ currentModel: params.currentModel,
121
+ provider: params.provider,
122
+ messageCount: params.messageCount,
123
+ routingMode: params.routingMode || void 0,
124
+ isFirstMessage: params.isFirstMessage
28
125
  });
126
+ if (API_KEY && result.suggestedModel) {
127
+ syncEvent(result, params).catch(() => {
128
+ });
129
+ }
29
130
  return {
30
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
131
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
31
132
  };
32
- });
133
+ }
134
+ );
135
+ async function syncEvent(result, params) {
136
+ try {
137
+ await fetch(`${API_BASE}/v1/classifier/events`, {
138
+ method: "POST",
139
+ headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
140
+ body: JSON.stringify({
141
+ complexity: result.level,
142
+ confidence: result.confidence,
143
+ suggestedModel: result.suggestedModel,
144
+ currentModel: params.currentModel,
145
+ provider: params.provider,
146
+ staticScore: result.signals.staticScore
147
+ }),
148
+ signal: AbortSignal.timeout(5e3)
149
+ });
150
+ } catch {
151
+ }
152
+ }
153
+ async function checkFeature(feature) {
154
+ if (!API_KEY) return false;
155
+ try {
156
+ const res = await fetch(`${API_BASE}/v1/plan`, {
157
+ headers: { "Authorization": `Bearer ${API_KEY}` },
158
+ signal: AbortSignal.timeout(5e3)
159
+ });
160
+ if (!res.ok) return false;
161
+ const plan = await res.json();
162
+ return plan.features?.includes(feature) ?? false;
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+ var productivityEnabled = null;
168
+ async function ensureProductivity() {
169
+ if (!API_KEY) return "Not connected to CostLens. Run: npx @costlens/mcp-server setup";
170
+ if (productivityEnabled === null) {
171
+ productivityEnabled = await checkFeature("productivity_basic");
172
+ }
173
+ if (!productivityEnabled) return "Productivity tracking requires Business plan or Productivity add-on.";
174
+ return null;
175
+ }
176
+ server.tool(
177
+ "start_session",
178
+ "Start a productivity tracking session",
179
+ {
180
+ task_description: import_zod.z.string().optional(),
181
+ branch: import_zod.z.string().optional(),
182
+ repo: import_zod.z.string().optional()
183
+ },
184
+ async (params) => {
185
+ const err = await ensureProductivity();
186
+ if (err) return { content: [{ type: "text", text: err }] };
187
+ try {
188
+ const res = await fetch(`${API_BASE}/v1/productivity/sessions`, {
189
+ method: "POST",
190
+ headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
191
+ body: JSON.stringify(params),
192
+ signal: AbortSignal.timeout(5e3)
193
+ });
194
+ if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
195
+ const data = await res.json();
196
+ if (data.data?.id) startHeartbeat(data.data.id);
197
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
198
+ } catch (e) {
199
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
200
+ }
201
+ }
202
+ );
203
+ server.tool(
204
+ "end_session",
205
+ "End a productivity tracking session",
206
+ {
207
+ session_id: import_zod.z.string(),
208
+ outcome: import_zod.z.enum(["completed", "abandoned", "paused"]).optional(),
209
+ artifacts: import_zod.z.array(import_zod.z.string()).optional()
210
+ },
211
+ async (params) => {
212
+ const err = await ensureProductivity();
213
+ if (err) return { content: [{ type: "text", text: err }] };
214
+ try {
215
+ const res = await fetch(`${API_BASE}/v1/productivity/sessions/${params.session_id}`, {
216
+ method: "PATCH",
217
+ headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
218
+ body: JSON.stringify({ outcome: params.outcome, artifacts: params.artifacts }),
219
+ signal: AbortSignal.timeout(5e3)
220
+ });
221
+ if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
222
+ const data = await res.json();
223
+ stopHeartbeat();
224
+ activeSessionId = null;
225
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
226
+ } catch (e) {
227
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
228
+ }
229
+ }
230
+ );
231
+ server.tool(
232
+ "log_event",
233
+ "Log a productivity event within a session",
234
+ {
235
+ session_id: import_zod.z.string(),
236
+ event_type: import_zod.z.enum(["prompt", "commit", "pr_created", "test_run", "deploy", "file_edit"]),
237
+ metadata: import_zod.z.record(import_zod.z.any()).optional()
238
+ },
239
+ async (params) => {
240
+ if (!API_KEY) return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
241
+ if (productivityEnabled === null) {
242
+ productivityEnabled = await checkFeature("productivity_basic");
243
+ }
244
+ const hasFull = await checkFeature("productivity_full");
245
+ if (!hasFull) return { content: [{ type: "text", text: "Event logging requires Productivity Insights add-on." }] };
246
+ try {
247
+ const res = await fetch(`${API_BASE}/v1/productivity/events`, {
248
+ method: "POST",
249
+ headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
250
+ body: JSON.stringify(params),
251
+ signal: AbortSignal.timeout(5e3)
252
+ });
253
+ if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
254
+ return { content: [{ type: "text", text: "Event logged." }] };
255
+ } catch (e) {
256
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
257
+ }
258
+ }
259
+ );
260
+ server.tool(
261
+ "get_productivity_summary",
262
+ "Get productivity summary for a time period",
263
+ {
264
+ period: import_zod.z.enum(["today", "week", "month"]).optional()
265
+ },
266
+ async (params) => {
267
+ const err = await ensureProductivity();
268
+ if (err) return { content: [{ type: "text", text: err }] };
269
+ try {
270
+ const period = params.period || "week";
271
+ const res = await fetch(`${API_BASE}/v1/productivity/summary?period=${period}`, {
272
+ headers: { "Authorization": `Bearer ${API_KEY}` },
273
+ signal: AbortSignal.timeout(5e3)
274
+ });
275
+ if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
276
+ const data = await res.json();
277
+ const lines = [
278
+ `\u{1F4CA} Productivity Summary (${period})`,
279
+ ``,
280
+ `Sessions: ${data.sessions}`,
281
+ `Time: ${data.total_time_minutes} min`,
282
+ `Tokens: ${data.tokens_used?.toLocaleString() || 0}`,
283
+ `Cost: $${data.cost?.toFixed(4) || "0.00"}`,
284
+ `Commits: ${data.commits || 0}`,
285
+ `PRs merged: ${data.prs_merged || 0}`
286
+ ];
287
+ if (data.cost_per_commit) lines.push(`Cost/commit: $${data.cost_per_commit.toFixed(4)}`);
288
+ if (data.cost_per_pr) lines.push(`Cost/PR: $${data.cost_per_pr.toFixed(4)}`);
289
+ if (data.estimated_time_saved_hours) lines.push(`Time saved: ~${data.estimated_time_saved_hours.toFixed(1)}h`);
290
+ return { content: [{ type: "text", text: lines.join("\n") }] };
291
+ } catch (e) {
292
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
293
+ }
294
+ }
295
+ );
296
+ server.tool(
297
+ "get_github_metrics",
298
+ "Get GitHub PR metrics \u2014 PRs merged, review rounds, first-pass rate, cost per PR",
299
+ {
300
+ period: import_zod.z.enum(["today", "week", "month"]).optional()
301
+ },
302
+ async (params) => {
303
+ if (!API_KEY) {
304
+ return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
305
+ }
306
+ try {
307
+ const period = params.period || "week";
308
+ const res = await fetch(`${API_BASE}/v1/productivity/github?period=${period}`, {
309
+ headers: { "Authorization": `Bearer ${API_KEY}` },
310
+ signal: AbortSignal.timeout(5e3)
311
+ });
312
+ if (res.status === 404) {
313
+ return { content: [{ type: "text", text: "GitHub not connected. Connect in Settings \u2192 GitHub at costlens.dev" }] };
314
+ }
315
+ if (!res.ok) {
316
+ return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
317
+ }
318
+ const { data } = await res.json();
319
+ const lines = [
320
+ `GitHub Metrics (${period}):`,
321
+ `PRs merged: ${data.prsMerged}`,
322
+ `Avg review rounds: ${data.avgReviewRounds}`,
323
+ `First-pass rate: ${data.firstPassRate}%`,
324
+ `Avg time to merge: ${data.avgTimeToMergeHours}h`,
325
+ `Cost per PR: $${data.costPerPr}`
326
+ ];
327
+ return { content: [{ type: "text", text: lines.join("\n") }] };
328
+ } catch (e) {
329
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
330
+ }
331
+ }
332
+ );
33
333
  async function main() {
34
- const transport = new stdio_js_1.StdioServerTransport();
35
- await server.connect(transport);
334
+ await loadPricing();
335
+ const transport = new import_stdio.StdioServerTransport();
336
+ await server.connect(transport);
337
+ const cleanup = async () => {
338
+ if (activeSessionId && API_KEY) {
339
+ try {
340
+ await fetch(`${API_BASE}/v1/productivity/sessions/${activeSessionId}`, {
341
+ method: "PATCH",
342
+ headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
343
+ body: JSON.stringify({ outcome: "completed" }),
344
+ signal: AbortSignal.timeout(3e3)
345
+ });
346
+ } catch {
347
+ }
348
+ }
349
+ stopHeartbeat();
350
+ process.exit(0);
351
+ };
352
+ process.on("SIGTERM", cleanup);
353
+ process.on("SIGINT", cleanup);
36
354
  }
37
355
  main().catch(console.error);
package/package.json CHANGED
@@ -1,21 +1,24 @@
1
1
  {
2
2
  "name": "@costlens/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.4.1",
4
4
  "description": "MCP server for AI cost optimization with prompt complexity classification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "bin": {
8
- "costlens-mcp": "./dist/index.js"
8
+ "costlens-mcp": "./dist/index.js",
9
+ "costlens": "./dist/cli.js"
9
10
  },
10
11
  "scripts": {
11
- "build": "tsc",
12
+ "build": "tsup src/index.ts src/cli.ts --format cjs --clean",
12
13
  "test": "echo 'no tests yet'"
13
14
  },
14
15
  "license": "MIT",
15
- "files": ["dist"],
16
+ "files": [
17
+ "dist"
18
+ ],
16
19
  "dependencies": {
17
20
  "@modelcontextprotocol/sdk": "^1.12.1",
18
- "@costlens/classifier": "0.1.0",
21
+ "@lens360/classifier": "0.1.0",
19
22
  "zod": "^3.25.0"
20
23
  }
21
24
  }
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};