@costlens/mcp-server 0.4.2 → 0.5.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.
- package/dist/cli.js +279 -170
- package/dist/index.js +110 -110
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
9
|
var __esm = (fn, res) => function __init() {
|
|
8
10
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
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
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -29,31 +39,41 @@ function resolveApiKey() {
|
|
|
29
39
|
return void 0;
|
|
30
40
|
}
|
|
31
41
|
}
|
|
42
|
+
async function apiFetch(path, opts = {}) {
|
|
43
|
+
const headers = { "Authorization": `Bearer ${API_KEY}`, ...opts.headers };
|
|
44
|
+
if (opts.body) headers["Content-Type"] = "application/json";
|
|
45
|
+
return fetch(`${API_BASE}${path}`, { ...opts, headers, signal: AbortSignal.timeout(5e3) });
|
|
46
|
+
}
|
|
32
47
|
async function loadPricing() {
|
|
33
48
|
try {
|
|
34
|
-
const
|
|
35
|
-
if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`;
|
|
36
|
-
const res = await fetch(`${API_BASE}/v1/pricing`, { headers, signal: AbortSignal.timeout(5e3) });
|
|
49
|
+
const res = await apiFetch("/v1/pricing");
|
|
37
50
|
if (res.ok) {
|
|
38
51
|
const data = await res.json();
|
|
39
|
-
for (const m of data.models) {
|
|
40
|
-
import_classifier.PRICING[m.model] = { input: m.input, output: m.output };
|
|
41
|
-
}
|
|
52
|
+
for (const m of data.models) import_classifier.PRICING[m.model] = { input: m.input, output: m.output };
|
|
42
53
|
}
|
|
43
54
|
} catch {
|
|
44
55
|
}
|
|
45
56
|
}
|
|
57
|
+
async function loadKeySettings() {
|
|
58
|
+
if (!API_KEY) return;
|
|
59
|
+
try {
|
|
60
|
+
const res = await apiFetch("/v1/plan");
|
|
61
|
+
if (!res.ok) return;
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
trackingMode = data.keySettings?.trackingMode || "manual";
|
|
64
|
+
productivityEnabled = data.features?.includes("productivity_basic") ?? false;
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
46
68
|
function startHeartbeat(sessionId) {
|
|
47
69
|
stopHeartbeat();
|
|
48
70
|
activeSessionId = sessionId;
|
|
49
71
|
heartbeatInterval = setInterval(async () => {
|
|
50
72
|
if (!activeSessionId || !API_KEY) return;
|
|
51
73
|
try {
|
|
52
|
-
const res = await
|
|
74
|
+
const res = await apiFetch("/v1/productivity/heartbeat", {
|
|
53
75
|
method: "POST",
|
|
54
|
-
|
|
55
|
-
body: JSON.stringify({ session_id: activeSessionId }),
|
|
56
|
-
signal: AbortSignal.timeout(5e3)
|
|
76
|
+
body: JSON.stringify({ session_id: activeSessionId })
|
|
57
77
|
});
|
|
58
78
|
if (res.status === 410) {
|
|
59
79
|
activeSessionId = null;
|
|
@@ -69,58 +89,65 @@ function stopHeartbeat() {
|
|
|
69
89
|
heartbeatInterval = null;
|
|
70
90
|
}
|
|
71
91
|
}
|
|
72
|
-
async function
|
|
92
|
+
async function ensureAutoSession() {
|
|
93
|
+
if (trackingMode !== "auto" || !API_KEY || !productivityEnabled || activeSessionId) return;
|
|
73
94
|
try {
|
|
74
|
-
await
|
|
95
|
+
const res = await apiFetch("/v1/productivity/sessions", {
|
|
75
96
|
method: "POST",
|
|
76
|
-
|
|
77
|
-
body: JSON.stringify({
|
|
78
|
-
complexity: result.level,
|
|
79
|
-
confidence: result.confidence,
|
|
80
|
-
suggestedModel: result.suggestedModel,
|
|
81
|
-
currentModel: params.currentModel,
|
|
82
|
-
provider: params.provider,
|
|
83
|
-
staticScore: result.signals.staticScore
|
|
84
|
-
}),
|
|
85
|
-
signal: AbortSignal.timeout(5e3)
|
|
97
|
+
body: JSON.stringify({ task_description: "Auto-tracked MCP session" })
|
|
86
98
|
});
|
|
99
|
+
if (res.ok) {
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
if (data.data?.id) startHeartbeat(data.data.id);
|
|
102
|
+
}
|
|
87
103
|
} catch {
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
|
-
async function
|
|
91
|
-
if (!API_KEY) return
|
|
106
|
+
async function autoLogEvent(eventType, metadata) {
|
|
107
|
+
if (trackingMode !== "auto" || !activeSessionId || !API_KEY) return;
|
|
92
108
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
109
|
+
await apiFetch("/v1/productivity/events", {
|
|
110
|
+
method: "POST",
|
|
111
|
+
body: JSON.stringify({ session_id: activeSessionId, event_type: eventType, metadata })
|
|
96
112
|
});
|
|
97
|
-
if (!res.ok) return false;
|
|
98
|
-
const plan = await res.json();
|
|
99
|
-
return plan.features?.includes(feature) ?? false;
|
|
100
113
|
} catch {
|
|
101
|
-
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function onToolCall(toolName) {
|
|
117
|
+
toolCallCount++;
|
|
118
|
+
if (trackingMode === "auto") {
|
|
119
|
+
await ensureAutoSession();
|
|
102
120
|
}
|
|
103
121
|
}
|
|
104
122
|
async function ensureProductivity() {
|
|
105
123
|
if (!API_KEY) return "Not connected to CostLens. Run: npx @costlens/mcp-server setup";
|
|
106
124
|
if (productivityEnabled === null) {
|
|
107
|
-
|
|
125
|
+
await loadKeySettings();
|
|
108
126
|
}
|
|
109
127
|
if (!productivityEnabled) return "Productivity tracking requires Business plan or Productivity add-on.";
|
|
110
128
|
return null;
|
|
111
129
|
}
|
|
130
|
+
async function checkFeature(feature) {
|
|
131
|
+
if (!API_KEY) return false;
|
|
132
|
+
try {
|
|
133
|
+
const res = await apiFetch("/v1/plan");
|
|
134
|
+
if (!res.ok) return false;
|
|
135
|
+
const plan = await res.json();
|
|
136
|
+
return plan.features?.includes(feature) ?? false;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
112
141
|
async function main() {
|
|
113
|
-
await loadPricing();
|
|
142
|
+
await Promise.all([loadPricing(), loadKeySettings()]);
|
|
114
143
|
const transport = new import_stdio.StdioServerTransport();
|
|
115
144
|
await server.connect(transport);
|
|
116
145
|
const cleanup = async () => {
|
|
117
146
|
if (activeSessionId && API_KEY) {
|
|
118
147
|
try {
|
|
119
|
-
await
|
|
148
|
+
await apiFetch(`/v1/productivity/sessions/${activeSessionId}`, {
|
|
120
149
|
method: "PATCH",
|
|
121
|
-
|
|
122
|
-
body: JSON.stringify({ outcome: "completed" }),
|
|
123
|
-
signal: AbortSignal.timeout(3e3)
|
|
150
|
+
body: JSON.stringify({ outcome: "completed", metadata: { toolCalls: toolCallCount } })
|
|
124
151
|
});
|
|
125
152
|
} catch {
|
|
126
153
|
}
|
|
@@ -131,7 +158,7 @@ async function main() {
|
|
|
131
158
|
process.on("SIGTERM", cleanup);
|
|
132
159
|
process.on("SIGINT", cleanup);
|
|
133
160
|
}
|
|
134
|
-
var import_mcp, import_stdio, import_zod, import_classifier, import_fs, import_path, import_os, API_KEY, API_BASE,
|
|
161
|
+
var import_mcp, import_stdio, import_zod, import_classifier, import_fs, import_path, import_os, API_KEY, API_BASE, activeSessionId, heartbeatInterval, trackingMode, productivityEnabled, toolCallCount, server;
|
|
135
162
|
var init_index = __esm({
|
|
136
163
|
"src/index.ts"() {
|
|
137
164
|
"use strict";
|
|
@@ -144,42 +171,32 @@ var init_index = __esm({
|
|
|
144
171
|
import_os = require("os");
|
|
145
172
|
API_KEY = resolveApiKey();
|
|
146
173
|
API_BASE = process.env.COSTLENS_API_URL || "https://api.costlens.dev";
|
|
147
|
-
server = new import_mcp.McpServer({
|
|
148
|
-
name: "@lens360/mcp-server",
|
|
149
|
-
version: "0.3.1"
|
|
150
|
-
});
|
|
151
174
|
activeSessionId = null;
|
|
152
175
|
heartbeatInterval = null;
|
|
176
|
+
trackingMode = "manual";
|
|
177
|
+
productivityEnabled = null;
|
|
178
|
+
toolCallCount = 0;
|
|
179
|
+
server = new import_mcp.McpServer({ name: "@lens360/mcp-server", version: "0.4.0" });
|
|
153
180
|
server.tool(
|
|
154
181
|
"get_spend_summary",
|
|
155
182
|
"Get current spend summary \u2014 daily, weekly, monthly, and session totals",
|
|
156
183
|
{},
|
|
157
184
|
async () => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
185
|
+
await onToolCall("get_spend_summary");
|
|
186
|
+
if (!API_KEY) return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
|
|
161
187
|
try {
|
|
162
|
-
const res = await
|
|
163
|
-
|
|
164
|
-
signal: AbortSignal.timeout(5e3)
|
|
165
|
-
});
|
|
166
|
-
if (!res.ok) {
|
|
167
|
-
return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
168
|
-
}
|
|
188
|
+
const res = await apiFetch("/v1/spend");
|
|
189
|
+
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
169
190
|
const data = await res.json();
|
|
170
191
|
const lines = [
|
|
171
|
-
|
|
192
|
+
`Spend Summary (${data.key})`,
|
|
172
193
|
``,
|
|
173
194
|
`Today: $${data.daily.cost.toFixed(4)} (${data.daily.requests} requests)`,
|
|
174
195
|
`Week: $${data.weekly.cost.toFixed(4)} (${data.weekly.requests} requests)`,
|
|
175
196
|
`Month: $${data.monthly.cost.toFixed(4)} (${data.monthly.requests} requests)`
|
|
176
197
|
];
|
|
177
|
-
if (data.session) {
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
if (data.dailyBudget) {
|
|
181
|
-
lines.push(``, `Budget: $${data.budgetRemaining.toFixed(2)} remaining of $${data.dailyBudget.toFixed(2)}/day`);
|
|
182
|
-
}
|
|
198
|
+
if (data.session) lines.push(`Session: $${data.session.cost.toFixed(4)} (${data.session.requests} requests) [${data.session.correlationId}]`);
|
|
199
|
+
if (data.dailyBudget) lines.push(``, `Budget: $${data.budgetRemaining.toFixed(2)} remaining of $${data.dailyBudget.toFixed(2)}/day`);
|
|
183
200
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
184
201
|
} catch (err) {
|
|
185
202
|
return { content: [{ type: "text", text: `Error fetching spend: ${err.message}` }] };
|
|
@@ -198,6 +215,7 @@ var init_index = __esm({
|
|
|
198
215
|
isFirstMessage: import_zod.z.boolean().optional()
|
|
199
216
|
},
|
|
200
217
|
async (params) => {
|
|
218
|
+
await onToolCall("costlens_suggest_model");
|
|
201
219
|
const result = (0, import_classifier.classify)({
|
|
202
220
|
prompt: params.prompt,
|
|
203
221
|
currentModel: params.currentModel,
|
|
@@ -207,15 +225,27 @@ var init_index = __esm({
|
|
|
207
225
|
isFirstMessage: params.isFirstMessage
|
|
208
226
|
});
|
|
209
227
|
if (API_KEY && result.suggestedModel) {
|
|
210
|
-
|
|
228
|
+
apiFetch("/v1/classifier/events", {
|
|
229
|
+
method: "POST",
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
complexity: result.level,
|
|
232
|
+
confidence: result.confidence,
|
|
233
|
+
suggestedModel: result.suggestedModel,
|
|
234
|
+
currentModel: params.currentModel,
|
|
235
|
+
provider: params.provider,
|
|
236
|
+
staticScore: result.signals.staticScore
|
|
237
|
+
})
|
|
238
|
+
}).catch(() => {
|
|
239
|
+
});
|
|
240
|
+
autoLogEvent("prompt", {
|
|
241
|
+
complexity: result.level,
|
|
242
|
+
model: result.suggestedModel,
|
|
243
|
+
provider: params.provider
|
|
211
244
|
});
|
|
212
245
|
}
|
|
213
|
-
return {
|
|
214
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
215
|
-
};
|
|
246
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
216
247
|
}
|
|
217
248
|
);
|
|
218
|
-
productivityEnabled = null;
|
|
219
249
|
server.tool(
|
|
220
250
|
"start_session",
|
|
221
251
|
"Start a productivity tracking session",
|
|
@@ -225,14 +255,13 @@ var init_index = __esm({
|
|
|
225
255
|
repo: import_zod.z.string().optional()
|
|
226
256
|
},
|
|
227
257
|
async (params) => {
|
|
258
|
+
await onToolCall("start_session");
|
|
228
259
|
const err = await ensureProductivity();
|
|
229
260
|
if (err) return { content: [{ type: "text", text: err }] };
|
|
230
261
|
try {
|
|
231
|
-
const res = await
|
|
262
|
+
const res = await apiFetch("/v1/productivity/sessions", {
|
|
232
263
|
method: "POST",
|
|
233
|
-
|
|
234
|
-
body: JSON.stringify(params),
|
|
235
|
-
signal: AbortSignal.timeout(5e3)
|
|
264
|
+
body: JSON.stringify(params)
|
|
236
265
|
});
|
|
237
266
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
238
267
|
const data = await res.json();
|
|
@@ -252,14 +281,13 @@ var init_index = __esm({
|
|
|
252
281
|
artifacts: import_zod.z.array(import_zod.z.string()).optional()
|
|
253
282
|
},
|
|
254
283
|
async (params) => {
|
|
284
|
+
await onToolCall("end_session");
|
|
255
285
|
const err = await ensureProductivity();
|
|
256
286
|
if (err) return { content: [{ type: "text", text: err }] };
|
|
257
287
|
try {
|
|
258
|
-
const res = await
|
|
288
|
+
const res = await apiFetch(`/v1/productivity/sessions/${params.session_id}`, {
|
|
259
289
|
method: "PATCH",
|
|
260
|
-
|
|
261
|
-
body: JSON.stringify({ outcome: params.outcome, artifacts: params.artifacts }),
|
|
262
|
-
signal: AbortSignal.timeout(5e3)
|
|
290
|
+
body: JSON.stringify({ outcome: params.outcome, artifacts: params.artifacts })
|
|
263
291
|
});
|
|
264
292
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
265
293
|
const data = await res.json();
|
|
@@ -280,18 +308,14 @@ var init_index = __esm({
|
|
|
280
308
|
metadata: import_zod.z.record(import_zod.z.any()).optional()
|
|
281
309
|
},
|
|
282
310
|
async (params) => {
|
|
311
|
+
await onToolCall("log_event");
|
|
283
312
|
if (!API_KEY) return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
|
|
284
|
-
if (productivityEnabled === null) {
|
|
285
|
-
productivityEnabled = await checkFeature("productivity_basic");
|
|
286
|
-
}
|
|
287
313
|
const hasFull = await checkFeature("productivity_full");
|
|
288
314
|
if (!hasFull) return { content: [{ type: "text", text: "Event logging requires Productivity Insights add-on." }] };
|
|
289
315
|
try {
|
|
290
|
-
const res = await
|
|
316
|
+
const res = await apiFetch("/v1/productivity/events", {
|
|
291
317
|
method: "POST",
|
|
292
|
-
|
|
293
|
-
body: JSON.stringify(params),
|
|
294
|
-
signal: AbortSignal.timeout(5e3)
|
|
318
|
+
body: JSON.stringify(params)
|
|
295
319
|
});
|
|
296
320
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
297
321
|
return { content: [{ type: "text", text: "Event logged." }] };
|
|
@@ -303,22 +327,18 @@ var init_index = __esm({
|
|
|
303
327
|
server.tool(
|
|
304
328
|
"get_productivity_summary",
|
|
305
329
|
"Get productivity summary for a time period",
|
|
306
|
-
{
|
|
307
|
-
period: import_zod.z.enum(["today", "week", "month"]).optional()
|
|
308
|
-
},
|
|
330
|
+
{ period: import_zod.z.enum(["today", "week", "month"]).optional() },
|
|
309
331
|
async (params) => {
|
|
332
|
+
await onToolCall("get_productivity_summary");
|
|
310
333
|
const err = await ensureProductivity();
|
|
311
334
|
if (err) return { content: [{ type: "text", text: err }] };
|
|
312
335
|
try {
|
|
313
336
|
const period = params.period || "week";
|
|
314
|
-
const res = await
|
|
315
|
-
headers: { "Authorization": `Bearer ${API_KEY}` },
|
|
316
|
-
signal: AbortSignal.timeout(5e3)
|
|
317
|
-
});
|
|
337
|
+
const res = await apiFetch(`/v1/productivity/summary?period=${period}`);
|
|
318
338
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
319
339
|
const data = await res.json();
|
|
320
340
|
const lines = [
|
|
321
|
-
|
|
341
|
+
`Productivity Summary (${period})`,
|
|
322
342
|
``,
|
|
323
343
|
`Sessions: ${data.sessions}`,
|
|
324
344
|
`Time: ${data.total_time_minutes} min`,
|
|
@@ -339,25 +359,15 @@ var init_index = __esm({
|
|
|
339
359
|
server.tool(
|
|
340
360
|
"get_github_metrics",
|
|
341
361
|
"Get GitHub PR metrics \u2014 PRs merged, review rounds, first-pass rate, cost per PR",
|
|
342
|
-
{
|
|
343
|
-
period: import_zod.z.enum(["today", "week", "month"]).optional()
|
|
344
|
-
},
|
|
362
|
+
{ period: import_zod.z.enum(["today", "week", "month"]).optional() },
|
|
345
363
|
async (params) => {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
364
|
+
await onToolCall("get_github_metrics");
|
|
365
|
+
if (!API_KEY) return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
|
|
349
366
|
try {
|
|
350
367
|
const period = params.period || "week";
|
|
351
|
-
const res = await
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
});
|
|
355
|
-
if (res.status === 404) {
|
|
356
|
-
return { content: [{ type: "text", text: "GitHub not connected. Connect in Settings \u2192 GitHub at costlens.dev" }] };
|
|
357
|
-
}
|
|
358
|
-
if (!res.ok) {
|
|
359
|
-
return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
360
|
-
}
|
|
368
|
+
const res = await apiFetch(`/v1/productivity/github?period=${period}`);
|
|
369
|
+
if (res.status === 404) return { content: [{ type: "text", text: "GitHub not connected. Connect in Settings \u2192 GitHub at costlens.dev" }] };
|
|
370
|
+
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
361
371
|
const { data } = await res.json();
|
|
362
372
|
const lines = [
|
|
363
373
|
`GitHub Metrics (${period}):`,
|
|
@@ -378,11 +388,11 @@ var init_index = __esm({
|
|
|
378
388
|
});
|
|
379
389
|
|
|
380
390
|
// src/cli.ts
|
|
381
|
-
var import_http = require("http");
|
|
382
391
|
var import_child_process = require("child_process");
|
|
383
392
|
var import_fs2 = require("fs");
|
|
384
393
|
var import_os2 = require("os");
|
|
385
394
|
var import_path2 = require("path");
|
|
395
|
+
var import_crypto = __toESM(require("crypto"));
|
|
386
396
|
var CONFIG_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".costlens");
|
|
387
397
|
var CONFIG_FILE = (0, import_path2.join)(CONFIG_DIR, "config.json");
|
|
388
398
|
var API_BASE2 = process.env.COSTLENS_API_URL || "https://api.costlens.dev";
|
|
@@ -398,57 +408,148 @@ function writeConfig(config) {
|
|
|
398
408
|
if (!(0, import_fs2.existsSync)(CONFIG_DIR)) (0, import_fs2.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
399
409
|
(0, import_fs2.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
400
410
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
411
|
+
var HOOK_MARKER = "# costlens-hook";
|
|
412
|
+
var POST_COMMIT_HOOK = `#!/bin/sh
|
|
413
|
+
${HOOK_MARKER}
|
|
414
|
+
# CostLens \u2014 tracks commits during AI sessions (metadata only, no code content)
|
|
415
|
+
# Sends: hash, message, files changed, insertions, deletions, branch, timestamp
|
|
416
|
+
# Remove this file to uninstall: .git/hooks/post-commit
|
|
417
|
+
|
|
418
|
+
COSTLENS_KEY=$(cat ~/.costlens/config.json 2>/dev/null | grep -o '"apiKey"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
|
|
419
|
+
if [ -z "$COSTLENS_KEY" ]; then exit 0; fi
|
|
420
|
+
|
|
421
|
+
HASH=$(git rev-parse HEAD)
|
|
422
|
+
MSG=$(git log -1 --format=%s | head -c 200)
|
|
423
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
424
|
+
FILES=$(git diff --numstat HEAD~1 HEAD 2>/dev/null | wc -l | tr -d ' ')
|
|
425
|
+
STATS=$(git diff --shortstat HEAD~1 HEAD 2>/dev/null)
|
|
426
|
+
INSERTIONS=$(echo "$STATS" | grep -o '[0-9]* insertion' | grep -o '[0-9]*' || echo "0")
|
|
427
|
+
DELETIONS=$(echo "$STATS" | grep -o '[0-9]* deletion' | grep -o '[0-9]*' || echo "0")
|
|
428
|
+
|
|
429
|
+
curl -s -X POST "\${COSTLENS_API_URL:-https://api.costlens.dev}/v1/hooks/commit" \\
|
|
430
|
+
-H "Authorization: Bearer $COSTLENS_KEY" \\
|
|
431
|
+
-H "Content-Type: application/json" \\
|
|
432
|
+
-d "{\\"hash\\":\\"$HASH\\",\\"message\\":\\"$MSG\\",\\"branch\\":\\"$BRANCH\\",\\"filesChanged\\":$FILES,\\"insertions\\":\${INSERTIONS:-0},\\"deletions\\":\${DELETIONS:-0}}" \\
|
|
433
|
+
--max-time 3 > /dev/null 2>&1 &
|
|
434
|
+
|
|
435
|
+
exit 0
|
|
436
|
+
`;
|
|
437
|
+
function findGitRepos() {
|
|
438
|
+
const cwd = process.cwd();
|
|
439
|
+
const repos = [];
|
|
440
|
+
if ((0, import_fs2.existsSync)((0, import_path2.join)(cwd, ".git"))) repos.push(cwd);
|
|
441
|
+
const searchDirs = [
|
|
442
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "Desktop"),
|
|
443
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "Projects"),
|
|
444
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "projects"),
|
|
445
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "dev"),
|
|
446
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "code"),
|
|
447
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "workspace"),
|
|
448
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "Workspace"),
|
|
449
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "Desktop", "Workspace")
|
|
450
|
+
];
|
|
451
|
+
for (const dir of searchDirs) {
|
|
452
|
+
if (!(0, import_fs2.existsSync)(dir)) continue;
|
|
453
|
+
try {
|
|
454
|
+
const entries = require("fs").readdirSync(dir, { withFileTypes: true });
|
|
455
|
+
for (const entry of entries) {
|
|
456
|
+
if (entry.isDirectory() && (0, import_fs2.existsSync)((0, import_path2.join)(dir, entry.name, ".git"))) {
|
|
457
|
+
repos.push((0, import_path2.join)(dir, entry.name));
|
|
420
458
|
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
res.writeHead(400);
|
|
424
|
-
res.end("Missing key");
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
425
461
|
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
462
|
+
}
|
|
463
|
+
return [...new Set(repos)];
|
|
464
|
+
}
|
|
465
|
+
function installGitHook(repoPath) {
|
|
466
|
+
const hooksDir = (0, import_path2.join)(repoPath, ".git", "hooks");
|
|
467
|
+
const hookPath = (0, import_path2.join)(hooksDir, "post-commit");
|
|
468
|
+
if ((0, import_fs2.existsSync)(hookPath)) {
|
|
469
|
+
try {
|
|
470
|
+
const content = (0, import_fs2.readFileSync)(hookPath, "utf-8");
|
|
471
|
+
if (content.includes(HOOK_MARKER)) return "exists";
|
|
472
|
+
return "skipped";
|
|
473
|
+
} catch {
|
|
474
|
+
return "skipped";
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
if (!(0, import_fs2.existsSync)(hooksDir)) (0, import_fs2.mkdirSync)(hooksDir, { recursive: true });
|
|
479
|
+
(0, import_fs2.writeFileSync)(hookPath, POST_COMMIT_HOOK);
|
|
480
|
+
(0, import_fs2.chmodSync)(hookPath, "755");
|
|
481
|
+
return "installed";
|
|
482
|
+
} catch {
|
|
483
|
+
return "skipped";
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function installHooks() {
|
|
487
|
+
const repos = findGitRepos();
|
|
488
|
+
if (repos.length === 0) {
|
|
489
|
+
console.log(" \xB7 No git repos detected\n");
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
console.log(`
|
|
493
|
+
Git hooks (tracks commits during AI sessions):
|
|
430
494
|
`);
|
|
431
|
-
|
|
495
|
+
for (const repo of repos.slice(0, 10)) {
|
|
496
|
+
const name = repo.split("/").pop();
|
|
497
|
+
const result = installGitHook(repo);
|
|
498
|
+
if (result === "installed") console.log(` + ${name} \u2014 hook installed`);
|
|
499
|
+
else if (result === "exists") console.log(` \xB7 ${name} \u2014 already installed`);
|
|
500
|
+
else console.log(` \xB7 ${name} \u2014 skipped (existing hook)`);
|
|
501
|
+
}
|
|
502
|
+
console.log(`
|
|
503
|
+
Hooks send: commit hash, message, branch, files changed.`);
|
|
504
|
+
console.log(` No code content is ever sent. Remove .git/hooks/post-commit to uninstall.`);
|
|
505
|
+
}
|
|
506
|
+
async function login() {
|
|
507
|
+
console.log(" CostLens \u2014 Authenticating...\n");
|
|
508
|
+
const sessionId = import_crypto.default.randomBytes(16).toString("hex");
|
|
509
|
+
const authUrl = `${APP_URL}/cli-auth?session=${sessionId}`;
|
|
510
|
+
console.log(` Opening browser: ${authUrl}
|
|
511
|
+
`);
|
|
512
|
+
const platform = process.platform;
|
|
513
|
+
try {
|
|
514
|
+
if (platform === "darwin") (0, import_child_process.execSync)(`open "${authUrl}"`);
|
|
515
|
+
else if (platform === "linux") (0, import_child_process.execSync)(`xdg-open "${authUrl}"`);
|
|
516
|
+
else if (platform === "win32") (0, import_child_process.execSync)(`start "${authUrl}"`);
|
|
517
|
+
else console.log(` Open this URL manually: ${authUrl}`);
|
|
518
|
+
} catch {
|
|
519
|
+
console.log(` Open this URL manually: ${authUrl}`);
|
|
520
|
+
}
|
|
521
|
+
console.log(" Waiting for authentication...");
|
|
522
|
+
const startTime = Date.now();
|
|
523
|
+
const TIMEOUT = 12e4;
|
|
524
|
+
const INTERVAL = 2e3;
|
|
525
|
+
while (Date.now() - startTime < TIMEOUT) {
|
|
526
|
+
await new Promise((r) => setTimeout(r, INTERVAL));
|
|
432
527
|
try {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
528
|
+
const res = await fetch(`${APP_URL}/api/cli-auth/poll?session=${sessionId}`, {
|
|
529
|
+
signal: AbortSignal.timeout(5e3)
|
|
530
|
+
});
|
|
531
|
+
if (res.ok) {
|
|
532
|
+
const data = await res.json();
|
|
533
|
+
if (data.key) {
|
|
534
|
+
writeConfig({ apiKey: data.key });
|
|
535
|
+
console.log("\n Authenticated successfully");
|
|
536
|
+
console.log(` Key saved to ${CONFIG_FILE}
|
|
537
|
+
`);
|
|
538
|
+
if (process.argv[2] === "setup") init();
|
|
539
|
+
else process.exit(0);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
437
543
|
} catch {
|
|
438
|
-
console.log(` Open this URL manually: ${authUrl}`);
|
|
439
544
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
console.log("\n\u2717 Timed out. Try again.");
|
|
444
|
-
server2.close();
|
|
445
|
-
process.exit(1);
|
|
446
|
-
}, 12e4);
|
|
545
|
+
}
|
|
546
|
+
console.log("\n Timed out. Try again.");
|
|
547
|
+
process.exit(1);
|
|
447
548
|
}
|
|
448
549
|
function init() {
|
|
449
550
|
const config = readConfig();
|
|
450
551
|
if (!config.apiKey) {
|
|
451
|
-
console.log("
|
|
552
|
+
console.log(" Not authenticated. Run: npx @costlens/mcp-server login\n");
|
|
452
553
|
process.exit(1);
|
|
453
554
|
}
|
|
454
555
|
const mcpConfig = {
|
|
@@ -456,20 +557,10 @@ function init() {
|
|
|
456
557
|
costlens: {
|
|
457
558
|
command: "npx",
|
|
458
559
|
args: ["-y", "@costlens/mcp-server"],
|
|
459
|
-
env: {
|
|
460
|
-
COSTLENS_MCP_KEY: config.apiKey
|
|
461
|
-
}
|
|
560
|
+
env: { COSTLENS_MCP_KEY: config.apiKey }
|
|
462
561
|
}
|
|
463
562
|
}
|
|
464
563
|
};
|
|
465
|
-
console.log("\u{1F4CB} MCP Configuration:\n");
|
|
466
|
-
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
467
|
-
console.log("\n Add this to your agent config file:\n");
|
|
468
|
-
console.log(" Kiro: ~/.kiro/settings/mcp.json");
|
|
469
|
-
console.log(" Cursor: ~/.cursor/mcp.json");
|
|
470
|
-
console.log(" Claude Code: ~/.claude/mcp_servers.json");
|
|
471
|
-
console.log(" VS Code: .vscode/mcp.json");
|
|
472
|
-
console.log("");
|
|
473
564
|
const kiroPath = (0, import_path2.join)((0, import_os2.homedir)(), ".kiro", "settings", "mcp.json");
|
|
474
565
|
const cursorPath = (0, import_path2.join)((0, import_os2.homedir)(), ".cursor", "mcp.json");
|
|
475
566
|
const claudePath = (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "mcp_servers.json");
|
|
@@ -478,6 +569,8 @@ function init() {
|
|
|
478
569
|
{ name: "Cursor", path: cursorPath },
|
|
479
570
|
{ name: "Claude Code", path: claudePath }
|
|
480
571
|
];
|
|
572
|
+
console.log(" Agent configuration:\n");
|
|
573
|
+
let configured = false;
|
|
481
574
|
for (const { name, path } of paths) {
|
|
482
575
|
if ((0, import_fs2.existsSync)(path)) {
|
|
483
576
|
try {
|
|
@@ -485,50 +578,63 @@ function init() {
|
|
|
485
578
|
if (!existing.mcpServers?.costlens) {
|
|
486
579
|
existing.mcpServers = { ...existing.mcpServers, ...mcpConfig.mcpServers };
|
|
487
580
|
(0, import_fs2.writeFileSync)(path, JSON.stringify(existing, null, 2));
|
|
488
|
-
console.log(`
|
|
581
|
+
console.log(` + ${name} \u2014 configured`);
|
|
582
|
+
configured = true;
|
|
489
583
|
} else {
|
|
490
|
-
console.log(` \xB7 ${name} already configured`);
|
|
584
|
+
console.log(` \xB7 ${name} \u2014 already configured`);
|
|
585
|
+
configured = true;
|
|
491
586
|
}
|
|
492
587
|
} catch {
|
|
493
|
-
console.log(` \xB7
|
|
588
|
+
console.log(` \xB7 ${name} \u2014 could not configure`);
|
|
494
589
|
}
|
|
495
590
|
}
|
|
496
591
|
}
|
|
592
|
+
if (!configured) {
|
|
593
|
+
console.log(" No agents detected. Add manually:\n");
|
|
594
|
+
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
595
|
+
console.log("\n Config paths:");
|
|
596
|
+
console.log(" Kiro: ~/.kiro/settings/mcp.json");
|
|
597
|
+
console.log(" Cursor: ~/.cursor/mcp.json");
|
|
598
|
+
console.log(" Claude Code: ~/.claude/mcp_servers.json");
|
|
599
|
+
console.log(" VS Code: .vscode/mcp.json");
|
|
600
|
+
}
|
|
601
|
+
installHooks();
|
|
497
602
|
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");
|
|
498
|
-
console.log("\n
|
|
603
|
+
console.log("\n Setup complete.\n");
|
|
499
604
|
console.log(" 1. Restart your coding agent");
|
|
500
|
-
console.log(" 2. Start coding \u2014 sessions
|
|
501
|
-
console.log(" 3.
|
|
502
|
-
console.log(" 4. Docs: https://costlens.dev/docs/mcp\n");
|
|
605
|
+
console.log(" 2. Start coding \u2014 sessions tracked automatically");
|
|
606
|
+
console.log(" 3. Dashboard: https://costlens.dev/dashboard\n");
|
|
503
607
|
console.log(" Commands:");
|
|
504
608
|
console.log(" npx @costlens/mcp-server status \u2014 check connection");
|
|
505
609
|
console.log(" npx @costlens/mcp-server login \u2014 re-authenticate");
|
|
610
|
+
console.log(" npx @costlens/mcp-server hooks \u2014 reinstall git hooks");
|
|
506
611
|
console.log("");
|
|
612
|
+
process.exit(0);
|
|
507
613
|
}
|
|
508
614
|
async function status() {
|
|
509
615
|
const config = readConfig();
|
|
510
616
|
if (!config.apiKey) {
|
|
511
|
-
console.log("
|
|
617
|
+
console.log(" Not authenticated. Run: npx @costlens/mcp-server login\n");
|
|
512
618
|
process.exit(1);
|
|
513
619
|
}
|
|
514
|
-
console.log("
|
|
620
|
+
console.log(" CostLens Status\n");
|
|
515
621
|
try {
|
|
516
622
|
const res = await fetch(`${API_BASE2}/v1/spend`, {
|
|
517
623
|
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
518
624
|
signal: AbortSignal.timeout(5e3)
|
|
519
625
|
});
|
|
520
626
|
if (!res.ok) {
|
|
521
|
-
console.log("
|
|
627
|
+
console.log(" Connection failed (invalid key or server error)");
|
|
522
628
|
process.exit(1);
|
|
523
629
|
}
|
|
524
630
|
const data = await res.json();
|
|
525
|
-
console.log(`
|
|
631
|
+
console.log(` Connected`);
|
|
526
632
|
console.log(` Key: ${config.apiKey.slice(0, 8)}...`);
|
|
527
633
|
console.log(` Today: $${(data.today || 0).toFixed(4)}`);
|
|
528
634
|
console.log(` This week: $${(data.week || 0).toFixed(4)}`);
|
|
529
635
|
console.log(` This month: $${(data.month || 0).toFixed(4)}`);
|
|
530
636
|
} catch (e) {
|
|
531
|
-
console.log(`
|
|
637
|
+
console.log(` Could not connect: ${e.message}`);
|
|
532
638
|
process.exit(1);
|
|
533
639
|
}
|
|
534
640
|
}
|
|
@@ -537,11 +643,14 @@ if (command === "login") login();
|
|
|
537
643
|
else if (command === "init") init();
|
|
538
644
|
else if (command === "status") status();
|
|
539
645
|
else if (command === "setup") setup();
|
|
540
|
-
else {
|
|
646
|
+
else if (command === "hooks") {
|
|
647
|
+
installHooks();
|
|
648
|
+
process.exit(0);
|
|
649
|
+
} else {
|
|
541
650
|
init_index();
|
|
542
651
|
}
|
|
543
652
|
async function setup() {
|
|
544
|
-
console.log("
|
|
653
|
+
console.log(" CostLens \u2014 One-step setup\n");
|
|
545
654
|
const existing = readConfig();
|
|
546
655
|
if (existing.apiKey) {
|
|
547
656
|
console.log(" Already authenticated. Running init...\n");
|
package/dist/index.js
CHANGED
|
@@ -21,37 +21,46 @@ function resolveApiKey() {
|
|
|
21
21
|
}
|
|
22
22
|
var API_KEY = resolveApiKey();
|
|
23
23
|
var API_BASE = process.env.COSTLENS_API_URL || "https://api.costlens.dev";
|
|
24
|
+
var activeSessionId = null;
|
|
25
|
+
var heartbeatInterval = null;
|
|
26
|
+
var trackingMode = "manual";
|
|
27
|
+
var productivityEnabled = null;
|
|
28
|
+
var toolCallCount = 0;
|
|
29
|
+
async function apiFetch(path, opts = {}) {
|
|
30
|
+
const headers = { "Authorization": `Bearer ${API_KEY}`, ...opts.headers };
|
|
31
|
+
if (opts.body) headers["Content-Type"] = "application/json";
|
|
32
|
+
return fetch(`${API_BASE}${path}`, { ...opts, headers, signal: AbortSignal.timeout(5e3) });
|
|
33
|
+
}
|
|
24
34
|
async function loadPricing() {
|
|
25
35
|
try {
|
|
26
|
-
const
|
|
27
|
-
if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`;
|
|
28
|
-
const res = await fetch(`${API_BASE}/v1/pricing`, { headers, signal: AbortSignal.timeout(5e3) });
|
|
36
|
+
const res = await apiFetch("/v1/pricing");
|
|
29
37
|
if (res.ok) {
|
|
30
38
|
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
|
-
}
|
|
39
|
+
for (const m of data.models) import_classifier.PRICING[m.model] = { input: m.input, output: m.output };
|
|
34
40
|
}
|
|
35
41
|
} catch {
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
async function loadKeySettings() {
|
|
45
|
+
if (!API_KEY) return;
|
|
46
|
+
try {
|
|
47
|
+
const res = await apiFetch("/v1/plan");
|
|
48
|
+
if (!res.ok) return;
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
trackingMode = data.keySettings?.trackingMode || "manual";
|
|
51
|
+
productivityEnabled = data.features?.includes("productivity_basic") ?? false;
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
44
55
|
function startHeartbeat(sessionId) {
|
|
45
56
|
stopHeartbeat();
|
|
46
57
|
activeSessionId = sessionId;
|
|
47
58
|
heartbeatInterval = setInterval(async () => {
|
|
48
59
|
if (!activeSessionId || !API_KEY) return;
|
|
49
60
|
try {
|
|
50
|
-
const res = await
|
|
61
|
+
const res = await apiFetch("/v1/productivity/heartbeat", {
|
|
51
62
|
method: "POST",
|
|
52
|
-
|
|
53
|
-
body: JSON.stringify({ session_id: activeSessionId }),
|
|
54
|
-
signal: AbortSignal.timeout(5e3)
|
|
63
|
+
body: JSON.stringify({ session_id: activeSessionId })
|
|
55
64
|
});
|
|
56
65
|
if (res.status === 410) {
|
|
57
66
|
activeSessionId = null;
|
|
@@ -67,36 +76,57 @@ function stopHeartbeat() {
|
|
|
67
76
|
heartbeatInterval = null;
|
|
68
77
|
}
|
|
69
78
|
}
|
|
79
|
+
async function ensureAutoSession() {
|
|
80
|
+
if (trackingMode !== "auto" || !API_KEY || !productivityEnabled || activeSessionId) return;
|
|
81
|
+
try {
|
|
82
|
+
const res = await apiFetch("/v1/productivity/sessions", {
|
|
83
|
+
method: "POST",
|
|
84
|
+
body: JSON.stringify({ task_description: "Auto-tracked MCP session" })
|
|
85
|
+
});
|
|
86
|
+
if (res.ok) {
|
|
87
|
+
const data = await res.json();
|
|
88
|
+
if (data.data?.id) startHeartbeat(data.data.id);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function autoLogEvent(eventType, metadata) {
|
|
94
|
+
if (trackingMode !== "auto" || !activeSessionId || !API_KEY) return;
|
|
95
|
+
try {
|
|
96
|
+
await apiFetch("/v1/productivity/events", {
|
|
97
|
+
method: "POST",
|
|
98
|
+
body: JSON.stringify({ session_id: activeSessionId, event_type: eventType, metadata })
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function onToolCall(toolName) {
|
|
104
|
+
toolCallCount++;
|
|
105
|
+
if (trackingMode === "auto") {
|
|
106
|
+
await ensureAutoSession();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
var server = new import_mcp.McpServer({ name: "@lens360/mcp-server", version: "0.4.0" });
|
|
70
110
|
server.tool(
|
|
71
111
|
"get_spend_summary",
|
|
72
112
|
"Get current spend summary \u2014 daily, weekly, monthly, and session totals",
|
|
73
113
|
{},
|
|
74
114
|
async () => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
115
|
+
await onToolCall("get_spend_summary");
|
|
116
|
+
if (!API_KEY) return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
|
|
78
117
|
try {
|
|
79
|
-
const res = await
|
|
80
|
-
|
|
81
|
-
signal: AbortSignal.timeout(5e3)
|
|
82
|
-
});
|
|
83
|
-
if (!res.ok) {
|
|
84
|
-
return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
85
|
-
}
|
|
118
|
+
const res = await apiFetch("/v1/spend");
|
|
119
|
+
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
86
120
|
const data = await res.json();
|
|
87
121
|
const lines = [
|
|
88
|
-
|
|
122
|
+
`Spend Summary (${data.key})`,
|
|
89
123
|
``,
|
|
90
124
|
`Today: $${data.daily.cost.toFixed(4)} (${data.daily.requests} requests)`,
|
|
91
125
|
`Week: $${data.weekly.cost.toFixed(4)} (${data.weekly.requests} requests)`,
|
|
92
126
|
`Month: $${data.monthly.cost.toFixed(4)} (${data.monthly.requests} requests)`
|
|
93
127
|
];
|
|
94
|
-
if (data.session) {
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
if (data.dailyBudget) {
|
|
98
|
-
lines.push(``, `Budget: $${data.budgetRemaining.toFixed(2)} remaining of $${data.dailyBudget.toFixed(2)}/day`);
|
|
99
|
-
}
|
|
128
|
+
if (data.session) lines.push(`Session: $${data.session.cost.toFixed(4)} (${data.session.requests} requests) [${data.session.correlationId}]`);
|
|
129
|
+
if (data.dailyBudget) lines.push(``, `Budget: $${data.budgetRemaining.toFixed(2)} remaining of $${data.dailyBudget.toFixed(2)}/day`);
|
|
100
130
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
101
131
|
} catch (err) {
|
|
102
132
|
return { content: [{ type: "text", text: `Error fetching spend: ${err.message}` }] };
|
|
@@ -115,6 +145,7 @@ server.tool(
|
|
|
115
145
|
isFirstMessage: import_zod.z.boolean().optional()
|
|
116
146
|
},
|
|
117
147
|
async (params) => {
|
|
148
|
+
await onToolCall("costlens_suggest_model");
|
|
118
149
|
const result = (0, import_classifier.classify)({
|
|
119
150
|
prompt: params.prompt,
|
|
120
151
|
currentModel: params.currentModel,
|
|
@@ -124,39 +155,39 @@ server.tool(
|
|
|
124
155
|
isFirstMessage: params.isFirstMessage
|
|
125
156
|
});
|
|
126
157
|
if (API_KEY && result.suggestedModel) {
|
|
127
|
-
|
|
158
|
+
apiFetch("/v1/classifier/events", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
complexity: result.level,
|
|
162
|
+
confidence: result.confidence,
|
|
163
|
+
suggestedModel: result.suggestedModel,
|
|
164
|
+
currentModel: params.currentModel,
|
|
165
|
+
provider: params.provider,
|
|
166
|
+
staticScore: result.signals.staticScore
|
|
167
|
+
})
|
|
168
|
+
}).catch(() => {
|
|
169
|
+
});
|
|
170
|
+
autoLogEvent("prompt", {
|
|
171
|
+
complexity: result.level,
|
|
172
|
+
model: result.suggestedModel,
|
|
173
|
+
provider: params.provider
|
|
128
174
|
});
|
|
129
175
|
}
|
|
130
|
-
return {
|
|
131
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
132
|
-
};
|
|
176
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
133
177
|
}
|
|
134
178
|
);
|
|
135
|
-
async function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 {
|
|
179
|
+
async function ensureProductivity() {
|
|
180
|
+
if (!API_KEY) return "Not connected to CostLens. Run: npx @costlens/mcp-server setup";
|
|
181
|
+
if (productivityEnabled === null) {
|
|
182
|
+
await loadKeySettings();
|
|
151
183
|
}
|
|
184
|
+
if (!productivityEnabled) return "Productivity tracking requires Business plan or Productivity add-on.";
|
|
185
|
+
return null;
|
|
152
186
|
}
|
|
153
187
|
async function checkFeature(feature) {
|
|
154
188
|
if (!API_KEY) return false;
|
|
155
189
|
try {
|
|
156
|
-
const res = await
|
|
157
|
-
headers: { "Authorization": `Bearer ${API_KEY}` },
|
|
158
|
-
signal: AbortSignal.timeout(5e3)
|
|
159
|
-
});
|
|
190
|
+
const res = await apiFetch("/v1/plan");
|
|
160
191
|
if (!res.ok) return false;
|
|
161
192
|
const plan = await res.json();
|
|
162
193
|
return plan.features?.includes(feature) ?? false;
|
|
@@ -164,15 +195,6 @@ async function checkFeature(feature) {
|
|
|
164
195
|
return false;
|
|
165
196
|
}
|
|
166
197
|
}
|
|
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
198
|
server.tool(
|
|
177
199
|
"start_session",
|
|
178
200
|
"Start a productivity tracking session",
|
|
@@ -182,14 +204,13 @@ server.tool(
|
|
|
182
204
|
repo: import_zod.z.string().optional()
|
|
183
205
|
},
|
|
184
206
|
async (params) => {
|
|
207
|
+
await onToolCall("start_session");
|
|
185
208
|
const err = await ensureProductivity();
|
|
186
209
|
if (err) return { content: [{ type: "text", text: err }] };
|
|
187
210
|
try {
|
|
188
|
-
const res = await
|
|
211
|
+
const res = await apiFetch("/v1/productivity/sessions", {
|
|
189
212
|
method: "POST",
|
|
190
|
-
|
|
191
|
-
body: JSON.stringify(params),
|
|
192
|
-
signal: AbortSignal.timeout(5e3)
|
|
213
|
+
body: JSON.stringify(params)
|
|
193
214
|
});
|
|
194
215
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
195
216
|
const data = await res.json();
|
|
@@ -209,14 +230,13 @@ server.tool(
|
|
|
209
230
|
artifacts: import_zod.z.array(import_zod.z.string()).optional()
|
|
210
231
|
},
|
|
211
232
|
async (params) => {
|
|
233
|
+
await onToolCall("end_session");
|
|
212
234
|
const err = await ensureProductivity();
|
|
213
235
|
if (err) return { content: [{ type: "text", text: err }] };
|
|
214
236
|
try {
|
|
215
|
-
const res = await
|
|
237
|
+
const res = await apiFetch(`/v1/productivity/sessions/${params.session_id}`, {
|
|
216
238
|
method: "PATCH",
|
|
217
|
-
|
|
218
|
-
body: JSON.stringify({ outcome: params.outcome, artifacts: params.artifacts }),
|
|
219
|
-
signal: AbortSignal.timeout(5e3)
|
|
239
|
+
body: JSON.stringify({ outcome: params.outcome, artifacts: params.artifacts })
|
|
220
240
|
});
|
|
221
241
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
222
242
|
const data = await res.json();
|
|
@@ -237,18 +257,14 @@ server.tool(
|
|
|
237
257
|
metadata: import_zod.z.record(import_zod.z.any()).optional()
|
|
238
258
|
},
|
|
239
259
|
async (params) => {
|
|
260
|
+
await onToolCall("log_event");
|
|
240
261
|
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
262
|
const hasFull = await checkFeature("productivity_full");
|
|
245
263
|
if (!hasFull) return { content: [{ type: "text", text: "Event logging requires Productivity Insights add-on." }] };
|
|
246
264
|
try {
|
|
247
|
-
const res = await
|
|
265
|
+
const res = await apiFetch("/v1/productivity/events", {
|
|
248
266
|
method: "POST",
|
|
249
|
-
|
|
250
|
-
body: JSON.stringify(params),
|
|
251
|
-
signal: AbortSignal.timeout(5e3)
|
|
267
|
+
body: JSON.stringify(params)
|
|
252
268
|
});
|
|
253
269
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
254
270
|
return { content: [{ type: "text", text: "Event logged." }] };
|
|
@@ -260,22 +276,18 @@ server.tool(
|
|
|
260
276
|
server.tool(
|
|
261
277
|
"get_productivity_summary",
|
|
262
278
|
"Get productivity summary for a time period",
|
|
263
|
-
{
|
|
264
|
-
period: import_zod.z.enum(["today", "week", "month"]).optional()
|
|
265
|
-
},
|
|
279
|
+
{ period: import_zod.z.enum(["today", "week", "month"]).optional() },
|
|
266
280
|
async (params) => {
|
|
281
|
+
await onToolCall("get_productivity_summary");
|
|
267
282
|
const err = await ensureProductivity();
|
|
268
283
|
if (err) return { content: [{ type: "text", text: err }] };
|
|
269
284
|
try {
|
|
270
285
|
const period = params.period || "week";
|
|
271
|
-
const res = await
|
|
272
|
-
headers: { "Authorization": `Bearer ${API_KEY}` },
|
|
273
|
-
signal: AbortSignal.timeout(5e3)
|
|
274
|
-
});
|
|
286
|
+
const res = await apiFetch(`/v1/productivity/summary?period=${period}`);
|
|
275
287
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
276
288
|
const data = await res.json();
|
|
277
289
|
const lines = [
|
|
278
|
-
|
|
290
|
+
`Productivity Summary (${period})`,
|
|
279
291
|
``,
|
|
280
292
|
`Sessions: ${data.sessions}`,
|
|
281
293
|
`Time: ${data.total_time_minutes} min`,
|
|
@@ -296,25 +308,15 @@ server.tool(
|
|
|
296
308
|
server.tool(
|
|
297
309
|
"get_github_metrics",
|
|
298
310
|
"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
|
-
},
|
|
311
|
+
{ period: import_zod.z.enum(["today", "week", "month"]).optional() },
|
|
302
312
|
async (params) => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
313
|
+
await onToolCall("get_github_metrics");
|
|
314
|
+
if (!API_KEY) return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
|
|
306
315
|
try {
|
|
307
316
|
const period = params.period || "week";
|
|
308
|
-
const res = await
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
}
|
|
317
|
+
const res = await apiFetch(`/v1/productivity/github?period=${period}`);
|
|
318
|
+
if (res.status === 404) return { content: [{ type: "text", text: "GitHub not connected. Connect in Settings \u2192 GitHub at costlens.dev" }] };
|
|
319
|
+
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
318
320
|
const { data } = await res.json();
|
|
319
321
|
const lines = [
|
|
320
322
|
`GitHub Metrics (${period}):`,
|
|
@@ -331,17 +333,15 @@ server.tool(
|
|
|
331
333
|
}
|
|
332
334
|
);
|
|
333
335
|
async function main() {
|
|
334
|
-
await loadPricing();
|
|
336
|
+
await Promise.all([loadPricing(), loadKeySettings()]);
|
|
335
337
|
const transport = new import_stdio.StdioServerTransport();
|
|
336
338
|
await server.connect(transport);
|
|
337
339
|
const cleanup = async () => {
|
|
338
340
|
if (activeSessionId && API_KEY) {
|
|
339
341
|
try {
|
|
340
|
-
await
|
|
342
|
+
await apiFetch(`/v1/productivity/sessions/${activeSessionId}`, {
|
|
341
343
|
method: "PATCH",
|
|
342
|
-
|
|
343
|
-
body: JSON.stringify({ outcome: "completed" }),
|
|
344
|
-
signal: AbortSignal.timeout(3e3)
|
|
344
|
+
body: JSON.stringify({ outcome: "completed", metadata: { toolCalls: toolCallCount } })
|
|
345
345
|
});
|
|
346
346
|
} catch {
|
|
347
347
|
}
|