@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.
Files changed (3) hide show
  1. package/dist/cli.js +279 -170
  2. package/dist/index.js +110 -110
  3. 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 headers = {};
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 fetch(`${API_BASE}/v1/productivity/heartbeat`, {
74
+ const res = await apiFetch("/v1/productivity/heartbeat", {
53
75
  method: "POST",
54
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
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 syncEvent(result, params) {
92
+ async function ensureAutoSession() {
93
+ if (trackingMode !== "auto" || !API_KEY || !productivityEnabled || activeSessionId) return;
73
94
  try {
74
- await fetch(`${API_BASE}/v1/classifier/events`, {
95
+ const res = await apiFetch("/v1/productivity/sessions", {
75
96
  method: "POST",
76
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
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 checkFeature(feature) {
91
- if (!API_KEY) return false;
106
+ async function autoLogEvent(eventType, metadata) {
107
+ if (trackingMode !== "auto" || !activeSessionId || !API_KEY) return;
92
108
  try {
93
- const res = await fetch(`${API_BASE}/v1/plan`, {
94
- headers: { "Authorization": `Bearer ${API_KEY}` },
95
- signal: AbortSignal.timeout(5e3)
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
- return false;
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
- productivityEnabled = await checkFeature("productivity_basic");
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 fetch(`${API_BASE}/v1/productivity/sessions/${activeSessionId}`, {
148
+ await apiFetch(`/v1/productivity/sessions/${activeSessionId}`, {
120
149
  method: "PATCH",
121
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
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, server, activeSessionId, heartbeatInterval, productivityEnabled;
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
- if (!API_KEY) {
159
- return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
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 fetch(`${API_BASE}/v1/spend`, {
163
- headers: { "Authorization": `Bearer ${API_KEY}` },
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
- `\u{1F4B0} Spend Summary (${data.key})`,
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
- lines.push(`Session: $${data.session.cost.toFixed(4)} (${data.session.requests} requests) [${data.session.correlationId}]`);
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
- syncEvent(result, params).catch(() => {
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 fetch(`${API_BASE}/v1/productivity/sessions`, {
262
+ const res = await apiFetch("/v1/productivity/sessions", {
232
263
  method: "POST",
233
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
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 fetch(`${API_BASE}/v1/productivity/sessions/${params.session_id}`, {
288
+ const res = await apiFetch(`/v1/productivity/sessions/${params.session_id}`, {
259
289
  method: "PATCH",
260
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
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 fetch(`${API_BASE}/v1/productivity/events`, {
316
+ const res = await apiFetch("/v1/productivity/events", {
291
317
  method: "POST",
292
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
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 fetch(`${API_BASE}/v1/productivity/summary?period=${period}`, {
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
- `\u{1F4CA} Productivity Summary (${period})`,
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
- if (!API_KEY) {
347
- return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
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 fetch(`${API_BASE}/v1/productivity/github?period=${period}`, {
352
- headers: { "Authorization": `Bearer ${API_KEY}` },
353
- signal: AbortSignal.timeout(5e3)
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
- async function login() {
402
- console.log("\u{1F511} CostLens \u2014 Authenticating...\n");
403
- const port = 9876 + Math.floor(Math.random() * 100);
404
- const server2 = (0, import_http.createServer)((req, res) => {
405
- const url = new URL(req.url, `http://localhost:${port}`);
406
- const key = url.searchParams.get("key");
407
- if (key) {
408
- writeConfig({ apiKey: key });
409
- res.writeHead(200, { "Content-Type": "text/html" });
410
- res.end('<html><body style="font-family:sans-serif;text-align:center;padding:60px"><h2>Authenticated</h2><p>You can close this tab and return to your terminal.</p></body></html>');
411
- console.log("\u2713 Authenticated successfully");
412
- console.log(` Key saved to ${CONFIG_FILE}
413
- `);
414
- setTimeout(() => {
415
- server2.close();
416
- if (process.argv[2] === "setup") {
417
- init();
418
- } else {
419
- process.exit(0);
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
- }, 500);
422
- } else {
423
- res.writeHead(400);
424
- res.end("Missing key");
459
+ }
460
+ } catch {
425
461
  }
426
- });
427
- server2.listen(port, () => {
428
- const authUrl = `${APP_URL}/cli-auth?port=${port}`;
429
- console.log(` Opening browser: ${authUrl}
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
- const platform = process.platform;
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
- if (platform === "darwin") (0, import_child_process.execSync)(`open "${authUrl}"`);
434
- else if (platform === "linux") (0, import_child_process.execSync)(`xdg-open "${authUrl}"`);
435
- else if (platform === "win32") (0, import_child_process.execSync)(`start "${authUrl}"`);
436
- else console.log(` Open this URL manually: ${authUrl}`);
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
- console.log(" Waiting for authentication...");
441
- });
442
- setTimeout(() => {
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("\u2717 Not authenticated. Run: npx @costlens/mcp-server login\n");
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(` \u2713 Auto-configured ${name} (${path})`);
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 Could not auto-configure ${name}`);
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 \u2713 Setup complete. What's next:\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 are tracked automatically");
501
- console.log(" 3. View your dashboard: https://costlens.dev/dashboard");
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("\u2717 Not authenticated. Run: npx @costlens/mcp-server login\n");
617
+ console.log(" Not authenticated. Run: npx @costlens/mcp-server login\n");
512
618
  process.exit(1);
513
619
  }
514
- console.log("\u{1F4CA} CostLens Status\n");
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(" \u2717 Connection failed (invalid key or server error)");
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(` \u2713 Connected`);
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(` \u2717 Could not connect: ${e.message}`);
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("\u{1F527} CostLens \u2014 One-step setup\n");
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 headers = {};
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
- var server = new import_mcp.McpServer({
39
- name: "@lens360/mcp-server",
40
- version: "0.3.1"
41
- });
42
- var activeSessionId = null;
43
- var heartbeatInterval = null;
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 fetch(`${API_BASE}/v1/productivity/heartbeat`, {
61
+ const res = await apiFetch("/v1/productivity/heartbeat", {
51
62
  method: "POST",
52
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
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
- if (!API_KEY) {
76
- return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
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 fetch(`${API_BASE}/v1/spend`, {
80
- headers: { "Authorization": `Bearer ${API_KEY}` },
81
- signal: AbortSignal.timeout(5e3)
82
- });
83
- if (!res.ok) {
84
- return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
85
- }
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
- `\u{1F4B0} Spend Summary (${data.key})`,
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
- lines.push(`Session: $${data.session.cost.toFixed(4)} (${data.session.requests} requests) [${data.session.correlationId}]`);
96
- }
97
- if (data.dailyBudget) {
98
- lines.push(``, `Budget: $${data.budgetRemaining.toFixed(2)} remaining of $${data.dailyBudget.toFixed(2)}/day`);
99
- }
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
- syncEvent(result, params).catch(() => {
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 syncEvent(result, params) {
136
- try {
137
- await fetch(`${API_BASE}/v1/classifier/events`, {
138
- method: "POST",
139
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
140
- body: JSON.stringify({
141
- complexity: result.level,
142
- confidence: result.confidence,
143
- suggestedModel: result.suggestedModel,
144
- currentModel: params.currentModel,
145
- provider: params.provider,
146
- staticScore: result.signals.staticScore
147
- }),
148
- signal: AbortSignal.timeout(5e3)
149
- });
150
- } catch {
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 fetch(`${API_BASE}/v1/plan`, {
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 fetch(`${API_BASE}/v1/productivity/sessions`, {
211
+ const res = await apiFetch("/v1/productivity/sessions", {
189
212
  method: "POST",
190
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
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 fetch(`${API_BASE}/v1/productivity/sessions/${params.session_id}`, {
237
+ const res = await apiFetch(`/v1/productivity/sessions/${params.session_id}`, {
216
238
  method: "PATCH",
217
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
218
- body: JSON.stringify({ outcome: params.outcome, artifacts: params.artifacts }),
219
- signal: AbortSignal.timeout(5e3)
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 fetch(`${API_BASE}/v1/productivity/events`, {
265
+ const res = await apiFetch("/v1/productivity/events", {
248
266
  method: "POST",
249
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
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 fetch(`${API_BASE}/v1/productivity/summary?period=${period}`, {
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
- `\u{1F4CA} Productivity Summary (${period})`,
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
- if (!API_KEY) {
304
- return { content: [{ type: "text", text: "Not connected to CostLens. Run: npx @costlens/mcp-server setup" }] };
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 fetch(`${API_BASE}/v1/productivity/github?period=${period}`, {
309
- headers: { "Authorization": `Bearer ${API_KEY}` },
310
- signal: AbortSignal.timeout(5e3)
311
- });
312
- if (res.status === 404) {
313
- return { content: [{ type: "text", text: "GitHub not connected. Connect in Settings \u2192 GitHub at costlens.dev" }] };
314
- }
315
- if (!res.ok) {
316
- return { content: [{ type: "text", text: `Error: ${res.status} \u2014 ${await res.text()}` }] };
317
- }
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 fetch(`${API_BASE}/v1/productivity/sessions/${activeSessionId}`, {
342
+ await apiFetch(`/v1/productivity/sessions/${activeSessionId}`, {
341
343
  method: "PATCH",
342
- headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@costlens/mcp-server",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for AI cost optimization with prompt complexity classification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",