@agentistics/mcp 2.17.0 → 2.18.1

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.
@@ -380,7 +380,7 @@ function filterSessions(sessions, harness) {
380
380
  return sessions;
381
381
  return sessions.filter((s) => sessionHarness(s) === harness);
382
382
  }
383
- function sessionTokens(s) {
383
+ function sessionTokens2(s) {
384
384
  const input = s.input_tokens ?? 0;
385
385
  const output = s.output_tokens ?? 0;
386
386
  const cacheRead = s.cache_read_input_tokens ?? 0;
@@ -453,6 +453,20 @@ async function apiGet(path) {
453
453
  async function getPrefs() {
454
454
  return apiGet("/api/preferences");
455
455
  }
456
+ async function apiSend(method, path, body) {
457
+ const res = await fetch(`${API}${path}`, {
458
+ method,
459
+ ...body === undefined ? {} : {
460
+ headers: { "Content-Type": "application/json" },
461
+ body: JSON.stringify(body)
462
+ }
463
+ });
464
+ if (!res.ok) {
465
+ const detail = await res.text().catch(() => "");
466
+ throw new Error(`${method} ${path} → HTTP ${res.status}${detail ? ` ${detail.slice(0, 400)}` : ""}`);
467
+ }
468
+ return res.json().catch(() => ({}));
469
+ }
456
470
  async function putPrefs(patch) {
457
471
  const res = await fetch(`${API}/api/preferences`, {
458
472
  method: "PUT",
@@ -503,6 +517,188 @@ function fillGaps(items, gridW = 12) {
503
517
  }
504
518
  var server = new Server({ name: "agentistics", version: "1.0.0" }, { capabilities: { tools: {} } });
505
519
  var TOOLS = [
520
+ {
521
+ name: "agentistics_tasks",
522
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. List every task on the ALM board with its rollup (cost, rounds, sessions used, tokens) plus a board-wide overview: tasks in flight, delivered, average cost per task and per delivery, top models and harnesses. START HERE to see what work exists and what the other assistants are on.",
523
+ inputSchema: { type: "object", properties: {}, required: [] }
524
+ },
525
+ {
526
+ name: "agentistics_task",
527
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. Open ONE task: its attempts (one per configuration tried), the sessions filed under it with their live state, its comments, subtasks, files, links and full metrics (models, harnesses, agent runs, tokens, delivery time). `ref` is the task id or its exact title.",
528
+ inputSchema: {
529
+ type: "object",
530
+ properties: { ref: { type: "string", description: "Task id or exact title." } },
531
+ required: ["ref"]
532
+ }
533
+ },
534
+ {
535
+ name: "agentistics_task_create",
536
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. Create a task. A title is required; a description is optional. Creating a title that already exists returns the existing task rather than a duplicate, so this is safe to call before filing work under a name you are not sure exists.",
537
+ inputSchema: {
538
+ type: "object",
539
+ properties: { title: { type: "string" }, detail: { type: "string" } },
540
+ required: ["title"]
541
+ }
542
+ },
543
+ {
544
+ name: "agentistics_task_status",
545
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. Move a task: backlog | todo | in_progress | blocked | in_review | done | abandoned. Only `done` stamps a delivery and closes rounds-to-delivery; `abandoned` records that the work was given up on, which is a real outcome and not a failure to report. **`blocked` REQUIRES a `reason` or a `blockedBy` list** and is refused (422) without one: it is the status that names a problem somebody has to go and solve, and a blocked card that does not say what it is waiting on cannot be unblocked by anyone but you. Pass `actor` (who you are) so the move is recorded against you in the activity log.",
546
+ inputSchema: {
547
+ type: "object",
548
+ properties: {
549
+ ref: { type: "string" },
550
+ status: {
551
+ type: "string",
552
+ enum: ["backlog", "todo", "in_progress", "blocked", "in_review", "done", "abandoned"]
553
+ },
554
+ actor: { type: "string" },
555
+ reason: {
556
+ type: "string",
557
+ description: "Why it is blocked, in your own words. Required by `blocked` unless you pass `blockedBy`."
558
+ },
559
+ blockedBy: {
560
+ type: "array",
561
+ items: { type: "string" },
562
+ description: "Task ids that must finish first — the other way to answer 'blocked on what'."
563
+ }
564
+ },
565
+ required: ["ref", "status"]
566
+ }
567
+ },
568
+ {
569
+ name: "agentistics_task_comment",
570
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. Leave a comment on a task. Use it to tell the person and the other assistants what you did, what you found, or what you are blocked on. `author` is free text — say who you are (e.g. 'claude:3f5f').",
571
+ inputSchema: {
572
+ type: "object",
573
+ properties: { ref: { type: "string" }, body: { type: "string" }, author: { type: "string" } },
574
+ required: ["ref", "body"]
575
+ }
576
+ },
577
+ {
578
+ name: "agentistics_task_subtask",
579
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. Add a subtask, or change one. Pass `title` to add; pass `id` with `done`, `status`, `assignee`, `dueDate` or `startDate` to edit one. A subtask carries the same columns its parent does, but no cost of its own: cost is measured per SESSION and rolls up to the task. To put a session ON a subtask use agentistics_task_session with `subtaskId` — a subtask holds ANY NUMBER of sessions, so the link lives on the session, not in a field here.",
580
+ inputSchema: {
581
+ type: "object",
582
+ properties: {
583
+ ref: { type: "string" },
584
+ title: { type: "string" },
585
+ id: { type: "string" },
586
+ done: { type: "boolean" },
587
+ status: {
588
+ type: "string",
589
+ enum: ["backlog", "todo", "in_progress", "blocked", "in_review", "done", "abandoned"]
590
+ },
591
+ assignee: { type: "string" },
592
+ dueDate: { type: "string" },
593
+ startDate: { type: "string" }
594
+ },
595
+ required: ["ref"]
596
+ }
597
+ },
598
+ {
599
+ name: "agentistics_task_link",
600
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. Attach a link to a task — a pull request, an issue, a document. Pass `url` (http/https only) with an optional `label` and `kind` ('pr', 'issue', …). Pass `remove` with a link id to detach one.",
601
+ inputSchema: {
602
+ type: "object",
603
+ properties: {
604
+ ref: { type: "string" },
605
+ url: { type: "string" },
606
+ label: { type: "string" },
607
+ kind: { type: "string" },
608
+ remove: { type: "string", description: "Link id to remove instead of adding." }
609
+ },
610
+ required: ["ref"]
611
+ }
612
+ },
613
+ {
614
+ name: "agentistics_task_blocked_by",
615
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. Set which tasks must finish before this one can proceed. Pass the full list of blocker task ids (it replaces, it does not append). A task cannot block itself.",
616
+ inputSchema: {
617
+ type: "object",
618
+ properties: { ref: { type: "string" }, blockedBy: { type: "array", items: { type: "string" } } },
619
+ required: ["ref", "blockedBy"]
620
+ }
621
+ },
622
+ {
623
+ name: "agentistics_task_next",
624
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. THE ORCHESTRATION CALL: what you can pick up right now. Returns the tasks that are open, not blocked by unfinished work, and not in another agent's hands — most urgent first, numbered from 1. It also returns `withheld`, every task that is NOT available with the reason (closed / blocked / claimed / status), because an agent told 'nothing' learns nothing; and `progress`, which separates 'it is all done' from 'everything is in somebody's hands right now'. Pass `actor` (who you are) so a task you already hold comes back as available to you rather than as taken.",
625
+ inputSchema: {
626
+ type: "object",
627
+ properties: {
628
+ actor: { type: "string", description: "Who is asking, e.g. 'claude:3f5f'." },
629
+ limit: { type: "number" }
630
+ },
631
+ required: []
632
+ }
633
+ },
634
+ {
635
+ name: "agentistics_task_claim",
636
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. TAKE a task before working on it, so two agents cannot do the same one twice. This is a LEASE, not a lock: it expires (default 30 minutes) and the task returns to the board on its own, so an agent that dies does not hold work forever. Re-claiming as the same `by` REFRESHES the lease — do that while you are still working. Pass `release: true` to give it back when you finish or stop. A refusal names who holds it and until when; `takeover` is for a person overriding a live claim on purpose and an agent should not send it.",
637
+ inputSchema: {
638
+ type: "object",
639
+ properties: {
640
+ ref: { type: "string" },
641
+ by: { type: "string", description: "Who you are. Free text, e.g. 'claude:3f5f'." },
642
+ release: { type: "boolean" },
643
+ leaseMs: { type: "number", description: "How long the claim lasts. Default 30 minutes." },
644
+ sessionId: { type: "string", description: "The managed session doing the work, if any." },
645
+ note: { type: "string" },
646
+ takeover: { type: "boolean" },
647
+ force: { type: "boolean", description: "Release a claim you do not hold." }
648
+ },
649
+ required: ["ref", "by"]
650
+ }
651
+ },
652
+ {
653
+ name: "agentistics_task_activity",
654
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. What has been HAPPENING, newest first — status moves, claims, releases, priority and assignee changes, sessions filed. Pass `ref` for one task, or nothing for the whole board. On a board several agents drive this is how you find out what the others did without asking them.",
655
+ inputSchema: {
656
+ type: "object",
657
+ properties: { ref: { type: "string" }, limit: { type: "number" } },
658
+ required: []
659
+ }
660
+ },
661
+ {
662
+ name: "agentistics_task_edit",
663
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. Set a task's fields: `title`, `detail`, `priority` (urgent | high | medium | low | none), `assignee`, `dueDate` / `startDate` (yyyy-mm-dd), `labels`. An absent field is left alone; an EMPTY STRING clears it. `priority` defaults to `none`, which means 'nobody has said' and is not the same as `low`. Pass `actor` so the change is recorded against you in the activity log.",
664
+ inputSchema: {
665
+ type: "object",
666
+ properties: {
667
+ ref: { type: "string" },
668
+ title: { type: "string" },
669
+ detail: { type: "string" },
670
+ priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"] },
671
+ assignee: { type: "string" },
672
+ dueDate: { type: "string" },
673
+ startDate: { type: "string" },
674
+ labels: { type: "array", items: { type: "string" } },
675
+ actor: { type: "string" }
676
+ },
677
+ required: ["ref"]
678
+ }
679
+ },
680
+ {
681
+ name: "agentistics_task_session",
682
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. File a SESSION under a SUBTASK of a task, which is what makes the task measurable: its cost, tokens, rounds and harness all come from the sessions filed under its parts. **A session is NEVER filed under a task directly — always name a `subtaskId`.** A task is the unit of DELIVERY and a subtask is the unit of WORK; filing at both levels made 'does this cost include the subtasks' unanswerable, so the server refuses it. Use agentistics_task to read the subtasks, or agentistics_task_subtask to create one, then pass `ref` + `sessionId` (a managed session id or conversation id) + `subtaskId`. Pass `detach` with a session id to unfile one. The task inherits the session's repository when it has none.",
683
+ inputSchema: {
684
+ type: "object",
685
+ properties: {
686
+ ref: { type: "string" },
687
+ sessionId: { type: "string" },
688
+ subtaskId: {
689
+ type: "string",
690
+ description: "The subtask to file it under. REQUIRED to file — a task does not take sessions itself."
691
+ },
692
+ detach: { type: "string" }
693
+ },
694
+ required: []
695
+ }
696
+ },
697
+ {
698
+ name: "agentistics_task_delete",
699
+ description: "BETA — the task board is new and still changing; its shapes may move between releases. Delete a task and its comments, subtasks, files and links. The SESSIONS filed under it are kept — deleting a board entry never deletes work.",
700
+ inputSchema: { type: "object", properties: { ref: { type: "string" } }, required: ["ref"] }
701
+ },
506
702
  {
507
703
  name: "agentistics_summary",
508
704
  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.",
@@ -711,6 +907,133 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
711
907
  const { name, arguments: args } = req.params;
712
908
  try {
713
909
  switch (name) {
910
+ case "agentistics_tasks": {
911
+ const body = await apiGet("/api/tasks");
912
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
913
+ }
914
+ case "agentistics_task": {
915
+ const ref = String(args?.ref ?? "");
916
+ const body = await apiGet(`/api/tasks/${encodeURIComponent(ref)}`);
917
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
918
+ }
919
+ case "agentistics_task_create": {
920
+ const a = args;
921
+ const body = await apiSend("POST", "/api/tasks", { title: a?.title, detail: a?.detail });
922
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
923
+ }
924
+ case "agentistics_task_status": {
925
+ const a = args;
926
+ const body = await apiSend("POST", `/api/tasks/${encodeURIComponent(String(a?.ref))}`, {
927
+ status: a?.status,
928
+ ...typeof a?.actor === "string" ? { actor: a.actor } : {},
929
+ ...typeof a?.reason === "string" ? { reason: a.reason } : {},
930
+ ...Array.isArray(a?.blockedBy) ? { blockedBy: a.blockedBy } : {}
931
+ });
932
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
933
+ }
934
+ case "agentistics_task_comment": {
935
+ const a = args;
936
+ const body = await apiSend("POST", `/api/tasks/${encodeURIComponent(String(a?.ref))}/comments`, {
937
+ body: a?.body,
938
+ author: a?.author ?? "assistant"
939
+ });
940
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
941
+ }
942
+ case "agentistics_task_subtask": {
943
+ const a = args;
944
+ const cols = ["status", "assignee", "dueDate", "startDate", "title"];
945
+ const named = cols.filter((c) => typeof a?.[c] === "string");
946
+ const payload = a?.id !== undefined ? named.length > 0 ? { id: a.id, ...Object.fromEntries(named.map((c) => [c, a[c]])) } : { id: a.id, done: a?.done === true } : { title: a?.title };
947
+ const body = await apiSend("POST", `/api/tasks/${encodeURIComponent(String(a?.ref))}/subtasks`, payload);
948
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
949
+ }
950
+ case "agentistics_task_link": {
951
+ const a = args;
952
+ const payload = a?.remove ? { remove: a.remove } : { url: a?.url, label: a?.label, kind: a?.kind };
953
+ const body = await apiSend("POST", `/api/tasks/${encodeURIComponent(String(a?.ref))}/links`, payload);
954
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
955
+ }
956
+ case "agentistics_task_blocked_by": {
957
+ const a = args;
958
+ const body = await apiSend("POST", `/api/tasks/${encodeURIComponent(String(a?.ref))}`, {
959
+ blockedBy: a?.blockedBy ?? []
960
+ });
961
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
962
+ }
963
+ case "agentistics_task_next": {
964
+ const a = args;
965
+ const q = new URLSearchParams;
966
+ if (a?.actor)
967
+ q.set("actor", String(a.actor));
968
+ if (a?.limit)
969
+ q.set("limit", String(a.limit));
970
+ const body = await apiGet(`/api/tasks/next${q.toString() ? `?${q}` : ""}`);
971
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
972
+ }
973
+ case "agentistics_task_claim": {
974
+ const a = args;
975
+ const ref = encodeURIComponent(String(a?.ref ?? ""));
976
+ const body = await apiSend("POST", `/api/tasks/${ref}/claim`, {
977
+ by: a?.by,
978
+ ...a?.release === true ? { release: true } : {},
979
+ ...a?.leaseMs !== undefined ? { leaseMs: a.leaseMs } : {},
980
+ ...a?.sessionId ? { sessionId: a.sessionId } : {},
981
+ ...a?.note ? { note: a.note } : {},
982
+ ...a?.takeover === true ? { takeover: true } : {},
983
+ ...a?.force === true ? { force: true } : {}
984
+ });
985
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
986
+ }
987
+ case "agentistics_task_activity": {
988
+ const a = args;
989
+ const q = new URLSearchParams;
990
+ if (a?.ref)
991
+ q.set("task", String(a.ref));
992
+ if (a?.limit)
993
+ q.set("limit", String(a.limit));
994
+ const body = await apiGet(`/api/tasks/activity${q.toString() ? `?${q}` : ""}`);
995
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
996
+ }
997
+ case "agentistics_task_edit": {
998
+ const a = args;
999
+ const ref = encodeURIComponent(String(a?.ref ?? ""));
1000
+ const patch = {};
1001
+ for (const f of ["title", "detail", "priority", "assignee", "dueDate", "startDate", "actor"]) {
1002
+ if (typeof a?.[f] === "string")
1003
+ patch[f] = a[f];
1004
+ }
1005
+ if (Array.isArray(a?.labels))
1006
+ patch.labels = a.labels;
1007
+ const body = await apiSend("POST", `/api/tasks/${ref}`, patch);
1008
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
1009
+ }
1010
+ case "agentistics_task_session": {
1011
+ const a = args;
1012
+ if (a?.detach) {
1013
+ const body = await apiSend("POST", "/api/tasks/x/sessions", { detach: a.detach });
1014
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
1015
+ }
1016
+ const ref = encodeURIComponent(String(a?.ref ?? ""));
1017
+ if (!a?.subtaskId) {
1018
+ return {
1019
+ content: [{
1020
+ type: "text",
1021
+ text: "A session is filed under a SUBTASK, never under the delivery itself — a task is the " + "unit of delivery, a subtask the unit of work. Read the parts with agentistics_task, or " + "create one with agentistics_task_subtask, then call this again with `subtaskId`."
1022
+ }],
1023
+ isError: true
1024
+ };
1025
+ }
1026
+ const body = await apiSend("POST", `/api/tasks/${ref}/sessions`, {
1027
+ sessionId: a?.sessionId,
1028
+ subtaskId: a.subtaskId
1029
+ });
1030
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
1031
+ }
1032
+ case "agentistics_task_delete": {
1033
+ const ref = String(args?.ref ?? "");
1034
+ const body = await apiSend("DELETE", `/api/tasks/${encodeURIComponent(ref)}`);
1035
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
1036
+ }
714
1037
  case "agentistics_summary": {
715
1038
  const harness = args?.harness;
716
1039
  const unified = !harness || harness === "all";
@@ -724,7 +1047,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
724
1047
  const projectSessions = {};
725
1048
  const activeDates = new Set;
726
1049
  for (const s of allSessions) {
727
- const { input, output, cacheRead, cacheWrite, cost } = sessionTokens(s);
1050
+ const { input, output, cacheRead, cacheWrite, cost } = sessionTokens2(s);
728
1051
  totalInput += input;
729
1052
  totalOutput += output;
730
1053
  totalCacheRead += cacheRead;
@@ -767,7 +1090,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
767
1090
  const hs = sessions.filter((s) => sessionHarness(s) === h);
768
1091
  let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0, messages = 0, lastActive = "";
769
1092
  for (const s of hs) {
770
- const t = sessionTokens(s);
1093
+ const t = sessionTokens2(s);
771
1094
  input += t.input;
772
1095
  output += t.output;
773
1096
  cacheRead += t.cacheRead;
@@ -804,7 +1127,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
804
1127
  if (!byPath[key])
805
1128
  byPath[key] = { sessions: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheWrite: 0, costUSD: 0, messages: 0, lastActive: "", languages: [] };
806
1129
  const agg = byPath[key];
807
- const { input, output, cacheRead, cacheWrite, cost } = sessionTokens(s);
1130
+ const { input, output, cacheRead, cacheWrite, cost } = sessionTokens2(s);
808
1131
  agg.sessions += 1;
809
1132
  agg.inputTokens += input;
810
1133
  agg.outputTokens += output;
@@ -841,7 +1164,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
841
1164
  const harness = args?.harness;
842
1165
  const data = await apiGet("/api/data");
843
1166
  const rows = filterSessions(data.sessions ?? [], harness).slice(0, limit).map((s) => {
844
- const { input, output, cacheRead, cacheWrite, cost } = sessionTokens(s);
1167
+ const { input, output, cacheRead, cacheWrite, cost } = sessionTokens2(s);
845
1168
  return {
846
1169
  id: s.session_id,
847
1170
  harness: sessionHarness(s),
@@ -882,7 +1205,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
882
1205
  if (!byModel[model])
883
1206
  byModel[model] = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, estimatedCostUSD: 0 };
884
1207
  const agg = byModel[model];
885
- const { input, output, cacheRead, cacheWrite, cost } = sessionTokens(s);
1208
+ const { input, output, cacheRead, cacheWrite, cost } = sessionTokens2(s);
886
1209
  agg.inputTokens += input;
887
1210
  agg.outputTokens += output;
888
1211
  agg.cacheReadTokens += cacheRead;
@@ -1142,7 +1465,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1142
1465
  if (!byRemote[key])
1143
1466
  byRemote[key] = { sessions: 0, messages: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheWrite: 0, costUSD: 0, lastActive: "" };
1144
1467
  const agg = byRemote[key];
1145
- const { input, output, cacheRead, cacheWrite, cost } = sessionTokens(s);
1468
+ const { input, output, cacheRead, cacheWrite, cost } = sessionTokens2(s);
1146
1469
  agg.sessions += 1;
1147
1470
  agg.messages += sessionMessages(s);
1148
1471
  agg.inputTokens += input;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentistics/mcp",
3
- "version": "2.17.0",
3
+ "version": "2.18.1",
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",