@costlens/mcp-server 0.4.1 → 0.4.3
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 +444 -61
- package/package.json +2 -5
package/dist/cli.js
CHANGED
|
@@ -1,73 +1,458 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
function resolveApiKey() {
|
|
33
|
+
const envKey = process.env.COSTLENS_API_KEY || process.env.COSTLENS_MCP_KEY || process.argv.find((a) => a.startsWith("--api-key="))?.split("=")[1];
|
|
34
|
+
if (envKey) return envKey;
|
|
35
|
+
try {
|
|
36
|
+
const config = JSON.parse((0, import_fs.readFileSync)((0, import_path.join)((0, import_os.homedir)(), ".costlens", "config.json"), "utf-8"));
|
|
37
|
+
return config.apiKey;
|
|
38
|
+
} catch {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function loadPricing() {
|
|
43
|
+
try {
|
|
44
|
+
const headers = {};
|
|
45
|
+
if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`;
|
|
46
|
+
const res = await fetch(`${API_BASE}/v1/pricing`, { headers, signal: AbortSignal.timeout(5e3) });
|
|
47
|
+
if (res.ok) {
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
for (const m of data.models) {
|
|
50
|
+
import_classifier.PRICING[m.model] = { input: m.input, output: m.output };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function startHeartbeat(sessionId) {
|
|
57
|
+
stopHeartbeat();
|
|
58
|
+
activeSessionId = sessionId;
|
|
59
|
+
heartbeatInterval = setInterval(async () => {
|
|
60
|
+
if (!activeSessionId || !API_KEY) return;
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`${API_BASE}/v1/productivity/heartbeat`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify({ session_id: activeSessionId }),
|
|
66
|
+
signal: AbortSignal.timeout(5e3)
|
|
67
|
+
});
|
|
68
|
+
if (res.status === 410) {
|
|
69
|
+
activeSessionId = null;
|
|
70
|
+
stopHeartbeat();
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}, 6e4);
|
|
75
|
+
}
|
|
76
|
+
function stopHeartbeat() {
|
|
77
|
+
if (heartbeatInterval) {
|
|
78
|
+
clearInterval(heartbeatInterval);
|
|
79
|
+
heartbeatInterval = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function syncEvent(result, params) {
|
|
83
|
+
try {
|
|
84
|
+
await fetch(`${API_BASE}/v1/classifier/events`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
complexity: result.level,
|
|
89
|
+
confidence: result.confidence,
|
|
90
|
+
suggestedModel: result.suggestedModel,
|
|
91
|
+
currentModel: params.currentModel,
|
|
92
|
+
provider: params.provider,
|
|
93
|
+
staticScore: result.signals.staticScore
|
|
94
|
+
}),
|
|
95
|
+
signal: AbortSignal.timeout(5e3)
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function checkFeature(feature) {
|
|
101
|
+
if (!API_KEY) return false;
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch(`${API_BASE}/v1/plan`, {
|
|
104
|
+
headers: { "Authorization": `Bearer ${API_KEY}` },
|
|
105
|
+
signal: AbortSignal.timeout(5e3)
|
|
106
|
+
});
|
|
107
|
+
if (!res.ok) return false;
|
|
108
|
+
const plan = await res.json();
|
|
109
|
+
return plan.features?.includes(feature) ?? false;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function ensureProductivity() {
|
|
115
|
+
if (!API_KEY) return "Not connected to CostLens. Run: npx @costlens/mcp-server setup";
|
|
116
|
+
if (productivityEnabled === null) {
|
|
117
|
+
productivityEnabled = await checkFeature("productivity_basic");
|
|
118
|
+
}
|
|
119
|
+
if (!productivityEnabled) return "Productivity tracking requires Business plan or Productivity add-on.";
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
async function main() {
|
|
123
|
+
await loadPricing();
|
|
124
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
125
|
+
await server.connect(transport);
|
|
126
|
+
const cleanup = async () => {
|
|
127
|
+
if (activeSessionId && API_KEY) {
|
|
128
|
+
try {
|
|
129
|
+
await fetch(`${API_BASE}/v1/productivity/sessions/${activeSessionId}`, {
|
|
130
|
+
method: "PATCH",
|
|
131
|
+
headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
|
|
132
|
+
body: JSON.stringify({ outcome: "completed" }),
|
|
133
|
+
signal: AbortSignal.timeout(3e3)
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
stopHeartbeat();
|
|
139
|
+
process.exit(0);
|
|
140
|
+
};
|
|
141
|
+
process.on("SIGTERM", cleanup);
|
|
142
|
+
process.on("SIGINT", cleanup);
|
|
143
|
+
}
|
|
144
|
+
var import_mcp, import_stdio, import_zod, import_classifier, import_fs, import_path, import_os, API_KEY, API_BASE, server, activeSessionId, heartbeatInterval, productivityEnabled;
|
|
145
|
+
var init_index = __esm({
|
|
146
|
+
"src/index.ts"() {
|
|
147
|
+
"use strict";
|
|
148
|
+
import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
149
|
+
import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
150
|
+
import_zod = require("zod");
|
|
151
|
+
import_classifier = require("@lens360/classifier");
|
|
152
|
+
import_fs = require("fs");
|
|
153
|
+
import_path = require("path");
|
|
154
|
+
import_os = require("os");
|
|
155
|
+
API_KEY = resolveApiKey();
|
|
156
|
+
API_BASE = process.env.COSTLENS_API_URL || "https://api.costlens.dev";
|
|
157
|
+
server = new import_mcp.McpServer({
|
|
158
|
+
name: "@lens360/mcp-server",
|
|
159
|
+
version: "0.3.1"
|
|
160
|
+
});
|
|
161
|
+
activeSessionId = null;
|
|
162
|
+
heartbeatInterval = null;
|
|
163
|
+
server.tool(
|
|
164
|
+
"get_spend_summary",
|
|
165
|
+
"Get current spend summary \u2014 daily, weekly, monthly, and session totals",
|
|
166
|
+
{},
|
|
167
|
+
async () => {
|
|
168
|
+
if (!API_KEY) {
|
|
169
|
+
return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(`${API_BASE}/v1/spend`, {
|
|
173
|
+
headers: { "Authorization": `Bearer ${API_KEY}` },
|
|
174
|
+
signal: AbortSignal.timeout(5e3)
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
178
|
+
}
|
|
179
|
+
const data = await res.json();
|
|
180
|
+
const lines = [
|
|
181
|
+
`\u{1F4B0} Spend Summary (${data.key})`,
|
|
182
|
+
``,
|
|
183
|
+
`Today: $${data.daily.cost.toFixed(4)} (${data.daily.requests} requests)`,
|
|
184
|
+
`Week: $${data.weekly.cost.toFixed(4)} (${data.weekly.requests} requests)`,
|
|
185
|
+
`Month: $${data.monthly.cost.toFixed(4)} (${data.monthly.requests} requests)`
|
|
186
|
+
];
|
|
187
|
+
if (data.session) {
|
|
188
|
+
lines.push(`Session: $${data.session.cost.toFixed(4)} (${data.session.requests} requests) [${data.session.correlationId}]`);
|
|
189
|
+
}
|
|
190
|
+
if (data.dailyBudget) {
|
|
191
|
+
lines.push(``, `Budget: $${data.budgetRemaining.toFixed(2)} remaining of $${data.dailyBudget.toFixed(2)}/day`);
|
|
192
|
+
}
|
|
193
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return { content: [{ type: "text", text: `Error fetching spend: ${err.message}` }] };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
server.tool(
|
|
200
|
+
"costlens_suggest_model",
|
|
201
|
+
"Analyze a prompt and suggest the cheapest adequate model",
|
|
202
|
+
{
|
|
203
|
+
prompt: import_zod.z.string(),
|
|
204
|
+
currentModel: import_zod.z.string(),
|
|
205
|
+
provider: import_zod.z.string(),
|
|
206
|
+
messageCount: import_zod.z.number().optional(),
|
|
207
|
+
routingMode: import_zod.z.string().optional(),
|
|
208
|
+
isFirstMessage: import_zod.z.boolean().optional()
|
|
209
|
+
},
|
|
210
|
+
async (params) => {
|
|
211
|
+
const result = (0, import_classifier.classify)({
|
|
212
|
+
prompt: params.prompt,
|
|
213
|
+
currentModel: params.currentModel,
|
|
214
|
+
provider: params.provider,
|
|
215
|
+
messageCount: params.messageCount,
|
|
216
|
+
routingMode: params.routingMode || void 0,
|
|
217
|
+
isFirstMessage: params.isFirstMessage
|
|
218
|
+
});
|
|
219
|
+
if (API_KEY && result.suggestedModel) {
|
|
220
|
+
syncEvent(result, params).catch(() => {
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
productivityEnabled = null;
|
|
229
|
+
server.tool(
|
|
230
|
+
"start_session",
|
|
231
|
+
"Start a productivity tracking session",
|
|
232
|
+
{
|
|
233
|
+
task_description: import_zod.z.string().optional(),
|
|
234
|
+
branch: import_zod.z.string().optional(),
|
|
235
|
+
repo: import_zod.z.string().optional()
|
|
236
|
+
},
|
|
237
|
+
async (params) => {
|
|
238
|
+
const err = await ensureProductivity();
|
|
239
|
+
if (err) return { content: [{ type: "text", text: err }] };
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch(`${API_BASE}/v1/productivity/sessions`, {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
|
|
244
|
+
body: JSON.stringify(params),
|
|
245
|
+
signal: AbortSignal.timeout(5e3)
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
248
|
+
const data = await res.json();
|
|
249
|
+
if (data.data?.id) startHeartbeat(data.data.id);
|
|
250
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
251
|
+
} catch (e) {
|
|
252
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
server.tool(
|
|
257
|
+
"end_session",
|
|
258
|
+
"End a productivity tracking session",
|
|
259
|
+
{
|
|
260
|
+
session_id: import_zod.z.string(),
|
|
261
|
+
outcome: import_zod.z.enum(["completed", "abandoned", "paused"]).optional(),
|
|
262
|
+
artifacts: import_zod.z.array(import_zod.z.string()).optional()
|
|
263
|
+
},
|
|
264
|
+
async (params) => {
|
|
265
|
+
const err = await ensureProductivity();
|
|
266
|
+
if (err) return { content: [{ type: "text", text: err }] };
|
|
267
|
+
try {
|
|
268
|
+
const res = await fetch(`${API_BASE}/v1/productivity/sessions/${params.session_id}`, {
|
|
269
|
+
method: "PATCH",
|
|
270
|
+
headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
|
|
271
|
+
body: JSON.stringify({ outcome: params.outcome, artifacts: params.artifacts }),
|
|
272
|
+
signal: AbortSignal.timeout(5e3)
|
|
273
|
+
});
|
|
274
|
+
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
275
|
+
const data = await res.json();
|
|
276
|
+
stopHeartbeat();
|
|
277
|
+
activeSessionId = null;
|
|
278
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
279
|
+
} catch (e) {
|
|
280
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
server.tool(
|
|
285
|
+
"log_event",
|
|
286
|
+
"Log a productivity event within a session",
|
|
287
|
+
{
|
|
288
|
+
session_id: import_zod.z.string(),
|
|
289
|
+
event_type: import_zod.z.enum(["prompt", "commit", "pr_created", "test_run", "deploy", "file_edit"]),
|
|
290
|
+
metadata: import_zod.z.record(import_zod.z.any()).optional()
|
|
291
|
+
},
|
|
292
|
+
async (params) => {
|
|
293
|
+
if (!API_KEY) return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
|
|
294
|
+
if (productivityEnabled === null) {
|
|
295
|
+
productivityEnabled = await checkFeature("productivity_basic");
|
|
296
|
+
}
|
|
297
|
+
const hasFull = await checkFeature("productivity_full");
|
|
298
|
+
if (!hasFull) return { content: [{ type: "text", text: "Event logging requires Productivity Insights add-on." }] };
|
|
299
|
+
try {
|
|
300
|
+
const res = await fetch(`${API_BASE}/v1/productivity/events`, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
|
|
303
|
+
body: JSON.stringify(params),
|
|
304
|
+
signal: AbortSignal.timeout(5e3)
|
|
305
|
+
});
|
|
306
|
+
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
307
|
+
return { content: [{ type: "text", text: "Event logged." }] };
|
|
308
|
+
} catch (e) {
|
|
309
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
server.tool(
|
|
314
|
+
"get_productivity_summary",
|
|
315
|
+
"Get productivity summary for a time period",
|
|
316
|
+
{
|
|
317
|
+
period: import_zod.z.enum(["today", "week", "month"]).optional()
|
|
318
|
+
},
|
|
319
|
+
async (params) => {
|
|
320
|
+
const err = await ensureProductivity();
|
|
321
|
+
if (err) return { content: [{ type: "text", text: err }] };
|
|
322
|
+
try {
|
|
323
|
+
const period = params.period || "week";
|
|
324
|
+
const res = await fetch(`${API_BASE}/v1/productivity/summary?period=${period}`, {
|
|
325
|
+
headers: { "Authorization": `Bearer ${API_KEY}` },
|
|
326
|
+
signal: AbortSignal.timeout(5e3)
|
|
327
|
+
});
|
|
328
|
+
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
329
|
+
const data = await res.json();
|
|
330
|
+
const lines = [
|
|
331
|
+
`\u{1F4CA} Productivity Summary (${period})`,
|
|
332
|
+
``,
|
|
333
|
+
`Sessions: ${data.sessions}`,
|
|
334
|
+
`Time: ${data.total_time_minutes} min`,
|
|
335
|
+
`Tokens: ${data.tokens_used?.toLocaleString() || 0}`,
|
|
336
|
+
`Cost: $${data.cost?.toFixed(4) || "0.00"}`,
|
|
337
|
+
`Commits: ${data.commits || 0}`,
|
|
338
|
+
`PRs merged: ${data.prs_merged || 0}`
|
|
339
|
+
];
|
|
340
|
+
if (data.cost_per_commit) lines.push(`Cost/commit: $${data.cost_per_commit.toFixed(4)}`);
|
|
341
|
+
if (data.cost_per_pr) lines.push(`Cost/PR: $${data.cost_per_pr.toFixed(4)}`);
|
|
342
|
+
if (data.estimated_time_saved_hours) lines.push(`Time saved: ~${data.estimated_time_saved_hours.toFixed(1)}h`);
|
|
343
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
344
|
+
} catch (e) {
|
|
345
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
server.tool(
|
|
350
|
+
"get_github_metrics",
|
|
351
|
+
"Get GitHub PR metrics \u2014 PRs merged, review rounds, first-pass rate, cost per PR",
|
|
352
|
+
{
|
|
353
|
+
period: import_zod.z.enum(["today", "week", "month"]).optional()
|
|
354
|
+
},
|
|
355
|
+
async (params) => {
|
|
356
|
+
if (!API_KEY) {
|
|
357
|
+
return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const period = params.period || "week";
|
|
361
|
+
const res = await fetch(`${API_BASE}/v1/productivity/github?period=${period}`, {
|
|
362
|
+
headers: { "Authorization": `Bearer ${API_KEY}` },
|
|
363
|
+
signal: AbortSignal.timeout(5e3)
|
|
364
|
+
});
|
|
365
|
+
if (res.status === 404) {
|
|
366
|
+
return { content: [{ type: "text", text: "GitHub not connected. Connect in Settings \u2192 GitHub at costlens.dev" }] };
|
|
367
|
+
}
|
|
368
|
+
if (!res.ok) {
|
|
369
|
+
return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
370
|
+
}
|
|
371
|
+
const { data } = await res.json();
|
|
372
|
+
const lines = [
|
|
373
|
+
`GitHub Metrics (${period}):`,
|
|
374
|
+
`PRs merged: ${data.prsMerged}`,
|
|
375
|
+
`Avg review rounds: ${data.avgReviewRounds}`,
|
|
376
|
+
`First-pass rate: ${data.firstPassRate}%`,
|
|
377
|
+
`Avg time to merge: ${data.avgTimeToMergeHours}h`,
|
|
378
|
+
`Cost per PR: $${data.costPerPr}`
|
|
379
|
+
];
|
|
380
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
381
|
+
} catch (e) {
|
|
382
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
main().catch(console.error);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
3
389
|
|
|
4
390
|
// src/cli.ts
|
|
5
|
-
var import_http = require("http");
|
|
6
391
|
var import_child_process = require("child_process");
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
var
|
|
11
|
-
var
|
|
12
|
-
var
|
|
392
|
+
var import_fs2 = require("fs");
|
|
393
|
+
var import_os2 = require("os");
|
|
394
|
+
var import_path2 = require("path");
|
|
395
|
+
var import_crypto = __toESM(require("crypto"));
|
|
396
|
+
var CONFIG_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".costlens");
|
|
397
|
+
var CONFIG_FILE = (0, import_path2.join)(CONFIG_DIR, "config.json");
|
|
398
|
+
var API_BASE2 = process.env.COSTLENS_API_URL || "https://api.costlens.dev";
|
|
13
399
|
var APP_URL = process.env.COSTLENS_APP_URL || "https://costlens.dev";
|
|
14
400
|
function readConfig() {
|
|
15
401
|
try {
|
|
16
|
-
return JSON.parse((0,
|
|
402
|
+
return JSON.parse((0, import_fs2.readFileSync)(CONFIG_FILE, "utf-8"));
|
|
17
403
|
} catch {
|
|
18
404
|
return {};
|
|
19
405
|
}
|
|
20
406
|
}
|
|
21
407
|
function writeConfig(config) {
|
|
22
|
-
if (!(0,
|
|
23
|
-
(0,
|
|
408
|
+
if (!(0, import_fs2.existsSync)(CONFIG_DIR)) (0, import_fs2.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
409
|
+
(0, import_fs2.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
24
410
|
}
|
|
25
411
|
async function login() {
|
|
26
412
|
console.log("\u{1F511} CostLens \u2014 Authenticating...\n");
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
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}
|
|
413
|
+
const sessionId = import_crypto.default.randomBytes(16).toString("hex");
|
|
414
|
+
const authUrl = `${APP_URL}/cli-auth?session=${sessionId}`;
|
|
415
|
+
console.log(` Opening browser: ${authUrl}
|
|
37
416
|
`);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
console.log(` Opening browser: ${authUrl}
|
|
54
|
-
`);
|
|
55
|
-
const platform = process.platform;
|
|
417
|
+
const platform = process.platform;
|
|
418
|
+
try {
|
|
419
|
+
if (platform === "darwin") (0, import_child_process.execSync)(`open "${authUrl}"`);
|
|
420
|
+
else if (platform === "linux") (0, import_child_process.execSync)(`xdg-open "${authUrl}"`);
|
|
421
|
+
else if (platform === "win32") (0, import_child_process.execSync)(`start "${authUrl}"`);
|
|
422
|
+
else console.log(` Open this URL manually: ${authUrl}`);
|
|
423
|
+
} catch {
|
|
424
|
+
console.log(` Open this URL manually: ${authUrl}`);
|
|
425
|
+
}
|
|
426
|
+
console.log(" Waiting for authentication...");
|
|
427
|
+
const startTime = Date.now();
|
|
428
|
+
const TIMEOUT = 12e4;
|
|
429
|
+
const INTERVAL = 2e3;
|
|
430
|
+
while (Date.now() - startTime < TIMEOUT) {
|
|
431
|
+
await new Promise((r) => setTimeout(r, INTERVAL));
|
|
56
432
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
433
|
+
const res = await fetch(`${APP_URL}/api/cli-auth/poll?session=${sessionId}`, {
|
|
434
|
+
signal: AbortSignal.timeout(5e3)
|
|
435
|
+
});
|
|
436
|
+
if (res.ok) {
|
|
437
|
+
const data = await res.json();
|
|
438
|
+
if (data.key) {
|
|
439
|
+
writeConfig({ apiKey: data.key });
|
|
440
|
+
console.log("\n\u2713 Authenticated successfully");
|
|
441
|
+
console.log(` Key saved to ${CONFIG_FILE}
|
|
442
|
+
`);
|
|
443
|
+
if (process.argv[2] === "setup") {
|
|
444
|
+
init();
|
|
445
|
+
} else {
|
|
446
|
+
process.exit(0);
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
61
451
|
} catch {
|
|
62
|
-
console.log(` Open this URL manually: ${authUrl}`);
|
|
63
452
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log("\n\u2717 Timed out. Try again.");
|
|
68
|
-
server.close();
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}, 12e4);
|
|
453
|
+
}
|
|
454
|
+
console.log("\n\u2717 Timed out. Try again.");
|
|
455
|
+
process.exit(1);
|
|
71
456
|
}
|
|
72
457
|
function init() {
|
|
73
458
|
const config = readConfig();
|
|
@@ -94,21 +479,21 @@ function init() {
|
|
|
94
479
|
console.log(" Claude Code: ~/.claude/mcp_servers.json");
|
|
95
480
|
console.log(" VS Code: .vscode/mcp.json");
|
|
96
481
|
console.log("");
|
|
97
|
-
const kiroPath = (0,
|
|
98
|
-
const cursorPath = (0,
|
|
99
|
-
const claudePath = (0,
|
|
482
|
+
const kiroPath = (0, import_path2.join)((0, import_os2.homedir)(), ".kiro", "settings", "mcp.json");
|
|
483
|
+
const cursorPath = (0, import_path2.join)((0, import_os2.homedir)(), ".cursor", "mcp.json");
|
|
484
|
+
const claudePath = (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "mcp_servers.json");
|
|
100
485
|
const paths = [
|
|
101
486
|
{ name: "Kiro", path: kiroPath },
|
|
102
487
|
{ name: "Cursor", path: cursorPath },
|
|
103
488
|
{ name: "Claude Code", path: claudePath }
|
|
104
489
|
];
|
|
105
490
|
for (const { name, path } of paths) {
|
|
106
|
-
if ((0,
|
|
491
|
+
if ((0, import_fs2.existsSync)(path)) {
|
|
107
492
|
try {
|
|
108
|
-
const existing = JSON.parse((0,
|
|
493
|
+
const existing = JSON.parse((0, import_fs2.readFileSync)(path, "utf-8"));
|
|
109
494
|
if (!existing.mcpServers?.costlens) {
|
|
110
495
|
existing.mcpServers = { ...existing.mcpServers, ...mcpConfig.mcpServers };
|
|
111
|
-
(0,
|
|
496
|
+
(0, import_fs2.writeFileSync)(path, JSON.stringify(existing, null, 2));
|
|
112
497
|
console.log(` \u2713 Auto-configured ${name} (${path})`);
|
|
113
498
|
} else {
|
|
114
499
|
console.log(` \xB7 ${name} already configured`);
|
|
@@ -137,7 +522,7 @@ async function status() {
|
|
|
137
522
|
}
|
|
138
523
|
console.log("\u{1F4CA} CostLens Status\n");
|
|
139
524
|
try {
|
|
140
|
-
const res = await fetch(`${
|
|
525
|
+
const res = await fetch(`${API_BASE2}/v1/spend`, {
|
|
141
526
|
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
142
527
|
signal: AbortSignal.timeout(5e3)
|
|
143
528
|
});
|
|
@@ -161,10 +546,8 @@ if (command === "login") login();
|
|
|
161
546
|
else if (command === "init") init();
|
|
162
547
|
else if (command === "status") status();
|
|
163
548
|
else if (command === "setup") setup();
|
|
164
|
-
else
|
|
165
|
-
|
|
166
|
-
console.log("Available: setup, login, init, status");
|
|
167
|
-
process.exit(1);
|
|
549
|
+
else {
|
|
550
|
+
init_index();
|
|
168
551
|
}
|
|
169
552
|
async function setup() {
|
|
170
553
|
console.log("\u{1F527} CostLens \u2014 One-step setup\n");
|
package/package.json
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@costlens/mcp-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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
|
-
"bin":
|
|
8
|
-
"costlens-mcp": "./dist/index.js",
|
|
9
|
-
"costlens": "./dist/cli.js"
|
|
10
|
-
},
|
|
7
|
+
"bin": "./dist/cli.js",
|
|
11
8
|
"scripts": {
|
|
12
9
|
"build": "tsup src/index.ts src/cli.ts --format cjs --clean",
|
|
13
10
|
"test": "echo 'no tests yet'"
|