@costlens/mcp-server 0.4.3 → 0.6.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 +378 -136
- package/dist/index.js +110 -110
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -39,31 +39,41 @@ function resolveApiKey() {
|
|
|
39
39
|
return void 0;
|
|
40
40
|
}
|
|
41
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
|
+
}
|
|
42
47
|
async function loadPricing() {
|
|
43
48
|
try {
|
|
44
|
-
const
|
|
45
|
-
if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`;
|
|
46
|
-
const res = await fetch(`${API_BASE}/v1/pricing`, { headers, signal: AbortSignal.timeout(5e3) });
|
|
49
|
+
const res = await apiFetch("/v1/pricing");
|
|
47
50
|
if (res.ok) {
|
|
48
51
|
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
|
+
for (const m of data.models) import_classifier.PRICING[m.model] = { input: m.input, output: m.output };
|
|
52
53
|
}
|
|
53
54
|
} catch {
|
|
54
55
|
}
|
|
55
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
|
+
}
|
|
56
68
|
function startHeartbeat(sessionId) {
|
|
57
69
|
stopHeartbeat();
|
|
58
70
|
activeSessionId = sessionId;
|
|
59
71
|
heartbeatInterval = setInterval(async () => {
|
|
60
72
|
if (!activeSessionId || !API_KEY) return;
|
|
61
73
|
try {
|
|
62
|
-
const res = await
|
|
74
|
+
const res = await apiFetch("/v1/productivity/heartbeat", {
|
|
63
75
|
method: "POST",
|
|
64
|
-
|
|
65
|
-
body: JSON.stringify({ session_id: activeSessionId }),
|
|
66
|
-
signal: AbortSignal.timeout(5e3)
|
|
76
|
+
body: JSON.stringify({ session_id: activeSessionId })
|
|
67
77
|
});
|
|
68
78
|
if (res.status === 410) {
|
|
69
79
|
activeSessionId = null;
|
|
@@ -79,58 +89,65 @@ function stopHeartbeat() {
|
|
|
79
89
|
heartbeatInterval = null;
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
|
-
async function
|
|
92
|
+
async function ensureAutoSession() {
|
|
93
|
+
if (trackingMode !== "auto" || !API_KEY || !productivityEnabled || activeSessionId) return;
|
|
83
94
|
try {
|
|
84
|
-
await
|
|
95
|
+
const res = await apiFetch("/v1/productivity/sessions", {
|
|
85
96
|
method: "POST",
|
|
86
|
-
|
|
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)
|
|
97
|
+
body: JSON.stringify({ task_description: "Auto-tracked MCP session" })
|
|
96
98
|
});
|
|
99
|
+
if (res.ok) {
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
if (data.data?.id) startHeartbeat(data.data.id);
|
|
102
|
+
}
|
|
97
103
|
} catch {
|
|
98
104
|
}
|
|
99
105
|
}
|
|
100
|
-
async function
|
|
101
|
-
if (!API_KEY) return
|
|
106
|
+
async function autoLogEvent(eventType, metadata) {
|
|
107
|
+
if (trackingMode !== "auto" || !activeSessionId || !API_KEY) return;
|
|
102
108
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
await apiFetch("/v1/productivity/events", {
|
|
110
|
+
method: "POST",
|
|
111
|
+
body: JSON.stringify({ session_id: activeSessionId, event_type: eventType, metadata })
|
|
106
112
|
});
|
|
107
|
-
if (!res.ok) return false;
|
|
108
|
-
const plan = await res.json();
|
|
109
|
-
return plan.features?.includes(feature) ?? false;
|
|
110
113
|
} catch {
|
|
111
|
-
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function onToolCall(toolName) {
|
|
117
|
+
toolCallCount++;
|
|
118
|
+
if (trackingMode === "auto") {
|
|
119
|
+
await ensureAutoSession();
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
122
|
async function ensureProductivity() {
|
|
115
123
|
if (!API_KEY) return "Not connected to CostLens. Run: npx @costlens/mcp-server setup";
|
|
116
124
|
if (productivityEnabled === null) {
|
|
117
|
-
|
|
125
|
+
await loadKeySettings();
|
|
118
126
|
}
|
|
119
127
|
if (!productivityEnabled) return "Productivity tracking requires Business plan or Productivity add-on.";
|
|
120
128
|
return null;
|
|
121
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
|
+
}
|
|
122
141
|
async function main() {
|
|
123
|
-
await loadPricing();
|
|
142
|
+
await Promise.all([loadPricing(), loadKeySettings()]);
|
|
124
143
|
const transport = new import_stdio.StdioServerTransport();
|
|
125
144
|
await server.connect(transport);
|
|
126
145
|
const cleanup = async () => {
|
|
127
146
|
if (activeSessionId && API_KEY) {
|
|
128
147
|
try {
|
|
129
|
-
await
|
|
148
|
+
await apiFetch(`/v1/productivity/sessions/${activeSessionId}`, {
|
|
130
149
|
method: "PATCH",
|
|
131
|
-
|
|
132
|
-
body: JSON.stringify({ outcome: "completed" }),
|
|
133
|
-
signal: AbortSignal.timeout(3e3)
|
|
150
|
+
body: JSON.stringify({ outcome: "completed", metadata: { toolCalls: toolCallCount } })
|
|
134
151
|
});
|
|
135
152
|
} catch {
|
|
136
153
|
}
|
|
@@ -141,7 +158,7 @@ async function main() {
|
|
|
141
158
|
process.on("SIGTERM", cleanup);
|
|
142
159
|
process.on("SIGINT", cleanup);
|
|
143
160
|
}
|
|
144
|
-
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;
|
|
145
162
|
var init_index = __esm({
|
|
146
163
|
"src/index.ts"() {
|
|
147
164
|
"use strict";
|
|
@@ -154,42 +171,32 @@ var init_index = __esm({
|
|
|
154
171
|
import_os = require("os");
|
|
155
172
|
API_KEY = resolveApiKey();
|
|
156
173
|
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
174
|
activeSessionId = null;
|
|
162
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" });
|
|
163
180
|
server.tool(
|
|
164
181
|
"get_spend_summary",
|
|
165
182
|
"Get current spend summary \u2014 daily, weekly, monthly, and session totals",
|
|
166
183
|
{},
|
|
167
184
|
async () => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
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" }] };
|
|
171
187
|
try {
|
|
172
|
-
const res = await
|
|
173
|
-
|
|
174
|
-
signal: AbortSignal.timeout(5e3)
|
|
175
|
-
});
|
|
176
|
-
if (!res.ok) {
|
|
177
|
-
return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
178
|
-
}
|
|
188
|
+
const res = await apiFetch("/v1/spend");
|
|
189
|
+
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
179
190
|
const data = await res.json();
|
|
180
191
|
const lines = [
|
|
181
|
-
|
|
192
|
+
`Spend Summary (${data.key})`,
|
|
182
193
|
``,
|
|
183
194
|
`Today: $${data.daily.cost.toFixed(4)} (${data.daily.requests} requests)`,
|
|
184
195
|
`Week: $${data.weekly.cost.toFixed(4)} (${data.weekly.requests} requests)`,
|
|
185
196
|
`Month: $${data.monthly.cost.toFixed(4)} (${data.monthly.requests} requests)`
|
|
186
197
|
];
|
|
187
|
-
if (data.session) {
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
if (data.dailyBudget) {
|
|
191
|
-
lines.push(``, `Budget: $${data.budgetRemaining.toFixed(2)} remaining of $${data.dailyBudget.toFixed(2)}/day`);
|
|
192
|
-
}
|
|
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`);
|
|
193
200
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
194
201
|
} catch (err) {
|
|
195
202
|
return { content: [{ type: "text", text: `Error fetching spend: ${err.message}` }] };
|
|
@@ -208,6 +215,7 @@ var init_index = __esm({
|
|
|
208
215
|
isFirstMessage: import_zod.z.boolean().optional()
|
|
209
216
|
},
|
|
210
217
|
async (params) => {
|
|
218
|
+
await onToolCall("costlens_suggest_model");
|
|
211
219
|
const result = (0, import_classifier.classify)({
|
|
212
220
|
prompt: params.prompt,
|
|
213
221
|
currentModel: params.currentModel,
|
|
@@ -217,15 +225,27 @@ var init_index = __esm({
|
|
|
217
225
|
isFirstMessage: params.isFirstMessage
|
|
218
226
|
});
|
|
219
227
|
if (API_KEY && result.suggestedModel) {
|
|
220
|
-
|
|
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
|
|
221
244
|
});
|
|
222
245
|
}
|
|
223
|
-
return {
|
|
224
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
225
|
-
};
|
|
246
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
226
247
|
}
|
|
227
248
|
);
|
|
228
|
-
productivityEnabled = null;
|
|
229
249
|
server.tool(
|
|
230
250
|
"start_session",
|
|
231
251
|
"Start a productivity tracking session",
|
|
@@ -235,14 +255,13 @@ var init_index = __esm({
|
|
|
235
255
|
repo: import_zod.z.string().optional()
|
|
236
256
|
},
|
|
237
257
|
async (params) => {
|
|
258
|
+
await onToolCall("start_session");
|
|
238
259
|
const err = await ensureProductivity();
|
|
239
260
|
if (err) return { content: [{ type: "text", text: err }] };
|
|
240
261
|
try {
|
|
241
|
-
const res = await
|
|
262
|
+
const res = await apiFetch("/v1/productivity/sessions", {
|
|
242
263
|
method: "POST",
|
|
243
|
-
|
|
244
|
-
body: JSON.stringify(params),
|
|
245
|
-
signal: AbortSignal.timeout(5e3)
|
|
264
|
+
body: JSON.stringify(params)
|
|
246
265
|
});
|
|
247
266
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
248
267
|
const data = await res.json();
|
|
@@ -262,14 +281,13 @@ var init_index = __esm({
|
|
|
262
281
|
artifacts: import_zod.z.array(import_zod.z.string()).optional()
|
|
263
282
|
},
|
|
264
283
|
async (params) => {
|
|
284
|
+
await onToolCall("end_session");
|
|
265
285
|
const err = await ensureProductivity();
|
|
266
286
|
if (err) return { content: [{ type: "text", text: err }] };
|
|
267
287
|
try {
|
|
268
|
-
const res = await
|
|
288
|
+
const res = await apiFetch(`/v1/productivity/sessions/${params.session_id}`, {
|
|
269
289
|
method: "PATCH",
|
|
270
|
-
|
|
271
|
-
body: JSON.stringify({ outcome: params.outcome, artifacts: params.artifacts }),
|
|
272
|
-
signal: AbortSignal.timeout(5e3)
|
|
290
|
+
body: JSON.stringify({ outcome: params.outcome, artifacts: params.artifacts })
|
|
273
291
|
});
|
|
274
292
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
275
293
|
const data = await res.json();
|
|
@@ -290,18 +308,14 @@ var init_index = __esm({
|
|
|
290
308
|
metadata: import_zod.z.record(import_zod.z.any()).optional()
|
|
291
309
|
},
|
|
292
310
|
async (params) => {
|
|
311
|
+
await onToolCall("log_event");
|
|
293
312
|
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
313
|
const hasFull = await checkFeature("productivity_full");
|
|
298
314
|
if (!hasFull) return { content: [{ type: "text", text: "Event logging requires Productivity Insights add-on." }] };
|
|
299
315
|
try {
|
|
300
|
-
const res = await
|
|
316
|
+
const res = await apiFetch("/v1/productivity/events", {
|
|
301
317
|
method: "POST",
|
|
302
|
-
|
|
303
|
-
body: JSON.stringify(params),
|
|
304
|
-
signal: AbortSignal.timeout(5e3)
|
|
318
|
+
body: JSON.stringify(params)
|
|
305
319
|
});
|
|
306
320
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
307
321
|
return { content: [{ type: "text", text: "Event logged." }] };
|
|
@@ -313,22 +327,18 @@ var init_index = __esm({
|
|
|
313
327
|
server.tool(
|
|
314
328
|
"get_productivity_summary",
|
|
315
329
|
"Get productivity summary for a time period",
|
|
316
|
-
{
|
|
317
|
-
period: import_zod.z.enum(["today", "week", "month"]).optional()
|
|
318
|
-
},
|
|
330
|
+
{ period: import_zod.z.enum(["today", "week", "month"]).optional() },
|
|
319
331
|
async (params) => {
|
|
332
|
+
await onToolCall("get_productivity_summary");
|
|
320
333
|
const err = await ensureProductivity();
|
|
321
334
|
if (err) return { content: [{ type: "text", text: err }] };
|
|
322
335
|
try {
|
|
323
336
|
const period = params.period || "week";
|
|
324
|
-
const res = await
|
|
325
|
-
headers: { "Authorization": `Bearer ${API_KEY}` },
|
|
326
|
-
signal: AbortSignal.timeout(5e3)
|
|
327
|
-
});
|
|
337
|
+
const res = await apiFetch(`/v1/productivity/summary?period=${period}`);
|
|
328
338
|
if (!res.ok) return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
|
|
329
339
|
const data = await res.json();
|
|
330
340
|
const lines = [
|
|
331
|
-
|
|
341
|
+
`Productivity Summary (${period})`,
|
|
332
342
|
``,
|
|
333
343
|
`Sessions: ${data.sessions}`,
|
|
334
344
|
`Time: ${data.total_time_minutes} min`,
|
|
@@ -349,25 +359,15 @@ var init_index = __esm({
|
|
|
349
359
|
server.tool(
|
|
350
360
|
"get_github_metrics",
|
|
351
361
|
"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
|
-
},
|
|
362
|
+
{ period: import_zod.z.enum(["today", "week", "month"]).optional() },
|
|
355
363
|
async (params) => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
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" }] };
|
|
359
366
|
try {
|
|
360
367
|
const period = params.period || "week";
|
|
361
|
-
const res = await
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
}
|
|
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()}` }] };
|
|
371
371
|
const { data } = await res.json();
|
|
372
372
|
const lines = [
|
|
373
373
|
`GitHub Metrics (${period}):`,
|
|
@@ -398,6 +398,11 @@ var CONFIG_FILE = (0, import_path2.join)(CONFIG_DIR, "config.json");
|
|
|
398
398
|
var API_BASE2 = process.env.COSTLENS_API_URL || "https://api.costlens.dev";
|
|
399
399
|
var APP_URL = process.env.COSTLENS_APP_URL || "https://costlens.dev";
|
|
400
400
|
function readConfig() {
|
|
401
|
+
try {
|
|
402
|
+
const projectConfig = JSON.parse((0, import_fs2.readFileSync)((0, import_path2.join)(process.cwd(), ".costlens.json"), "utf-8"));
|
|
403
|
+
if (projectConfig.key) return { apiKey: projectConfig.key };
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
401
406
|
try {
|
|
402
407
|
return JSON.parse((0, import_fs2.readFileSync)(CONFIG_FILE, "utf-8"));
|
|
403
408
|
} catch {
|
|
@@ -408,8 +413,103 @@ function writeConfig(config) {
|
|
|
408
413
|
if (!(0, import_fs2.existsSync)(CONFIG_DIR)) (0, import_fs2.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
409
414
|
(0, import_fs2.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
410
415
|
}
|
|
416
|
+
var HOOK_MARKER = "# costlens-hook";
|
|
417
|
+
var POST_COMMIT_HOOK = `#!/bin/sh
|
|
418
|
+
${HOOK_MARKER}
|
|
419
|
+
# CostLens \u2014 tracks commits during AI sessions (metadata only, no code content)
|
|
420
|
+
# Sends: hash, message, files changed, insertions, deletions, branch, timestamp
|
|
421
|
+
# Remove this file to uninstall: .git/hooks/post-commit
|
|
422
|
+
|
|
423
|
+
COSTLENS_KEY=$(cat ~/.costlens/config.json 2>/dev/null | grep -o '"apiKey"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
|
|
424
|
+
if [ -z "$COSTLENS_KEY" ]; then exit 0; fi
|
|
425
|
+
|
|
426
|
+
HASH=$(git rev-parse HEAD)
|
|
427
|
+
MSG=$(git log -1 --format=%s | head -c 200)
|
|
428
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
429
|
+
FILES=$(git diff --numstat HEAD~1 HEAD 2>/dev/null | wc -l | tr -d ' ')
|
|
430
|
+
STATS=$(git diff --shortstat HEAD~1 HEAD 2>/dev/null)
|
|
431
|
+
INSERTIONS=$(echo "$STATS" | grep -o '[0-9]* insertion' | grep -o '[0-9]*' || echo "0")
|
|
432
|
+
DELETIONS=$(echo "$STATS" | grep -o '[0-9]* deletion' | grep -o '[0-9]*' || echo "0")
|
|
433
|
+
|
|
434
|
+
curl -s -X POST "\${COSTLENS_API_URL:-https://api.costlens.dev}/v1/hooks/commit" \\
|
|
435
|
+
-H "Authorization: Bearer $COSTLENS_KEY" \\
|
|
436
|
+
-H "Content-Type: application/json" \\
|
|
437
|
+
-d "{\\"hash\\":\\"$HASH\\",\\"message\\":\\"$MSG\\",\\"branch\\":\\"$BRANCH\\",\\"filesChanged\\":$FILES,\\"insertions\\":\${INSERTIONS:-0},\\"deletions\\":\${DELETIONS:-0}}" \\
|
|
438
|
+
--max-time 3 > /dev/null 2>&1 &
|
|
439
|
+
|
|
440
|
+
exit 0
|
|
441
|
+
`;
|
|
442
|
+
function findGitRepos() {
|
|
443
|
+
const cwd = process.cwd();
|
|
444
|
+
const repos = [];
|
|
445
|
+
if ((0, import_fs2.existsSync)((0, import_path2.join)(cwd, ".git"))) repos.push(cwd);
|
|
446
|
+
const searchDirs = [
|
|
447
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "Desktop"),
|
|
448
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "Projects"),
|
|
449
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "projects"),
|
|
450
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "dev"),
|
|
451
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "code"),
|
|
452
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "workspace"),
|
|
453
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "Workspace"),
|
|
454
|
+
(0, import_path2.join)((0, import_os2.homedir)(), "Desktop", "Workspace")
|
|
455
|
+
];
|
|
456
|
+
for (const dir of searchDirs) {
|
|
457
|
+
if (!(0, import_fs2.existsSync)(dir)) continue;
|
|
458
|
+
try {
|
|
459
|
+
const entries = require("fs").readdirSync(dir, { withFileTypes: true });
|
|
460
|
+
for (const entry of entries) {
|
|
461
|
+
if (entry.isDirectory() && (0, import_fs2.existsSync)((0, import_path2.join)(dir, entry.name, ".git"))) {
|
|
462
|
+
repos.push((0, import_path2.join)(dir, entry.name));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return [...new Set(repos)];
|
|
469
|
+
}
|
|
470
|
+
function installGitHook(repoPath) {
|
|
471
|
+
const hooksDir = (0, import_path2.join)(repoPath, ".git", "hooks");
|
|
472
|
+
const hookPath = (0, import_path2.join)(hooksDir, "post-commit");
|
|
473
|
+
if ((0, import_fs2.existsSync)(hookPath)) {
|
|
474
|
+
try {
|
|
475
|
+
const content = (0, import_fs2.readFileSync)(hookPath, "utf-8");
|
|
476
|
+
if (content.includes(HOOK_MARKER)) return "exists";
|
|
477
|
+
return "skipped";
|
|
478
|
+
} catch {
|
|
479
|
+
return "skipped";
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
if (!(0, import_fs2.existsSync)(hooksDir)) (0, import_fs2.mkdirSync)(hooksDir, { recursive: true });
|
|
484
|
+
(0, import_fs2.writeFileSync)(hookPath, POST_COMMIT_HOOK);
|
|
485
|
+
(0, import_fs2.chmodSync)(hookPath, "755");
|
|
486
|
+
return "installed";
|
|
487
|
+
} catch {
|
|
488
|
+
return "skipped";
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function installHooks() {
|
|
492
|
+
const repos = findGitRepos();
|
|
493
|
+
if (repos.length === 0) {
|
|
494
|
+
console.log(" \xB7 No git repos detected\n");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
console.log(`
|
|
498
|
+
Git hooks (tracks commits during AI sessions):
|
|
499
|
+
`);
|
|
500
|
+
for (const repo of repos.slice(0, 10)) {
|
|
501
|
+
const name = repo.split("/").pop();
|
|
502
|
+
const result = installGitHook(repo);
|
|
503
|
+
if (result === "installed") console.log(` + ${name} \u2014 hook installed`);
|
|
504
|
+
else if (result === "exists") console.log(` \xB7 ${name} \u2014 already installed`);
|
|
505
|
+
else console.log(` \xB7 ${name} \u2014 skipped (existing hook)`);
|
|
506
|
+
}
|
|
507
|
+
console.log(`
|
|
508
|
+
Hooks send: commit hash, message, branch, files changed.`);
|
|
509
|
+
console.log(` No code content is ever sent. Remove .git/hooks/post-commit to uninstall.`);
|
|
510
|
+
}
|
|
411
511
|
async function login() {
|
|
412
|
-
console.log("
|
|
512
|
+
console.log(" CostLens \u2014 Authenticating...\n");
|
|
413
513
|
const sessionId = import_crypto.default.randomBytes(16).toString("hex");
|
|
414
514
|
const authUrl = `${APP_URL}/cli-auth?session=${sessionId}`;
|
|
415
515
|
console.log(` Opening browser: ${authUrl}
|
|
@@ -437,27 +537,24 @@ async function login() {
|
|
|
437
537
|
const data = await res.json();
|
|
438
538
|
if (data.key) {
|
|
439
539
|
writeConfig({ apiKey: data.key });
|
|
440
|
-
console.log("\n
|
|
540
|
+
console.log("\n Authenticated successfully");
|
|
441
541
|
console.log(` Key saved to ${CONFIG_FILE}
|
|
442
542
|
`);
|
|
443
|
-
if (process.argv[2] === "setup")
|
|
444
|
-
|
|
445
|
-
} else {
|
|
446
|
-
process.exit(0);
|
|
447
|
-
}
|
|
543
|
+
if (process.argv[2] === "setup") init();
|
|
544
|
+
else process.exit(0);
|
|
448
545
|
return;
|
|
449
546
|
}
|
|
450
547
|
}
|
|
451
548
|
} catch {
|
|
452
549
|
}
|
|
453
550
|
}
|
|
454
|
-
console.log("\n
|
|
551
|
+
console.log("\n Timed out. Try again.");
|
|
455
552
|
process.exit(1);
|
|
456
553
|
}
|
|
457
554
|
function init() {
|
|
458
555
|
const config = readConfig();
|
|
459
556
|
if (!config.apiKey) {
|
|
460
|
-
console.log("
|
|
557
|
+
console.log(" Not authenticated. Run: npx @costlens/mcp-server login\n");
|
|
461
558
|
process.exit(1);
|
|
462
559
|
}
|
|
463
560
|
const mcpConfig = {
|
|
@@ -465,20 +562,10 @@ function init() {
|
|
|
465
562
|
costlens: {
|
|
466
563
|
command: "npx",
|
|
467
564
|
args: ["-y", "@costlens/mcp-server"],
|
|
468
|
-
env: {
|
|
469
|
-
COSTLENS_MCP_KEY: config.apiKey
|
|
470
|
-
}
|
|
565
|
+
env: { COSTLENS_MCP_KEY: config.apiKey }
|
|
471
566
|
}
|
|
472
567
|
}
|
|
473
568
|
};
|
|
474
|
-
console.log("\u{1F4CB} MCP Configuration:\n");
|
|
475
|
-
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
476
|
-
console.log("\n Add this to your agent config file:\n");
|
|
477
|
-
console.log(" Kiro: ~/.kiro/settings/mcp.json");
|
|
478
|
-
console.log(" Cursor: ~/.cursor/mcp.json");
|
|
479
|
-
console.log(" Claude Code: ~/.claude/mcp_servers.json");
|
|
480
|
-
console.log(" VS Code: .vscode/mcp.json");
|
|
481
|
-
console.log("");
|
|
482
569
|
const kiroPath = (0, import_path2.join)((0, import_os2.homedir)(), ".kiro", "settings", "mcp.json");
|
|
483
570
|
const cursorPath = (0, import_path2.join)((0, import_os2.homedir)(), ".cursor", "mcp.json");
|
|
484
571
|
const claudePath = (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "mcp_servers.json");
|
|
@@ -487,6 +574,8 @@ function init() {
|
|
|
487
574
|
{ name: "Cursor", path: cursorPath },
|
|
488
575
|
{ name: "Claude Code", path: claudePath }
|
|
489
576
|
];
|
|
577
|
+
console.log(" Agent configuration:\n");
|
|
578
|
+
let configured = false;
|
|
490
579
|
for (const { name, path } of paths) {
|
|
491
580
|
if ((0, import_fs2.existsSync)(path)) {
|
|
492
581
|
try {
|
|
@@ -494,50 +583,63 @@ function init() {
|
|
|
494
583
|
if (!existing.mcpServers?.costlens) {
|
|
495
584
|
existing.mcpServers = { ...existing.mcpServers, ...mcpConfig.mcpServers };
|
|
496
585
|
(0, import_fs2.writeFileSync)(path, JSON.stringify(existing, null, 2));
|
|
497
|
-
console.log(`
|
|
586
|
+
console.log(` + ${name} \u2014 configured`);
|
|
587
|
+
configured = true;
|
|
498
588
|
} else {
|
|
499
|
-
console.log(` \xB7 ${name} already configured`);
|
|
589
|
+
console.log(` \xB7 ${name} \u2014 already configured`);
|
|
590
|
+
configured = true;
|
|
500
591
|
}
|
|
501
592
|
} catch {
|
|
502
|
-
console.log(` \xB7
|
|
593
|
+
console.log(` \xB7 ${name} \u2014 could not configure`);
|
|
503
594
|
}
|
|
504
595
|
}
|
|
505
596
|
}
|
|
597
|
+
if (!configured) {
|
|
598
|
+
console.log(" No agents detected. Add manually:\n");
|
|
599
|
+
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
600
|
+
console.log("\n Config paths:");
|
|
601
|
+
console.log(" Kiro: ~/.kiro/settings/mcp.json");
|
|
602
|
+
console.log(" Cursor: ~/.cursor/mcp.json");
|
|
603
|
+
console.log(" Claude Code: ~/.claude/mcp_servers.json");
|
|
604
|
+
console.log(" VS Code: .vscode/mcp.json");
|
|
605
|
+
}
|
|
606
|
+
installHooks();
|
|
506
607
|
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");
|
|
507
|
-
console.log("\n
|
|
608
|
+
console.log("\n Setup complete.\n");
|
|
508
609
|
console.log(" 1. Restart your coding agent");
|
|
509
|
-
console.log(" 2. Start coding \u2014 sessions
|
|
510
|
-
console.log(" 3.
|
|
511
|
-
console.log(" 4. Docs: https://costlens.dev/docs/mcp\n");
|
|
610
|
+
console.log(" 2. Start coding \u2014 sessions tracked automatically");
|
|
611
|
+
console.log(" 3. Dashboard: https://costlens.dev/dashboard\n");
|
|
512
612
|
console.log(" Commands:");
|
|
513
613
|
console.log(" npx @costlens/mcp-server status \u2014 check connection");
|
|
514
614
|
console.log(" npx @costlens/mcp-server login \u2014 re-authenticate");
|
|
615
|
+
console.log(" npx @costlens/mcp-server hooks \u2014 reinstall git hooks");
|
|
515
616
|
console.log("");
|
|
617
|
+
process.exit(0);
|
|
516
618
|
}
|
|
517
619
|
async function status() {
|
|
518
620
|
const config = readConfig();
|
|
519
621
|
if (!config.apiKey) {
|
|
520
|
-
console.log("
|
|
622
|
+
console.log(" Not authenticated. Run: npx @costlens/mcp-server login\n");
|
|
521
623
|
process.exit(1);
|
|
522
624
|
}
|
|
523
|
-
console.log("
|
|
625
|
+
console.log(" CostLens Status\n");
|
|
524
626
|
try {
|
|
525
627
|
const res = await fetch(`${API_BASE2}/v1/spend`, {
|
|
526
628
|
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
527
629
|
signal: AbortSignal.timeout(5e3)
|
|
528
630
|
});
|
|
529
631
|
if (!res.ok) {
|
|
530
|
-
console.log("
|
|
632
|
+
console.log(" Connection failed (invalid key or server error)");
|
|
531
633
|
process.exit(1);
|
|
532
634
|
}
|
|
533
635
|
const data = await res.json();
|
|
534
|
-
console.log(`
|
|
636
|
+
console.log(` Connected`);
|
|
535
637
|
console.log(` Key: ${config.apiKey.slice(0, 8)}...`);
|
|
536
638
|
console.log(` Today: $${(data.today || 0).toFixed(4)}`);
|
|
537
639
|
console.log(` This week: $${(data.week || 0).toFixed(4)}`);
|
|
538
640
|
console.log(` This month: $${(data.month || 0).toFixed(4)}`);
|
|
539
641
|
} catch (e) {
|
|
540
|
-
console.log(`
|
|
642
|
+
console.log(` Could not connect: ${e.message}`);
|
|
541
643
|
process.exit(1);
|
|
542
644
|
}
|
|
543
645
|
}
|
|
@@ -546,11 +648,15 @@ if (command === "login") login();
|
|
|
546
648
|
else if (command === "init") init();
|
|
547
649
|
else if (command === "status") status();
|
|
548
650
|
else if (command === "setup") setup();
|
|
651
|
+
else if (command === "hooks") {
|
|
652
|
+
installHooks();
|
|
653
|
+
process.exit(0);
|
|
654
|
+
} else if (command === "doctor") doctor();
|
|
549
655
|
else {
|
|
550
656
|
init_index();
|
|
551
657
|
}
|
|
552
658
|
async function setup() {
|
|
553
|
-
console.log("
|
|
659
|
+
console.log(" CostLens \u2014 One-step setup\n");
|
|
554
660
|
const existing = readConfig();
|
|
555
661
|
if (existing.apiKey) {
|
|
556
662
|
console.log(" Already authenticated. Running init...\n");
|
|
@@ -559,3 +665,139 @@ async function setup() {
|
|
|
559
665
|
}
|
|
560
666
|
await login();
|
|
561
667
|
}
|
|
668
|
+
async function doctor() {
|
|
669
|
+
console.log("\n CostLens Doctor\n");
|
|
670
|
+
const config = readConfig();
|
|
671
|
+
const issues = [];
|
|
672
|
+
let keyValid = false;
|
|
673
|
+
let planData = null;
|
|
674
|
+
let spendData = null;
|
|
675
|
+
if (!config.apiKey) {
|
|
676
|
+
console.log(" Key: not configured");
|
|
677
|
+
issues.push("No API key. Run: npx @costlens/mcp-server setup");
|
|
678
|
+
} else {
|
|
679
|
+
try {
|
|
680
|
+
const res = await fetch(`${API_BASE2}/v1/plan`, {
|
|
681
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
682
|
+
signal: AbortSignal.timeout(5e3)
|
|
683
|
+
});
|
|
684
|
+
if (res.ok) {
|
|
685
|
+
planData = await res.json();
|
|
686
|
+
keyValid = true;
|
|
687
|
+
console.log(` Key: valid (${config.apiKey.slice(0, 10)}...)`);
|
|
688
|
+
} else {
|
|
689
|
+
console.log(` Key: invalid (${res.status})`);
|
|
690
|
+
issues.push("API key is invalid or expired. Run: npx @costlens/mcp-server login");
|
|
691
|
+
}
|
|
692
|
+
} catch (e) {
|
|
693
|
+
console.log(` Key: error (${e.message})`);
|
|
694
|
+
issues.push("Cannot reach CostLens API. Check your network.");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (planData) {
|
|
698
|
+
console.log(` Plan: ${planData.plan}`);
|
|
699
|
+
console.log(` Tracking: ${planData.keySettings?.trackingMode || "manual"}`);
|
|
700
|
+
}
|
|
701
|
+
if (keyValid) {
|
|
702
|
+
try {
|
|
703
|
+
const res = await fetch(`${API_BASE2}/v1/spend`, {
|
|
704
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
705
|
+
signal: AbortSignal.timeout(5e3)
|
|
706
|
+
});
|
|
707
|
+
if (res.ok) {
|
|
708
|
+
spendData = await res.json();
|
|
709
|
+
const daily = spendData.daily?.cost ?? 0;
|
|
710
|
+
const budget = spendData.dailyBudget;
|
|
711
|
+
if (budget) {
|
|
712
|
+
console.log(` Budget: $${daily.toFixed(2)} / $${budget.toFixed(2)} today`);
|
|
713
|
+
} else {
|
|
714
|
+
console.log(` Spend: $${daily.toFixed(4)} today`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} catch {
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const kiroPath = (0, import_path2.join)((0, import_os2.homedir)(), ".kiro", "settings", "mcp.json");
|
|
721
|
+
const cursorPath = (0, import_path2.join)((0, import_os2.homedir)(), ".cursor", "mcp.json");
|
|
722
|
+
const claudePath = (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "mcp_servers.json");
|
|
723
|
+
const agents = [
|
|
724
|
+
{ name: "Kiro", path: kiroPath },
|
|
725
|
+
{ name: "Cursor", path: cursorPath },
|
|
726
|
+
{ name: "Claude Code", path: claudePath }
|
|
727
|
+
];
|
|
728
|
+
let agentConfigured = false;
|
|
729
|
+
for (const { name, path } of agents) {
|
|
730
|
+
if ((0, import_fs2.existsSync)(path)) {
|
|
731
|
+
try {
|
|
732
|
+
const content = JSON.parse((0, import_fs2.readFileSync)(path, "utf-8"));
|
|
733
|
+
if (content.mcpServers?.costlens) {
|
|
734
|
+
console.log(` MCP: ${name} (configured)`);
|
|
735
|
+
agentConfigured = true;
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (!agentConfigured) {
|
|
743
|
+
console.log(" MCP: not configured");
|
|
744
|
+
issues.push("No agent configured. Run: npx @costlens/mcp-server setup");
|
|
745
|
+
}
|
|
746
|
+
const cwd = process.cwd();
|
|
747
|
+
const hookPath = (0, import_path2.join)(cwd, ".git", "hooks", "post-commit");
|
|
748
|
+
if ((0, import_fs2.existsSync)(hookPath)) {
|
|
749
|
+
try {
|
|
750
|
+
const content = (0, import_fs2.readFileSync)(hookPath, "utf-8");
|
|
751
|
+
if (content.includes(HOOK_MARKER)) {
|
|
752
|
+
console.log(" Git hook: installed (post-commit)");
|
|
753
|
+
} else {
|
|
754
|
+
console.log(" Git hook: exists (not CostLens)");
|
|
755
|
+
}
|
|
756
|
+
} catch {
|
|
757
|
+
console.log(" Git hook: error reading");
|
|
758
|
+
}
|
|
759
|
+
} else if ((0, import_fs2.existsSync)((0, import_path2.join)(cwd, ".git"))) {
|
|
760
|
+
console.log(" Git hook: not installed");
|
|
761
|
+
issues.push("Git hook not installed. Run: npx @costlens/mcp-server hooks");
|
|
762
|
+
} else {
|
|
763
|
+
console.log(" Git hook: no git repo in cwd");
|
|
764
|
+
}
|
|
765
|
+
const pkgPath = (0, import_path2.join)(cwd, "package.json");
|
|
766
|
+
if ((0, import_fs2.existsSync)(pkgPath)) {
|
|
767
|
+
try {
|
|
768
|
+
const pkg = JSON.parse((0, import_fs2.readFileSync)(pkgPath, "utf-8"));
|
|
769
|
+
const sdkVersion = pkg.dependencies?.costlens || pkg.devDependencies?.costlens;
|
|
770
|
+
if (sdkVersion) {
|
|
771
|
+
console.log(` SDK: installed (${sdkVersion})`);
|
|
772
|
+
} else {
|
|
773
|
+
console.log(" SDK: not installed");
|
|
774
|
+
}
|
|
775
|
+
} catch {
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (keyValid) {
|
|
779
|
+
try {
|
|
780
|
+
const res = await fetch(`${API_BASE2}/v1/productivity/github?period=week`, {
|
|
781
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
782
|
+
signal: AbortSignal.timeout(5e3)
|
|
783
|
+
});
|
|
784
|
+
if (res.ok) {
|
|
785
|
+
console.log(" GitHub: connected");
|
|
786
|
+
} else if (res.status === 404) {
|
|
787
|
+
console.log(" GitHub: not connected");
|
|
788
|
+
}
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
console.log("");
|
|
793
|
+
if (issues.length === 0) {
|
|
794
|
+
console.log(" All good.\n");
|
|
795
|
+
} else {
|
|
796
|
+
console.log(" Issues:\n");
|
|
797
|
+
for (const issue of issues) {
|
|
798
|
+
console.log(` - ${issue}`);
|
|
799
|
+
}
|
|
800
|
+
console.log("");
|
|
801
|
+
}
|
|
802
|
+
process.exit(issues.length > 0 ? 1 : 0);
|
|
803
|
+
}
|
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
|
}
|