@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 +3 -3
- package/dist/main-http.js +2 -2
- package/dist/main.js +1 -1
- package/dist/{mcp-server-Cm72B6O_.js → mcp-server-Drv9wBc-.js} +65 -63
- package/dist/mcp-server.d.ts +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/{require-valid-todoist-token-DFE6h87D.js → require-valid-todoist-token-CC6hWyEx.js} +1 -1
- package/dist/tools/find-activity.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/eval-instructions.ts +150 -42
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as s } from "./mcp-server-
|
|
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-
|
|
3
|
-
import { r as es, v as ts } from "./require-valid-todoist-token-
|
|
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-
|
|
7
|
-
import { r as w } from "./require-valid-todoist-token-
|
|
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-
|
|
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(),
|
|
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(
|
|
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.
|
|
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
|
-
},
|
|
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
|
|
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 ??=
|
|
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 =
|
|
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
|
|
814
|
+
function qe(e) {
|
|
815
815
|
if (e == null)
|
|
816
816
|
return e;
|
|
817
817
|
if (Array.isArray(e))
|
|
818
|
-
return e.map((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] =
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
}),
|
|
1981
|
+
}), qo = {
|
|
1982
1982
|
projects: s.array(zo).min(1).describe("The array of projects to add.")
|
|
1983
|
-
},
|
|
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:
|
|
1996
|
-
outputSchema:
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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")),
|
|
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:
|
|
3187
|
+
previewLines: Kn(e, Math.min(e.length, t.limit)),
|
|
3186
3188
|
zeroReasonHints: a
|
|
3187
3189
|
});
|
|
3188
3190
|
}
|
|
3189
|
-
function
|
|
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:
|
|
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')),
|
|
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
|
|
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")),
|
|
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`))),
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(...
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(...
|
|
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(...
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
-
},
|
|
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 =
|
|
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
|
|
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(
|
|
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"],
|
|
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
|
-
|
|
6553
|
+
Kc
|
|
6552
6554
|
);
|
|
6553
6555
|
j !== void 0 && (v = { ...v, dueString: j });
|
|
6554
|
-
const A = Pt(h,
|
|
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:
|
|
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?",
|
|
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
|
-
|
|
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
|
-
|
|
7007
|
+
qa as N,
|
|
7006
7008
|
Va as O,
|
|
7007
7009
|
Za as P,
|
|
7008
7010
|
rc as Q,
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -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?\",
|
|
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.
|
package/dist/mcp-server.d.ts.map
CHANGED
|
@@ -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,
|
|
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 +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;
|
|
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
|
@@ -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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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 {
|