@doist/automations-mcp 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9418,6 +9418,11 @@ var automationListResponseSchema = z11.object({
9418
9418
  data: z11.array(automationSchema),
9419
9419
  nextCursor: z11.string().nullable()
9420
9420
  });
9421
+ var triggerAutomationResponseSchema = z11.object({
9422
+ automationId: z11.string(),
9423
+ runId: z11.string(),
9424
+ status: z11.literal("pending")
9425
+ });
9421
9426
  var updateAutomationSchema = z11.object({
9422
9427
  enabled: z11.boolean().optional(),
9423
9428
  manifest: manifestSchema.optional()
@@ -11203,13 +11208,19 @@ var AutomationsRequestError = class extends Error {
11203
11208
  * scope: a challenge can say the grant falls short without saying of what.
11204
11209
  */
11205
11210
  requiredScope;
11206
- constructor(message, httpStatusCode, responseData, authChallenge) {
11211
+ /**
11212
+ * The `X-Request-Id` the response carried, which the backend logs the request
11213
+ * under. Undefined in a browser unless the header is exposed by CORS.
11214
+ */
11215
+ requestId;
11216
+ constructor(message, httpStatusCode, responseData, authChallenge, requestId) {
11207
11217
  super(message);
11208
11218
  this.name = "AutomationsRequestError";
11209
11219
  this.httpStatusCode = httpStatusCode;
11210
11220
  this.responseData = responseData;
11211
11221
  this.authChallenge = authChallenge;
11212
11222
  this.requiredScope = parseChallengeScope(authChallenge);
11223
+ this.requestId = requestId;
11213
11224
  }
11214
11225
  isAuthError() {
11215
11226
  return this.httpStatusCode === 401 || this.httpStatusCode === 403;
@@ -11280,7 +11291,7 @@ async function request(args) {
11280
11291
  const data = await readResponseBody(response);
11281
11292
  if (!response.ok) {
11282
11293
  const message = typeof data === "object" && data !== null && "message" in data ? String(data.message) : response.statusText || `Request failed with status ${response.status}`;
11283
- throw new AutomationsRequestError(message, response.status, data, response.headers.get("WWW-Authenticate") ?? void 0);
11294
+ throw new AutomationsRequestError(message, response.status, data, response.headers.get("WWW-Authenticate") ?? void 0, response.headers.get("X-Request-Id") ?? void 0);
11284
11295
  }
11285
11296
  return data;
11286
11297
  }
@@ -11700,13 +11711,15 @@ var AutomationsClient = class extends BaseClient {
11700
11711
  });
11701
11712
  return createDraftResponseSchema.parse(data);
11702
11713
  }
11714
+ /** Starts a run now. Undefined from an API that predates returning the run. */
11703
11715
  async trigger(id, options) {
11704
11716
  const parsedId = IdSchema.parse(id);
11705
- await this.request({
11717
+ const data = await this.request({
11706
11718
  method: "POST",
11707
11719
  path: automationsEndpoints.trigger(parsedId),
11708
11720
  options
11709
11721
  });
11722
+ return triggerAutomationResponseSchema.optional().parse(data);
11710
11723
  }
11711
11724
  async test(id, options) {
11712
11725
  const parsedId = IdSchema.parse(id);
@@ -12041,13 +12054,17 @@ var WorkspaceAutomationsClient = class extends BaseClient {
12041
12054
  });
12042
12055
  return automationSchema.parse(data);
12043
12056
  }
12044
- /** Runs the automation now. The run executes as its creator, not as the caller. */
12057
+ /**
12058
+ * Runs the automation now. The run executes as its creator, not as the caller.
12059
+ * Undefined from an API that predates returning the run.
12060
+ */
12045
12061
  async trigger(workspaceId, id, options) {
12046
- await this.request({
12062
+ const data = await this.request({
12047
12063
  method: "POST",
12048
12064
  path: workspaceAutomationsEndpoints.trigger(IdSchema.parse(workspaceId), IdSchema.parse(id)),
12049
12065
  options
12050
12066
  });
12067
+ return triggerAutomationResponseSchema.optional().parse(data);
12051
12068
  }
12052
12069
  /**
12053
12070
  * Deletes the automation. Workspace admins may delete another member's
@@ -13452,31 +13469,11 @@ function readManifestName(manifest) {
13452
13469
  return typeof name === "string" && name.trim() ? name : "(unnamed)";
13453
13470
  }
13454
13471
 
13455
- // ../../packages/sdk/dist/esm/drafts/draft-test.js
13456
- var DRAFT_TEST_OUTCOMES = [
13457
- "passed",
13458
- "failed",
13459
- "skipped",
13460
- "cancelled",
13461
- "timed_out"
13462
- ];
13472
+ // ../../packages/sdk/dist/esm/runs/poll-until-closed.js
13463
13473
  var DEFAULT_POLL_INTERVALS_MS = { first: 2e3, max: 1e4 };
13464
13474
  var STATUS_REQUEST_GRACE_MS = 1e4;
13465
- var OPEN_RUN_STATUSES = /* @__PURE__ */ new Set([
13466
- "pending",
13467
- "running"
13468
- ]);
13469
- async function startDraftTest(draftApi, { draftId, request: request2 }) {
13470
- const started = await draftApi.test(draftId, request2);
13471
- return {
13472
- draftId,
13473
- testRunId: started.testRunId,
13474
- trigger: started.trigger,
13475
- connectorCalls: started.connectorCalls,
13476
- status: started.status
13477
- };
13478
- }
13479
- async function waitForDraftTest(draftApi, { draftId, testRunId }, { timeoutMs, pollIntervalsMs, requestGraceMs, onPoll, signal }) {
13475
+ var OPEN_RUN_STATUSES = /* @__PURE__ */ new Set(["pending", "running"]);
13476
+ async function pollUntilClosed(read, { timeoutMs, pollIntervalsMs, requestGraceMs, onPoll, signal }) {
13480
13477
  const intervals = pollIntervalsMs ?? DEFAULT_POLL_INTERVALS_MS;
13481
13478
  const deadline = Date.now() + timeoutMs;
13482
13479
  let intervalMs = intervals.first;
@@ -13485,24 +13482,64 @@ async function waitForDraftTest(draftApi, { draftId, testRunId }, { timeoutMs, p
13485
13482
  let last = null;
13486
13483
  while (true) {
13487
13484
  try {
13488
- last = await draftApi.getTestRun(draftId, testRunId, void 0, requestSignal);
13485
+ last = await read(requestSignal);
13489
13486
  } catch (error) {
13490
13487
  if (signal.aborted || !requestTimeout.aborted)
13491
13488
  throw error;
13492
- return toFinished(last, "timed_out");
13489
+ return { timedOut: true, run: last };
13493
13490
  }
13494
13491
  if (!OPEN_RUN_STATUSES.has(last.status)) {
13495
- return toFinished(last, toOutcome(last.status));
13492
+ return { timedOut: false, run: last };
13496
13493
  }
13497
13494
  onPoll?.(last);
13498
13495
  const remainingMs = deadline - Date.now();
13499
13496
  if (remainingMs <= 0) {
13500
- return toFinished(last, "timed_out");
13497
+ return { timedOut: true, run: last };
13501
13498
  }
13502
13499
  await delay(Math.min(intervalMs, remainingMs), signal);
13503
13500
  intervalMs = Math.min(intervalMs * 2, intervals.max);
13504
13501
  }
13505
13502
  }
13503
+ function delay(ms, signal) {
13504
+ return new Promise((resolve, reject) => {
13505
+ if (signal.aborted) {
13506
+ reject(signal.reason);
13507
+ return;
13508
+ }
13509
+ const onAbort = () => {
13510
+ clearTimeout(timer);
13511
+ reject(signal.reason);
13512
+ };
13513
+ const timer = setTimeout(() => {
13514
+ signal.removeEventListener("abort", onAbort);
13515
+ resolve();
13516
+ }, ms);
13517
+ signal.addEventListener("abort", onAbort, { once: true });
13518
+ });
13519
+ }
13520
+
13521
+ // ../../packages/sdk/dist/esm/drafts/draft-test.js
13522
+ var DRAFT_TEST_OUTCOMES = [
13523
+ "passed",
13524
+ "failed",
13525
+ "skipped",
13526
+ "cancelled",
13527
+ "timed_out"
13528
+ ];
13529
+ async function startDraftTest(draftApi, { draftId, request: request2 }) {
13530
+ const started = await draftApi.test(draftId, request2);
13531
+ return {
13532
+ draftId,
13533
+ testRunId: started.testRunId,
13534
+ trigger: started.trigger,
13535
+ connectorCalls: started.connectorCalls,
13536
+ status: started.status
13537
+ };
13538
+ }
13539
+ async function waitForDraftTest(draftApi, { draftId, testRunId }, options) {
13540
+ const polled = await pollUntilClosed((requestSignal) => draftApi.getTestRun(draftId, testRunId, void 0, requestSignal), options);
13541
+ return polled.timedOut ? toFinished(polled.run, "timed_out") : toFinished(polled.run, toOutcome(polled.run.status));
13542
+ }
13506
13543
  function toDraftTestTriggerSelector(value) {
13507
13544
  if (typeof value === "number")
13508
13545
  return { index: value };
@@ -13529,22 +13566,65 @@ function toOutcome(status) {
13529
13566
  return "failed";
13530
13567
  }
13531
13568
  }
13532
- function delay(ms, signal) {
13533
- return new Promise((resolve, reject) => {
13534
- if (signal.aborted) {
13535
- reject(signal.reason);
13536
- return;
13537
- }
13538
- const onAbort = () => {
13539
- clearTimeout(timer);
13540
- reject(signal.reason);
13569
+
13570
+ // ../../packages/sdk/dist/esm/runs/run-api.js
13571
+ function bindRunApi(api, workspaceId, { signal } = {}) {
13572
+ if (workspaceId === null) {
13573
+ return {
13574
+ workspaceId: null,
13575
+ getAutomation: (automationId) => api.automations.get(automationId, { signal }),
13576
+ trigger: (automationId) => api.automations.trigger(automationId, { signal }),
13577
+ getRun: (automationId, runId, args = {}, requestSignal) => api.automationRuns.get(automationId, runId, args, {
13578
+ signal: requestSignal ?? signal
13579
+ })
13541
13580
  };
13542
- const timer = setTimeout(() => {
13543
- signal.removeEventListener("abort", onAbort);
13544
- resolve();
13545
- }, ms);
13546
- signal.addEventListener("abort", onAbort, { once: true });
13547
- });
13581
+ }
13582
+ return {
13583
+ workspaceId,
13584
+ getAutomation: (automationId) => api.workspaceAutomations.get(workspaceId, automationId, { signal }),
13585
+ trigger: (automationId) => api.workspaceAutomations.trigger(workspaceId, automationId, { signal }),
13586
+ getRun: (automationId, runId, args = {}, requestSignal) => api.workspaceAutomationRuns.get(workspaceId, automationId, runId, args, {
13587
+ signal: requestSignal ?? signal
13588
+ })
13589
+ };
13590
+ }
13591
+
13592
+ // ../../packages/sdk/dist/esm/runs/run-wait.js
13593
+ var RUN_OUTCOMES = ["completed", "failed", "skipped", "cancelled", "timed_out"];
13594
+ async function waitForRun(runApi, { automationId, runId }, options) {
13595
+ const polled = await pollUntilClosed((requestSignal) => runApi.getRun(automationId, runId, void 0, requestSignal), options);
13596
+ return polled.timedOut ? toFinished2(polled.run, "timed_out") : toFinished2(polled.run, toOutcome2(polled.run.status));
13597
+ }
13598
+ function toFinished2(run, outcome) {
13599
+ const failedStep = run ? findFailedStep(run) : null;
13600
+ return {
13601
+ outcome,
13602
+ status: run?.status ?? "pending",
13603
+ failedStep,
13604
+ error: failedStep?.error ?? (run ? getRunOutcomeReason(run) : null) ?? (run?.status === "data-expired" ? "Temporal no longer holds this run's history, so its outcome is unknown" : null),
13605
+ result: run?.result ?? null
13606
+ };
13607
+ }
13608
+ function findFailedStep(run) {
13609
+ const step = run.steps.find((candidate) => candidate.status === "failed");
13610
+ return step ? { stepId: step.stepId, description: step.description, error: step.error ?? null } : null;
13611
+ }
13612
+ function toOutcome2(status) {
13613
+ switch (status) {
13614
+ case "completed":
13615
+ case "skipped":
13616
+ case "cancelled":
13617
+ return status;
13618
+ default:
13619
+ return "failed";
13620
+ }
13621
+ }
13622
+
13623
+ // ../../packages/sdk/dist/esm/drafts/conversation-draft.js
13624
+ async function findConversationDraftId(api, { conversationId, workspaceId, signal }) {
13625
+ const args = { limit: MAX_PAGINATION_LIMIT };
13626
+ const page = workspaceId === null ? await api.automationDrafts.list(args, { signal }) : await api.workspaceAutomationDrafts.list(workspaceId, args, { signal });
13627
+ return page.data.find((draft) => draft.conversationId === conversationId)?.id ?? null;
13548
13628
  }
13549
13629
 
13550
13630
  // ../../packages/sdk/dist/esm/chat-turn/connection-requirements.js
@@ -13936,6 +14016,67 @@ function assertSelectionsUnambiguous(requirements, selections) {
13936
14016
 
13937
14017
  // ../../packages/sdk/dist/esm/chat-turn/transport.js
13938
14018
  import { DefaultChatTransport, readUIMessageStream } from "ai";
14019
+
14020
+ // ../../packages/sdk/dist/esm/chat-turn/errors.js
14021
+ var ChatTurnError = class extends Error {
14022
+ conversationId;
14023
+ requestId;
14024
+ stage;
14025
+ /** The API's error body, when it refused the request with one. */
14026
+ response;
14027
+ constructor({ conversationId, requestId, stage, cause }) {
14028
+ const response = stage === "request" ? parseChatStreamRequestError(cause) : void 0;
14029
+ super(response?.message ?? (cause instanceof Error ? cause.message : String(cause)), {
14030
+ cause
14031
+ });
14032
+ this.name = "ChatTurnError";
14033
+ this.conversationId = conversationId;
14034
+ this.requestId = requestId;
14035
+ this.stage = stage;
14036
+ this.response = response;
14037
+ }
14038
+ get httpStatus() {
14039
+ return this.response?.statusCode;
14040
+ }
14041
+ };
14042
+ function parseChatStreamRequestError(error) {
14043
+ if (!(error instanceof Error) || !error.message.trim().startsWith("{"))
14044
+ return void 0;
14045
+ let body;
14046
+ try {
14047
+ body = JSON.parse(error.message);
14048
+ } catch {
14049
+ return void 0;
14050
+ }
14051
+ const parsed = chatStreamRequestErrorResponseSchema.safeParse(body);
14052
+ return parsed.success ? parsed.data : void 0;
14053
+ }
14054
+ function getChatTurnBillingRefusal(error) {
14055
+ return getChatBillingRefusal(error instanceof ChatTurnError ? error.cause : error);
14056
+ }
14057
+ function readFailedRequest(error) {
14058
+ if (error instanceof ChatTurnError) {
14059
+ return {
14060
+ requestId: error.requestId,
14061
+ ...error.httpStatus !== void 0 ? { httpStatus: error.httpStatus } : {}
14062
+ };
14063
+ }
14064
+ if (error instanceof AutomationsRequestError) {
14065
+ return {
14066
+ ...error.requestId ? { requestId: error.requestId } : {},
14067
+ httpStatus: error.httpStatusCode
14068
+ };
14069
+ }
14070
+ return {};
14071
+ }
14072
+ function classifyChatTurnFailure(error) {
14073
+ const { httpStatus } = readFailedRequest(error);
14074
+ if (httpStatus !== void 0)
14075
+ return httpStatus < 500 ? "refused" : "broken";
14076
+ return error instanceof ChatTurnError || error instanceof AutomationsNetworkError ? "broken" : "other";
14077
+ }
14078
+
14079
+ // ../../packages/sdk/dist/esm/chat-turn/transport.js
13939
14080
  var ChatTurnTransport = class {
13940
14081
  transport;
13941
14082
  constructor({ token, baseUrl, headers, fetch: fetch2 }) {
@@ -13962,39 +14103,62 @@ var ChatTurnTransport = class {
13962
14103
  }
13963
14104
  /** Streams one server turn, reporting incremental text and deduplicated tool state changes. */
13964
14105
  async streamTurn({ conversationId, workspaceId = null, message, signal, onText, onToolActivity }) {
13965
- const stream = await this.transport.sendMessages({
13966
- trigger: "submit-message",
13967
- chatId: conversationId,
13968
- messageId: void 0,
13969
- messages: [message],
13970
- abortSignal: signal,
13971
- body: { conversationId, workspaceId }
13972
- });
14106
+ const requestId = globalThis.crypto.randomUUID();
14107
+ const fail = (stage, cause) => signal?.aborted ? cause : new ChatTurnError({ conversationId, requestId, stage, cause });
14108
+ let stream;
14109
+ try {
14110
+ stream = await this.transport.sendMessages({
14111
+ trigger: "submit-message",
14112
+ chatId: conversationId,
14113
+ messageId: void 0,
14114
+ messages: [message],
14115
+ abortSignal: signal,
14116
+ headers: { "X-Request-Id": requestId },
14117
+ body: { conversationId, workspaceId }
14118
+ });
14119
+ } catch (error) {
14120
+ throw fail("request", error);
14121
+ }
13973
14122
  let finalMessage;
13974
14123
  let emittedText = "";
13975
14124
  const emittedToolStates = /* @__PURE__ */ new Set();
13976
- for await (const currentMessage of readUIMessageStream({
13977
- stream,
13978
- terminateOnError: true
13979
- })) {
13980
- finalMessage = currentMessage;
13981
- const visibleText = getVisibleText(currentMessage);
13982
- if (visibleText.startsWith(emittedText)) {
13983
- const delta = visibleText.slice(emittedText.length);
13984
- if (delta)
13985
- onText?.(delta);
13986
- emittedText = visibleText;
13987
- }
13988
- for (const part of currentMessage.parts) {
13989
- const activity = toToolActivity(part);
13990
- if (!activity)
13991
- continue;
13992
- const key = `${activity.toolCallId}:${activity.state}`;
13993
- if (emittedToolStates.has(key))
13994
- continue;
13995
- emittedToolStates.add(key);
13996
- onToolActivity?.(activity);
14125
+ let callbackError;
14126
+ const report = (callback) => {
14127
+ try {
14128
+ callback();
14129
+ } catch (error) {
14130
+ callbackError = { error };
14131
+ throw error;
14132
+ }
14133
+ };
14134
+ try {
14135
+ for await (const currentMessage of readUIMessageStream({
14136
+ stream,
14137
+ terminateOnError: true
14138
+ })) {
14139
+ finalMessage = currentMessage;
14140
+ const visibleText = getVisibleText(currentMessage);
14141
+ if (visibleText.startsWith(emittedText)) {
14142
+ const delta = visibleText.slice(emittedText.length);
14143
+ if (delta)
14144
+ report(() => onText?.(delta));
14145
+ emittedText = visibleText;
14146
+ }
14147
+ for (const part of currentMessage.parts) {
14148
+ const activity = toToolActivity(part);
14149
+ if (!activity)
14150
+ continue;
14151
+ const key = `${activity.toolCallId}:${activity.state}`;
14152
+ if (emittedToolStates.has(key))
14153
+ continue;
14154
+ emittedToolStates.add(key);
14155
+ report(() => onToolActivity?.(activity));
14156
+ }
13997
14157
  }
14158
+ } catch (error) {
14159
+ if (callbackError)
14160
+ throw callbackError.error;
14161
+ throw fail("stream", error);
13998
14162
  }
13999
14163
  if (!finalMessage) {
14000
14164
  throw new Error("The Automations chat returned an empty response.");
@@ -14019,7 +14183,7 @@ import { McpServer } from "@modelcontextprotocol/server";
14019
14183
  // package.json
14020
14184
  var package_default = {
14021
14185
  name: "@doist/automations-mcp",
14022
- version: "1.12.0",
14186
+ version: "1.13.0",
14023
14187
  description: "MCP server and importable AI tools for Todoist Automations",
14024
14188
  type: "module",
14025
14189
  main: "./dist/index.js",
@@ -14079,7 +14243,7 @@ var package_default = {
14079
14243
  "@types/node": "24.19.2",
14080
14244
  "conventional-changelog-conventionalcommits": "9.3.1",
14081
14245
  esbuild: "0.28.2",
14082
- "gpt-tokenizer": "4.0.4",
14246
+ "gpt-tokenizer": "4.1.0",
14083
14247
  "semantic-release": "25.0.9",
14084
14248
  tsx: "4.23.15",
14085
14249
  typescript: "6.0.3",
@@ -15144,6 +15308,7 @@ function startProgressTicker(reportProgress, message, intervalMs = PROGRESS_INTE
15144
15308
 
15145
15309
  // src/utils/chat-turn.ts
15146
15310
  var TEXT_PREVIEW_LENGTH = 120;
15311
+ var DRAFT_LOOKUP_TIMEOUT_MS = 5e3;
15147
15312
  async function runTurn({
15148
15313
  reportProgress,
15149
15314
  progressIntervalMs = PROGRESS_INTERVAL_MS,
@@ -15170,8 +15335,9 @@ async function runTurn({
15170
15335
  onToolActivity: ({ tool, state }) => reportProgress(`${tool}: ${state}`)
15171
15336
  });
15172
15337
  } catch (error) {
15338
+ stopProgress();
15173
15339
  throw new Error(
15174
- `Conversation ${turn.conversationId}: ${describeTurnError(error, turn.signal)}`
15340
+ `Conversation ${turn.conversationId}: ${await describeTurnError(error, turn)}`
15175
15341
  );
15176
15342
  } finally {
15177
15343
  stopProgress();
@@ -15186,8 +15352,8 @@ function describeConversationError(conversationId) {
15186
15352
  throw new Error(`Conversation ${conversationId}: ${formatToolError(error)}`);
15187
15353
  };
15188
15354
  }
15189
- function describeTurnError(error, signal) {
15190
- if (signal?.aborted) {
15355
+ async function describeTurnError(error, turn) {
15356
+ if (turn.signal?.aborted) {
15191
15357
  return "the turn was cancelled before it finished. Whatever the assistant had said is saved; get-conversation shows it and continue-conversation resumes.";
15192
15358
  }
15193
15359
  if (error instanceof ChatTurnInputError) {
@@ -15204,11 +15370,28 @@ function describeTurnError(error, signal) {
15204
15370
  return error.message;
15205
15371
  }
15206
15372
  }
15207
- const refusal = getChatBillingRefusal(error)?.message;
15373
+ const refusal = getChatTurnBillingRefusal(error)?.message;
15208
15374
  if (refusal !== void 0) {
15209
15375
  return `the turn was refused. ${refusal.endsWith(".") ? refusal : `${refusal}.`} Retrying won't help until the user acts.`;
15210
15376
  }
15211
- return `the turn failed. ${formatToolError(error)} The conversation is intact; continue-conversation resumes it.`;
15377
+ const { requestId } = readFailedRequest(error);
15378
+ const reason = formatToolError(error).replace(/\.?$/, ".");
15379
+ const failed = `the turn failed${requestId ? ` (request ${requestId})` : ""}. ${reason} The conversation is intact; continue-conversation resumes it.`;
15380
+ const draftId = classifyChatTurnFailure(error) === "broken" ? await findDraft(turn) : null;
15381
+ return draftId ? `${failed} Its draft ${draftId} is saved; get-draft shows it.` : failed;
15382
+ }
15383
+ function findDraft({
15384
+ api,
15385
+ conversationId,
15386
+ workspaceId,
15387
+ signal
15388
+ }) {
15389
+ const deadline = AbortSignal.timeout(DRAFT_LOOKUP_TIMEOUT_MS);
15390
+ return findConversationDraftId(api, {
15391
+ conversationId,
15392
+ workspaceId: workspaceId ?? null,
15393
+ signal: signal ? AbortSignal.any([signal, deadline]) : deadline
15394
+ }).catch(() => null);
15212
15395
  }
15213
15396
  function describeTurnResult(result) {
15214
15397
  const next = describeNextStep(result);
@@ -16278,6 +16461,37 @@ var startConversation = {
16278
16461
 
16279
16462
  // src/tools/test-draft.ts
16280
16463
  import { z as z91 } from "zod";
16464
+
16465
+ // src/utils/run-wait.ts
16466
+ async function waitWithProgress(wait, {
16467
+ run,
16468
+ subject,
16469
+ startedStatus,
16470
+ recovery,
16471
+ timeoutMs,
16472
+ signal,
16473
+ reportProgress
16474
+ }) {
16475
+ let state = startedStatus;
16476
+ reportProgress(`${run} started; waiting up to ${timeoutMs / 6e4} min`);
16477
+ const stopProgress = startProgressTicker(
16478
+ reportProgress,
16479
+ (elapsedSeconds) => `${run}: ${state} (${elapsedSeconds}s)`
16480
+ );
16481
+ try {
16482
+ return await wait((polled) => {
16483
+ const step = polled.steps.find((candidate) => candidate.status === "running");
16484
+ state = step ? `running ${step.stepId}` : polled.status;
16485
+ });
16486
+ } catch (error) {
16487
+ const why = signal.aborted ? "the wait was cancelled." : `checking on it failed: ${formatToolError(error).replace(/\.?$/, ".")}`;
16488
+ throw new Error(`${subject} started, but ${why} ${recovery}`);
16489
+ } finally {
16490
+ stopProgress();
16491
+ }
16492
+ }
16493
+
16494
+ // src/tools/test-draft.ts
16281
16495
  var DEFAULT_WAIT_MINUTES = 10;
16282
16496
  var parameters27 = {
16283
16497
  draftId: z91.string().describe("The draft to test, as listed by find-drafts or get-draft."),
@@ -16334,11 +16548,23 @@ var testDraft = {
16334
16548
  structuredContent: { ...base, outcome: "started", status: started.status }
16335
16549
  };
16336
16550
  }
16337
- const finished = await waitWithProgress(draftApi, started, {
16338
- timeoutMs: waitMinutes * 6e4,
16339
- signal,
16340
- reportProgress
16341
- });
16551
+ const timeoutMs = waitMinutes * 6e4;
16552
+ const finished = await waitWithProgress(
16553
+ (onPoll) => waitForDraftTest(draftApi, started, {
16554
+ timeoutMs,
16555
+ signal,
16556
+ onPoll
16557
+ }),
16558
+ {
16559
+ run: `Test run ${started.testRunId}`,
16560
+ subject: `Test run ${started.testRunId} of draft ${draftId}`,
16561
+ startedStatus: started.status,
16562
+ recovery: "It keeps running; get-test-run shows how it ends, so do not start another test to check on it.",
16563
+ timeoutMs,
16564
+ signal,
16565
+ reportProgress
16566
+ }
16567
+ );
16342
16568
  const output = {
16343
16569
  ...base,
16344
16570
  outcome: finished.outcome,
@@ -16359,36 +16585,6 @@ var testDraft = {
16359
16585
  };
16360
16586
  }
16361
16587
  };
16362
- async function waitWithProgress(draftApi, started, {
16363
- timeoutMs,
16364
- signal,
16365
- reportProgress
16366
- }) {
16367
- const run = `Test run ${started.testRunId}`;
16368
- let state = started.status;
16369
- reportProgress(`${run} started; waiting up to ${timeoutMs / 6e4} min`);
16370
- const stopProgress = startProgressTicker(
16371
- reportProgress,
16372
- (elapsedSeconds) => `${run}: ${state} (${elapsedSeconds}s)`
16373
- );
16374
- try {
16375
- return await waitForDraftTest(draftApi, started, {
16376
- timeoutMs,
16377
- signal,
16378
- onPoll: (polled) => {
16379
- const step = polled.steps.find((candidate) => candidate.status === "running");
16380
- state = step ? `running ${step.stepId}` : polled.status;
16381
- }
16382
- });
16383
- } catch (error) {
16384
- const why = signal.aborted ? "the wait was cancelled." : `checking on it failed: ${formatToolError(error).replace(/\.?$/, ".")}`;
16385
- throw new Error(
16386
- `${run} of draft ${started.draftId} started, but ${why} It keeps running; get-test-run shows how it ends, so do not start another test to check on it.`
16387
- );
16388
- } finally {
16389
- stopProgress();
16390
- }
16391
- }
16392
16588
  function describeFinished(output, waitMinutes) {
16393
16589
  const run = `Test run ${output.testRunId} of draft ${output.draftId}`;
16394
16590
  const otherTriggers = output.trigger.count > 1 ? ` It tested trigger ${output.trigger.index} of ${output.trigger.count}; pass trigger to test another.` : "";
@@ -16411,24 +16607,35 @@ function describeFinished(output, waitMinutes) {
16411
16607
 
16412
16608
  // src/tools/trigger-automation.ts
16413
16609
  import { z as z92 } from "zod";
16610
+ var DEFAULT_WAIT_MINUTES2 = 10;
16414
16611
  var parameters28 = {
16415
16612
  automationId: z92.string().describe("The id of the automation, as returned by find-automations."),
16613
+ waitMinutes: z92.number().int().min(0).max(30).optional().describe(
16614
+ `How long to wait for the run to end; 0 returns once it starts. Default ${DEFAULT_WAIT_MINUTES2}.`
16615
+ ),
16416
16616
  workspaceId: workspaceIdParameter
16417
16617
  };
16418
16618
  var outputSchema26 = {
16419
16619
  automationId: z92.string(),
16420
16620
  name: z92.string(),
16421
- accepted: z92.literal(true).describe("The backend accepted the run. It has not finished.")
16621
+ runId: z92.string().optional().describe("get-run reads the run with it."),
16622
+ outcome: z92.enum(["started", ...RUN_OUTCOMES]).describe(
16623
+ "started: not waited for. timed_out: still going when the wait ended; it keeps running."
16624
+ ),
16625
+ status: runDetailSchema.shape.status.optional(),
16626
+ failedStep: draftTestRunSchema.shape.failedStep,
16627
+ error: z92.string().optional().describe("The failed step's error, else why the run failed or was skipped."),
16628
+ result: runDetailSchema.shape.result
16422
16629
  };
16423
16630
  var triggerAutomation = {
16424
16631
  name: ToolNames.TRIGGER_AUTOMATION,
16425
- description: "Starts a run of a schedule-triggered automation now, without waiting for its next scheduled time, and returns as soon as the backend accepts it. The run happens in the background; find-runs shows the outcome. Runs even when the automation is disabled. Cannot start automations that run on events or API calls. Confirm intent with the user first.",
16632
+ description: "Starts a run of a schedule-triggered automation now, without waiting for its next scheduled time, and waits for it to end, reporting progress meanwhile. Runs even when the automation is disabled. Cannot start automations that run on events or API calls. Confirm intent with the user first.",
16426
16633
  parameters: parameters28,
16427
16634
  outputSchema: outputSchema26,
16428
16635
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
16429
- async execute({ automationId, workspaceId }, { api, signal }) {
16430
- const options = { signal };
16431
- const dto = workspaceId === void 0 ? await api.automations.get(automationId, options) : await api.workspaceAutomations.get(workspaceId, automationId, options);
16636
+ async execute({ automationId, waitMinutes = DEFAULT_WAIT_MINUTES2, workspaceId }, { api, signal, reportProgress }) {
16637
+ const runApi = bindRunApi(api, workspaceId ?? null, { signal });
16638
+ const dto = await runApi.getAutomation(automationId);
16432
16639
  const name = readManifestMetadata(dto)?.name ?? "(unnamed)";
16433
16640
  const triggerType = readPrimaryTriggerType(dto);
16434
16641
  if (triggerType !== "schedule") {
@@ -16436,17 +16643,71 @@ var triggerAutomation = {
16436
16643
  `"${name}" runs on ${triggerType ?? "no"} triggers and cannot be started by hand: a webhook automation runs when its event arrives, and an api automation needs an input payload this tool cannot supply.`
16437
16644
  );
16438
16645
  }
16439
- if (workspaceId === void 0) {
16440
- await api.automations.trigger(automationId, options);
16441
- } else {
16442
- await api.workspaceAutomations.trigger(workspaceId, automationId, options);
16646
+ const started = await runApi.trigger(automationId);
16647
+ if (!started) {
16648
+ return {
16649
+ textContent: `Run of "${name}" accepted. It runs in the background; find-runs shows the outcome.`,
16650
+ structuredContent: { automationId, name, outcome: "started" }
16651
+ };
16652
+ }
16653
+ const base = { automationId, name, runId: started.runId };
16654
+ if (waitMinutes === 0) {
16655
+ return {
16656
+ textContent: `Run ${started.runId} of "${name}" started and runs in the background; get-run shows how it ends.`,
16657
+ structuredContent: { ...base, outcome: "started", status: started.status }
16658
+ };
16443
16659
  }
16660
+ const timeoutMs = waitMinutes * 6e4;
16661
+ const finished = await waitWithProgress(
16662
+ (onPoll) => waitForRun(runApi, started, { timeoutMs, signal, onPoll }),
16663
+ {
16664
+ run: `Run ${started.runId}`,
16665
+ subject: `Run ${started.runId} of "${name}"`,
16666
+ startedStatus: started.status,
16667
+ recovery: "It keeps running; get-run shows how it ends, so do not trigger it again to check on it.",
16668
+ timeoutMs,
16669
+ signal,
16670
+ reportProgress
16671
+ }
16672
+ );
16673
+ const output = {
16674
+ ...base,
16675
+ outcome: finished.outcome,
16676
+ status: finished.status,
16677
+ ...finished.failedStep ? {
16678
+ failedStep: {
16679
+ stepId: finished.failedStep.stepId,
16680
+ description: finished.failedStep.description,
16681
+ ...finished.failedStep.error ? { error: finished.failedStep.error } : {}
16682
+ }
16683
+ } : {},
16684
+ ...finished.error ? { error: finished.error } : {},
16685
+ ...finished.result ? { result: finished.result } : {}
16686
+ };
16444
16687
  return {
16445
- textContent: `Run of "${name}" accepted. It runs in the background; find-runs shows the outcome.`,
16446
- structuredContent: { automationId, name, accepted: true }
16688
+ textContent: describeFinished2(output, waitMinutes),
16689
+ structuredContent: output
16447
16690
  };
16448
16691
  }
16449
16692
  };
16693
+ function describeFinished2(output, waitMinutes) {
16694
+ const run = `Run ${output.runId} of "${output.name}"`;
16695
+ switch (output.outcome) {
16696
+ case "completed":
16697
+ return `${run} completed.`;
16698
+ case "failed": {
16699
+ const step = output.failedStep ? ` at step ${output.failedStep.stepId}` : "";
16700
+ const error = output.error ? `: ${output.error}` : "";
16701
+ return `${run} failed${step}${error}. get-run shows every step.`;
16702
+ }
16703
+ case "skipped":
16704
+ return `${run} was skipped${output.error ? `: ${output.error}` : ""}.`;
16705
+ case "cancelled":
16706
+ return `${run} was cancelled.`;
16707
+ case "timed_out":
16708
+ return `${run} was still ${output.status} after ${waitMinutes} min. It keeps running, possibly through a delay step; get-run shows how it ends.`;
16709
+ }
16710
+ }
16450
16711
 
16451
16712
  // src/tools/update-automation.ts
16452
16713
  import { z as z93 } from "zod";
@@ -16596,7 +16857,7 @@ You have access to a user's Todoist Automations through the tools listed.
16596
16857
  - Every reference is an opaque id taken from a find- or get- tool. There is no lookup by name and no URL parsing: list first, then act on the id. Connector ids are the one exception: they are stable slugs such as github or slack.
16597
16858
  - An automation belongs either to the user's personal space or to one Todoist workspace, and the two are separate. Pass workspaceId to reach workspace automations; omit it for personal ones.
16598
16859
  - update-automation, set-automation-schedule, set-automation-visibility, move-automation, trigger-automation, delete-automation, update-draft, connect-draft, publish-draft, delete-draft and disconnect-service act immediately: no preview, and no confirmation of their own. Read the target first (get-automation, get-draft for a draft, or get-connector for a connection) so it is the right one, and confirm intent with the user before changing, running or deleting anything they did not explicitly ask for.
16599
- - trigger-automation returns before the run finishes; find-runs shows the outcome.
16860
+ - trigger-automation waits for the run to end (waitMinutes, default 10); a run still going then keeps running, and get-run reads it.
16600
16861
  - A workspace automation is either visible to the whole workspace or private to its creator; isOwner says whether the user created it, and createdBy gives the creator's Todoist user id. Another member's automation is read-only for the user, but trigger-automation still works on it and the run executes on its creator's connections; find-runs shows who started each run as triggeredBy.
16601
16862
  - A workspace admin can update-automation, set-automation-visibility and delete-automation on another member's team-visible automation, but cannot reschedule it, change what it does (that needs its creator's draft or conversation) or reach a private one. updatedBy on an automation tells the creator's own change from an admin's.
16602
16863
  - move-automation puts an automation the user created into a workspace, from the personal space or another workspace, and never back out. Connections stay personal in every space: a moved or shared automation keeps running on its creator's own connections.
@@ -16634,4 +16895,4 @@ export {
16634
16895
  DEFAULT_BASE_URL,
16635
16896
  getMcpServer
16636
16897
  };
16637
- //# sourceMappingURL=chunk-6UJYYF3M.js.map
16898
+ //# sourceMappingURL=chunk-UZ45CAIT.js.map