@agentistics/mcp 1.3.3 → 1.7.6

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/README.md CHANGED
@@ -4,7 +4,7 @@ MCP server that exposes your [Claude Code](https://claude.ai/code) analytics as
4
4
 
5
5
  ## What it does
6
6
 
7
- Reads data from `~/.claude/` (the same data [agentistics](https://github.com/blpsoares/agentistics) visualizes) and exposes it as 12 structured MCP tools.
7
+ Reads data from `~/.claude/` (the same data [agentistics](https://github.com/blpsoares/agentistics) visualizes) and exposes it as 13 structured MCP tools.
8
8
 
9
9
  ## Usage
10
10
 
@@ -36,10 +36,11 @@ The `AGENTISTICS_API` env var must point to a running agentistics server.
36
36
 
37
37
  | Tool | Returns |
38
38
  |------|---------|
39
- | `agentistics_summary` | All-time totals: tokens, cost, sessions, streak, cache hit rate |
40
- | `agentistics_projects` | Per-project token and cost breakdown |
41
- | `agentistics_sessions` | Recent sessions with duration, model, cost |
42
- | `agentistics_costs` | Model pricing breakdown and cache savings |
39
+ | `agentistics_summary` | All-time totals: tokens, cost, sessions, streak unified or per-harness |
40
+ | `agentistics_harnesses` | Side-by-side comparison of every tracked harness |
41
+ | `agentistics_projects` | Per-project token and cost breakdown (optionally per-harness) |
42
+ | `agentistics_sessions` | Recent sessions with harness, duration, model, cost |
43
+ | `agentistics_costs` | Model pricing breakdown and cache savings (optionally per-harness) |
43
44
  | `agentistics_component_catalog` | Available dashboard components |
44
45
  | `agentistics_get_layouts` | Current custom page layouts |
45
46
  | `agentistics_build_layout` | Create a full layout from a component list |
@@ -48,6 +49,7 @@ The `AGENTISTICS_API` env var must point to a running agentistics server.
48
49
  | `agentistics_create_layout` | Create a new empty layout |
49
50
  | `agentistics_set_active_layout` | Switch the active /custom layout |
50
51
  | `agentistics_delete_layout` | Delete a layout permanently |
52
+ | `agentistics_export_pdf` | Generate a PDF report download link for a date range |
51
53
 
52
54
  ## Requirements
53
55
 
@@ -8,9 +8,38 @@ import {
8
8
  ListToolsRequestSchema
9
9
  } from "@modelcontextprotocol/sdk/types.js";
10
10
 
11
+ // ../core/src/local-models.ts
12
+ var LOCAL_RUNTIME_PREFIXES = [
13
+ "ollama",
14
+ "lmstudio",
15
+ "llamacpp",
16
+ "llama.cpp",
17
+ "local/",
18
+ "localhost"
19
+ ];
20
+ function isLocalModelId(modelId) {
21
+ const id = String(modelId ?? "").toLowerCase();
22
+ return LOCAL_RUNTIME_PREFIXES.some((p) => id.startsWith(p));
23
+ }
24
+ var LOCAL_MODEL_PRICE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
25
+
11
26
  // ../core/src/types.ts
27
+ var HARNESS_SORT = {
28
+ claude: 0,
29
+ codex: 1,
30
+ gemini: 2,
31
+ copilot: 3,
32
+ antigravity: 4,
33
+ kimi: 5
34
+ };
35
+ var HARNESS_ORDER = Object.keys(HARNESS_SORT).sort((a, b) => HARNESS_SORT[a] - HARNESS_SORT[b]);
12
36
  var MODEL_PRICING = {
37
+ "claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
38
+ "claude-mythos-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
39
+ "claude-opus-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
40
+ "claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
13
41
  "claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
42
+ "claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
14
43
  "claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
15
44
  "claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
16
45
  "claude-haiku-4-5-20251001": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
@@ -20,21 +49,57 @@ var MODEL_PRICING = {
20
49
  "claude-sonnet-4-5-20250929": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
21
50
  "claude-sonnet-4-20250514": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
22
51
  "claude-haiku-3-5-20241022": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
23
- "claude-3-haiku-20240307": { input: 0.25, output: 1.25, cacheRead: 0.03, cacheWrite: 0.3 }
52
+ "claude-3-haiku-20240307": { input: 0.25, output: 1.25, cacheRead: 0.03, cacheWrite: 0.3 },
53
+ "gemini-3.6-flash": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 1.5 },
54
+ "gemini-3.5-flash-lite": { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0.3 },
55
+ "gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 1.5 },
56
+ "gemini-3.1-flash-lite": { input: 0.25, output: 1.5, cacheRead: 0.025, cacheWrite: 0.25 },
57
+ "gemini-3.1-pro": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2 },
58
+ "gemini-3-flash-preview": { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0.5 },
59
+ "gemini-3-flash": { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0.5 },
60
+ "gemini-2.5-flash": { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0.3 },
61
+ "gpt-5.6-sol": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 5 },
62
+ "gpt-5.6-terra": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 2.5 },
63
+ "gpt-5.6-luna": { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 1 },
64
+ "gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 5 },
65
+ "gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0.75 },
66
+ "gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 2.5 },
67
+ "gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0.25 },
68
+ "gpt-5": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 1.25 }
24
69
  };
25
70
  function getModelPrice(modelId) {
26
71
  if (MODEL_PRICING[modelId])
27
72
  return MODEL_PRICING[modelId];
28
- for (const [key, price] of Object.entries(MODEL_PRICING)) {
29
- if (modelId.startsWith(key) || key.startsWith(modelId))
30
- return price;
73
+ if (isLocalModelId(modelId))
74
+ return LOCAL_MODEL_PRICE;
75
+ const id = String(modelId ?? "");
76
+ let forwardKey = "";
77
+ let reverseKey = "";
78
+ for (const key of Object.keys(MODEL_PRICING)) {
79
+ if (id.startsWith(key)) {
80
+ if (key.length > forwardKey.length)
81
+ forwardKey = key;
82
+ } else if (id && key.startsWith(id) && key[id.length] === "-") {
83
+ if (!reverseKey || key.length < reverseKey.length)
84
+ reverseKey = key;
85
+ }
31
86
  }
87
+ const hit = forwardKey || reverseKey;
88
+ if (hit)
89
+ return MODEL_PRICING[hit];
32
90
  return { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
33
91
  }
34
92
  function calcCost(usage, modelId) {
35
93
  const price = getModelPrice(modelId);
36
94
  return usage.inputTokens / 1e6 * price.input + usage.outputTokens / 1e6 * price.output + usage.cacheReadInputTokens / 1e6 * price.cacheRead + usage.cacheCreationInputTokens / 1e6 * price.cacheWrite;
37
95
  }
96
+ // ../core/src/team.ts
97
+ var DEFAULT_TEAM = Object.freeze({
98
+ schema: 2,
99
+ mode: "solo",
100
+ connections: Object.freeze([])
101
+ });
102
+ var SHARE_SOURCE_TYPES = new Set(["repo", "project", "none"]);
38
103
  // agentistics-mcp.ts
39
104
  var API = process.env.AGENTISTICS_API ?? "http://localhost:47291";
40
105
  function calcCostUSD(input, output, cacheRead, cacheWrite, model) {
@@ -47,6 +112,30 @@ function calcCostUSD(input, output, cacheRead, cacheWrite, model) {
47
112
  costUSD: 0
48
113
  }, model);
49
114
  }
115
+ var HARNESS_IDS = ["claude", "codex", "gemini", "copilot", "antigravity"];
116
+ var HARNESS_PARAM = {
117
+ type: "string",
118
+ enum: ["all", ...HARNESS_IDS],
119
+ description: "Scope to one harness (claude | codex | gemini | copilot | antigravity), or 'all' (default) for the unified view across every harness."
120
+ };
121
+ function sessionHarness(s) {
122
+ return s.harness ?? "claude";
123
+ }
124
+ function filterSessions(sessions, harness) {
125
+ if (!harness || harness === "all")
126
+ return sessions;
127
+ return sessions.filter((s) => sessionHarness(s) === harness);
128
+ }
129
+ function sessionTokens(s) {
130
+ const input = s.input_tokens ?? 0;
131
+ const output = s.output_tokens ?? 0;
132
+ const cacheRead = s.cache_read_input_tokens ?? 0;
133
+ const cacheWrite = s.cache_creation_input_tokens ?? 0;
134
+ return { input, output, cacheRead, cacheWrite, cost: calcCostUSD(input, output, cacheRead, cacheWrite, s.model ?? "") };
135
+ }
136
+ function sessionMessages(s) {
137
+ return (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0);
138
+ }
50
139
  var CATALOG = [
51
140
  { id: "kpi.messages", label: "Messages", category: "kpi", defaultW: 3, defaultH: 3, minW: 2, minH: 2, description: "Total message count in selected period" },
52
141
  { id: "kpi.sessions", label: "Sessions", category: "kpi", defaultW: 3, defaultH: 3, minW: 2, minH: 2, description: "Session count + avg msgs/session" },
@@ -138,31 +227,37 @@ var server = new Server({ name: "agentistics", version: "1.0.0" }, { capabilitie
138
227
  var TOOLS = [
139
228
  {
140
229
  name: "agentistics_summary",
141
- description: "Get an overview of Claude Code usage metrics: total tokens, estimated cost, sessions, streak, most used model, and top project. Good starting point for any metrics question.",
230
+ description: "Get an overview of AI coding usage metrics (across all tracked harnesses — Claude Code, Codex, Gemini, Copilot, Antigravity — or scoped to one): total tokens, estimated cost, sessions, streak, most used model, and top project. Good starting point for any metrics question.",
231
+ inputSchema: { type: "object", properties: { harness: HARNESS_PARAM }, required: [] }
232
+ },
233
+ {
234
+ name: "agentistics_harnesses",
235
+ description: "Compare the tracked AI coding harnesses side by side. Lists every harness present in the data with its sessions, messages, token usage, estimated cost, and last-active date. Use this to answer 'which harness do I use most / costs most' questions.",
142
236
  inputSchema: { type: "object", properties: {}, required: [] }
143
237
  },
144
238
  {
145
239
  name: "agentistics_projects",
146
- description: "List all Claude Code projects with session counts, message counts, token usage, estimated cost, and last active date.",
147
- inputSchema: { type: "object", properties: {}, required: [] }
240
+ description: "List projects with session counts, message counts, token usage, estimated cost, and last active date. Optionally scope to a single harness.",
241
+ inputSchema: { type: "object", properties: { harness: HARNESS_PARAM }, required: [] }
148
242
  },
149
243
  {
150
244
  name: "agentistics_sessions",
151
- description: "Get the most recent Claude Code sessions with project path, duration, message count, token usage, and model.",
245
+ description: "Get the most recent sessions with harness, project path, duration, message count, token usage, and model. Optionally scope to a single harness.",
152
246
  inputSchema: {
153
247
  type: "object",
154
248
  properties: {
155
249
  limit: {
156
250
  type: "number",
157
251
  description: "Max sessions to return (default 20, max 50)"
158
- }
252
+ },
253
+ harness: HARNESS_PARAM
159
254
  }
160
255
  }
161
256
  },
162
257
  {
163
258
  name: "agentistics_costs",
164
- description: "Get cost breakdown by model: token counts (input/output/cache read/write) and estimated USD cost per model.",
165
- inputSchema: { type: "object", properties: {}, required: [] }
259
+ description: "Get cost breakdown by model: token counts (input/output/cache read/write) and estimated USD cost per model. Scope to a single harness, or 'all' (default) for the unified breakdown.",
260
+ inputSchema: { type: "object", properties: { harness: HARNESS_PARAM }, required: [] }
166
261
  },
167
262
  {
168
263
  name: "agentistics_component_catalog",
@@ -300,6 +395,37 @@ var TOOLS = [
300
395
  },
301
396
  required: ["name", "componentIds"]
302
397
  }
398
+ },
399
+ {
400
+ name: "agentistics_tags",
401
+ description: "List all tags — saved cross-cutting filters over repos/projects/machines/teams/accounts — with their aggregate sessions, cost, and tokens. Use for 'what tags exist' or 'how much did tag X cost' questions.",
402
+ inputSchema: { type: "object", properties: {}, required: [] }
403
+ },
404
+ {
405
+ name: "agentistics_tag_detail",
406
+ description: "Get full detail for one tag by name or id: aggregate totals plus per-source breakdown and distributions (top projects, models, harnesses, repos, members/users), daily activity series, and the active date window. Call agentistics_tags first to find the id/name.",
407
+ inputSchema: {
408
+ type: "object",
409
+ properties: {
410
+ tag: { type: "string", description: "Tag name or id, from agentistics_tags" }
411
+ },
412
+ required: ["tag"]
413
+ }
414
+ },
415
+ {
416
+ name: "agentistics_repos",
417
+ description: "List repositories, grouped by normalized git remote (independent of local path or which machine produced the session), with session/message/token/cost totals and last-active date. Sessions with no linked repository are grouped under 'unlinked'. Optionally scope to a single harness.",
418
+ inputSchema: { type: "object", properties: { harness: HARNESS_PARAM }, required: [] }
419
+ },
420
+ {
421
+ name: "agentistics_team_status",
422
+ description: "Get this machine's team-mode status: solo, central, or member. In member mode, lists its central connections (label, endpoint, connected/error state, last push). Use for 'am I connected to a team' or 'what central do I push to' questions.",
423
+ inputSchema: { type: "object", properties: {}, required: [] }
424
+ },
425
+ {
426
+ name: "agentistics_team_members",
427
+ description: "List members of this team, central only: display name, online/presence status, latency, and last-seen timestamp. For usage/cost per member use agentistics_summary or agentistics_sessions against this central's own /api/data. Returns an explanatory message when this machine is not running as a central (solo/member mode).",
428
+ inputSchema: { type: "object", properties: {}, required: [] }
303
429
  }
304
430
  ];
305
431
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
@@ -308,71 +434,106 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
308
434
  try {
309
435
  switch (name) {
310
436
  case "agentistics_summary": {
437
+ const harness = args?.harness;
438
+ const unified = !harness || harness === "all";
311
439
  const data = await apiGet("/api/data");
312
440
  const sc = data.statsCache ?? {};
313
441
  const totals = sc.allTimeTotals ?? {};
314
- const models = sc.modelUsage ?? {};
315
- const topModel = Object.entries(models).sort(([, a], [, b]) => (b.totalTokens ?? 0) - (a.totalTokens ?? 0))[0]?.[0] ?? "—";
316
- const projects = data.projects ?? [];
317
- const topProject = [...projects].sort((a, b) => (b.sessions?.length ?? 0) - (a.sessions?.length ?? 0))[0]?.name ?? "—";
318
- const allSessions = data.sessions ?? [];
442
+ const allSessions = filterSessions(data.sessions ?? [], harness);
319
443
  let totalCostUSD = 0;
320
444
  let totalInput = 0, totalOutput = 0, totalCacheRead = 0, totalCacheWrite = 0;
445
+ const modelTokens = {};
446
+ const projectSessions = {};
447
+ const activeDates = new Set;
321
448
  for (const s of allSessions) {
322
- const inp = s.input_tokens ?? 0;
323
- const out = s.output_tokens ?? 0;
324
- const cr = s.cache_read_input_tokens ?? 0;
325
- const cw = s.cache_creation_input_tokens ?? 0;
326
- totalInput += inp;
327
- totalOutput += out;
328
- totalCacheRead += cr;
329
- totalCacheWrite += cw;
330
- totalCostUSD += calcCostUSD(inp, out, cr, cw, s.model ?? "");
449
+ const { input, output, cacheRead, cacheWrite, cost } = sessionTokens(s);
450
+ totalInput += input;
451
+ totalOutput += output;
452
+ totalCacheRead += cacheRead;
453
+ totalCacheWrite += cacheWrite;
454
+ totalCostUSD += cost;
455
+ if (s.model)
456
+ modelTokens[s.model] = (modelTokens[s.model] ?? 0) + input + output;
457
+ if (s.project_path)
458
+ projectSessions[s.project_path] = (projectSessions[s.project_path] ?? 0) + 1;
459
+ if (s.start_time)
460
+ activeDates.add(String(s.start_time).slice(0, 10));
331
461
  }
332
- const finalInput = totalInput || (totals.inputTokens ?? 0);
333
- const finalOutput = totalOutput || (totals.outputTokens ?? 0);
334
- const finalCacheRead = totalCacheRead || (totals.cacheReadTokens ?? 0);
335
- const finalCacheWrite = totalCacheWrite || (totals.cacheWriteTokens ?? 0);
462
+ const claudeFallback = unified || harness === "claude";
463
+ const topModel = Object.entries(modelTokens).sort(([, a], [, b]) => b - a)[0]?.[0] ?? (claudeFallback ? Object.entries(sc.modelUsage ?? {}).sort(([, a], [, b]) => (b.totalTokens ?? 0) - (a.totalTokens ?? 0))[0]?.[0] ?? "—" : "—");
464
+ const topProject = Object.entries(projectSessions).sort(([, a], [, b]) => b - a)[0]?.[0] ?? "—";
336
465
  return {
337
466
  content: [{
338
467
  type: "text",
339
468
  text: JSON.stringify({
340
- totalInputTokens: finalInput,
341
- totalOutputTokens: finalOutput,
342
- totalCacheReadTokens: finalCacheRead,
343
- totalCacheWriteTokens: finalCacheWrite,
469
+ harness: unified ? "all" : harness,
470
+ totalInputTokens: totalInput || (claudeFallback ? totals.inputTokens ?? 0 : 0),
471
+ totalOutputTokens: totalOutput || (claudeFallback ? totals.outputTokens ?? 0 : 0),
472
+ totalCacheReadTokens: totalCacheRead || (claudeFallback ? totals.cacheReadTokens ?? 0 : 0),
473
+ totalCacheWriteTokens: totalCacheWrite || (claudeFallback ? totals.cacheWriteTokens ?? 0 : 0),
344
474
  estimatedCostUSD: Math.round(totalCostUSD * 100) / 100,
345
475
  totalSessions: allSessions.length,
346
- totalProjects: projects.length,
347
476
  topModel,
348
477
  topProject,
349
- activeDays: sc.activeDays ?? 0,
350
- currentStreak: sc.currentStreak ?? 0
478
+ activeDays: activeDates.size || (claudeFallback ? sc.activeDays ?? 0 : 0),
479
+ currentStreak: claudeFallback ? sc.currentStreak ?? 0 : null
351
480
  }, null, 2)
352
481
  }]
353
482
  };
354
483
  }
484
+ case "agentistics_harnesses": {
485
+ const data = await apiGet("/api/data");
486
+ const present = data.harnesses ?? ["claude"];
487
+ const sessions = data.sessions ?? [];
488
+ const rows = present.map((h) => {
489
+ const hs = sessions.filter((s) => sessionHarness(s) === h);
490
+ let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0, messages = 0, lastActive = "";
491
+ for (const s of hs) {
492
+ const t = sessionTokens(s);
493
+ input += t.input;
494
+ output += t.output;
495
+ cacheRead += t.cacheRead;
496
+ cacheWrite += t.cacheWrite;
497
+ cost += t.cost;
498
+ messages += sessionMessages(s);
499
+ if (s.start_time && String(s.start_time) > lastActive)
500
+ lastActive = String(s.start_time);
501
+ }
502
+ return {
503
+ harness: h,
504
+ sessions: hs.length,
505
+ messages,
506
+ inputTokens: input,
507
+ outputTokens: output,
508
+ cacheReadTokens: cacheRead,
509
+ cacheWriteTokens: cacheWrite,
510
+ totalTokens: input + output + cacheRead + cacheWrite,
511
+ estimatedCostUSD: Math.round(cost * 100) / 100,
512
+ lastActive: lastActive || null
513
+ };
514
+ }).sort((a, b) => b.totalTokens - a.totalTokens);
515
+ return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
516
+ }
355
517
  case "agentistics_projects": {
518
+ const harness = args?.harness;
356
519
  const data = await apiGet("/api/data");
357
- const allSessions = data.sessions ?? [];
520
+ const allSessions = filterSessions(data.sessions ?? [], harness);
358
521
  const byPath = {};
359
522
  for (const s of allSessions) {
360
523
  const key = s.project_path;
361
524
  if (!key)
362
525
  continue;
363
526
  if (!byPath[key])
364
- byPath[key] = { inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheWrite: 0, costUSD: 0, messages: 0, lastActive: "", languages: [] };
527
+ byPath[key] = { sessions: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheWrite: 0, costUSD: 0, messages: 0, lastActive: "", languages: [] };
365
528
  const agg = byPath[key];
366
- const inp = s.input_tokens ?? 0;
367
- const out = s.output_tokens ?? 0;
368
- const cr = s.cache_read_input_tokens ?? 0;
369
- const cw = s.cache_creation_input_tokens ?? 0;
370
- agg.inputTokens += inp;
371
- agg.outputTokens += out;
372
- agg.cacheRead += cr;
373
- agg.cacheWrite += cw;
374
- agg.costUSD += calcCostUSD(inp, out, cr, cw, s.model ?? "");
375
- agg.messages += (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0);
529
+ const { input, output, cacheRead, cacheWrite, cost } = sessionTokens(s);
530
+ agg.sessions += 1;
531
+ agg.inputTokens += input;
532
+ agg.outputTokens += output;
533
+ agg.cacheRead += cacheRead;
534
+ agg.cacheWrite += cacheWrite;
535
+ agg.costUSD += cost;
536
+ agg.messages += sessionMessages(s);
376
537
  if (!agg.lastActive || (s.start_time ?? "") > agg.lastActive)
377
538
  agg.lastActive = s.start_time ?? "";
378
539
  for (const lang of s.languages ?? [])
@@ -380,12 +541,12 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
380
541
  agg.languages.push(lang);
381
542
  }
382
543
  const projects = data.projects ?? [];
383
- const summary = projects.map((p) => {
384
- const agg = byPath[p.path] ?? { inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheWrite: 0, costUSD: 0, messages: 0, lastActive: "", languages: [] };
544
+ const summary = projects.filter((p) => !harness || harness === "all" || byPath[p.path]).map((p) => {
545
+ const agg = byPath[p.path] ?? { sessions: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheWrite: 0, costUSD: 0, messages: 0, lastActive: "", languages: [] };
385
546
  return {
386
547
  name: p.name,
387
548
  path: p.path,
388
- sessions: (p.sessions ?? []).length,
549
+ sessions: !harness || harness === "all" ? (p.sessions ?? []).length : agg.sessions,
389
550
  messages: agg.messages,
390
551
  inputTokens: agg.inputTokens,
391
552
  outputTokens: agg.outputTokens,
@@ -399,40 +560,65 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
399
560
  }
400
561
  case "agentistics_sessions": {
401
562
  const limit = Math.min(args?.limit ?? 20, 50);
563
+ const harness = args?.harness;
402
564
  const data = await apiGet("/api/data");
403
- const rows = (data.sessions ?? []).slice(0, limit).map((s) => {
404
- const inp = s.input_tokens ?? 0;
405
- const out = s.output_tokens ?? 0;
406
- const cr = s.cache_read_input_tokens ?? 0;
407
- const cw = s.cache_creation_input_tokens ?? 0;
565
+ const rows = filterSessions(data.sessions ?? [], harness).slice(0, limit).map((s) => {
566
+ const { input, output, cacheRead, cacheWrite, cost } = sessionTokens(s);
408
567
  return {
409
568
  id: s.session_id,
569
+ harness: sessionHarness(s),
410
570
  project: s.project_path,
411
571
  startedAt: s.start_time,
412
572
  durationMinutes: s.duration_minutes,
413
- messages: (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0),
414
- inputTokens: inp,
415
- outputTokens: out,
416
- cacheReadTokens: cr,
417
- cacheWriteTokens: cw,
418
- totalTokens: inp + out + cr + cw,
419
- estimatedCostUSD: Math.round(calcCostUSD(inp, out, cr, cw, s.model ?? "") * 1e4) / 1e4,
573
+ messages: sessionMessages(s),
574
+ inputTokens: input,
575
+ outputTokens: output,
576
+ cacheReadTokens: cacheRead,
577
+ cacheWriteTokens: cacheWrite,
578
+ totalTokens: input + output + cacheRead + cacheWrite,
579
+ estimatedCostUSD: Math.round(cost * 1e4) / 1e4,
420
580
  model: s.model ?? null
421
581
  };
422
582
  });
423
583
  return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
424
584
  }
425
585
  case "agentistics_costs": {
586
+ const harness = args?.harness;
426
587
  const data = await apiGet("/api/data");
427
- const usage = data.statsCache?.modelUsage ?? {};
428
- const breakdown = Object.entries(usage).map(([model, u]) => ({
588
+ if (!harness || harness === "all" || harness === "claude") {
589
+ const usage = data.statsCache?.modelUsage ?? {};
590
+ const breakdown2 = Object.entries(usage).map(([model, u]) => ({
591
+ model,
592
+ inputTokens: u.inputTokens ?? 0,
593
+ outputTokens: u.outputTokens ?? 0,
594
+ cacheReadTokens: u.cacheReadTokens ?? 0,
595
+ cacheWriteTokens: u.cacheWriteTokens ?? 0,
596
+ totalTokens: u.totalTokens ?? 0,
597
+ estimatedCostUSD: u.costUSD ?? 0
598
+ })).sort((a, b) => b.totalTokens - a.totalTokens);
599
+ return { content: [{ type: "text", text: JSON.stringify(breakdown2, null, 2) }] };
600
+ }
601
+ const byModel = {};
602
+ for (const s of filterSessions(data.sessions ?? [], harness)) {
603
+ const model = s.model || "unknown";
604
+ if (!byModel[model])
605
+ byModel[model] = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, estimatedCostUSD: 0 };
606
+ const agg = byModel[model];
607
+ const { input, output, cacheRead, cacheWrite, cost } = sessionTokens(s);
608
+ agg.inputTokens += input;
609
+ agg.outputTokens += output;
610
+ agg.cacheReadTokens += cacheRead;
611
+ agg.cacheWriteTokens += cacheWrite;
612
+ agg.estimatedCostUSD += cost;
613
+ }
614
+ const breakdown = Object.entries(byModel).map(([model, u]) => ({
429
615
  model,
430
- inputTokens: u.inputTokens ?? 0,
431
- outputTokens: u.outputTokens ?? 0,
432
- cacheReadTokens: u.cacheReadTokens ?? 0,
433
- cacheWriteTokens: u.cacheWriteTokens ?? 0,
434
- totalTokens: u.totalTokens ?? 0,
435
- estimatedCostUSD: u.costUSD ?? 0
616
+ inputTokens: u.inputTokens,
617
+ outputTokens: u.outputTokens,
618
+ cacheReadTokens: u.cacheReadTokens,
619
+ cacheWriteTokens: u.cacheWriteTokens,
620
+ totalTokens: u.inputTokens + u.outputTokens + u.cacheReadTokens + u.cacheWriteTokens,
621
+ estimatedCostUSD: Math.round(u.estimatedCostUSD * 1e4) / 1e4
436
622
  })).sort((a, b) => b.totalTokens - a.totalTokens);
437
623
  return { content: [{ type: "text", text: JSON.stringify(breakdown, null, 2) }] };
438
624
  }
@@ -610,6 +796,133 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
610
796
  }]
611
797
  };
612
798
  }
799
+ case "agentistics_tags": {
800
+ const data = await apiGet("/api/tags");
801
+ const tags = data.tags ?? [];
802
+ const rows = tags.map((t) => ({
803
+ id: t._id,
804
+ name: t.name,
805
+ color: t.color ?? null,
806
+ sessions: t.aggregate?.sessions ?? 0,
807
+ estimatedCostUSD: Math.round((t.aggregate?.costUSD ?? 0) * 1e4) / 1e4,
808
+ inputTokens: t.aggregate?.inputTokens ?? 0,
809
+ outputTokens: t.aggregate?.outputTokens ?? 0,
810
+ topProject: t.aggregate?.topProject ?? null,
811
+ topModel: t.aggregate?.topModel ?? null,
812
+ topHarness: t.aggregate?.topHarness ?? null,
813
+ window: t.window ?? null
814
+ }));
815
+ return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
816
+ }
817
+ case "agentistics_tag_detail": {
818
+ const wanted = (args?.tag ?? "").trim();
819
+ if (!wanted)
820
+ throw new Error("tag is required — pass a name or id from agentistics_tags.");
821
+ const list = await apiGet("/api/tags");
822
+ const tags = list.tags ?? [];
823
+ const match = tags.find((t) => t._id === wanted) ?? tags.find((t) => t.name?.toLowerCase() === wanted.toLowerCase());
824
+ if (!match)
825
+ throw new Error(`No tag matching "${wanted}". Available: ${tags.map((t) => t.name).join(", ") || "(none)"}`);
826
+ const detail = await apiGet(`/api/tags/${encodeURIComponent(match._id)}`);
827
+ const d = detail;
828
+ const bucketTop = (b, n = 5) => (b ?? []).slice(0, n).map((x) => ({ key: x.key, sessions: x.sessions, estimatedCostUSD: Math.round((x.costUSD ?? 0) * 1e4) / 1e4 }));
829
+ return {
830
+ content: [{
831
+ type: "text",
832
+ text: JSON.stringify({
833
+ id: d.tag?._id,
834
+ name: d.tag?.name,
835
+ color: d.tag?.color ?? null,
836
+ window: d.tag?.window ?? null,
837
+ sources: d.tag?.sources ?? [],
838
+ aggregate: {
839
+ sessions: d.tag?.aggregate?.sessions ?? 0,
840
+ estimatedCostUSD: Math.round((d.tag?.aggregate?.costUSD ?? 0) * 1e4) / 1e4,
841
+ inputTokens: d.tag?.aggregate?.inputTokens ?? 0,
842
+ outputTokens: d.tag?.aggregate?.outputTokens ?? 0
843
+ },
844
+ topProjects: bucketTop(d.stats?.projects),
845
+ topModels: bucketTop(d.stats?.models),
846
+ topHarnesses: bucketTop(d.stats?.harnesses),
847
+ topRepos: bucketTop(d.stats?.repos),
848
+ topMembers: bucketTop(d.stats?.members),
849
+ distinctMembers: d.stats?.distinctMembers ?? 0,
850
+ distinctMachines: d.stats?.distinctMachines ?? 0,
851
+ firstSessionDate: d.stats?.firstSessionDate ?? null,
852
+ lastSessionDate: d.stats?.lastSessionDate ?? null
853
+ }, null, 2)
854
+ }]
855
+ };
856
+ }
857
+ case "agentistics_repos": {
858
+ const harness = args?.harness;
859
+ const data = await apiGet("/api/data");
860
+ const allSessions = filterSessions(data.sessions ?? [], harness);
861
+ const byRemote = {};
862
+ for (const s of allSessions) {
863
+ const key = s.git_remote || "";
864
+ if (!byRemote[key])
865
+ byRemote[key] = { sessions: 0, messages: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheWrite: 0, costUSD: 0, lastActive: "" };
866
+ const agg = byRemote[key];
867
+ const { input, output, cacheRead, cacheWrite, cost } = sessionTokens(s);
868
+ agg.sessions += 1;
869
+ agg.messages += sessionMessages(s);
870
+ agg.inputTokens += input;
871
+ agg.outputTokens += output;
872
+ agg.cacheRead += cacheRead;
873
+ agg.cacheWrite += cacheWrite;
874
+ agg.costUSD += cost;
875
+ if (s.start_time && String(s.start_time) > agg.lastActive)
876
+ agg.lastActive = String(s.start_time);
877
+ }
878
+ const rows = Object.entries(byRemote).map(([remote, agg]) => ({
879
+ repo: remote || "unlinked",
880
+ remote: remote || null,
881
+ sessions: agg.sessions,
882
+ messages: agg.messages,
883
+ inputTokens: agg.inputTokens,
884
+ outputTokens: agg.outputTokens,
885
+ totalTokens: agg.inputTokens + agg.outputTokens + agg.cacheRead + agg.cacheWrite,
886
+ estimatedCostUSD: Math.round(agg.costUSD * 1e4) / 1e4,
887
+ lastActive: agg.lastActive || null
888
+ })).sort((a, b) => b.totalTokens - a.totalTokens);
889
+ return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
890
+ }
891
+ case "agentistics_team_status": {
892
+ const prefs = await getPrefs();
893
+ const team2 = prefs.team;
894
+ const mode = team2?.mode ?? "solo";
895
+ const connections = (team2?.connections ?? []).map((c) => ({
896
+ id: c.id,
897
+ label: c.label ?? null,
898
+ endpoint: c.endpoint
899
+ }));
900
+ return {
901
+ content: [{
902
+ type: "text",
903
+ text: JSON.stringify({ mode, connections }, null, 2)
904
+ }]
905
+ };
906
+ }
907
+ case "agentistics_team_members": {
908
+ const res = await fetch(`${API}/api/team/members`);
909
+ if (res.status === 404) {
910
+ return { content: [{ type: "text", text: "This machine is not running as a central (solo or member mode) — there is no member roster here. Use agentistics_team_status to see the current mode." }] };
911
+ }
912
+ if (!res.ok)
913
+ throw new Error(`GET /api/team/members → HTTP ${res.status}`);
914
+ const data = await res.json();
915
+ const members = data.members ?? [];
916
+ const rows = members.map((m) => ({
917
+ id: m.id,
918
+ name: m.user,
919
+ label: m.label,
920
+ online: m.online ?? false,
921
+ latencyMs: m.latencyMs ?? null,
922
+ lastSeenAt: m.lastSeenAt ?? null
923
+ }));
924
+ return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
925
+ }
613
926
  default:
614
927
  throw new Error(`Unknown tool: ${name}`);
615
928
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentistics/mcp",
3
- "version": "1.3.3",
3
+ "version": "1.7.6",
4
4
  "description": "Agentistics MCP server — Claude Code analytics for Claude Desktop and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./dist/agentistics-mcp.js",