@asteroidai/cli 1.9.115 → 1.9.123

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.
Files changed (3) hide show
  1. package/dist/asteroid.js +378 -34
  2. package/dist/core.js +385 -33
  3. package/package.json +3 -3
package/dist/asteroid.js CHANGED
@@ -1946,6 +1946,34 @@ var COMMANDS = [
1946
1946
  destructive: false,
1947
1947
  capability: "run_execution"
1948
1948
  },
1949
+ {
1950
+ command: "execution sync",
1951
+ tool: "execution_sync",
1952
+ title: "Wait for an execution to finish",
1953
+ description: "Wait up to `timeoutSeconds` (1-25, default 25) for a running execution to finish. Returns the terminal `status` and `executionResult` when it finishes in time. Otherwise returns the current status: call it again to keep waiting. A paused or `awaiting_confirmation` execution returns at once, since it needs a person. Use it after `workflow_execute_post`, or after `workflow_execute_sync_post` timed out, instead of polling `execution_get`.",
1954
+ method: "GET",
1955
+ path: "/agents/v2/executions/{executionId}/sync",
1956
+ args: [
1957
+ {
1958
+ name: "executionId",
1959
+ flag: "execution-id",
1960
+ type: "string",
1961
+ required: true
1962
+ }
1963
+ ],
1964
+ query: [
1965
+ {
1966
+ name: "timeoutSeconds",
1967
+ flag: "timeout-seconds",
1968
+ type: "integer",
1969
+ required: false,
1970
+ description: "Seconds to wait for the execution to finish before returning 202. Defaults to 25."
1971
+ }
1972
+ ],
1973
+ binary: false,
1974
+ readOnly: true,
1975
+ destructive: false
1976
+ },
1949
1977
  {
1950
1978
  command: "insight get",
1951
1979
  tool: "insight_get",
@@ -4886,6 +4914,26 @@ var COMMANDS = [
4886
4914
  readOnly: false,
4887
4915
  destructive: false
4888
4916
  },
4917
+ {
4918
+ command: "workflow head validate",
4919
+ tool: "workflow_head_validate",
4920
+ title: "Validate workflow head",
4921
+ description: 'Check the workflow\'s editable head the way publish and run do, without changing it. Returns blocking errors and advisory warnings, each with the path of the field it names, such as ["nodes", "Login", "model"]. Use after `workflow_files_patch` and before `workflow_head_publish`; an empty `issues` list means the head can publish and run. The same list comes back on every `workflow_files_patch` response.',
4922
+ method: "POST",
4923
+ path: "/agents/v2/workflows/{workflowId}/head/validate",
4924
+ args: [
4925
+ {
4926
+ name: "workflowId",
4927
+ flag: "workflow-id",
4928
+ type: "string",
4929
+ required: true
4930
+ }
4931
+ ],
4932
+ query: [],
4933
+ binary: false,
4934
+ readOnly: true,
4935
+ destructive: false
4936
+ },
4889
4937
  {
4890
4938
  command: "workflow list",
4891
4939
  tool: "workflow_list",
@@ -6200,6 +6248,85 @@ var COMMANDS = [
6200
6248
  }
6201
6249
  ];
6202
6250
 
6251
+ // src/sync/types.ts
6252
+ var MANIFEST = ".asteroid.json";
6253
+ function get(path, query = {}) {
6254
+ return { method: "GET", path, query: new URLSearchParams(query) };
6255
+ }
6256
+ function statusOf(error) {
6257
+ const status = error?.status;
6258
+ return typeof status === "number" ? status : void 0;
6259
+ }
6260
+
6261
+ // src/sync/execution-wait.ts
6262
+ var WAIT_TIMEOUT_SECONDS = 600;
6263
+ var WAIT_INTERVAL_SECONDS = 15;
6264
+ var MAX_WAIT_TIMEOUT_SECONDS = 3600;
6265
+ var MAX_WAIT_INTERVAL_SECONDS = 120;
6266
+ var TERMINAL = /* @__PURE__ */ new Set(["completed", "cancelled", "failed"]);
6267
+ function isTerminalExecutionStatus(status) {
6268
+ return TERMINAL.has(status);
6269
+ }
6270
+ var SYNC_TIMEOUT_SECONDS = 25;
6271
+ function pause(ms) {
6272
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
6273
+ }
6274
+ async function readStatus(sync, executionId2, timeoutMs) {
6275
+ if (timeoutMs <= 0)
6276
+ return await sync.remote.json(
6277
+ get(`/agents/v2/executions/${executionId2}`)
6278
+ );
6279
+ const timeoutSeconds = Math.min(
6280
+ SYNC_TIMEOUT_SECONDS,
6281
+ Math.max(1, Math.ceil(timeoutMs / 1e3))
6282
+ );
6283
+ const response = await sync.remote.json(
6284
+ get(`/agents/v2/executions/${executionId2}/sync`, {
6285
+ timeoutSeconds: String(timeoutSeconds)
6286
+ })
6287
+ );
6288
+ return {
6289
+ id: response.executionId,
6290
+ status: response.status,
6291
+ executionResult: response.executionResult
6292
+ };
6293
+ }
6294
+ async function waitForExecution(sync, options) {
6295
+ const timeoutMs = (options.timeoutSeconds ?? WAIT_TIMEOUT_SECONDS) * 1e3;
6296
+ const intervalMs = (options.intervalSeconds ?? WAIT_INTERVAL_SECONDS) * 1e3;
6297
+ const now = options.now ?? Date.now;
6298
+ const sleep = options.sleep ?? pause;
6299
+ const started = now();
6300
+ let polls = 0;
6301
+ for (; ; ) {
6302
+ const called = now();
6303
+ const execution = await readStatus(
6304
+ sync,
6305
+ options.executionId,
6306
+ timeoutMs - (called - started)
6307
+ );
6308
+ polls++;
6309
+ const elapsed = now() - started;
6310
+ const waitedSeconds = Math.round(elapsed / 1e3);
6311
+ if (isTerminalExecutionStatus(execution.status))
6312
+ return { execution, terminal: true, waitedSeconds, polls };
6313
+ const remaining = timeoutMs - elapsed;
6314
+ if (remaining <= 0)
6315
+ return { execution, terminal: false, waitedSeconds, polls };
6316
+ const early = intervalMs - (now() - called);
6317
+ if (early > 0) await sleep(Math.min(early, remaining));
6318
+ }
6319
+ }
6320
+ function formatExecutionWait(result) {
6321
+ const { execution } = result;
6322
+ if (!result.terminal) {
6323
+ return `Execution ${execution.id} still ${execution.status} after ${result.waitedSeconds}s. Wait again, or read what it has so far with \`asteroid execution pull ${execution.id}\`.`;
6324
+ }
6325
+ const line2 = `Execution ${execution.id} ${execution.status}.`;
6326
+ const outcome = execution.executionResult;
6327
+ return outcome ? `${line2} Result: ${outcome.outcome}: ${outcome.reasoning}` : line2;
6328
+ }
6329
+
6203
6330
  // src/sync/commands.ts
6204
6331
  function option(name, type, description, extra = {}) {
6205
6332
  return { name, flag: name, type, required: false, description, ...extra };
@@ -6211,6 +6338,12 @@ var local = {
6211
6338
  destructive: false,
6212
6339
  local: true
6213
6340
  };
6341
+ var executionId = {
6342
+ name: "executionId",
6343
+ flag: "execution-id",
6344
+ type: "string",
6345
+ required: true
6346
+ };
6214
6347
  var SYNC_COMMANDS = [
6215
6348
  {
6216
6349
  ...local,
@@ -6219,14 +6352,7 @@ var SYNC_COMMANDS = [
6219
6352
  title: "Pull an execution to disk",
6220
6353
  description: "Write an execution to `<workflow-name>/executions/<id>/`: summary.json, activities.md (the step-by-step log), and the files the run produced.",
6221
6354
  readOnly: true,
6222
- args: [
6223
- {
6224
- name: "executionId",
6225
- flag: "execution-id",
6226
- type: "string",
6227
- required: true
6228
- }
6229
- ],
6355
+ args: [executionId],
6230
6356
  query: [
6231
6357
  option(
6232
6358
  "files",
@@ -6252,6 +6378,54 @@ var SYNC_COMMANDS = [
6252
6378
  "Write here instead of `<workflow-name>/executions/<id>/` under the current directory."
6253
6379
  )
6254
6380
  ]
6381
+ },
6382
+ {
6383
+ ...local,
6384
+ command: "execution wait",
6385
+ tool: "execution_wait",
6386
+ title: "Wait for an execution to end",
6387
+ description: "Poll an execution until it is completed, failed or cancelled, then print its status and, when the run produced one, its result. When the timeout passes first, print the status it still has, so you can wait again.",
6388
+ readOnly: true,
6389
+ args: [executionId],
6390
+ query: [
6391
+ option(
6392
+ "timeout",
6393
+ "integer",
6394
+ `Give up after this many seconds, ${WAIT_TIMEOUT_SECONDS} unless set, ${MAX_WAIT_TIMEOUT_SECONDS} at most.`
6395
+ ),
6396
+ option(
6397
+ "interval",
6398
+ "integer",
6399
+ `Poll every this many seconds, ${WAIT_INTERVAL_SECONDS} unless set, ${MAX_WAIT_INTERVAL_SECONDS} at most.`
6400
+ )
6401
+ ]
6402
+ },
6403
+ {
6404
+ ...local,
6405
+ command: "insight pull",
6406
+ tool: "insight_pull",
6407
+ title: "Pull insights to disk",
6408
+ description: "Write insights to `insights/<name>.md`, one markdown file each with YAML frontmatter (id, type, status, name, next_steps). Name an insight to pull that one. A file another insight or an unpushed draft already holds keeps its name; the pulled one gets a -2, -3 suffix.",
6409
+ readOnly: true,
6410
+ args: [
6411
+ {
6412
+ name: "insightId",
6413
+ flag: "insight-id",
6414
+ type: "string",
6415
+ required: false,
6416
+ description: "One insight to pull. Without it, every insight, or one workflow's with --workflow-id."
6417
+ }
6418
+ ],
6419
+ query: [
6420
+ option("workflowId", "string", "Only this workflow's insights.", {
6421
+ flag: "workflow-id"
6422
+ }),
6423
+ option(
6424
+ "dir",
6425
+ "string",
6426
+ "Write here instead of `insights/` under the current directory."
6427
+ )
6428
+ ]
6255
6429
  }
6256
6430
  ];
6257
6431
 
@@ -6710,15 +6884,16 @@ function defineCommands(root, options, dotted2 = /* @__PURE__ */ new Map(), onIn
6710
6884
  (arg) => options.defaultArg?.(command, arg)
6711
6885
  );
6712
6886
  const defaulted = defaults[0] !== void 0;
6887
+ const optional = (arg, i) => i === 0 && defaulted || !arg.required;
6713
6888
  command.args.forEach((arg) => {
6714
6889
  leaf.argument(
6715
- defaulted ? `[${arg.flag}]` : `<${arg.flag}>`,
6890
+ defaulted || !arg.required ? `[${arg.flag}]` : `<${arg.flag}>`,
6716
6891
  arg.description ?? ""
6717
6892
  );
6718
6893
  });
6719
6894
  if (defaulted) {
6720
6895
  const names = command.args.map(
6721
- (arg, i) => i === 0 ? `[${arg.flag}]` : `<${arg.flag}>`
6896
+ (arg, i) => optional(arg, i) ? `[${arg.flag}]` : `<${arg.flag}>`
6722
6897
  );
6723
6898
  leaf.usage(`[options] ${names.join(" ")}`);
6724
6899
  }
@@ -6735,8 +6910,9 @@ ${notes.join("\n")}`);
6735
6910
  if (positionals.length === command.args.length - 1 && defaults[0] !== void 0) {
6736
6911
  positionals = [defaults[0], ...positionals];
6737
6912
  }
6738
- if (positionals.length !== command.args.length) {
6739
- const expected = command.args.map((a, i) => i === 0 && defaulted ? `[${a.flag}]` : `<${a.flag}>`).join(" ");
6913
+ const required = command.args.filter((arg) => arg.required).length;
6914
+ if (positionals.length < required || positionals.length > command.args.length) {
6915
+ const expected = command.args.map((a, i) => optional(a, i) ? `[${a.flag}]` : `<${a.flag}>`).join(" ");
6740
6916
  throw new UsageError(
6741
6917
  `expected ${expected}, got ${cmd.args.length} argument(s)`
6742
6918
  );
@@ -7258,7 +7434,7 @@ var ApiError = class extends Error {
7258
7434
  status;
7259
7435
  body;
7260
7436
  };
7261
- var VERSION = "1.9.115";
7437
+ var VERSION = "1.9.123";
7262
7438
  function problem(status, body, fallback) {
7263
7439
  if (body && typeof body === "object") {
7264
7440
  const record2 = body;
@@ -8070,6 +8246,13 @@ function normalizeNodeName(name) {
8070
8246
  }
8071
8247
  return result;
8072
8248
  }
8249
+ function lossySlugOr(name, fallback) {
8250
+ try {
8251
+ return normalizeNodeName(name);
8252
+ } catch {
8253
+ return fallback;
8254
+ }
8255
+ }
8073
8256
  function nodeDirName(name) {
8074
8257
  if (isNameCharsetConforming(name)) {
8075
8258
  return name.replaceAll(" ", "_");
@@ -8083,8 +8266,8 @@ function agentDirName(name) {
8083
8266
  return "agent";
8084
8267
  }
8085
8268
  }
8086
- function executionDirName(executionId) {
8087
- return executionId.slice(0, 8);
8269
+ function executionDirName(executionId2) {
8270
+ return executionId2.slice(0, 8);
8088
8271
  }
8089
8272
  function workflowDirName(workflowId, version, parentId, parentVersion) {
8090
8273
  const short = workflowId.slice(0, 8);
@@ -8132,16 +8315,6 @@ function matchesGlob(path, pattern) {
8132
8315
  return new RegExp(`^${source}$`).test(path);
8133
8316
  }
8134
8317
 
8135
- // src/sync/types.ts
8136
- var MANIFEST = ".asteroid.json";
8137
- function get(path, query = {}) {
8138
- return { method: "GET", path, query: new URLSearchParams(query) };
8139
- }
8140
- function statusOf(error) {
8141
- const status = error?.status;
8142
- return typeof status === "number" ? status : void 0;
8143
- }
8144
-
8145
8318
  // src/sync/execution-pull.ts
8146
8319
  var DIRECTORIES = ["workspace", "downloads", "shared"];
8147
8320
  function relativeFilePath(file) {
@@ -8179,14 +8352,14 @@ async function writeActivities(sync, dir, execution) {
8179
8352
  return false;
8180
8353
  }
8181
8354
  }
8182
- async function listFiles(sync, executionId) {
8355
+ async function listFiles(sync, executionId2) {
8183
8356
  const listing = await sync.remote.json(
8184
- get(`/agents/v2/executions/${executionId}/files`)
8357
+ get(`/agents/v2/executions/${executionId2}/files`)
8185
8358
  );
8186
8359
  return DIRECTORIES.flatMap((name) => listing.directories[name] ?? []);
8187
8360
  }
8188
- async function download(sync, executionId, file) {
8189
- const path = `/agents/v2/executions/${executionId}/files/${file.id}/signed-url`;
8361
+ async function download(sync, executionId2, file) {
8362
+ const path = `/agents/v2/executions/${executionId2}/files/${file.id}/signed-url`;
8190
8363
  const { url } = await sync.remote.json(get(path));
8191
8364
  return sync.remote.download(url);
8192
8365
  }
@@ -8314,6 +8487,114 @@ ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
8314
8487
  ].filter(Boolean).join("\n");
8315
8488
  }
8316
8489
 
8490
+ // src/sync/insight-file.ts
8491
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
8492
+ var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)(?:\r?\n)?/;
8493
+ function serializeInsightFile(meta, body) {
8494
+ const front = {};
8495
+ if (meta.id) front.id = meta.id;
8496
+ front.type = meta.type;
8497
+ if (meta.status) front.status = meta.status;
8498
+ front.name = meta.name;
8499
+ if (meta.nextSteps) front.next_steps = meta.nextSteps;
8500
+ const yaml = stringifyYaml(front).trimEnd();
8501
+ return `---
8502
+ ${yaml}
8503
+ ---
8504
+
8505
+ ${body}`;
8506
+ }
8507
+ function insightFileId(text3) {
8508
+ const match = FRONTMATTER.exec(text3);
8509
+ if (!match) return void 0;
8510
+ try {
8511
+ const id = parseYaml(match[1])?.id;
8512
+ return typeof id === "string" || typeof id === "number" ? String(id) : void 0;
8513
+ } catch {
8514
+ return void 0;
8515
+ }
8516
+ }
8517
+
8518
+ // src/sync/insight-pull.ts
8519
+ var INSIGHTS_DIR = "insights";
8520
+ var PAGE_SIZE = 100;
8521
+ async function listInsights(sync, workflowId) {
8522
+ const all = [];
8523
+ for (let page = 1; ; page++) {
8524
+ const query = {
8525
+ page: String(page),
8526
+ pageSize: String(PAGE_SIZE)
8527
+ };
8528
+ if (workflowId) query.workflowId = workflowId;
8529
+ const { items, total } = await sync.remote.json(
8530
+ get("/astro/v2/insights", query)
8531
+ );
8532
+ all.push(...items);
8533
+ if (items.length === 0 || all.length >= total) return all;
8534
+ }
8535
+ }
8536
+ async function stemOwners(sync, dir) {
8537
+ const owners = /* @__PURE__ */ new Map();
8538
+ for (const name of await sync.files.list(dir)) {
8539
+ if (!name.endsWith(".md") || name.includes("/")) continue;
8540
+ const bytes = await sync.files.read(join3(dir, name));
8541
+ const text3 = bytes ? Buffer.from(bytes).toString("utf8") : "";
8542
+ owners.set(name.slice(0, -".md".length).toLowerCase(), insightFileId(text3));
8543
+ }
8544
+ return owners;
8545
+ }
8546
+ function assignStems(owners, insights) {
8547
+ const used = /* @__PURE__ */ new Set();
8548
+ const heldByOther = (stem, id) => owners.has(stem) && owners.get(stem) !== id;
8549
+ return insights.map((insight) => {
8550
+ const base = lossySlugOr(insight.name, "insight");
8551
+ let stem = base;
8552
+ for (let n = 2; used.has(stem) || heldByOther(stem, insight.id); n++) {
8553
+ stem = `${base}-${n}`;
8554
+ }
8555
+ used.add(stem);
8556
+ return [insight, stem];
8557
+ });
8558
+ }
8559
+ async function pullInsights(sync, options = {}) {
8560
+ const dir = options.dir ?? INSIGHTS_DIR;
8561
+ const insights = options.insightId ? [
8562
+ await sync.remote.json(
8563
+ get(`/astro/v2/insights/${options.insightId}`)
8564
+ )
8565
+ ] : await listInsights(sync, options.workflowId);
8566
+ const pulled = [];
8567
+ for (const [insight, stem] of assignStems(
8568
+ await stemOwners(sync, dir),
8569
+ insights
8570
+ )) {
8571
+ const path = join3(dir, `${stem}.md`);
8572
+ const text3 = serializeInsightFile(
8573
+ {
8574
+ id: insight.id,
8575
+ type: insight.type,
8576
+ status: insight.status,
8577
+ name: insight.name,
8578
+ nextSteps: insight.nextSteps ?? void 0
8579
+ },
8580
+ insight.content
8581
+ );
8582
+ await sync.files.write(path, Buffer.from(text3, "utf8"));
8583
+ const { id, name, type, status } = insight;
8584
+ pulled.push({ id, name, type, status, path });
8585
+ }
8586
+ return { dir, insights: pulled };
8587
+ }
8588
+ function formatInsightPull(result) {
8589
+ const count = result.insights.length;
8590
+ return [
8591
+ `Pulled ${count} insight${count === 1 ? "" : "s"} into ${result.dir}/.`,
8592
+ ...result.insights.map(
8593
+ (insight) => ` - ${insight.path}: ${insight.name} (${insight.type}, ${insight.status})`
8594
+ )
8595
+ ].join("\n");
8596
+ }
8597
+
8317
8598
  // src/sync/node-files.ts
8318
8599
  import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
8319
8600
  import { dirname as dirname2, relative, resolve, sep } from "node:path";
@@ -8605,7 +8886,7 @@ function conflict(manifest) {
8605
8886
  }
8606
8887
  async function patch(sync, manifest, writes, deletes) {
8607
8888
  try {
8608
- await sync.remote.json({
8889
+ const snapshot = await sync.remote.json({
8609
8890
  method: "PATCH",
8610
8891
  path: `/agents/v2/workflows/${manifest.workflowId}/head/files`,
8611
8892
  query: new URLSearchParams(),
@@ -8618,7 +8899,7 @@ async function patch(sync, manifest, writes, deletes) {
8618
8899
  deletes
8619
8900
  }
8620
8901
  });
8621
- return "applied";
8902
+ return { issues: snapshot?.issues ?? [] };
8622
8903
  } catch (error) {
8623
8904
  if (statusOf(error) !== 409) throw error;
8624
8905
  return "conflict";
@@ -8688,14 +8969,20 @@ async function pushWorkflow(sync, dir, workflowId, options = {}) {
8688
8969
  if (writes.length === 0 && deletes.length === 0) {
8689
8970
  return { ...result, rev: manifest.rev, versionId: manifest.versionId };
8690
8971
  }
8691
- if (await patch(sync, manifest, writes, deletes) === "applied") {
8972
+ const applied = await patch(sync, manifest, writes, deletes);
8973
+ if (applied !== "conflict") {
8692
8974
  const next = await refresh2(
8693
8975
  sync,
8694
8976
  dir,
8695
8977
  manifest,
8696
8978
  new Map(writes.map((w) => [w.path, w.hash]))
8697
8979
  );
8698
- return { ...result, rev: next.rev, versionId: next.versionId };
8980
+ return {
8981
+ ...result,
8982
+ rev: next.rev,
8983
+ versionId: next.versionId,
8984
+ issues: applied.issues
8985
+ };
8699
8986
  }
8700
8987
  if (!options.rebase || attempt >= REBASE_ATTEMPTS) throw conflict(manifest);
8701
8988
  rebasedFrom ??= manifest.rev;
@@ -8711,9 +8998,23 @@ function formatWorkflowPush(result) {
8711
8998
  `Pushed to workflow ${result.workflowId}: ${result.written.length} written, ${result.deleted.length} deleted.${rebased}`,
8712
8999
  ...result.written.map((path) => ` + ${path}`),
8713
9000
  ...result.deleted.map((path) => ` - ${path}`),
8714
- `Head is now rev ${result.rev} (version ${result.versionId}).`
9001
+ `Head is now rev ${result.rev} (version ${result.versionId}).`,
9002
+ ...formatIssues(result.issues ?? [])
8715
9003
  ].join("\n");
8716
9004
  }
9005
+ function formatIssues(issues) {
9006
+ if (issues.length === 0) return [];
9007
+ const errors = issues.filter((issue) => issue.severity === "error").length;
9008
+ const warnings = issues.length - errors;
9009
+ const count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
9010
+ const summary2 = errors === 0 ? `Validation: ${count(warnings, "warning")}, nothing blocks a publish.` : `Validation: ${count(errors, "error")}${warnings > 0 ? `, ${count(warnings, "warning")}` : ""}. Fix the errors before you publish.`;
9011
+ return [
9012
+ summary2,
9013
+ ...issues.map(
9014
+ (issue) => ` ${issue.severity === "error" ? "!" : "?"} ${issue.path.join("/") || "(workflow)"}: ${issue.message}`
9015
+ )
9016
+ ];
9017
+ }
8717
9018
 
8718
9019
  // src/sync/run.ts
8719
9020
  function flag(parsed, name) {
@@ -8726,6 +9027,35 @@ function integer(parsed, name) {
8726
9027
  throw new UsageError(`--${name} must be a whole number`);
8727
9028
  return Number(raw);
8728
9029
  }
9030
+ function bounded(parsed, name, min, max) {
9031
+ const value2 = integer(parsed, name);
9032
+ if (value2 === void 0) return void 0;
9033
+ if (value2 < min || value2 > max)
9034
+ throw new UsageError(`--${name} must be between ${min} and ${max}`);
9035
+ return value2;
9036
+ }
9037
+ function executionWaitOptions(parsed) {
9038
+ return {
9039
+ executionId: parsed.positionals[0],
9040
+ timeoutSeconds: bounded(parsed, "timeout", 0, MAX_WAIT_TIMEOUT_SECONDS),
9041
+ intervalSeconds: bounded(parsed, "interval", 1, MAX_WAIT_INTERVAL_SECONDS)
9042
+ };
9043
+ }
9044
+ function given(value2, name) {
9045
+ if (value2 === void 0) return void 0;
9046
+ if (value2 === "") throw new UsageError(`${name} must not be empty`);
9047
+ return value2;
9048
+ }
9049
+ function insightPullOptions(parsed) {
9050
+ const options = {};
9051
+ const insightId = given(parsed.positionals[0], "<insight-id>");
9052
+ if (insightId) options.insightId = insightId;
9053
+ const workflowId = given(flag(parsed, "workflow-id"), "--workflow-id");
9054
+ if (workflowId) options.workflowId = workflowId;
9055
+ const dir = given(flag(parsed, "dir"), "--dir");
9056
+ if (dir) options.dir = dir;
9057
+ return options;
9058
+ }
8729
9059
  function executionOptions(parsed) {
8730
9060
  const summaryOnly = flag(parsed, "summary-only") === "true";
8731
9061
  const glob = flag(parsed, "files");
@@ -8787,6 +9117,20 @@ async function execute(command, parsed, sync, deps) {
8787
9117
  );
8788
9118
  return { result: result2, text: formatWorkflowPush(result2) };
8789
9119
  }
9120
+ if (command.command === "execution wait") {
9121
+ const result2 = await waitForExecution(sync, {
9122
+ ...executionWaitOptions(parsed),
9123
+ ...deps.executionWait
9124
+ });
9125
+ return { result: result2, text: formatExecutionWait(result2) };
9126
+ }
9127
+ if (command.command === "insight pull") {
9128
+ const result2 = await pullInsights(sync, {
9129
+ ...deps.insightPull,
9130
+ ...insightPullOptions(parsed)
9131
+ });
9132
+ return { result: result2, text: formatInsightPull(result2) };
9133
+ }
8790
9134
  if (command.command !== "execution pull")
8791
9135
  throw new UsageError(`unknown command ${command.command}`);
8792
9136
  const result = await pullExecution(sync, {
package/dist/core.js CHANGED
@@ -1967,6 +1967,34 @@ var COMMANDS = [
1967
1967
  destructive: false,
1968
1968
  capability: "run_execution"
1969
1969
  },
1970
+ {
1971
+ command: "execution sync",
1972
+ tool: "execution_sync",
1973
+ title: "Wait for an execution to finish",
1974
+ description: "Wait up to `timeoutSeconds` (1-25, default 25) for a running execution to finish. Returns the terminal `status` and `executionResult` when it finishes in time. Otherwise returns the current status: call it again to keep waiting. A paused or `awaiting_confirmation` execution returns at once, since it needs a person. Use it after `workflow_execute_post`, or after `workflow_execute_sync_post` timed out, instead of polling `execution_get`.",
1975
+ method: "GET",
1976
+ path: "/agents/v2/executions/{executionId}/sync",
1977
+ args: [
1978
+ {
1979
+ name: "executionId",
1980
+ flag: "execution-id",
1981
+ type: "string",
1982
+ required: true
1983
+ }
1984
+ ],
1985
+ query: [
1986
+ {
1987
+ name: "timeoutSeconds",
1988
+ flag: "timeout-seconds",
1989
+ type: "integer",
1990
+ required: false,
1991
+ description: "Seconds to wait for the execution to finish before returning 202. Defaults to 25."
1992
+ }
1993
+ ],
1994
+ binary: false,
1995
+ readOnly: true,
1996
+ destructive: false
1997
+ },
1970
1998
  {
1971
1999
  command: "insight get",
1972
2000
  tool: "insight_get",
@@ -4907,6 +4935,26 @@ var COMMANDS = [
4907
4935
  readOnly: false,
4908
4936
  destructive: false
4909
4937
  },
4938
+ {
4939
+ command: "workflow head validate",
4940
+ tool: "workflow_head_validate",
4941
+ title: "Validate workflow head",
4942
+ description: 'Check the workflow\'s editable head the way publish and run do, without changing it. Returns blocking errors and advisory warnings, each with the path of the field it names, such as ["nodes", "Login", "model"]. Use after `workflow_files_patch` and before `workflow_head_publish`; an empty `issues` list means the head can publish and run. The same list comes back on every `workflow_files_patch` response.',
4943
+ method: "POST",
4944
+ path: "/agents/v2/workflows/{workflowId}/head/validate",
4945
+ args: [
4946
+ {
4947
+ name: "workflowId",
4948
+ flag: "workflow-id",
4949
+ type: "string",
4950
+ required: true
4951
+ }
4952
+ ],
4953
+ query: [],
4954
+ binary: false,
4955
+ readOnly: true,
4956
+ destructive: false
4957
+ },
4910
4958
  {
4911
4959
  command: "workflow list",
4912
4960
  tool: "workflow_list",
@@ -6580,15 +6628,16 @@ function defineCommands(root, options, dotted2 = /* @__PURE__ */ new Map(), onIn
6580
6628
  (arg) => options.defaultArg?.(command, arg)
6581
6629
  );
6582
6630
  const defaulted = defaults[0] !== void 0;
6631
+ const optional = (arg, i) => i === 0 && defaulted || !arg.required;
6583
6632
  command.args.forEach((arg) => {
6584
6633
  leaf.argument(
6585
- defaulted ? `[${arg.flag}]` : `<${arg.flag}>`,
6634
+ defaulted || !arg.required ? `[${arg.flag}]` : `<${arg.flag}>`,
6586
6635
  arg.description ?? ""
6587
6636
  );
6588
6637
  });
6589
6638
  if (defaulted) {
6590
6639
  const names = command.args.map(
6591
- (arg, i) => i === 0 ? `[${arg.flag}]` : `<${arg.flag}>`
6640
+ (arg, i) => optional(arg, i) ? `[${arg.flag}]` : `<${arg.flag}>`
6592
6641
  );
6593
6642
  leaf.usage(`[options] ${names.join(" ")}`);
6594
6643
  }
@@ -6605,8 +6654,9 @@ ${notes.join("\n")}`);
6605
6654
  if (positionals.length === command.args.length - 1 && defaults[0] !== void 0) {
6606
6655
  positionals = [defaults[0], ...positionals];
6607
6656
  }
6608
- if (positionals.length !== command.args.length) {
6609
- const expected = command.args.map((a, i) => i === 0 && defaulted ? `[${a.flag}]` : `<${a.flag}>`).join(" ");
6657
+ const required = command.args.filter((arg) => arg.required).length;
6658
+ if (positionals.length < required || positionals.length > command.args.length) {
6659
+ const expected = command.args.map((a, i) => optional(a, i) ? `[${a.flag}]` : `<${a.flag}>`).join(" ");
6610
6660
  throw new UsageError(
6611
6661
  `expected ${expected}, got ${cmd.args.length} argument(s)`
6612
6662
  );
@@ -6904,6 +6954,85 @@ function rejectUnknown(flags) {
6904
6954
  }
6905
6955
  }
6906
6956
 
6957
+ // src/sync/types.ts
6958
+ var MANIFEST = ".asteroid.json";
6959
+ function get(path, query = {}) {
6960
+ return { method: "GET", path, query: new URLSearchParams(query) };
6961
+ }
6962
+ function statusOf(error) {
6963
+ const status = error?.status;
6964
+ return typeof status === "number" ? status : void 0;
6965
+ }
6966
+
6967
+ // src/sync/execution-wait.ts
6968
+ var WAIT_TIMEOUT_SECONDS = 600;
6969
+ var WAIT_INTERVAL_SECONDS = 15;
6970
+ var MAX_WAIT_TIMEOUT_SECONDS = 3600;
6971
+ var MAX_WAIT_INTERVAL_SECONDS = 120;
6972
+ var TERMINAL = /* @__PURE__ */ new Set(["completed", "cancelled", "failed"]);
6973
+ function isTerminalExecutionStatus(status) {
6974
+ return TERMINAL.has(status);
6975
+ }
6976
+ var SYNC_TIMEOUT_SECONDS = 25;
6977
+ function pause(ms) {
6978
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
6979
+ }
6980
+ async function readStatus(sync, executionId2, timeoutMs) {
6981
+ if (timeoutMs <= 0)
6982
+ return await sync.remote.json(
6983
+ get(`/agents/v2/executions/${executionId2}`)
6984
+ );
6985
+ const timeoutSeconds = Math.min(
6986
+ SYNC_TIMEOUT_SECONDS,
6987
+ Math.max(1, Math.ceil(timeoutMs / 1e3))
6988
+ );
6989
+ const response = await sync.remote.json(
6990
+ get(`/agents/v2/executions/${executionId2}/sync`, {
6991
+ timeoutSeconds: String(timeoutSeconds)
6992
+ })
6993
+ );
6994
+ return {
6995
+ id: response.executionId,
6996
+ status: response.status,
6997
+ executionResult: response.executionResult
6998
+ };
6999
+ }
7000
+ async function waitForExecution(sync, options) {
7001
+ const timeoutMs = (options.timeoutSeconds ?? WAIT_TIMEOUT_SECONDS) * 1e3;
7002
+ const intervalMs = (options.intervalSeconds ?? WAIT_INTERVAL_SECONDS) * 1e3;
7003
+ const now = options.now ?? Date.now;
7004
+ const sleep = options.sleep ?? pause;
7005
+ const started = now();
7006
+ let polls = 0;
7007
+ for (; ; ) {
7008
+ const called = now();
7009
+ const execution = await readStatus(
7010
+ sync,
7011
+ options.executionId,
7012
+ timeoutMs - (called - started)
7013
+ );
7014
+ polls++;
7015
+ const elapsed = now() - started;
7016
+ const waitedSeconds = Math.round(elapsed / 1e3);
7017
+ if (isTerminalExecutionStatus(execution.status))
7018
+ return { execution, terminal: true, waitedSeconds, polls };
7019
+ const remaining = timeoutMs - elapsed;
7020
+ if (remaining <= 0)
7021
+ return { execution, terminal: false, waitedSeconds, polls };
7022
+ const early = intervalMs - (now() - called);
7023
+ if (early > 0) await sleep(Math.min(early, remaining));
7024
+ }
7025
+ }
7026
+ function formatExecutionWait(result) {
7027
+ const { execution } = result;
7028
+ if (!result.terminal) {
7029
+ return `Execution ${execution.id} still ${execution.status} after ${result.waitedSeconds}s. Wait again, or read what it has so far with \`asteroid execution pull ${execution.id}\`.`;
7030
+ }
7031
+ const line2 = `Execution ${execution.id} ${execution.status}.`;
7032
+ const outcome = execution.executionResult;
7033
+ return outcome ? `${line2} Result: ${outcome.outcome}: ${outcome.reasoning}` : line2;
7034
+ }
7035
+
6907
7036
  // src/sync/commands.ts
6908
7037
  function option(name, type, description, extra = {}) {
6909
7038
  return { name, flag: name, type, required: false, description, ...extra };
@@ -6915,6 +7044,12 @@ var local = {
6915
7044
  destructive: false,
6916
7045
  local: true
6917
7046
  };
7047
+ var executionId = {
7048
+ name: "executionId",
7049
+ flag: "execution-id",
7050
+ type: "string",
7051
+ required: true
7052
+ };
6918
7053
  var SYNC_COMMANDS = [
6919
7054
  {
6920
7055
  ...local,
@@ -6923,14 +7058,7 @@ var SYNC_COMMANDS = [
6923
7058
  title: "Pull an execution to disk",
6924
7059
  description: "Write an execution to `<workflow-name>/executions/<id>/`: summary.json, activities.md (the step-by-step log), and the files the run produced.",
6925
7060
  readOnly: true,
6926
- args: [
6927
- {
6928
- name: "executionId",
6929
- flag: "execution-id",
6930
- type: "string",
6931
- required: true
6932
- }
6933
- ],
7061
+ args: [executionId],
6934
7062
  query: [
6935
7063
  option(
6936
7064
  "files",
@@ -6956,6 +7084,54 @@ var SYNC_COMMANDS = [
6956
7084
  "Write here instead of `<workflow-name>/executions/<id>/` under the current directory."
6957
7085
  )
6958
7086
  ]
7087
+ },
7088
+ {
7089
+ ...local,
7090
+ command: "execution wait",
7091
+ tool: "execution_wait",
7092
+ title: "Wait for an execution to end",
7093
+ description: "Poll an execution until it is completed, failed or cancelled, then print its status and, when the run produced one, its result. When the timeout passes first, print the status it still has, so you can wait again.",
7094
+ readOnly: true,
7095
+ args: [executionId],
7096
+ query: [
7097
+ option(
7098
+ "timeout",
7099
+ "integer",
7100
+ `Give up after this many seconds, ${WAIT_TIMEOUT_SECONDS} unless set, ${MAX_WAIT_TIMEOUT_SECONDS} at most.`
7101
+ ),
7102
+ option(
7103
+ "interval",
7104
+ "integer",
7105
+ `Poll every this many seconds, ${WAIT_INTERVAL_SECONDS} unless set, ${MAX_WAIT_INTERVAL_SECONDS} at most.`
7106
+ )
7107
+ ]
7108
+ },
7109
+ {
7110
+ ...local,
7111
+ command: "insight pull",
7112
+ tool: "insight_pull",
7113
+ title: "Pull insights to disk",
7114
+ description: "Write insights to `insights/<name>.md`, one markdown file each with YAML frontmatter (id, type, status, name, next_steps). Name an insight to pull that one. A file another insight or an unpushed draft already holds keeps its name; the pulled one gets a -2, -3 suffix.",
7115
+ readOnly: true,
7116
+ args: [
7117
+ {
7118
+ name: "insightId",
7119
+ flag: "insight-id",
7120
+ type: "string",
7121
+ required: false,
7122
+ description: "One insight to pull. Without it, every insight, or one workflow's with --workflow-id."
7123
+ }
7124
+ ],
7125
+ query: [
7126
+ option("workflowId", "string", "Only this workflow's insights.", {
7127
+ flag: "workflow-id"
7128
+ }),
7129
+ option(
7130
+ "dir",
7131
+ "string",
7132
+ "Write here instead of `insights/` under the current directory."
7133
+ )
7134
+ ]
6959
7135
  }
6960
7136
  ];
6961
7137
 
@@ -7068,6 +7244,13 @@ function normalizeNodeName(name) {
7068
7244
  }
7069
7245
  return result;
7070
7246
  }
7247
+ function lossySlugOr(name, fallback) {
7248
+ try {
7249
+ return normalizeNodeName(name);
7250
+ } catch {
7251
+ return fallback;
7252
+ }
7253
+ }
7071
7254
  function nodeDirName(name) {
7072
7255
  if (isNameCharsetConforming(name)) {
7073
7256
  return name.replaceAll(" ", "_");
@@ -7081,8 +7264,8 @@ function agentDirName(name) {
7081
7264
  return "agent";
7082
7265
  }
7083
7266
  }
7084
- function executionDirName(executionId) {
7085
- return executionId.slice(0, 8);
7267
+ function executionDirName(executionId2) {
7268
+ return executionId2.slice(0, 8);
7086
7269
  }
7087
7270
  function workflowDirName(workflowId, version, parentId, parentVersion) {
7088
7271
  const short = workflowId.slice(0, 8);
@@ -7130,16 +7313,6 @@ function matchesGlob(path, pattern) {
7130
7313
  return new RegExp(`^${source}$`).test(path);
7131
7314
  }
7132
7315
 
7133
- // src/sync/types.ts
7134
- var MANIFEST = ".asteroid.json";
7135
- function get(path, query = {}) {
7136
- return { method: "GET", path, query: new URLSearchParams(query) };
7137
- }
7138
- function statusOf(error) {
7139
- const status = error?.status;
7140
- return typeof status === "number" ? status : void 0;
7141
- }
7142
-
7143
7316
  // src/sync/execution-pull.ts
7144
7317
  var DIRECTORIES = ["workspace", "downloads", "shared"];
7145
7318
  function relativeFilePath(file) {
@@ -7177,14 +7350,14 @@ async function writeActivities(sync, dir, execution) {
7177
7350
  return false;
7178
7351
  }
7179
7352
  }
7180
- async function listFiles(sync, executionId) {
7353
+ async function listFiles(sync, executionId2) {
7181
7354
  const listing = await sync.remote.json(
7182
- get(`/agents/v2/executions/${executionId}/files`)
7355
+ get(`/agents/v2/executions/${executionId2}/files`)
7183
7356
  );
7184
7357
  return DIRECTORIES.flatMap((name) => listing.directories[name] ?? []);
7185
7358
  }
7186
- async function download(sync, executionId, file) {
7187
- const path = `/agents/v2/executions/${executionId}/files/${file.id}/signed-url`;
7359
+ async function download(sync, executionId2, file) {
7360
+ const path = `/agents/v2/executions/${executionId2}/files/${file.id}/signed-url`;
7188
7361
  const { url } = await sync.remote.json(get(path));
7189
7362
  return sync.remote.download(url);
7190
7363
  }
@@ -7312,6 +7485,114 @@ ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
7312
7485
  ].filter(Boolean).join("\n");
7313
7486
  }
7314
7487
 
7488
+ // src/sync/insight-file.ts
7489
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
7490
+ var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)(?:\r?\n)?/;
7491
+ function serializeInsightFile(meta, body) {
7492
+ const front = {};
7493
+ if (meta.id) front.id = meta.id;
7494
+ front.type = meta.type;
7495
+ if (meta.status) front.status = meta.status;
7496
+ front.name = meta.name;
7497
+ if (meta.nextSteps) front.next_steps = meta.nextSteps;
7498
+ const yaml = stringifyYaml(front).trimEnd();
7499
+ return `---
7500
+ ${yaml}
7501
+ ---
7502
+
7503
+ ${body}`;
7504
+ }
7505
+ function insightFileId(text2) {
7506
+ const match = FRONTMATTER.exec(text2);
7507
+ if (!match) return void 0;
7508
+ try {
7509
+ const id = parseYaml(match[1])?.id;
7510
+ return typeof id === "string" || typeof id === "number" ? String(id) : void 0;
7511
+ } catch {
7512
+ return void 0;
7513
+ }
7514
+ }
7515
+
7516
+ // src/sync/insight-pull.ts
7517
+ var INSIGHTS_DIR = "insights";
7518
+ var PAGE_SIZE = 100;
7519
+ async function listInsights(sync, workflowId) {
7520
+ const all = [];
7521
+ for (let page = 1; ; page++) {
7522
+ const query = {
7523
+ page: String(page),
7524
+ pageSize: String(PAGE_SIZE)
7525
+ };
7526
+ if (workflowId) query.workflowId = workflowId;
7527
+ const { items, total } = await sync.remote.json(
7528
+ get("/astro/v2/insights", query)
7529
+ );
7530
+ all.push(...items);
7531
+ if (items.length === 0 || all.length >= total) return all;
7532
+ }
7533
+ }
7534
+ async function stemOwners(sync, dir) {
7535
+ const owners = /* @__PURE__ */ new Map();
7536
+ for (const name of await sync.files.list(dir)) {
7537
+ if (!name.endsWith(".md") || name.includes("/")) continue;
7538
+ const bytes = await sync.files.read(join(dir, name));
7539
+ const text2 = bytes ? Buffer.from(bytes).toString("utf8") : "";
7540
+ owners.set(name.slice(0, -".md".length).toLowerCase(), insightFileId(text2));
7541
+ }
7542
+ return owners;
7543
+ }
7544
+ function assignStems(owners, insights) {
7545
+ const used = /* @__PURE__ */ new Set();
7546
+ const heldByOther = (stem, id) => owners.has(stem) && owners.get(stem) !== id;
7547
+ return insights.map((insight) => {
7548
+ const base = lossySlugOr(insight.name, "insight");
7549
+ let stem = base;
7550
+ for (let n = 2; used.has(stem) || heldByOther(stem, insight.id); n++) {
7551
+ stem = `${base}-${n}`;
7552
+ }
7553
+ used.add(stem);
7554
+ return [insight, stem];
7555
+ });
7556
+ }
7557
+ async function pullInsights(sync, options = {}) {
7558
+ const dir = options.dir ?? INSIGHTS_DIR;
7559
+ const insights = options.insightId ? [
7560
+ await sync.remote.json(
7561
+ get(`/astro/v2/insights/${options.insightId}`)
7562
+ )
7563
+ ] : await listInsights(sync, options.workflowId);
7564
+ const pulled = [];
7565
+ for (const [insight, stem] of assignStems(
7566
+ await stemOwners(sync, dir),
7567
+ insights
7568
+ )) {
7569
+ const path = join(dir, `${stem}.md`);
7570
+ const text2 = serializeInsightFile(
7571
+ {
7572
+ id: insight.id,
7573
+ type: insight.type,
7574
+ status: insight.status,
7575
+ name: insight.name,
7576
+ nextSteps: insight.nextSteps ?? void 0
7577
+ },
7578
+ insight.content
7579
+ );
7580
+ await sync.files.write(path, Buffer.from(text2, "utf8"));
7581
+ const { id, name, type, status } = insight;
7582
+ pulled.push({ id, name, type, status, path });
7583
+ }
7584
+ return { dir, insights: pulled };
7585
+ }
7586
+ function formatInsightPull(result) {
7587
+ const count = result.insights.length;
7588
+ return [
7589
+ `Pulled ${count} insight${count === 1 ? "" : "s"} into ${result.dir}/.`,
7590
+ ...result.insights.map(
7591
+ (insight) => ` - ${insight.path}: ${insight.name} (${insight.type}, ${insight.status})`
7592
+ )
7593
+ ].join("\n");
7594
+ }
7595
+
7315
7596
  // src/sync/manifest.ts
7316
7597
  async function loadManifest(sync, dir) {
7317
7598
  const bytes = await sync.files.read(join(dir, MANIFEST));
@@ -7603,7 +7884,7 @@ function conflict(manifest) {
7603
7884
  }
7604
7885
  async function patch(sync, manifest, writes, deletes) {
7605
7886
  try {
7606
- await sync.remote.json({
7887
+ const snapshot = await sync.remote.json({
7607
7888
  method: "PATCH",
7608
7889
  path: `/agents/v2/workflows/${manifest.workflowId}/head/files`,
7609
7890
  query: new URLSearchParams(),
@@ -7616,7 +7897,7 @@ async function patch(sync, manifest, writes, deletes) {
7616
7897
  deletes
7617
7898
  }
7618
7899
  });
7619
- return "applied";
7900
+ return { issues: snapshot?.issues ?? [] };
7620
7901
  } catch (error) {
7621
7902
  if (statusOf(error) !== 409) throw error;
7622
7903
  return "conflict";
@@ -7686,14 +7967,20 @@ async function pushWorkflow(sync, dir, workflowId, options = {}) {
7686
7967
  if (writes.length === 0 && deletes.length === 0) {
7687
7968
  return { ...result, rev: manifest.rev, versionId: manifest.versionId };
7688
7969
  }
7689
- if (await patch(sync, manifest, writes, deletes) === "applied") {
7970
+ const applied = await patch(sync, manifest, writes, deletes);
7971
+ if (applied !== "conflict") {
7690
7972
  const next = await refresh(
7691
7973
  sync,
7692
7974
  dir,
7693
7975
  manifest,
7694
7976
  new Map(writes.map((w) => [w.path, w.hash]))
7695
7977
  );
7696
- return { ...result, rev: next.rev, versionId: next.versionId };
7978
+ return {
7979
+ ...result,
7980
+ rev: next.rev,
7981
+ versionId: next.versionId,
7982
+ issues: applied.issues
7983
+ };
7697
7984
  }
7698
7985
  if (!options.rebase || attempt >= REBASE_ATTEMPTS) throw conflict(manifest);
7699
7986
  rebasedFrom ??= manifest.rev;
@@ -7709,9 +7996,23 @@ function formatWorkflowPush(result) {
7709
7996
  `Pushed to workflow ${result.workflowId}: ${result.written.length} written, ${result.deleted.length} deleted.${rebased}`,
7710
7997
  ...result.written.map((path) => ` + ${path}`),
7711
7998
  ...result.deleted.map((path) => ` - ${path}`),
7712
- `Head is now rev ${result.rev} (version ${result.versionId}).`
7999
+ `Head is now rev ${result.rev} (version ${result.versionId}).`,
8000
+ ...formatIssues(result.issues ?? [])
7713
8001
  ].join("\n");
7714
8002
  }
8003
+ function formatIssues(issues) {
8004
+ if (issues.length === 0) return [];
8005
+ const errors = issues.filter((issue) => issue.severity === "error").length;
8006
+ const warnings = issues.length - errors;
8007
+ const count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
8008
+ const summary2 = errors === 0 ? `Validation: ${count(warnings, "warning")}, nothing blocks a publish.` : `Validation: ${count(errors, "error")}${warnings > 0 ? `, ${count(warnings, "warning")}` : ""}. Fix the errors before you publish.`;
8009
+ return [
8010
+ summary2,
8011
+ ...issues.map(
8012
+ (issue) => ` ${issue.severity === "error" ? "!" : "?"} ${issue.path.join("/") || "(workflow)"}: ${issue.message}`
8013
+ )
8014
+ ];
8015
+ }
7715
8016
 
7716
8017
  // src/sync/run.ts
7717
8018
  function flag(parsed, name) {
@@ -7724,6 +8025,35 @@ function integer(parsed, name) {
7724
8025
  throw new UsageError(`--${name} must be a whole number`);
7725
8026
  return Number(raw);
7726
8027
  }
8028
+ function bounded(parsed, name, min, max) {
8029
+ const value2 = integer(parsed, name);
8030
+ if (value2 === void 0) return void 0;
8031
+ if (value2 < min || value2 > max)
8032
+ throw new UsageError(`--${name} must be between ${min} and ${max}`);
8033
+ return value2;
8034
+ }
8035
+ function executionWaitOptions(parsed) {
8036
+ return {
8037
+ executionId: parsed.positionals[0],
8038
+ timeoutSeconds: bounded(parsed, "timeout", 0, MAX_WAIT_TIMEOUT_SECONDS),
8039
+ intervalSeconds: bounded(parsed, "interval", 1, MAX_WAIT_INTERVAL_SECONDS)
8040
+ };
8041
+ }
8042
+ function given(value2, name) {
8043
+ if (value2 === void 0) return void 0;
8044
+ if (value2 === "") throw new UsageError(`${name} must not be empty`);
8045
+ return value2;
8046
+ }
8047
+ function insightPullOptions(parsed) {
8048
+ const options = {};
8049
+ const insightId = given(parsed.positionals[0], "<insight-id>");
8050
+ if (insightId) options.insightId = insightId;
8051
+ const workflowId = given(flag(parsed, "workflow-id"), "--workflow-id");
8052
+ if (workflowId) options.workflowId = workflowId;
8053
+ const dir = given(flag(parsed, "dir"), "--dir");
8054
+ if (dir) options.dir = dir;
8055
+ return options;
8056
+ }
7727
8057
  function executionOptions(parsed) {
7728
8058
  const summaryOnly = flag(parsed, "summary-only") === "true";
7729
8059
  const glob = flag(parsed, "files");
@@ -7785,6 +8115,20 @@ async function execute(command, parsed, sync, deps) {
7785
8115
  );
7786
8116
  return { result: result2, text: formatWorkflowPush(result2) };
7787
8117
  }
8118
+ if (command.command === "execution wait") {
8119
+ const result2 = await waitForExecution(sync, {
8120
+ ...executionWaitOptions(parsed),
8121
+ ...deps.executionWait
8122
+ });
8123
+ return { result: result2, text: formatExecutionWait(result2) };
8124
+ }
8125
+ if (command.command === "insight pull") {
8126
+ const result2 = await pullInsights(sync, {
8127
+ ...deps.insightPull,
8128
+ ...insightPullOptions(parsed)
8129
+ });
8130
+ return { result: result2, text: formatInsightPull(result2) };
8131
+ }
7788
8132
  if (command.command !== "execution pull")
7789
8133
  throw new UsageError(`unknown command ${command.command}`);
7790
8134
  const result = await pullExecution(sync, {
@@ -7807,15 +8151,20 @@ async function runSync(command, parsed, deps) {
7807
8151
  export {
7808
8152
  ALL_COMMANDS,
7809
8153
  COMMANDS,
8154
+ INSIGHTS_DIR,
7810
8155
  MANIFEST,
7811
8156
  SYNC_COMMANDS,
7812
8157
  UsageError,
7813
8158
  buildRequest,
7814
8159
  formatActivitiesMarkdown,
7815
8160
  formatExecutionPull,
8161
+ formatExecutionWait,
8162
+ formatInsightPull,
7816
8163
  formatWorkflowPull,
7817
8164
  formatWorkflowPush,
7818
8165
  headFilesRequest,
8166
+ insightFileId,
8167
+ isTerminalExecutionStatus,
7819
8168
  leadingWords,
7820
8169
  loadManifest,
7821
8170
  matchCommand,
@@ -7823,12 +8172,15 @@ export {
7823
8172
  parseInvocation,
7824
8173
  pullDir,
7825
8174
  pullExecution,
8175
+ pullInsights,
7826
8176
  pullWorkflow,
7827
8177
  pushWorkflow,
7828
8178
  readManifest,
7829
8179
  render,
7830
8180
  runSync,
7831
8181
  runsLocally,
8182
+ serializeInsightFile,
8183
+ waitForExecution,
7832
8184
  withSyncCommands,
7833
8185
  writeManifest
7834
8186
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteroidai/cli",
3
- "version": "1.9.115",
3
+ "version": "1.9.123",
4
4
  "description": "The asteroid command-line interface for the Asteroid platform.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,7 +23,8 @@
23
23
  "cli-table3": "^0.6.5",
24
24
  "commander": "^15.0.0",
25
25
  "env-paths": "^4.0.0",
26
- "proper-lockfile": "^4.1.2"
26
+ "proper-lockfile": "^4.1.2",
27
+ "yaml": "^2.9.0"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@types/node": "^22.20.1",
@@ -33,7 +34,6 @@
33
34
  "tsx": "^4.23.12",
34
35
  "typescript": "5.9.3",
35
36
  "vitest": "^4.1.11",
36
- "yaml": "^2.9.0",
37
37
  "@asteroid/workflow-files": "0.1.0"
38
38
  },
39
39
  "scripts": {