@doist/automations-cli 1.26.0 → 1.28.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.
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  collectSkillStatus,
8
8
  getInstaller,
9
9
  updateAllInstalledSkills
10
- } from "./chunk-ZTBPLXQS.js";
10
+ } from "./chunk-TGFCBT2H.js";
11
11
 
12
12
  // ../../node_modules/ajv/dist/compile/codegen/code.js
13
13
  var require_code = __commonJS({
@@ -6899,7 +6899,7 @@ import { fileURLToPath } from "node:url";
6899
6899
  // package.json
6900
6900
  var package_default = {
6901
6901
  name: "@doist/automations-cli",
6902
- version: "1.26.0",
6902
+ version: "1.28.0",
6903
6903
  description: "Command-line client for Todoist Automations",
6904
6904
  type: "module",
6905
6905
  bin: {
@@ -29192,6 +29192,11 @@ var automationListResponseSchema = external_exports.object({
29192
29192
  data: external_exports.array(automationSchema),
29193
29193
  nextCursor: external_exports.string().nullable()
29194
29194
  });
29195
+ var triggerAutomationResponseSchema = external_exports.object({
29196
+ automationId: external_exports.string(),
29197
+ runId: external_exports.string(),
29198
+ status: external_exports.literal("pending")
29199
+ });
29195
29200
  var updateAutomationSchema = external_exports.object({
29196
29201
  enabled: external_exports.boolean().optional(),
29197
29202
  manifest: manifestSchema.optional()
@@ -29962,7 +29967,8 @@ var FRONTEND_COHORT_EVENTS = [
29962
29967
  "template:used",
29963
29968
  "top-up:completed",
29964
29969
  "guided-creation:started",
29965
- "guided-creation:completed"
29970
+ "guided-creation:completed",
29971
+ "workspace:switched"
29966
29972
  ];
29967
29973
  var BACKEND_COHORT_EVENTS = [
29968
29974
  // API-process (publish lifecycle)
@@ -31056,13 +31062,19 @@ var AutomationsRequestError = class extends Error {
31056
31062
  * scope: a challenge can say the grant falls short without saying of what.
31057
31063
  */
31058
31064
  requiredScope;
31059
- constructor(message, httpStatusCode, responseData, authChallenge2) {
31065
+ /**
31066
+ * The `X-Request-Id` the response carried, which the backend logs the request
31067
+ * under. Undefined in a browser unless the header is exposed by CORS.
31068
+ */
31069
+ requestId;
31070
+ constructor(message, httpStatusCode, responseData, authChallenge2, requestId) {
31060
31071
  super(message);
31061
31072
  this.name = "AutomationsRequestError";
31062
31073
  this.httpStatusCode = httpStatusCode;
31063
31074
  this.responseData = responseData;
31064
31075
  this.authChallenge = authChallenge2;
31065
31076
  this.requiredScope = parseChallengeScope(authChallenge2);
31077
+ this.requestId = requestId;
31066
31078
  }
31067
31079
  isAuthError() {
31068
31080
  return this.httpStatusCode === 401 || this.httpStatusCode === 403;
@@ -31133,7 +31145,7 @@ async function request(args) {
31133
31145
  const data = await readResponseBody(response);
31134
31146
  if (!response.ok) {
31135
31147
  const message = typeof data === "object" && data !== null && "message" in data ? String(data.message) : response.statusText || `Request failed with status ${response.status}`;
31136
- throw new AutomationsRequestError(message, response.status, data, response.headers.get("WWW-Authenticate") ?? void 0);
31148
+ throw new AutomationsRequestError(message, response.status, data, response.headers.get("WWW-Authenticate") ?? void 0, response.headers.get("X-Request-Id") ?? void 0);
31137
31149
  }
31138
31150
  return data;
31139
31151
  }
@@ -31544,13 +31556,15 @@ var AutomationsClient = class extends BaseClient {
31544
31556
  });
31545
31557
  return createDraftResponseSchema.parse(data);
31546
31558
  }
31559
+ /** Starts a run now. Undefined from an API that predates returning the run. */
31547
31560
  async trigger(id, options) {
31548
31561
  const parsedId = IdSchema.parse(id);
31549
- await this.request({
31562
+ const data = await this.request({
31550
31563
  method: "POST",
31551
31564
  path: automationsEndpoints.trigger(parsedId),
31552
31565
  options
31553
31566
  });
31567
+ return triggerAutomationResponseSchema.optional().parse(data);
31554
31568
  }
31555
31569
  async test(id, options) {
31556
31570
  const parsedId = IdSchema.parse(id);
@@ -31885,13 +31899,17 @@ var WorkspaceAutomationsClient = class extends BaseClient {
31885
31899
  });
31886
31900
  return automationSchema.parse(data);
31887
31901
  }
31888
- /** Runs the automation now. The run executes as its creator, not as the caller. */
31902
+ /**
31903
+ * Runs the automation now. The run executes as its creator, not as the caller.
31904
+ * Undefined from an API that predates returning the run.
31905
+ */
31889
31906
  async trigger(workspaceId, id, options) {
31890
- await this.request({
31907
+ const data = await this.request({
31891
31908
  method: "POST",
31892
31909
  path: workspaceAutomationsEndpoints.trigger(IdSchema.parse(workspaceId), IdSchema.parse(id)),
31893
31910
  options
31894
31911
  });
31912
+ return triggerAutomationResponseSchema.optional().parse(data);
31895
31913
  }
31896
31914
  /**
31897
31915
  * Deletes the automation. Workspace admins may delete another member's
@@ -32801,6 +32819,9 @@ function readManifestMetadata(dto) {
32801
32819
  ...typeof dto.manifest.description === "string" ? { description: dto.manifest.description } : {}
32802
32820
  };
32803
32821
  }
32822
+ function readPrimaryTriggerType(dto) {
32823
+ return hasTriggerType(dto.manifest) ? getManifestTriggers(dto.manifest)[0]?.type : void 0;
32824
+ }
32804
32825
  function summarizeTrigger(trigger) {
32805
32826
  if (!isManifestRecord(trigger))
32806
32827
  return void 0;
@@ -33300,24 +33321,11 @@ function readManifestName(manifest) {
33300
33321
  return typeof name === "string" && name.trim() ? name : "(unnamed)";
33301
33322
  }
33302
33323
 
33303
- // ../../packages/sdk/dist/esm/drafts/draft-test.js
33324
+ // ../../packages/sdk/dist/esm/runs/poll-until-closed.js
33304
33325
  var DEFAULT_POLL_INTERVALS_MS = { first: 2e3, max: 1e4 };
33305
33326
  var STATUS_REQUEST_GRACE_MS = 1e4;
33306
- var OPEN_RUN_STATUSES = /* @__PURE__ */ new Set([
33307
- "pending",
33308
- "running"
33309
- ]);
33310
- async function startDraftTest(draftApi, { draftId, request: request2 }) {
33311
- const started = await draftApi.test(draftId, request2);
33312
- return {
33313
- draftId,
33314
- testRunId: started.testRunId,
33315
- trigger: started.trigger,
33316
- connectorCalls: started.connectorCalls,
33317
- status: started.status
33318
- };
33319
- }
33320
- async function waitForDraftTest(draftApi, { draftId, testRunId }, { timeoutMs, pollIntervalsMs, requestGraceMs, onPoll, signal }) {
33327
+ var OPEN_RUN_STATUSES = /* @__PURE__ */ new Set(["pending", "running"]);
33328
+ async function pollUntilClosed(read2, { timeoutMs, pollIntervalsMs, requestGraceMs, onPoll, signal }) {
33321
33329
  const intervals = pollIntervalsMs ?? DEFAULT_POLL_INTERVALS_MS;
33322
33330
  const deadline = Date.now() + timeoutMs;
33323
33331
  let intervalMs = intervals.first;
@@ -33326,24 +33334,57 @@ async function waitForDraftTest(draftApi, { draftId, testRunId }, { timeoutMs, p
33326
33334
  let last = null;
33327
33335
  while (true) {
33328
33336
  try {
33329
- last = await draftApi.getTestRun(draftId, testRunId, void 0, requestSignal);
33337
+ last = await read2(requestSignal);
33330
33338
  } catch (error62) {
33331
33339
  if (signal.aborted || !requestTimeout.aborted)
33332
33340
  throw error62;
33333
- return toFinished(last, "timed_out");
33341
+ return { timedOut: true, run: last };
33334
33342
  }
33335
33343
  if (!OPEN_RUN_STATUSES.has(last.status)) {
33336
- return toFinished(last, toOutcome(last.status));
33344
+ return { timedOut: false, run: last };
33337
33345
  }
33338
33346
  onPoll?.(last);
33339
33347
  const remainingMs = deadline - Date.now();
33340
33348
  if (remainingMs <= 0) {
33341
- return toFinished(last, "timed_out");
33349
+ return { timedOut: true, run: last };
33342
33350
  }
33343
33351
  await delay(Math.min(intervalMs, remainingMs), signal);
33344
33352
  intervalMs = Math.min(intervalMs * 2, intervals.max);
33345
33353
  }
33346
33354
  }
33355
+ function delay(ms, signal) {
33356
+ return new Promise((resolve, reject) => {
33357
+ if (signal.aborted) {
33358
+ reject(signal.reason);
33359
+ return;
33360
+ }
33361
+ const onAbort = () => {
33362
+ clearTimeout(timer);
33363
+ reject(signal.reason);
33364
+ };
33365
+ const timer = setTimeout(() => {
33366
+ signal.removeEventListener("abort", onAbort);
33367
+ resolve();
33368
+ }, ms);
33369
+ signal.addEventListener("abort", onAbort, { once: true });
33370
+ });
33371
+ }
33372
+
33373
+ // ../../packages/sdk/dist/esm/drafts/draft-test.js
33374
+ async function startDraftTest(draftApi, { draftId, request: request2 }) {
33375
+ const started = await draftApi.test(draftId, request2);
33376
+ return {
33377
+ draftId,
33378
+ testRunId: started.testRunId,
33379
+ trigger: started.trigger,
33380
+ connectorCalls: started.connectorCalls,
33381
+ status: started.status
33382
+ };
33383
+ }
33384
+ async function waitForDraftTest(draftApi, { draftId, testRunId }, options) {
33385
+ const polled = await pollUntilClosed((requestSignal) => draftApi.getTestRun(draftId, testRunId, void 0, requestSignal), options);
33386
+ return polled.timedOut ? toFinished(polled.run, "timed_out") : toFinished(polled.run, toOutcome(polled.run.status));
33387
+ }
33347
33388
  function toDraftTestTriggerSelector(value) {
33348
33389
  if (typeof value === "number")
33349
33390
  return { index: value };
@@ -33370,22 +33411,64 @@ function toOutcome(status) {
33370
33411
  return "failed";
33371
33412
  }
33372
33413
  }
33373
- function delay(ms, signal) {
33374
- return new Promise((resolve, reject) => {
33375
- if (signal.aborted) {
33376
- reject(signal.reason);
33377
- return;
33378
- }
33379
- const onAbort = () => {
33380
- clearTimeout(timer);
33381
- reject(signal.reason);
33414
+
33415
+ // ../../packages/sdk/dist/esm/runs/run-api.js
33416
+ function bindRunApi(api, workspaceId, { signal } = {}) {
33417
+ if (workspaceId === null) {
33418
+ return {
33419
+ workspaceId: null,
33420
+ getAutomation: (automationId) => api.automations.get(automationId, { signal }),
33421
+ trigger: (automationId) => api.automations.trigger(automationId, { signal }),
33422
+ getRun: (automationId, runId, args = {}, requestSignal) => api.automationRuns.get(automationId, runId, args, {
33423
+ signal: requestSignal ?? signal
33424
+ })
33382
33425
  };
33383
- const timer = setTimeout(() => {
33384
- signal.removeEventListener("abort", onAbort);
33385
- resolve();
33386
- }, ms);
33387
- signal.addEventListener("abort", onAbort, { once: true });
33388
- });
33426
+ }
33427
+ return {
33428
+ workspaceId,
33429
+ getAutomation: (automationId) => api.workspaceAutomations.get(workspaceId, automationId, { signal }),
33430
+ trigger: (automationId) => api.workspaceAutomations.trigger(workspaceId, automationId, { signal }),
33431
+ getRun: (automationId, runId, args = {}, requestSignal) => api.workspaceAutomationRuns.get(workspaceId, automationId, runId, args, {
33432
+ signal: requestSignal ?? signal
33433
+ })
33434
+ };
33435
+ }
33436
+
33437
+ // ../../packages/sdk/dist/esm/runs/run-wait.js
33438
+ async function waitForRun(runApi, { automationId, runId }, options) {
33439
+ const polled = await pollUntilClosed((requestSignal) => runApi.getRun(automationId, runId, void 0, requestSignal), options);
33440
+ return polled.timedOut ? toFinished2(polled.run, "timed_out") : toFinished2(polled.run, toOutcome2(polled.run.status));
33441
+ }
33442
+ function toFinished2(run, outcome) {
33443
+ const failedStep = run ? findFailedStep(run) : null;
33444
+ return {
33445
+ outcome,
33446
+ status: run?.status ?? "pending",
33447
+ failedStep,
33448
+ 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),
33449
+ result: run?.result ?? null
33450
+ };
33451
+ }
33452
+ function findFailedStep(run) {
33453
+ const step = run.steps.find((candidate) => candidate.status === "failed");
33454
+ return step ? { stepId: step.stepId, description: step.description, error: step.error ?? null } : null;
33455
+ }
33456
+ function toOutcome2(status) {
33457
+ switch (status) {
33458
+ case "completed":
33459
+ case "skipped":
33460
+ case "cancelled":
33461
+ return status;
33462
+ default:
33463
+ return "failed";
33464
+ }
33465
+ }
33466
+
33467
+ // ../../packages/sdk/dist/esm/drafts/conversation-draft.js
33468
+ async function findConversationDraftId(api, { conversationId, workspaceId, signal }) {
33469
+ const args = { limit: MAX_PAGINATION_LIMIT };
33470
+ const page = workspaceId === null ? await api.automationDrafts.list(args, { signal }) : await api.workspaceAutomationDrafts.list(workspaceId, args, { signal });
33471
+ return page.data.find((draft) => draft.conversationId === conversationId)?.id ?? null;
33389
33472
  }
33390
33473
 
33391
33474
  // src/lib/auth-environment.ts
@@ -34594,172 +34677,6 @@ import { CliError as CliError3 } from "@doist/cli-core";
34594
34677
  // src/lib/command-error.ts
34595
34678
  import { formatJson as formatJson4, formatNdjson as formatNdjson4, getErrorMessage as getErrorMessage6 } from "@doist/cli-core";
34596
34679
 
34597
- // src/lib/billing-errors.ts
34598
- import { CliError as CliError2 } from "@doist/cli-core";
34599
- var PAYMENT_REQUIRED_CODE = "PAYMENT_REQUIRED";
34600
- var TOP_UP_PATH = "/settings/top-up";
34601
- var TODOIST_SUBSCRIPTION_PATH = "/app/settings/subscription";
34602
- function topUpUrl() {
34603
- return `${getEnvironment().appUrl}${TOP_UP_PATH}`;
34604
- }
34605
- function todoistSubscriptionUrl() {
34606
- return `${getEnvironment().todoistAppUrl}${TODOIST_SUBSCRIPTION_PATH}`;
34607
- }
34608
- var topUpHint = () => `Top up at ${topUpUrl()}.`;
34609
- var HINT_BY_DENIAL_CODE = {
34610
- usage_billing_insufficient_credit: topUpHint,
34611
- usage_billing_no_billing_account: topUpHint,
34612
- usage_billing_plan_required: () => `Upgrade at ${todoistSubscriptionUrl()}.`,
34613
- usage_billing_billing_paused: topUpHint
34614
- };
34615
- var PaymentRequiredError = class extends CliError2 {
34616
- constructor(message, hints) {
34617
- super(PAYMENT_REQUIRED_CODE, message, { hints });
34618
- this.name = "PaymentRequiredError";
34619
- }
34620
- };
34621
- function explainBillingFailure(error62) {
34622
- const refusal = getChatBillingRefusal(error62);
34623
- if (refusal === null) return error62;
34624
- const hints = refusal.code === void 0 ? [] : [HINT_BY_DENIAL_CODE[refusal.code]()];
34625
- return new PaymentRequiredError(refusal.message, hints);
34626
- }
34627
-
34628
- // src/lib/output.ts
34629
- import { formatJson as formatJson3, formatNdjson as formatNdjson3 } from "@doist/cli-core";
34630
- import { stripVTControlCharacters } from "node:util";
34631
- function getOutputMode(args) {
34632
- if (args.json && args.ndjson) {
34633
- throw new Error("Choose either --json or --ndjson, not both.");
34634
- }
34635
- if (args.json) return "json";
34636
- if (args.ndjson) return "ndjson";
34637
- return "human";
34638
- }
34639
- function sanitizeTerminalText(value) {
34640
- return stripVTControlCharacters(value).replace(
34641
- /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu,
34642
- ""
34643
- );
34644
- }
34645
- function writeResult(mode, result, presentation, stream = process.stdout) {
34646
- if (mode === "json") {
34647
- stream.write(`${formatJson3(presentation.json?.(result) ?? result)}
34648
- `);
34649
- return;
34650
- }
34651
- if (mode === "ndjson") {
34652
- const output3 = formatNdjson3(
34653
- presentation.ndjson?.(result) ?? [presentation.json?.(result) ?? result]
34654
- );
34655
- if (output3) stream.write(`${output3}
34656
- `);
34657
- return;
34658
- }
34659
- const output2 = sanitizeTerminalText(presentation.human(result));
34660
- stream.write(output2.endsWith("\n") ? output2 : `${output2}
34661
- `);
34662
- }
34663
- async function writeNdjsonItem(item, stream = process.stdout) {
34664
- if (stream.write(`${formatNdjson3([item])}
34665
- `) !== false) return;
34666
- await new Promise((resolve) => stream.once("drain", resolve));
34667
- }
34668
-
34669
- // src/lib/command-error.ts
34670
- function renderFailure(error62, context = {}) {
34671
- const authExplained = context.source ? explainAuthFailure(error62, context.source) : error62;
34672
- const explained = explainBillingFailure(authExplained);
34673
- const code = getErrorCode(explained);
34674
- return {
34675
- message: getErrorMessage6(explained),
34676
- humanMessage: formatError2(explained),
34677
- ...code ? { code } : {}
34678
- };
34679
- }
34680
- function writeFailure(mode, status, failure2) {
34681
- const { message, code } = failure2;
34682
- const result = { status, message, ...code ? { code } : {} };
34683
- if (mode === "json") {
34684
- process.stdout.write(`${formatJson4(result)}
34685
- `);
34686
- } else if (mode === "ndjson") {
34687
- process.stdout.write(`${formatNdjson4([{ type: status, ...result, terminal: true }])}
34688
- `);
34689
- } else {
34690
- const prefix = status === "error" ? "Error: " : "";
34691
- process.stdout.write(`${prefix}${sanitizeTerminalText(failure2.humanMessage)}
34692
- `);
34693
- }
34694
- }
34695
-
34696
- // src/lib/api-command.ts
34697
- async function runApiCommand(command, operation, presentation, execution = {}) {
34698
- let mode = "human";
34699
- let source;
34700
- const abortController = new AbortController();
34701
- const onInterrupt = () => abortController.abort();
34702
- process.once("SIGINT", onInterrupt);
34703
- try {
34704
- const globalOptions = getGlobalOptions(command);
34705
- mode = getOutputMode(globalOptions);
34706
- const apiConfiguration = await resolveApiConfiguration(globalOptions, {
34707
- minimumTokenLifetimeMs: execution.minimumTokenLifetimeMs
34708
- });
34709
- source = apiConfiguration.source;
34710
- const context = {
34711
- api: createAutomationsApi(apiConfiguration),
34712
- signal: abortController.signal
34713
- };
34714
- if (mode === "ndjson" && execution.streamNdjson) {
34715
- for await (const item of execution.streamNdjson(context)) await writeNdjsonItem(item);
34716
- return;
34717
- }
34718
- const result = await withSpinner(
34719
- { text: execution.spinner ?? "Loading..." },
34720
- () => operation(context)
34721
- );
34722
- stopEarlySpinner();
34723
- writeResult(mode, result, presentation);
34724
- const exitCode = execution.exitCode?.(result);
34725
- if (exitCode !== void 0) process.exitCode = exitCode;
34726
- } catch (error62) {
34727
- if (abortController.signal.aborted) {
34728
- writeFailure(
34729
- mode,
34730
- "interrupted",
34731
- error62 instanceof CliError3 ? renderFailure(error62, { mode, source }) : { message: "Interrupted.", humanMessage: "Interrupted." }
34732
- );
34733
- process.exitCode = 130;
34734
- return;
34735
- }
34736
- writeFailure(mode, "error", renderFailure(error62, { mode, source }));
34737
- process.exitCode = 1;
34738
- } finally {
34739
- process.removeListener("SIGINT", onInterrupt);
34740
- }
34741
- }
34742
-
34743
- // src/lib/workspace-scope.ts
34744
- import { Option } from "commander";
34745
- async function resolveWorkspaceScope(options) {
34746
- if (options.workspace !== void 0) {
34747
- return options.workspace;
34748
- }
34749
- if (options.personal) {
34750
- return null;
34751
- }
34752
- return getDefaultWorkspaceId();
34753
- }
34754
- function addWorkspaceScopeOptions(command) {
34755
- return command.addOption(
34756
- new Option(
34757
- "--workspace <workspace-id>",
34758
- "Act on this workspace instead of your personal space"
34759
- ).conflicts("personal")
34760
- ).option("--personal", "Act on your personal space, ignoring any pinned workspace");
34761
- }
34762
-
34763
34680
  // ../../packages/sdk/dist/esm/chat-turn/connection-requirements.js
34764
34681
  var CONNECTION_LOOKUP_CONCURRENCY = 4;
34765
34682
  function deduplicateConnectionRequirements(requirements) {
@@ -35168,6 +35085,67 @@ function assertSelectionsUnambiguous(requirements, selections) {
35168
35085
 
35169
35086
  // ../../packages/sdk/dist/esm/chat-turn/transport.js
35170
35087
  import { DefaultChatTransport, readUIMessageStream } from "ai";
35088
+
35089
+ // ../../packages/sdk/dist/esm/chat-turn/errors.js
35090
+ var ChatTurnError = class extends Error {
35091
+ conversationId;
35092
+ requestId;
35093
+ stage;
35094
+ /** The API's error body, when it refused the request with one. */
35095
+ response;
35096
+ constructor({ conversationId, requestId, stage, cause }) {
35097
+ const response = stage === "request" ? parseChatStreamRequestError(cause) : void 0;
35098
+ super(response?.message ?? (cause instanceof Error ? cause.message : String(cause)), {
35099
+ cause
35100
+ });
35101
+ this.name = "ChatTurnError";
35102
+ this.conversationId = conversationId;
35103
+ this.requestId = requestId;
35104
+ this.stage = stage;
35105
+ this.response = response;
35106
+ }
35107
+ get httpStatus() {
35108
+ return this.response?.statusCode;
35109
+ }
35110
+ };
35111
+ function parseChatStreamRequestError(error62) {
35112
+ if (!(error62 instanceof Error) || !error62.message.trim().startsWith("{"))
35113
+ return void 0;
35114
+ let body;
35115
+ try {
35116
+ body = JSON.parse(error62.message);
35117
+ } catch {
35118
+ return void 0;
35119
+ }
35120
+ const parsed = chatStreamRequestErrorResponseSchema.safeParse(body);
35121
+ return parsed.success ? parsed.data : void 0;
35122
+ }
35123
+ function getChatTurnBillingRefusal(error62) {
35124
+ return getChatBillingRefusal(error62 instanceof ChatTurnError ? error62.cause : error62);
35125
+ }
35126
+ function readFailedRequest(error62) {
35127
+ if (error62 instanceof ChatTurnError) {
35128
+ return {
35129
+ requestId: error62.requestId,
35130
+ ...error62.httpStatus !== void 0 ? { httpStatus: error62.httpStatus } : {}
35131
+ };
35132
+ }
35133
+ if (error62 instanceof AutomationsRequestError) {
35134
+ return {
35135
+ ...error62.requestId ? { requestId: error62.requestId } : {},
35136
+ httpStatus: error62.httpStatusCode
35137
+ };
35138
+ }
35139
+ return {};
35140
+ }
35141
+ function classifyChatTurnFailure(error62) {
35142
+ const { httpStatus: httpStatus2 } = readFailedRequest(error62);
35143
+ if (httpStatus2 !== void 0)
35144
+ return httpStatus2 < 500 ? "refused" : "broken";
35145
+ return error62 instanceof ChatTurnError || error62 instanceof AutomationsNetworkError ? "broken" : "other";
35146
+ }
35147
+
35148
+ // ../../packages/sdk/dist/esm/chat-turn/transport.js
35171
35149
  var ChatTurnTransport = class {
35172
35150
  transport;
35173
35151
  constructor({ token, baseUrl, headers, fetch: fetch2 }) {
@@ -35194,39 +35172,62 @@ var ChatTurnTransport = class {
35194
35172
  }
35195
35173
  /** Streams one server turn, reporting incremental text and deduplicated tool state changes. */
35196
35174
  async streamTurn({ conversationId, workspaceId = null, message, signal, onText, onToolActivity }) {
35197
- const stream = await this.transport.sendMessages({
35198
- trigger: "submit-message",
35199
- chatId: conversationId,
35200
- messageId: void 0,
35201
- messages: [message],
35202
- abortSignal: signal,
35203
- body: { conversationId, workspaceId }
35204
- });
35175
+ const requestId = globalThis.crypto.randomUUID();
35176
+ const fail = (stage, cause) => signal?.aborted ? cause : new ChatTurnError({ conversationId, requestId, stage, cause });
35177
+ let stream;
35178
+ try {
35179
+ stream = await this.transport.sendMessages({
35180
+ trigger: "submit-message",
35181
+ chatId: conversationId,
35182
+ messageId: void 0,
35183
+ messages: [message],
35184
+ abortSignal: signal,
35185
+ headers: { "X-Request-Id": requestId },
35186
+ body: { conversationId, workspaceId }
35187
+ });
35188
+ } catch (error62) {
35189
+ throw fail("request", error62);
35190
+ }
35205
35191
  let finalMessage;
35206
35192
  let emittedText = "";
35207
35193
  const emittedToolStates = /* @__PURE__ */ new Set();
35208
- for await (const currentMessage of readUIMessageStream({
35209
- stream,
35210
- terminateOnError: true
35211
- })) {
35212
- finalMessage = currentMessage;
35213
- const visibleText = getVisibleText(currentMessage);
35214
- if (visibleText.startsWith(emittedText)) {
35215
- const delta = visibleText.slice(emittedText.length);
35216
- if (delta)
35217
- onText?.(delta);
35218
- emittedText = visibleText;
35219
- }
35220
- for (const part of currentMessage.parts) {
35221
- const activity = toToolActivity(part);
35222
- if (!activity)
35223
- continue;
35224
- const key = `${activity.toolCallId}:${activity.state}`;
35225
- if (emittedToolStates.has(key))
35226
- continue;
35227
- emittedToolStates.add(key);
35228
- onToolActivity?.(activity);
35194
+ let callbackError;
35195
+ const report = (callback) => {
35196
+ try {
35197
+ callback();
35198
+ } catch (error62) {
35199
+ callbackError = { error: error62 };
35200
+ throw error62;
35201
+ }
35202
+ };
35203
+ try {
35204
+ for await (const currentMessage of readUIMessageStream({
35205
+ stream,
35206
+ terminateOnError: true
35207
+ })) {
35208
+ finalMessage = currentMessage;
35209
+ const visibleText = getVisibleText(currentMessage);
35210
+ if (visibleText.startsWith(emittedText)) {
35211
+ const delta = visibleText.slice(emittedText.length);
35212
+ if (delta)
35213
+ report(() => onText?.(delta));
35214
+ emittedText = visibleText;
35215
+ }
35216
+ for (const part of currentMessage.parts) {
35217
+ const activity = toToolActivity(part);
35218
+ if (!activity)
35219
+ continue;
35220
+ const key = `${activity.toolCallId}:${activity.state}`;
35221
+ if (emittedToolStates.has(key))
35222
+ continue;
35223
+ emittedToolStates.add(key);
35224
+ report(() => onToolActivity?.(activity));
35225
+ }
35229
35226
  }
35227
+ } catch (error62) {
35228
+ if (callbackError)
35229
+ throw callbackError.error;
35230
+ throw fail("stream", error62);
35230
35231
  }
35231
35232
  if (!finalMessage) {
35232
35233
  throw new Error("The Automations chat returned an empty response.");
@@ -35245,6 +35246,236 @@ function toToolActivity(part) {
35245
35246
  };
35246
35247
  }
35247
35248
 
35249
+ // src/lib/billing-errors.ts
35250
+ import { CliError as CliError2 } from "@doist/cli-core";
35251
+ var PAYMENT_REQUIRED_CODE = "PAYMENT_REQUIRED";
35252
+ var TOP_UP_PATH = "/settings/top-up";
35253
+ var TODOIST_SUBSCRIPTION_PATH = "/app/settings/subscription";
35254
+ function topUpUrl() {
35255
+ return `${getEnvironment().appUrl}${TOP_UP_PATH}`;
35256
+ }
35257
+ function todoistSubscriptionUrl() {
35258
+ return `${getEnvironment().todoistAppUrl}${TODOIST_SUBSCRIPTION_PATH}`;
35259
+ }
35260
+ var topUpHint = () => `Top up at ${topUpUrl()}.`;
35261
+ var HINT_BY_DENIAL_CODE = {
35262
+ usage_billing_insufficient_credit: topUpHint,
35263
+ usage_billing_no_billing_account: topUpHint,
35264
+ usage_billing_plan_required: () => `Upgrade at ${todoistSubscriptionUrl()}.`,
35265
+ usage_billing_billing_paused: topUpHint
35266
+ };
35267
+ var PaymentRequiredError = class extends CliError2 {
35268
+ constructor(message, hints) {
35269
+ super(PAYMENT_REQUIRED_CODE, message, { hints });
35270
+ this.name = "PaymentRequiredError";
35271
+ }
35272
+ };
35273
+ function explainBillingFailure(error62) {
35274
+ const refusal = getChatTurnBillingRefusal(error62);
35275
+ if (refusal === null) return error62;
35276
+ const hints = refusal.code === void 0 ? [] : [HINT_BY_DENIAL_CODE[refusal.code]()];
35277
+ return new PaymentRequiredError(refusal.message, hints);
35278
+ }
35279
+
35280
+ // src/lib/output.ts
35281
+ import { formatJson as formatJson3, formatNdjson as formatNdjson3 } from "@doist/cli-core";
35282
+ import { stripVTControlCharacters } from "node:util";
35283
+ function getOutputMode(args) {
35284
+ if (args.json && args.ndjson) {
35285
+ throw new Error("Choose either --json or --ndjson, not both.");
35286
+ }
35287
+ if (args.json) return "json";
35288
+ if (args.ndjson) return "ndjson";
35289
+ return "human";
35290
+ }
35291
+ function sanitizeTerminalText(value) {
35292
+ return stripVTControlCharacters(value).replace(
35293
+ /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu,
35294
+ ""
35295
+ );
35296
+ }
35297
+ function writeResult(mode, result, presentation, stream = process.stdout) {
35298
+ if (mode === "json") {
35299
+ stream.write(`${formatJson3(presentation.json?.(result) ?? result)}
35300
+ `);
35301
+ return;
35302
+ }
35303
+ if (mode === "ndjson") {
35304
+ const output3 = formatNdjson3(
35305
+ presentation.ndjson?.(result) ?? [presentation.json?.(result) ?? result]
35306
+ );
35307
+ if (output3) stream.write(`${output3}
35308
+ `);
35309
+ return;
35310
+ }
35311
+ const output2 = sanitizeTerminalText(presentation.human(result));
35312
+ stream.write(output2.endsWith("\n") ? output2 : `${output2}
35313
+ `);
35314
+ }
35315
+ async function writeNdjsonItem(item, stream = process.stdout) {
35316
+ if (stream.write(`${formatNdjson3([item])}
35317
+ `) !== false) return;
35318
+ await new Promise((resolve) => stream.once("drain", resolve));
35319
+ }
35320
+
35321
+ // src/lib/command-error.ts
35322
+ function renderFailure(error62, context = {}) {
35323
+ const authExplained = context.source ? explainAuthFailure(error62, context.source) : error62;
35324
+ const explained = explainBillingFailure(authExplained);
35325
+ const code = getErrorCode(explained);
35326
+ return {
35327
+ message: getErrorMessage6(explained),
35328
+ humanMessage: formatError2(explained),
35329
+ ...code ? { code } : {}
35330
+ };
35331
+ }
35332
+ var FALLBACK_FAILURE_CODE = "TDA_COMMAND_FAILED";
35333
+ function toFailureRecord(failure2, details) {
35334
+ return {
35335
+ ...details,
35336
+ status: "error",
35337
+ code: failure2.code ?? FALLBACK_FAILURE_CODE,
35338
+ message: failure2.message
35339
+ };
35340
+ }
35341
+ function writeFailure(mode, status, failure2) {
35342
+ const result = status === "error" ? toFailureRecord(failure2) : { status, message: failure2.message };
35343
+ if (mode === "json") {
35344
+ process.stdout.write(`${formatJson4(result)}
35345
+ `);
35346
+ } else if (mode === "ndjson") {
35347
+ process.stdout.write(`${formatNdjson4([{ type: status, ...result, terminal: true }])}
35348
+ `);
35349
+ } else {
35350
+ const prefix = status === "error" ? "Error: " : "";
35351
+ process.stdout.write(`${prefix}${sanitizeTerminalText(failure2.humanMessage)}
35352
+ `);
35353
+ }
35354
+ }
35355
+
35356
+ // src/lib/api-command.ts
35357
+ async function runApiCommand(command, operation, presentation, execution = {}) {
35358
+ let mode = "human";
35359
+ let source;
35360
+ const abortController = new AbortController();
35361
+ const onInterrupt = () => abortController.abort();
35362
+ process.once("SIGINT", onInterrupt);
35363
+ try {
35364
+ const globalOptions = getGlobalOptions(command);
35365
+ mode = getOutputMode(globalOptions);
35366
+ const apiConfiguration = await resolveApiConfiguration(globalOptions, {
35367
+ minimumTokenLifetimeMs: execution.minimumTokenLifetimeMs
35368
+ });
35369
+ source = apiConfiguration.source;
35370
+ const outputMode = mode;
35371
+ const context = {
35372
+ api: createAutomationsApi(apiConfiguration),
35373
+ signal: abortController.signal,
35374
+ progress: (record2) => {
35375
+ if (outputMode === "ndjson") void writeNdjsonItem({ ...record2, terminal: false });
35376
+ }
35377
+ };
35378
+ if (mode === "ndjson" && execution.streamNdjson) {
35379
+ for await (const item of execution.streamNdjson(context)) await writeNdjsonItem(item);
35380
+ return;
35381
+ }
35382
+ const result = await withSpinner(
35383
+ { text: execution.spinner ?? "Loading..." },
35384
+ () => operation(context)
35385
+ );
35386
+ stopEarlySpinner();
35387
+ writeResult(mode, result, presentation);
35388
+ const exitCode = execution.exitCode?.(result);
35389
+ if (exitCode !== void 0) process.exitCode = exitCode;
35390
+ } catch (error62) {
35391
+ if (abortController.signal.aborted) {
35392
+ writeFailure(
35393
+ mode,
35394
+ "interrupted",
35395
+ error62 instanceof CliError3 ? renderFailure(error62, { mode, source }) : { message: "Interrupted.", humanMessage: "Interrupted." }
35396
+ );
35397
+ process.exitCode = 130;
35398
+ return;
35399
+ }
35400
+ writeFailure(mode, "error", renderFailure(error62, { mode, source }));
35401
+ process.exitCode = 1;
35402
+ } finally {
35403
+ process.removeListener("SIGINT", onInterrupt);
35404
+ }
35405
+ }
35406
+
35407
+ // src/lib/wait-options.ts
35408
+ var DEFAULT_WAIT = "10m";
35409
+ var TOKEN_LIFETIME_MARGIN_MS = 2 * 60 * 1e3;
35410
+ function addWaitOptions(command, waitDescription) {
35411
+ return command.option("--wait", waitDescription).option(
35412
+ "--timeout <duration>",
35413
+ `How long --wait waits, such as 90s or 15m (default ${DEFAULT_WAIT})`
35414
+ );
35415
+ }
35416
+ function resolveWait(options) {
35417
+ if (options.timeout !== void 0 && options.wait !== true) {
35418
+ throw new Error("--timeout only applies with --wait.");
35419
+ }
35420
+ if (options.wait !== true) {
35421
+ return null;
35422
+ }
35423
+ return { timeoutMs: parseTimeout(options.timeout ?? DEFAULT_WAIT) };
35424
+ }
35425
+ function waitTokenLifetimeMs(options) {
35426
+ if (options.wait !== true) {
35427
+ return void 0;
35428
+ }
35429
+ try {
35430
+ return parseTimeout(options.timeout ?? DEFAULT_WAIT) + TOKEN_LIFETIME_MARGIN_MS;
35431
+ } catch {
35432
+ return TOKEN_LIFETIME_MARGIN_MS;
35433
+ }
35434
+ }
35435
+ function parseTimeout(value) {
35436
+ try {
35437
+ return durationStringToMilliseconds(value);
35438
+ } catch {
35439
+ throw new Error(`Invalid --timeout ${value}; use a duration such as 90s, 15m or 1h.`);
35440
+ }
35441
+ }
35442
+ function reportRunProgress(report) {
35443
+ let last;
35444
+ return (run) => {
35445
+ const step = run.steps.find((candidate) => candidate.status === "running")?.stepId;
35446
+ const key = `${run.status}:${step ?? ""}`;
35447
+ if (key === last) return;
35448
+ last = key;
35449
+ report({ status: run.status, ...step ? { step } : {} });
35450
+ };
35451
+ }
35452
+ function toTerminalRecords(result) {
35453
+ return [{ type: result.action, ...result, terminal: true }];
35454
+ }
35455
+
35456
+ // src/lib/workspace-scope.ts
35457
+ import { Option } from "commander";
35458
+ async function resolveWorkspaceScope(options) {
35459
+ if (options.workspace !== void 0) {
35460
+ return options.workspace;
35461
+ }
35462
+ if (options.personal) {
35463
+ return null;
35464
+ }
35465
+ return getDefaultWorkspaceId();
35466
+ }
35467
+ function scopeFlag(scope) {
35468
+ return scope === null ? "--personal" : `--workspace ${scope}`;
35469
+ }
35470
+ function addWorkspaceScopeOptions(command) {
35471
+ return command.addOption(
35472
+ new Option(
35473
+ "--workspace <workspace-id>",
35474
+ "Act on this workspace instead of your personal space"
35475
+ ).conflicts("personal")
35476
+ ).option("--personal", "Act on your personal space, ignoring any pinned workspace");
35477
+ }
35478
+
35248
35479
  // src/commands/read/format.ts
35249
35480
  function formatAutomationList(page) {
35250
35481
  if (page.data.length === 0) return "No automations found.";
@@ -35456,8 +35687,35 @@ function describeVisibility(automation) {
35456
35687
  function summarizeAutomationChange(result) {
35457
35688
  return { ...result, automation: summarizeAutomation(result.automation) };
35458
35689
  }
35690
+ var RUN_OUTCOME_LABELS = {
35691
+ completed: "completed",
35692
+ failed: "failed",
35693
+ skipped: "was skipped",
35694
+ cancelled: "was cancelled",
35695
+ timed_out: "did not finish before the timeout"
35696
+ };
35459
35697
  function formatAutomationTrigger(result) {
35460
- return `Triggered automation ${result.automationId}.`;
35698
+ if (result.action === "triggered") {
35699
+ if (result.runId === null) {
35700
+ return `Triggered automation ${result.automationId}.`;
35701
+ }
35702
+ return [
35703
+ `Started run ${result.runId} of automation ${result.automationId}.`,
35704
+ `Details: ${result.command}`
35705
+ ].join("\n");
35706
+ }
35707
+ const lines = [
35708
+ `Run ${result.runId} of automation ${result.automationId} ${RUN_OUTCOME_LABELS[result.outcome]}.`
35709
+ ];
35710
+ if (result.outcome === "timed_out") lines.push(`Status: ${result.status}`);
35711
+ if (result.failedStep) {
35712
+ lines.push(`Failed step: ${result.failedStep.stepId} (${result.failedStep.description})`);
35713
+ }
35714
+ if (result.error)
35715
+ lines.push(`${result.outcome === "skipped" ? "Reason" : "Error"}: ${result.error}`);
35716
+ if (result.result) lines.push(`Result: ${result.result.text}`);
35717
+ lines.push(`Details: ${result.command}`);
35718
+ return lines.join("\n");
35461
35719
  }
35462
35720
  function formatAutomationDelete(result) {
35463
35721
  if (result.action === "delete_preview") {
@@ -35472,6 +35730,56 @@ function capitalize(value) {
35472
35730
  return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
35473
35731
  }
35474
35732
 
35733
+ // src/commands/automation/trigger-service.ts
35734
+ import { CliError as CliError4 } from "@doist/cli-core";
35735
+ async function triggerAutomation(runApi, {
35736
+ automationId,
35737
+ wait: wait2,
35738
+ signal,
35739
+ onProgress
35740
+ }) {
35741
+ const triggerType = readPrimaryTriggerType(await runApi.getAutomation(automationId));
35742
+ if (triggerType !== "schedule") {
35743
+ throw new Error(
35744
+ "This automation can only be triggered via its configured mechanism, not manually."
35745
+ );
35746
+ }
35747
+ const started = await runApi.trigger(automationId);
35748
+ const flag = scopeFlag(runApi.workspaceId);
35749
+ if (!started) {
35750
+ if (wait2) {
35751
+ throw new CliError4(
35752
+ "TRIGGER_RUN_UNREPORTED",
35753
+ "The run started, but the API did not say which one it was, so it cannot be waited for.",
35754
+ { hints: [`See the latest run: tda run list ${automationId} --limit 1 ${flag}`] }
35755
+ );
35756
+ }
35757
+ return { action: "triggered", automationId, runId: null, status: null, command: null };
35758
+ }
35759
+ const identity = {
35760
+ automationId,
35761
+ runId: started.runId,
35762
+ command: `tda run view ${automationId} ${started.runId} ${flag}`
35763
+ };
35764
+ if (!wait2) {
35765
+ return { action: "triggered", ...identity, status: started.status };
35766
+ }
35767
+ const finished = await waitForRun(runApi, identity, {
35768
+ ...wait2,
35769
+ signal,
35770
+ ...onProgress ? { onPoll: reportRunProgress(onProgress) } : {}
35771
+ });
35772
+ return {
35773
+ action: "run_finished",
35774
+ ...identity,
35775
+ outcome: finished.outcome,
35776
+ status: finished.status,
35777
+ failedStep: finished.failedStep,
35778
+ error: finished.error,
35779
+ result: finished.result
35780
+ };
35781
+ }
35782
+
35475
35783
  // src/commands/automation/index.ts
35476
35784
  function registerAutomationCommand(program) {
35477
35785
  const automation = program.command("automation").description("Inspect and manage published automations");
@@ -35521,30 +35829,29 @@ function registerAutomationCommand(program) {
35521
35829
  registerEnabledStateCommand(automation, "enable", true);
35522
35830
  registerEnabledStateCommand(automation, "disable", false);
35523
35831
  addWorkspaceScopeOptions(
35524
- automation.command("trigger").description("Start a live automation run without waiting for it to finish").argument("<automation-id>", "Automation ID")
35832
+ addWaitOptions(
35833
+ automation.command("trigger").description("Start a live automation run and print its run ID").argument("<automation-id>", "Automation ID"),
35834
+ "Wait for the run to end and exit 1 unless it completed"
35835
+ )
35525
35836
  ).action(async (automationId, options, command) => {
35526
35837
  await runApiCommand(
35527
35838
  command,
35528
- async ({ api, signal }) => {
35529
- const workspaceId = await resolveWorkspaceScope(options);
35530
- const automation2 = workspaceId === null ? await api.automations.get(automationId, { signal }) : await api.workspaceAutomations.get(workspaceId, automationId, {
35531
- signal
35839
+ async ({ api, signal, progress }) => {
35840
+ const wait2 = resolveWait(options);
35841
+ const runApi = bindRunApi(api, await resolveWorkspaceScope(options), { signal });
35842
+ return triggerAutomation(runApi, {
35843
+ automationId,
35844
+ wait: wait2,
35845
+ signal,
35846
+ onProgress: (state) => progress({ type: "run_progress", automationId, ...state })
35532
35847
  });
35533
- const triggerType = hasTriggerType(automation2.manifest) ? getManifestTriggers(automation2.manifest)[0]?.type : void 0;
35534
- if (triggerType !== "schedule") {
35535
- throw new Error(
35536
- "This automation can only be triggered via its configured mechanism, not manually."
35537
- );
35538
- }
35539
- if (workspaceId === null) {
35540
- await api.automations.trigger(automationId, { signal });
35541
- } else {
35542
- await api.workspaceAutomations.trigger(workspaceId, automationId, { signal });
35543
- }
35544
- return { action: "triggered", automationId };
35545
35848
  },
35546
- { human: formatAutomationTrigger },
35547
- { spinner: "Triggering automation..." }
35849
+ { human: formatAutomationTrigger, ndjson: toTerminalRecords },
35850
+ {
35851
+ spinner: options.wait === true ? "Running automation..." : "Triggering automation...",
35852
+ minimumTokenLifetimeMs: waitTokenLifetimeMs(options),
35853
+ exitCode: (result) => result.action === "run_finished" && result.outcome !== "completed" ? 1 : void 0
35854
+ }
35548
35855
  );
35549
35856
  });
35550
35857
  addWorkspaceScopeOptions(
@@ -35801,6 +36108,7 @@ function collectOption(value, previous) {
35801
36108
  }
35802
36109
 
35803
36110
  // src/commands/chat/chat-service.ts
36111
+ import { CliError as CliError6 } from "@doist/cli-core";
35804
36112
  import { createInterface } from "node:readline/promises";
35805
36113
 
35806
36114
  // src/commands/chat/connection-authorizer.ts
@@ -35815,6 +36123,13 @@ var FINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
35815
36123
  "session_status",
35816
36124
  "turn_completed"
35817
36125
  ]);
36126
+ var UNRECORDED_EVENT_TYPES = /* @__PURE__ */ new Set([
36127
+ "assistant_text",
36128
+ "connection_authorization_waiting",
36129
+ "turn_progress"
36130
+ ]);
36131
+ var INTERRUPTING_EVENT_TYPES = /* @__PURE__ */ new Set(["error", "interrupted"]);
36132
+ var HEARTBEAT_EVENT_TYPES = /* @__PURE__ */ new Set(["connection_authorization_waiting", "turn_progress"]);
35818
36133
  var SPINNER_TEXT = {
35819
36134
  connection_authorization_opened: "Waiting for authorization...",
35820
36135
  connection_authorization_started: "Waiting for authorization...",
@@ -35825,16 +36140,47 @@ var OutputWriter = class {
35825
36140
  stream;
35826
36141
  diagnosticStream;
35827
36142
  events = [];
36143
+ streamText;
35828
36144
  assistantLineOpen = false;
35829
36145
  currentConversationId;
35830
- constructor(mode, stream = process.stdout, diagnosticStream = process.stderr) {
36146
+ /** The current turn's text, one entry per step. */
36147
+ turnSteps = [];
36148
+ stepOpen = false;
36149
+ bufferedText = "";
36150
+ bufferedTextConversationId;
36151
+ constructor(mode, stream = process.stdout, diagnosticStream = process.stderr, { streamText = false } = {}) {
35831
36152
  this.mode = mode;
35832
36153
  this.stream = stream;
35833
36154
  this.diagnosticStream = diagnosticStream;
36155
+ this.streamText = streamText;
36156
+ }
36157
+ /**
36158
+ * Assistant text as it arrives. A human reader, or an NDJSON caller that
36159
+ * asked with `--stream-text`, gets each fragment as `assistant_text`;
36160
+ * otherwise the text of a step comes out whole as one `assistant_message`
36161
+ * before whatever follows it.
36162
+ */
36163
+ appendAssistantText(conversationId, text2) {
36164
+ if (!this.stepOpen) this.turnSteps.push("");
36165
+ this.stepOpen = true;
36166
+ this.turnSteps[this.turnSteps.length - 1] += text2;
36167
+ if (this.mode === "human" || this.mode === "ndjson" && this.streamText) {
36168
+ this.emit({ type: "assistant_text", conversationId, text: text2 });
36169
+ return;
36170
+ }
36171
+ this.bufferedText += text2;
36172
+ this.bufferedTextConversationId = conversationId;
35834
36173
  }
35835
36174
  /** Records and renders an event; JSON mode retains structured output until `finish`. */
35836
36175
  emit(event) {
35837
- if (this.mode === "json") this.events.push(event);
36176
+ const endsStep = event.type !== "assistant_message" && event.type !== "assistant_text" && !HEARTBEAT_EVENT_TYPES.has(event.type);
36177
+ if (endsStep) {
36178
+ this.flushAssistantMessage(INTERRUPTING_EVENT_TYPES.has(event.type));
36179
+ this.stepOpen = false;
36180
+ }
36181
+ if (this.mode === "json" && !UNRECORDED_EVENT_TYPES.has(event.type)) {
36182
+ this.events.push(event);
36183
+ }
35838
36184
  if (event.conversationId) this.currentConversationId = event.conversationId;
35839
36185
  if (this.mode === "ndjson") {
35840
36186
  this.stream.write(`${formatNdjson5([event])}
@@ -35850,17 +36196,28 @@ var OutputWriter = class {
35850
36196
  get conversationId() {
35851
36197
  return this.currentConversationId;
35852
36198
  }
36199
+ /** The current turn's text so far, its steps separated by blank lines. */
36200
+ get assistantText() {
36201
+ return this.turnSteps.join("\n\n");
36202
+ }
36203
+ /** Starts collecting the text of a new turn. */
36204
+ startTurn() {
36205
+ this.turnSteps = [];
36206
+ this.stepOpen = false;
36207
+ }
35853
36208
  /** Emits structured conversation status with human formatting owned by this output layer. */
35854
36209
  emitConversationStatus(snapshot, pendingActionKind) {
35855
36210
  this.emit({
35856
36211
  type: "session_status",
35857
36212
  conversationId: snapshot.conversation.id,
35858
36213
  ...snapshot,
35859
- humanText: formatConversationStatus(snapshot, pendingActionKind)
36214
+ humanText: formatConversationStatus(snapshot, pendingActionKind),
36215
+ terminal: true
35860
36216
  });
35861
36217
  }
35862
36218
  /** Finalizes output, including the accumulated event list in JSON mode. */
35863
36219
  finish(summary) {
36220
+ this.flushAssistantMessage(false);
35864
36221
  stopEarlySpinner();
35865
36222
  if (this.mode === "json") {
35866
36223
  this.stream.write(`${formatJson5({ ...summary, events: this.events })}
@@ -35872,6 +36229,17 @@ var OutputWriter = class {
35872
36229
  this.assistantLineOpen = false;
35873
36230
  }
35874
36231
  }
36232
+ flushAssistantMessage(partial2) {
36233
+ if (!this.bufferedText) return;
36234
+ const text2 = this.bufferedText;
36235
+ this.bufferedText = "";
36236
+ this.emit({
36237
+ type: "assistant_message",
36238
+ conversationId: this.bufferedTextConversationId,
36239
+ text: text2,
36240
+ ...partial2 ? { partial: true } : {}
36241
+ });
36242
+ }
35875
36243
  /**
35876
36244
  * The agent goes quiet for long stretches between events, so a spinner
35877
36245
  * fills the gap. Taking it down before every write keeps its next frame
@@ -36095,6 +36463,7 @@ function getMessageRoleLabel(role) {
36095
36463
 
36096
36464
  // src/commands/chat/connection-authorizer.ts
36097
36465
  var CONNECTION_POLL_INTERVAL_MS = 2e3;
36466
+ var CONNECTION_WAITING_REPORT_INTERVAL_MS = 15e3;
36098
36467
  var CONNECTION_AUTHORIZATION_TIMEOUT_MS = 10 * 60 * 1e3;
36099
36468
  var ApiConnectionAuthorizer = class {
36100
36469
  connections;
@@ -36186,6 +36555,7 @@ var ApiConnectionAuthorizer = class {
36186
36555
  }
36187
36556
  async waitUntilAuthorized(request2, connectionId, signal) {
36188
36557
  const startedAt = Date.now();
36558
+ let reportedAt;
36189
36559
  while (true) {
36190
36560
  if (signal?.aborted) throw new Error("Connection authorization interrupted.");
36191
36561
  const connection = await this.connections.get(connectionId, { signal });
@@ -36213,16 +36583,20 @@ var ApiConnectionAuthorizer = class {
36213
36583
  `Timed out waiting for ${request2.serviceName} connection authorization.`
36214
36584
  );
36215
36585
  }
36216
- this.output.emit({
36217
- type: "connection_authorization_waiting",
36218
- conversationId: request2.conversationId,
36219
- connectionId,
36220
- connectorId: request2.connectorId,
36221
- serviceName: request2.serviceName,
36222
- elapsedSeconds: Math.floor(elapsedMs / 1e3),
36223
- state: "waiting_for_user",
36224
- terminal: false
36225
- });
36586
+ const now = Date.now();
36587
+ if (reportedAt === void 0 || now - reportedAt >= CONNECTION_WAITING_REPORT_INTERVAL_MS) {
36588
+ reportedAt = now;
36589
+ this.output.emit({
36590
+ type: "connection_authorization_waiting",
36591
+ conversationId: request2.conversationId,
36592
+ connectionId,
36593
+ connectorId: request2.connectorId,
36594
+ serviceName: request2.serviceName,
36595
+ elapsedSeconds: Math.floor(elapsedMs / 1e3),
36596
+ state: "waiting_for_user",
36597
+ terminal: false
36598
+ });
36599
+ }
36226
36600
  await wait(Math.min(this.pollIntervalMs, this.timeoutMs - elapsedMs), void 0, {
36227
36601
  signal
36228
36602
  });
@@ -36237,11 +36611,48 @@ function isSafeSetupUrl(value) {
36237
36611
  }
36238
36612
  }
36239
36613
 
36614
+ // src/commands/chat/chat-failure.ts
36615
+ var SELECTION_CODES = /* @__PURE__ */ new Set([
36616
+ "CHAT_AWAITING_SELECTION",
36617
+ "CHAT_AMBIGUOUS_SELECTION",
36618
+ "CHAT_UNKNOWN_CONNECTION"
36619
+ ]);
36620
+ function chatFailureCode(error62) {
36621
+ switch (classifyChatTurnFailure(error62)) {
36622
+ case "broken":
36623
+ return "CHAT_TURN_FAILED";
36624
+ case "refused":
36625
+ return "CHAT_TURN_REJECTED";
36626
+ default:
36627
+ return FALLBACK_FAILURE_CODE;
36628
+ }
36629
+ }
36630
+ function messageResumeCommand(conversationId) {
36631
+ return `tda chat continue ${conversationId} "<message>"`;
36632
+ }
36633
+ function resumeCommandFor(conversationId, code) {
36634
+ if (code === "CHAT_TURN_ACTIVE") return void 0;
36635
+ if (code === "CHAT_AWAITING_PUSH") return `tda chat continue ${conversationId}`;
36636
+ if (SELECTION_CODES.has(code)) {
36637
+ return `tda chat continue ${conversationId} --connection <connector[:type]=connection-id>`;
36638
+ }
36639
+ return messageResumeCommand(conversationId);
36640
+ }
36641
+ function formatChatFailureDetails(details) {
36642
+ return [
36643
+ `Conversation: ${details.conversationId}`,
36644
+ ...details.requestId ? [`Request ID: ${details.requestId}`] : [],
36645
+ ...details.draftCommand ? [`Its draft is saved: ${details.draftCommand}`] : [],
36646
+ ...details.resumeCommand ? [`Resume with: ${details.resumeCommand}`] : []
36647
+ ];
36648
+ }
36649
+
36240
36650
  // src/commands/chat/conversation-turn-lock.ts
36241
36651
  import { link, mkdir, readFile, rm, writeFile } from "node:fs/promises";
36242
36652
  import { tmpdir } from "node:os";
36243
36653
  import { join } from "node:path";
36244
36654
  import { randomUUID } from "node:crypto";
36655
+ import { CliError as CliError5 } from "@doist/cli-core";
36245
36656
  var ConversationTurnLock = class {
36246
36657
  directory;
36247
36658
  pid;
@@ -36278,7 +36689,8 @@ var ConversationTurnLock = class {
36278
36689
  }
36279
36690
  const current = await readLock(path);
36280
36691
  if (current && this.isProcessRunning(current.pid)) {
36281
- throw new Error(
36692
+ throw new CliError5(
36693
+ "CHAT_TURN_ACTIVE",
36282
36694
  `Conversation ${conversationId} already has an active turn in process ${current.pid}. Continue monitoring that process; do not invoke continue until it exits.`
36283
36695
  );
36284
36696
  }
@@ -36325,7 +36737,8 @@ function isNoSuchProcessError(error62) {
36325
36737
 
36326
36738
  // src/commands/chat/chat-service.ts
36327
36739
  var TURN_HEARTBEAT_INTERVAL_MS = 15e3;
36328
- var TURN_RUNNING_MESSAGE = "Keep monitoring this process; do not invoke continue until turn_completed with terminal=true is emitted and the process exits.";
36740
+ var DRAFT_LOOKUP_TIMEOUT_MS = 5e3;
36741
+ var TURN_RUNNING_MESSAGE = "Keep monitoring this process; do not invoke continue until a record with terminal=true is emitted and the process exits.";
36329
36742
  var ChatService = class {
36330
36743
  api;
36331
36744
  transport;
@@ -36335,6 +36748,8 @@ var ChatService = class {
36335
36748
  turnLock;
36336
36749
  heartbeatIntervalMs;
36337
36750
  readline;
36751
+ /** The conversation a start or continue is working on, once it is known. */
36752
+ failureScope;
36338
36753
  constructor({
36339
36754
  token,
36340
36755
  baseUrl,
@@ -36365,6 +36780,10 @@ var ChatService = class {
36365
36780
  */
36366
36781
  async startConversation(message, signal, workspaceId = null) {
36367
36782
  const conversation = workspaceId === null ? await this.api.chat.createConversation({}, { signal }) : await this.api.workspaceChat.createConversation(workspaceId, {}, { signal });
36783
+ this.failureScope = {
36784
+ conversationId: conversation.id,
36785
+ workspaceId: conversation.workspaceId
36786
+ };
36368
36787
  this.output.emit({ type: "session_started", conversationId: conversation.id });
36369
36788
  return this.withActiveTurn(
36370
36789
  conversation.id,
@@ -36383,8 +36802,10 @@ var ChatService = class {
36383
36802
  selections,
36384
36803
  signal
36385
36804
  }) {
36805
+ this.failureScope = { conversationId };
36386
36806
  const conversation = await this.api.chat.getConversation(conversationId, { signal });
36387
36807
  const scope = { conversationId, workspaceId: conversation.workspaceId };
36808
+ this.failureScope = scope;
36388
36809
  return this.withActiveTurn(
36389
36810
  conversationId,
36390
36811
  async () => {
@@ -36397,7 +36818,8 @@ var ChatService = class {
36397
36818
  } else {
36398
36819
  const nextMessage = this.interactive ? await this.promptForMessage(signal) : "";
36399
36820
  if (!nextMessage) {
36400
- throw new Error(
36821
+ throw new CliError6(
36822
+ "CHAT_INPUT_REQUIRED",
36401
36823
  "A message or --connection selection is required to continue."
36402
36824
  );
36403
36825
  }
@@ -36503,6 +36925,7 @@ var ChatService = class {
36503
36925
  }
36504
36926
  }
36505
36927
  async runTurn({ conversationId, workspaceId }, input2, signal, pendingAction, { markActivity }) {
36928
+ this.output.startTurn();
36506
36929
  try {
36507
36930
  return await runConversationTurn({
36508
36931
  api: this.api,
@@ -36514,7 +36937,7 @@ var ChatService = class {
36514
36937
  signal,
36515
36938
  onText: (text2) => {
36516
36939
  markActivity();
36517
- this.output.emit({ type: "assistant_text", conversationId, text: text2 });
36940
+ this.output.appendAssistantText(conversationId, text2);
36518
36941
  },
36519
36942
  onToolActivity: (activity) => {
36520
36943
  markActivity();
@@ -36536,7 +36959,7 @@ var ChatService = class {
36536
36959
  });
36537
36960
  } catch (error62) {
36538
36961
  if (error62 instanceof ChatTurnInputError) {
36539
- throw new Error(describeInputError(error62));
36962
+ throw new CliError6(`CHAT_${error62.code.toUpperCase()}`, describeInputError(error62));
36540
36963
  }
36541
36964
  throw error62;
36542
36965
  }
@@ -36560,13 +36983,17 @@ var ChatService = class {
36560
36983
  terminal: false
36561
36984
  });
36562
36985
  }
36563
- completeTurn(summary) {
36986
+ completeTurn(turn) {
36987
+ const summary = { ...turn, assistantText: this.output.assistantText || turn.assistantText };
36564
36988
  const resumeCommand = getResumeCommand(summary);
36565
36989
  this.output.emit({
36566
36990
  type: "turn_completed",
36567
36991
  conversationId: summary.conversationId,
36568
36992
  status: summary.status,
36569
36993
  outcome: summary.status,
36994
+ assistantText: summary.assistantText,
36995
+ ...summary.automationId ? { automationId: summary.automationId } : {},
36996
+ ...summary.action ? { action: summary.action } : {},
36570
36997
  state: "completed",
36571
36998
  terminal: true,
36572
36999
  ...summary.conversationArchived !== void 0 ? { conversationArchived: summary.conversationArchived } : {},
@@ -36607,6 +37034,32 @@ var ChatService = class {
36607
37034
  await release();
36608
37035
  }
36609
37036
  }
37037
+ /** Whether a start or continue has a conversation its failure can name. */
37038
+ get hasFailureScope() {
37039
+ return this.failureScope !== void 0;
37040
+ }
37041
+ /**
37042
+ * What a caller needs to pick up a conversation whose start or continue
37043
+ * failed, or undefined before one was known. The draft is looked up only for
37044
+ * a turn that broke, not one refused for its input, credentials or balance.
37045
+ */
37046
+ async describeFailure(error62, code) {
37047
+ const scope = this.failureScope;
37048
+ if (!scope) return void 0;
37049
+ const { conversationId, workspaceId } = scope;
37050
+ const draftId = workspaceId !== void 0 && classifyChatTurnFailure(error62) === "broken" ? await findConversationDraftId(this.api, {
37051
+ conversationId,
37052
+ workspaceId,
37053
+ signal: AbortSignal.timeout(DRAFT_LOOKUP_TIMEOUT_MS)
37054
+ }).catch(() => null) : null;
37055
+ const resumeCommand = resumeCommandFor(conversationId, code);
37056
+ return {
37057
+ conversationId,
37058
+ ...readFailedRequest(error62),
37059
+ ...draftId && workspaceId !== void 0 ? { draftId, draftCommand: `tda draft view ${draftId} ${scopeFlag(workspaceId)}` } : {},
37060
+ ...resumeCommand ? { resumeCommand } : {}
37061
+ };
37062
+ }
36610
37063
  async getPersistedPendingAction(conversationId, signal) {
36611
37064
  const messages = await this.api.chat.getMessages(conversationId, { signal });
36612
37065
  return getPendingActionFromMessages(messages);
@@ -36693,21 +37146,25 @@ function getResumeCommand(summary) {
36693
37146
  if (typeof actionResume === "string") {
36694
37147
  return actionResume.replace("<conversation-id>", summary.conversationId);
36695
37148
  }
36696
- return `tda chat continue ${summary.conversationId} "<message>"`;
37149
+ return messageResumeCommand(summary.conversationId);
36697
37150
  }
36698
37151
 
36699
37152
  // src/commands/chat/index.ts
36700
37153
  function registerChatCommand(program) {
36701
37154
  const chat = program.command("chat").description("Talk to the Todoist Automations agent");
36702
37155
  addWorkspaceScopeOptions(
36703
- chat.command("start").description("Start a new conversation").argument("<message...>", "Initial message")
37156
+ addStreamTextOption(
37157
+ chat.command("start").description("Start a new conversation").argument("<message...>", "Initial message")
37158
+ )
36704
37159
  ).action(async (messageParts, options, command) => {
36705
37160
  await runCommand(command, async ({ service, signal }) => {
36706
37161
  const workspaceId = await resolveWorkspaceScope(options);
36707
37162
  return service.startConversation(messageParts.join(" "), signal, workspaceId);
36708
37163
  });
36709
37164
  });
36710
- chat.command("continue").description("Continue a persisted conversation").argument("<conversation-id>", "Conversation ID emitted by start").argument("[message...]", "Answer or follow-up message").option(
37165
+ addStreamTextOption(
37166
+ chat.command("continue").description("Continue a persisted conversation").argument("<conversation-id>", "Conversation ID emitted by start").argument("[message...]", "Answer or follow-up message")
37167
+ ).option(
36711
37168
  "--connection <connector[:type]=connection-id>",
36712
37169
  "Resolve a pending connection selection; repeat for multiple connectors",
36713
37170
  collectOption,
@@ -36732,11 +37189,19 @@ function registerChatCommand(program) {
36732
37189
  );
36733
37190
  });
36734
37191
  }
37192
+ function addStreamTextOption(command) {
37193
+ return command.option(
37194
+ "--stream-text",
37195
+ "Under --ndjson, emit the assistant text as it arrives (assistant_text) rather than one assistant_message per step"
37196
+ );
37197
+ }
36735
37198
  async function runCommand(command, operation) {
36736
37199
  let service;
36737
37200
  let source;
36738
37201
  await runStreamingCommand(command, {
36739
- createOutput: (mode) => new OutputWriter(mode),
37202
+ createOutput: (mode) => new OutputWriter(mode, process.stdout, process.stderr, {
37203
+ streamText: command.opts().streamText === true
37204
+ }),
36740
37205
  operation: async ({ options, mode, output: output2, signal }) => {
36741
37206
  const apiConfiguration = await withSpinner(
36742
37207
  { text: "Connecting..." },
@@ -36759,33 +37224,33 @@ async function runCommand(command, operation) {
36759
37224
  onSuccess: (result, { output: output2 }) => output2.finish(result),
36760
37225
  onInterrupted: ({ output: output2 }) => {
36761
37226
  const conversationId = output2.conversationId;
36762
- output2.emit({ type: "interrupted", conversationId });
37227
+ const resume = conversationId ? { resumeCommand: messageResumeCommand(conversationId) } : {};
36763
37228
  output2.emit({
36764
- type: "turn_completed",
37229
+ type: "interrupted",
36765
37230
  conversationId,
36766
37231
  status: "interrupted",
36767
- outcome: "interrupted",
36768
- state: "completed",
36769
- terminal: true,
36770
- ...conversationId ? { resumeCommand: `tda chat continue ${conversationId} "<message>"` } : {}
37232
+ message: "Interrupted.",
37233
+ ...resume,
37234
+ terminal: true
36771
37235
  });
36772
37236
  output2.finish({
36773
37237
  conversationId,
36774
37238
  status: "interrupted",
36775
- assistantText: ""
37239
+ assistantText: output2.assistantText,
37240
+ ...resume
36776
37241
  });
36777
37242
  },
36778
- onError: (error62, { mode, output: output2 }) => {
37243
+ onError: async (error62, { mode, output: output2 }) => {
36779
37244
  const failure2 = renderFailure(error62, { mode, source });
36780
- const message = mode === "human" ? failure2.humanMessage : failure2.message;
36781
- output2.emit({
36782
- type: "error",
36783
- code: failure2.code ?? "TDA_COMMAND_FAILED",
36784
- message,
36785
- state: "failed",
36786
- terminal: true
36787
- });
36788
- output2.finish({ status: "error", message });
37245
+ const code = failure2.code ?? (service?.hasFailureScope ? chatFailureCode(error62) : FALLBACK_FAILURE_CODE);
37246
+ const details = await service?.describeFailure(error62, code);
37247
+ const message = mode === "human" ? [
37248
+ failure2.humanMessage,
37249
+ ...details ? formatChatFailureDetails(details) : []
37250
+ ].join("\n") : failure2.message;
37251
+ const record2 = toFailureRecord({ code, message }, details);
37252
+ output2.emit({ type: "error", ...record2, state: "failed", terminal: true });
37253
+ output2.finish(record2);
36789
37254
  },
36790
37255
  onFinally: () => {
36791
37256
  stopEarlySpinner();
@@ -36971,7 +37436,12 @@ async function runAuthorizationCommand(command, operation) {
36971
37436
  onSuccess: (result, { mode, output: output2 }) => {
36972
37437
  if (mode === "json") return output2.finish(result);
36973
37438
  stopEarlySpinner();
36974
- writeResult(mode, result, { human: formatConnectionChange });
37439
+ writeResult(mode, result, {
37440
+ human: formatConnectionChange,
37441
+ ndjson: (completed) => [
37442
+ { type: "connection_completed", ...completed, terminal: true }
37443
+ ]
37444
+ });
36975
37445
  },
36976
37446
  onInterrupted: ({ output: output2 }) => {
36977
37447
  output2.emit({
@@ -36985,14 +37455,9 @@ async function runAuthorizationCommand(command, operation) {
36985
37455
  onError: (error62, { mode, output: output2 }) => {
36986
37456
  const failure2 = renderFailure(error62, { mode, source });
36987
37457
  const message = mode === "human" ? failure2.humanMessage : failure2.message;
36988
- output2.emit({
36989
- type: "error",
36990
- status: "error",
36991
- message,
36992
- ...failure2.code ? { code: failure2.code } : {},
36993
- terminal: true
36994
- });
36995
- output2.finish({ status: "error", message });
37458
+ const record2 = toFailureRecord({ code: failure2.code, message });
37459
+ output2.emit({ type: "error", ...record2, terminal: true });
37460
+ output2.finish(record2);
36996
37461
  },
36997
37462
  onFinally: () => stopEarlySpinner()
36998
37463
  });
@@ -37365,7 +37830,7 @@ function registerConnectorCommand(program) {
37365
37830
  }
37366
37831
 
37367
37832
  // src/lib/manifest-input.ts
37368
- import { CliError as CliError4, getErrorMessage as getErrorMessage7 } from "@doist/cli-core";
37833
+ import { CliError as CliError7, getErrorMessage as getErrorMessage7 } from "@doist/cli-core";
37369
37834
  import { Option as Option6 } from "commander";
37370
37835
  import { readFile as readFile2 } from "node:fs/promises";
37371
37836
  import { text } from "node:stream/consumers";
@@ -37379,7 +37844,7 @@ async function readManifestInput(options, input2 = process.stdin) {
37379
37844
  try {
37380
37845
  return JSON.parse(source.content);
37381
37846
  } catch (error62) {
37382
- throw new CliError4(
37847
+ throw new CliError7(
37383
37848
  "MANIFEST_UNREADABLE",
37384
37849
  `${source.label} is not valid JSON: ${getErrorMessage7(error62)}`
37385
37850
  );
@@ -37390,7 +37855,7 @@ async function readSource(options, input2) {
37390
37855
  try {
37391
37856
  return { label: options.file, content: await readFile2(options.file, "utf8") };
37392
37857
  } catch (error62) {
37393
- throw new CliError4(
37858
+ throw new CliError7(
37394
37859
  "MANIFEST_UNREADABLE",
37395
37860
  `Could not read ${options.file}: ${getErrorMessage7(error62)}`
37396
37861
  );
@@ -37400,17 +37865,17 @@ async function readSource(options, input2) {
37400
37865
  try {
37401
37866
  return { label: "Standard input", content: await text(input2) };
37402
37867
  } catch (error62) {
37403
- throw new CliError4(
37868
+ throw new CliError7(
37404
37869
  "MANIFEST_UNREADABLE",
37405
37870
  `Could not read standard input: ${getErrorMessage7(error62)}`
37406
37871
  );
37407
37872
  }
37408
37873
  }
37409
- throw new CliError4("MANIFEST_UNREADABLE", "Provide the manifest with --file <path> or --stdin.");
37874
+ throw new CliError7("MANIFEST_UNREADABLE", "Provide the manifest with --file <path> or --stdin.");
37410
37875
  }
37411
37876
 
37412
37877
  // src/commands/draft/draft-service.ts
37413
- import { CliError as CliError5, getErrorMessage as getErrorMessage8 } from "@doist/cli-core";
37878
+ import { CliError as CliError8, getErrorMessage as getErrorMessage8 } from "@doist/cli-core";
37414
37879
  import { readFile as readFile3 } from "node:fs/promises";
37415
37880
  function createDraft2(draftApi, input2) {
37416
37881
  return withRecoveryHints(draftApi, createDraft(draftApi, input2));
@@ -37443,13 +37908,13 @@ async function withRecoveryHints(draftApi, operation) {
37443
37908
  const draftId = error62.draftId ?? "<draft-id>";
37444
37909
  switch (error62.code) {
37445
37910
  case "manifest_invalid":
37446
- throw new CliError5("MANIFEST_INVALID", error62.message);
37911
+ throw new CliError8("MANIFEST_INVALID", error62.message);
37447
37912
  case "draft_conflict":
37448
- throw new CliError5("DRAFT_CONFLICT", error62.message, {
37913
+ throw new CliError8("DRAFT_CONFLICT", error62.message, {
37449
37914
  hints: draftHints(scope, draftId)
37450
37915
  });
37451
37916
  case "draft_manifest_rejected":
37452
- throw new CliError5("DRAFT_MANIFEST_REJECTED", error62.message, {
37917
+ throw new CliError8("DRAFT_MANIFEST_REJECTED", error62.message, {
37453
37918
  hints: draftHints(scope, draftId)
37454
37919
  });
37455
37920
  case "draft_not_ready":
@@ -37467,14 +37932,12 @@ function draftHints(scope, draftId) {
37467
37932
  `Or discard it first: tda draft delete ${draftId} --yes ${scopeFlag(scope)}`
37468
37933
  ];
37469
37934
  }
37470
- function scopeFlag(scope) {
37471
- return scope === null ? "--personal" : `--workspace ${scope}`;
37472
- }
37473
37935
  async function startDraftTest2(draftApi, {
37474
37936
  draftId,
37475
37937
  request: request2,
37476
37938
  wait: waitFor,
37477
- signal
37939
+ signal,
37940
+ onProgress
37478
37941
  }) {
37479
37942
  let started;
37480
37943
  try {
@@ -37496,7 +37959,11 @@ async function startDraftTest2(draftApi, {
37496
37959
  }
37497
37960
  let finished;
37498
37961
  try {
37499
- finished = await waitForDraftTest(draftApi, started, { ...waitFor, signal });
37962
+ finished = await waitForDraftTest(draftApi, started, {
37963
+ ...waitFor,
37964
+ signal,
37965
+ ...onProgress ? { onPoll: reportRunProgress(onProgress) } : {}
37966
+ });
37500
37967
  } catch (error62) {
37501
37968
  throw toDraftTestWaitError(error62, identity, signal);
37502
37969
  }
@@ -37514,13 +37981,13 @@ function toDraftTestWaitError(error62, identity, signal) {
37514
37981
  const run = `test run ${identity.testRunId} of draft ${identity.draftId}`;
37515
37982
  const hints = [`See how it ends: ${identity.command}`];
37516
37983
  if (signal.aborted) {
37517
- return new CliError5(
37984
+ return new CliError8(
37518
37985
  "DRAFT_TEST_WAIT_INTERRUPTED",
37519
37986
  `Stopped waiting for ${run}. It keeps running.`,
37520
37987
  { hints }
37521
37988
  );
37522
37989
  }
37523
- return new CliError5(
37990
+ return new CliError8(
37524
37991
  "DRAFT_TEST_WAIT_FAILED",
37525
37992
  `Checking on ${run} failed: ${getErrorMessage8(error62).replace(/\.?$/, ".")} It keeps running.`,
37526
37993
  { hints }
@@ -37528,7 +37995,7 @@ function toDraftTestWaitError(error62, identity, signal) {
37528
37995
  }
37529
37996
  function draftNotReadyError(message, draftId, scope) {
37530
37997
  const flag = scopeFlag(scope);
37531
- return new CliError5("DRAFT_NOT_READY", message, {
37998
+ return new CliError8("DRAFT_NOT_READY", message, {
37532
37999
  hints: [
37533
38000
  `See the issues: tda draft view ${draftId} ${flag}`,
37534
38001
  `Bind connections: tda draft connect ${draftId} --auto ${flag}`
@@ -37542,13 +38009,13 @@ function toDraftTestCliError(refusal, draftId, scope) {
37542
38009
  return draftNotReadyError(refusal.message, draftId, scope);
37543
38010
  case "draft_test_payload_required":
37544
38011
  case "draft_test_payload_filtered":
37545
- return new CliError5(refusal.code.toUpperCase(), refusal.message, {
38012
+ return new CliError8(refusal.code.toUpperCase(), refusal.message, {
37546
38013
  hints: [
37547
38014
  `Test with a real event: tda draft test ${draftId} --trigger <event> --payload-file <path> ${flag}`
37548
38015
  ]
37549
38016
  });
37550
38017
  default:
37551
- return new CliError5(refusal.code.toUpperCase(), refusal.message);
38018
+ return new CliError8(refusal.code.toUpperCase(), refusal.message);
37552
38019
  }
37553
38020
  }
37554
38021
  async function readTestPayloadFile(path) {
@@ -37556,7 +38023,7 @@ async function readTestPayloadFile(path) {
37556
38023
  try {
37557
38024
  content = await readFile3(path, "utf8");
37558
38025
  } catch (error62) {
37559
- throw new CliError5(
38026
+ throw new CliError8(
37560
38027
  "PAYLOAD_UNREADABLE",
37561
38028
  `Could not read ${path}: ${getErrorMessage8(error62)}`
37562
38029
  );
@@ -37564,7 +38031,7 @@ async function readTestPayloadFile(path) {
37564
38031
  try {
37565
38032
  return JSON.parse(content);
37566
38033
  } catch (error62) {
37567
- throw new CliError5(
38034
+ throw new CliError8(
37568
38035
  "PAYLOAD_UNREADABLE",
37569
38036
  `${path} is not valid JSON: ${getErrorMessage8(error62)}`
37570
38037
  );
@@ -37753,7 +38220,7 @@ function formatDraftTest(result) {
37753
38220
  }
37754
38221
 
37755
38222
  // src/commands/draft/validate-command.ts
37756
- import { CliError as CliError6 } from "@doist/cli-core";
38223
+ import { CliError as CliError9 } from "@doist/cli-core";
37757
38224
 
37758
38225
  // src/lib/local-command.ts
37759
38226
  async function runLocalCommand(command, operation, presentation, execution = {}) {
@@ -37785,7 +38252,7 @@ function registerDraftValidateCommand(draft) {
37785
38252
  function validateManifestInput(manifest) {
37786
38253
  const validation = validateManifest(manifest);
37787
38254
  if (!validation.isValid) {
37788
- throw new CliError6(
38255
+ throw new CliError9(
37789
38256
  "MANIFEST_INVALID",
37790
38257
  `Manifest is invalid: ${validation.errors.join("; ")}`
37791
38258
  );
@@ -37805,8 +38272,6 @@ function formatManifestValidation(result) {
37805
38272
  }
37806
38273
 
37807
38274
  // src/commands/draft/index.ts
37808
- var DEFAULT_TEST_WAIT = "10m";
37809
- var TOKEN_LIFETIME_MARGIN_MS = 2 * 60 * 1e3;
37810
38275
  function registerDraftCommand(program) {
37811
38276
  const draft = program.command("draft").description("Author, test and inspect automation drafts");
37812
38277
  registerDraftValidateCommand(draft);
@@ -37933,9 +38398,9 @@ function registerDraftCommand(program) {
37933
38398
  );
37934
38399
  });
37935
38400
  addWorkspaceScopeOptions(
37936
- draft.command("test").description("Start a test run of a draft, with mocked connector calls").argument("<draft-id>", "Draft ID").option("--wait", "Wait for the run to end and exit 1 unless it passed").option(
37937
- "--timeout <duration>",
37938
- `How long --wait waits, such as 90s or 15m (default ${DEFAULT_TEST_WAIT})`
38401
+ addWaitOptions(
38402
+ draft.command("test").description("Start a test run of a draft, with mocked connector calls").argument("<draft-id>", "Draft ID"),
38403
+ "Wait for the run to end and exit 1 unless it passed"
37939
38404
  ).option(
37940
38405
  "--trigger <index|event>",
37941
38406
  "The trigger to test: its position in the manifest's triggers, or a webhook event"
@@ -37946,11 +38411,8 @@ function registerDraftCommand(program) {
37946
38411
  ).action(async (draftId, options, command) => {
37947
38412
  await runApiCommand(
37948
38413
  command,
37949
- async ({ api, signal }) => {
37950
- if (options.timeout !== void 0 && options.wait !== true) {
37951
- throw new Error("--timeout only applies with --wait.");
37952
- }
37953
- const timeoutMs = parseTestTimeout(options.timeout ?? DEFAULT_TEST_WAIT);
38414
+ async ({ api, signal, progress }) => {
38415
+ const wait2 = resolveWait(options);
37954
38416
  const draftApi = bindDraftApi(api, await resolveWorkspaceScope(options), { signal });
37955
38417
  return startDraftTest2(draftApi, {
37956
38418
  draftId,
@@ -37958,14 +38420,15 @@ function registerDraftCommand(program) {
37958
38420
  ...options.trigger !== void 0 ? { trigger: toDraftTestTriggerSelector(options.trigger) } : {},
37959
38421
  ...options.payloadFile !== void 0 ? { payload: await readTestPayloadFile(options.payloadFile) } : {}
37960
38422
  },
37961
- wait: options.wait === true ? { timeoutMs } : null,
37962
- signal
38423
+ wait: wait2,
38424
+ signal,
38425
+ onProgress: (state) => progress({ type: "test_progress", draftId, ...state })
37963
38426
  });
37964
38427
  },
37965
- { human: formatDraftTest },
38428
+ { human: formatDraftTest, ndjson: toTerminalRecords },
37966
38429
  {
37967
38430
  spinner: options.wait === true ? "Testing draft..." : "Starting test run...",
37968
- minimumTokenLifetimeMs: options.wait === true ? tryParseTestTimeout(options.timeout ?? DEFAULT_TEST_WAIT) + TOKEN_LIFETIME_MARGIN_MS : void 0,
38431
+ minimumTokenLifetimeMs: waitTokenLifetimeMs(options),
37969
38432
  exitCode: (result) => result.action === "test_finished" && result.outcome !== "passed" ? 1 : void 0
37970
38433
  }
37971
38434
  );
@@ -38025,20 +38488,6 @@ function streamDraftList(options, fetchPage) {
38025
38488
  async (args) => summarizeDraftListPage(await fetchPage(args))
38026
38489
  );
38027
38490
  }
38028
- function tryParseTestTimeout(value) {
38029
- try {
38030
- return durationStringToMilliseconds(value);
38031
- } catch {
38032
- return 0;
38033
- }
38034
- }
38035
- function parseTestTimeout(value) {
38036
- try {
38037
- return durationStringToMilliseconds(value);
38038
- } catch {
38039
- throw new Error(`Invalid --timeout ${value}; use a duration such as 90s, 15m or 1h.`);
38040
- }
38041
- }
38042
38491
 
38043
38492
  // src/commands/run/index.ts
38044
38493
  import { Option as Option7 } from "commander";
@@ -38134,11 +38583,11 @@ function registerRunCommand(program) {
38134
38583
  }
38135
38584
 
38136
38585
  // src/commands/skill/shared.ts
38137
- import { CliError as CliError7 } from "@doist/cli-core";
38586
+ import { CliError as CliError10 } from "@doist/cli-core";
38138
38587
  function requireInstaller(agent) {
38139
38588
  const installer = getInstaller(agent);
38140
38589
  if (!installer) {
38141
- throw new CliError7("UNKNOWN_AGENT", `Unknown agent: ${agent}.`, {
38590
+ throw new CliError10("UNKNOWN_AGENT", `Unknown agent: ${agent}.`, {
38142
38591
  hints: [`Available agents: ${AGENT_NAMES.join(", ")}.`]
38143
38592
  });
38144
38593
  }
@@ -38216,7 +38665,7 @@ async function uninstallSkill(agent, options, command) {
38216
38665
  }
38217
38666
 
38218
38667
  // src/commands/skill/update.ts
38219
- import { CliError as CliError8 } from "@doist/cli-core";
38668
+ import { CliError as CliError11 } from "@doist/cli-core";
38220
38669
  async function updateSkill(agent, options, command) {
38221
38670
  await runLocalCommand(
38222
38671
  command,
@@ -38242,7 +38691,7 @@ async function updateAllSkills(options, command) {
38242
38691
  async () => {
38243
38692
  const result = await updateAllInstalledSkills(options.local === true);
38244
38693
  if (result.errors.length > 0) {
38245
- throw new CliError8(
38694
+ throw new CliError11(
38246
38695
  "UPDATE_FAILED",
38247
38696
  `Could not update ${result.errors.length} skill file(s).`,
38248
38697
  { hints: result.errors.map((error62) => `${error62.agent}: ${error62.message}`) }
@@ -38302,7 +38751,7 @@ function registerUpdateCommand(program) {
38302
38751
  }
38303
38752
 
38304
38753
  // src/commands/workspace/index.ts
38305
- import { CliError as CliError9 } from "@doist/cli-core";
38754
+ import { CliError as CliError12 } from "@doist/cli-core";
38306
38755
 
38307
38756
  // src/commands/workspace/format.ts
38308
38757
  function formatWorkspaceList(workspaces) {
@@ -38345,7 +38794,7 @@ function registerWorkspaceCommand(program) {
38345
38794
  if (!(error62 instanceof AutomationsRequestError) || error62.httpStatusCode !== 404) {
38346
38795
  throw error62;
38347
38796
  }
38348
- throw new CliError9(
38797
+ throw new CliError12(
38349
38798
  "WORKSPACE_ACCESS_UNAVAILABLE",
38350
38799
  "Workspace access is not available for this account or environment.",
38351
38800
  {