@asteroidai/cli 1.9.115 → 1.9.122

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 +333 -29
  2. package/dist/core.js +340 -28
  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",
@@ -6200,6 +6228,85 @@ var COMMANDS = [
6200
6228
  }
6201
6229
  ];
6202
6230
 
6231
+ // src/sync/types.ts
6232
+ var MANIFEST = ".asteroid.json";
6233
+ function get(path, query = {}) {
6234
+ return { method: "GET", path, query: new URLSearchParams(query) };
6235
+ }
6236
+ function statusOf(error) {
6237
+ const status = error?.status;
6238
+ return typeof status === "number" ? status : void 0;
6239
+ }
6240
+
6241
+ // src/sync/execution-wait.ts
6242
+ var WAIT_TIMEOUT_SECONDS = 600;
6243
+ var WAIT_INTERVAL_SECONDS = 15;
6244
+ var MAX_WAIT_TIMEOUT_SECONDS = 3600;
6245
+ var MAX_WAIT_INTERVAL_SECONDS = 120;
6246
+ var TERMINAL = /* @__PURE__ */ new Set(["completed", "cancelled", "failed"]);
6247
+ function isTerminalExecutionStatus(status) {
6248
+ return TERMINAL.has(status);
6249
+ }
6250
+ var SYNC_TIMEOUT_SECONDS = 25;
6251
+ function pause(ms) {
6252
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
6253
+ }
6254
+ async function readStatus(sync, executionId2, timeoutMs) {
6255
+ if (timeoutMs <= 0)
6256
+ return await sync.remote.json(
6257
+ get(`/agents/v2/executions/${executionId2}`)
6258
+ );
6259
+ const timeoutSeconds = Math.min(
6260
+ SYNC_TIMEOUT_SECONDS,
6261
+ Math.max(1, Math.ceil(timeoutMs / 1e3))
6262
+ );
6263
+ const response = await sync.remote.json(
6264
+ get(`/agents/v2/executions/${executionId2}/sync`, {
6265
+ timeoutSeconds: String(timeoutSeconds)
6266
+ })
6267
+ );
6268
+ return {
6269
+ id: response.executionId,
6270
+ status: response.status,
6271
+ executionResult: response.executionResult
6272
+ };
6273
+ }
6274
+ async function waitForExecution(sync, options) {
6275
+ const timeoutMs = (options.timeoutSeconds ?? WAIT_TIMEOUT_SECONDS) * 1e3;
6276
+ const intervalMs = (options.intervalSeconds ?? WAIT_INTERVAL_SECONDS) * 1e3;
6277
+ const now = options.now ?? Date.now;
6278
+ const sleep = options.sleep ?? pause;
6279
+ const started = now();
6280
+ let polls = 0;
6281
+ for (; ; ) {
6282
+ const called = now();
6283
+ const execution = await readStatus(
6284
+ sync,
6285
+ options.executionId,
6286
+ timeoutMs - (called - started)
6287
+ );
6288
+ polls++;
6289
+ const elapsed = now() - started;
6290
+ const waitedSeconds = Math.round(elapsed / 1e3);
6291
+ if (isTerminalExecutionStatus(execution.status))
6292
+ return { execution, terminal: true, waitedSeconds, polls };
6293
+ const remaining = timeoutMs - elapsed;
6294
+ if (remaining <= 0)
6295
+ return { execution, terminal: false, waitedSeconds, polls };
6296
+ const early = intervalMs - (now() - called);
6297
+ if (early > 0) await sleep(Math.min(early, remaining));
6298
+ }
6299
+ }
6300
+ function formatExecutionWait(result) {
6301
+ const { execution } = result;
6302
+ if (!result.terminal) {
6303
+ 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}\`.`;
6304
+ }
6305
+ const line2 = `Execution ${execution.id} ${execution.status}.`;
6306
+ const outcome = execution.executionResult;
6307
+ return outcome ? `${line2} Result: ${outcome.outcome}: ${outcome.reasoning}` : line2;
6308
+ }
6309
+
6203
6310
  // src/sync/commands.ts
6204
6311
  function option(name, type, description, extra = {}) {
6205
6312
  return { name, flag: name, type, required: false, description, ...extra };
@@ -6211,6 +6318,12 @@ var local = {
6211
6318
  destructive: false,
6212
6319
  local: true
6213
6320
  };
6321
+ var executionId = {
6322
+ name: "executionId",
6323
+ flag: "execution-id",
6324
+ type: "string",
6325
+ required: true
6326
+ };
6214
6327
  var SYNC_COMMANDS = [
6215
6328
  {
6216
6329
  ...local,
@@ -6219,14 +6332,7 @@ var SYNC_COMMANDS = [
6219
6332
  title: "Pull an execution to disk",
6220
6333
  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
6334
  readOnly: true,
6222
- args: [
6223
- {
6224
- name: "executionId",
6225
- flag: "execution-id",
6226
- type: "string",
6227
- required: true
6228
- }
6229
- ],
6335
+ args: [executionId],
6230
6336
  query: [
6231
6337
  option(
6232
6338
  "files",
@@ -6252,6 +6358,54 @@ var SYNC_COMMANDS = [
6252
6358
  "Write here instead of `<workflow-name>/executions/<id>/` under the current directory."
6253
6359
  )
6254
6360
  ]
6361
+ },
6362
+ {
6363
+ ...local,
6364
+ command: "execution wait",
6365
+ tool: "execution_wait",
6366
+ title: "Wait for an execution to end",
6367
+ 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.",
6368
+ readOnly: true,
6369
+ args: [executionId],
6370
+ query: [
6371
+ option(
6372
+ "timeout",
6373
+ "integer",
6374
+ `Give up after this many seconds, ${WAIT_TIMEOUT_SECONDS} unless set, ${MAX_WAIT_TIMEOUT_SECONDS} at most.`
6375
+ ),
6376
+ option(
6377
+ "interval",
6378
+ "integer",
6379
+ `Poll every this many seconds, ${WAIT_INTERVAL_SECONDS} unless set, ${MAX_WAIT_INTERVAL_SECONDS} at most.`
6380
+ )
6381
+ ]
6382
+ },
6383
+ {
6384
+ ...local,
6385
+ command: "insight pull",
6386
+ tool: "insight_pull",
6387
+ title: "Pull insights to disk",
6388
+ 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.",
6389
+ readOnly: true,
6390
+ args: [
6391
+ {
6392
+ name: "insightId",
6393
+ flag: "insight-id",
6394
+ type: "string",
6395
+ required: false,
6396
+ description: "One insight to pull. Without it, every insight, or one workflow's with --workflow-id."
6397
+ }
6398
+ ],
6399
+ query: [
6400
+ option("workflowId", "string", "Only this workflow's insights.", {
6401
+ flag: "workflow-id"
6402
+ }),
6403
+ option(
6404
+ "dir",
6405
+ "string",
6406
+ "Write here instead of `insights/` under the current directory."
6407
+ )
6408
+ ]
6255
6409
  }
6256
6410
  ];
6257
6411
 
@@ -6710,15 +6864,16 @@ function defineCommands(root, options, dotted2 = /* @__PURE__ */ new Map(), onIn
6710
6864
  (arg) => options.defaultArg?.(command, arg)
6711
6865
  );
6712
6866
  const defaulted = defaults[0] !== void 0;
6867
+ const optional = (arg, i) => i === 0 && defaulted || !arg.required;
6713
6868
  command.args.forEach((arg) => {
6714
6869
  leaf.argument(
6715
- defaulted ? `[${arg.flag}]` : `<${arg.flag}>`,
6870
+ defaulted || !arg.required ? `[${arg.flag}]` : `<${arg.flag}>`,
6716
6871
  arg.description ?? ""
6717
6872
  );
6718
6873
  });
6719
6874
  if (defaulted) {
6720
6875
  const names = command.args.map(
6721
- (arg, i) => i === 0 ? `[${arg.flag}]` : `<${arg.flag}>`
6876
+ (arg, i) => optional(arg, i) ? `[${arg.flag}]` : `<${arg.flag}>`
6722
6877
  );
6723
6878
  leaf.usage(`[options] ${names.join(" ")}`);
6724
6879
  }
@@ -6735,8 +6890,9 @@ ${notes.join("\n")}`);
6735
6890
  if (positionals.length === command.args.length - 1 && defaults[0] !== void 0) {
6736
6891
  positionals = [defaults[0], ...positionals];
6737
6892
  }
6738
- if (positionals.length !== command.args.length) {
6739
- const expected = command.args.map((a, i) => i === 0 && defaulted ? `[${a.flag}]` : `<${a.flag}>`).join(" ");
6893
+ const required = command.args.filter((arg) => arg.required).length;
6894
+ if (positionals.length < required || positionals.length > command.args.length) {
6895
+ const expected = command.args.map((a, i) => optional(a, i) ? `[${a.flag}]` : `<${a.flag}>`).join(" ");
6740
6896
  throw new UsageError(
6741
6897
  `expected ${expected}, got ${cmd.args.length} argument(s)`
6742
6898
  );
@@ -7258,7 +7414,7 @@ var ApiError = class extends Error {
7258
7414
  status;
7259
7415
  body;
7260
7416
  };
7261
- var VERSION = "1.9.115";
7417
+ var VERSION = "1.9.122";
7262
7418
  function problem(status, body, fallback) {
7263
7419
  if (body && typeof body === "object") {
7264
7420
  const record2 = body;
@@ -8070,6 +8226,13 @@ function normalizeNodeName(name) {
8070
8226
  }
8071
8227
  return result;
8072
8228
  }
8229
+ function lossySlugOr(name, fallback) {
8230
+ try {
8231
+ return normalizeNodeName(name);
8232
+ } catch {
8233
+ return fallback;
8234
+ }
8235
+ }
8073
8236
  function nodeDirName(name) {
8074
8237
  if (isNameCharsetConforming(name)) {
8075
8238
  return name.replaceAll(" ", "_");
@@ -8083,8 +8246,8 @@ function agentDirName(name) {
8083
8246
  return "agent";
8084
8247
  }
8085
8248
  }
8086
- function executionDirName(executionId) {
8087
- return executionId.slice(0, 8);
8249
+ function executionDirName(executionId2) {
8250
+ return executionId2.slice(0, 8);
8088
8251
  }
8089
8252
  function workflowDirName(workflowId, version, parentId, parentVersion) {
8090
8253
  const short = workflowId.slice(0, 8);
@@ -8132,16 +8295,6 @@ function matchesGlob(path, pattern) {
8132
8295
  return new RegExp(`^${source}$`).test(path);
8133
8296
  }
8134
8297
 
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
8298
  // src/sync/execution-pull.ts
8146
8299
  var DIRECTORIES = ["workspace", "downloads", "shared"];
8147
8300
  function relativeFilePath(file) {
@@ -8179,14 +8332,14 @@ async function writeActivities(sync, dir, execution) {
8179
8332
  return false;
8180
8333
  }
8181
8334
  }
8182
- async function listFiles(sync, executionId) {
8335
+ async function listFiles(sync, executionId2) {
8183
8336
  const listing = await sync.remote.json(
8184
- get(`/agents/v2/executions/${executionId}/files`)
8337
+ get(`/agents/v2/executions/${executionId2}/files`)
8185
8338
  );
8186
8339
  return DIRECTORIES.flatMap((name) => listing.directories[name] ?? []);
8187
8340
  }
8188
- async function download(sync, executionId, file) {
8189
- const path = `/agents/v2/executions/${executionId}/files/${file.id}/signed-url`;
8341
+ async function download(sync, executionId2, file) {
8342
+ const path = `/agents/v2/executions/${executionId2}/files/${file.id}/signed-url`;
8190
8343
  const { url } = await sync.remote.json(get(path));
8191
8344
  return sync.remote.download(url);
8192
8345
  }
@@ -8314,6 +8467,114 @@ ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
8314
8467
  ].filter(Boolean).join("\n");
8315
8468
  }
8316
8469
 
8470
+ // src/sync/insight-file.ts
8471
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
8472
+ var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)(?:\r?\n)?/;
8473
+ function serializeInsightFile(meta, body) {
8474
+ const front = {};
8475
+ if (meta.id) front.id = meta.id;
8476
+ front.type = meta.type;
8477
+ if (meta.status) front.status = meta.status;
8478
+ front.name = meta.name;
8479
+ if (meta.nextSteps) front.next_steps = meta.nextSteps;
8480
+ const yaml = stringifyYaml(front).trimEnd();
8481
+ return `---
8482
+ ${yaml}
8483
+ ---
8484
+
8485
+ ${body}`;
8486
+ }
8487
+ function insightFileId(text3) {
8488
+ const match = FRONTMATTER.exec(text3);
8489
+ if (!match) return void 0;
8490
+ try {
8491
+ const id = parseYaml(match[1])?.id;
8492
+ return typeof id === "string" || typeof id === "number" ? String(id) : void 0;
8493
+ } catch {
8494
+ return void 0;
8495
+ }
8496
+ }
8497
+
8498
+ // src/sync/insight-pull.ts
8499
+ var INSIGHTS_DIR = "insights";
8500
+ var PAGE_SIZE = 100;
8501
+ async function listInsights(sync, workflowId) {
8502
+ const all = [];
8503
+ for (let page = 1; ; page++) {
8504
+ const query = {
8505
+ page: String(page),
8506
+ pageSize: String(PAGE_SIZE)
8507
+ };
8508
+ if (workflowId) query.workflowId = workflowId;
8509
+ const { items, total } = await sync.remote.json(
8510
+ get("/astro/v2/insights", query)
8511
+ );
8512
+ all.push(...items);
8513
+ if (items.length === 0 || all.length >= total) return all;
8514
+ }
8515
+ }
8516
+ async function stemOwners(sync, dir) {
8517
+ const owners = /* @__PURE__ */ new Map();
8518
+ for (const name of await sync.files.list(dir)) {
8519
+ if (!name.endsWith(".md") || name.includes("/")) continue;
8520
+ const bytes = await sync.files.read(join3(dir, name));
8521
+ const text3 = bytes ? Buffer.from(bytes).toString("utf8") : "";
8522
+ owners.set(name.slice(0, -".md".length).toLowerCase(), insightFileId(text3));
8523
+ }
8524
+ return owners;
8525
+ }
8526
+ function assignStems(owners, insights) {
8527
+ const used = /* @__PURE__ */ new Set();
8528
+ const heldByOther = (stem, id) => owners.has(stem) && owners.get(stem) !== id;
8529
+ return insights.map((insight) => {
8530
+ const base = lossySlugOr(insight.name, "insight");
8531
+ let stem = base;
8532
+ for (let n = 2; used.has(stem) || heldByOther(stem, insight.id); n++) {
8533
+ stem = `${base}-${n}`;
8534
+ }
8535
+ used.add(stem);
8536
+ return [insight, stem];
8537
+ });
8538
+ }
8539
+ async function pullInsights(sync, options = {}) {
8540
+ const dir = options.dir ?? INSIGHTS_DIR;
8541
+ const insights = options.insightId ? [
8542
+ await sync.remote.json(
8543
+ get(`/astro/v2/insights/${options.insightId}`)
8544
+ )
8545
+ ] : await listInsights(sync, options.workflowId);
8546
+ const pulled = [];
8547
+ for (const [insight, stem] of assignStems(
8548
+ await stemOwners(sync, dir),
8549
+ insights
8550
+ )) {
8551
+ const path = join3(dir, `${stem}.md`);
8552
+ const text3 = serializeInsightFile(
8553
+ {
8554
+ id: insight.id,
8555
+ type: insight.type,
8556
+ status: insight.status,
8557
+ name: insight.name,
8558
+ nextSteps: insight.nextSteps ?? void 0
8559
+ },
8560
+ insight.content
8561
+ );
8562
+ await sync.files.write(path, Buffer.from(text3, "utf8"));
8563
+ const { id, name, type, status } = insight;
8564
+ pulled.push({ id, name, type, status, path });
8565
+ }
8566
+ return { dir, insights: pulled };
8567
+ }
8568
+ function formatInsightPull(result) {
8569
+ const count = result.insights.length;
8570
+ return [
8571
+ `Pulled ${count} insight${count === 1 ? "" : "s"} into ${result.dir}/.`,
8572
+ ...result.insights.map(
8573
+ (insight) => ` - ${insight.path}: ${insight.name} (${insight.type}, ${insight.status})`
8574
+ )
8575
+ ].join("\n");
8576
+ }
8577
+
8317
8578
  // src/sync/node-files.ts
8318
8579
  import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
8319
8580
  import { dirname as dirname2, relative, resolve, sep } from "node:path";
@@ -8726,6 +8987,35 @@ function integer(parsed, name) {
8726
8987
  throw new UsageError(`--${name} must be a whole number`);
8727
8988
  return Number(raw);
8728
8989
  }
8990
+ function bounded(parsed, name, min, max) {
8991
+ const value2 = integer(parsed, name);
8992
+ if (value2 === void 0) return void 0;
8993
+ if (value2 < min || value2 > max)
8994
+ throw new UsageError(`--${name} must be between ${min} and ${max}`);
8995
+ return value2;
8996
+ }
8997
+ function executionWaitOptions(parsed) {
8998
+ return {
8999
+ executionId: parsed.positionals[0],
9000
+ timeoutSeconds: bounded(parsed, "timeout", 0, MAX_WAIT_TIMEOUT_SECONDS),
9001
+ intervalSeconds: bounded(parsed, "interval", 1, MAX_WAIT_INTERVAL_SECONDS)
9002
+ };
9003
+ }
9004
+ function given(value2, name) {
9005
+ if (value2 === void 0) return void 0;
9006
+ if (value2 === "") throw new UsageError(`${name} must not be empty`);
9007
+ return value2;
9008
+ }
9009
+ function insightPullOptions(parsed) {
9010
+ const options = {};
9011
+ const insightId = given(parsed.positionals[0], "<insight-id>");
9012
+ if (insightId) options.insightId = insightId;
9013
+ const workflowId = given(flag(parsed, "workflow-id"), "--workflow-id");
9014
+ if (workflowId) options.workflowId = workflowId;
9015
+ const dir = given(flag(parsed, "dir"), "--dir");
9016
+ if (dir) options.dir = dir;
9017
+ return options;
9018
+ }
8729
9019
  function executionOptions(parsed) {
8730
9020
  const summaryOnly = flag(parsed, "summary-only") === "true";
8731
9021
  const glob = flag(parsed, "files");
@@ -8787,6 +9077,20 @@ async function execute(command, parsed, sync, deps) {
8787
9077
  );
8788
9078
  return { result: result2, text: formatWorkflowPush(result2) };
8789
9079
  }
9080
+ if (command.command === "execution wait") {
9081
+ const result2 = await waitForExecution(sync, {
9082
+ ...executionWaitOptions(parsed),
9083
+ ...deps.executionWait
9084
+ });
9085
+ return { result: result2, text: formatExecutionWait(result2) };
9086
+ }
9087
+ if (command.command === "insight pull") {
9088
+ const result2 = await pullInsights(sync, {
9089
+ ...deps.insightPull,
9090
+ ...insightPullOptions(parsed)
9091
+ });
9092
+ return { result: result2, text: formatInsightPull(result2) };
9093
+ }
8790
9094
  if (command.command !== "execution pull")
8791
9095
  throw new UsageError(`unknown command ${command.command}`);
8792
9096
  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",
@@ -6580,15 +6608,16 @@ function defineCommands(root, options, dotted2 = /* @__PURE__ */ new Map(), onIn
6580
6608
  (arg) => options.defaultArg?.(command, arg)
6581
6609
  );
6582
6610
  const defaulted = defaults[0] !== void 0;
6611
+ const optional = (arg, i) => i === 0 && defaulted || !arg.required;
6583
6612
  command.args.forEach((arg) => {
6584
6613
  leaf.argument(
6585
- defaulted ? `[${arg.flag}]` : `<${arg.flag}>`,
6614
+ defaulted || !arg.required ? `[${arg.flag}]` : `<${arg.flag}>`,
6586
6615
  arg.description ?? ""
6587
6616
  );
6588
6617
  });
6589
6618
  if (defaulted) {
6590
6619
  const names = command.args.map(
6591
- (arg, i) => i === 0 ? `[${arg.flag}]` : `<${arg.flag}>`
6620
+ (arg, i) => optional(arg, i) ? `[${arg.flag}]` : `<${arg.flag}>`
6592
6621
  );
6593
6622
  leaf.usage(`[options] ${names.join(" ")}`);
6594
6623
  }
@@ -6605,8 +6634,9 @@ ${notes.join("\n")}`);
6605
6634
  if (positionals.length === command.args.length - 1 && defaults[0] !== void 0) {
6606
6635
  positionals = [defaults[0], ...positionals];
6607
6636
  }
6608
- if (positionals.length !== command.args.length) {
6609
- const expected = command.args.map((a, i) => i === 0 && defaulted ? `[${a.flag}]` : `<${a.flag}>`).join(" ");
6637
+ const required = command.args.filter((arg) => arg.required).length;
6638
+ if (positionals.length < required || positionals.length > command.args.length) {
6639
+ const expected = command.args.map((a, i) => optional(a, i) ? `[${a.flag}]` : `<${a.flag}>`).join(" ");
6610
6640
  throw new UsageError(
6611
6641
  `expected ${expected}, got ${cmd.args.length} argument(s)`
6612
6642
  );
@@ -6904,6 +6934,85 @@ function rejectUnknown(flags) {
6904
6934
  }
6905
6935
  }
6906
6936
 
6937
+ // src/sync/types.ts
6938
+ var MANIFEST = ".asteroid.json";
6939
+ function get(path, query = {}) {
6940
+ return { method: "GET", path, query: new URLSearchParams(query) };
6941
+ }
6942
+ function statusOf(error) {
6943
+ const status = error?.status;
6944
+ return typeof status === "number" ? status : void 0;
6945
+ }
6946
+
6947
+ // src/sync/execution-wait.ts
6948
+ var WAIT_TIMEOUT_SECONDS = 600;
6949
+ var WAIT_INTERVAL_SECONDS = 15;
6950
+ var MAX_WAIT_TIMEOUT_SECONDS = 3600;
6951
+ var MAX_WAIT_INTERVAL_SECONDS = 120;
6952
+ var TERMINAL = /* @__PURE__ */ new Set(["completed", "cancelled", "failed"]);
6953
+ function isTerminalExecutionStatus(status) {
6954
+ return TERMINAL.has(status);
6955
+ }
6956
+ var SYNC_TIMEOUT_SECONDS = 25;
6957
+ function pause(ms) {
6958
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
6959
+ }
6960
+ async function readStatus(sync, executionId2, timeoutMs) {
6961
+ if (timeoutMs <= 0)
6962
+ return await sync.remote.json(
6963
+ get(`/agents/v2/executions/${executionId2}`)
6964
+ );
6965
+ const timeoutSeconds = Math.min(
6966
+ SYNC_TIMEOUT_SECONDS,
6967
+ Math.max(1, Math.ceil(timeoutMs / 1e3))
6968
+ );
6969
+ const response = await sync.remote.json(
6970
+ get(`/agents/v2/executions/${executionId2}/sync`, {
6971
+ timeoutSeconds: String(timeoutSeconds)
6972
+ })
6973
+ );
6974
+ return {
6975
+ id: response.executionId,
6976
+ status: response.status,
6977
+ executionResult: response.executionResult
6978
+ };
6979
+ }
6980
+ async function waitForExecution(sync, options) {
6981
+ const timeoutMs = (options.timeoutSeconds ?? WAIT_TIMEOUT_SECONDS) * 1e3;
6982
+ const intervalMs = (options.intervalSeconds ?? WAIT_INTERVAL_SECONDS) * 1e3;
6983
+ const now = options.now ?? Date.now;
6984
+ const sleep = options.sleep ?? pause;
6985
+ const started = now();
6986
+ let polls = 0;
6987
+ for (; ; ) {
6988
+ const called = now();
6989
+ const execution = await readStatus(
6990
+ sync,
6991
+ options.executionId,
6992
+ timeoutMs - (called - started)
6993
+ );
6994
+ polls++;
6995
+ const elapsed = now() - started;
6996
+ const waitedSeconds = Math.round(elapsed / 1e3);
6997
+ if (isTerminalExecutionStatus(execution.status))
6998
+ return { execution, terminal: true, waitedSeconds, polls };
6999
+ const remaining = timeoutMs - elapsed;
7000
+ if (remaining <= 0)
7001
+ return { execution, terminal: false, waitedSeconds, polls };
7002
+ const early = intervalMs - (now() - called);
7003
+ if (early > 0) await sleep(Math.min(early, remaining));
7004
+ }
7005
+ }
7006
+ function formatExecutionWait(result) {
7007
+ const { execution } = result;
7008
+ if (!result.terminal) {
7009
+ 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}\`.`;
7010
+ }
7011
+ const line2 = `Execution ${execution.id} ${execution.status}.`;
7012
+ const outcome = execution.executionResult;
7013
+ return outcome ? `${line2} Result: ${outcome.outcome}: ${outcome.reasoning}` : line2;
7014
+ }
7015
+
6907
7016
  // src/sync/commands.ts
6908
7017
  function option(name, type, description, extra = {}) {
6909
7018
  return { name, flag: name, type, required: false, description, ...extra };
@@ -6915,6 +7024,12 @@ var local = {
6915
7024
  destructive: false,
6916
7025
  local: true
6917
7026
  };
7027
+ var executionId = {
7028
+ name: "executionId",
7029
+ flag: "execution-id",
7030
+ type: "string",
7031
+ required: true
7032
+ };
6918
7033
  var SYNC_COMMANDS = [
6919
7034
  {
6920
7035
  ...local,
@@ -6923,14 +7038,7 @@ var SYNC_COMMANDS = [
6923
7038
  title: "Pull an execution to disk",
6924
7039
  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
7040
  readOnly: true,
6926
- args: [
6927
- {
6928
- name: "executionId",
6929
- flag: "execution-id",
6930
- type: "string",
6931
- required: true
6932
- }
6933
- ],
7041
+ args: [executionId],
6934
7042
  query: [
6935
7043
  option(
6936
7044
  "files",
@@ -6956,6 +7064,54 @@ var SYNC_COMMANDS = [
6956
7064
  "Write here instead of `<workflow-name>/executions/<id>/` under the current directory."
6957
7065
  )
6958
7066
  ]
7067
+ },
7068
+ {
7069
+ ...local,
7070
+ command: "execution wait",
7071
+ tool: "execution_wait",
7072
+ title: "Wait for an execution to end",
7073
+ 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.",
7074
+ readOnly: true,
7075
+ args: [executionId],
7076
+ query: [
7077
+ option(
7078
+ "timeout",
7079
+ "integer",
7080
+ `Give up after this many seconds, ${WAIT_TIMEOUT_SECONDS} unless set, ${MAX_WAIT_TIMEOUT_SECONDS} at most.`
7081
+ ),
7082
+ option(
7083
+ "interval",
7084
+ "integer",
7085
+ `Poll every this many seconds, ${WAIT_INTERVAL_SECONDS} unless set, ${MAX_WAIT_INTERVAL_SECONDS} at most.`
7086
+ )
7087
+ ]
7088
+ },
7089
+ {
7090
+ ...local,
7091
+ command: "insight pull",
7092
+ tool: "insight_pull",
7093
+ title: "Pull insights to disk",
7094
+ 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.",
7095
+ readOnly: true,
7096
+ args: [
7097
+ {
7098
+ name: "insightId",
7099
+ flag: "insight-id",
7100
+ type: "string",
7101
+ required: false,
7102
+ description: "One insight to pull. Without it, every insight, or one workflow's with --workflow-id."
7103
+ }
7104
+ ],
7105
+ query: [
7106
+ option("workflowId", "string", "Only this workflow's insights.", {
7107
+ flag: "workflow-id"
7108
+ }),
7109
+ option(
7110
+ "dir",
7111
+ "string",
7112
+ "Write here instead of `insights/` under the current directory."
7113
+ )
7114
+ ]
6959
7115
  }
6960
7116
  ];
6961
7117
 
@@ -7068,6 +7224,13 @@ function normalizeNodeName(name) {
7068
7224
  }
7069
7225
  return result;
7070
7226
  }
7227
+ function lossySlugOr(name, fallback) {
7228
+ try {
7229
+ return normalizeNodeName(name);
7230
+ } catch {
7231
+ return fallback;
7232
+ }
7233
+ }
7071
7234
  function nodeDirName(name) {
7072
7235
  if (isNameCharsetConforming(name)) {
7073
7236
  return name.replaceAll(" ", "_");
@@ -7081,8 +7244,8 @@ function agentDirName(name) {
7081
7244
  return "agent";
7082
7245
  }
7083
7246
  }
7084
- function executionDirName(executionId) {
7085
- return executionId.slice(0, 8);
7247
+ function executionDirName(executionId2) {
7248
+ return executionId2.slice(0, 8);
7086
7249
  }
7087
7250
  function workflowDirName(workflowId, version, parentId, parentVersion) {
7088
7251
  const short = workflowId.slice(0, 8);
@@ -7130,16 +7293,6 @@ function matchesGlob(path, pattern) {
7130
7293
  return new RegExp(`^${source}$`).test(path);
7131
7294
  }
7132
7295
 
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
7296
  // src/sync/execution-pull.ts
7144
7297
  var DIRECTORIES = ["workspace", "downloads", "shared"];
7145
7298
  function relativeFilePath(file) {
@@ -7177,14 +7330,14 @@ async function writeActivities(sync, dir, execution) {
7177
7330
  return false;
7178
7331
  }
7179
7332
  }
7180
- async function listFiles(sync, executionId) {
7333
+ async function listFiles(sync, executionId2) {
7181
7334
  const listing = await sync.remote.json(
7182
- get(`/agents/v2/executions/${executionId}/files`)
7335
+ get(`/agents/v2/executions/${executionId2}/files`)
7183
7336
  );
7184
7337
  return DIRECTORIES.flatMap((name) => listing.directories[name] ?? []);
7185
7338
  }
7186
- async function download(sync, executionId, file) {
7187
- const path = `/agents/v2/executions/${executionId}/files/${file.id}/signed-url`;
7339
+ async function download(sync, executionId2, file) {
7340
+ const path = `/agents/v2/executions/${executionId2}/files/${file.id}/signed-url`;
7188
7341
  const { url } = await sync.remote.json(get(path));
7189
7342
  return sync.remote.download(url);
7190
7343
  }
@@ -7312,6 +7465,114 @@ ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
7312
7465
  ].filter(Boolean).join("\n");
7313
7466
  }
7314
7467
 
7468
+ // src/sync/insight-file.ts
7469
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
7470
+ var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)(?:\r?\n)?/;
7471
+ function serializeInsightFile(meta, body) {
7472
+ const front = {};
7473
+ if (meta.id) front.id = meta.id;
7474
+ front.type = meta.type;
7475
+ if (meta.status) front.status = meta.status;
7476
+ front.name = meta.name;
7477
+ if (meta.nextSteps) front.next_steps = meta.nextSteps;
7478
+ const yaml = stringifyYaml(front).trimEnd();
7479
+ return `---
7480
+ ${yaml}
7481
+ ---
7482
+
7483
+ ${body}`;
7484
+ }
7485
+ function insightFileId(text2) {
7486
+ const match = FRONTMATTER.exec(text2);
7487
+ if (!match) return void 0;
7488
+ try {
7489
+ const id = parseYaml(match[1])?.id;
7490
+ return typeof id === "string" || typeof id === "number" ? String(id) : void 0;
7491
+ } catch {
7492
+ return void 0;
7493
+ }
7494
+ }
7495
+
7496
+ // src/sync/insight-pull.ts
7497
+ var INSIGHTS_DIR = "insights";
7498
+ var PAGE_SIZE = 100;
7499
+ async function listInsights(sync, workflowId) {
7500
+ const all = [];
7501
+ for (let page = 1; ; page++) {
7502
+ const query = {
7503
+ page: String(page),
7504
+ pageSize: String(PAGE_SIZE)
7505
+ };
7506
+ if (workflowId) query.workflowId = workflowId;
7507
+ const { items, total } = await sync.remote.json(
7508
+ get("/astro/v2/insights", query)
7509
+ );
7510
+ all.push(...items);
7511
+ if (items.length === 0 || all.length >= total) return all;
7512
+ }
7513
+ }
7514
+ async function stemOwners(sync, dir) {
7515
+ const owners = /* @__PURE__ */ new Map();
7516
+ for (const name of await sync.files.list(dir)) {
7517
+ if (!name.endsWith(".md") || name.includes("/")) continue;
7518
+ const bytes = await sync.files.read(join(dir, name));
7519
+ const text2 = bytes ? Buffer.from(bytes).toString("utf8") : "";
7520
+ owners.set(name.slice(0, -".md".length).toLowerCase(), insightFileId(text2));
7521
+ }
7522
+ return owners;
7523
+ }
7524
+ function assignStems(owners, insights) {
7525
+ const used = /* @__PURE__ */ new Set();
7526
+ const heldByOther = (stem, id) => owners.has(stem) && owners.get(stem) !== id;
7527
+ return insights.map((insight) => {
7528
+ const base = lossySlugOr(insight.name, "insight");
7529
+ let stem = base;
7530
+ for (let n = 2; used.has(stem) || heldByOther(stem, insight.id); n++) {
7531
+ stem = `${base}-${n}`;
7532
+ }
7533
+ used.add(stem);
7534
+ return [insight, stem];
7535
+ });
7536
+ }
7537
+ async function pullInsights(sync, options = {}) {
7538
+ const dir = options.dir ?? INSIGHTS_DIR;
7539
+ const insights = options.insightId ? [
7540
+ await sync.remote.json(
7541
+ get(`/astro/v2/insights/${options.insightId}`)
7542
+ )
7543
+ ] : await listInsights(sync, options.workflowId);
7544
+ const pulled = [];
7545
+ for (const [insight, stem] of assignStems(
7546
+ await stemOwners(sync, dir),
7547
+ insights
7548
+ )) {
7549
+ const path = join(dir, `${stem}.md`);
7550
+ const text2 = serializeInsightFile(
7551
+ {
7552
+ id: insight.id,
7553
+ type: insight.type,
7554
+ status: insight.status,
7555
+ name: insight.name,
7556
+ nextSteps: insight.nextSteps ?? void 0
7557
+ },
7558
+ insight.content
7559
+ );
7560
+ await sync.files.write(path, Buffer.from(text2, "utf8"));
7561
+ const { id, name, type, status } = insight;
7562
+ pulled.push({ id, name, type, status, path });
7563
+ }
7564
+ return { dir, insights: pulled };
7565
+ }
7566
+ function formatInsightPull(result) {
7567
+ const count = result.insights.length;
7568
+ return [
7569
+ `Pulled ${count} insight${count === 1 ? "" : "s"} into ${result.dir}/.`,
7570
+ ...result.insights.map(
7571
+ (insight) => ` - ${insight.path}: ${insight.name} (${insight.type}, ${insight.status})`
7572
+ )
7573
+ ].join("\n");
7574
+ }
7575
+
7315
7576
  // src/sync/manifest.ts
7316
7577
  async function loadManifest(sync, dir) {
7317
7578
  const bytes = await sync.files.read(join(dir, MANIFEST));
@@ -7724,6 +7985,35 @@ function integer(parsed, name) {
7724
7985
  throw new UsageError(`--${name} must be a whole number`);
7725
7986
  return Number(raw);
7726
7987
  }
7988
+ function bounded(parsed, name, min, max) {
7989
+ const value2 = integer(parsed, name);
7990
+ if (value2 === void 0) return void 0;
7991
+ if (value2 < min || value2 > max)
7992
+ throw new UsageError(`--${name} must be between ${min} and ${max}`);
7993
+ return value2;
7994
+ }
7995
+ function executionWaitOptions(parsed) {
7996
+ return {
7997
+ executionId: parsed.positionals[0],
7998
+ timeoutSeconds: bounded(parsed, "timeout", 0, MAX_WAIT_TIMEOUT_SECONDS),
7999
+ intervalSeconds: bounded(parsed, "interval", 1, MAX_WAIT_INTERVAL_SECONDS)
8000
+ };
8001
+ }
8002
+ function given(value2, name) {
8003
+ if (value2 === void 0) return void 0;
8004
+ if (value2 === "") throw new UsageError(`${name} must not be empty`);
8005
+ return value2;
8006
+ }
8007
+ function insightPullOptions(parsed) {
8008
+ const options = {};
8009
+ const insightId = given(parsed.positionals[0], "<insight-id>");
8010
+ if (insightId) options.insightId = insightId;
8011
+ const workflowId = given(flag(parsed, "workflow-id"), "--workflow-id");
8012
+ if (workflowId) options.workflowId = workflowId;
8013
+ const dir = given(flag(parsed, "dir"), "--dir");
8014
+ if (dir) options.dir = dir;
8015
+ return options;
8016
+ }
7727
8017
  function executionOptions(parsed) {
7728
8018
  const summaryOnly = flag(parsed, "summary-only") === "true";
7729
8019
  const glob = flag(parsed, "files");
@@ -7785,6 +8075,20 @@ async function execute(command, parsed, sync, deps) {
7785
8075
  );
7786
8076
  return { result: result2, text: formatWorkflowPush(result2) };
7787
8077
  }
8078
+ if (command.command === "execution wait") {
8079
+ const result2 = await waitForExecution(sync, {
8080
+ ...executionWaitOptions(parsed),
8081
+ ...deps.executionWait
8082
+ });
8083
+ return { result: result2, text: formatExecutionWait(result2) };
8084
+ }
8085
+ if (command.command === "insight pull") {
8086
+ const result2 = await pullInsights(sync, {
8087
+ ...deps.insightPull,
8088
+ ...insightPullOptions(parsed)
8089
+ });
8090
+ return { result: result2, text: formatInsightPull(result2) };
8091
+ }
7788
8092
  if (command.command !== "execution pull")
7789
8093
  throw new UsageError(`unknown command ${command.command}`);
7790
8094
  const result = await pullExecution(sync, {
@@ -7807,15 +8111,20 @@ async function runSync(command, parsed, deps) {
7807
8111
  export {
7808
8112
  ALL_COMMANDS,
7809
8113
  COMMANDS,
8114
+ INSIGHTS_DIR,
7810
8115
  MANIFEST,
7811
8116
  SYNC_COMMANDS,
7812
8117
  UsageError,
7813
8118
  buildRequest,
7814
8119
  formatActivitiesMarkdown,
7815
8120
  formatExecutionPull,
8121
+ formatExecutionWait,
8122
+ formatInsightPull,
7816
8123
  formatWorkflowPull,
7817
8124
  formatWorkflowPush,
7818
8125
  headFilesRequest,
8126
+ insightFileId,
8127
+ isTerminalExecutionStatus,
7819
8128
  leadingWords,
7820
8129
  loadManifest,
7821
8130
  matchCommand,
@@ -7823,12 +8132,15 @@ export {
7823
8132
  parseInvocation,
7824
8133
  pullDir,
7825
8134
  pullExecution,
8135
+ pullInsights,
7826
8136
  pullWorkflow,
7827
8137
  pushWorkflow,
7828
8138
  readManifest,
7829
8139
  render,
7830
8140
  runSync,
7831
8141
  runsLocally,
8142
+ serializeInsightFile,
8143
+ waitForExecution,
7832
8144
  withSyncCommands,
7833
8145
  writeManifest
7834
8146
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteroidai/cli",
3
- "version": "1.9.115",
3
+ "version": "1.9.122",
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": {