@asteroidai/cli 1.9.98 → 1.9.103

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 +367 -81
  2. package/dist/core.js +379 -81
  3. package/package.json +1 -1
package/dist/asteroid.js CHANGED
@@ -25,6 +25,61 @@ var COMMANDS = [
25
25
  readOnly: false,
26
26
  destructive: true
27
27
  },
28
+ {
29
+ command: "batch create",
30
+ tool: "execution_batch_create",
31
+ title: "Create execution batch",
32
+ description: 'Queue one execution per row of `items` for `workflowId`, starting at `startAt` (ISO 8601; now or earlier starts at once). `config` paces the runs: `{type: "maxConcurrent", maxConcurrent}` or `{type: "timeBatching", batchSize, batchInterval}` (seconds). Each item takes `inputs` (see `workflow_version_inputs_get`), optional `profileId` or `profileGroupId`, `metadata`, and `workflowVersionId` to pin a version. Check the same body first with `execution_batch_validate`. Returns the batch; follow it with `execution_batch_get`, and its runs with `executions_list` filtered by `batchId`.',
33
+ method: "POST",
34
+ path: "/agents/v2/execution-batches",
35
+ args: [],
36
+ query: [],
37
+ binary: false,
38
+ readOnly: false,
39
+ destructive: false,
40
+ body: {
41
+ required: true,
42
+ contentType: "application/json",
43
+ fields: [
44
+ {
45
+ name: "workflowId",
46
+ flag: "workflow-id",
47
+ type: "string",
48
+ required: false,
49
+ description: "Workflow ID for this batch. Set this or agentId."
50
+ },
51
+ {
52
+ name: "name",
53
+ flag: "name",
54
+ type: "string",
55
+ required: true
56
+ },
57
+ {
58
+ name: "startAt",
59
+ flag: "start-at",
60
+ type: "string",
61
+ required: true,
62
+ description: "When to start triggering executions"
63
+ },
64
+ {
65
+ name: "config",
66
+ flag: "config",
67
+ type: "json",
68
+ required: true,
69
+ description: "Discriminated union of batch configuration types (discriminated by type)"
70
+ },
71
+ {
72
+ name: "items",
73
+ flag: "items",
74
+ type: "json",
75
+ required: true
76
+ }
77
+ ],
78
+ files: [],
79
+ credentialOrganization: false
80
+ },
81
+ capability: "run_execution"
82
+ },
28
83
  {
29
84
  command: "batch get",
30
85
  tool: "execution_batch_get",
@@ -127,6 +182,60 @@ var COMMANDS = [
127
182
  destructive: false,
128
183
  capability: "run_execution"
129
184
  },
185
+ {
186
+ command: "batch validate",
187
+ tool: "execution_batch_validate",
188
+ title: "Validate execution batch",
189
+ description: "Check an `execution_batch_create` body without creating anything. Returns `valid`, `itemCount`, and `issues`: each row (by its 0-based `index` in `items`) whose inputs would fail the input schema of the version it runs. A problem with the whole batch, such as an empty `items` or a bad `config`, returns the same error create would.",
190
+ method: "POST",
191
+ path: "/agents/v2/execution-batches/validate",
192
+ args: [],
193
+ query: [],
194
+ binary: false,
195
+ readOnly: true,
196
+ destructive: false,
197
+ body: {
198
+ required: true,
199
+ contentType: "application/json",
200
+ fields: [
201
+ {
202
+ name: "workflowId",
203
+ flag: "workflow-id",
204
+ type: "string",
205
+ required: false,
206
+ description: "Workflow ID for this batch. Set this or agentId."
207
+ },
208
+ {
209
+ name: "name",
210
+ flag: "name",
211
+ type: "string",
212
+ required: true
213
+ },
214
+ {
215
+ name: "startAt",
216
+ flag: "start-at",
217
+ type: "string",
218
+ required: true,
219
+ description: "When to start triggering executions"
220
+ },
221
+ {
222
+ name: "config",
223
+ flag: "config",
224
+ type: "json",
225
+ required: true,
226
+ description: "Discriminated union of batch configuration types (discriminated by type)"
227
+ },
228
+ {
229
+ name: "items",
230
+ flag: "items",
231
+ type: "json",
232
+ required: true
233
+ }
234
+ ],
235
+ files: [],
236
+ credentialOrganization: false
237
+ }
238
+ },
130
239
  {
131
240
  command: "context",
132
241
  tool: "get_context",
@@ -1336,6 +1445,13 @@ var COMMANDS = [
1336
1445
  type: "string[]",
1337
1446
  required: false,
1338
1447
  description: "Filter by login profile IDs (can specify multiple, there is an 'OR' condition applied to these)"
1448
+ },
1449
+ {
1450
+ name: "batchId",
1451
+ flag: "batch-id",
1452
+ type: "string",
1453
+ required: false,
1454
+ description: "Only executions started by this batch, an ID from `execution_batches_list`. Rows not yet triggered have no execution yet: list them with `scheduled_executions_list` and `batchId`."
1339
1455
  }
1340
1456
  ],
1341
1457
  binary: false,
@@ -1610,6 +1726,13 @@ var COMMANDS = [
1610
1726
  type: "string[]",
1611
1727
  required: false,
1612
1728
  description: "Filter by login profile IDs (can specify multiple, there is an 'OR' condition applied to these)"
1729
+ },
1730
+ {
1731
+ name: "batchId",
1732
+ flag: "batch-id",
1733
+ type: "string",
1734
+ required: false,
1735
+ description: "Only executions started by this batch, an ID from `execution_batches_list`. Rows not yet triggered have no execution yet: list them with `scheduled_executions_list` and `batchId`."
1613
1736
  }
1614
1737
  ],
1615
1738
  binary: false,
@@ -1907,7 +2030,7 @@ var COMMANDS = [
1907
2030
  command: "portal-skills add",
1908
2031
  tool: "portal_skills_add",
1909
2032
  title: "Add portal skills",
1910
- description: "Add a portal's skills to an organization, by portal id. It is general knowledge about how a portal works. Needs a Startup plan or above. Astro loads the portal's skills once it is added. Adding one the organization already has is a no-op.",
2033
+ description: "Add a portal's skills to an organization, by a `portalId` from `portal_skills_list`. Needs a Startup plan or above. Astro loads the portal's skills once it is added. Adding one the organization already has is a no-op.",
1911
2034
  method: "POST",
1912
2035
  path: "/agents/v2/portal-skills/{portalId}",
1913
2036
  args: [
@@ -1923,6 +2046,27 @@ var COMMANDS = [
1923
2046
  readOnly: false,
1924
2047
  destructive: false
1925
2048
  },
2049
+ {
2050
+ command: "portal-skills list",
2051
+ tool: "portal_skills_list",
2052
+ title: "List portal skills",
2053
+ description: "List the skills for third-party portals, or only for the portals matching a portal name or task, such as `caloptima eligibility`. A portal skill is general knowledge about how a portal works: sign-in, page quirks, field mappings. Each portal comes with its skill names, and whether the organization has added it. Add one with `portal_skills_add`.",
2054
+ method: "GET",
2055
+ path: "/astro/v2/portal-skills",
2056
+ args: [],
2057
+ query: [
2058
+ {
2059
+ name: "query",
2060
+ flag: "query",
2061
+ type: "string",
2062
+ required: false,
2063
+ description: "A portal name or task, at least three characters. Omit to list every portal with skills."
2064
+ }
2065
+ ],
2066
+ binary: false,
2067
+ readOnly: true,
2068
+ destructive: false
2069
+ },
1926
2070
  {
1927
2071
  command: "profile clear-cache",
1928
2072
  tool: "profile_clear_browser_cache",
@@ -4441,7 +4585,7 @@ var COMMANDS = [
4441
4585
  command: "workflow head publish",
4442
4586
  tool: "workflow_head_publish",
4443
4587
  title: "Publish workflow head",
4444
- description: "Publish the workflow's editable head, assigning it the next version number and making it the default for `workflow_execute_post`. Use after `workflow_files_patch`; `workflow_version_publish` does the same for a version you already have the ID of.",
4588
+ description: "Publish the workflow's editable head, assigning it the next version number and making it the default for `workflow_execute_post`. Use after `workflow_files_patch`; `workflow_version_publish` does the same for a version you already have the ID of. `schemaChanges` lists the input and output schema changes against the version published before, which can break callers that send inputs or read outputs by name.",
4445
4589
  method: "POST",
4446
4590
  path: "/agents/v2/workflows/{workflowId}/head/publish",
4447
4591
  args: [
@@ -5378,7 +5522,7 @@ var COMMANDS = [
5378
5522
  command: "workflow version publish",
5379
5523
  tool: "workflow_version_publish",
5380
5524
  title: "Publish workflow version",
5381
- description: "Publish a workflow version, assigning it a permanent version number and making it the default for `workflow_execute_post`.",
5525
+ description: "Publish a workflow version, assigning it a permanent version number and making it the default for `workflow_execute_post`. This also publishes an older version again. `schemaChanges` lists the input and output schema changes against the version published before, which can break callers that send inputs or read outputs by name.",
5382
5526
  method: "POST",
5383
5527
  path: "/agents/v2/workflows/{workflowId}/versions/{versionId}/publish",
5384
5528
  args: [
@@ -6033,16 +6177,26 @@ function leafOptions(leaf, command) {
6033
6177
  for (const file of command.body?.files ?? []) {
6034
6178
  leaf.option(`--${file} <path>`, "File to upload. Repeatable.", collect);
6035
6179
  }
6036
- if (command.files === "pull")
6180
+ if (command.files === "pull") {
6037
6181
  leaf.option(
6038
6182
  "--out <dir>",
6039
6183
  "Write here instead of ./<workflow-name>/head (a version: ./<workflow-name>/.versions/...). --json prints the file listing instead."
6040
6184
  );
6041
- if (command.files === "push")
6185
+ leaf.option(
6186
+ "--keep-edits",
6187
+ "Into a folder pulled before: keep every file you edited since that pull and refresh the rest. From a head, a later push sends those edits on top of the new revision."
6188
+ );
6189
+ }
6190
+ if (command.files === "push") {
6042
6191
  leaf.option(
6043
6192
  "--from <dir>",
6044
6193
  "The pulled head folder to push. Defaults to the current folder."
6045
6194
  );
6195
+ leaf.option(
6196
+ "--rebase",
6197
+ "When the head moved since your pull, pull it with --keep-edits and push again, twice at most."
6198
+ );
6199
+ }
6046
6200
  }
6047
6201
  function toParsed(leaf, positionals, dotted2) {
6048
6202
  const globals = leaf.optsWithGlobals();
@@ -6649,7 +6803,7 @@ var ApiError = class extends Error {
6649
6803
  status;
6650
6804
  body;
6651
6805
  };
6652
- var VERSION = "1.9.98";
6806
+ var VERSION = "1.9.103";
6653
6807
  function problem(status, body, fallback) {
6654
6808
  if (body && typeof body === "object") {
6655
6809
  const record2 = body;
@@ -7749,6 +7903,18 @@ function nodeFiles(cwd) {
7749
7903
  };
7750
7904
  }
7751
7905
 
7906
+ // src/sync/manifest.ts
7907
+ async function loadManifest(sync, dir) {
7908
+ const bytes = await sync.files.read(join3(dir, MANIFEST));
7909
+ if (!bytes) return void 0;
7910
+ return JSON.parse(Buffer.from(bytes).toString("utf8"));
7911
+ }
7912
+ async function writeManifest(sync, dir, manifest) {
7913
+ const text3 = `${JSON.stringify(manifest, null, 2)}
7914
+ `;
7915
+ await sync.files.write(join3(dir, MANIFEST), Buffer.from(text3, "utf8"));
7916
+ }
7917
+
7752
7918
  // src/sync/workflow-pull.ts
7753
7919
  var CONCURRENCY = 16;
7754
7920
  function pullDir(root, tree, head) {
@@ -7756,6 +7922,9 @@ function pullDir(root, tree, head) {
7756
7922
  if (head) return join3(base, "head");
7757
7923
  return join3(base, ".versions", workflowDirName(tree.versionId, tree.version));
7758
7924
  }
7925
+ function headFilesRequest(workflowId) {
7926
+ return get(`/agents/v2/workflows/${workflowId}/head/files`);
7927
+ }
7759
7928
  async function agentFileBytes(sync, workflowId, tree, entry) {
7760
7929
  const path = `/agents/v2/workflows/${workflowId}/versions/${tree.versionId}/files/${entry.fileId}/signed-url`;
7761
7930
  const { url } = await sync.remote.json(get(path));
@@ -7765,32 +7934,105 @@ async function entryBytes(sync, workflowId, tree, entry) {
7765
7934
  if (entry.contentBase64 !== void 0)
7766
7935
  return Buffer.from(entry.contentBase64, "base64");
7767
7936
  if (entry.content !== void 0) return Buffer.from(entry.content, "utf8");
7937
+ if (entry.storageObjectKey && sync.remote.object)
7938
+ return sync.remote.object(entry.storageObjectKey);
7768
7939
  if (!entry.fileId) return void 0;
7769
7940
  return agentFileBytes(sync, workflowId, tree, entry);
7770
7941
  }
7771
- async function writeEntries(sync, options, tree, dir) {
7772
- const hashes = {};
7773
- const skipped = [];
7774
- let shared = 0;
7775
- const wanted = tree.files.filter((entry) => {
7942
+ function inlineBytes(entry) {
7943
+ if (entry.contentBase64 !== void 0)
7944
+ return Buffer.from(entry.contentBase64, "base64");
7945
+ if (entry.content !== void 0) return Buffer.from(entry.content, "utf8");
7946
+ return void 0;
7947
+ }
7948
+ function wantedEntries(options, tree, skipped) {
7949
+ return tree.files.filter((entry) => {
7776
7950
  if (!isContained(entry.path)) skipped.push(`${entry.path} (unsafe path)`);
7777
7951
  else if (options.maxFileBytes !== void 0 && entry.size > options.maxFileBytes)
7778
7952
  skipped.push(`${entry.path} (${entry.size} bytes, over the size limit)`);
7779
7953
  else return true;
7780
7954
  return false;
7781
7955
  });
7782
- for (let i = 0; i < wanted.length; i += CONCURRENCY) {
7783
- await Promise.all(
7784
- wanted.slice(i, i + CONCURRENCY).map(async (entry) => {
7785
- const bytes = await entryBytes(sync, options.workflowId, tree, entry);
7786
- if (!bytes) return void skipped.push(`${entry.path} (no content)`);
7787
- await sync.files.write(join3(dir, entry.path), bytes);
7788
- hashes[entry.path] = sha256(bytes);
7789
- if (entry.kind === "agent_file") shared++;
7790
- })
7791
- );
7956
+ }
7957
+ async function inBatches(items, each) {
7958
+ for (let i = 0; i < items.length; i += CONCURRENCY) {
7959
+ await Promise.all(items.slice(i, i + CONCURRENCY).map(each));
7792
7960
  }
7793
- return { hashes, skipped, shared };
7961
+ }
7962
+ async function writeEntries(sync, options, tree, dir) {
7963
+ const hashes = {};
7964
+ const skipped = [];
7965
+ let shared = 0;
7966
+ let written = 0;
7967
+ await inBatches(wantedEntries(options, tree, skipped), async (entry) => {
7968
+ const bytes = await entryBytes(sync, options.workflowId, tree, entry);
7969
+ if (!bytes) return void skipped.push(`${entry.path} (no content)`);
7970
+ await sync.files.write(join3(dir, entry.path), bytes);
7971
+ hashes[entry.path] = sha256(bytes);
7972
+ written++;
7973
+ if (entry.kind === "agent_file") shared++;
7974
+ });
7975
+ return { hashes, skipped, shared, kept: [], written };
7976
+ }
7977
+ async function mergeEntry(sync, options, tree, dir, previous, entry, out) {
7978
+ const local2 = await sync.files.read(join3(dir, entry.path));
7979
+ const localHash = local2 ? sha256(local2) : void 0;
7980
+ const baseline = previous.files[entry.path];
7981
+ const edited = baseline === void 0 ? local2 !== void 0 : localHash !== baseline;
7982
+ let bytes = inlineBytes(entry);
7983
+ let serverHash = bytes ? sha256(bytes) : entry.checksum;
7984
+ if (edited) {
7985
+ if (serverHash === void 0) {
7986
+ bytes = await entryBytes(sync, options.workflowId, tree, entry);
7987
+ if (!bytes) return void out.skipped.push(`${entry.path} (no content)`);
7988
+ serverHash = sha256(bytes);
7989
+ }
7990
+ out.hashes[entry.path] = serverHash;
7991
+ out.kept.push(entry.path);
7992
+ return;
7993
+ }
7994
+ if (serverHash !== void 0 && serverHash === localHash) {
7995
+ out.hashes[entry.path] = serverHash;
7996
+ return;
7997
+ }
7998
+ bytes ??= await entryBytes(sync, options.workflowId, tree, entry);
7999
+ if (!bytes) return void out.skipped.push(`${entry.path} (no content)`);
8000
+ serverHash = sha256(bytes);
8001
+ if (serverHash !== localHash) {
8002
+ await sync.files.write(join3(dir, entry.path), bytes);
8003
+ out.written++;
8004
+ if (entry.kind === "agent_file") out.shared++;
8005
+ }
8006
+ out.hashes[entry.path] = serverHash;
8007
+ }
8008
+ async function mergeEntries(sync, options, tree, dir, previous) {
8009
+ const out = {
8010
+ hashes: {},
8011
+ skipped: [],
8012
+ shared: 0,
8013
+ kept: [],
8014
+ written: 0
8015
+ };
8016
+ const wanted = wantedEntries(options, tree, out.skipped);
8017
+ await inBatches(
8018
+ wanted,
8019
+ (entry) => mergeEntry(sync, options, tree, dir, previous, entry, out)
8020
+ );
8021
+ const remote = new Set(tree.files.map((entry) => entry.path));
8022
+ for (const path of Object.keys(previous.files)) {
8023
+ if (path in out.hashes) continue;
8024
+ if (remote.has(path)) {
8025
+ out.hashes[path] = previous.files[path];
8026
+ continue;
8027
+ }
8028
+ const local2 = await sync.files.read(join3(dir, path));
8029
+ if (!local2) continue;
8030
+ if (sha256(local2) === previous.files[path])
8031
+ await sync.files.remove(join3(dir, path));
8032
+ else out.kept.push(path);
8033
+ }
8034
+ out.kept.sort();
8035
+ return out;
7794
8036
  }
7795
8037
  async function removeStale(sync, dir, keep) {
7796
8038
  for (const path of await sync.files.list(dir)) {
@@ -7798,11 +8040,6 @@ async function removeStale(sync, dir, keep) {
7798
8040
  await sync.files.remove(join3(dir, path));
7799
8041
  }
7800
8042
  }
7801
- async function writeManifest(sync, dir, manifest) {
7802
- const text3 = `${JSON.stringify(manifest, null, 2)}
7803
- `;
7804
- await sync.files.write(join3(dir, MANIFEST), Buffer.from(text3, "utf8"));
7805
- }
7806
8043
  async function pullWorkflow(sync, options) {
7807
8044
  const request = {
7808
8045
  ...options.request,
@@ -7812,20 +8049,25 @@ async function pullWorkflow(sync, options) {
7812
8049
  const tree = await sync.remote.json(request);
7813
8050
  const head = request.path.endsWith("/head/files");
7814
8051
  const dir = options.dir ?? pullDir(options.root ?? "", tree, head);
7815
- const { hashes, skipped, shared } = await writeEntries(
7816
- sync,
7817
- options,
7818
- tree,
7819
- dir
7820
- );
7821
- await removeStale(sync, dir, new Set(Object.keys(hashes)));
8052
+ const previous = options.keepEdits ? await loadManifest(sync, dir) : void 0;
8053
+ const written = previous ? await mergeEntries(sync, options, tree, dir, previous) : await writeEntries(sync, options, tree, dir);
8054
+ if (!previous)
8055
+ await removeStale(sync, dir, new Set(Object.keys(written.hashes)));
7822
8056
  if (options.manifest !== false) {
8057
+ const variants = request.query.get("variants") ?? void 0;
7823
8058
  await writeManifest(sync, dir, {
7824
8059
  workflowId: options.workflowId,
7825
8060
  versionId: tree.versionId,
7826
8061
  head,
7827
8062
  rev: tree.rev,
7828
- files: hashes
8063
+ files: written.hashes,
8064
+ ...variants !== void 0 && { variants },
8065
+ ...options.maxFileBytes !== void 0 && {
8066
+ maxFileBytes: options.maxFileBytes
8067
+ },
8068
+ ...options.maxTotalBytes !== void 0 && {
8069
+ maxTotalBytes: options.maxTotalBytes
8070
+ }
7829
8071
  });
7830
8072
  }
7831
8073
  return {
@@ -7836,9 +8078,11 @@ async function pullWorkflow(sync, options) {
7836
8078
  rev: tree.rev,
7837
8079
  dir,
7838
8080
  head,
7839
- written: Object.keys(hashes).length,
7840
- shared,
7841
- skipped
8081
+ written: written.written,
8082
+ shared: written.shared,
8083
+ skipped: written.skipped,
8084
+ kept: written.kept,
8085
+ ...tree.variantKeys && { variantKeys: tree.variantKeys }
7842
8086
  };
7843
8087
  }
7844
8088
  function formatWorkflowPull(result) {
@@ -7849,6 +8093,8 @@ function formatWorkflowPull(result) {
7849
8093
  `Workflow version: ${result.versionId}${result.version ? ` (v${result.version})` : " (draft)"}`,
7850
8094
  `Written to: ${result.dir}/`,
7851
8095
  files,
8096
+ result.kept.length > 0 ? `Kept your edits in:
8097
+ ${result.kept.map((s) => ` - ${s}`).join("\n")}` : null,
7852
8098
  result.skipped.length > 0 ? `Skipped:
7853
8099
  ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
7854
8100
  result.head ? `Edit the files, then run \`asteroid workflow push\` from ${result.dir}/.` : "This is a pinned version. It is read-only."
@@ -7856,14 +8102,14 @@ ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
7856
8102
  }
7857
8103
 
7858
8104
  // src/sync/workflow-push.ts
8105
+ var REBASE_ATTEMPTS = 3;
7859
8106
  async function readManifest(sync, dir) {
7860
- const bytes = await sync.files.read(join3(dir, MANIFEST));
7861
- if (!bytes) {
8107
+ const manifest = await loadManifest(sync, dir);
8108
+ if (!manifest) {
7862
8109
  throw new UsageError(
7863
8110
  `no pulled workflow in ${dir}. Run \`asteroid workflow pull <workflow-id>\`, then push from the head folder it writes.`
7864
8111
  );
7865
8112
  }
7866
- const manifest = JSON.parse(Buffer.from(bytes).toString("utf8"));
7867
8113
  if (!manifest.head) {
7868
8114
  throw new UsageError(
7869
8115
  `${dir} holds a pinned workflow version, which is read-only. Push from a pulled head.`
@@ -7893,25 +8139,44 @@ async function localChanges(sync, dir, manifest) {
7893
8139
  );
7894
8140
  return { writes, deletes };
7895
8141
  }
8142
+ function conflict(manifest) {
8143
+ return new UsageError(
8144
+ `the workflow head changed since your pull (rev ${manifest.rev}). Run \`asteroid workflow pull ${manifest.workflowId} --keep-edits\` to bring its changes under yours, then push again, or push with --rebase to do both.`
8145
+ );
8146
+ }
7896
8147
  async function patch(sync, manifest, writes, deletes) {
7897
8148
  try {
7898
8149
  await sync.remote.json({
7899
8150
  method: "PATCH",
7900
8151
  path: `/agents/v2/workflows/${manifest.workflowId}/head/files`,
7901
8152
  query: new URLSearchParams(),
7902
- json: { baseRev: manifest.rev, writes, deletes }
8153
+ json: {
8154
+ baseRev: manifest.rev,
8155
+ writes: writes.map(({ path, contentBase64 }) => ({
8156
+ path,
8157
+ contentBase64
8158
+ })),
8159
+ deletes
8160
+ }
7903
8161
  });
8162
+ return "applied";
7904
8163
  } catch (error) {
7905
8164
  if (statusOf(error) !== 409) throw error;
7906
- throw new UsageError(
7907
- `the workflow head changed since your pull (rev ${manifest.rev}). Copy your edits aside, run \`asteroid workflow pull ${manifest.workflowId}\`, reapply them, then push again.`
7908
- );
8165
+ return "conflict";
7909
8166
  }
7910
8167
  }
8168
+ function headRequest(manifest) {
8169
+ const request = headFilesRequest(manifest.workflowId);
8170
+ if (manifest.variants !== void 0)
8171
+ request.query.set("variants", manifest.variants);
8172
+ if (manifest.maxFileBytes !== void 0)
8173
+ request.query.set("maxFileBytes", String(manifest.maxFileBytes));
8174
+ if (manifest.maxTotalBytes !== void 0)
8175
+ request.query.set("maxTotalBytes", String(manifest.maxTotalBytes));
8176
+ return request;
8177
+ }
7911
8178
  async function refresh2(sync, dir, manifest, pushed) {
7912
- const tree = await sync.remote.json(
7913
- get(`/agents/v2/workflows/${manifest.workflowId}/head/files`)
7914
- );
8179
+ const tree = await sync.remote.json(headRequest(manifest));
7915
8180
  const files = {};
7916
8181
  for (const entry of tree.files) {
7917
8182
  if (entry.content !== void 0) {
@@ -7921,7 +8186,7 @@ async function refresh2(sync, dir, manifest, pushed) {
7921
8186
  await sync.files.write(join3(dir, entry.path), bytes);
7922
8187
  } else {
7923
8188
  const local2 = await sync.files.read(join3(dir, entry.path));
7924
- const hash = local2 ? sha256(local2) : pushed.get(entry.path) ?? entry.checksum;
8189
+ const hash = local2 ? sha256(local2) : pushed.get(entry.path);
7925
8190
  if (hash) files[entry.path] = hash;
7926
8191
  }
7927
8192
  }
@@ -7933,43 +8198,58 @@ async function refresh2(sync, dir, manifest, pushed) {
7933
8198
  await writeManifest(sync, dir, next);
7934
8199
  return next;
7935
8200
  }
7936
- async function pushWorkflow(sync, dir, workflowId) {
7937
- const manifest = await readManifest(sync, dir);
8201
+ async function rebase(sync, dir, manifest) {
8202
+ await pullWorkflow(sync, {
8203
+ workflowId: manifest.workflowId,
8204
+ request: headRequest(manifest),
8205
+ dir,
8206
+ keepEdits: true,
8207
+ maxFileBytes: manifest.maxFileBytes,
8208
+ maxTotalBytes: manifest.maxTotalBytes
8209
+ });
8210
+ return readManifest(sync, dir);
8211
+ }
8212
+ async function pushWorkflow(sync, dir, workflowId, options = {}) {
8213
+ let manifest = await readManifest(sync, dir);
7938
8214
  if (workflowId !== void 0 && manifest.workflowId !== workflowId) {
7939
8215
  throw new UsageError(
7940
8216
  `${dir} holds workflow ${manifest.workflowId}, not ${workflowId}.`
7941
8217
  );
7942
8218
  }
7943
- const { writes, deletes } = await localChanges(sync, dir, manifest);
7944
- const result = {
7945
- workflowId: manifest.workflowId,
7946
- dir,
7947
- written: writes.map((w) => w.path),
7948
- deleted: deletes
7949
- };
7950
- if (writes.length === 0 && deletes.length === 0) {
7951
- return { ...result, rev: manifest.rev, versionId: manifest.versionId };
8219
+ let rebasedFrom;
8220
+ for (let attempt = 1; ; attempt++) {
8221
+ const { writes, deletes } = await localChanges(sync, dir, manifest);
8222
+ const result = {
8223
+ workflowId: manifest.workflowId,
8224
+ dir,
8225
+ written: writes.map((w) => w.path),
8226
+ deleted: deletes,
8227
+ ...rebasedFrom !== void 0 && { rebasedFrom }
8228
+ };
8229
+ if (writes.length === 0 && deletes.length === 0) {
8230
+ return { ...result, rev: manifest.rev, versionId: manifest.versionId };
8231
+ }
8232
+ if (await patch(sync, manifest, writes, deletes) === "applied") {
8233
+ const next = await refresh2(
8234
+ sync,
8235
+ dir,
8236
+ manifest,
8237
+ new Map(writes.map((w) => [w.path, w.hash]))
8238
+ );
8239
+ return { ...result, rev: next.rev, versionId: next.versionId };
8240
+ }
8241
+ if (!options.rebase || attempt >= REBASE_ATTEMPTS) throw conflict(manifest);
8242
+ rebasedFrom ??= manifest.rev;
8243
+ manifest = await rebase(sync, dir, manifest);
7952
8244
  }
7953
- await patch(
7954
- sync,
7955
- manifest,
7956
- writes.map(({ path, contentBase64 }) => ({ path, contentBase64 })),
7957
- deletes
7958
- );
7959
- const next = await refresh2(
7960
- sync,
7961
- dir,
7962
- manifest,
7963
- new Map(writes.map((w) => [w.path, w.hash]))
7964
- );
7965
- return { ...result, rev: next.rev, versionId: next.versionId };
7966
8245
  }
7967
8246
  function formatWorkflowPush(result) {
8247
+ const rebased = result.rebasedFrom !== void 0 ? ` Rebased on the head first: your pull was rev ${result.rebasedFrom}.` : "";
7968
8248
  if (result.written.length === 0 && result.deleted.length === 0) {
7969
- return `Nothing to push: ${result.dir}/ matches the head you pulled (rev ${result.rev}).`;
8249
+ return `Nothing to push: ${result.dir}/ matches the head you pulled (rev ${result.rev}).${rebased}`;
7970
8250
  }
7971
8251
  return [
7972
- `Pushed to workflow ${result.workflowId}: ${result.written.length} written, ${result.deleted.length} deleted.`,
8252
+ `Pushed to workflow ${result.workflowId}: ${result.written.length} written, ${result.deleted.length} deleted.${rebased}`,
7973
8253
  ...result.written.map((path) => ` + ${path}`),
7974
8254
  ...result.deleted.map((path) => ` - ${path}`),
7975
8255
  `Head is now rev ${result.rev} (version ${result.versionId}).`
@@ -8000,9 +8280,9 @@ function executionOptions(parsed) {
8000
8280
  dir: dir ? () => dir : void 0
8001
8281
  };
8002
8282
  }
8003
- function withoutFlag(parsed, name) {
8283
+ function withoutFlags(parsed, ...names) {
8004
8284
  const flags = new Map(parsed.flags);
8005
- flags.delete(name);
8285
+ for (const name of names) flags.delete(name);
8006
8286
  return { ...parsed, flags };
8007
8287
  }
8008
8288
  function runsLocally(command, parsed) {
@@ -8023,11 +8303,16 @@ async function pull(command, parsed, sync, deps) {
8023
8303
  rejectPartial(parsed);
8024
8304
  const result = await pullWorkflow(sync, {
8025
8305
  workflowId: parsed.positionals[0],
8026
- request: await buildRequest(command, withoutFlag(parsed, "out"), {
8027
- io: deps.io
8028
- }),
8306
+ request: await buildRequest(
8307
+ command,
8308
+ withoutFlags(parsed, "out", "keep-edits"),
8309
+ {
8310
+ io: deps.io
8311
+ }
8312
+ ),
8029
8313
  dir: flag(parsed, "out"),
8030
8314
  root: deps.root ?? "",
8315
+ keepEdits: flag(parsed, "keep-edits") === "true",
8031
8316
  ...deps.workflowPull
8032
8317
  });
8033
8318
  return { result, text: formatWorkflowPull(result) };
@@ -8038,7 +8323,8 @@ async function execute(command, parsed, sync, deps) {
8038
8323
  const result2 = await pushWorkflow(
8039
8324
  sync,
8040
8325
  flag(parsed, "from") ?? ".",
8041
- parsed.positionals[0] || void 0
8326
+ parsed.positionals[0] || void 0,
8327
+ { rebase: flag(parsed, "rebase") === "true" }
8042
8328
  );
8043
8329
  return { result: result2, text: formatWorkflowPush(result2) };
8044
8330
  }
package/dist/core.js CHANGED
@@ -46,6 +46,61 @@ var COMMANDS = [
46
46
  readOnly: false,
47
47
  destructive: true
48
48
  },
49
+ {
50
+ command: "batch create",
51
+ tool: "execution_batch_create",
52
+ title: "Create execution batch",
53
+ description: 'Queue one execution per row of `items` for `workflowId`, starting at `startAt` (ISO 8601; now or earlier starts at once). `config` paces the runs: `{type: "maxConcurrent", maxConcurrent}` or `{type: "timeBatching", batchSize, batchInterval}` (seconds). Each item takes `inputs` (see `workflow_version_inputs_get`), optional `profileId` or `profileGroupId`, `metadata`, and `workflowVersionId` to pin a version. Check the same body first with `execution_batch_validate`. Returns the batch; follow it with `execution_batch_get`, and its runs with `executions_list` filtered by `batchId`.',
54
+ method: "POST",
55
+ path: "/agents/v2/execution-batches",
56
+ args: [],
57
+ query: [],
58
+ binary: false,
59
+ readOnly: false,
60
+ destructive: false,
61
+ body: {
62
+ required: true,
63
+ contentType: "application/json",
64
+ fields: [
65
+ {
66
+ name: "workflowId",
67
+ flag: "workflow-id",
68
+ type: "string",
69
+ required: false,
70
+ description: "Workflow ID for this batch. Set this or agentId."
71
+ },
72
+ {
73
+ name: "name",
74
+ flag: "name",
75
+ type: "string",
76
+ required: true
77
+ },
78
+ {
79
+ name: "startAt",
80
+ flag: "start-at",
81
+ type: "string",
82
+ required: true,
83
+ description: "When to start triggering executions"
84
+ },
85
+ {
86
+ name: "config",
87
+ flag: "config",
88
+ type: "json",
89
+ required: true,
90
+ description: "Discriminated union of batch configuration types (discriminated by type)"
91
+ },
92
+ {
93
+ name: "items",
94
+ flag: "items",
95
+ type: "json",
96
+ required: true
97
+ }
98
+ ],
99
+ files: [],
100
+ credentialOrganization: false
101
+ },
102
+ capability: "run_execution"
103
+ },
49
104
  {
50
105
  command: "batch get",
51
106
  tool: "execution_batch_get",
@@ -148,6 +203,60 @@ var COMMANDS = [
148
203
  destructive: false,
149
204
  capability: "run_execution"
150
205
  },
206
+ {
207
+ command: "batch validate",
208
+ tool: "execution_batch_validate",
209
+ title: "Validate execution batch",
210
+ description: "Check an `execution_batch_create` body without creating anything. Returns `valid`, `itemCount`, and `issues`: each row (by its 0-based `index` in `items`) whose inputs would fail the input schema of the version it runs. A problem with the whole batch, such as an empty `items` or a bad `config`, returns the same error create would.",
211
+ method: "POST",
212
+ path: "/agents/v2/execution-batches/validate",
213
+ args: [],
214
+ query: [],
215
+ binary: false,
216
+ readOnly: true,
217
+ destructive: false,
218
+ body: {
219
+ required: true,
220
+ contentType: "application/json",
221
+ fields: [
222
+ {
223
+ name: "workflowId",
224
+ flag: "workflow-id",
225
+ type: "string",
226
+ required: false,
227
+ description: "Workflow ID for this batch. Set this or agentId."
228
+ },
229
+ {
230
+ name: "name",
231
+ flag: "name",
232
+ type: "string",
233
+ required: true
234
+ },
235
+ {
236
+ name: "startAt",
237
+ flag: "start-at",
238
+ type: "string",
239
+ required: true,
240
+ description: "When to start triggering executions"
241
+ },
242
+ {
243
+ name: "config",
244
+ flag: "config",
245
+ type: "json",
246
+ required: true,
247
+ description: "Discriminated union of batch configuration types (discriminated by type)"
248
+ },
249
+ {
250
+ name: "items",
251
+ flag: "items",
252
+ type: "json",
253
+ required: true
254
+ }
255
+ ],
256
+ files: [],
257
+ credentialOrganization: false
258
+ }
259
+ },
151
260
  {
152
261
  command: "context",
153
262
  tool: "get_context",
@@ -1357,6 +1466,13 @@ var COMMANDS = [
1357
1466
  type: "string[]",
1358
1467
  required: false,
1359
1468
  description: "Filter by login profile IDs (can specify multiple, there is an 'OR' condition applied to these)"
1469
+ },
1470
+ {
1471
+ name: "batchId",
1472
+ flag: "batch-id",
1473
+ type: "string",
1474
+ required: false,
1475
+ description: "Only executions started by this batch, an ID from `execution_batches_list`. Rows not yet triggered have no execution yet: list them with `scheduled_executions_list` and `batchId`."
1360
1476
  }
1361
1477
  ],
1362
1478
  binary: false,
@@ -1631,6 +1747,13 @@ var COMMANDS = [
1631
1747
  type: "string[]",
1632
1748
  required: false,
1633
1749
  description: "Filter by login profile IDs (can specify multiple, there is an 'OR' condition applied to these)"
1750
+ },
1751
+ {
1752
+ name: "batchId",
1753
+ flag: "batch-id",
1754
+ type: "string",
1755
+ required: false,
1756
+ description: "Only executions started by this batch, an ID from `execution_batches_list`. Rows not yet triggered have no execution yet: list them with `scheduled_executions_list` and `batchId`."
1634
1757
  }
1635
1758
  ],
1636
1759
  binary: false,
@@ -1928,7 +2051,7 @@ var COMMANDS = [
1928
2051
  command: "portal-skills add",
1929
2052
  tool: "portal_skills_add",
1930
2053
  title: "Add portal skills",
1931
- description: "Add a portal's skills to an organization, by portal id. It is general knowledge about how a portal works. Needs a Startup plan or above. Astro loads the portal's skills once it is added. Adding one the organization already has is a no-op.",
2054
+ description: "Add a portal's skills to an organization, by a `portalId` from `portal_skills_list`. Needs a Startup plan or above. Astro loads the portal's skills once it is added. Adding one the organization already has is a no-op.",
1932
2055
  method: "POST",
1933
2056
  path: "/agents/v2/portal-skills/{portalId}",
1934
2057
  args: [
@@ -1944,6 +2067,27 @@ var COMMANDS = [
1944
2067
  readOnly: false,
1945
2068
  destructive: false
1946
2069
  },
2070
+ {
2071
+ command: "portal-skills list",
2072
+ tool: "portal_skills_list",
2073
+ title: "List portal skills",
2074
+ description: "List the skills for third-party portals, or only for the portals matching a portal name or task, such as `caloptima eligibility`. A portal skill is general knowledge about how a portal works: sign-in, page quirks, field mappings. Each portal comes with its skill names, and whether the organization has added it. Add one with `portal_skills_add`.",
2075
+ method: "GET",
2076
+ path: "/astro/v2/portal-skills",
2077
+ args: [],
2078
+ query: [
2079
+ {
2080
+ name: "query",
2081
+ flag: "query",
2082
+ type: "string",
2083
+ required: false,
2084
+ description: "A portal name or task, at least three characters. Omit to list every portal with skills."
2085
+ }
2086
+ ],
2087
+ binary: false,
2088
+ readOnly: true,
2089
+ destructive: false
2090
+ },
1947
2091
  {
1948
2092
  command: "profile clear-cache",
1949
2093
  tool: "profile_clear_browser_cache",
@@ -4462,7 +4606,7 @@ var COMMANDS = [
4462
4606
  command: "workflow head publish",
4463
4607
  tool: "workflow_head_publish",
4464
4608
  title: "Publish workflow head",
4465
- description: "Publish the workflow's editable head, assigning it the next version number and making it the default for `workflow_execute_post`. Use after `workflow_files_patch`; `workflow_version_publish` does the same for a version you already have the ID of.",
4609
+ description: "Publish the workflow's editable head, assigning it the next version number and making it the default for `workflow_execute_post`. Use after `workflow_files_patch`; `workflow_version_publish` does the same for a version you already have the ID of. `schemaChanges` lists the input and output schema changes against the version published before, which can break callers that send inputs or read outputs by name.",
4466
4610
  method: "POST",
4467
4611
  path: "/agents/v2/workflows/{workflowId}/head/publish",
4468
4612
  args: [
@@ -5399,7 +5543,7 @@ var COMMANDS = [
5399
5543
  command: "workflow version publish",
5400
5544
  tool: "workflow_version_publish",
5401
5545
  title: "Publish workflow version",
5402
- description: "Publish a workflow version, assigning it a permanent version number and making it the default for `workflow_execute_post`.",
5546
+ description: "Publish a workflow version, assigning it a permanent version number and making it the default for `workflow_execute_post`. This also publishes an older version again. `schemaChanges` lists the input and output schema changes against the version published before, which can break callers that send inputs or read outputs by name.",
5403
5547
  method: "POST",
5404
5548
  path: "/agents/v2/workflows/{workflowId}/versions/{versionId}/publish",
5405
5549
  args: [
@@ -5903,16 +6047,26 @@ function leafOptions(leaf, command) {
5903
6047
  for (const file of command.body?.files ?? []) {
5904
6048
  leaf.option(`--${file} <path>`, "File to upload. Repeatable.", collect);
5905
6049
  }
5906
- if (command.files === "pull")
6050
+ if (command.files === "pull") {
5907
6051
  leaf.option(
5908
6052
  "--out <dir>",
5909
6053
  "Write here instead of ./<workflow-name>/head (a version: ./<workflow-name>/.versions/...). --json prints the file listing instead."
5910
6054
  );
5911
- if (command.files === "push")
6055
+ leaf.option(
6056
+ "--keep-edits",
6057
+ "Into a folder pulled before: keep every file you edited since that pull and refresh the rest. From a head, a later push sends those edits on top of the new revision."
6058
+ );
6059
+ }
6060
+ if (command.files === "push") {
5912
6061
  leaf.option(
5913
6062
  "--from <dir>",
5914
6063
  "The pulled head folder to push. Defaults to the current folder."
5915
6064
  );
6065
+ leaf.option(
6066
+ "--rebase",
6067
+ "When the head moved since your pull, pull it with --keep-edits and push again, twice at most."
6068
+ );
6069
+ }
5916
6070
  }
5917
6071
  function toParsed(leaf, positionals, dotted2) {
5918
6072
  const globals = leaf.optsWithGlobals();
@@ -6703,6 +6857,18 @@ ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
6703
6857
  ].filter(Boolean).join("\n");
6704
6858
  }
6705
6859
 
6860
+ // src/sync/manifest.ts
6861
+ async function loadManifest(sync, dir) {
6862
+ const bytes = await sync.files.read(join(dir, MANIFEST));
6863
+ if (!bytes) return void 0;
6864
+ return JSON.parse(Buffer.from(bytes).toString("utf8"));
6865
+ }
6866
+ async function writeManifest(sync, dir, manifest) {
6867
+ const text2 = `${JSON.stringify(manifest, null, 2)}
6868
+ `;
6869
+ await sync.files.write(join(dir, MANIFEST), Buffer.from(text2, "utf8"));
6870
+ }
6871
+
6706
6872
  // src/sync/node-files.ts
6707
6873
  import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
6708
6874
  import { dirname, relative, resolve, sep } from "node:path";
@@ -6754,6 +6920,9 @@ function pullDir(root, tree, head) {
6754
6920
  if (head) return join(base, "head");
6755
6921
  return join(base, ".versions", workflowDirName(tree.versionId, tree.version));
6756
6922
  }
6923
+ function headFilesRequest(workflowId) {
6924
+ return get(`/agents/v2/workflows/${workflowId}/head/files`);
6925
+ }
6757
6926
  async function agentFileBytes(sync, workflowId, tree, entry) {
6758
6927
  const path = `/agents/v2/workflows/${workflowId}/versions/${tree.versionId}/files/${entry.fileId}/signed-url`;
6759
6928
  const { url } = await sync.remote.json(get(path));
@@ -6763,32 +6932,105 @@ async function entryBytes(sync, workflowId, tree, entry) {
6763
6932
  if (entry.contentBase64 !== void 0)
6764
6933
  return Buffer.from(entry.contentBase64, "base64");
6765
6934
  if (entry.content !== void 0) return Buffer.from(entry.content, "utf8");
6935
+ if (entry.storageObjectKey && sync.remote.object)
6936
+ return sync.remote.object(entry.storageObjectKey);
6766
6937
  if (!entry.fileId) return void 0;
6767
6938
  return agentFileBytes(sync, workflowId, tree, entry);
6768
6939
  }
6769
- async function writeEntries(sync, options, tree, dir) {
6770
- const hashes = {};
6771
- const skipped = [];
6772
- let shared = 0;
6773
- const wanted = tree.files.filter((entry) => {
6940
+ function inlineBytes(entry) {
6941
+ if (entry.contentBase64 !== void 0)
6942
+ return Buffer.from(entry.contentBase64, "base64");
6943
+ if (entry.content !== void 0) return Buffer.from(entry.content, "utf8");
6944
+ return void 0;
6945
+ }
6946
+ function wantedEntries(options, tree, skipped) {
6947
+ return tree.files.filter((entry) => {
6774
6948
  if (!isContained(entry.path)) skipped.push(`${entry.path} (unsafe path)`);
6775
6949
  else if (options.maxFileBytes !== void 0 && entry.size > options.maxFileBytes)
6776
6950
  skipped.push(`${entry.path} (${entry.size} bytes, over the size limit)`);
6777
6951
  else return true;
6778
6952
  return false;
6779
6953
  });
6780
- for (let i = 0; i < wanted.length; i += CONCURRENCY) {
6781
- await Promise.all(
6782
- wanted.slice(i, i + CONCURRENCY).map(async (entry) => {
6783
- const bytes = await entryBytes(sync, options.workflowId, tree, entry);
6784
- if (!bytes) return void skipped.push(`${entry.path} (no content)`);
6785
- await sync.files.write(join(dir, entry.path), bytes);
6786
- hashes[entry.path] = sha256(bytes);
6787
- if (entry.kind === "agent_file") shared++;
6788
- })
6789
- );
6954
+ }
6955
+ async function inBatches(items, each) {
6956
+ for (let i = 0; i < items.length; i += CONCURRENCY) {
6957
+ await Promise.all(items.slice(i, i + CONCURRENCY).map(each));
6790
6958
  }
6791
- return { hashes, skipped, shared };
6959
+ }
6960
+ async function writeEntries(sync, options, tree, dir) {
6961
+ const hashes = {};
6962
+ const skipped = [];
6963
+ let shared = 0;
6964
+ let written = 0;
6965
+ await inBatches(wantedEntries(options, tree, skipped), async (entry) => {
6966
+ const bytes = await entryBytes(sync, options.workflowId, tree, entry);
6967
+ if (!bytes) return void skipped.push(`${entry.path} (no content)`);
6968
+ await sync.files.write(join(dir, entry.path), bytes);
6969
+ hashes[entry.path] = sha256(bytes);
6970
+ written++;
6971
+ if (entry.kind === "agent_file") shared++;
6972
+ });
6973
+ return { hashes, skipped, shared, kept: [], written };
6974
+ }
6975
+ async function mergeEntry(sync, options, tree, dir, previous, entry, out) {
6976
+ const local2 = await sync.files.read(join(dir, entry.path));
6977
+ const localHash = local2 ? sha256(local2) : void 0;
6978
+ const baseline = previous.files[entry.path];
6979
+ const edited = baseline === void 0 ? local2 !== void 0 : localHash !== baseline;
6980
+ let bytes = inlineBytes(entry);
6981
+ let serverHash = bytes ? sha256(bytes) : entry.checksum;
6982
+ if (edited) {
6983
+ if (serverHash === void 0) {
6984
+ bytes = await entryBytes(sync, options.workflowId, tree, entry);
6985
+ if (!bytes) return void out.skipped.push(`${entry.path} (no content)`);
6986
+ serverHash = sha256(bytes);
6987
+ }
6988
+ out.hashes[entry.path] = serverHash;
6989
+ out.kept.push(entry.path);
6990
+ return;
6991
+ }
6992
+ if (serverHash !== void 0 && serverHash === localHash) {
6993
+ out.hashes[entry.path] = serverHash;
6994
+ return;
6995
+ }
6996
+ bytes ??= await entryBytes(sync, options.workflowId, tree, entry);
6997
+ if (!bytes) return void out.skipped.push(`${entry.path} (no content)`);
6998
+ serverHash = sha256(bytes);
6999
+ if (serverHash !== localHash) {
7000
+ await sync.files.write(join(dir, entry.path), bytes);
7001
+ out.written++;
7002
+ if (entry.kind === "agent_file") out.shared++;
7003
+ }
7004
+ out.hashes[entry.path] = serverHash;
7005
+ }
7006
+ async function mergeEntries(sync, options, tree, dir, previous) {
7007
+ const out = {
7008
+ hashes: {},
7009
+ skipped: [],
7010
+ shared: 0,
7011
+ kept: [],
7012
+ written: 0
7013
+ };
7014
+ const wanted = wantedEntries(options, tree, out.skipped);
7015
+ await inBatches(
7016
+ wanted,
7017
+ (entry) => mergeEntry(sync, options, tree, dir, previous, entry, out)
7018
+ );
7019
+ const remote = new Set(tree.files.map((entry) => entry.path));
7020
+ for (const path of Object.keys(previous.files)) {
7021
+ if (path in out.hashes) continue;
7022
+ if (remote.has(path)) {
7023
+ out.hashes[path] = previous.files[path];
7024
+ continue;
7025
+ }
7026
+ const local2 = await sync.files.read(join(dir, path));
7027
+ if (!local2) continue;
7028
+ if (sha256(local2) === previous.files[path])
7029
+ await sync.files.remove(join(dir, path));
7030
+ else out.kept.push(path);
7031
+ }
7032
+ out.kept.sort();
7033
+ return out;
6792
7034
  }
6793
7035
  async function removeStale(sync, dir, keep) {
6794
7036
  for (const path of await sync.files.list(dir)) {
@@ -6796,11 +7038,6 @@ async function removeStale(sync, dir, keep) {
6796
7038
  await sync.files.remove(join(dir, path));
6797
7039
  }
6798
7040
  }
6799
- async function writeManifest(sync, dir, manifest) {
6800
- const text2 = `${JSON.stringify(manifest, null, 2)}
6801
- `;
6802
- await sync.files.write(join(dir, MANIFEST), Buffer.from(text2, "utf8"));
6803
- }
6804
7041
  async function pullWorkflow(sync, options) {
6805
7042
  const request = {
6806
7043
  ...options.request,
@@ -6810,20 +7047,25 @@ async function pullWorkflow(sync, options) {
6810
7047
  const tree = await sync.remote.json(request);
6811
7048
  const head = request.path.endsWith("/head/files");
6812
7049
  const dir = options.dir ?? pullDir(options.root ?? "", tree, head);
6813
- const { hashes, skipped, shared } = await writeEntries(
6814
- sync,
6815
- options,
6816
- tree,
6817
- dir
6818
- );
6819
- await removeStale(sync, dir, new Set(Object.keys(hashes)));
7050
+ const previous = options.keepEdits ? await loadManifest(sync, dir) : void 0;
7051
+ const written = previous ? await mergeEntries(sync, options, tree, dir, previous) : await writeEntries(sync, options, tree, dir);
7052
+ if (!previous)
7053
+ await removeStale(sync, dir, new Set(Object.keys(written.hashes)));
6820
7054
  if (options.manifest !== false) {
7055
+ const variants = request.query.get("variants") ?? void 0;
6821
7056
  await writeManifest(sync, dir, {
6822
7057
  workflowId: options.workflowId,
6823
7058
  versionId: tree.versionId,
6824
7059
  head,
6825
7060
  rev: tree.rev,
6826
- files: hashes
7061
+ files: written.hashes,
7062
+ ...variants !== void 0 && { variants },
7063
+ ...options.maxFileBytes !== void 0 && {
7064
+ maxFileBytes: options.maxFileBytes
7065
+ },
7066
+ ...options.maxTotalBytes !== void 0 && {
7067
+ maxTotalBytes: options.maxTotalBytes
7068
+ }
6827
7069
  });
6828
7070
  }
6829
7071
  return {
@@ -6834,9 +7076,11 @@ async function pullWorkflow(sync, options) {
6834
7076
  rev: tree.rev,
6835
7077
  dir,
6836
7078
  head,
6837
- written: Object.keys(hashes).length,
6838
- shared,
6839
- skipped
7079
+ written: written.written,
7080
+ shared: written.shared,
7081
+ skipped: written.skipped,
7082
+ kept: written.kept,
7083
+ ...tree.variantKeys && { variantKeys: tree.variantKeys }
6840
7084
  };
6841
7085
  }
6842
7086
  function formatWorkflowPull(result) {
@@ -6847,6 +7091,8 @@ function formatWorkflowPull(result) {
6847
7091
  `Workflow version: ${result.versionId}${result.version ? ` (v${result.version})` : " (draft)"}`,
6848
7092
  `Written to: ${result.dir}/`,
6849
7093
  files,
7094
+ result.kept.length > 0 ? `Kept your edits in:
7095
+ ${result.kept.map((s) => ` - ${s}`).join("\n")}` : null,
6850
7096
  result.skipped.length > 0 ? `Skipped:
6851
7097
  ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
6852
7098
  result.head ? `Edit the files, then run \`asteroid workflow push\` from ${result.dir}/.` : "This is a pinned version. It is read-only."
@@ -6854,14 +7100,14 @@ ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
6854
7100
  }
6855
7101
 
6856
7102
  // src/sync/workflow-push.ts
7103
+ var REBASE_ATTEMPTS = 3;
6857
7104
  async function readManifest(sync, dir) {
6858
- const bytes = await sync.files.read(join(dir, MANIFEST));
6859
- if (!bytes) {
7105
+ const manifest = await loadManifest(sync, dir);
7106
+ if (!manifest) {
6860
7107
  throw new UsageError(
6861
7108
  `no pulled workflow in ${dir}. Run \`asteroid workflow pull <workflow-id>\`, then push from the head folder it writes.`
6862
7109
  );
6863
7110
  }
6864
- const manifest = JSON.parse(Buffer.from(bytes).toString("utf8"));
6865
7111
  if (!manifest.head) {
6866
7112
  throw new UsageError(
6867
7113
  `${dir} holds a pinned workflow version, which is read-only. Push from a pulled head.`
@@ -6891,25 +7137,44 @@ async function localChanges(sync, dir, manifest) {
6891
7137
  );
6892
7138
  return { writes, deletes };
6893
7139
  }
7140
+ function conflict(manifest) {
7141
+ return new UsageError(
7142
+ `the workflow head changed since your pull (rev ${manifest.rev}). Run \`asteroid workflow pull ${manifest.workflowId} --keep-edits\` to bring its changes under yours, then push again, or push with --rebase to do both.`
7143
+ );
7144
+ }
6894
7145
  async function patch(sync, manifest, writes, deletes) {
6895
7146
  try {
6896
7147
  await sync.remote.json({
6897
7148
  method: "PATCH",
6898
7149
  path: `/agents/v2/workflows/${manifest.workflowId}/head/files`,
6899
7150
  query: new URLSearchParams(),
6900
- json: { baseRev: manifest.rev, writes, deletes }
7151
+ json: {
7152
+ baseRev: manifest.rev,
7153
+ writes: writes.map(({ path, contentBase64 }) => ({
7154
+ path,
7155
+ contentBase64
7156
+ })),
7157
+ deletes
7158
+ }
6901
7159
  });
7160
+ return "applied";
6902
7161
  } catch (error) {
6903
7162
  if (statusOf(error) !== 409) throw error;
6904
- throw new UsageError(
6905
- `the workflow head changed since your pull (rev ${manifest.rev}). Copy your edits aside, run \`asteroid workflow pull ${manifest.workflowId}\`, reapply them, then push again.`
6906
- );
7163
+ return "conflict";
6907
7164
  }
6908
7165
  }
7166
+ function headRequest(manifest) {
7167
+ const request = headFilesRequest(manifest.workflowId);
7168
+ if (manifest.variants !== void 0)
7169
+ request.query.set("variants", manifest.variants);
7170
+ if (manifest.maxFileBytes !== void 0)
7171
+ request.query.set("maxFileBytes", String(manifest.maxFileBytes));
7172
+ if (manifest.maxTotalBytes !== void 0)
7173
+ request.query.set("maxTotalBytes", String(manifest.maxTotalBytes));
7174
+ return request;
7175
+ }
6909
7176
  async function refresh(sync, dir, manifest, pushed) {
6910
- const tree = await sync.remote.json(
6911
- get(`/agents/v2/workflows/${manifest.workflowId}/head/files`)
6912
- );
7177
+ const tree = await sync.remote.json(headRequest(manifest));
6913
7178
  const files = {};
6914
7179
  for (const entry of tree.files) {
6915
7180
  if (entry.content !== void 0) {
@@ -6919,7 +7184,7 @@ async function refresh(sync, dir, manifest, pushed) {
6919
7184
  await sync.files.write(join(dir, entry.path), bytes);
6920
7185
  } else {
6921
7186
  const local2 = await sync.files.read(join(dir, entry.path));
6922
- const hash = local2 ? sha256(local2) : pushed.get(entry.path) ?? entry.checksum;
7187
+ const hash = local2 ? sha256(local2) : pushed.get(entry.path);
6923
7188
  if (hash) files[entry.path] = hash;
6924
7189
  }
6925
7190
  }
@@ -6931,43 +7196,58 @@ async function refresh(sync, dir, manifest, pushed) {
6931
7196
  await writeManifest(sync, dir, next);
6932
7197
  return next;
6933
7198
  }
6934
- async function pushWorkflow(sync, dir, workflowId) {
6935
- const manifest = await readManifest(sync, dir);
7199
+ async function rebase(sync, dir, manifest) {
7200
+ await pullWorkflow(sync, {
7201
+ workflowId: manifest.workflowId,
7202
+ request: headRequest(manifest),
7203
+ dir,
7204
+ keepEdits: true,
7205
+ maxFileBytes: manifest.maxFileBytes,
7206
+ maxTotalBytes: manifest.maxTotalBytes
7207
+ });
7208
+ return readManifest(sync, dir);
7209
+ }
7210
+ async function pushWorkflow(sync, dir, workflowId, options = {}) {
7211
+ let manifest = await readManifest(sync, dir);
6936
7212
  if (workflowId !== void 0 && manifest.workflowId !== workflowId) {
6937
7213
  throw new UsageError(
6938
7214
  `${dir} holds workflow ${manifest.workflowId}, not ${workflowId}.`
6939
7215
  );
6940
7216
  }
6941
- const { writes, deletes } = await localChanges(sync, dir, manifest);
6942
- const result = {
6943
- workflowId: manifest.workflowId,
6944
- dir,
6945
- written: writes.map((w) => w.path),
6946
- deleted: deletes
6947
- };
6948
- if (writes.length === 0 && deletes.length === 0) {
6949
- return { ...result, rev: manifest.rev, versionId: manifest.versionId };
7217
+ let rebasedFrom;
7218
+ for (let attempt = 1; ; attempt++) {
7219
+ const { writes, deletes } = await localChanges(sync, dir, manifest);
7220
+ const result = {
7221
+ workflowId: manifest.workflowId,
7222
+ dir,
7223
+ written: writes.map((w) => w.path),
7224
+ deleted: deletes,
7225
+ ...rebasedFrom !== void 0 && { rebasedFrom }
7226
+ };
7227
+ if (writes.length === 0 && deletes.length === 0) {
7228
+ return { ...result, rev: manifest.rev, versionId: manifest.versionId };
7229
+ }
7230
+ if (await patch(sync, manifest, writes, deletes) === "applied") {
7231
+ const next = await refresh(
7232
+ sync,
7233
+ dir,
7234
+ manifest,
7235
+ new Map(writes.map((w) => [w.path, w.hash]))
7236
+ );
7237
+ return { ...result, rev: next.rev, versionId: next.versionId };
7238
+ }
7239
+ if (!options.rebase || attempt >= REBASE_ATTEMPTS) throw conflict(manifest);
7240
+ rebasedFrom ??= manifest.rev;
7241
+ manifest = await rebase(sync, dir, manifest);
6950
7242
  }
6951
- await patch(
6952
- sync,
6953
- manifest,
6954
- writes.map(({ path, contentBase64 }) => ({ path, contentBase64 })),
6955
- deletes
6956
- );
6957
- const next = await refresh(
6958
- sync,
6959
- dir,
6960
- manifest,
6961
- new Map(writes.map((w) => [w.path, w.hash]))
6962
- );
6963
- return { ...result, rev: next.rev, versionId: next.versionId };
6964
7243
  }
6965
7244
  function formatWorkflowPush(result) {
7245
+ const rebased = result.rebasedFrom !== void 0 ? ` Rebased on the head first: your pull was rev ${result.rebasedFrom}.` : "";
6966
7246
  if (result.written.length === 0 && result.deleted.length === 0) {
6967
- return `Nothing to push: ${result.dir}/ matches the head you pulled (rev ${result.rev}).`;
7247
+ return `Nothing to push: ${result.dir}/ matches the head you pulled (rev ${result.rev}).${rebased}`;
6968
7248
  }
6969
7249
  return [
6970
- `Pushed to workflow ${result.workflowId}: ${result.written.length} written, ${result.deleted.length} deleted.`,
7250
+ `Pushed to workflow ${result.workflowId}: ${result.written.length} written, ${result.deleted.length} deleted.${rebased}`,
6971
7251
  ...result.written.map((path) => ` + ${path}`),
6972
7252
  ...result.deleted.map((path) => ` - ${path}`),
6973
7253
  `Head is now rev ${result.rev} (version ${result.versionId}).`
@@ -6998,11 +7278,17 @@ function executionOptions(parsed) {
6998
7278
  dir: dir ? () => dir : void 0
6999
7279
  };
7000
7280
  }
7001
- function withoutFlag(parsed, name) {
7281
+ function withoutFlags(parsed, ...names) {
7002
7282
  const flags = new Map(parsed.flags);
7003
- flags.delete(name);
7283
+ for (const name of names) flags.delete(name);
7004
7284
  return { ...parsed, flags };
7005
7285
  }
7286
+ function runsLocally(command, parsed) {
7287
+ if (command.local) return true;
7288
+ if (command.files === "pull") return !parsed.json;
7289
+ if (command.files === "push") return parsed.body === void 0;
7290
+ return false;
7291
+ }
7006
7292
  var PARTIAL = ["paths", "variants"];
7007
7293
  function rejectPartial(parsed) {
7008
7294
  const used = PARTIAL.filter((name) => parsed.flags.has(name));
@@ -7015,11 +7301,16 @@ async function pull(command, parsed, sync, deps) {
7015
7301
  rejectPartial(parsed);
7016
7302
  const result = await pullWorkflow(sync, {
7017
7303
  workflowId: parsed.positionals[0],
7018
- request: await buildRequest(command, withoutFlag(parsed, "out"), {
7019
- io: deps.io
7020
- }),
7304
+ request: await buildRequest(
7305
+ command,
7306
+ withoutFlags(parsed, "out", "keep-edits"),
7307
+ {
7308
+ io: deps.io
7309
+ }
7310
+ ),
7021
7311
  dir: flag(parsed, "out"),
7022
7312
  root: deps.root ?? "",
7313
+ keepEdits: flag(parsed, "keep-edits") === "true",
7023
7314
  ...deps.workflowPull
7024
7315
  });
7025
7316
  return { result, text: formatWorkflowPull(result) };
@@ -7030,7 +7321,8 @@ async function execute(command, parsed, sync, deps) {
7030
7321
  const result2 = await pushWorkflow(
7031
7322
  sync,
7032
7323
  flag(parsed, "from") ?? ".",
7033
- parsed.positionals[0] || void 0
7324
+ parsed.positionals[0] || void 0,
7325
+ { rebase: flag(parsed, "rebase") === "true" }
7034
7326
  );
7035
7327
  return { result: result2, text: formatWorkflowPush(result2) };
7036
7328
  }
@@ -7064,14 +7356,20 @@ export {
7064
7356
  formatExecutionPull,
7065
7357
  formatWorkflowPull,
7066
7358
  formatWorkflowPush,
7359
+ headFilesRequest,
7067
7360
  leadingWords,
7361
+ loadManifest,
7068
7362
  matchCommand,
7069
7363
  matchesGlob,
7070
7364
  parseInvocation,
7365
+ pullDir,
7071
7366
  pullExecution,
7072
7367
  pullWorkflow,
7073
7368
  pushWorkflow,
7369
+ readManifest,
7074
7370
  render,
7075
7371
  runSync,
7076
- withSyncCommands
7372
+ runsLocally,
7373
+ withSyncCommands,
7374
+ writeManifest
7077
7375
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteroidai/cli",
3
- "version": "1.9.98",
3
+ "version": "1.9.103",
4
4
  "description": "The asteroid command-line interface for the Asteroid platform.",
5
5
  "type": "module",
6
6
  "license": "MIT",