@asteroidai/cli 1.9.2 → 1.9.11

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.2",
3
+ "version": "1.9.11",
4
4
  "description": "The asteroid command-line interface for the Asteroid platform.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,7 +38,8 @@
38
38
  "tsx": "^4.23.12",
39
39
  "typescript": "5.9.3",
40
40
  "vitest": "^4.1.11",
41
- "yaml": "^2.9.0"
41
+ "yaml": "^2.9.0",
42
+ "@asteroid/workflow-files": "0.1.0"
42
43
  },
43
44
  "scripts": {
44
45
  "generate": "tsx scripts/generate.ts && pnpm exec prettier --write src/generated/commands.ts",
@@ -0,0 +1,13 @@
1
+ import { COMMANDS } from "./generated/commands";
2
+ import { SYNC_COMMANDS } from "./sync/commands";
3
+ import type { CliCommand } from "./types";
4
+
5
+ export function withSyncCommands(
6
+ commands: readonly CliCommand[],
7
+ ): CliCommand[] {
8
+ return [...commands, ...SYNC_COMMANDS].sort((a, b) =>
9
+ a.command.localeCompare(b.command),
10
+ );
11
+ }
12
+
13
+ export const ALL_COMMANDS = withSyncCommands(COMMANDS);
package/src/core.ts CHANGED
@@ -8,3 +8,34 @@ export {
8
8
  } from "./program";
9
9
  export { type BuiltRequest, buildRequest, type InputReader } from "./request";
10
10
  export type { CliCommand, Option } from "./types";
11
+ export { ALL_COMMANDS, withSyncCommands } from "./commands";
12
+ export { type Activity, formatActivitiesMarkdown } from "./sync/activities";
13
+ export {
14
+ type Execution,
15
+ type ExecutionPullOptions,
16
+ type ExecutionPullResult,
17
+ formatExecutionPull,
18
+ pullExecution,
19
+ } from "./sync/execution-pull";
20
+ export { matchesGlob } from "./sync/paths";
21
+ export { runSync, type SyncDeps, SYNC_COMMANDS } from "./sync/run";
22
+ export {
23
+ type FileEntry,
24
+ type FileTree,
25
+ type LocalFiles,
26
+ MANIFEST,
27
+ type Manifest,
28
+ type Remote,
29
+ type Sync,
30
+ } from "./sync/types";
31
+ export {
32
+ formatWorkflowPull,
33
+ pullWorkflow,
34
+ type WorkflowPullOptions,
35
+ type WorkflowPullResult,
36
+ } from "./sync/workflow-pull";
37
+ export {
38
+ formatWorkflowPush,
39
+ pushWorkflow,
40
+ type WorkflowPushResult,
41
+ } from "./sync/workflow-push";
package/src/main.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
 
3
+ import { ALL_COMMANDS } from "./commands";
3
4
  import { readEnv } from "./config";
4
- import { COMMANDS } from "./generated/commands";
5
5
  import { runCompletion } from "./program";
6
6
  import { BUILTINS } from "./reference";
7
7
  import { run } from "./run";
@@ -27,7 +27,7 @@ function openUrl(url: string) {
27
27
 
28
28
  const argv = process.argv.slice(2);
29
29
  if (argv[0] === "complete") {
30
- await runCompletion(argv, { commands: COMMANDS, builtins: BUILTINS });
30
+ await runCompletion(argv, { commands: ALL_COMMANDS, builtins: BUILTINS });
31
31
  process.exit(0);
32
32
  }
33
33
 
@@ -49,4 +49,23 @@ describe("render", () => {
49
49
  expect(out).toContain("EXECUTION.STATUS");
50
50
  expect(out).not.toContain("AGENTID");
51
51
  });
52
+
53
+ it("keeps long cells whole when asked", () => {
54
+ const secrets = `ECW(51d1db07-a6e1-4b1d-8303-a472b78923e8)[PASSWORD=##ECW_PASSWORD##]`;
55
+ const rows = [{ id: "p-1", secrets }];
56
+ expect(render(rows)).toContain("…");
57
+ expect(render(rows, { full: true })).toContain(secrets);
58
+ });
59
+
60
+ it("shows every column when asked, six otherwise", () => {
61
+ const row = Object.fromEntries(
62
+ ["id", "a", "b", "c", "d", "e", "f", "g"].map((key) => [
63
+ key,
64
+ key.toUpperCase(),
65
+ ]),
66
+ );
67
+ const header = (out: string) => out.split("\n")[0].trim().split(/\s+/);
68
+ expect(header(render([row]))).toHaveLength(6);
69
+ expect(header(render([row], { full: true }))).toHaveLength(8);
70
+ });
52
71
  });
package/src/output.ts CHANGED
@@ -22,6 +22,7 @@ type Row = Record<string, unknown>;
22
22
 
23
23
  export interface RenderOptions {
24
24
  styled?: boolean;
25
+ full?: boolean;
25
26
  }
26
27
 
27
28
  export function clean(text: string): string {
@@ -49,13 +50,14 @@ function text(value: unknown): string {
49
50
  return clean(isScalar(value) ? String(value) : JSON.stringify(value));
50
51
  }
51
52
 
52
- function cell(value: unknown): string {
53
+ function cell(value: unknown, options: RenderOptions = {}): string {
53
54
  if (value === null || value === undefined) return "";
54
55
  const line = text(value).replace(/\s+/g, " ");
55
- return line.length > MAX_CELL ? `${line.slice(0, MAX_CELL - 1)}…` : line;
56
+ if (options.full || line.length <= MAX_CELL) return line;
57
+ return `${line.slice(0, MAX_CELL - 1)}…`;
56
58
  }
57
59
 
58
- function columns(rows: Row[]): string[] {
60
+ function columns(rows: Row[], options: RenderOptions): string[] {
59
61
  const scalarKeys: string[] = [];
60
62
  for (const row of rows) {
61
63
  for (const [key, value] of Object.entries(row)) {
@@ -71,7 +73,7 @@ function columns(rows: Row[]): string[] {
71
73
  ...PREFERRED.filter((key) => scalarKeys.includes(key)),
72
74
  ...scalarKeys.filter((key) => !PREFERRED.includes(key)),
73
75
  ];
74
- return ordered.slice(0, MAX_COLUMNS);
76
+ return options.full ? ordered : ordered.slice(0, MAX_COLUMNS);
75
77
  }
76
78
 
77
79
  function style(
@@ -100,9 +102,9 @@ function flatten(row: Row): Row {
100
102
  export function table(input: Row[], options: RenderOptions = {}): string {
101
103
  if (input.length === 0) return "No results.";
102
104
  const rows = input.map(flatten);
103
- const keys = columns(rows);
105
+ const keys = columns(rows, options);
104
106
  if (keys.length === 0) return rows.map((row) => text(row)).join("\n");
105
- const cells = rows.map((row) => keys.map((key) => cell(row[key])));
107
+ const cells = rows.map((row) => keys.map((key) => cell(row[key], options)));
106
108
  if (options.styled) {
107
109
  const grid = new Table({
108
110
  head: keys.map((key) => style("bold", key, options)),
@@ -124,10 +126,10 @@ export function table(input: Row[], options: RenderOptions = {}): string {
124
126
  );
125
127
  }
126
128
 
127
- function summary(value: unknown): string {
129
+ function summary(value: unknown, options: RenderOptions): string {
128
130
  if (Array.isArray(value))
129
131
  return `[${value.length} item${value.length === 1 ? "" : "s"}]`;
130
- if (value && typeof value === "object") return cell(value);
132
+ if (value && typeof value === "object") return cell(value, options);
131
133
  return text(value);
132
134
  }
133
135
 
@@ -145,7 +147,7 @@ function record(value: Row, options: RenderOptions): string {
145
147
  if (isObject(v) && Object.keys(v as Row).length > 0) {
146
148
  return `${style("bold", key, options)}:\n${indent(record(v as Row, options))}`;
147
149
  }
148
- return `${label(key)} ${isScalar(v) ? text(v) : summary(v)}`;
150
+ return `${label(key)} ${isScalar(v) ? text(v) : summary(v, options)}`;
149
151
  })
150
152
  .join("\n");
151
153
  }
@@ -133,4 +133,23 @@ describe("parseInvocation", () => {
133
133
  "expected [workflow-id] <schedule-id>, got 0 argument(s)",
134
134
  );
135
135
  });
136
+
137
+ it("prints the version only as the first argument", async () => {
138
+ const options = {
139
+ commands: COMMANDS,
140
+ builtins: BUILTINS,
141
+ version: "1.2.3",
142
+ };
143
+
144
+ const version = await parseInvocation(["--version"], options);
145
+ const create = await parseInvocation(
146
+ ["workflow", "create", "--name", "W", "--version", "{}"],
147
+ options,
148
+ );
149
+
150
+ expect(version).toMatchObject({ kind: "exit", code: 0, stdout: "1.2.3\n" });
151
+ expect(create.kind).toBe("command");
152
+ if (create.kind === "command")
153
+ expect(create.parsed.flags.get("version")).toEqual(["{}"]);
154
+ });
136
155
  });
package/src/program.ts CHANGED
@@ -85,6 +85,8 @@ function leafOptions(leaf: Command, command: CliCommand) {
85
85
  optionText(option),
86
86
  collect,
87
87
  );
88
+ if (option.negatable)
89
+ leaf.option(`--no-${option.flag}`, `Skip ${option.flag}.`);
88
90
  }
89
91
  for (const file of command.body?.files ?? []) {
90
92
  leaf.option(`--${file} <path>`, "File to upload. Repeatable.", collect);
@@ -122,7 +124,11 @@ export function defineCommands(
122
124
  onInvocation: (invocation: Invocation) => void = () => undefined,
123
125
  ): Command {
124
126
  if (options.description) root.description(options.description);
125
- if (options.version) root.version(options.version, "-v, --version");
127
+ if (options.version)
128
+ root.addHelpText(
129
+ "after",
130
+ "\nRun `asteroid --version` to print the CLI version.",
131
+ );
126
132
  for (const [flags, text] of GLOBAL_FLAGS) {
127
133
  if (!flags.startsWith("-h")) root.option(flags, text);
128
134
  }
@@ -224,6 +230,14 @@ export async function parseInvocation(
224
230
  let stderr = "";
225
231
  let result: Invocation | undefined;
226
232
 
233
+ if (options.version && (argv[0] === "-v" || argv[0] === "--version")) {
234
+ return {
235
+ kind: "exit",
236
+ code: 0,
237
+ stdout: `${options.version}\n`,
238
+ stderr: "",
239
+ };
240
+ }
227
241
  const { argv: rest, dotted } = splitDotted(argv);
228
242
  const root = new Command("asteroid")
229
243
  .exitOverride()
package/src/reference.ts CHANGED
@@ -138,6 +138,7 @@ export function renderReference(commands: CliCommand[]): string {
138
138
  "Sign in with `asteroid login`, or set `ASTEROID_API_KEY` for CI and agents.",
139
139
  "Commands that return data print compact text. Add `--json` to get the response as JSON.",
140
140
  "The text output may change between releases. Scripts should read `--json`.",
141
+ "At a terminal, tables show up to 6 columns and cut long cells to fit; when output is piped, they show every column in full. Use `--json` for every field.",
141
142
  "Downloads write raw bytes. `login`, `logout` and `use` only report status on stderr.",
142
143
  "Exit codes: 0 ok, 1 API error, 2 usage error.",
143
144
  "",
package/src/run.test.ts CHANGED
@@ -4,8 +4,8 @@ import { join } from "node:path";
4
4
  import { beforeEach, describe, expect, it } from "vitest";
5
5
 
6
6
  import { CredentialStore } from "./auth/store";
7
+ import { ALL_COMMANDS } from "./commands";
7
8
  import { readEnv, saveConfig } from "./config";
8
- import { COMMANDS } from "./generated/commands";
9
9
  import { renderReference } from "./reference";
10
10
  import { type Deps, run } from "./run";
11
11
 
@@ -269,6 +269,26 @@ describe("guards", () => {
269
269
  });
270
270
  });
271
271
 
272
+ describe("cell width", () => {
273
+ const long = "x".repeat(80);
274
+ const reply = () => json({ items: [{ id: "w-1", name: long }] });
275
+
276
+ it("keeps cells whole when stdout is not a terminal", async () => {
277
+ const h = harness({ ASTEROID_API_KEY: "sk_test" }, reply);
278
+ await run(["workflow", "list"], h.deps);
279
+ expect(h.out.join("")).toContain(long);
280
+ });
281
+
282
+ it("cuts long cells at a terminal", async () => {
283
+ const h = harness({ ASTEROID_API_KEY: "sk_test" }, reply);
284
+ h.deps.stdoutIsTTY = true;
285
+ await run(["workflow", "list"], h.deps);
286
+ const out = h.out.join("");
287
+ expect(out).toContain(`${"x".repeat(59)}…`);
288
+ expect(out).not.toContain(long);
289
+ });
290
+ });
291
+
272
292
  describe("terminal safety", () => {
273
293
  it("refuses to print a download to a terminal", async () => {
274
294
  const h = harness(
@@ -303,12 +323,45 @@ describe("terminal safety", () => {
303
323
  });
304
324
  });
305
325
 
326
+ describe("workflow pull", () => {
327
+ it("pulls a pinned version into the folder it is given", async () => {
328
+ const h = harness({ ASTEROID_API_KEY: "sk_test" }, () =>
329
+ json({
330
+ rev: 1,
331
+ version: 2,
332
+ versionId: "v-2",
333
+ workflowName: "Fill form",
334
+ files: [
335
+ {
336
+ path: "settings.yaml",
337
+ kind: "structural",
338
+ size: 3,
339
+ content: "a: 1",
340
+ },
341
+ ],
342
+ }),
343
+ );
344
+ const dir = join(h.configDir, "pulled");
345
+
346
+ const code = await run(
347
+ ["workflow", "pull", "w1", "--version-id", "v-2", "--dir", dir],
348
+ h.deps,
349
+ );
350
+
351
+ expect(code).toBe(0);
352
+ expect(h.calls[0].url).toBe(
353
+ "https://odyssey.asteroid.ai/agents/v2/workflows/w1/versions/v-2/files?contents=all",
354
+ );
355
+ expect(readFileSync(join(dir, "settings.yaml"), "utf8")).toBe("a: 1");
356
+ });
357
+ });
358
+
306
359
  describe("reference", () => {
307
360
  it("matches the generated agent-docs page", () => {
308
361
  const page = readFileSync(
309
362
  new URL("../../../docs/cli/reference.mdx", import.meta.url),
310
363
  "utf8",
311
364
  );
312
- expect(page).toBe(renderReference(COMMANDS));
365
+ expect(page).toBe(renderReference(ALL_COMMANDS));
313
366
  });
314
367
  });
package/src/run.ts CHANGED
@@ -10,13 +10,15 @@ import {
10
10
  UNSET_API_KEY,
11
11
  } from "./auth/credentials";
12
12
  import { WorkosError } from "./auth/workos";
13
+ import { ALL_COMMANDS } from "./commands";
13
14
  import { type Env, loadConfig, orgLabel, requestedOrg } from "./config";
14
- import { COMMANDS } from "./generated/commands";
15
15
  import { ApiError, send, VERSION } from "./http";
16
16
  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";
21
+ import type { Remote } from "./sync/types";
20
22
  import type { CliCommand } from "./types";
21
23
  import type { Ui } from "./ui";
22
24
 
@@ -82,6 +84,28 @@ async function credentialOrganization(
82
84
  ?.organizationId;
83
85
  }
84
86
 
87
+ async function authFor(parsed: Parsed, deps: Deps): Promise<Auth> {
88
+ rejectOrgWithApiKey(deps, parsed.org);
89
+ const config = loadConfig(deps.env);
90
+ return resolveAuth(
91
+ deps.env,
92
+ config,
93
+ requestedOrg(parsed.org, deps.env, deps.cwd, config),
94
+ deps.fetch,
95
+ );
96
+ }
97
+
98
+ function announce(command: CliCommand, auth: Auth, deps: Deps) {
99
+ if (command.readOnly) return;
100
+ const org =
101
+ auth.kind === "api-key"
102
+ ? "the API key's organization"
103
+ : auth.org
104
+ ? orgLabel(auth.org)
105
+ : "your default organization";
106
+ deps.stderr(`Acting in ${org}.\n`);
107
+ }
108
+
85
109
  async function runCommand(
86
110
  command: CliCommand,
87
111
  parsed: Parsed,
@@ -91,26 +115,11 @@ async function runCommand(
91
115
  let request = needsOrganization
92
116
  ? undefined
93
117
  : await buildRequest(command, parsed);
94
- rejectOrgWithApiKey(deps, parsed.org);
95
- const config = loadConfig(deps.env);
96
- const auth = await resolveAuth(
97
- deps.env,
98
- config,
99
- requestedOrg(parsed.org, deps.env, deps.cwd, config),
100
- deps.fetch,
101
- );
118
+ const auth = await authFor(parsed, deps);
102
119
  request ??= await buildRequest(command, parsed, {
103
120
  organizationId: await credentialOrganization(deps, auth),
104
121
  });
105
- if (!command.readOnly) {
106
- const org =
107
- auth.kind === "api-key"
108
- ? "the API key's organization"
109
- : auth.org
110
- ? orgLabel(auth.org)
111
- : "your default organization";
112
- deps.stderr(`Acting in ${org}.\n`);
113
- }
122
+ announce(command, auth, deps);
114
123
 
115
124
  const response = await send(
116
125
  deps.env.apiUrl,
@@ -138,7 +147,14 @@ async function runCommand(
138
147
  }
139
148
  if (response.json !== undefined) {
140
149
  deps.stdout(
141
- `${parsed.json ? JSON.stringify(response.json, null, 2) : render(response.json, { styled: deps.styled })}\n`,
150
+ `${
151
+ parsed.json
152
+ ? JSON.stringify(response.json, null, 2)
153
+ : render(response.json, {
154
+ styled: deps.styled,
155
+ full: !deps.stdoutIsTTY,
156
+ })
157
+ }\n`,
142
158
  );
143
159
  return 0;
144
160
  }
@@ -146,13 +162,30 @@ async function runCommand(
146
162
  return 0;
147
163
  }
148
164
 
165
+ function nodeRemote(
166
+ apiUrl: string,
167
+ auth: Auth,
168
+ fetchImpl: typeof fetch,
169
+ ): Remote {
170
+ return {
171
+ json: async (request) =>
172
+ (await send(apiUrl, auth, request, false, fetchImpl)).json,
173
+ async download(url) {
174
+ const response = await fetchImpl(url);
175
+ if (!response.ok)
176
+ throw new Error(`download failed: HTTP ${response.status}`);
177
+ return new Uint8Array(await response.arrayBuffer());
178
+ },
179
+ };
180
+ }
181
+
149
182
  const DESCRIPTION =
150
- "The Asteroid CLI. Sign in with `asteroid login`, or set ASTEROID_API_KEY.";
183
+ "The Asteroid CLI. Sign in with `asteroid login`, or set ASTEROID_API_KEY.\nAt a terminal, tables show up to 6 columns and cut long cells to fit; when output is piped, they show every column in full. Use `--json` for every field.";
151
184
 
152
185
  export async function run(argv: string[], deps: Deps): Promise<number> {
153
186
  try {
154
187
  const invocation = await parseInvocation(argv, {
155
- commands: COMMANDS,
188
+ commands: ALL_COMMANDS,
156
189
  builtins: BUILTINS,
157
190
  version: VERSION,
158
191
  description: DESCRIPTION,
@@ -165,6 +198,14 @@ export async function run(argv: string[], deps: Deps): Promise<number> {
165
198
  if (invocation.kind === "builtin") {
166
199
  return await runBuiltin(invocation.name, invocation.parsed, deps);
167
200
  }
201
+ if (invocation.command.local) {
202
+ const auth = await authFor(invocation.parsed, deps);
203
+ announce(invocation.command, auth, deps);
204
+ return await runSync(invocation.command, invocation.parsed, {
205
+ ...deps,
206
+ remote: nodeRemote(deps.env.apiUrl, auth, deps.fetch),
207
+ });
208
+ }
168
209
  return await runCommand(invocation.command, invocation.parsed, deps);
169
210
  } catch (error) {
170
211
  if (error instanceof UsageError) {
@@ -0,0 +1,80 @@
1
+ interface Payload {
2
+ activityType: string;
3
+ display?: { turnId?: string; summaryText?: string };
4
+ reasoning?: string;
5
+ message?: string;
6
+ actionName?: string;
7
+ info?: Record<string, unknown>;
8
+ summaryTokens?: number;
9
+ durationMs?: number;
10
+ }
11
+
12
+ export interface Activity {
13
+ payload: Payload;
14
+ }
15
+
16
+ function indentJson(data: Record<string, unknown>): string {
17
+ return JSON.stringify(data, null, 2)
18
+ .split("\n")
19
+ .map((line) => ` ${line}`)
20
+ .join("\n");
21
+ }
22
+
23
+ function action(label: string, payload: Payload): string {
24
+ const info = payload.info;
25
+ const detail =
26
+ info && Object.keys(info).length > 0 ? `\ninfo:\n${indentJson(info)}` : "";
27
+ return `[${label}: ${payload.actionName}]:${detail}\n`;
28
+ }
29
+
30
+ function line(payload: Payload): string {
31
+ switch (payload.activityType) {
32
+ case "reasoning":
33
+ return `[reasoning]:\n${payload.reasoning}\n`;
34
+ case "generic":
35
+ return `[message]:\n${payload.message}\n`;
36
+ case "user_message_received":
37
+ return `[user_message]:\n${payload.message}\n`;
38
+ case "action_started":
39
+ return action("action_started", payload);
40
+ case "action_completed":
41
+ return action("action_completed", payload);
42
+ case "action_failed":
43
+ return `[action_failed: ${payload.actionName}]:\nfailure: ${payload.message}\n`;
44
+ case "compaction_performed":
45
+ return `[compaction]: summary_tokens: ${payload.summaryTokens}, duration_ms: ${payload.durationMs}\n`;
46
+ case "compaction_started":
47
+ return "[compaction_started]\n";
48
+ default: {
49
+ const summary = payload.display?.summaryText;
50
+ return summary
51
+ ? `[${payload.activityType}]: ${summary}\n`
52
+ : `[${payload.activityType}]\n`;
53
+ }
54
+ }
55
+ }
56
+
57
+ export function formatActivitiesMarkdown(
58
+ activities: Activity[],
59
+ status: string,
60
+ createdAt: string,
61
+ ): string {
62
+ const lines = [
63
+ "# Execution Activities\n",
64
+ `Status: ${status}`,
65
+ `Started: ${createdAt}`,
66
+ ];
67
+ let currentTurn = "";
68
+ for (const { payload } of activities) {
69
+ const turnId = payload.display?.turnId ?? "";
70
+ if (turnId && turnId !== currentTurn) {
71
+ currentTurn = turnId;
72
+ lines.push(`\n---\n\n## ${turnId}\n`);
73
+ } else if (!currentTurn && !turnId) {
74
+ currentTurn = "_started";
75
+ lines.push("\n---\n");
76
+ }
77
+ lines.push(line(payload));
78
+ }
79
+ return lines.join("\n");
80
+ }
@@ -0,0 +1,111 @@
1
+ import type { CliCommand, Option } from "../types";
2
+
3
+ function option(
4
+ name: string,
5
+ type: Option["type"],
6
+ description: string,
7
+ extra: Partial<Option> = {},
8
+ ): Option {
9
+ return { name, flag: name, type, required: false, description, ...extra };
10
+ }
11
+
12
+ const DIR = option(
13
+ "dir",
14
+ "string",
15
+ "Write here instead of `<workflow-name>/...` under the current directory.",
16
+ );
17
+
18
+ const local = {
19
+ method: "GET",
20
+ path: "",
21
+ binary: false,
22
+ destructive: false,
23
+ local: true,
24
+ } as const;
25
+
26
+ export const SYNC_COMMANDS: CliCommand[] = [
27
+ {
28
+ ...local,
29
+ command: "workflow pull",
30
+ tool: "workflow_pull",
31
+ title: "Pull a workflow to disk",
32
+ description:
33
+ "Write a workflow's editable head to `<workflow-name>/head/` as files: settings, per-node instructions and scripts, and the workflow's own files. Pass --version-id for a pinned version, written read-only to `<workflow-name>/.versions/`. Edit the head files, then run `asteroid workflow push` from that folder.",
34
+ readOnly: true,
35
+ args: [
36
+ {
37
+ name: "workflowId",
38
+ flag: "workflow-id",
39
+ type: "string",
40
+ required: true,
41
+ },
42
+ ],
43
+ query: [
44
+ option(
45
+ "version-id",
46
+ "string",
47
+ "A workflow version ID. Omit for the editable head.",
48
+ ),
49
+ DIR,
50
+ ],
51
+ },
52
+ {
53
+ ...local,
54
+ command: "workflow push",
55
+ tool: "workflow_push",
56
+ title: "Push local edits to a workflow head",
57
+ description:
58
+ "Send the edits in a pulled head folder to the workflow's editable head: changed and new files are written, removed files are deleted. Fails if the head changed since the pull; pull again and reapply. Push does not publish.",
59
+ readOnly: false,
60
+ args: [],
61
+ query: [
62
+ option(
63
+ "dir",
64
+ "string",
65
+ "The pulled head folder. Defaults to the current directory.",
66
+ ),
67
+ ],
68
+ },
69
+ {
70
+ ...local,
71
+ command: "execution pull",
72
+ tool: "execution_pull",
73
+ title: "Pull an execution to disk",
74
+ description:
75
+ "Write an execution to `<workflow-name>/executions/<id>/`: summary.json, activities.md (the step-by-step log), and the files the run produced.",
76
+ readOnly: true,
77
+ args: [
78
+ {
79
+ name: "executionId",
80
+ flag: "execution-id",
81
+ type: "string",
82
+ required: true,
83
+ },
84
+ ],
85
+ query: [
86
+ option(
87
+ "files",
88
+ "string",
89
+ "Only download files whose path matches this glob. --no-files skips downloads.",
90
+ {
91
+ negatable: true,
92
+ },
93
+ ),
94
+ option(
95
+ "summary-only",
96
+ "boolean",
97
+ "Write summary.json only: no activities, no files.",
98
+ ),
99
+ option(
100
+ "max-file-size",
101
+ "integer",
102
+ "Skip files larger than this many bytes.",
103
+ ),
104
+ option(
105
+ "dir",
106
+ "string",
107
+ "Write here instead of `<workflow-name>/executions/<id>/` under the current directory.",
108
+ ),
109
+ ],
110
+ },
111
+ ];