@asteroidai/cli 1.9.11 → 1.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteroidai/cli",
3
- "version": "1.9.11",
3
+ "version": "1.9.12",
4
4
  "description": "The asteroid command-line interface for the Asteroid platform.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -128,6 +128,7 @@ export const COMMANDS: CliCommand[] = [
128
128
  binary: false,
129
129
  readOnly: false,
130
130
  destructive: false,
131
+ capability: "run_execution",
131
132
  },
132
133
  {
133
134
  command: "context",
@@ -1092,6 +1093,7 @@ export const COMMANDS: CliCommand[] = [
1092
1093
  files: [],
1093
1094
  credentialOrganization: false,
1094
1095
  },
1096
+ capability: "run_execution",
1095
1097
  },
1096
1098
  {
1097
1099
  command: "execution resume",
@@ -1113,6 +1115,7 @@ export const COMMANDS: CliCommand[] = [
1113
1115
  binary: false,
1114
1116
  readOnly: false,
1115
1117
  destructive: false,
1118
+ capability: "run_execution",
1116
1119
  },
1117
1120
  {
1118
1121
  command: "insight get",
@@ -2004,6 +2007,7 @@ export const COMMANDS: CliCommand[] = [
2004
2007
  files: [],
2005
2008
  credentialOrganization: false,
2006
2009
  },
2010
+ capability: "run_execution",
2007
2011
  },
2008
2012
  {
2009
2013
  command: "schedule delete",
@@ -2031,6 +2035,7 @@ export const COMMANDS: CliCommand[] = [
2031
2035
  binary: false,
2032
2036
  readOnly: false,
2033
2037
  destructive: true,
2038
+ capability: "run_execution",
2034
2039
  },
2035
2040
  {
2036
2041
  command: "schedule get",
@@ -2222,6 +2227,7 @@ export const COMMANDS: CliCommand[] = [
2222
2227
  files: [],
2223
2228
  credentialOrganization: false,
2224
2229
  },
2230
+ capability: "run_execution",
2225
2231
  },
2226
2232
  {
2227
2233
  command: "scheduled-execution cancel",
@@ -2243,6 +2249,7 @@ export const COMMANDS: CliCommand[] = [
2243
2249
  binary: false,
2244
2250
  readOnly: false,
2245
2251
  destructive: false,
2252
+ capability: "run_execution",
2246
2253
  },
2247
2254
  {
2248
2255
  command: "scheduled-execution create",
@@ -2315,6 +2322,7 @@ export const COMMANDS: CliCommand[] = [
2315
2322
  files: [],
2316
2323
  credentialOrganization: false,
2317
2324
  },
2325
+ capability: "run_execution",
2318
2326
  },
2319
2327
  {
2320
2328
  command: "scheduled-execution list",
@@ -2421,6 +2429,7 @@ export const COMMANDS: CliCommand[] = [
2421
2429
  files: [],
2422
2430
  credentialOrganization: false,
2423
2431
  },
2432
+ capability: "run_execution",
2424
2433
  },
2425
2434
  {
2426
2435
  command: "schema validate",
@@ -3621,6 +3630,7 @@ export const COMMANDS: CliCommand[] = [
3621
3630
  files: [],
3622
3631
  credentialOrganization: false,
3623
3632
  },
3633
+ capability: "run_execution",
3624
3634
  },
3625
3635
  {
3626
3636
  command: "workflow execute-sync",
@@ -3731,119 +3741,7 @@ export const COMMANDS: CliCommand[] = [
3731
3741
  files: [],
3732
3742
  credentialOrganization: false,
3733
3743
  },
3734
- },
3735
- {
3736
- command: "workflow file list",
3737
- tool: "workflow_files_list",
3738
- title: "List workflow files",
3739
- description:
3740
- "List the workflow's editable head as a directory of files: the graph rendered as structural files (settings.yaml, per-node instructions.md, scripts) alongside the workflow's own files. Those files are referenced by fileId unless contents=all is set. Pass paths to read named files only, or contents=none for a manifest with no bodies. The returned rev is what you pass as baseRev to write.",
3741
- method: "GET",
3742
- path: "/agents/v2/workflows/{workflowId}/head/files",
3743
- args: [
3744
- {
3745
- name: "workflowId",
3746
- flag: "workflow-id",
3747
- type: "string",
3748
- required: true,
3749
- },
3750
- ],
3751
- query: [
3752
- {
3753
- name: "contents",
3754
- flag: "contents",
3755
- type: "string",
3756
- required: false,
3757
- description:
3758
- "Which contents come inline: none (a pure manifest), structural (the default), or all (structural plus file bytes within the response budget).",
3759
- enum: ["none", "structural", "all"],
3760
- },
3761
- {
3762
- name: "paths",
3763
- flag: "paths",
3764
- type: "string[]",
3765
- required: false,
3766
- description:
3767
- "Return only the files at these exact tree paths. A path that names nothing yields no entry. The inline budget applies to the filtered set, so a narrow read can inline files a whole-tree read would have to skip.",
3768
- },
3769
- {
3770
- name: "variants",
3771
- flag: "variants",
3772
- type: "string",
3773
- required: false,
3774
- description:
3775
- 'Narrow which variants/<key>/ files the tree carries: "none" strips every variant, a comma-separated list keeps only those variant keys. Files outside variants/ are always kept, and the response\'s variantKeys still names every variant. Ignored unless the workflow has variant mode enabled; absent keeps every variant.',
3776
- },
3777
- {
3778
- name: "maxFileBytes",
3779
- flag: "max-file-bytes",
3780
- type: "integer",
3781
- required: false,
3782
- description:
3783
- "Per-file ceiling, in bytes, for inlined file contents. Clamped to the server maximum. Files above the ceiling stay references.",
3784
- },
3785
- {
3786
- name: "maxTotalBytes",
3787
- flag: "max-total-bytes",
3788
- type: "integer",
3789
- required: false,
3790
- description:
3791
- "Ceiling, in bytes, on the total inlined file contents in one response. Clamped to the server maximum. Files are admitted in path order until the ceiling is reached; the rest stay references.",
3792
- },
3793
- ],
3794
- binary: false,
3795
- readOnly: true,
3796
- destructive: false,
3797
- },
3798
- {
3799
- command: "workflow file patch",
3800
- tool: "workflow_files_patch",
3801
- title: "Write workflow files",
3802
- description:
3803
- "Write and delete files on the workflow's editable head in one patch. Structural files re-derive the graph; the workflow's own files go to its file store, you just write paths. Pass the rev from `workflow_files_list` as baseRev; a stale value is rejected with 409, so re-read and retry. Editing a published or already-executed version forks a new one instead of rewriting it. Each write carries `contentBase64`: base64-encode the file content, plain text is rejected with 400.",
3804
- method: "PATCH",
3805
- path: "/agents/v2/workflows/{workflowId}/head/files",
3806
- args: [
3807
- {
3808
- name: "workflowId",
3809
- flag: "workflow-id",
3810
- type: "string",
3811
- required: true,
3812
- },
3813
- ],
3814
- query: [],
3815
- binary: false,
3816
- readOnly: false,
3817
- destructive: true,
3818
- body: {
3819
- required: true,
3820
- contentType: "application/json",
3821
- fields: [
3822
- {
3823
- name: "baseRev",
3824
- flag: "base-rev",
3825
- type: "integer",
3826
- required: true,
3827
- description:
3828
- "The revision this edit is based on. Agent-file-only patches overlay the current head even when this is stale. A structural-file patch is rejected with 409 when it does not match the stored head revision.",
3829
- },
3830
- {
3831
- name: "writes",
3832
- flag: "writes",
3833
- type: "json",
3834
- required: true,
3835
- description: "Files to upsert.",
3836
- },
3837
- {
3838
- name: "deletes",
3839
- flag: "deletes",
3840
- type: "string[]",
3841
- required: true,
3842
- },
3843
- ],
3844
- files: [],
3845
- credentialOrganization: false,
3846
- },
3744
+ capability: "run_execution",
3847
3745
  },
3848
3746
  {
3849
3747
  command: "workflow get",
@@ -3894,6 +3792,7 @@ export const COMMANDS: CliCommand[] = [
3894
3792
  binary: false,
3895
3793
  readOnly: false,
3896
3794
  destructive: false,
3795
+ capability: "publish",
3897
3796
  },
3898
3797
  {
3899
3798
  command: "workflow head revert",
@@ -4024,6 +3923,121 @@ export const COMMANDS: CliCommand[] = [
4024
3923
  readOnly: true,
4025
3924
  destructive: false,
4026
3925
  },
3926
+ {
3927
+ command: "workflow pull",
3928
+ tool: "workflow_files_list",
3929
+ title: "List workflow files",
3930
+ description:
3931
+ "List the workflow's editable head as a directory of files: the graph rendered as structural files (settings.yaml, per-node instructions.md, scripts) alongside the workflow's own files. Those files are referenced by fileId unless contents=all is set. Pass paths to read named files only, or contents=none for a manifest with no bodies. The returned rev is what you pass as baseRev to write.",
3932
+ method: "GET",
3933
+ path: "/agents/v2/workflows/{workflowId}/head/files",
3934
+ args: [
3935
+ {
3936
+ name: "workflowId",
3937
+ flag: "workflow-id",
3938
+ type: "string",
3939
+ required: true,
3940
+ },
3941
+ ],
3942
+ query: [
3943
+ {
3944
+ name: "contents",
3945
+ flag: "contents",
3946
+ type: "string",
3947
+ required: false,
3948
+ description:
3949
+ "Which contents come inline: none (a pure manifest), structural (the default), or all (structural plus file bytes within the response budget).",
3950
+ enum: ["none", "structural", "all"],
3951
+ },
3952
+ {
3953
+ name: "paths",
3954
+ flag: "paths",
3955
+ type: "string[]",
3956
+ required: false,
3957
+ description:
3958
+ "Return only the files at these exact tree paths. A path that names nothing yields no entry. The inline budget applies to the filtered set, so a narrow read can inline files a whole-tree read would have to skip.",
3959
+ },
3960
+ {
3961
+ name: "variants",
3962
+ flag: "variants",
3963
+ type: "string",
3964
+ required: false,
3965
+ description:
3966
+ 'Narrow which variants/<key>/ files the tree carries: "none" strips every variant, a comma-separated list keeps only those variant keys. Files outside variants/ are always kept, and the response\'s variantKeys still names every variant. Ignored unless the workflow has variant mode enabled; absent keeps every variant.',
3967
+ },
3968
+ {
3969
+ name: "maxFileBytes",
3970
+ flag: "max-file-bytes",
3971
+ type: "integer",
3972
+ required: false,
3973
+ description:
3974
+ "Per-file ceiling, in bytes, for inlined file contents. Clamped to the server maximum. Files above the ceiling stay references.",
3975
+ },
3976
+ {
3977
+ name: "maxTotalBytes",
3978
+ flag: "max-total-bytes",
3979
+ type: "integer",
3980
+ required: false,
3981
+ description:
3982
+ "Ceiling, in bytes, on the total inlined file contents in one response. Clamped to the server maximum. Files are admitted in path order until the ceiling is reached; the rest stay references.",
3983
+ },
3984
+ ],
3985
+ binary: false,
3986
+ readOnly: true,
3987
+ destructive: false,
3988
+ files: "pull",
3989
+ },
3990
+ {
3991
+ command: "workflow push",
3992
+ tool: "workflow_files_patch",
3993
+ title: "Write workflow files",
3994
+ description:
3995
+ "Write and delete files on the workflow's editable head in one patch. Structural files re-derive the graph; the workflow's own files go to its file store, you just write paths. Pass the rev from `workflow_files_list` as baseRev; a stale value is rejected with 409, so re-read and retry. Editing a published or already-executed version forks a new one instead of rewriting it. Each write carries `contentBase64`: base64-encode the file content, plain text is rejected with 400.",
3996
+ method: "PATCH",
3997
+ path: "/agents/v2/workflows/{workflowId}/head/files",
3998
+ args: [
3999
+ {
4000
+ name: "workflowId",
4001
+ flag: "workflow-id",
4002
+ type: "string",
4003
+ required: true,
4004
+ },
4005
+ ],
4006
+ query: [],
4007
+ binary: false,
4008
+ readOnly: false,
4009
+ destructive: true,
4010
+ body: {
4011
+ required: true,
4012
+ contentType: "application/json",
4013
+ fields: [
4014
+ {
4015
+ name: "baseRev",
4016
+ flag: "base-rev",
4017
+ type: "integer",
4018
+ required: true,
4019
+ description:
4020
+ "The revision this edit is based on. Agent-file-only patches overlay the current head even when this is stale. A structural-file patch is rejected with 409 when it does not match the stored head revision.",
4021
+ },
4022
+ {
4023
+ name: "writes",
4024
+ flag: "writes",
4025
+ type: "json",
4026
+ required: true,
4027
+ description: "Files to upsert.",
4028
+ },
4029
+ {
4030
+ name: "deletes",
4031
+ flag: "deletes",
4032
+ type: "string[]",
4033
+ required: true,
4034
+ },
4035
+ ],
4036
+ files: [],
4037
+ credentialOrganization: false,
4038
+ },
4039
+ files: "push",
4040
+ },
4027
4041
  {
4028
4042
  command: "workflow rename",
4029
4043
  tool: "workflow_rename",
@@ -4449,75 +4463,7 @@ export const COMMANDS: CliCommand[] = [
4449
4463
  files: [],
4450
4464
  credentialOrganization: false,
4451
4465
  },
4452
- },
4453
- {
4454
- command: "workflow version file list",
4455
- tool: "workflow_version_files_list",
4456
- title: "List workflow version files",
4457
- description:
4458
- "List a version's files, like `workflow_files_list` does for the editable head. Versions are immutable, so this is read-only.",
4459
- method: "GET",
4460
- path: "/agents/v2/workflows/{workflowId}/versions/{versionId}/files",
4461
- args: [
4462
- {
4463
- name: "workflowId",
4464
- flag: "workflow-id",
4465
- type: "string",
4466
- required: true,
4467
- },
4468
- {
4469
- name: "versionId",
4470
- flag: "version-id",
4471
- type: "string",
4472
- required: true,
4473
- },
4474
- ],
4475
- query: [
4476
- {
4477
- name: "contents",
4478
- flag: "contents",
4479
- type: "string",
4480
- required: false,
4481
- description:
4482
- "Which contents come inline: none (a pure manifest), structural (the default), or all (structural plus file bytes within the response budget).",
4483
- enum: ["none", "structural", "all"],
4484
- },
4485
- {
4486
- name: "paths",
4487
- flag: "paths",
4488
- type: "string[]",
4489
- required: false,
4490
- description:
4491
- "Return only the files at these exact tree paths. A path that names nothing yields no entry. The inline budget applies to the filtered set, so a narrow read can inline files a whole-tree read would have to skip.",
4492
- },
4493
- {
4494
- name: "variants",
4495
- flag: "variants",
4496
- type: "string",
4497
- required: false,
4498
- description:
4499
- 'Narrow which variants/<key>/ files the tree carries: "none" strips every variant, a comma-separated list keeps only those variant keys. Files outside variants/ are always kept, and the response\'s variantKeys still names every variant. Ignored unless the workflow has variant mode enabled; absent keeps every variant.',
4500
- },
4501
- {
4502
- name: "maxFileBytes",
4503
- flag: "max-file-bytes",
4504
- type: "integer",
4505
- required: false,
4506
- description:
4507
- "Per-file ceiling, in bytes, for inlined file contents. Clamped to the server maximum. Files above the ceiling stay references.",
4508
- },
4509
- {
4510
- name: "maxTotalBytes",
4511
- flag: "max-total-bytes",
4512
- type: "integer",
4513
- required: false,
4514
- description:
4515
- "Ceiling, in bytes, on the total inlined file contents in one response. Clamped to the server maximum. Files are admitted in path order until the ceiling is reached; the rest stay references.",
4516
- },
4517
- ],
4518
- binary: false,
4519
- readOnly: true,
4520
- destructive: false,
4466
+ capability: "run_execution",
4521
4467
  },
4522
4468
  {
4523
4469
  command: "workflow version file url",
@@ -4680,6 +4626,77 @@ export const COMMANDS: CliCommand[] = [
4680
4626
  binary: false,
4681
4627
  readOnly: false,
4682
4628
  destructive: false,
4629
+ capability: "publish",
4630
+ },
4631
+ {
4632
+ command: "workflow version pull",
4633
+ tool: "workflow_version_files_list",
4634
+ title: "List workflow version files",
4635
+ description:
4636
+ "List a version's files, like `workflow_files_list` does for the editable head. Versions are immutable, so this is read-only.",
4637
+ method: "GET",
4638
+ path: "/agents/v2/workflows/{workflowId}/versions/{versionId}/files",
4639
+ args: [
4640
+ {
4641
+ name: "workflowId",
4642
+ flag: "workflow-id",
4643
+ type: "string",
4644
+ required: true,
4645
+ },
4646
+ {
4647
+ name: "versionId",
4648
+ flag: "version-id",
4649
+ type: "string",
4650
+ required: true,
4651
+ },
4652
+ ],
4653
+ query: [
4654
+ {
4655
+ name: "contents",
4656
+ flag: "contents",
4657
+ type: "string",
4658
+ required: false,
4659
+ description:
4660
+ "Which contents come inline: none (a pure manifest), structural (the default), or all (structural plus file bytes within the response budget).",
4661
+ enum: ["none", "structural", "all"],
4662
+ },
4663
+ {
4664
+ name: "paths",
4665
+ flag: "paths",
4666
+ type: "string[]",
4667
+ required: false,
4668
+ description:
4669
+ "Return only the files at these exact tree paths. A path that names nothing yields no entry. The inline budget applies to the filtered set, so a narrow read can inline files a whole-tree read would have to skip.",
4670
+ },
4671
+ {
4672
+ name: "variants",
4673
+ flag: "variants",
4674
+ type: "string",
4675
+ required: false,
4676
+ description:
4677
+ 'Narrow which variants/<key>/ files the tree carries: "none" strips every variant, a comma-separated list keeps only those variant keys. Files outside variants/ are always kept, and the response\'s variantKeys still names every variant. Ignored unless the workflow has variant mode enabled; absent keeps every variant.',
4678
+ },
4679
+ {
4680
+ name: "maxFileBytes",
4681
+ flag: "max-file-bytes",
4682
+ type: "integer",
4683
+ required: false,
4684
+ description:
4685
+ "Per-file ceiling, in bytes, for inlined file contents. Clamped to the server maximum. Files above the ceiling stay references.",
4686
+ },
4687
+ {
4688
+ name: "maxTotalBytes",
4689
+ flag: "max-total-bytes",
4690
+ type: "integer",
4691
+ required: false,
4692
+ description:
4693
+ "Ceiling, in bytes, on the total inlined file contents in one response. Clamped to the server maximum. Files are admitted in path order until the ceiling is reached; the rest stay references.",
4694
+ },
4695
+ ],
4696
+ binary: false,
4697
+ readOnly: true,
4698
+ destructive: false,
4699
+ files: "pull",
4683
4700
  },
4684
4701
  {
4685
4702
  command: "workflow version validate",
package/src/program.ts CHANGED
@@ -91,6 +91,16 @@ function leafOptions(leaf: Command, command: CliCommand) {
91
91
  for (const file of command.body?.files ?? []) {
92
92
  leaf.option(`--${file} <path>`, "File to upload. Repeatable.", collect);
93
93
  }
94
+ if (command.files === "pull")
95
+ leaf.option(
96
+ "--out <dir>",
97
+ "Write here instead of ./<workflow-name>/head (a version: ./<workflow-name>/.versions/...). --json prints the file listing instead.",
98
+ );
99
+ if (command.files === "push")
100
+ leaf.option(
101
+ "--from <dir>",
102
+ "The pulled head folder to push. Defaults to the current folder.",
103
+ );
94
104
  }
95
105
 
96
106
  function toParsed(
package/src/reference.ts CHANGED
@@ -75,7 +75,11 @@ export const GLOBAL_FLAGS: [string, string][] = [
75
75
  ];
76
76
 
77
77
  export function usage(command: CliCommand): string {
78
- const args = command.args.map((arg) => `<${arg.flag}>`).join(" ");
78
+ const args = command.args
79
+ .map((arg) =>
80
+ command.files === "push" ? `[${arg.flag}]` : `<${arg.flag}>`,
81
+ )
82
+ .join(" ");
79
83
  return `asteroid ${command.command}${args ? ` ${args}` : ""} [flags]`;
80
84
  }
81
85
 
@@ -109,6 +113,14 @@ export function flagLines(command: CliCommand): string[] {
109
113
  for (const file of command.body?.files ?? []) {
110
114
  lines.push(`--${file} <path> File to upload (repeatable)`);
111
115
  }
116
+ if (command.files === "pull")
117
+ lines.push(
118
+ "--out <dir> Write here instead of ./<workflow-name>/head (a version: ./<workflow-name>/.versions/...). --json prints the file listing instead",
119
+ );
120
+ if (command.files === "push")
121
+ lines.push(
122
+ "--from <dir> The pulled head folder to push. Defaults to the current folder",
123
+ );
112
124
  return lines;
113
125
  }
114
126
 
package/src/run.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, readFileSync } from "node:fs";
1
+ import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { beforeEach, describe, expect, it } from "vitest";
@@ -323,8 +323,8 @@ describe("terminal safety", () => {
323
323
  });
324
324
  });
325
325
 
326
- describe("workflow pull", () => {
327
- it("pulls a pinned version into the folder it is given", async () => {
326
+ describe("files --out", () => {
327
+ it("writes a pinned version into the folder it is given", async () => {
328
328
  const h = harness({ ASTEROID_API_KEY: "sk_test" }, () =>
329
329
  json({
330
330
  rev: 1,
@@ -344,7 +344,7 @@ describe("workflow pull", () => {
344
344
  const dir = join(h.configDir, "pulled");
345
345
 
346
346
  const code = await run(
347
- ["workflow", "pull", "w1", "--version-id", "v-2", "--dir", dir],
347
+ ["workflow", "version", "pull", "w1", "v-2", "--out", dir],
348
348
  h.deps,
349
349
  );
350
350
 
@@ -356,6 +356,70 @@ describe("workflow pull", () => {
356
356
  });
357
357
  });
358
358
 
359
+ describe("workflow pull and push", () => {
360
+ const tree = {
361
+ rev: 1,
362
+ version: 2,
363
+ versionId: "v-2",
364
+ workflowName: "Fill form",
365
+ files: [
366
+ { path: "settings.yaml", kind: "structural", size: 4, content: "a: 1" },
367
+ ],
368
+ };
369
+
370
+ it("pulls into ./<workflow-name>/head and pushes from there with no arguments", async () => {
371
+ const h = harness({ ASTEROID_API_KEY: "sk_test" }, (call) =>
372
+ call.init.method === "PATCH" ? json({}) : json({ ...tree, rev: 2 }),
373
+ );
374
+
375
+ expect(await run(["workflow", "pull", "w1"], h.deps)).toBe(0);
376
+ const head = join(h.configDir, "Fill_form", "head");
377
+ expect(readFileSync(join(head, "settings.yaml"), "utf8")).toBe("a: 1");
378
+
379
+ writeFileSync(join(head, "settings.yaml"), "a: 2");
380
+ const code = await run(["workflow", "push"], { ...h.deps, cwd: head });
381
+
382
+ expect(code).toBe(0);
383
+ const patch = h.calls.find((call) => call.init.method === "PATCH")!;
384
+ expect(patch.url).toBe(
385
+ "https://odyssey.asteroid.ai/agents/v2/workflows/w1/head/files",
386
+ );
387
+ expect(JSON.parse(String(patch.init.body))).toMatchObject({ baseRev: 2 });
388
+ });
389
+
390
+ it("pushes the folder --from names, taking the workflow from its manifest", async () => {
391
+ const h = harness({ ASTEROID_API_KEY: "sk_test" }, (call) =>
392
+ call.init.method === "PATCH" ? json({}) : json({ ...tree, rev: 2 }),
393
+ );
394
+ await run(["workflow", "pull", "w9"], h.deps);
395
+ const head = join(h.configDir, "Fill_form", "head");
396
+ writeFileSync(join(head, "settings.yaml"), "a: 3");
397
+
398
+ const code = await run(["workflow", "push", "--from", head], h.deps);
399
+
400
+ expect(code).toBe(0);
401
+ const patch = h.calls.find((call) => call.init.method === "PATCH")!;
402
+ expect(patch.url).toBe(
403
+ "https://odyssey.asteroid.ai/agents/v2/workflows/w9/head/files",
404
+ );
405
+ });
406
+ });
407
+
408
+ describe("workflow pull with a filter", () => {
409
+ it("refuses a partial tree, which would delete the files it leaves out", async () => {
410
+ const h = harness({ ASTEROID_API_KEY: "sk_test" }, () => json({}));
411
+
412
+ const code = await run(
413
+ ["workflow", "pull", "w1", "--paths", "a.md"],
414
+ h.deps,
415
+ );
416
+
417
+ expect(code).toBe(2);
418
+ expect(h.calls).toEqual([]);
419
+ expect(h.err.join("")).toContain("cannot take --paths");
420
+ });
421
+ });
422
+
359
423
  describe("reference", () => {
360
424
  it("matches the generated agent-docs page", () => {
361
425
  const page = readFileSync(
package/src/run.ts CHANGED
@@ -17,7 +17,7 @@ import { clean, render } from "./output";
17
17
  import { parseInvocation } from "./program";
18
18
  import { BUILTINS } from "./reference";
19
19
  import { buildRequest } from "./request";
20
- import { runSync } from "./sync/run";
20
+ import { runsLocally, runSync } from "./sync/run";
21
21
  import type { Remote } from "./sync/types";
22
22
  import type { CliCommand } from "./types";
23
23
  import type { Ui } from "./ui";
@@ -189,6 +189,10 @@ export async function run(argv: string[], deps: Deps): Promise<number> {
189
189
  builtins: BUILTINS,
190
190
  version: VERSION,
191
191
  description: DESCRIPTION,
192
+ defaultArg: (command, arg) =>
193
+ command.files === "push" && arg.name === "workflowId" ? "" : undefined,
194
+ defaultArgNote:
195
+ "<workflow-id> defaults to the workflow the pushed folder was pulled from.",
192
196
  });
193
197
  if (invocation.kind === "exit") {
194
198
  if (invocation.stdout) deps.stdout(invocation.stdout);
@@ -198,7 +202,7 @@ export async function run(argv: string[], deps: Deps): Promise<number> {
198
202
  if (invocation.kind === "builtin") {
199
203
  return await runBuiltin(invocation.name, invocation.parsed, deps);
200
204
  }
201
- if (invocation.command.local) {
205
+ if (runsLocally(invocation.command, invocation.parsed)) {
202
206
  const auth = await authFor(invocation.parsed, deps);
203
207
  announce(invocation.command, auth, deps);
204
208
  return await runSync(invocation.command, invocation.parsed, {