@doist/automations-cli 1.27.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.27.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()
@@ -31057,13 +31062,19 @@ var AutomationsRequestError = class extends Error {
31057
31062
  * scope: a challenge can say the grant falls short without saying of what.
31058
31063
  */
31059
31064
  requiredScope;
31060
- 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) {
31061
31071
  super(message);
31062
31072
  this.name = "AutomationsRequestError";
31063
31073
  this.httpStatusCode = httpStatusCode;
31064
31074
  this.responseData = responseData;
31065
31075
  this.authChallenge = authChallenge2;
31066
31076
  this.requiredScope = parseChallengeScope(authChallenge2);
31077
+ this.requestId = requestId;
31067
31078
  }
31068
31079
  isAuthError() {
31069
31080
  return this.httpStatusCode === 401 || this.httpStatusCode === 403;
@@ -31134,7 +31145,7 @@ async function request(args) {
31134
31145
  const data = await readResponseBody(response);
31135
31146
  if (!response.ok) {
31136
31147
  const message = typeof data === "object" && data !== null && "message" in data ? String(data.message) : response.statusText || `Request failed with status ${response.status}`;
31137
- 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);
31138
31149
  }
31139
31150
  return data;
31140
31151
  }
@@ -31545,13 +31556,15 @@ var AutomationsClient = class extends BaseClient {
31545
31556
  });
31546
31557
  return createDraftResponseSchema.parse(data);
31547
31558
  }
31559
+ /** Starts a run now. Undefined from an API that predates returning the run. */
31548
31560
  async trigger(id, options) {
31549
31561
  const parsedId = IdSchema.parse(id);
31550
- await this.request({
31562
+ const data = await this.request({
31551
31563
  method: "POST",
31552
31564
  path: automationsEndpoints.trigger(parsedId),
31553
31565
  options
31554
31566
  });
31567
+ return triggerAutomationResponseSchema.optional().parse(data);
31555
31568
  }
31556
31569
  async test(id, options) {
31557
31570
  const parsedId = IdSchema.parse(id);
@@ -31886,13 +31899,17 @@ var WorkspaceAutomationsClient = class extends BaseClient {
31886
31899
  });
31887
31900
  return automationSchema.parse(data);
31888
31901
  }
31889
- /** 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
+ */
31890
31906
  async trigger(workspaceId, id, options) {
31891
- await this.request({
31907
+ const data = await this.request({
31892
31908
  method: "POST",
31893
31909
  path: workspaceAutomationsEndpoints.trigger(IdSchema.parse(workspaceId), IdSchema.parse(id)),
31894
31910
  options
31895
31911
  });
31912
+ return triggerAutomationResponseSchema.optional().parse(data);
31896
31913
  }
31897
31914
  /**
31898
31915
  * Deletes the automation. Workspace admins may delete another member's
@@ -32802,6 +32819,9 @@ function readManifestMetadata(dto) {
32802
32819
  ...typeof dto.manifest.description === "string" ? { description: dto.manifest.description } : {}
32803
32820
  };
32804
32821
  }
32822
+ function readPrimaryTriggerType(dto) {
32823
+ return hasTriggerType(dto.manifest) ? getManifestTriggers(dto.manifest)[0]?.type : void 0;
32824
+ }
32805
32825
  function summarizeTrigger(trigger) {
32806
32826
  if (!isManifestRecord(trigger))
32807
32827
  return void 0;
@@ -33301,24 +33321,11 @@ function readManifestName(manifest) {
33301
33321
  return typeof name === "string" && name.trim() ? name : "(unnamed)";
33302
33322
  }
33303
33323
 
33304
- // ../../packages/sdk/dist/esm/drafts/draft-test.js
33324
+ // ../../packages/sdk/dist/esm/runs/poll-until-closed.js
33305
33325
  var DEFAULT_POLL_INTERVALS_MS = { first: 2e3, max: 1e4 };
33306
33326
  var STATUS_REQUEST_GRACE_MS = 1e4;
33307
- var OPEN_RUN_STATUSES = /* @__PURE__ */ new Set([
33308
- "pending",
33309
- "running"
33310
- ]);
33311
- async function startDraftTest(draftApi, { draftId, request: request2 }) {
33312
- const started = await draftApi.test(draftId, request2);
33313
- return {
33314
- draftId,
33315
- testRunId: started.testRunId,
33316
- trigger: started.trigger,
33317
- connectorCalls: started.connectorCalls,
33318
- status: started.status
33319
- };
33320
- }
33321
- 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 }) {
33322
33329
  const intervals = pollIntervalsMs ?? DEFAULT_POLL_INTERVALS_MS;
33323
33330
  const deadline = Date.now() + timeoutMs;
33324
33331
  let intervalMs = intervals.first;
@@ -33327,24 +33334,57 @@ async function waitForDraftTest(draftApi, { draftId, testRunId }, { timeoutMs, p
33327
33334
  let last = null;
33328
33335
  while (true) {
33329
33336
  try {
33330
- last = await draftApi.getTestRun(draftId, testRunId, void 0, requestSignal);
33337
+ last = await read2(requestSignal);
33331
33338
  } catch (error62) {
33332
33339
  if (signal.aborted || !requestTimeout.aborted)
33333
33340
  throw error62;
33334
- return toFinished(last, "timed_out");
33341
+ return { timedOut: true, run: last };
33335
33342
  }
33336
33343
  if (!OPEN_RUN_STATUSES.has(last.status)) {
33337
- return toFinished(last, toOutcome(last.status));
33344
+ return { timedOut: false, run: last };
33338
33345
  }
33339
33346
  onPoll?.(last);
33340
33347
  const remainingMs = deadline - Date.now();
33341
33348
  if (remainingMs <= 0) {
33342
- return toFinished(last, "timed_out");
33349
+ return { timedOut: true, run: last };
33343
33350
  }
33344
33351
  await delay(Math.min(intervalMs, remainingMs), signal);
33345
33352
  intervalMs = Math.min(intervalMs * 2, intervals.max);
33346
33353
  }
33347
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
+ }
33348
33388
  function toDraftTestTriggerSelector(value) {
33349
33389
  if (typeof value === "number")
33350
33390
  return { index: value };
@@ -33371,22 +33411,64 @@ function toOutcome(status) {
33371
33411
  return "failed";
33372
33412
  }
33373
33413
  }
33374
- function delay(ms, signal) {
33375
- return new Promise((resolve, reject) => {
33376
- if (signal.aborted) {
33377
- reject(signal.reason);
33378
- return;
33379
- }
33380
- const onAbort = () => {
33381
- clearTimeout(timer);
33382
- 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
+ })
33383
33425
  };
33384
- const timer = setTimeout(() => {
33385
- signal.removeEventListener("abort", onAbort);
33386
- resolve();
33387
- }, ms);
33388
- signal.addEventListener("abort", onAbort, { once: true });
33389
- });
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;
33390
33472
  }
33391
33473
 
33392
33474
  // src/lib/auth-environment.ts
@@ -34595,172 +34677,6 @@ import { CliError as CliError3 } from "@doist/cli-core";
34595
34677
  // src/lib/command-error.ts
34596
34678
  import { formatJson as formatJson4, formatNdjson as formatNdjson4, getErrorMessage as getErrorMessage6 } from "@doist/cli-core";
34597
34679
 
34598
- // src/lib/billing-errors.ts
34599
- import { CliError as CliError2 } from "@doist/cli-core";
34600
- var PAYMENT_REQUIRED_CODE = "PAYMENT_REQUIRED";
34601
- var TOP_UP_PATH = "/settings/top-up";
34602
- var TODOIST_SUBSCRIPTION_PATH = "/app/settings/subscription";
34603
- function topUpUrl() {
34604
- return `${getEnvironment().appUrl}${TOP_UP_PATH}`;
34605
- }
34606
- function todoistSubscriptionUrl() {
34607
- return `${getEnvironment().todoistAppUrl}${TODOIST_SUBSCRIPTION_PATH}`;
34608
- }
34609
- var topUpHint = () => `Top up at ${topUpUrl()}.`;
34610
- var HINT_BY_DENIAL_CODE = {
34611
- usage_billing_insufficient_credit: topUpHint,
34612
- usage_billing_no_billing_account: topUpHint,
34613
- usage_billing_plan_required: () => `Upgrade at ${todoistSubscriptionUrl()}.`,
34614
- usage_billing_billing_paused: topUpHint
34615
- };
34616
- var PaymentRequiredError = class extends CliError2 {
34617
- constructor(message, hints) {
34618
- super(PAYMENT_REQUIRED_CODE, message, { hints });
34619
- this.name = "PaymentRequiredError";
34620
- }
34621
- };
34622
- function explainBillingFailure(error62) {
34623
- const refusal = getChatBillingRefusal(error62);
34624
- if (refusal === null) return error62;
34625
- const hints = refusal.code === void 0 ? [] : [HINT_BY_DENIAL_CODE[refusal.code]()];
34626
- return new PaymentRequiredError(refusal.message, hints);
34627
- }
34628
-
34629
- // src/lib/output.ts
34630
- import { formatJson as formatJson3, formatNdjson as formatNdjson3 } from "@doist/cli-core";
34631
- import { stripVTControlCharacters } from "node:util";
34632
- function getOutputMode(args) {
34633
- if (args.json && args.ndjson) {
34634
- throw new Error("Choose either --json or --ndjson, not both.");
34635
- }
34636
- if (args.json) return "json";
34637
- if (args.ndjson) return "ndjson";
34638
- return "human";
34639
- }
34640
- function sanitizeTerminalText(value) {
34641
- return stripVTControlCharacters(value).replace(
34642
- /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu,
34643
- ""
34644
- );
34645
- }
34646
- function writeResult(mode, result, presentation, stream = process.stdout) {
34647
- if (mode === "json") {
34648
- stream.write(`${formatJson3(presentation.json?.(result) ?? result)}
34649
- `);
34650
- return;
34651
- }
34652
- if (mode === "ndjson") {
34653
- const output3 = formatNdjson3(
34654
- presentation.ndjson?.(result) ?? [presentation.json?.(result) ?? result]
34655
- );
34656
- if (output3) stream.write(`${output3}
34657
- `);
34658
- return;
34659
- }
34660
- const output2 = sanitizeTerminalText(presentation.human(result));
34661
- stream.write(output2.endsWith("\n") ? output2 : `${output2}
34662
- `);
34663
- }
34664
- async function writeNdjsonItem(item, stream = process.stdout) {
34665
- if (stream.write(`${formatNdjson3([item])}
34666
- `) !== false) return;
34667
- await new Promise((resolve) => stream.once("drain", resolve));
34668
- }
34669
-
34670
- // src/lib/command-error.ts
34671
- function renderFailure(error62, context = {}) {
34672
- const authExplained = context.source ? explainAuthFailure(error62, context.source) : error62;
34673
- const explained = explainBillingFailure(authExplained);
34674
- const code = getErrorCode(explained);
34675
- return {
34676
- message: getErrorMessage6(explained),
34677
- humanMessage: formatError2(explained),
34678
- ...code ? { code } : {}
34679
- };
34680
- }
34681
- function writeFailure(mode, status, failure2) {
34682
- const { message, code } = failure2;
34683
- const result = { status, message, ...code ? { code } : {} };
34684
- if (mode === "json") {
34685
- process.stdout.write(`${formatJson4(result)}
34686
- `);
34687
- } else if (mode === "ndjson") {
34688
- process.stdout.write(`${formatNdjson4([{ type: status, ...result, terminal: true }])}
34689
- `);
34690
- } else {
34691
- const prefix = status === "error" ? "Error: " : "";
34692
- process.stdout.write(`${prefix}${sanitizeTerminalText(failure2.humanMessage)}
34693
- `);
34694
- }
34695
- }
34696
-
34697
- // src/lib/api-command.ts
34698
- async function runApiCommand(command, operation, presentation, execution = {}) {
34699
- let mode = "human";
34700
- let source;
34701
- const abortController = new AbortController();
34702
- const onInterrupt = () => abortController.abort();
34703
- process.once("SIGINT", onInterrupt);
34704
- try {
34705
- const globalOptions = getGlobalOptions(command);
34706
- mode = getOutputMode(globalOptions);
34707
- const apiConfiguration = await resolveApiConfiguration(globalOptions, {
34708
- minimumTokenLifetimeMs: execution.minimumTokenLifetimeMs
34709
- });
34710
- source = apiConfiguration.source;
34711
- const context = {
34712
- api: createAutomationsApi(apiConfiguration),
34713
- signal: abortController.signal
34714
- };
34715
- if (mode === "ndjson" && execution.streamNdjson) {
34716
- for await (const item of execution.streamNdjson(context)) await writeNdjsonItem(item);
34717
- return;
34718
- }
34719
- const result = await withSpinner(
34720
- { text: execution.spinner ?? "Loading..." },
34721
- () => operation(context)
34722
- );
34723
- stopEarlySpinner();
34724
- writeResult(mode, result, presentation);
34725
- const exitCode = execution.exitCode?.(result);
34726
- if (exitCode !== void 0) process.exitCode = exitCode;
34727
- } catch (error62) {
34728
- if (abortController.signal.aborted) {
34729
- writeFailure(
34730
- mode,
34731
- "interrupted",
34732
- error62 instanceof CliError3 ? renderFailure(error62, { mode, source }) : { message: "Interrupted.", humanMessage: "Interrupted." }
34733
- );
34734
- process.exitCode = 130;
34735
- return;
34736
- }
34737
- writeFailure(mode, "error", renderFailure(error62, { mode, source }));
34738
- process.exitCode = 1;
34739
- } finally {
34740
- process.removeListener("SIGINT", onInterrupt);
34741
- }
34742
- }
34743
-
34744
- // src/lib/workspace-scope.ts
34745
- import { Option } from "commander";
34746
- async function resolveWorkspaceScope(options) {
34747
- if (options.workspace !== void 0) {
34748
- return options.workspace;
34749
- }
34750
- if (options.personal) {
34751
- return null;
34752
- }
34753
- return getDefaultWorkspaceId();
34754
- }
34755
- function addWorkspaceScopeOptions(command) {
34756
- return command.addOption(
34757
- new Option(
34758
- "--workspace <workspace-id>",
34759
- "Act on this workspace instead of your personal space"
34760
- ).conflicts("personal")
34761
- ).option("--personal", "Act on your personal space, ignoring any pinned workspace");
34762
- }
34763
-
34764
34680
  // ../../packages/sdk/dist/esm/chat-turn/connection-requirements.js
34765
34681
  var CONNECTION_LOOKUP_CONCURRENCY = 4;
34766
34682
  function deduplicateConnectionRequirements(requirements) {
@@ -35169,6 +35085,67 @@ function assertSelectionsUnambiguous(requirements, selections) {
35169
35085
 
35170
35086
  // ../../packages/sdk/dist/esm/chat-turn/transport.js
35171
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
35172
35149
  var ChatTurnTransport = class {
35173
35150
  transport;
35174
35151
  constructor({ token, baseUrl, headers, fetch: fetch2 }) {
@@ -35195,39 +35172,62 @@ var ChatTurnTransport = class {
35195
35172
  }
35196
35173
  /** Streams one server turn, reporting incremental text and deduplicated tool state changes. */
35197
35174
  async streamTurn({ conversationId, workspaceId = null, message, signal, onText, onToolActivity }) {
35198
- const stream = await this.transport.sendMessages({
35199
- trigger: "submit-message",
35200
- chatId: conversationId,
35201
- messageId: void 0,
35202
- messages: [message],
35203
- abortSignal: signal,
35204
- body: { conversationId, workspaceId }
35205
- });
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
+ }
35206
35191
  let finalMessage;
35207
35192
  let emittedText = "";
35208
35193
  const emittedToolStates = /* @__PURE__ */ new Set();
35209
- for await (const currentMessage of readUIMessageStream({
35210
- stream,
35211
- terminateOnError: true
35212
- })) {
35213
- finalMessage = currentMessage;
35214
- const visibleText = getVisibleText(currentMessage);
35215
- if (visibleText.startsWith(emittedText)) {
35216
- const delta = visibleText.slice(emittedText.length);
35217
- if (delta)
35218
- onText?.(delta);
35219
- emittedText = visibleText;
35220
- }
35221
- for (const part of currentMessage.parts) {
35222
- const activity = toToolActivity(part);
35223
- if (!activity)
35224
- continue;
35225
- const key = `${activity.toolCallId}:${activity.state}`;
35226
- if (emittedToolStates.has(key))
35227
- continue;
35228
- emittedToolStates.add(key);
35229
- 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
+ }
35230
35226
  }
35227
+ } catch (error62) {
35228
+ if (callbackError)
35229
+ throw callbackError.error;
35230
+ throw fail("stream", error62);
35231
35231
  }
35232
35232
  if (!finalMessage) {
35233
35233
  throw new Error("The Automations chat returned an empty response.");
@@ -35246,6 +35246,236 @@ function toToolActivity(part) {
35246
35246
  };
35247
35247
  }
35248
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
+
35249
35479
  // src/commands/read/format.ts
35250
35480
  function formatAutomationList(page) {
35251
35481
  if (page.data.length === 0) return "No automations found.";
@@ -35457,8 +35687,35 @@ function describeVisibility(automation) {
35457
35687
  function summarizeAutomationChange(result) {
35458
35688
  return { ...result, automation: summarizeAutomation(result.automation) };
35459
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
+ };
35460
35697
  function formatAutomationTrigger(result) {
35461
- 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");
35462
35719
  }
35463
35720
  function formatAutomationDelete(result) {
35464
35721
  if (result.action === "delete_preview") {
@@ -35473,6 +35730,56 @@ function capitalize(value) {
35473
35730
  return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
35474
35731
  }
35475
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
+
35476
35783
  // src/commands/automation/index.ts
35477
35784
  function registerAutomationCommand(program) {
35478
35785
  const automation = program.command("automation").description("Inspect and manage published automations");
@@ -35522,30 +35829,29 @@ function registerAutomationCommand(program) {
35522
35829
  registerEnabledStateCommand(automation, "enable", true);
35523
35830
  registerEnabledStateCommand(automation, "disable", false);
35524
35831
  addWorkspaceScopeOptions(
35525
- 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
+ )
35526
35836
  ).action(async (automationId, options, command) => {
35527
35837
  await runApiCommand(
35528
35838
  command,
35529
- async ({ api, signal }) => {
35530
- const workspaceId = await resolveWorkspaceScope(options);
35531
- const automation2 = workspaceId === null ? await api.automations.get(automationId, { signal }) : await api.workspaceAutomations.get(workspaceId, automationId, {
35532
- 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 })
35533
35847
  });
35534
- const triggerType = hasTriggerType(automation2.manifest) ? getManifestTriggers(automation2.manifest)[0]?.type : void 0;
35535
- if (triggerType !== "schedule") {
35536
- throw new Error(
35537
- "This automation can only be triggered via its configured mechanism, not manually."
35538
- );
35539
- }
35540
- if (workspaceId === null) {
35541
- await api.automations.trigger(automationId, { signal });
35542
- } else {
35543
- await api.workspaceAutomations.trigger(workspaceId, automationId, { signal });
35544
- }
35545
- return { action: "triggered", automationId };
35546
35848
  },
35547
- { human: formatAutomationTrigger },
35548
- { 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
+ }
35549
35855
  );
35550
35856
  });
35551
35857
  addWorkspaceScopeOptions(
@@ -35802,6 +36108,7 @@ function collectOption(value, previous) {
35802
36108
  }
35803
36109
 
35804
36110
  // src/commands/chat/chat-service.ts
36111
+ import { CliError as CliError6 } from "@doist/cli-core";
35805
36112
  import { createInterface } from "node:readline/promises";
35806
36113
 
35807
36114
  // src/commands/chat/connection-authorizer.ts
@@ -35816,6 +36123,13 @@ var FINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
35816
36123
  "session_status",
35817
36124
  "turn_completed"
35818
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"]);
35819
36133
  var SPINNER_TEXT = {
35820
36134
  connection_authorization_opened: "Waiting for authorization...",
35821
36135
  connection_authorization_started: "Waiting for authorization...",
@@ -35826,16 +36140,47 @@ var OutputWriter = class {
35826
36140
  stream;
35827
36141
  diagnosticStream;
35828
36142
  events = [];
36143
+ streamText;
35829
36144
  assistantLineOpen = false;
35830
36145
  currentConversationId;
35831
- 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 } = {}) {
35832
36152
  this.mode = mode;
35833
36153
  this.stream = stream;
35834
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;
35835
36173
  }
35836
36174
  /** Records and renders an event; JSON mode retains structured output until `finish`. */
35837
36175
  emit(event) {
35838
- 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
+ }
35839
36184
  if (event.conversationId) this.currentConversationId = event.conversationId;
35840
36185
  if (this.mode === "ndjson") {
35841
36186
  this.stream.write(`${formatNdjson5([event])}
@@ -35851,17 +36196,28 @@ var OutputWriter = class {
35851
36196
  get conversationId() {
35852
36197
  return this.currentConversationId;
35853
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
+ }
35854
36208
  /** Emits structured conversation status with human formatting owned by this output layer. */
35855
36209
  emitConversationStatus(snapshot, pendingActionKind) {
35856
36210
  this.emit({
35857
36211
  type: "session_status",
35858
36212
  conversationId: snapshot.conversation.id,
35859
36213
  ...snapshot,
35860
- humanText: formatConversationStatus(snapshot, pendingActionKind)
36214
+ humanText: formatConversationStatus(snapshot, pendingActionKind),
36215
+ terminal: true
35861
36216
  });
35862
36217
  }
35863
36218
  /** Finalizes output, including the accumulated event list in JSON mode. */
35864
36219
  finish(summary) {
36220
+ this.flushAssistantMessage(false);
35865
36221
  stopEarlySpinner();
35866
36222
  if (this.mode === "json") {
35867
36223
  this.stream.write(`${formatJson5({ ...summary, events: this.events })}
@@ -35873,6 +36229,17 @@ var OutputWriter = class {
35873
36229
  this.assistantLineOpen = false;
35874
36230
  }
35875
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
+ }
35876
36243
  /**
35877
36244
  * The agent goes quiet for long stretches between events, so a spinner
35878
36245
  * fills the gap. Taking it down before every write keeps its next frame
@@ -36096,6 +36463,7 @@ function getMessageRoleLabel(role) {
36096
36463
 
36097
36464
  // src/commands/chat/connection-authorizer.ts
36098
36465
  var CONNECTION_POLL_INTERVAL_MS = 2e3;
36466
+ var CONNECTION_WAITING_REPORT_INTERVAL_MS = 15e3;
36099
36467
  var CONNECTION_AUTHORIZATION_TIMEOUT_MS = 10 * 60 * 1e3;
36100
36468
  var ApiConnectionAuthorizer = class {
36101
36469
  connections;
@@ -36187,6 +36555,7 @@ var ApiConnectionAuthorizer = class {
36187
36555
  }
36188
36556
  async waitUntilAuthorized(request2, connectionId, signal) {
36189
36557
  const startedAt = Date.now();
36558
+ let reportedAt;
36190
36559
  while (true) {
36191
36560
  if (signal?.aborted) throw new Error("Connection authorization interrupted.");
36192
36561
  const connection = await this.connections.get(connectionId, { signal });
@@ -36214,16 +36583,20 @@ var ApiConnectionAuthorizer = class {
36214
36583
  `Timed out waiting for ${request2.serviceName} connection authorization.`
36215
36584
  );
36216
36585
  }
36217
- this.output.emit({
36218
- type: "connection_authorization_waiting",
36219
- conversationId: request2.conversationId,
36220
- connectionId,
36221
- connectorId: request2.connectorId,
36222
- serviceName: request2.serviceName,
36223
- elapsedSeconds: Math.floor(elapsedMs / 1e3),
36224
- state: "waiting_for_user",
36225
- terminal: false
36226
- });
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
+ }
36227
36600
  await wait(Math.min(this.pollIntervalMs, this.timeoutMs - elapsedMs), void 0, {
36228
36601
  signal
36229
36602
  });
@@ -36238,11 +36611,48 @@ function isSafeSetupUrl(value) {
36238
36611
  }
36239
36612
  }
36240
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
+
36241
36650
  // src/commands/chat/conversation-turn-lock.ts
36242
36651
  import { link, mkdir, readFile, rm, writeFile } from "node:fs/promises";
36243
36652
  import { tmpdir } from "node:os";
36244
36653
  import { join } from "node:path";
36245
36654
  import { randomUUID } from "node:crypto";
36655
+ import { CliError as CliError5 } from "@doist/cli-core";
36246
36656
  var ConversationTurnLock = class {
36247
36657
  directory;
36248
36658
  pid;
@@ -36279,7 +36689,8 @@ var ConversationTurnLock = class {
36279
36689
  }
36280
36690
  const current = await readLock(path);
36281
36691
  if (current && this.isProcessRunning(current.pid)) {
36282
- throw new Error(
36692
+ throw new CliError5(
36693
+ "CHAT_TURN_ACTIVE",
36283
36694
  `Conversation ${conversationId} already has an active turn in process ${current.pid}. Continue monitoring that process; do not invoke continue until it exits.`
36284
36695
  );
36285
36696
  }
@@ -36326,7 +36737,8 @@ function isNoSuchProcessError(error62) {
36326
36737
 
36327
36738
  // src/commands/chat/chat-service.ts
36328
36739
  var TURN_HEARTBEAT_INTERVAL_MS = 15e3;
36329
- 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.";
36330
36742
  var ChatService = class {
36331
36743
  api;
36332
36744
  transport;
@@ -36336,6 +36748,8 @@ var ChatService = class {
36336
36748
  turnLock;
36337
36749
  heartbeatIntervalMs;
36338
36750
  readline;
36751
+ /** The conversation a start or continue is working on, once it is known. */
36752
+ failureScope;
36339
36753
  constructor({
36340
36754
  token,
36341
36755
  baseUrl,
@@ -36366,6 +36780,10 @@ var ChatService = class {
36366
36780
  */
36367
36781
  async startConversation(message, signal, workspaceId = null) {
36368
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
+ };
36369
36787
  this.output.emit({ type: "session_started", conversationId: conversation.id });
36370
36788
  return this.withActiveTurn(
36371
36789
  conversation.id,
@@ -36384,8 +36802,10 @@ var ChatService = class {
36384
36802
  selections,
36385
36803
  signal
36386
36804
  }) {
36805
+ this.failureScope = { conversationId };
36387
36806
  const conversation = await this.api.chat.getConversation(conversationId, { signal });
36388
36807
  const scope = { conversationId, workspaceId: conversation.workspaceId };
36808
+ this.failureScope = scope;
36389
36809
  return this.withActiveTurn(
36390
36810
  conversationId,
36391
36811
  async () => {
@@ -36398,7 +36818,8 @@ var ChatService = class {
36398
36818
  } else {
36399
36819
  const nextMessage = this.interactive ? await this.promptForMessage(signal) : "";
36400
36820
  if (!nextMessage) {
36401
- throw new Error(
36821
+ throw new CliError6(
36822
+ "CHAT_INPUT_REQUIRED",
36402
36823
  "A message or --connection selection is required to continue."
36403
36824
  );
36404
36825
  }
@@ -36504,6 +36925,7 @@ var ChatService = class {
36504
36925
  }
36505
36926
  }
36506
36927
  async runTurn({ conversationId, workspaceId }, input2, signal, pendingAction, { markActivity }) {
36928
+ this.output.startTurn();
36507
36929
  try {
36508
36930
  return await runConversationTurn({
36509
36931
  api: this.api,
@@ -36515,7 +36937,7 @@ var ChatService = class {
36515
36937
  signal,
36516
36938
  onText: (text2) => {
36517
36939
  markActivity();
36518
- this.output.emit({ type: "assistant_text", conversationId, text: text2 });
36940
+ this.output.appendAssistantText(conversationId, text2);
36519
36941
  },
36520
36942
  onToolActivity: (activity) => {
36521
36943
  markActivity();
@@ -36537,7 +36959,7 @@ var ChatService = class {
36537
36959
  });
36538
36960
  } catch (error62) {
36539
36961
  if (error62 instanceof ChatTurnInputError) {
36540
- throw new Error(describeInputError(error62));
36962
+ throw new CliError6(`CHAT_${error62.code.toUpperCase()}`, describeInputError(error62));
36541
36963
  }
36542
36964
  throw error62;
36543
36965
  }
@@ -36561,13 +36983,17 @@ var ChatService = class {
36561
36983
  terminal: false
36562
36984
  });
36563
36985
  }
36564
- completeTurn(summary) {
36986
+ completeTurn(turn) {
36987
+ const summary = { ...turn, assistantText: this.output.assistantText || turn.assistantText };
36565
36988
  const resumeCommand = getResumeCommand(summary);
36566
36989
  this.output.emit({
36567
36990
  type: "turn_completed",
36568
36991
  conversationId: summary.conversationId,
36569
36992
  status: summary.status,
36570
36993
  outcome: summary.status,
36994
+ assistantText: summary.assistantText,
36995
+ ...summary.automationId ? { automationId: summary.automationId } : {},
36996
+ ...summary.action ? { action: summary.action } : {},
36571
36997
  state: "completed",
36572
36998
  terminal: true,
36573
36999
  ...summary.conversationArchived !== void 0 ? { conversationArchived: summary.conversationArchived } : {},
@@ -36608,6 +37034,32 @@ var ChatService = class {
36608
37034
  await release();
36609
37035
  }
36610
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
+ }
36611
37063
  async getPersistedPendingAction(conversationId, signal) {
36612
37064
  const messages = await this.api.chat.getMessages(conversationId, { signal });
36613
37065
  return getPendingActionFromMessages(messages);
@@ -36694,21 +37146,25 @@ function getResumeCommand(summary) {
36694
37146
  if (typeof actionResume === "string") {
36695
37147
  return actionResume.replace("<conversation-id>", summary.conversationId);
36696
37148
  }
36697
- return `tda chat continue ${summary.conversationId} "<message>"`;
37149
+ return messageResumeCommand(summary.conversationId);
36698
37150
  }
36699
37151
 
36700
37152
  // src/commands/chat/index.ts
36701
37153
  function registerChatCommand(program) {
36702
37154
  const chat = program.command("chat").description("Talk to the Todoist Automations agent");
36703
37155
  addWorkspaceScopeOptions(
36704
- 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
+ )
36705
37159
  ).action(async (messageParts, options, command) => {
36706
37160
  await runCommand(command, async ({ service, signal }) => {
36707
37161
  const workspaceId = await resolveWorkspaceScope(options);
36708
37162
  return service.startConversation(messageParts.join(" "), signal, workspaceId);
36709
37163
  });
36710
37164
  });
36711
- 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(
36712
37168
  "--connection <connector[:type]=connection-id>",
36713
37169
  "Resolve a pending connection selection; repeat for multiple connectors",
36714
37170
  collectOption,
@@ -36733,11 +37189,19 @@ function registerChatCommand(program) {
36733
37189
  );
36734
37190
  });
36735
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
+ }
36736
37198
  async function runCommand(command, operation) {
36737
37199
  let service;
36738
37200
  let source;
36739
37201
  await runStreamingCommand(command, {
36740
- createOutput: (mode) => new OutputWriter(mode),
37202
+ createOutput: (mode) => new OutputWriter(mode, process.stdout, process.stderr, {
37203
+ streamText: command.opts().streamText === true
37204
+ }),
36741
37205
  operation: async ({ options, mode, output: output2, signal }) => {
36742
37206
  const apiConfiguration = await withSpinner(
36743
37207
  { text: "Connecting..." },
@@ -36760,33 +37224,33 @@ async function runCommand(command, operation) {
36760
37224
  onSuccess: (result, { output: output2 }) => output2.finish(result),
36761
37225
  onInterrupted: ({ output: output2 }) => {
36762
37226
  const conversationId = output2.conversationId;
36763
- output2.emit({ type: "interrupted", conversationId });
37227
+ const resume = conversationId ? { resumeCommand: messageResumeCommand(conversationId) } : {};
36764
37228
  output2.emit({
36765
- type: "turn_completed",
37229
+ type: "interrupted",
36766
37230
  conversationId,
36767
37231
  status: "interrupted",
36768
- outcome: "interrupted",
36769
- state: "completed",
36770
- terminal: true,
36771
- ...conversationId ? { resumeCommand: `tda chat continue ${conversationId} "<message>"` } : {}
37232
+ message: "Interrupted.",
37233
+ ...resume,
37234
+ terminal: true
36772
37235
  });
36773
37236
  output2.finish({
36774
37237
  conversationId,
36775
37238
  status: "interrupted",
36776
- assistantText: ""
37239
+ assistantText: output2.assistantText,
37240
+ ...resume
36777
37241
  });
36778
37242
  },
36779
- onError: (error62, { mode, output: output2 }) => {
37243
+ onError: async (error62, { mode, output: output2 }) => {
36780
37244
  const failure2 = renderFailure(error62, { mode, source });
36781
- const message = mode === "human" ? failure2.humanMessage : failure2.message;
36782
- output2.emit({
36783
- type: "error",
36784
- code: failure2.code ?? "TDA_COMMAND_FAILED",
36785
- message,
36786
- state: "failed",
36787
- terminal: true
36788
- });
36789
- 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);
36790
37254
  },
36791
37255
  onFinally: () => {
36792
37256
  stopEarlySpinner();
@@ -36972,7 +37436,12 @@ async function runAuthorizationCommand(command, operation) {
36972
37436
  onSuccess: (result, { mode, output: output2 }) => {
36973
37437
  if (mode === "json") return output2.finish(result);
36974
37438
  stopEarlySpinner();
36975
- writeResult(mode, result, { human: formatConnectionChange });
37439
+ writeResult(mode, result, {
37440
+ human: formatConnectionChange,
37441
+ ndjson: (completed) => [
37442
+ { type: "connection_completed", ...completed, terminal: true }
37443
+ ]
37444
+ });
36976
37445
  },
36977
37446
  onInterrupted: ({ output: output2 }) => {
36978
37447
  output2.emit({
@@ -36986,14 +37455,9 @@ async function runAuthorizationCommand(command, operation) {
36986
37455
  onError: (error62, { mode, output: output2 }) => {
36987
37456
  const failure2 = renderFailure(error62, { mode, source });
36988
37457
  const message = mode === "human" ? failure2.humanMessage : failure2.message;
36989
- output2.emit({
36990
- type: "error",
36991
- status: "error",
36992
- message,
36993
- ...failure2.code ? { code: failure2.code } : {},
36994
- terminal: true
36995
- });
36996
- 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);
36997
37461
  },
36998
37462
  onFinally: () => stopEarlySpinner()
36999
37463
  });
@@ -37366,7 +37830,7 @@ function registerConnectorCommand(program) {
37366
37830
  }
37367
37831
 
37368
37832
  // src/lib/manifest-input.ts
37369
- import { CliError as CliError4, getErrorMessage as getErrorMessage7 } from "@doist/cli-core";
37833
+ import { CliError as CliError7, getErrorMessage as getErrorMessage7 } from "@doist/cli-core";
37370
37834
  import { Option as Option6 } from "commander";
37371
37835
  import { readFile as readFile2 } from "node:fs/promises";
37372
37836
  import { text } from "node:stream/consumers";
@@ -37380,7 +37844,7 @@ async function readManifestInput(options, input2 = process.stdin) {
37380
37844
  try {
37381
37845
  return JSON.parse(source.content);
37382
37846
  } catch (error62) {
37383
- throw new CliError4(
37847
+ throw new CliError7(
37384
37848
  "MANIFEST_UNREADABLE",
37385
37849
  `${source.label} is not valid JSON: ${getErrorMessage7(error62)}`
37386
37850
  );
@@ -37391,7 +37855,7 @@ async function readSource(options, input2) {
37391
37855
  try {
37392
37856
  return { label: options.file, content: await readFile2(options.file, "utf8") };
37393
37857
  } catch (error62) {
37394
- throw new CliError4(
37858
+ throw new CliError7(
37395
37859
  "MANIFEST_UNREADABLE",
37396
37860
  `Could not read ${options.file}: ${getErrorMessage7(error62)}`
37397
37861
  );
@@ -37401,17 +37865,17 @@ async function readSource(options, input2) {
37401
37865
  try {
37402
37866
  return { label: "Standard input", content: await text(input2) };
37403
37867
  } catch (error62) {
37404
- throw new CliError4(
37868
+ throw new CliError7(
37405
37869
  "MANIFEST_UNREADABLE",
37406
37870
  `Could not read standard input: ${getErrorMessage7(error62)}`
37407
37871
  );
37408
37872
  }
37409
37873
  }
37410
- 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.");
37411
37875
  }
37412
37876
 
37413
37877
  // src/commands/draft/draft-service.ts
37414
- import { CliError as CliError5, getErrorMessage as getErrorMessage8 } from "@doist/cli-core";
37878
+ import { CliError as CliError8, getErrorMessage as getErrorMessage8 } from "@doist/cli-core";
37415
37879
  import { readFile as readFile3 } from "node:fs/promises";
37416
37880
  function createDraft2(draftApi, input2) {
37417
37881
  return withRecoveryHints(draftApi, createDraft(draftApi, input2));
@@ -37444,13 +37908,13 @@ async function withRecoveryHints(draftApi, operation) {
37444
37908
  const draftId = error62.draftId ?? "<draft-id>";
37445
37909
  switch (error62.code) {
37446
37910
  case "manifest_invalid":
37447
- throw new CliError5("MANIFEST_INVALID", error62.message);
37911
+ throw new CliError8("MANIFEST_INVALID", error62.message);
37448
37912
  case "draft_conflict":
37449
- throw new CliError5("DRAFT_CONFLICT", error62.message, {
37913
+ throw new CliError8("DRAFT_CONFLICT", error62.message, {
37450
37914
  hints: draftHints(scope, draftId)
37451
37915
  });
37452
37916
  case "draft_manifest_rejected":
37453
- throw new CliError5("DRAFT_MANIFEST_REJECTED", error62.message, {
37917
+ throw new CliError8("DRAFT_MANIFEST_REJECTED", error62.message, {
37454
37918
  hints: draftHints(scope, draftId)
37455
37919
  });
37456
37920
  case "draft_not_ready":
@@ -37468,14 +37932,12 @@ function draftHints(scope, draftId) {
37468
37932
  `Or discard it first: tda draft delete ${draftId} --yes ${scopeFlag(scope)}`
37469
37933
  ];
37470
37934
  }
37471
- function scopeFlag(scope) {
37472
- return scope === null ? "--personal" : `--workspace ${scope}`;
37473
- }
37474
37935
  async function startDraftTest2(draftApi, {
37475
37936
  draftId,
37476
37937
  request: request2,
37477
37938
  wait: waitFor,
37478
- signal
37939
+ signal,
37940
+ onProgress
37479
37941
  }) {
37480
37942
  let started;
37481
37943
  try {
@@ -37497,7 +37959,11 @@ async function startDraftTest2(draftApi, {
37497
37959
  }
37498
37960
  let finished;
37499
37961
  try {
37500
- finished = await waitForDraftTest(draftApi, started, { ...waitFor, signal });
37962
+ finished = await waitForDraftTest(draftApi, started, {
37963
+ ...waitFor,
37964
+ signal,
37965
+ ...onProgress ? { onPoll: reportRunProgress(onProgress) } : {}
37966
+ });
37501
37967
  } catch (error62) {
37502
37968
  throw toDraftTestWaitError(error62, identity, signal);
37503
37969
  }
@@ -37515,13 +37981,13 @@ function toDraftTestWaitError(error62, identity, signal) {
37515
37981
  const run = `test run ${identity.testRunId} of draft ${identity.draftId}`;
37516
37982
  const hints = [`See how it ends: ${identity.command}`];
37517
37983
  if (signal.aborted) {
37518
- return new CliError5(
37984
+ return new CliError8(
37519
37985
  "DRAFT_TEST_WAIT_INTERRUPTED",
37520
37986
  `Stopped waiting for ${run}. It keeps running.`,
37521
37987
  { hints }
37522
37988
  );
37523
37989
  }
37524
- return new CliError5(
37990
+ return new CliError8(
37525
37991
  "DRAFT_TEST_WAIT_FAILED",
37526
37992
  `Checking on ${run} failed: ${getErrorMessage8(error62).replace(/\.?$/, ".")} It keeps running.`,
37527
37993
  { hints }
@@ -37529,7 +37995,7 @@ function toDraftTestWaitError(error62, identity, signal) {
37529
37995
  }
37530
37996
  function draftNotReadyError(message, draftId, scope) {
37531
37997
  const flag = scopeFlag(scope);
37532
- return new CliError5("DRAFT_NOT_READY", message, {
37998
+ return new CliError8("DRAFT_NOT_READY", message, {
37533
37999
  hints: [
37534
38000
  `See the issues: tda draft view ${draftId} ${flag}`,
37535
38001
  `Bind connections: tda draft connect ${draftId} --auto ${flag}`
@@ -37543,13 +38009,13 @@ function toDraftTestCliError(refusal, draftId, scope) {
37543
38009
  return draftNotReadyError(refusal.message, draftId, scope);
37544
38010
  case "draft_test_payload_required":
37545
38011
  case "draft_test_payload_filtered":
37546
- return new CliError5(refusal.code.toUpperCase(), refusal.message, {
38012
+ return new CliError8(refusal.code.toUpperCase(), refusal.message, {
37547
38013
  hints: [
37548
38014
  `Test with a real event: tda draft test ${draftId} --trigger <event> --payload-file <path> ${flag}`
37549
38015
  ]
37550
38016
  });
37551
38017
  default:
37552
- return new CliError5(refusal.code.toUpperCase(), refusal.message);
38018
+ return new CliError8(refusal.code.toUpperCase(), refusal.message);
37553
38019
  }
37554
38020
  }
37555
38021
  async function readTestPayloadFile(path) {
@@ -37557,7 +38023,7 @@ async function readTestPayloadFile(path) {
37557
38023
  try {
37558
38024
  content = await readFile3(path, "utf8");
37559
38025
  } catch (error62) {
37560
- throw new CliError5(
38026
+ throw new CliError8(
37561
38027
  "PAYLOAD_UNREADABLE",
37562
38028
  `Could not read ${path}: ${getErrorMessage8(error62)}`
37563
38029
  );
@@ -37565,7 +38031,7 @@ async function readTestPayloadFile(path) {
37565
38031
  try {
37566
38032
  return JSON.parse(content);
37567
38033
  } catch (error62) {
37568
- throw new CliError5(
38034
+ throw new CliError8(
37569
38035
  "PAYLOAD_UNREADABLE",
37570
38036
  `${path} is not valid JSON: ${getErrorMessage8(error62)}`
37571
38037
  );
@@ -37754,7 +38220,7 @@ function formatDraftTest(result) {
37754
38220
  }
37755
38221
 
37756
38222
  // src/commands/draft/validate-command.ts
37757
- import { CliError as CliError6 } from "@doist/cli-core";
38223
+ import { CliError as CliError9 } from "@doist/cli-core";
37758
38224
 
37759
38225
  // src/lib/local-command.ts
37760
38226
  async function runLocalCommand(command, operation, presentation, execution = {}) {
@@ -37786,7 +38252,7 @@ function registerDraftValidateCommand(draft) {
37786
38252
  function validateManifestInput(manifest) {
37787
38253
  const validation = validateManifest(manifest);
37788
38254
  if (!validation.isValid) {
37789
- throw new CliError6(
38255
+ throw new CliError9(
37790
38256
  "MANIFEST_INVALID",
37791
38257
  `Manifest is invalid: ${validation.errors.join("; ")}`
37792
38258
  );
@@ -37806,8 +38272,6 @@ function formatManifestValidation(result) {
37806
38272
  }
37807
38273
 
37808
38274
  // src/commands/draft/index.ts
37809
- var DEFAULT_TEST_WAIT = "10m";
37810
- var TOKEN_LIFETIME_MARGIN_MS = 2 * 60 * 1e3;
37811
38275
  function registerDraftCommand(program) {
37812
38276
  const draft = program.command("draft").description("Author, test and inspect automation drafts");
37813
38277
  registerDraftValidateCommand(draft);
@@ -37934,9 +38398,9 @@ function registerDraftCommand(program) {
37934
38398
  );
37935
38399
  });
37936
38400
  addWorkspaceScopeOptions(
37937
- 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(
37938
- "--timeout <duration>",
37939
- `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"
37940
38404
  ).option(
37941
38405
  "--trigger <index|event>",
37942
38406
  "The trigger to test: its position in the manifest's triggers, or a webhook event"
@@ -37947,11 +38411,8 @@ function registerDraftCommand(program) {
37947
38411
  ).action(async (draftId, options, command) => {
37948
38412
  await runApiCommand(
37949
38413
  command,
37950
- async ({ api, signal }) => {
37951
- if (options.timeout !== void 0 && options.wait !== true) {
37952
- throw new Error("--timeout only applies with --wait.");
37953
- }
37954
- const timeoutMs = parseTestTimeout(options.timeout ?? DEFAULT_TEST_WAIT);
38414
+ async ({ api, signal, progress }) => {
38415
+ const wait2 = resolveWait(options);
37955
38416
  const draftApi = bindDraftApi(api, await resolveWorkspaceScope(options), { signal });
37956
38417
  return startDraftTest2(draftApi, {
37957
38418
  draftId,
@@ -37959,14 +38420,15 @@ function registerDraftCommand(program) {
37959
38420
  ...options.trigger !== void 0 ? { trigger: toDraftTestTriggerSelector(options.trigger) } : {},
37960
38421
  ...options.payloadFile !== void 0 ? { payload: await readTestPayloadFile(options.payloadFile) } : {}
37961
38422
  },
37962
- wait: options.wait === true ? { timeoutMs } : null,
37963
- signal
38423
+ wait: wait2,
38424
+ signal,
38425
+ onProgress: (state) => progress({ type: "test_progress", draftId, ...state })
37964
38426
  });
37965
38427
  },
37966
- { human: formatDraftTest },
38428
+ { human: formatDraftTest, ndjson: toTerminalRecords },
37967
38429
  {
37968
38430
  spinner: options.wait === true ? "Testing draft..." : "Starting test run...",
37969
- minimumTokenLifetimeMs: options.wait === true ? tryParseTestTimeout(options.timeout ?? DEFAULT_TEST_WAIT) + TOKEN_LIFETIME_MARGIN_MS : void 0,
38431
+ minimumTokenLifetimeMs: waitTokenLifetimeMs(options),
37970
38432
  exitCode: (result) => result.action === "test_finished" && result.outcome !== "passed" ? 1 : void 0
37971
38433
  }
37972
38434
  );
@@ -38026,20 +38488,6 @@ function streamDraftList(options, fetchPage) {
38026
38488
  async (args) => summarizeDraftListPage(await fetchPage(args))
38027
38489
  );
38028
38490
  }
38029
- function tryParseTestTimeout(value) {
38030
- try {
38031
- return durationStringToMilliseconds(value);
38032
- } catch {
38033
- return 0;
38034
- }
38035
- }
38036
- function parseTestTimeout(value) {
38037
- try {
38038
- return durationStringToMilliseconds(value);
38039
- } catch {
38040
- throw new Error(`Invalid --timeout ${value}; use a duration such as 90s, 15m or 1h.`);
38041
- }
38042
- }
38043
38491
 
38044
38492
  // src/commands/run/index.ts
38045
38493
  import { Option as Option7 } from "commander";
@@ -38135,11 +38583,11 @@ function registerRunCommand(program) {
38135
38583
  }
38136
38584
 
38137
38585
  // src/commands/skill/shared.ts
38138
- import { CliError as CliError7 } from "@doist/cli-core";
38586
+ import { CliError as CliError10 } from "@doist/cli-core";
38139
38587
  function requireInstaller(agent) {
38140
38588
  const installer = getInstaller(agent);
38141
38589
  if (!installer) {
38142
- throw new CliError7("UNKNOWN_AGENT", `Unknown agent: ${agent}.`, {
38590
+ throw new CliError10("UNKNOWN_AGENT", `Unknown agent: ${agent}.`, {
38143
38591
  hints: [`Available agents: ${AGENT_NAMES.join(", ")}.`]
38144
38592
  });
38145
38593
  }
@@ -38217,7 +38665,7 @@ async function uninstallSkill(agent, options, command) {
38217
38665
  }
38218
38666
 
38219
38667
  // src/commands/skill/update.ts
38220
- import { CliError as CliError8 } from "@doist/cli-core";
38668
+ import { CliError as CliError11 } from "@doist/cli-core";
38221
38669
  async function updateSkill(agent, options, command) {
38222
38670
  await runLocalCommand(
38223
38671
  command,
@@ -38243,7 +38691,7 @@ async function updateAllSkills(options, command) {
38243
38691
  async () => {
38244
38692
  const result = await updateAllInstalledSkills(options.local === true);
38245
38693
  if (result.errors.length > 0) {
38246
- throw new CliError8(
38694
+ throw new CliError11(
38247
38695
  "UPDATE_FAILED",
38248
38696
  `Could not update ${result.errors.length} skill file(s).`,
38249
38697
  { hints: result.errors.map((error62) => `${error62.agent}: ${error62.message}`) }
@@ -38303,7 +38751,7 @@ function registerUpdateCommand(program) {
38303
38751
  }
38304
38752
 
38305
38753
  // src/commands/workspace/index.ts
38306
- import { CliError as CliError9 } from "@doist/cli-core";
38754
+ import { CliError as CliError12 } from "@doist/cli-core";
38307
38755
 
38308
38756
  // src/commands/workspace/format.ts
38309
38757
  function formatWorkspaceList(workspaces) {
@@ -38346,7 +38794,7 @@ function registerWorkspaceCommand(program) {
38346
38794
  if (!(error62 instanceof AutomationsRequestError) || error62.httpStatusCode !== 404) {
38347
38795
  throw error62;
38348
38796
  }
38349
- throw new CliError9(
38797
+ throw new CliError12(
38350
38798
  "WORKSPACE_ACCESS_UNAVAILABLE",
38351
38799
  "Workspace access is not available for this account or environment.",
38352
38800
  {