@doist/todoist-mcp 12.5.5 → 12.5.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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { t as s } from "./mcp-server-Cm72B6O_.js";
2
- import { F as r, a as d, b as i, c as n, d as c, e as l, f as m, h as p, i as f, j as T, k as j, l as g, m as k, n as u, o as v, p as P, q as h, r as b, s as S, u as y, v as A, w as C, x as R, y as x, z as F, g as M, A as O, B as E, C as L, D as w, E as H, G as I, H as W, I as q, J as z, K as B, L as D, M as N, N as U, O as V, P as G, Q as J, R as K, S as Q, T as X, U as Y, V as _, W as Z, X as $, Y as ss } from "./mcp-server-Cm72B6O_.js";
3
- import { r as es, v as ts } from "./require-valid-todoist-token-DFE6h87D.js";
1
+ import { t as s } from "./mcp-server-Drv9wBc-.js";
2
+ import { F as r, a as d, b as i, c as n, d as c, e as l, f as m, h as p, i as f, j as T, k as j, l as g, m as k, n as u, o as v, p as P, q as h, r as b, s as S, u as y, v as A, w as C, x as R, y as x, z as F, g as M, A as O, B as E, C as L, D as w, E as H, G as I, H as W, I as q, J as z, K as B, L as D, M as N, N as U, O as V, P as G, Q as J, R as K, S as Q, T as X, U as Y, V as _, W as Z, X as $, Y as ss } from "./mcp-server-Drv9wBc-.js";
3
+ import { r as es, v as ts } from "./require-valid-todoist-token-CC6hWyEx.js";
4
4
  const e = s;
5
5
  export {
6
6
  r as FEATURE_NAMES,
package/dist/main-http.js CHANGED
@@ -3,8 +3,8 @@ import { isIP as p } from "node:net";
3
3
  import O from "dotenv";
4
4
  import { StreamableHTTPServerTransport as S } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
5
  import m from "express";
6
- import { g as v } from "./mcp-server-Cm72B6O_.js";
7
- import { r as w } from "./require-valid-todoist-token-DFE6h87D.js";
6
+ import { g as v } from "./mcp-server-Drv9wBc-.js";
7
+ import { r as w } from "./require-valid-todoist-token-CC6hWyEx.js";
8
8
  function f(t) {
9
9
  return t.startsWith("[") && t.endsWith("]") ? t.slice(1, -1) : t;
10
10
  }
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport as s } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import c from "dotenv";
4
- import { g as i } from "./mcp-server-Cm72B6O_.js";
4
+ import { g as i } from "./mcp-server-Drv9wBc-.js";
5
5
  function p() {
6
6
  const e = process.env.TODOIST_BASE_URL, r = process.env.TODOIST_API_KEY;
7
7
  if (!r)
@@ -28,7 +28,7 @@ function zs() {
28
28
  }
29
29
  return console.error("Failed to load task list app HTML from any known path:", e), Bs;
30
30
  }
31
- const Ht = zs(), Ks = Rt("sha256").update(Ht).digest("hex").slice(0, 12), Le = `ui://todoist/task-list@${Ks}`, qs = "ui://todoist/task-list@{hash}", Ne = "Interactive task list widget", Ys = "https://ai.todoist.net", Me = {
31
+ const Ht = zs(), qs = Rt("sha256").update(Ht).digest("hex").slice(0, 12), Le = `ui://todoist/task-list@${qs}`, Ks = "ui://todoist/task-list@{hash}", Ne = "Interactive task list widget", Ys = "https://ai.todoist.net", Me = {
32
32
  ui: {
33
33
  prefersBorder: !0,
34
34
  csp: {
@@ -69,7 +69,7 @@ function Gs(e) {
69
69
  async () => pt(Le)
70
70
  ), e.registerResource(
71
71
  "todoist-task-list-hash-fallback",
72
- new Ds(qs, { list: void 0 }),
72
+ new Ds(Ks, { list: void 0 }),
73
73
  {
74
74
  description: `${Ne} compatibility fallback`,
75
75
  mimeType: Lt,
@@ -371,7 +371,7 @@ function or(e) {
371
371
  const t = Ve(e);
372
372
  return t ? sr(t) : Bt(e);
373
373
  }
374
- const nr = "12.5.5", ir = {
374
+ const nr = "12.5.6", ir = {
375
375
  version: nr
376
376
  }, C = {
377
377
  /** Default limit for task listings */
@@ -407,7 +407,7 @@ const nr = "12.5.5", ir = {
407
407
  }, fe = {
408
408
  /** Maximum number of failures to show in detailed error messages */
409
409
  MAX_FAILURES_SHOWN: 3
410
- }, Kt = {
410
+ }, qt = {
411
411
  /** Maximum tasks accepted by one task create or update operation */
412
412
  TASKS_PER_OPERATION: 25
413
413
  }, Be = {
@@ -475,7 +475,7 @@ function mt(e, { queueTimeoutMs: t = Be.QUEUE_WAIT_MS } = {}) {
475
475
  }
476
476
  };
477
477
  }
478
- function qt() {
478
+ function Kt() {
479
479
  return {
480
480
  moves: mt(Be.TASK_MOVES),
481
481
  writes: mt(Be.WRITES)
@@ -484,7 +484,7 @@ function qt() {
484
484
  const ht = /* @__PURE__ */ new Map(), Yt = /* @__PURE__ */ new WeakMap();
485
485
  let ft;
486
486
  function ar() {
487
- return ft ??= qt(), ft;
487
+ return ft ??= Kt(), ft;
488
488
  }
489
489
  function cr(e) {
490
490
  return Rt("sha256").update(e).digest("hex");
@@ -492,7 +492,7 @@ function cr(e) {
492
492
  function dr(e, t) {
493
493
  const r = cr(t);
494
494
  let o = ht.get(r);
495
- o || (o = qt(), ht.set(r, o)), Yt.set(e, o);
495
+ o || (o = Kt(), ht.set(r, o)), Yt.set(e, o);
496
496
  }
497
497
  function Gt(e) {
498
498
  return Yt.get(e) ?? ar();
@@ -811,15 +811,15 @@ const Br = [
811
811
  g.FIND_PROJECT_COLLABORATORS,
812
812
  g.FIND_COMPLETED_TASKS
813
813
  ];
814
- function Ke(e) {
814
+ function qe(e) {
815
815
  if (e == null)
816
816
  return e;
817
817
  if (Array.isArray(e))
818
- return e.map((t) => Ke(t));
818
+ return e.map((t) => qe(t));
819
819
  if (typeof e == "object") {
820
820
  const t = {};
821
821
  for (const [r, o] of Object.entries(e))
822
- r !== "email" && (t[r] = Ke(o));
822
+ r !== "email" && (t[r] = qe(o));
823
823
  return t;
824
824
  }
825
825
  return e;
@@ -828,7 +828,7 @@ function zr(e) {
828
828
  const t = /\s*\([^)]*@[^)]+\)/g, r = /\S+@\S+\.\S+/g;
829
829
  return e.replace(t, "").replace(r, "[email hidden]");
830
830
  }
831
- function Kr({
831
+ function qr({
832
832
  tool: e,
833
833
  server: t,
834
834
  client: r,
@@ -840,7 +840,7 @@ function Kr({
840
840
  e.name,
841
841
  () => O(() => i(d, r))
842
842
  );
843
- return n && (u && (u = zr(u)), p && (p = Ke(p))), Ur({ textContent: u, structuredContent: p, contentItems: m });
843
+ return n && (u && (u = zr(u)), p && (p = qe(p))), Ur({ textContent: u, structuredContent: p, contentItems: m });
844
844
  } catch (u) {
845
845
  return console.error(`Error executing tool ${e.name}:`, { args: Mr(d), error: u }), Lr(or(u));
846
846
  }
@@ -865,7 +865,7 @@ function Kr({
865
865
  }
866
866
  t.registerTool(e.name, c, a);
867
867
  }
868
- const qr = {
868
+ const Kr = {
869
869
  period: s.enum(["today", "7d", "14d", "30d", "this-week", "this-month"]).default("7d").describe(
870
870
  'Time period for the analysis. "today" analyzes the current day, "7d"/"14d"/"30d" analyze the last N days, "this-week" and "this-month" analyze the current week or month.'
871
871
  ),
@@ -1001,7 +1001,7 @@ const he = {
1001
1001
  name: "productivity-analysis",
1002
1002
  title: "Productivity Analysis",
1003
1003
  description: "Analyze your Todoist productivity with insights on completion trends, goal streaks, project distribution, and actionable recommendations. Gathers data from multiple tools and synthesizes a comprehensive report.",
1004
- argsSchema: qr,
1004
+ argsSchema: Kr,
1005
1005
  callback: Jr
1006
1006
  };
1007
1007
  class _ extends Error {
@@ -1539,7 +1539,7 @@ const ue = s.enum(Ie).optional().catch(void 0), F = s.object({
1539
1539
  sectionOrder: s.number().describe("Ordering index within the project."),
1540
1540
  description: s.string().optional().describe("Supports Markdown.")
1541
1541
  });
1542
- function K({ id: e, name: t, sectionOrder: r, description: o }) {
1542
+ function q({ id: e, name: t, sectionOrder: r, description: o }) {
1543
1543
  return { id: e, name: t, sectionOrder: r, description: o ?? void 0 };
1544
1544
  }
1545
1545
  const bo = s.object({
@@ -1883,7 +1883,7 @@ function Se(e, t = 5) {
1883
1883
  }
1884
1884
  return o;
1885
1885
  }
1886
- function q({
1886
+ function K({
1887
1887
  subject: e,
1888
1888
  count: t,
1889
1889
  limit: r,
@@ -1978,9 +1978,9 @@ const as = new Bo(), zo = s.object({
1978
1978
  workspace: s.string().trim().min(1).optional().describe(
1979
1979
  "The workspace to create the project in. Accepts a workspace name or workspace ID. If not provided, creates a personal project. Use list-workspaces to see available workspaces."
1980
1980
  )
1981
- }), Ko = {
1981
+ }), qo = {
1982
1982
  projects: s.array(zo).min(1).describe("The array of projects to add.")
1983
- }, qo = {
1983
+ }, Ko = {
1984
1984
  projects: s.array(Q).describe("The created projects."),
1985
1985
  totalCount: s.number().describe("The total number of projects created."),
1986
1986
  failures: s.array(ee).describe(
@@ -1992,8 +1992,8 @@ const as = new Bo(), zo = s.object({
1992
1992
  }, Yo = {
1993
1993
  name: g.ADD_PROJECTS,
1994
1994
  description: "Add one or more new projects.",
1995
- parameters: Ko,
1996
- outputSchema: qo,
1995
+ parameters: qo,
1996
+ outputSchema: Ko,
1997
1997
  annotations: { readOnlyHint: !1, destructiveHint: !1, idempotentHint: !1 },
1998
1998
  async execute({ projects: e }, t) {
1999
1999
  const r = /* @__PURE__ */ new Map(), o = (l) => {
@@ -2185,7 +2185,7 @@ const rn = s.object({
2185
2185
  }), {
2186
2186
  textContent: cn({ sections: n, failures: i }),
2187
2187
  structuredContent: {
2188
- sections: n.map(K),
2188
+ sections: n.map(q),
2189
2189
  totalCount: n.length,
2190
2190
  failures: i,
2191
2191
  totalRequested: e.length,
@@ -2426,7 +2426,7 @@ const at = new dn();
2426
2426
  function U(e) {
2427
2427
  return s.string().transform((t) => t === "" ? void 0 : t).optional().describe(e);
2428
2428
  }
2429
- const Tt = Kt.TASKS_PER_OPERATION, ln = s.object({
2429
+ const Tt = qt.TASKS_PER_OPERATION, ln = s.object({
2430
2430
  content: s.string().min(1).describe(
2431
2431
  'The task name/title. Should be concise and actionable (e.g., "Review PR #123", "Call dentist"). For longer content, use the description field instead. Supports Markdown.'
2432
2432
  ),
@@ -2999,7 +2999,7 @@ const ps = ["task", "project", "comment", "section"], Rn = {
2999
2999
  const i = await t.getSection(o);
3000
3000
  if (!i)
3001
3001
  throw new Error(`Section ${o} not found.`);
3002
- const a = K(i);
3002
+ const a = q(i);
3003
3003
  return {
3004
3004
  textContent: `Found section: ${a.name} • id=${a.id}`,
3005
3005
  structuredContent: {
@@ -3110,7 +3110,9 @@ const Wn = {
3110
3110
  ]).optional().describe("Type of event to filter by."),
3111
3111
  projectId: s.string().optional().describe("Filter events by parent project ID."),
3112
3112
  taskId: s.string().optional().describe("Filter events by parent task ID (for subtask events)."),
3113
- initiatorId: s.string().optional().describe("Filter by the user ID who initiated the event."),
3113
+ initiatorId: s.string().optional().describe(
3114
+ 'Filter by the user ID who initiated the event. A first-person question ("what did I complete") needs this: without it the results cover every collaborator. Get the ID from user-info when you do not already have it.'
3115
+ ),
3114
3116
  dateFrom: St(
3115
3117
  `Inclusive start of the activity range, as an ISO 8601 date or date-time. For all events on one local calendar day, use that day's start, for example "2026-08-02T00:00:00-04:00". Natural-language dates such as "tomorrow" are not supported.`
3116
3118
  ),
@@ -3127,7 +3129,7 @@ const Wn = {
3127
3129
  appliedFilters: s.record(s.string(), s.unknown())
3128
3130
  }, zn = {
3129
3131
  name: g.FIND_ACTIVITY,
3130
- description: 'Retrieve activity logs to monitor and audit changes in Todoist. Shows events from all users by default (use initiatorId to filter by specific user). To answer what someone completed in a period, including recurring task occurrences, use objectType "task", eventType "completed", and dateFrom/dateTo. Track task completions, updates, deletions, project changes, and more with flexible filtering. Activity history availability and retention depend on the user plan.',
3132
+ description: `Retrieve activity logs to monitor and audit changes in Todoist. Shows events from all users by default (use initiatorId to filter by specific user). To answer what someone completed in a period, including recurring task occurrences, use objectType "task", eventType "completed", and dateFrom/dateTo. For a first-person question ("what did I get done"), also set initiatorId to the current user from user-info, or the answer includes collaborators' completions. Track task completions, updates, deletions, project changes, and more with flexible filtering. Activity history availability and retention depend on the user plan.`,
3131
3133
  parameters: Wn,
3132
3134
  outputSchema: Bn,
3133
3135
  annotations: { readOnlyHint: !0, destructiveHint: !1, idempotentHint: !0 },
@@ -3150,7 +3152,7 @@ const Wn = {
3150
3152
  r && n ? m.objectEventTypes = `${r}:${n}` : r ? m.objectEventTypes = `${r}:` : n && (m.objectEventTypes = `:${n}`), o && o !== "remove" && (m.objectId = o), i && (m.parentProjectId = i), a && (m.parentItemId = a), c && (m.initiatorId = c), d && (m.dateFrom = d), l && (m.dateTo = l);
3151
3153
  const { results: h, nextCursor: y } = await t.getActivityLogs(m), f = h.map(po);
3152
3154
  return {
3153
- textContent: Kn({ events: f, args: e, nextCursor: y }),
3155
+ textContent: qn({ events: f, args: e, nextCursor: y }),
3154
3156
  structuredContent: {
3155
3157
  events: f,
3156
3158
  nextCursor: y ?? void 0,
@@ -3161,7 +3163,7 @@ const Wn = {
3161
3163
  };
3162
3164
  }
3163
3165
  };
3164
- function Kn({
3166
+ function qn({
3165
3167
  events: e,
3166
3168
  args: t,
3167
3169
  nextCursor: r
@@ -3176,17 +3178,17 @@ function Kn({
3176
3178
  const i = [];
3177
3179
  t.objectId && i.push(`object ID: ${t.objectId}`), t.projectId && i.push(`project: ${t.projectId}`), t.taskId && i.push(`task: ${t.taskId}`), t.initiatorId && i.push(`initiator: ${t.initiatorId}`), t.dateFrom && i.push(`from: ${t.dateFrom}`), t.dateTo && i.push(`until (exclusive): ${t.dateTo}`);
3178
3180
  const a = [];
3179
- return e.length === 0 && (a.push("No activity events match the specified filters"), a.push("Note: Activity logs only show recent events"), t.eventType && a.push(`Try removing the eventType filter (${t.eventType})`), t.objectType && a.push(`Try removing the objectType filter (${t.objectType})`), (t.objectId || t.projectId || t.taskId) && a.push("Verify the object ID is correct")), q({
3181
+ return e.length === 0 && (a.push("No activity events match the specified filters"), a.push("Note: Activity logs only show recent events"), t.eventType && a.push(`Try removing the eventType filter (${t.eventType})`), t.objectType && a.push(`Try removing the objectType filter (${t.objectType})`), (t.objectId || t.projectId || t.taskId) && a.push("Verify the object ID is correct")), K({
3180
3182
  subject: o,
3181
3183
  count: e.length,
3182
3184
  limit: t.limit,
3183
3185
  nextCursor: r ?? void 0,
3184
3186
  filterHints: i,
3185
- previewLines: qn(e, Math.min(e.length, t.limit)),
3187
+ previewLines: Kn(e, Math.min(e.length, t.limit)),
3186
3188
  zeroReasonHints: a
3187
3189
  });
3188
3190
  }
3189
- function qn(e, t = 10) {
3191
+ function Kn(e, t = 10) {
3190
3192
  const o = e.slice(0, t).map(Yn).join(`
3191
3193
  `);
3192
3194
  if (e.length > t) {
@@ -3397,7 +3399,7 @@ const oi = {
3397
3399
  appliedFilters: s.record(s.string(), s.unknown())
3398
3400
  }, ii = {
3399
3401
  name: g.FIND_COMPLETED_TASKS,
3400
- description: "Get completed tasks. since/until are optional and default to a 7-day window when omitted. Includes all collaborators by default. Person-specific queries (summaries, plans, reports) require responsibleUser.",
3402
+ description: 'Get completed tasks in a date range. For "what did I complete/get done" questions, use find-activity instead — it reports completion events, including every occurrence of a recurring task, which this tool does not. since/until are optional and default to a 7-day window when omitted. Includes all collaborators by default. Person-specific queries (summaries, plans, reports) require responsibleUser.',
3401
3403
  parameters: oi,
3402
3404
  outputSchema: ni,
3403
3405
  annotations: { readOnlyHint: !0, destructiveHint: !1, idempotentHint: !0 },
@@ -3467,7 +3469,7 @@ function ai({
3467
3469
  a.push(`assigned to: ${d}`);
3468
3470
  }
3469
3471
  const c = [];
3470
- return e.length === 0 && (c.push("No tasks completed in this date range"), c.push("Try expanding the date range"), (t.projectId || t.sectionId || t.parentId) && c.push("Try removing project/section/parent filters"), t.getBy === "due" && c.push('Try switching to "completion" date instead')), q({
3472
+ return e.length === 0 && (c.push("No tasks completed in this date range"), c.push("Try expanding the date range"), (t.projectId || t.sectionId || t.parentId) && c.push("Try removing project/section/parent filters"), t.getBy === "due" && c.push('Try switching to "completion" date instead')), K({
3471
3473
  subject: i,
3472
3474
  count: e.length,
3473
3475
  limit: t.limit,
@@ -3536,7 +3538,7 @@ function ui({
3536
3538
  const m = o.length > 0 ? `
3537
3539
  Shared labels (${o.length}): ${o.join(", ")}` : `
3538
3540
  No shared labels.`;
3539
- return q({
3541
+ return K({
3540
3542
  subject: n,
3541
3543
  count: e.length,
3542
3544
  limit: t.searchText ? void 0 : t.limit,
@@ -3716,7 +3718,7 @@ function ys({
3716
3718
  const l = [];
3717
3719
  e.length === 0 && r && (l.push(`No users match "${r}"`), l.push("Try a broader search term or check spelling"), o > 0 && l.push(`${o} users available without filter`));
3718
3720
  const u = [];
3719
- return e.length > 0 ? (u.push(`Use ${fs} with responsibleUser to assign new tasks`), u.push(`Use ${bs} with responsibleUser to reassign existing tasks`), u.push("Use user names, emails, or IDs for assignments")) : (u.push(`Use ${hs} to find other projects`), r && o > 0 && u.push("Try searching without filters to see all users")), q({
3721
+ return e.length > 0 ? (u.push(`Use ${fs} with responsibleUser to assign new tasks`), u.push(`Use ${bs} with responsibleUser to reassign existing tasks`), u.push("Use user names, emails, or IDs for assignments")) : (u.push(`Use ${hs} to find other projects`), r && o > 0 && u.push("Try searching without filters to see all users")), K({
3720
3722
  subject: a,
3721
3723
  count: e.length,
3722
3724
  filterHints: c,
@@ -3792,7 +3794,7 @@ function wi({
3792
3794
  const a = 10, d = e.slice(0, a).map(Fo).join(`
3793
3795
  `), l = e.length - a, u = l > 0 ? `${d}
3794
3796
  …and ${l} more` : d, p = [];
3795
- return e.length === 0 && (t.searchText ? (p.push("Try broader search terms"), p.push("Check spelling"), p.push("Remove searchText to see all projects")) : t.archivedStatus === "archived" ? (p.push("No archived projects"), p.push("Use archivedStatus: 'all' to also see active projects")) : t.archivedStatus === "all" ? (p.push("No projects created yet"), p.push(`Use ${Ct} to create a project`)) : (p.push("No active projects"), p.push("Use archivedStatus: 'all' to include archived projects"), p.push(`Use ${Ct} to create a project`))), q({
3797
+ return e.length === 0 && (t.searchText ? (p.push("Try broader search terms"), p.push("Check spelling"), p.push("Remove searchText to see all projects")) : t.archivedStatus === "archived" ? (p.push("No archived projects"), p.push("Use archivedStatus: 'all' to also see active projects")) : t.archivedStatus === "all" ? (p.push("No projects created yet"), p.push(`Use ${Ct} to create a project`)) : (p.push("No active projects"), p.push("Use archivedStatus: 'all' to include archived projects"), p.push(`Use ${Ct} to create a project`))), K({
3796
3798
  subject: n,
3797
3799
  count: e.length,
3798
3800
  limit: t.searchText || t.archivedStatus === "all" ? void 0 : t.limit,
@@ -3915,7 +3917,7 @@ const { ADD_SECTIONS: Ci } = g, $i = {
3915
3917
  e.searchText ? o = await uo(t, e.searchText, r) : o = (await t.getSections({
3916
3918
  projectId: r
3917
3919
  })).results;
3918
- const n = o.map(K);
3920
+ const n = o.map(q);
3919
3921
  return {
3920
3922
  textContent: Ai({
3921
3923
  sections: n,
@@ -3939,7 +3941,7 @@ function Ai({
3939
3941
  r ? (o.push("Try broader search terms"), o.push("Check spelling"), o.push("Remove searchText to see all sections")) : (o.push("Project has no sections yet"), o.push(`Use ${Ci} to create sections`));
3940
3942
  const n = r ? `Sections in project ${t} matching "${r}"` : `Sections in project ${t}`, i = e.length > 0 ? e.map((a) => ` ${a.name} • id=${a.id}`).join(`
3941
3943
  `) : void 0;
3942
- return q({
3944
+ return K({
3943
3945
  subject: n,
3944
3946
  count: e.length,
3945
3947
  previewLines: i,
@@ -4067,7 +4069,7 @@ function _i({
4067
4069
  a.push(`Great job! No tasks for today${c}`);
4068
4070
  } else
4069
4071
  a.push("Expand date range with larger 'daysCount'"), a.push("Check today's tasks with startDate='today'");
4070
- return q({
4072
+ return K({
4071
4073
  subject: i,
4072
4074
  count: e.length,
4073
4075
  limit: t.limit,
@@ -4353,7 +4355,7 @@ function Ee({
4353
4355
  t.searchText && (c.push("Try broader search terms"), c.push("Verify spelling and try partial words"), t.responsibleUser || c.push(`Check completed tasks with ${$t}`));
4354
4356
  }
4355
4357
  }
4356
- return q({
4358
+ return K({
4357
4359
  subject: i,
4358
4360
  count: e.length,
4359
4361
  limit: t.limit,
@@ -4441,7 +4443,7 @@ function zi(e) {
4441
4443
  }
4442
4444
  return o(r), r;
4443
4445
  }
4444
- async function Ki(e, t) {
4446
+ async function qi(e, t) {
4445
4447
  const r = {};
4446
4448
  return await Promise.all(
4447
4449
  t.map(async (o) => {
@@ -4477,11 +4479,11 @@ function xt(e) {
4477
4479
  }
4478
4480
  return r;
4479
4481
  }
4480
- function qe(e, t = "") {
4482
+ function Ke(e, t = "") {
4481
4483
  const r = [];
4482
4484
  for (const o of e) {
4483
4485
  const n = `id=${o.id}`, i = o.dueDate ? `; due=${o.dueDate}` : "", a = `; content=${o.content}`;
4484
- r.push(`${t}- ${n}${i}${a}`), o.children.length > 0 && r.push(...qe(o.children, `${t} `));
4486
+ r.push(`${t}- ${n}${i}${a}`), o.children.length > 0 && r.push(...Ke(o.children, `${t} `));
4485
4487
  }
4486
4488
  return r;
4487
4489
  }
@@ -4492,13 +4494,13 @@ function vs(e, t) {
4492
4494
  parentId: B(e) ? e.parentId ?? void 0 : void 0,
4493
4495
  folderId: ke(e) ? e.folderId ?? void 0 : void 0,
4494
4496
  childOrder: e.childOrder,
4495
- sections: (t[e.id] || []).map(K),
4497
+ sections: (t[e.id] || []).map(q),
4496
4498
  children: e.children.map(
4497
4499
  (r) => vs(r, t)
4498
4500
  )
4499
4501
  };
4500
4502
  }
4501
- async function qi(e, t) {
4503
+ async function Ki(e, t) {
4502
4504
  let r = [], o;
4503
4505
  do {
4504
4506
  const { results: n, nextCursor: i } = await e.getTasks({
@@ -4515,7 +4517,7 @@ async function Yi(e, t) {
4515
4517
  return r;
4516
4518
  }
4517
4519
  async function Gi(e) {
4518
- const { results: t } = await e.getProjects({}), r = t.find((p) => B(p) && p.inboxProject === !0), o = t.filter((p) => !B(p) || p.inboxProject !== !0), n = zi(o), i = t.map((p) => p.id), a = await Ki(e, i), c = ["# Personal Projects", ""];
4520
+ const { results: t } = await e.getProjects({}), r = t.find((p) => B(p) && p.inboxProject === !0), o = t.filter((p) => !B(p) || p.inboxProject !== !0), n = zi(o), i = t.map((p) => p.id), a = await qi(e, i), c = ["# Personal Projects", ""];
4519
4521
  if (r) {
4520
4522
  c.push(`- Inbox Project: ${r.name} (id=${r.id})`);
4521
4523
  for (const p of a[r.id] || [])
@@ -4538,7 +4540,7 @@ async function Gi(e) {
4538
4540
  inbox: r ? {
4539
4541
  id: r.id,
4540
4542
  name: r.name,
4541
- sections: (a[r.id] || []).map(K)
4543
+ sections: (a[r.id] || []).map(q)
4542
4544
  } : void 0,
4543
4545
  projects: n.map(
4544
4546
  (p) => vs(p, a)
@@ -4556,7 +4558,7 @@ async function Vi(e, t) {
4556
4558
  const [r, o, n] = await Promise.all([
4557
4559
  e.getProject(t),
4558
4560
  Yi(e, t),
4559
- qi(e, t)
4561
+ Ki(e, t)
4560
4562
  ]), i = {};
4561
4563
  for (const u of o)
4562
4564
  i[u.id] = [];
@@ -4567,7 +4569,7 @@ async function Vi(e, t) {
4567
4569
  if (a.length > 0) {
4568
4570
  c.push("");
4569
4571
  const u = xt(a);
4570
- c.push(...qe(u));
4572
+ c.push(...Ke(u));
4571
4573
  }
4572
4574
  for (const u of o) {
4573
4575
  c.push(""), c.push(`## ${u.name}`);
@@ -4575,7 +4577,7 @@ async function Vi(e, t) {
4575
4577
  if (!p?.length)
4576
4578
  continue;
4577
4579
  const m = xt(p);
4578
- c.push(...qe(m));
4580
+ c.push(...Ke(m));
4579
4581
  }
4580
4582
  const d = c.join(`
4581
4583
  `), l = {
@@ -4584,7 +4586,7 @@ async function Vi(e, t) {
4584
4586
  id: r.id,
4585
4587
  name: r.name
4586
4588
  },
4587
- sections: o.map(K),
4589
+ sections: o.map(q),
4588
4590
  tasks: n.map((u) => ({
4589
4591
  ...u,
4590
4592
  children: []
@@ -4669,7 +4671,7 @@ const Ji = {
4669
4671
  projectColors: s.record(s.string(), s.string()).describe("Map of project ID to color key.")
4670
4672
  }, Qi = {
4671
4673
  name: g.GET_PRODUCTIVITY_STATS,
4672
- description: "Get comprehensive productivity statistics including daily/weekly completion breakdowns, goal streaks (current, last, max), karma score and trends, and historical karma data. Useful for productivity analysis and tracking goal progress.",
4674
+ description: 'Get comprehensive productivity statistics including daily/weekly completion breakdowns, goal streaks (current, last, max), karma score and trends, and historical karma data. Useful for productivity analysis and tracking goal progress. Reports counts, streaks and karma only, never the tasks themselves — for "what did I complete", use find-activity.',
4673
4675
  parameters: Xi,
4674
4676
  outputSchema: Zi,
4675
4677
  annotations: {
@@ -5057,7 +5059,7 @@ const ya = {
5057
5059
  projectId: e,
5058
5060
  file: r ?? "",
5059
5061
  fileName: "template.csv"
5060
- }), a = i.sections.map(K), c = i.tasks.map(P), d = i.comments.map(ge), l = i.projects.length + a.length + c.length + d.length;
5062
+ }), a = i.sections.map(q), c = i.tasks.map(P), d = i.comments.map(ge), l = i.projects.length + a.length + c.length + d.length;
5061
5063
  return {
5062
5064
  textContent: wa({
5063
5065
  projectId: e,
@@ -5567,7 +5569,7 @@ const Pa = {
5567
5569
  tasks: s.array(F).describe("The rescheduled tasks."),
5568
5570
  totalCount: s.number().describe("The total number of tasks rescheduled."),
5569
5571
  rescheduledTaskIds: s.array(s.string()).describe("The IDs of the rescheduled tasks.")
5570
- }, Ka = {
5572
+ }, qa = {
5571
5573
  name: g.RESCHEDULE_TASKS,
5572
5574
  description: "Reschedule tasks to new dates while preserving recurring schedules. Unlike update-tasks (which replaces the entire due string and can wipe recurrence), this tool changes only the date, keeping recurrence patterns intact. Use this when moving recurring tasks to a different date without altering their repeat pattern.",
5573
5575
  parameters: Ba,
@@ -5584,7 +5586,7 @@ const Pa = {
5584
5586
  throw new Error(
5585
5587
  `Task "${l.content}" (${l.id}) has no due date. Rescheduling requires an existing due date.`
5586
5588
  );
5587
- const u = qa(d.date, l.due);
5589
+ const u = Ka(d.date, l.due);
5588
5590
  return V("item_update", {
5589
5591
  id: d.id,
5590
5592
  due: {
@@ -5615,7 +5617,7 @@ const Pa = {
5615
5617
  };
5616
5618
  }
5617
5619
  };
5618
- function qa(e, t) {
5620
+ function Ka(e, t) {
5619
5621
  if (/^\d{4}-\d{2}-\d{2}$/.test(e) && t.datetime) {
5620
5622
  const o = t.datetime.substring(10);
5621
5623
  return e + o;
@@ -6171,7 +6173,7 @@ const Cc = s.object({
6171
6173
  sections: r
6172
6174
  }),
6173
6175
  structuredContent: {
6174
- sections: r.map(K),
6176
+ sections: r.map(q),
6175
6177
  totalCount: r.length,
6176
6178
  updatedSectionIds: r.map((n) => n.id)
6177
6179
  }
@@ -6263,7 +6265,7 @@ const Wc = s.object({
6263
6265
  isUncompletable: s.boolean().optional().describe(
6264
6266
  "Whether this task should be uncompletable (organizational header). Tasks with isUncompletable: true appear as organizational headers and cannot be completed."
6265
6267
  )
6266
- }), Bc = (e) => !!(e.projectId || e.sectionId || e.parentId), js = (e) => Object.keys(e).length > 0, zc = ["remove", "no date"], Kc = ["remove", "no date", "no deadline"], qc = "no date", Ye = Kt.TASKS_PER_OPERATION, Ot = {
6268
+ }), Bc = (e) => !!(e.projectId || e.sectionId || e.parentId), js = (e) => Object.keys(e).length > 0, zc = ["remove", "no date"], qc = ["remove", "no date", "no deadline"], Kc = "no date", Ye = qt.TASKS_PER_OPERATION, Ot = {
6267
6269
  tasks: s.array(Wc).min(1).max(Ye).describe(`The tasks to update (max ${Ye}).`)
6268
6270
  }, Yc = {
6269
6271
  tasks: s.array(F).describe("The updated tasks."),
@@ -6548,10 +6550,10 @@ async function ed({
6548
6550
  const j = Pt(
6549
6551
  d,
6550
6552
  zc,
6551
- qc
6553
+ Kc
6552
6554
  );
6553
6555
  j !== void 0 && (v = { ...v, dueString: j });
6554
- const A = Pt(h, Kc, null);
6556
+ const A = Pt(h, qc, null);
6555
6557
  if (A !== void 0 && (v = { ...v, deadlineDate: A }), l)
6556
6558
  try {
6557
6559
  const { minutes: k } = Xt(l);
@@ -6849,7 +6851,7 @@ const vd = {
6849
6851
  completeTasks: wn,
6850
6852
  uncompleteTasks: Za,
6851
6853
  updateTasks: Gc,
6852
- rescheduleTasks: Ka,
6854
+ rescheduleTasks: qa,
6853
6855
  findTasks: Fi,
6854
6856
  findTasksByDate: Sd,
6855
6857
  findCompletedTasks: ii,
@@ -6930,7 +6932,7 @@ What each tool does and how to fill its parameters is in the tool's own descript
6930
6932
 
6931
6933
  **Finding things**
6932
6934
 
6933
- - For "what did I complete?", use **find-activity** with objectType="task" and eventType="completed". It reports completion events, including each occurrence of a recurring task. **find-completed-tasks** lists completed tasks, which is not the same question.
6935
+ - For "what did I complete?", call **user-info** for the asker's user ID, then **find-activity** with objectType="task", eventType="completed", initiatorId, and dateFrom/dateTo. Without initiatorId the answer covers every collaborator. find-activity reports completion events, including each occurrence of a recurring task; **find-completed-tasks** lists completed tasks, which is not the same question.
6934
6936
  - To resolve a person's name or email to a user ID, or to answer "who is X?", use **find-project-collaborators** with just a searchTerm. It covers every shared project you can access plus yourself, so an empty result means they collaborate on none of them — not that they do not exist.
6935
6937
  - Before assigning work to someone, call **find-project-collaborators** again with the target projectId. Resolving an ID only proves they collaborate on *some* project you can see; assignment fails unless they collaborate on that one.
6936
6938
  - To find out whether a task hides subtasks, use **fetch-object** with includeChildren rather than a speculative **find-tasks** call.
@@ -6976,7 +6978,7 @@ function Hd({
6976
6978
  Gs(o);
6977
6979
  const i = { server: o, client: n, features: r };
6978
6980
  for (const a of Cd)
6979
- Kr({ tool: a, ...i });
6981
+ qr({ tool: a, ...i });
6980
6982
  return o.registerPrompt(
6981
6983
  he.name,
6982
6984
  {
@@ -7002,7 +7004,7 @@ export {
7002
7004
  Na as K,
7003
7005
  Cd as L,
7004
7006
  Ha as M,
7005
- Ka as N,
7007
+ qa as N,
7006
7008
  Va as O,
7007
7009
  Za as P,
7008
7010
  rc as Q,
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { FEATURE_NAMES, Feature, FeatureName, Features } from './mcp-helpers.js';
3
- export declare const instructions = "\n## Todoist Task and Project Management Tools\n\nYou have access to comprehensive Todoist management tools for personal productivity and team collaboration. Use these tools to help users manage tasks, projects, sections, comments, and assignments effectively.\n\n### Core Capabilities:\n- Create, update, complete, and search tasks with rich metadata (priorities, due dates, durations, assignments)\n- Manage projects and sections with flexible organization\n- Handle comments and collaboration features\n- Bulk assignment operations for team workflows\n- Get overviews and insights about workload and progress\n\n### Choosing between tools\n\nWhat each tool does and how to fill its parameters is in the tool's own description and input schema. This section covers only what those cannot: which tool to reach for, and how tools relate to each other.\n\n**Dates**\n\n- To move a task to a different date use **reschedule-tasks**, never **update-tasks**. update-tasks replaces the whole due string, which destroys recurrence on recurring tasks.\n- Never send a task's existing projectId, sectionId or parentId back to **update-tasks** \u2014 those fields are treated as a move.\n- All dates respect the user's timezone.\n\n**Finding things**\n\n- For \"what did I complete?\", use **find-activity** with objectType=\"task\" and eventType=\"completed\". It reports completion events, including each occurrence of a recurring task. **find-completed-tasks** lists completed tasks, which is not the same question.\n- To resolve a person's name or email to a user ID, or to answer \"who is X?\", use **find-project-collaborators** with just a searchTerm. It covers every shared project you can access plus yourself, so an empty result means they collaborate on none of them \u2014 not that they do not exist.\n- Before assigning work to someone, call **find-project-collaborators** again with the target projectId. Resolving an ID only proves they collaborate on *some* project you can see; assignment fails unless they collaborate on that one.\n- To find out whether a task hides subtasks, use **fetch-object** with includeChildren rather than a speculative **find-tasks** call.\n- Filter tasks by label **name**. Label IDs are only for **delete-object** and **update-labels**. Shared labels can be renamed but not recoloured, reordered or favourited.\n\n**Deleting and archiving**\n\n- **delete-object** removes every object type; there is no per-type delete tool. Reminders use type \"reminder\", location reminders \"location_reminder\".\n- A workspace project must be archived with **project-management** before it can be deleted. A personal project can be deleted directly.\n\n**Templates**\n\n- Templates cannot be listed through this server, so only use an ID or URL the user gave you. To start a new project from one, call **add-projects** first and import into it. Imports write immediately and cannot be undone.\n\n**Project health**\n\n- Health data may be stale \u2014 check the isStale flag, and call **analyze-project-health** to refresh it before reading again.\n\n**Batches**\n\n- Prefer a batch tool over one call per item.\n- Where a batch tool returns per-item `failures` alongside successes, one failure does not undo the rest: never retry the whole batch, re-send only the items whose failure reason is fixable. **reschedule-tasks** is the exception \u2014 it has no per-item failures and throws if any task fails, so retry it as a whole.\n\nUse the **productivity-analysis** prompt for a combined productivity review \u2014 it pulls together user-info, get-productivity-stats and find-completed-tasks.\n\nAlways provide clear, actionable task titles and descriptions. Use the overview tools to give users context about their workload and project status.\n";
3
+ export declare const instructions = "\n## Todoist Task and Project Management Tools\n\nYou have access to comprehensive Todoist management tools for personal productivity and team collaboration. Use these tools to help users manage tasks, projects, sections, comments, and assignments effectively.\n\n### Core Capabilities:\n- Create, update, complete, and search tasks with rich metadata (priorities, due dates, durations, assignments)\n- Manage projects and sections with flexible organization\n- Handle comments and collaboration features\n- Bulk assignment operations for team workflows\n- Get overviews and insights about workload and progress\n\n### Choosing between tools\n\nWhat each tool does and how to fill its parameters is in the tool's own description and input schema. This section covers only what those cannot: which tool to reach for, and how tools relate to each other.\n\n**Dates**\n\n- To move a task to a different date use **reschedule-tasks**, never **update-tasks**. update-tasks replaces the whole due string, which destroys recurrence on recurring tasks.\n- Never send a task's existing projectId, sectionId or parentId back to **update-tasks** \u2014 those fields are treated as a move.\n- All dates respect the user's timezone.\n\n**Finding things**\n\n- For \"what did I complete?\", call **user-info** for the asker's user ID, then **find-activity** with objectType=\"task\", eventType=\"completed\", initiatorId, and dateFrom/dateTo. Without initiatorId the answer covers every collaborator. find-activity reports completion events, including each occurrence of a recurring task; **find-completed-tasks** lists completed tasks, which is not the same question.\n- To resolve a person's name or email to a user ID, or to answer \"who is X?\", use **find-project-collaborators** with just a searchTerm. It covers every shared project you can access plus yourself, so an empty result means they collaborate on none of them \u2014 not that they do not exist.\n- Before assigning work to someone, call **find-project-collaborators** again with the target projectId. Resolving an ID only proves they collaborate on *some* project you can see; assignment fails unless they collaborate on that one.\n- To find out whether a task hides subtasks, use **fetch-object** with includeChildren rather than a speculative **find-tasks** call.\n- Filter tasks by label **name**. Label IDs are only for **delete-object** and **update-labels**. Shared labels can be renamed but not recoloured, reordered or favourited.\n\n**Deleting and archiving**\n\n- **delete-object** removes every object type; there is no per-type delete tool. Reminders use type \"reminder\", location reminders \"location_reminder\".\n- A workspace project must be archived with **project-management** before it can be deleted. A personal project can be deleted directly.\n\n**Templates**\n\n- Templates cannot be listed through this server, so only use an ID or URL the user gave you. To start a new project from one, call **add-projects** first and import into it. Imports write immediately and cannot be undone.\n\n**Project health**\n\n- Health data may be stale \u2014 check the isStale flag, and call **analyze-project-health** to refresh it before reading again.\n\n**Batches**\n\n- Prefer a batch tool over one call per item.\n- Where a batch tool returns per-item `failures` alongside successes, one failure does not undo the rest: never retry the whole batch, re-send only the items whose failure reason is fixable. **reschedule-tasks** is the exception \u2014 it has no per-item failures and throws if any task fails, so retry it as a whole.\n\nUse the **productivity-analysis** prompt for a combined productivity review \u2014 it pulls together user-info, get-productivity-stats and find-completed-tasks.\n\nAlways provide clear, actionable task titles and descriptions. Use the overview tools to give users context about their workload and project status.\n";
4
4
  /**
5
5
  * Create the MCP server.
6
6
  * @param todoistApiKey - The API key for the todoist account.
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGnE,OAAO,EACH,aAAa,EACb,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,QAAQ,EAEhB,MAAM,kBAAkB,CAAA;AAKzB,eAAO,MAAM,YAAY,ksHAmDxB,CAAA;AAED;;;;;;GAMG;AACH,iBAAS,YAAY,CAAC,EAClB,aAAa,EACb,OAAO,EACP,QAAa,GAChB,EAAE;IACC,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,QAAQ,CAAA;CACtB,aA4CA;AAED,OAAO,EAAE,aAAa,EAAE,KAAK,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGnE,OAAO,EACH,aAAa,EACb,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,QAAQ,EAEhB,MAAM,kBAAkB,CAAA;AAKzB,eAAO,MAAM,YAAY,m1HAmDxB,CAAA;AAED;;;;;;GAMG;AACH,iBAAS,YAAY,CAAC,EAClB,aAAa,EACb,OAAO,EACP,QAAa,GAChB,EAAE;IACC,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,QAAQ,CAAA;CACtB,aA4CA;AAED,OAAO,EAAE,aAAa,EAAE,KAAK,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,YAAY,EAAE,CAAA"}
@@ -1,4 +1,4 @@
1
- import { Z as A, _, $ as T } from "./mcp-server-Cm72B6O_.js";
1
+ import { Z as A, _, $ as T } from "./mcp-server-Drv9wBc-.js";
2
2
  const v = /* @__PURE__ */ new Set([401, 403]);
3
3
  function y(e) {
4
4
  const t = T(e);
@@ -1 +1 @@
1
- {"version":3,"file":"find-activity.d.ts","sourceRoot":"","sources":["../../src/tools/find-activity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AA6EvB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsD6C,CAAA;AA6I/D,OAAO,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"find-activity.d.ts","sourceRoot":"","sources":["../../src/tools/find-activity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAkFvB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsD6C,CAAA;AA6I/D,OAAO,EAAE,YAAY,EAAE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/todoist-mcp",
3
- "version": "12.5.5",
3
+ "version": "12.5.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -14,9 +14,11 @@
14
14
  * git stash pop
15
15
  * npx tsx scripts/eval-instructions.ts --label after
16
16
  *
17
- * Tool calls are never executed only the first call of each turn is
18
- * inspected — so this touches no Todoist data and needs no Todoist token. It
19
- * does spend money on model calls; see --repeats and --models.
17
+ * Tool calls are never executed against Todoist, so this touches no Todoist
18
+ * data and needs no Todoist token. Judging looks at one call per attempt: the
19
+ * first, unless it is a context tool (see CONTEXT_TOOLS), in which case a
20
+ * canned result is fed back and the next call is judged. It does spend money
21
+ * on model calls; see --repeats and --models.
20
22
  *
21
23
  * Auth uses the Anthropic SDK's standard credential chain: either
22
24
  * ANTHROPIC_API_KEY (a key from the Anthropic Console at platform.claude.com),
@@ -34,9 +36,17 @@ import Anthropic from '@anthropic-ai/sdk'
34
36
  import { z } from 'zod'
35
37
  import { instructions } from '../src/mcp-server.js'
36
38
  import { registeredTools } from '../src/tool-registry.js'
39
+ import type { UserInfoStructured } from '../src/tools/user-info.js'
37
40
  import { createLimiter } from '../src/utils/concurrency.js'
38
41
  import { ToolNames } from '../src/utils/tool-names.js'
39
42
 
43
+ /**
44
+ * The user ID the stubbed `user-info` reports. A scenario asking a first-person
45
+ * question can assert the model carried it into a filter argument rather than
46
+ * querying everyone.
47
+ */
48
+ const EVAL_USER_ID = '2671355'
49
+
40
50
  type Check = (input: Record<string, unknown>) => string | null
41
51
 
42
52
  /** First item of a batch tool's array argument, e.g. add-tasks' `tasks`. */
@@ -89,9 +99,14 @@ const SCENARIOS: Scenario[] = [
89
99
  if (!input.dateFrom || !input.dateTo) {
90
100
  return 'no dateFrom/dateTo, so this searches all history rather than last week'
91
101
  }
102
+ // find-activity reports every user's events by default, so "what did
103
+ // I get done" answered without this includes collaborators' work.
104
+ if (input.initiatorId !== EVAL_USER_ID) {
105
+ return `initiatorId is ${JSON.stringify(input.initiatorId)}, so this reports every collaborator's completions, not "I"`
106
+ }
92
107
  return null
93
108
  },
94
- guards: 'instructions: find-activity vs find-completed-tasks, over a bounded range',
109
+ guards: 'instructions: find-activity vs find-completed-tasks, over a bounded range, filtered to the asker',
95
110
  },
96
111
  {
97
112
  id: 'resolve-person',
@@ -195,6 +210,51 @@ const DEFAULT_MODELS = ['claude-haiku-4-5', 'claude-sonnet-5']
195
210
  const DEFAULT_REPEATS = 5
196
211
  const MAX_CONCURRENCY = 4
197
212
 
213
+ /**
214
+ * Tools a model may legitimately call before the one a scenario is testing, to
215
+ * establish context it has no other way to get — today's date, the user's
216
+ * timezone. "What did I get done last week?" cannot be turned into a date range
217
+ * without them.
218
+ *
219
+ * Judging the very first call outright scores that correct behaviour as a
220
+ * failure: an `expect` allowlist naming the tool under test cannot also name
221
+ * every reasonable lookup that precedes it. So a call to one of these is
222
+ * answered with the canned result below and judging moves to the next call.
223
+ *
224
+ * The result is fiction, but its shape has to be real: a scenario may assert
225
+ * that the model carried a value through (see EVAL_USER_ID), and a model reads
226
+ * a malformed field the way it would read any other bad data. `satisfies` ties
227
+ * it to the tool's own output type, so a renamed or added field breaks here
228
+ * rather than drifting quietly. Formats must match too — `currentLocalTime`
229
+ * is what `toLocaleString('en-US', …)` produces, not ISO.
230
+ */
231
+ const CONTEXT_TOOLS: Record<string, unknown> = {
232
+ [ToolNames.USER_INFO]: {
233
+ type: 'user_info',
234
+ userId: EVAL_USER_ID,
235
+ fullName: 'Eval User',
236
+ timezone: 'Europe/London',
237
+ currentLocalTime: '08/11/2026, 09:30:00',
238
+ startDay: 1,
239
+ startDayName: 'Monday',
240
+ weekStartDate: '2026-08-10',
241
+ weekEndDate: '2026-08-16',
242
+ currentWeekNumber: 33,
243
+ completedToday: 3,
244
+ dailyGoal: 5,
245
+ weeklyGoal: 30,
246
+ email: 'eval@example.com',
247
+ plan: 'Todoist Pro',
248
+ } satisfies UserInfoStructured,
249
+ }
250
+
251
+ /**
252
+ * How many context calls an attempt may make before it is judged a failure.
253
+ * A model that keeps gathering context is not answering the question, and
254
+ * without a cap a loop would bill for turns forever.
255
+ */
256
+ const MAX_CONTEXT_HOPS = 3
257
+
198
258
  function parseArgs() {
199
259
  const args = process.argv.slice(2)
200
260
  const get = (flag: string) => {
@@ -254,6 +314,28 @@ type Attempt = {
254
314
  */
255
315
  errored: boolean
256
316
  usage: Usage
317
+ /** Context calls answered with a stub before the judged one. */
318
+ contextHops: number
319
+ }
320
+
321
+ /** Verdict on the one call a scenario is judged by. */
322
+ function judge(
323
+ scenario: Scenario,
324
+ call: Anthropic.ToolUseBlock,
325
+ ): { pass: boolean; reason: string } {
326
+ if (scenario.forbid?.includes(call.name)) {
327
+ return { pass: false, reason: `called ${call.name}, which this rule forbids` }
328
+ }
329
+ if (scenario.expect && !scenario.expect.includes(call.name)) {
330
+ return { pass: false, reason: `expected ${scenario.expect.join(' or ')}` }
331
+ }
332
+ // An argument check written for a specific tool must not run against a
333
+ // different one a forbid-only scenario legitimately allows.
334
+ if (scenario.forbid && !scenario.expect) {
335
+ return { pass: true, reason: '' }
336
+ }
337
+ const reason = scenario.check?.(call.input as Record<string, unknown>) ?? null
338
+ return { pass: reason === null, reason: reason ?? '' }
257
339
  }
258
340
 
259
341
  async function runAttempt(
@@ -267,51 +349,77 @@ async function runAttempt(
267
349
  model,
268
350
  errored: false,
269
351
  usage: { input: 0, output: 0, cacheWrite: 0, cacheRead: 0 },
352
+ contextHops: 0,
270
353
  }
354
+ const messages: Anthropic.MessageParam[] = [{ role: 'user', content: scenario.prompt }]
271
355
  try {
272
- const response = await client.messages.create({
273
- model,
274
- max_tokens: 4096,
275
- // Tools render before system, so one breakpoint here caches both.
276
- system: [{ type: 'text', text: instructions, cache_control: { type: 'ephemeral' } }],
277
- tools,
278
- messages: [{ role: 'user', content: scenario.prompt }],
279
- })
356
+ for (let hop = 0; ; hop++) {
357
+ const response = await client.messages.create({
358
+ model,
359
+ max_tokens: 4096,
360
+ // Tools render before system, so one breakpoint here caches both.
361
+ system: [
362
+ { type: 'text', text: instructions, cache_control: { type: 'ephemeral' } },
363
+ ],
364
+ tools,
365
+ messages,
366
+ })
280
367
 
281
- base.usage = {
282
- input: response.usage.input_tokens,
283
- output: response.usage.output_tokens,
284
- cacheWrite: response.usage.cache_creation_input_tokens ?? 0,
285
- cacheRead: response.usage.cache_read_input_tokens ?? 0,
286
- }
368
+ base.usage = {
369
+ input: base.usage.input + response.usage.input_tokens,
370
+ output: base.usage.output + response.usage.output_tokens,
371
+ cacheWrite:
372
+ base.usage.cacheWrite + (response.usage.cache_creation_input_tokens ?? 0),
373
+ cacheRead: base.usage.cacheRead + (response.usage.cache_read_input_tokens ?? 0),
374
+ }
287
375
 
288
- const call = response.content.find((b) => b.type === 'tool_use')
289
- if (!call) {
290
- return { ...base, calledTool: null, pass: false, reason: 'no tool call' }
291
- }
292
- if (scenario.forbid?.includes(call.name)) {
293
- return {
294
- ...base,
295
- calledTool: call.name,
296
- pass: false,
297
- reason: `called ${call.name}, which this rule forbids`,
376
+ const calls = response.content.filter((b) => b.type === 'tool_use')
377
+ const call = calls[0]
378
+ if (!call) {
379
+ return { ...base, calledTool: null, pass: false, reason: 'no tool call' }
298
380
  }
299
- }
300
- if (scenario.expect && !scenario.expect.includes(call.name)) {
301
- return {
302
- ...base,
303
- calledTool: call.name,
304
- pass: false,
305
- reason: `expected ${scenario.expect.join(' or ')}`,
381
+
382
+ // A turn can carry several calls at once. Judge the substantive one
383
+ // rather than whichever block came first: a model that asks for the
384
+ // date and queries the activity log in the same turn has made its
385
+ // choice, and the order between the two is arbitrary. A forbidden
386
+ // call outranks that — catching it is the point of a forbid rule.
387
+ //
388
+ // A tool a scenario expects is never treated as context, or a
389
+ // scenario testing the route *to* user-info could never pass: its
390
+ // expected call would be stubbed instead of judged.
391
+ const isContext = (c: Anthropic.ToolUseBlock) =>
392
+ Object.hasOwn(CONTEXT_TOOLS, c.name) && !scenario.expect?.includes(c.name)
393
+ const substantive =
394
+ calls.find((c) => scenario.forbid?.includes(c.name)) ??
395
+ calls.find((c) => !isContext(c))
396
+ if (substantive) {
397
+ const { pass, reason } = judge(scenario, substantive)
398
+ return { ...base, calledTool: substantive.name, pass, reason: reason || null }
306
399
  }
400
+
401
+ // Nothing but context gathering, so answer it and judge what the
402
+ // model reaches for next.
403
+ if (hop >= MAX_CONTEXT_HOPS) {
404
+ return {
405
+ ...base,
406
+ calledTool: call.name,
407
+ pass: false,
408
+ reason: `still gathering context after ${MAX_CONTEXT_HOPS} calls`,
409
+ }
410
+ }
411
+
412
+ base.contextHops = hop + 1
413
+ messages.push({ role: 'assistant', content: response.content })
414
+ messages.push({
415
+ role: 'user',
416
+ content: calls.map((c) => ({
417
+ type: 'tool_result' as const,
418
+ tool_use_id: c.id,
419
+ content: JSON.stringify(CONTEXT_TOOLS[c.name]),
420
+ })),
421
+ })
307
422
  }
308
- // An argument check written for a specific tool must not run against a
309
- // different one a forbid-only scenario legitimately allows.
310
- if (scenario.forbid && !scenario.expect) {
311
- return { ...base, calledTool: call.name, pass: true, reason: null }
312
- }
313
- const reason = scenario.check?.(call.input as Record<string, unknown>) ?? null
314
- return { ...base, calledTool: call.name, pass: reason === null, reason }
315
423
  } catch (error) {
316
424
  const message = error instanceof Error ? error.message : String(error)
317
425
  return {