@doist/automations-cli 1.27.0 → 1.28.1

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-3JSM3SAL.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.1",
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;
35230
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
+ }
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,255 @@ 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, ...failure2.code ? { code: failure2.code } : {}, 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
+ import { CliError as CliError4, getErrorMessage as getErrorMessage7 } from "@doist/cli-core";
35409
+ var DEFAULT_WAIT = "10m";
35410
+ var TOKEN_LIFETIME_MARGIN_MS = 2 * 60 * 1e3;
35411
+ function addWaitOptions(command, waitDescription) {
35412
+ return command.option("--wait", waitDescription).option(
35413
+ "--timeout <duration>",
35414
+ `How long --wait waits, such as 90s or 15m (default ${DEFAULT_WAIT})`
35415
+ );
35416
+ }
35417
+ function resolveWait(options) {
35418
+ if (options.timeout !== void 0 && options.wait !== true) {
35419
+ throw new Error("--timeout only applies with --wait.");
35420
+ }
35421
+ if (options.wait !== true) {
35422
+ return null;
35423
+ }
35424
+ return { timeoutMs: parseTimeout(options.timeout ?? DEFAULT_WAIT) };
35425
+ }
35426
+ function waitTokenLifetimeMs(options) {
35427
+ if (options.wait !== true) {
35428
+ return void 0;
35429
+ }
35430
+ try {
35431
+ return parseTimeout(options.timeout ?? DEFAULT_WAIT) + TOKEN_LIFETIME_MARGIN_MS;
35432
+ } catch {
35433
+ return TOKEN_LIFETIME_MARGIN_MS;
35434
+ }
35435
+ }
35436
+ function parseTimeout(value) {
35437
+ try {
35438
+ return durationStringToMilliseconds(value);
35439
+ } catch {
35440
+ throw new Error(`Invalid --timeout ${value}; use a duration such as 90s, 15m or 1h.`);
35441
+ }
35442
+ }
35443
+ function reportRunProgress(report) {
35444
+ let last;
35445
+ return (run) => {
35446
+ const step = run.steps.find((candidate) => candidate.status === "running")?.stepId;
35447
+ const key = `${run.status}:${step ?? ""}`;
35448
+ if (key === last) return;
35449
+ last = key;
35450
+ report({ status: run.status, ...step ? { step } : {} });
35451
+ };
35452
+ }
35453
+ function toRunWaitError(error62, {
35454
+ run,
35455
+ command,
35456
+ codes,
35457
+ signal
35458
+ }) {
35459
+ const hints = [`See how it ends: ${command}`];
35460
+ if (signal.aborted) {
35461
+ return new CliError4(codes.interrupted, `Stopped waiting for ${run}. It keeps running.`, {
35462
+ hints
35463
+ });
35464
+ }
35465
+ return new CliError4(
35466
+ codes.failed,
35467
+ `Checking on ${run} failed: ${getErrorMessage7(error62).replace(/\.?$/, ".")} It keeps running.`,
35468
+ { hints }
35469
+ );
35470
+ }
35471
+ function toTerminalRecords(result) {
35472
+ return [{ type: result.action, ...result, terminal: true }];
35473
+ }
35474
+
35475
+ // src/lib/workspace-scope.ts
35476
+ import { Option } from "commander";
35477
+ async function resolveWorkspaceScope(options) {
35478
+ if (options.workspace !== void 0) {
35479
+ return options.workspace;
35480
+ }
35481
+ if (options.personal) {
35482
+ return null;
35483
+ }
35484
+ return getDefaultWorkspaceId();
35485
+ }
35486
+ function scopeFlag(scope) {
35487
+ return scope === null ? "--personal" : `--workspace ${scope}`;
35488
+ }
35489
+ function addWorkspaceScopeOptions(command) {
35490
+ return command.addOption(
35491
+ new Option(
35492
+ "--workspace <workspace-id>",
35493
+ "Act on this workspace instead of your personal space"
35494
+ ).conflicts("personal")
35495
+ ).option("--personal", "Act on your personal space, ignoring any pinned workspace");
35496
+ }
35497
+
35249
35498
  // src/commands/read/format.ts
35250
35499
  function formatAutomationList(page) {
35251
35500
  if (page.data.length === 0) return "No automations found.";
@@ -35457,8 +35706,35 @@ function describeVisibility(automation) {
35457
35706
  function summarizeAutomationChange(result) {
35458
35707
  return { ...result, automation: summarizeAutomation(result.automation) };
35459
35708
  }
35709
+ var RUN_OUTCOME_LABELS = {
35710
+ completed: "completed",
35711
+ failed: "failed",
35712
+ skipped: "was skipped",
35713
+ cancelled: "was cancelled",
35714
+ timed_out: "did not finish before the timeout"
35715
+ };
35460
35716
  function formatAutomationTrigger(result) {
35461
- return `Triggered automation ${result.automationId}.`;
35717
+ if (result.action === "triggered") {
35718
+ if (result.runId === null) {
35719
+ return `Triggered automation ${result.automationId}.`;
35720
+ }
35721
+ return [
35722
+ `Started run ${result.runId} of automation ${result.automationId}.`,
35723
+ `Details: ${result.command}`
35724
+ ].join("\n");
35725
+ }
35726
+ const lines = [
35727
+ `Run ${result.runId} of automation ${result.automationId} ${RUN_OUTCOME_LABELS[result.outcome]}.`
35728
+ ];
35729
+ if (result.outcome === "timed_out") lines.push(`Status: ${result.status}`);
35730
+ if (result.failedStep) {
35731
+ lines.push(`Failed step: ${result.failedStep.stepId} (${result.failedStep.description})`);
35732
+ }
35733
+ if (result.error)
35734
+ lines.push(`${result.outcome === "skipped" ? "Reason" : "Error"}: ${result.error}`);
35735
+ if (result.result) lines.push(`Result: ${result.result.text}`);
35736
+ lines.push(`Details: ${result.command}`);
35737
+ return lines.join("\n");
35462
35738
  }
35463
35739
  function formatAutomationDelete(result) {
35464
35740
  if (result.action === "delete_preview") {
@@ -35473,6 +35749,66 @@ function capitalize(value) {
35473
35749
  return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
35474
35750
  }
35475
35751
 
35752
+ // src/commands/automation/trigger-service.ts
35753
+ import { CliError as CliError5 } from "@doist/cli-core";
35754
+ async function triggerAutomation(runApi, {
35755
+ automationId,
35756
+ wait: wait2,
35757
+ signal,
35758
+ onProgress
35759
+ }) {
35760
+ const triggerType = readPrimaryTriggerType(await runApi.getAutomation(automationId));
35761
+ if (triggerType !== "schedule") {
35762
+ throw new Error(
35763
+ "This automation can only be triggered via its configured mechanism, not manually."
35764
+ );
35765
+ }
35766
+ const started = await runApi.trigger(automationId);
35767
+ const flag = scopeFlag(runApi.workspaceId);
35768
+ if (!started) {
35769
+ if (wait2) {
35770
+ throw new CliError5(
35771
+ "TRIGGER_RUN_UNREPORTED",
35772
+ "The run started, but the API did not say which one it was, so it cannot be waited for.",
35773
+ { hints: [`See the latest run: tda run list ${automationId} --limit 1 ${flag}`] }
35774
+ );
35775
+ }
35776
+ return { action: "triggered", automationId, runId: null, status: null, command: null };
35777
+ }
35778
+ const identity = {
35779
+ automationId,
35780
+ runId: started.runId,
35781
+ command: `tda run view ${automationId} ${started.runId} ${flag}`
35782
+ };
35783
+ if (!wait2) {
35784
+ return { action: "triggered", ...identity, status: started.status };
35785
+ }
35786
+ let finished;
35787
+ try {
35788
+ finished = await waitForRun(runApi, identity, {
35789
+ ...wait2,
35790
+ signal,
35791
+ ...onProgress ? { onPoll: reportRunProgress(onProgress) } : {}
35792
+ });
35793
+ } catch (error62) {
35794
+ throw toRunWaitError(error62, {
35795
+ run: `run ${identity.runId} of automation ${automationId}`,
35796
+ command: identity.command,
35797
+ codes: { interrupted: "TRIGGER_WAIT_INTERRUPTED", failed: "TRIGGER_WAIT_FAILED" },
35798
+ signal
35799
+ });
35800
+ }
35801
+ return {
35802
+ action: "run_finished",
35803
+ ...identity,
35804
+ outcome: finished.outcome,
35805
+ status: finished.status,
35806
+ failedStep: finished.failedStep,
35807
+ error: finished.error,
35808
+ result: finished.result
35809
+ };
35810
+ }
35811
+
35476
35812
  // src/commands/automation/index.ts
35477
35813
  function registerAutomationCommand(program) {
35478
35814
  const automation = program.command("automation").description("Inspect and manage published automations");
@@ -35522,30 +35858,29 @@ function registerAutomationCommand(program) {
35522
35858
  registerEnabledStateCommand(automation, "enable", true);
35523
35859
  registerEnabledStateCommand(automation, "disable", false);
35524
35860
  addWorkspaceScopeOptions(
35525
- automation.command("trigger").description("Start a live automation run without waiting for it to finish").argument("<automation-id>", "Automation ID")
35861
+ addWaitOptions(
35862
+ automation.command("trigger").description("Start a live automation run and print its run ID").argument("<automation-id>", "Automation ID"),
35863
+ "Wait for the run to end and exit 1 unless it completed"
35864
+ )
35526
35865
  ).action(async (automationId, options, command) => {
35527
35866
  await runApiCommand(
35528
35867
  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
35868
+ async ({ api, signal, progress }) => {
35869
+ const wait2 = resolveWait(options);
35870
+ const runApi = bindRunApi(api, await resolveWorkspaceScope(options), { signal });
35871
+ return triggerAutomation(runApi, {
35872
+ automationId,
35873
+ wait: wait2,
35874
+ signal,
35875
+ onProgress: (state) => progress({ type: "run_progress", automationId, ...state })
35533
35876
  });
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
35877
  },
35547
- { human: formatAutomationTrigger },
35548
- { spinner: "Triggering automation..." }
35878
+ { human: formatAutomationTrigger, ndjson: toTerminalRecords },
35879
+ {
35880
+ spinner: options.wait === true ? "Running automation..." : "Triggering automation...",
35881
+ minimumTokenLifetimeMs: waitTokenLifetimeMs(options),
35882
+ exitCode: (result) => result.action === "run_finished" && result.outcome !== "completed" ? 1 : void 0
35883
+ }
35549
35884
  );
35550
35885
  });
35551
35886
  addWorkspaceScopeOptions(
@@ -35802,6 +36137,7 @@ function collectOption(value, previous) {
35802
36137
  }
35803
36138
 
35804
36139
  // src/commands/chat/chat-service.ts
36140
+ import { CliError as CliError7 } from "@doist/cli-core";
35805
36141
  import { createInterface } from "node:readline/promises";
35806
36142
 
35807
36143
  // src/commands/chat/connection-authorizer.ts
@@ -35816,6 +36152,13 @@ var FINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
35816
36152
  "session_status",
35817
36153
  "turn_completed"
35818
36154
  ]);
36155
+ var UNRECORDED_EVENT_TYPES = /* @__PURE__ */ new Set([
36156
+ "assistant_text",
36157
+ "connection_authorization_waiting",
36158
+ "turn_progress"
36159
+ ]);
36160
+ var INTERRUPTING_EVENT_TYPES = /* @__PURE__ */ new Set(["error", "interrupted"]);
36161
+ var HEARTBEAT_EVENT_TYPES = /* @__PURE__ */ new Set(["connection_authorization_waiting", "turn_progress"]);
35819
36162
  var SPINNER_TEXT = {
35820
36163
  connection_authorization_opened: "Waiting for authorization...",
35821
36164
  connection_authorization_started: "Waiting for authorization...",
@@ -35826,16 +36169,47 @@ var OutputWriter = class {
35826
36169
  stream;
35827
36170
  diagnosticStream;
35828
36171
  events = [];
36172
+ streamText;
35829
36173
  assistantLineOpen = false;
35830
36174
  currentConversationId;
35831
- constructor(mode, stream = process.stdout, diagnosticStream = process.stderr) {
36175
+ /** The current turn's text, one entry per step. */
36176
+ turnSteps = [];
36177
+ stepOpen = false;
36178
+ bufferedText = "";
36179
+ bufferedTextConversationId;
36180
+ constructor(mode, stream = process.stdout, diagnosticStream = process.stderr, { streamText = false } = {}) {
35832
36181
  this.mode = mode;
35833
36182
  this.stream = stream;
35834
36183
  this.diagnosticStream = diagnosticStream;
36184
+ this.streamText = streamText;
36185
+ }
36186
+ /**
36187
+ * Assistant text as it arrives. A human reader, or an NDJSON caller that
36188
+ * asked with `--stream-text`, gets each fragment as `assistant_text`;
36189
+ * otherwise the text of a step comes out whole as one `assistant_message`
36190
+ * before whatever follows it.
36191
+ */
36192
+ appendAssistantText(conversationId, text2) {
36193
+ if (!this.stepOpen) this.turnSteps.push("");
36194
+ this.stepOpen = true;
36195
+ this.turnSteps[this.turnSteps.length - 1] += text2;
36196
+ if (this.mode === "human" || this.mode === "ndjson" && this.streamText) {
36197
+ this.emit({ type: "assistant_text", conversationId, text: text2 });
36198
+ return;
36199
+ }
36200
+ this.bufferedText += text2;
36201
+ this.bufferedTextConversationId = conversationId;
35835
36202
  }
35836
36203
  /** Records and renders an event; JSON mode retains structured output until `finish`. */
35837
36204
  emit(event) {
35838
- if (this.mode === "json") this.events.push(event);
36205
+ const endsStep = event.type !== "assistant_message" && event.type !== "assistant_text" && !HEARTBEAT_EVENT_TYPES.has(event.type);
36206
+ if (endsStep) {
36207
+ this.flushAssistantMessage(INTERRUPTING_EVENT_TYPES.has(event.type));
36208
+ this.stepOpen = false;
36209
+ }
36210
+ if (this.mode === "json" && !UNRECORDED_EVENT_TYPES.has(event.type)) {
36211
+ this.events.push(event);
36212
+ }
35839
36213
  if (event.conversationId) this.currentConversationId = event.conversationId;
35840
36214
  if (this.mode === "ndjson") {
35841
36215
  this.stream.write(`${formatNdjson5([event])}
@@ -35851,17 +36225,28 @@ var OutputWriter = class {
35851
36225
  get conversationId() {
35852
36226
  return this.currentConversationId;
35853
36227
  }
36228
+ /** The current turn's text so far, its steps separated by blank lines. */
36229
+ get assistantText() {
36230
+ return this.turnSteps.join("\n\n");
36231
+ }
36232
+ /** Starts collecting the text of a new turn. */
36233
+ startTurn() {
36234
+ this.turnSteps = [];
36235
+ this.stepOpen = false;
36236
+ }
35854
36237
  /** Emits structured conversation status with human formatting owned by this output layer. */
35855
36238
  emitConversationStatus(snapshot, pendingActionKind) {
35856
36239
  this.emit({
35857
36240
  type: "session_status",
35858
36241
  conversationId: snapshot.conversation.id,
35859
36242
  ...snapshot,
35860
- humanText: formatConversationStatus(snapshot, pendingActionKind)
36243
+ humanText: formatConversationStatus(snapshot, pendingActionKind),
36244
+ terminal: true
35861
36245
  });
35862
36246
  }
35863
36247
  /** Finalizes output, including the accumulated event list in JSON mode. */
35864
36248
  finish(summary) {
36249
+ this.flushAssistantMessage(false);
35865
36250
  stopEarlySpinner();
35866
36251
  if (this.mode === "json") {
35867
36252
  this.stream.write(`${formatJson5({ ...summary, events: this.events })}
@@ -35873,6 +36258,17 @@ var OutputWriter = class {
35873
36258
  this.assistantLineOpen = false;
35874
36259
  }
35875
36260
  }
36261
+ flushAssistantMessage(partial2) {
36262
+ if (!this.bufferedText) return;
36263
+ const text2 = this.bufferedText;
36264
+ this.bufferedText = "";
36265
+ this.emit({
36266
+ type: "assistant_message",
36267
+ conversationId: this.bufferedTextConversationId,
36268
+ text: text2,
36269
+ ...partial2 ? { partial: true } : {}
36270
+ });
36271
+ }
35876
36272
  /**
35877
36273
  * The agent goes quiet for long stretches between events, so a spinner
35878
36274
  * fills the gap. Taking it down before every write keeps its next frame
@@ -36096,6 +36492,7 @@ function getMessageRoleLabel(role) {
36096
36492
 
36097
36493
  // src/commands/chat/connection-authorizer.ts
36098
36494
  var CONNECTION_POLL_INTERVAL_MS = 2e3;
36495
+ var CONNECTION_WAITING_REPORT_INTERVAL_MS = 15e3;
36099
36496
  var CONNECTION_AUTHORIZATION_TIMEOUT_MS = 10 * 60 * 1e3;
36100
36497
  var ApiConnectionAuthorizer = class {
36101
36498
  connections;
@@ -36187,6 +36584,7 @@ var ApiConnectionAuthorizer = class {
36187
36584
  }
36188
36585
  async waitUntilAuthorized(request2, connectionId, signal) {
36189
36586
  const startedAt = Date.now();
36587
+ let reportedAt;
36190
36588
  while (true) {
36191
36589
  if (signal?.aborted) throw new Error("Connection authorization interrupted.");
36192
36590
  const connection = await this.connections.get(connectionId, { signal });
@@ -36214,16 +36612,20 @@ var ApiConnectionAuthorizer = class {
36214
36612
  `Timed out waiting for ${request2.serviceName} connection authorization.`
36215
36613
  );
36216
36614
  }
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
- });
36615
+ const now = Date.now();
36616
+ if (reportedAt === void 0 || now - reportedAt >= CONNECTION_WAITING_REPORT_INTERVAL_MS) {
36617
+ reportedAt = now;
36618
+ this.output.emit({
36619
+ type: "connection_authorization_waiting",
36620
+ conversationId: request2.conversationId,
36621
+ connectionId,
36622
+ connectorId: request2.connectorId,
36623
+ serviceName: request2.serviceName,
36624
+ elapsedSeconds: Math.floor(elapsedMs / 1e3),
36625
+ state: "waiting_for_user",
36626
+ terminal: false
36627
+ });
36628
+ }
36227
36629
  await wait(Math.min(this.pollIntervalMs, this.timeoutMs - elapsedMs), void 0, {
36228
36630
  signal
36229
36631
  });
@@ -36238,11 +36640,48 @@ function isSafeSetupUrl(value) {
36238
36640
  }
36239
36641
  }
36240
36642
 
36643
+ // src/commands/chat/chat-failure.ts
36644
+ var SELECTION_CODES = /* @__PURE__ */ new Set([
36645
+ "CHAT_AWAITING_SELECTION",
36646
+ "CHAT_AMBIGUOUS_SELECTION",
36647
+ "CHAT_UNKNOWN_CONNECTION"
36648
+ ]);
36649
+ function chatFailureCode(error62) {
36650
+ switch (classifyChatTurnFailure(error62)) {
36651
+ case "broken":
36652
+ return "CHAT_TURN_FAILED";
36653
+ case "refused":
36654
+ return "CHAT_TURN_REJECTED";
36655
+ default:
36656
+ return FALLBACK_FAILURE_CODE;
36657
+ }
36658
+ }
36659
+ function messageResumeCommand(conversationId) {
36660
+ return `tda chat continue ${conversationId} "<message>"`;
36661
+ }
36662
+ function resumeCommandFor(conversationId, code) {
36663
+ if (code === "CHAT_TURN_ACTIVE") return void 0;
36664
+ if (code === "CHAT_AWAITING_PUSH") return `tda chat continue ${conversationId}`;
36665
+ if (SELECTION_CODES.has(code)) {
36666
+ return `tda chat continue ${conversationId} --connection <connector[:type]=connection-id>`;
36667
+ }
36668
+ return messageResumeCommand(conversationId);
36669
+ }
36670
+ function formatChatFailureDetails(details) {
36671
+ return [
36672
+ `Conversation: ${details.conversationId}`,
36673
+ ...details.requestId ? [`Request ID: ${details.requestId}`] : [],
36674
+ ...details.draftCommand ? [`Its draft is saved: ${details.draftCommand}`] : [],
36675
+ ...details.resumeCommand ? [`Resume with: ${details.resumeCommand}`] : []
36676
+ ];
36677
+ }
36678
+
36241
36679
  // src/commands/chat/conversation-turn-lock.ts
36242
36680
  import { link, mkdir, readFile, rm, writeFile } from "node:fs/promises";
36243
36681
  import { tmpdir } from "node:os";
36244
36682
  import { join } from "node:path";
36245
36683
  import { randomUUID } from "node:crypto";
36684
+ import { CliError as CliError6 } from "@doist/cli-core";
36246
36685
  var ConversationTurnLock = class {
36247
36686
  directory;
36248
36687
  pid;
@@ -36279,7 +36718,8 @@ var ConversationTurnLock = class {
36279
36718
  }
36280
36719
  const current = await readLock(path);
36281
36720
  if (current && this.isProcessRunning(current.pid)) {
36282
- throw new Error(
36721
+ throw new CliError6(
36722
+ "CHAT_TURN_ACTIVE",
36283
36723
  `Conversation ${conversationId} already has an active turn in process ${current.pid}. Continue monitoring that process; do not invoke continue until it exits.`
36284
36724
  );
36285
36725
  }
@@ -36326,7 +36766,8 @@ function isNoSuchProcessError(error62) {
36326
36766
 
36327
36767
  // src/commands/chat/chat-service.ts
36328
36768
  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.";
36769
+ var DRAFT_LOOKUP_TIMEOUT_MS = 5e3;
36770
+ 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
36771
  var ChatService = class {
36331
36772
  api;
36332
36773
  transport;
@@ -36336,6 +36777,8 @@ var ChatService = class {
36336
36777
  turnLock;
36337
36778
  heartbeatIntervalMs;
36338
36779
  readline;
36780
+ /** The conversation a start or continue is working on, once it is known. */
36781
+ failureScope;
36339
36782
  constructor({
36340
36783
  token,
36341
36784
  baseUrl,
@@ -36366,6 +36809,10 @@ var ChatService = class {
36366
36809
  */
36367
36810
  async startConversation(message, signal, workspaceId = null) {
36368
36811
  const conversation = workspaceId === null ? await this.api.chat.createConversation({}, { signal }) : await this.api.workspaceChat.createConversation(workspaceId, {}, { signal });
36812
+ this.failureScope = {
36813
+ conversationId: conversation.id,
36814
+ workspaceId: conversation.workspaceId
36815
+ };
36369
36816
  this.output.emit({ type: "session_started", conversationId: conversation.id });
36370
36817
  return this.withActiveTurn(
36371
36818
  conversation.id,
@@ -36384,8 +36831,10 @@ var ChatService = class {
36384
36831
  selections,
36385
36832
  signal
36386
36833
  }) {
36834
+ this.failureScope = { conversationId };
36387
36835
  const conversation = await this.api.chat.getConversation(conversationId, { signal });
36388
36836
  const scope = { conversationId, workspaceId: conversation.workspaceId };
36837
+ this.failureScope = scope;
36389
36838
  return this.withActiveTurn(
36390
36839
  conversationId,
36391
36840
  async () => {
@@ -36398,7 +36847,8 @@ var ChatService = class {
36398
36847
  } else {
36399
36848
  const nextMessage = this.interactive ? await this.promptForMessage(signal) : "";
36400
36849
  if (!nextMessage) {
36401
- throw new Error(
36850
+ throw new CliError7(
36851
+ "CHAT_INPUT_REQUIRED",
36402
36852
  "A message or --connection selection is required to continue."
36403
36853
  );
36404
36854
  }
@@ -36504,6 +36954,7 @@ var ChatService = class {
36504
36954
  }
36505
36955
  }
36506
36956
  async runTurn({ conversationId, workspaceId }, input2, signal, pendingAction, { markActivity }) {
36957
+ this.output.startTurn();
36507
36958
  try {
36508
36959
  return await runConversationTurn({
36509
36960
  api: this.api,
@@ -36515,7 +36966,7 @@ var ChatService = class {
36515
36966
  signal,
36516
36967
  onText: (text2) => {
36517
36968
  markActivity();
36518
- this.output.emit({ type: "assistant_text", conversationId, text: text2 });
36969
+ this.output.appendAssistantText(conversationId, text2);
36519
36970
  },
36520
36971
  onToolActivity: (activity) => {
36521
36972
  markActivity();
@@ -36537,7 +36988,7 @@ var ChatService = class {
36537
36988
  });
36538
36989
  } catch (error62) {
36539
36990
  if (error62 instanceof ChatTurnInputError) {
36540
- throw new Error(describeInputError(error62));
36991
+ throw new CliError7(`CHAT_${error62.code.toUpperCase()}`, describeInputError(error62));
36541
36992
  }
36542
36993
  throw error62;
36543
36994
  }
@@ -36561,13 +37012,17 @@ var ChatService = class {
36561
37012
  terminal: false
36562
37013
  });
36563
37014
  }
36564
- completeTurn(summary) {
37015
+ completeTurn(turn) {
37016
+ const summary = { ...turn, assistantText: this.output.assistantText || turn.assistantText };
36565
37017
  const resumeCommand = getResumeCommand(summary);
36566
37018
  this.output.emit({
36567
37019
  type: "turn_completed",
36568
37020
  conversationId: summary.conversationId,
36569
37021
  status: summary.status,
36570
37022
  outcome: summary.status,
37023
+ assistantText: summary.assistantText,
37024
+ ...summary.automationId ? { automationId: summary.automationId } : {},
37025
+ ...summary.action ? { action: summary.action } : {},
36571
37026
  state: "completed",
36572
37027
  terminal: true,
36573
37028
  ...summary.conversationArchived !== void 0 ? { conversationArchived: summary.conversationArchived } : {},
@@ -36608,6 +37063,32 @@ var ChatService = class {
36608
37063
  await release();
36609
37064
  }
36610
37065
  }
37066
+ /** Whether a start or continue has a conversation its failure can name. */
37067
+ get hasFailureScope() {
37068
+ return this.failureScope !== void 0;
37069
+ }
37070
+ /**
37071
+ * What a caller needs to pick up a conversation whose start or continue
37072
+ * failed, or undefined before one was known. The draft is looked up only for
37073
+ * a turn that broke, not one refused for its input, credentials or balance.
37074
+ */
37075
+ async describeFailure(error62, code) {
37076
+ const scope = this.failureScope;
37077
+ if (!scope) return void 0;
37078
+ const { conversationId, workspaceId } = scope;
37079
+ const draftId = workspaceId !== void 0 && classifyChatTurnFailure(error62) === "broken" ? await findConversationDraftId(this.api, {
37080
+ conversationId,
37081
+ workspaceId,
37082
+ signal: AbortSignal.timeout(DRAFT_LOOKUP_TIMEOUT_MS)
37083
+ }).catch(() => null) : null;
37084
+ const resumeCommand = resumeCommandFor(conversationId, code);
37085
+ return {
37086
+ conversationId,
37087
+ ...readFailedRequest(error62),
37088
+ ...draftId && workspaceId !== void 0 ? { draftId, draftCommand: `tda draft view ${draftId} ${scopeFlag(workspaceId)}` } : {},
37089
+ ...resumeCommand ? { resumeCommand } : {}
37090
+ };
37091
+ }
36611
37092
  async getPersistedPendingAction(conversationId, signal) {
36612
37093
  const messages = await this.api.chat.getMessages(conversationId, { signal });
36613
37094
  return getPendingActionFromMessages(messages);
@@ -36694,21 +37175,25 @@ function getResumeCommand(summary) {
36694
37175
  if (typeof actionResume === "string") {
36695
37176
  return actionResume.replace("<conversation-id>", summary.conversationId);
36696
37177
  }
36697
- return `tda chat continue ${summary.conversationId} "<message>"`;
37178
+ return messageResumeCommand(summary.conversationId);
36698
37179
  }
36699
37180
 
36700
37181
  // src/commands/chat/index.ts
36701
37182
  function registerChatCommand(program) {
36702
37183
  const chat = program.command("chat").description("Talk to the Todoist Automations agent");
36703
37184
  addWorkspaceScopeOptions(
36704
- chat.command("start").description("Start a new conversation").argument("<message...>", "Initial message")
37185
+ addStreamTextOption(
37186
+ chat.command("start").description("Start a new conversation").argument("<message...>", "Initial message")
37187
+ )
36705
37188
  ).action(async (messageParts, options, command) => {
36706
37189
  await runCommand(command, async ({ service, signal }) => {
36707
37190
  const workspaceId = await resolveWorkspaceScope(options);
36708
37191
  return service.startConversation(messageParts.join(" "), signal, workspaceId);
36709
37192
  });
36710
37193
  });
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(
37194
+ addStreamTextOption(
37195
+ chat.command("continue").description("Continue a persisted conversation").argument("<conversation-id>", "Conversation ID emitted by start").argument("[message...]", "Answer or follow-up message")
37196
+ ).option(
36712
37197
  "--connection <connector[:type]=connection-id>",
36713
37198
  "Resolve a pending connection selection; repeat for multiple connectors",
36714
37199
  collectOption,
@@ -36733,11 +37218,19 @@ function registerChatCommand(program) {
36733
37218
  );
36734
37219
  });
36735
37220
  }
37221
+ function addStreamTextOption(command) {
37222
+ return command.option(
37223
+ "--stream-text",
37224
+ "Under --ndjson, emit the assistant text as it arrives (assistant_text) rather than one assistant_message per step"
37225
+ );
37226
+ }
36736
37227
  async function runCommand(command, operation) {
36737
37228
  let service;
36738
37229
  let source;
36739
37230
  await runStreamingCommand(command, {
36740
- createOutput: (mode) => new OutputWriter(mode),
37231
+ createOutput: (mode) => new OutputWriter(mode, process.stdout, process.stderr, {
37232
+ streamText: command.opts().streamText === true
37233
+ }),
36741
37234
  operation: async ({ options, mode, output: output2, signal }) => {
36742
37235
  const apiConfiguration = await withSpinner(
36743
37236
  { text: "Connecting..." },
@@ -36760,33 +37253,33 @@ async function runCommand(command, operation) {
36760
37253
  onSuccess: (result, { output: output2 }) => output2.finish(result),
36761
37254
  onInterrupted: ({ output: output2 }) => {
36762
37255
  const conversationId = output2.conversationId;
36763
- output2.emit({ type: "interrupted", conversationId });
37256
+ const resume = conversationId ? { resumeCommand: messageResumeCommand(conversationId) } : {};
36764
37257
  output2.emit({
36765
- type: "turn_completed",
37258
+ type: "interrupted",
36766
37259
  conversationId,
36767
37260
  status: "interrupted",
36768
- outcome: "interrupted",
36769
- state: "completed",
36770
- terminal: true,
36771
- ...conversationId ? { resumeCommand: `tda chat continue ${conversationId} "<message>"` } : {}
37261
+ message: "Interrupted.",
37262
+ ...resume,
37263
+ terminal: true
36772
37264
  });
36773
37265
  output2.finish({
36774
37266
  conversationId,
36775
37267
  status: "interrupted",
36776
- assistantText: ""
37268
+ assistantText: output2.assistantText,
37269
+ ...resume
36777
37270
  });
36778
37271
  },
36779
- onError: (error62, { mode, output: output2 }) => {
37272
+ onError: async (error62, { mode, output: output2 }) => {
36780
37273
  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 });
37274
+ const code = failure2.code ?? (service?.hasFailureScope ? chatFailureCode(error62) : FALLBACK_FAILURE_CODE);
37275
+ const details = await service?.describeFailure(error62, code);
37276
+ const message = mode === "human" ? [
37277
+ failure2.humanMessage,
37278
+ ...details ? formatChatFailureDetails(details) : []
37279
+ ].join("\n") : failure2.message;
37280
+ const record2 = toFailureRecord({ code, message }, details);
37281
+ output2.emit({ type: "error", ...record2, state: "failed", terminal: true });
37282
+ output2.finish(record2);
36790
37283
  },
36791
37284
  onFinally: () => {
36792
37285
  stopEarlySpinner();
@@ -36972,7 +37465,12 @@ async function runAuthorizationCommand(command, operation) {
36972
37465
  onSuccess: (result, { mode, output: output2 }) => {
36973
37466
  if (mode === "json") return output2.finish(result);
36974
37467
  stopEarlySpinner();
36975
- writeResult(mode, result, { human: formatConnectionChange });
37468
+ writeResult(mode, result, {
37469
+ human: formatConnectionChange,
37470
+ ndjson: (completed) => [
37471
+ { type: "connection_completed", ...completed, terminal: true }
37472
+ ]
37473
+ });
36976
37474
  },
36977
37475
  onInterrupted: ({ output: output2 }) => {
36978
37476
  output2.emit({
@@ -36986,14 +37484,9 @@ async function runAuthorizationCommand(command, operation) {
36986
37484
  onError: (error62, { mode, output: output2 }) => {
36987
37485
  const failure2 = renderFailure(error62, { mode, source });
36988
37486
  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 });
37487
+ const record2 = toFailureRecord({ code: failure2.code, message });
37488
+ output2.emit({ type: "error", ...record2, terminal: true });
37489
+ output2.finish(record2);
36997
37490
  },
36998
37491
  onFinally: () => stopEarlySpinner()
36999
37492
  });
@@ -37366,7 +37859,7 @@ function registerConnectorCommand(program) {
37366
37859
  }
37367
37860
 
37368
37861
  // src/lib/manifest-input.ts
37369
- import { CliError as CliError4, getErrorMessage as getErrorMessage7 } from "@doist/cli-core";
37862
+ import { CliError as CliError8, getErrorMessage as getErrorMessage8 } from "@doist/cli-core";
37370
37863
  import { Option as Option6 } from "commander";
37371
37864
  import { readFile as readFile2 } from "node:fs/promises";
37372
37865
  import { text } from "node:stream/consumers";
@@ -37380,9 +37873,9 @@ async function readManifestInput(options, input2 = process.stdin) {
37380
37873
  try {
37381
37874
  return JSON.parse(source.content);
37382
37875
  } catch (error62) {
37383
- throw new CliError4(
37876
+ throw new CliError8(
37384
37877
  "MANIFEST_UNREADABLE",
37385
- `${source.label} is not valid JSON: ${getErrorMessage7(error62)}`
37878
+ `${source.label} is not valid JSON: ${getErrorMessage8(error62)}`
37386
37879
  );
37387
37880
  }
37388
37881
  }
@@ -37391,9 +37884,9 @@ async function readSource(options, input2) {
37391
37884
  try {
37392
37885
  return { label: options.file, content: await readFile2(options.file, "utf8") };
37393
37886
  } catch (error62) {
37394
- throw new CliError4(
37887
+ throw new CliError8(
37395
37888
  "MANIFEST_UNREADABLE",
37396
- `Could not read ${options.file}: ${getErrorMessage7(error62)}`
37889
+ `Could not read ${options.file}: ${getErrorMessage8(error62)}`
37397
37890
  );
37398
37891
  }
37399
37892
  }
@@ -37401,17 +37894,17 @@ async function readSource(options, input2) {
37401
37894
  try {
37402
37895
  return { label: "Standard input", content: await text(input2) };
37403
37896
  } catch (error62) {
37404
- throw new CliError4(
37897
+ throw new CliError8(
37405
37898
  "MANIFEST_UNREADABLE",
37406
- `Could not read standard input: ${getErrorMessage7(error62)}`
37899
+ `Could not read standard input: ${getErrorMessage8(error62)}`
37407
37900
  );
37408
37901
  }
37409
37902
  }
37410
- throw new CliError4("MANIFEST_UNREADABLE", "Provide the manifest with --file <path> or --stdin.");
37903
+ throw new CliError8("MANIFEST_UNREADABLE", "Provide the manifest with --file <path> or --stdin.");
37411
37904
  }
37412
37905
 
37413
37906
  // src/commands/draft/draft-service.ts
37414
- import { CliError as CliError5, getErrorMessage as getErrorMessage8 } from "@doist/cli-core";
37907
+ import { CliError as CliError9, getErrorMessage as getErrorMessage9 } from "@doist/cli-core";
37415
37908
  import { readFile as readFile3 } from "node:fs/promises";
37416
37909
  function createDraft2(draftApi, input2) {
37417
37910
  return withRecoveryHints(draftApi, createDraft(draftApi, input2));
@@ -37444,13 +37937,13 @@ async function withRecoveryHints(draftApi, operation) {
37444
37937
  const draftId = error62.draftId ?? "<draft-id>";
37445
37938
  switch (error62.code) {
37446
37939
  case "manifest_invalid":
37447
- throw new CliError5("MANIFEST_INVALID", error62.message);
37940
+ throw new CliError9("MANIFEST_INVALID", error62.message);
37448
37941
  case "draft_conflict":
37449
- throw new CliError5("DRAFT_CONFLICT", error62.message, {
37942
+ throw new CliError9("DRAFT_CONFLICT", error62.message, {
37450
37943
  hints: draftHints(scope, draftId)
37451
37944
  });
37452
37945
  case "draft_manifest_rejected":
37453
- throw new CliError5("DRAFT_MANIFEST_REJECTED", error62.message, {
37946
+ throw new CliError9("DRAFT_MANIFEST_REJECTED", error62.message, {
37454
37947
  hints: draftHints(scope, draftId)
37455
37948
  });
37456
37949
  case "draft_not_ready":
@@ -37468,14 +37961,12 @@ function draftHints(scope, draftId) {
37468
37961
  `Or discard it first: tda draft delete ${draftId} --yes ${scopeFlag(scope)}`
37469
37962
  ];
37470
37963
  }
37471
- function scopeFlag(scope) {
37472
- return scope === null ? "--personal" : `--workspace ${scope}`;
37473
- }
37474
37964
  async function startDraftTest2(draftApi, {
37475
37965
  draftId,
37476
37966
  request: request2,
37477
37967
  wait: waitFor,
37478
- signal
37968
+ signal,
37969
+ onProgress
37479
37970
  }) {
37480
37971
  let started;
37481
37972
  try {
@@ -37497,9 +37988,21 @@ async function startDraftTest2(draftApi, {
37497
37988
  }
37498
37989
  let finished;
37499
37990
  try {
37500
- finished = await waitForDraftTest(draftApi, started, { ...waitFor, signal });
37991
+ finished = await waitForDraftTest(draftApi, started, {
37992
+ ...waitFor,
37993
+ signal,
37994
+ ...onProgress ? { onPoll: reportRunProgress(onProgress) } : {}
37995
+ });
37501
37996
  } catch (error62) {
37502
- throw toDraftTestWaitError(error62, identity, signal);
37997
+ throw toRunWaitError(error62, {
37998
+ run: `test run ${identity.testRunId} of draft ${identity.draftId}`,
37999
+ command: identity.command,
38000
+ codes: {
38001
+ interrupted: "DRAFT_TEST_WAIT_INTERRUPTED",
38002
+ failed: "DRAFT_TEST_WAIT_FAILED"
38003
+ },
38004
+ signal
38005
+ });
37503
38006
  }
37504
38007
  return {
37505
38008
  action: "test_finished",
@@ -37511,25 +38014,9 @@ async function startDraftTest2(draftApi, {
37511
38014
  result: finished.result
37512
38015
  };
37513
38016
  }
37514
- function toDraftTestWaitError(error62, identity, signal) {
37515
- const run = `test run ${identity.testRunId} of draft ${identity.draftId}`;
37516
- const hints = [`See how it ends: ${identity.command}`];
37517
- if (signal.aborted) {
37518
- return new CliError5(
37519
- "DRAFT_TEST_WAIT_INTERRUPTED",
37520
- `Stopped waiting for ${run}. It keeps running.`,
37521
- { hints }
37522
- );
37523
- }
37524
- return new CliError5(
37525
- "DRAFT_TEST_WAIT_FAILED",
37526
- `Checking on ${run} failed: ${getErrorMessage8(error62).replace(/\.?$/, ".")} It keeps running.`,
37527
- { hints }
37528
- );
37529
- }
37530
38017
  function draftNotReadyError(message, draftId, scope) {
37531
38018
  const flag = scopeFlag(scope);
37532
- return new CliError5("DRAFT_NOT_READY", message, {
38019
+ return new CliError9("DRAFT_NOT_READY", message, {
37533
38020
  hints: [
37534
38021
  `See the issues: tda draft view ${draftId} ${flag}`,
37535
38022
  `Bind connections: tda draft connect ${draftId} --auto ${flag}`
@@ -37543,13 +38030,13 @@ function toDraftTestCliError(refusal, draftId, scope) {
37543
38030
  return draftNotReadyError(refusal.message, draftId, scope);
37544
38031
  case "draft_test_payload_required":
37545
38032
  case "draft_test_payload_filtered":
37546
- return new CliError5(refusal.code.toUpperCase(), refusal.message, {
38033
+ return new CliError9(refusal.code.toUpperCase(), refusal.message, {
37547
38034
  hints: [
37548
38035
  `Test with a real event: tda draft test ${draftId} --trigger <event> --payload-file <path> ${flag}`
37549
38036
  ]
37550
38037
  });
37551
38038
  default:
37552
- return new CliError5(refusal.code.toUpperCase(), refusal.message);
38039
+ return new CliError9(refusal.code.toUpperCase(), refusal.message);
37553
38040
  }
37554
38041
  }
37555
38042
  async function readTestPayloadFile(path) {
@@ -37557,17 +38044,17 @@ async function readTestPayloadFile(path) {
37557
38044
  try {
37558
38045
  content = await readFile3(path, "utf8");
37559
38046
  } catch (error62) {
37560
- throw new CliError5(
38047
+ throw new CliError9(
37561
38048
  "PAYLOAD_UNREADABLE",
37562
- `Could not read ${path}: ${getErrorMessage8(error62)}`
38049
+ `Could not read ${path}: ${getErrorMessage9(error62)}`
37563
38050
  );
37564
38051
  }
37565
38052
  try {
37566
38053
  return JSON.parse(content);
37567
38054
  } catch (error62) {
37568
- throw new CliError5(
38055
+ throw new CliError9(
37569
38056
  "PAYLOAD_UNREADABLE",
37570
- `${path} is not valid JSON: ${getErrorMessage8(error62)}`
38057
+ `${path} is not valid JSON: ${getErrorMessage9(error62)}`
37571
38058
  );
37572
38059
  }
37573
38060
  }
@@ -37754,7 +38241,7 @@ function formatDraftTest(result) {
37754
38241
  }
37755
38242
 
37756
38243
  // src/commands/draft/validate-command.ts
37757
- import { CliError as CliError6 } from "@doist/cli-core";
38244
+ import { CliError as CliError10 } from "@doist/cli-core";
37758
38245
 
37759
38246
  // src/lib/local-command.ts
37760
38247
  async function runLocalCommand(command, operation, presentation, execution = {}) {
@@ -37786,7 +38273,7 @@ function registerDraftValidateCommand(draft) {
37786
38273
  function validateManifestInput(manifest) {
37787
38274
  const validation = validateManifest(manifest);
37788
38275
  if (!validation.isValid) {
37789
- throw new CliError6(
38276
+ throw new CliError10(
37790
38277
  "MANIFEST_INVALID",
37791
38278
  `Manifest is invalid: ${validation.errors.join("; ")}`
37792
38279
  );
@@ -37806,8 +38293,6 @@ function formatManifestValidation(result) {
37806
38293
  }
37807
38294
 
37808
38295
  // src/commands/draft/index.ts
37809
- var DEFAULT_TEST_WAIT = "10m";
37810
- var TOKEN_LIFETIME_MARGIN_MS = 2 * 60 * 1e3;
37811
38296
  function registerDraftCommand(program) {
37812
38297
  const draft = program.command("draft").description("Author, test and inspect automation drafts");
37813
38298
  registerDraftValidateCommand(draft);
@@ -37934,9 +38419,9 @@ function registerDraftCommand(program) {
37934
38419
  );
37935
38420
  });
37936
38421
  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})`
38422
+ addWaitOptions(
38423
+ draft.command("test").description("Start a test run of a draft, with mocked connector calls").argument("<draft-id>", "Draft ID"),
38424
+ "Wait for the run to end and exit 1 unless it passed"
37940
38425
  ).option(
37941
38426
  "--trigger <index|event>",
37942
38427
  "The trigger to test: its position in the manifest's triggers, or a webhook event"
@@ -37947,11 +38432,8 @@ function registerDraftCommand(program) {
37947
38432
  ).action(async (draftId, options, command) => {
37948
38433
  await runApiCommand(
37949
38434
  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);
38435
+ async ({ api, signal, progress }) => {
38436
+ const wait2 = resolveWait(options);
37955
38437
  const draftApi = bindDraftApi(api, await resolveWorkspaceScope(options), { signal });
37956
38438
  return startDraftTest2(draftApi, {
37957
38439
  draftId,
@@ -37959,14 +38441,15 @@ function registerDraftCommand(program) {
37959
38441
  ...options.trigger !== void 0 ? { trigger: toDraftTestTriggerSelector(options.trigger) } : {},
37960
38442
  ...options.payloadFile !== void 0 ? { payload: await readTestPayloadFile(options.payloadFile) } : {}
37961
38443
  },
37962
- wait: options.wait === true ? { timeoutMs } : null,
37963
- signal
38444
+ wait: wait2,
38445
+ signal,
38446
+ onProgress: (state) => progress({ type: "test_progress", draftId, ...state })
37964
38447
  });
37965
38448
  },
37966
- { human: formatDraftTest },
38449
+ { human: formatDraftTest, ndjson: toTerminalRecords },
37967
38450
  {
37968
38451
  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,
38452
+ minimumTokenLifetimeMs: waitTokenLifetimeMs(options),
37970
38453
  exitCode: (result) => result.action === "test_finished" && result.outcome !== "passed" ? 1 : void 0
37971
38454
  }
37972
38455
  );
@@ -38026,20 +38509,6 @@ function streamDraftList(options, fetchPage) {
38026
38509
  async (args) => summarizeDraftListPage(await fetchPage(args))
38027
38510
  );
38028
38511
  }
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
38512
 
38044
38513
  // src/commands/run/index.ts
38045
38514
  import { Option as Option7 } from "commander";
@@ -38135,11 +38604,11 @@ function registerRunCommand(program) {
38135
38604
  }
38136
38605
 
38137
38606
  // src/commands/skill/shared.ts
38138
- import { CliError as CliError7 } from "@doist/cli-core";
38607
+ import { CliError as CliError11 } from "@doist/cli-core";
38139
38608
  function requireInstaller(agent) {
38140
38609
  const installer = getInstaller(agent);
38141
38610
  if (!installer) {
38142
- throw new CliError7("UNKNOWN_AGENT", `Unknown agent: ${agent}.`, {
38611
+ throw new CliError11("UNKNOWN_AGENT", `Unknown agent: ${agent}.`, {
38143
38612
  hints: [`Available agents: ${AGENT_NAMES.join(", ")}.`]
38144
38613
  });
38145
38614
  }
@@ -38217,7 +38686,7 @@ async function uninstallSkill(agent, options, command) {
38217
38686
  }
38218
38687
 
38219
38688
  // src/commands/skill/update.ts
38220
- import { CliError as CliError8 } from "@doist/cli-core";
38689
+ import { CliError as CliError12 } from "@doist/cli-core";
38221
38690
  async function updateSkill(agent, options, command) {
38222
38691
  await runLocalCommand(
38223
38692
  command,
@@ -38243,7 +38712,7 @@ async function updateAllSkills(options, command) {
38243
38712
  async () => {
38244
38713
  const result = await updateAllInstalledSkills(options.local === true);
38245
38714
  if (result.errors.length > 0) {
38246
- throw new CliError8(
38715
+ throw new CliError12(
38247
38716
  "UPDATE_FAILED",
38248
38717
  `Could not update ${result.errors.length} skill file(s).`,
38249
38718
  { hints: result.errors.map((error62) => `${error62.agent}: ${error62.message}`) }
@@ -38303,7 +38772,7 @@ function registerUpdateCommand(program) {
38303
38772
  }
38304
38773
 
38305
38774
  // src/commands/workspace/index.ts
38306
- import { CliError as CliError9 } from "@doist/cli-core";
38775
+ import { CliError as CliError13 } from "@doist/cli-core";
38307
38776
 
38308
38777
  // src/commands/workspace/format.ts
38309
38778
  function formatWorkspaceList(workspaces) {
@@ -38346,7 +38815,7 @@ function registerWorkspaceCommand(program) {
38346
38815
  if (!(error62 instanceof AutomationsRequestError) || error62.httpStatusCode !== 404) {
38347
38816
  throw error62;
38348
38817
  }
38349
- throw new CliError9(
38818
+ throw new CliError13(
38350
38819
  "WORKSPACE_ACCESS_UNAVAILABLE",
38351
38820
  "Workspace access is not available for this account or environment.",
38352
38821
  {