@doist/automations-mcp 1.11.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()
@@ -10204,7 +10209,8 @@ var FRONTEND_COHORT_EVENTS = [
10204
10209
  "template:used",
10205
10210
  "top-up:completed",
10206
10211
  "guided-creation:started",
10207
- "guided-creation:completed"
10212
+ "guided-creation:completed",
10213
+ "workspace:switched"
10208
10214
  ];
10209
10215
  var BACKEND_COHORT_EVENTS = [
10210
10216
  // API-process (publish lifecycle)
@@ -11202,13 +11208,19 @@ var AutomationsRequestError = class extends Error {
11202
11208
  * scope: a challenge can say the grant falls short without saying of what.
11203
11209
  */
11204
11210
  requiredScope;
11205
- 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) {
11206
11217
  super(message);
11207
11218
  this.name = "AutomationsRequestError";
11208
11219
  this.httpStatusCode = httpStatusCode;
11209
11220
  this.responseData = responseData;
11210
11221
  this.authChallenge = authChallenge;
11211
11222
  this.requiredScope = parseChallengeScope(authChallenge);
11223
+ this.requestId = requestId;
11212
11224
  }
11213
11225
  isAuthError() {
11214
11226
  return this.httpStatusCode === 401 || this.httpStatusCode === 403;
@@ -11279,7 +11291,7 @@ async function request(args) {
11279
11291
  const data = await readResponseBody(response);
11280
11292
  if (!response.ok) {
11281
11293
  const message = typeof data === "object" && data !== null && "message" in data ? String(data.message) : response.statusText || `Request failed with status ${response.status}`;
11282
- 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);
11283
11295
  }
11284
11296
  return data;
11285
11297
  }
@@ -11699,13 +11711,15 @@ var AutomationsClient = class extends BaseClient {
11699
11711
  });
11700
11712
  return createDraftResponseSchema.parse(data);
11701
11713
  }
11714
+ /** Starts a run now. Undefined from an API that predates returning the run. */
11702
11715
  async trigger(id, options) {
11703
11716
  const parsedId = IdSchema.parse(id);
11704
- await this.request({
11717
+ const data = await this.request({
11705
11718
  method: "POST",
11706
11719
  path: automationsEndpoints.trigger(parsedId),
11707
11720
  options
11708
11721
  });
11722
+ return triggerAutomationResponseSchema.optional().parse(data);
11709
11723
  }
11710
11724
  async test(id, options) {
11711
11725
  const parsedId = IdSchema.parse(id);
@@ -12040,13 +12054,17 @@ var WorkspaceAutomationsClient = class extends BaseClient {
12040
12054
  });
12041
12055
  return automationSchema.parse(data);
12042
12056
  }
12043
- /** 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
+ */
12044
12061
  async trigger(workspaceId, id, options) {
12045
- await this.request({
12062
+ const data = await this.request({
12046
12063
  method: "POST",
12047
12064
  path: workspaceAutomationsEndpoints.trigger(IdSchema.parse(workspaceId), IdSchema.parse(id)),
12048
12065
  options
12049
12066
  });
12067
+ return triggerAutomationResponseSchema.optional().parse(data);
12050
12068
  }
12051
12069
  /**
12052
12070
  * Deletes the automation. Workspace admins may delete another member's
@@ -13451,31 +13469,11 @@ function readManifestName(manifest) {
13451
13469
  return typeof name === "string" && name.trim() ? name : "(unnamed)";
13452
13470
  }
13453
13471
 
13454
- // ../../packages/sdk/dist/esm/drafts/draft-test.js
13455
- var DRAFT_TEST_OUTCOMES = [
13456
- "passed",
13457
- "failed",
13458
- "skipped",
13459
- "cancelled",
13460
- "timed_out"
13461
- ];
13472
+ // ../../packages/sdk/dist/esm/runs/poll-until-closed.js
13462
13473
  var DEFAULT_POLL_INTERVALS_MS = { first: 2e3, max: 1e4 };
13463
13474
  var STATUS_REQUEST_GRACE_MS = 1e4;
13464
- var OPEN_RUN_STATUSES = /* @__PURE__ */ new Set([
13465
- "pending",
13466
- "running"
13467
- ]);
13468
- async function startDraftTest(draftApi, { draftId, request: request2 }) {
13469
- const started = await draftApi.test(draftId, request2);
13470
- return {
13471
- draftId,
13472
- testRunId: started.testRunId,
13473
- trigger: started.trigger,
13474
- connectorCalls: started.connectorCalls,
13475
- status: started.status
13476
- };
13477
- }
13478
- 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 }) {
13479
13477
  const intervals = pollIntervalsMs ?? DEFAULT_POLL_INTERVALS_MS;
13480
13478
  const deadline = Date.now() + timeoutMs;
13481
13479
  let intervalMs = intervals.first;
@@ -13484,24 +13482,64 @@ async function waitForDraftTest(draftApi, { draftId, testRunId }, { timeoutMs, p
13484
13482
  let last = null;
13485
13483
  while (true) {
13486
13484
  try {
13487
- last = await draftApi.getTestRun(draftId, testRunId, void 0, requestSignal);
13485
+ last = await read(requestSignal);
13488
13486
  } catch (error) {
13489
13487
  if (signal.aborted || !requestTimeout.aborted)
13490
13488
  throw error;
13491
- return toFinished(last, "timed_out");
13489
+ return { timedOut: true, run: last };
13492
13490
  }
13493
13491
  if (!OPEN_RUN_STATUSES.has(last.status)) {
13494
- return toFinished(last, toOutcome(last.status));
13492
+ return { timedOut: false, run: last };
13495
13493
  }
13496
13494
  onPoll?.(last);
13497
13495
  const remainingMs = deadline - Date.now();
13498
13496
  if (remainingMs <= 0) {
13499
- return toFinished(last, "timed_out");
13497
+ return { timedOut: true, run: last };
13500
13498
  }
13501
13499
  await delay(Math.min(intervalMs, remainingMs), signal);
13502
13500
  intervalMs = Math.min(intervalMs * 2, intervals.max);
13503
13501
  }
13504
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
+ }
13505
13543
  function toDraftTestTriggerSelector(value) {
13506
13544
  if (typeof value === "number")
13507
13545
  return { index: value };
@@ -13528,22 +13566,65 @@ function toOutcome(status) {
13528
13566
  return "failed";
13529
13567
  }
13530
13568
  }
13531
- function delay(ms, signal) {
13532
- return new Promise((resolve, reject) => {
13533
- if (signal.aborted) {
13534
- reject(signal.reason);
13535
- return;
13536
- }
13537
- const onAbort = () => {
13538
- clearTimeout(timer);
13539
- 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
+ })
13540
13580
  };
13541
- const timer = setTimeout(() => {
13542
- signal.removeEventListener("abort", onAbort);
13543
- resolve();
13544
- }, ms);
13545
- signal.addEventListener("abort", onAbort, { once: true });
13546
- });
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;
13547
13628
  }
13548
13629
 
13549
13630
  // ../../packages/sdk/dist/esm/chat-turn/connection-requirements.js
@@ -13935,6 +14016,67 @@ function assertSelectionsUnambiguous(requirements, selections) {
13935
14016
 
13936
14017
  // ../../packages/sdk/dist/esm/chat-turn/transport.js
13937
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
13938
14080
  var ChatTurnTransport = class {
13939
14081
  transport;
13940
14082
  constructor({ token, baseUrl, headers, fetch: fetch2 }) {
@@ -13961,39 +14103,62 @@ var ChatTurnTransport = class {
13961
14103
  }
13962
14104
  /** Streams one server turn, reporting incremental text and deduplicated tool state changes. */
13963
14105
  async streamTurn({ conversationId, workspaceId = null, message, signal, onText, onToolActivity }) {
13964
- const stream = await this.transport.sendMessages({
13965
- trigger: "submit-message",
13966
- chatId: conversationId,
13967
- messageId: void 0,
13968
- messages: [message],
13969
- abortSignal: signal,
13970
- body: { conversationId, workspaceId }
13971
- });
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
+ }
13972
14122
  let finalMessage;
13973
14123
  let emittedText = "";
13974
14124
  const emittedToolStates = /* @__PURE__ */ new Set();
13975
- for await (const currentMessage of readUIMessageStream({
13976
- stream,
13977
- terminateOnError: true
13978
- })) {
13979
- finalMessage = currentMessage;
13980
- const visibleText = getVisibleText(currentMessage);
13981
- if (visibleText.startsWith(emittedText)) {
13982
- const delta = visibleText.slice(emittedText.length);
13983
- if (delta)
13984
- onText?.(delta);
13985
- emittedText = visibleText;
13986
- }
13987
- for (const part of currentMessage.parts) {
13988
- const activity = toToolActivity(part);
13989
- if (!activity)
13990
- continue;
13991
- const key = `${activity.toolCallId}:${activity.state}`;
13992
- if (emittedToolStates.has(key))
13993
- continue;
13994
- emittedToolStates.add(key);
13995
- 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
+ }
13996
14157
  }
14158
+ } catch (error) {
14159
+ if (callbackError)
14160
+ throw callbackError.error;
14161
+ throw fail("stream", error);
13997
14162
  }
13998
14163
  if (!finalMessage) {
13999
14164
  throw new Error("The Automations chat returned an empty response.");
@@ -14018,7 +14183,7 @@ import { McpServer } from "@modelcontextprotocol/server";
14018
14183
  // package.json
14019
14184
  var package_default = {
14020
14185
  name: "@doist/automations-mcp",
14021
- version: "1.11.0",
14186
+ version: "1.13.0",
14022
14187
  description: "MCP server and importable AI tools for Todoist Automations",
14023
14188
  type: "module",
14024
14189
  main: "./dist/index.js",
@@ -14078,7 +14243,7 @@ var package_default = {
14078
14243
  "@types/node": "24.19.2",
14079
14244
  "conventional-changelog-conventionalcommits": "9.3.1",
14080
14245
  esbuild: "0.28.2",
14081
- "gpt-tokenizer": "4.0.3",
14246
+ "gpt-tokenizer": "4.1.0",
14082
14247
  "semantic-release": "25.0.9",
14083
14248
  tsx: "4.23.15",
14084
14249
  typescript: "6.0.3",
@@ -15143,6 +15308,7 @@ function startProgressTicker(reportProgress, message, intervalMs = PROGRESS_INTE
15143
15308
 
15144
15309
  // src/utils/chat-turn.ts
15145
15310
  var TEXT_PREVIEW_LENGTH = 120;
15311
+ var DRAFT_LOOKUP_TIMEOUT_MS = 5e3;
15146
15312
  async function runTurn({
15147
15313
  reportProgress,
15148
15314
  progressIntervalMs = PROGRESS_INTERVAL_MS,
@@ -15169,8 +15335,9 @@ async function runTurn({
15169
15335
  onToolActivity: ({ tool, state }) => reportProgress(`${tool}: ${state}`)
15170
15336
  });
15171
15337
  } catch (error) {
15338
+ stopProgress();
15172
15339
  throw new Error(
15173
- `Conversation ${turn.conversationId}: ${describeTurnError(error, turn.signal)}`
15340
+ `Conversation ${turn.conversationId}: ${await describeTurnError(error, turn)}`
15174
15341
  );
15175
15342
  } finally {
15176
15343
  stopProgress();
@@ -15185,8 +15352,8 @@ function describeConversationError(conversationId) {
15185
15352
  throw new Error(`Conversation ${conversationId}: ${formatToolError(error)}`);
15186
15353
  };
15187
15354
  }
15188
- function describeTurnError(error, signal) {
15189
- if (signal?.aborted) {
15355
+ async function describeTurnError(error, turn) {
15356
+ if (turn.signal?.aborted) {
15190
15357
  return "the turn was cancelled before it finished. Whatever the assistant had said is saved; get-conversation shows it and continue-conversation resumes.";
15191
15358
  }
15192
15359
  if (error instanceof ChatTurnInputError) {
@@ -15203,11 +15370,28 @@ function describeTurnError(error, signal) {
15203
15370
  return error.message;
15204
15371
  }
15205
15372
  }
15206
- const refusal = getChatBillingRefusal(error)?.message;
15373
+ const refusal = getChatTurnBillingRefusal(error)?.message;
15207
15374
  if (refusal !== void 0) {
15208
15375
  return `the turn was refused. ${refusal.endsWith(".") ? refusal : `${refusal}.`} Retrying won't help until the user acts.`;
15209
15376
  }
15210
- 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);
15211
15395
  }
15212
15396
  function describeTurnResult(result) {
15213
15397
  const next = describeNextStep(result);
@@ -16277,6 +16461,37 @@ var startConversation = {
16277
16461
 
16278
16462
  // src/tools/test-draft.ts
16279
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
16280
16495
  var DEFAULT_WAIT_MINUTES = 10;
16281
16496
  var parameters27 = {
16282
16497
  draftId: z91.string().describe("The draft to test, as listed by find-drafts or get-draft."),
@@ -16333,11 +16548,23 @@ var testDraft = {
16333
16548
  structuredContent: { ...base, outcome: "started", status: started.status }
16334
16549
  };
16335
16550
  }
16336
- const finished = await waitWithProgress(draftApi, started, {
16337
- timeoutMs: waitMinutes * 6e4,
16338
- signal,
16339
- reportProgress
16340
- });
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
+ );
16341
16568
  const output = {
16342
16569
  ...base,
16343
16570
  outcome: finished.outcome,
@@ -16358,36 +16585,6 @@ var testDraft = {
16358
16585
  };
16359
16586
  }
16360
16587
  };
16361
- async function waitWithProgress(draftApi, started, {
16362
- timeoutMs,
16363
- signal,
16364
- reportProgress
16365
- }) {
16366
- const run = `Test run ${started.testRunId}`;
16367
- let state = started.status;
16368
- reportProgress(`${run} started; waiting up to ${timeoutMs / 6e4} min`);
16369
- const stopProgress = startProgressTicker(
16370
- reportProgress,
16371
- (elapsedSeconds) => `${run}: ${state} (${elapsedSeconds}s)`
16372
- );
16373
- try {
16374
- return await waitForDraftTest(draftApi, started, {
16375
- timeoutMs,
16376
- signal,
16377
- onPoll: (polled) => {
16378
- const step = polled.steps.find((candidate) => candidate.status === "running");
16379
- state = step ? `running ${step.stepId}` : polled.status;
16380
- }
16381
- });
16382
- } catch (error) {
16383
- const why = signal.aborted ? "the wait was cancelled." : `checking on it failed: ${formatToolError(error).replace(/\.?$/, ".")}`;
16384
- throw new Error(
16385
- `${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.`
16386
- );
16387
- } finally {
16388
- stopProgress();
16389
- }
16390
- }
16391
16588
  function describeFinished(output, waitMinutes) {
16392
16589
  const run = `Test run ${output.testRunId} of draft ${output.draftId}`;
16393
16590
  const otherTriggers = output.trigger.count > 1 ? ` It tested trigger ${output.trigger.index} of ${output.trigger.count}; pass trigger to test another.` : "";
@@ -16410,24 +16607,35 @@ function describeFinished(output, waitMinutes) {
16410
16607
 
16411
16608
  // src/tools/trigger-automation.ts
16412
16609
  import { z as z92 } from "zod";
16610
+ var DEFAULT_WAIT_MINUTES2 = 10;
16413
16611
  var parameters28 = {
16414
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
+ ),
16415
16616
  workspaceId: workspaceIdParameter
16416
16617
  };
16417
16618
  var outputSchema26 = {
16418
16619
  automationId: z92.string(),
16419
16620
  name: z92.string(),
16420
- 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
16421
16629
  };
16422
16630
  var triggerAutomation = {
16423
16631
  name: ToolNames.TRIGGER_AUTOMATION,
16424
- 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.",
16425
16633
  parameters: parameters28,
16426
16634
  outputSchema: outputSchema26,
16427
16635
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
16428
- async execute({ automationId, workspaceId }, { api, signal }) {
16429
- const options = { signal };
16430
- 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);
16431
16639
  const name = readManifestMetadata(dto)?.name ?? "(unnamed)";
16432
16640
  const triggerType = readPrimaryTriggerType(dto);
16433
16641
  if (triggerType !== "schedule") {
@@ -16435,17 +16643,71 @@ var triggerAutomation = {
16435
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.`
16436
16644
  );
16437
16645
  }
16438
- if (workspaceId === void 0) {
16439
- await api.automations.trigger(automationId, options);
16440
- } else {
16441
- 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
+ };
16442
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
+ };
16443
16687
  return {
16444
- textContent: `Run of "${name}" accepted. It runs in the background; find-runs shows the outcome.`,
16445
- structuredContent: { automationId, name, accepted: true }
16688
+ textContent: describeFinished2(output, waitMinutes),
16689
+ structuredContent: output
16446
16690
  };
16447
16691
  }
16448
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
+ }
16449
16711
 
16450
16712
  // src/tools/update-automation.ts
16451
16713
  import { z as z93 } from "zod";
@@ -16595,7 +16857,7 @@ You have access to a user's Todoist Automations through the tools listed.
16595
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.
16596
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.
16597
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.
16598
- - 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.
16599
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.
16600
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.
16601
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.
@@ -16633,4 +16895,4 @@ export {
16633
16895
  DEFAULT_BASE_URL,
16634
16896
  getMcpServer
16635
16897
  };
16636
- //# sourceMappingURL=chunk-66AC5E4I.js.map
16898
+ //# sourceMappingURL=chunk-UZ45CAIT.js.map