@asteroidai/cli 1.9.12 → 1.9.28

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/src/run.ts DELETED
@@ -1,229 +0,0 @@
1
- import { writeFileSync } from "node:fs";
2
-
3
- import { login, logout, orgs, use, whoami } from "./account";
4
- import { type Parsed, UsageError } from "./args";
5
- import {
6
- API_KEY_OVERRIDES,
7
- type Auth,
8
- AuthError,
9
- resolveAuth,
10
- UNSET_API_KEY,
11
- } from "./auth/credentials";
12
- import { WorkosError } from "./auth/workos";
13
- import { ALL_COMMANDS } from "./commands";
14
- import { type Env, loadConfig, orgLabel, requestedOrg } from "./config";
15
- import { ApiError, send, VERSION } from "./http";
16
- import { clean, render } from "./output";
17
- import { parseInvocation } from "./program";
18
- import { BUILTINS } from "./reference";
19
- import { buildRequest } from "./request";
20
- import { runsLocally, runSync } from "./sync/run";
21
- import type { Remote } from "./sync/types";
22
- import type { CliCommand } from "./types";
23
- import type { Ui } from "./ui";
24
-
25
- export interface Deps {
26
- env: Env;
27
- cwd: string;
28
- fetch: typeof fetch;
29
- stdout: (text: string) => void;
30
- stderr: (text: string) => void;
31
- writeBytes: (bytes: Uint8Array) => void;
32
- stdoutIsTTY?: boolean;
33
- styled?: boolean;
34
- ui?: Ui;
35
- openUrl?: (url: string) => void;
36
- }
37
-
38
- function rejectOrgWithApiKey(deps: Deps, flagOrg: string | undefined) {
39
- if (deps.env.apiKey && (flagOrg || deps.env.org)) {
40
- throw new UsageError(
41
- `${API_KEY_OVERRIDES} The key acts in its own organization, so drop --org and ASTEROID_ORG. ${UNSET_API_KEY}`,
42
- );
43
- }
44
- }
45
-
46
- async function runBuiltin(
47
- name: string,
48
- parsed: Parsed,
49
- deps: Deps,
50
- ): Promise<number> {
51
- if (name !== "login" && name !== "logout")
52
- rejectOrgWithApiKey(deps, parsed.org);
53
- switch (name) {
54
- case "login":
55
- return login(deps);
56
- case "logout":
57
- return logout(deps);
58
- case "orgs":
59
- return orgs(deps, parsed.org, parsed.json);
60
- case "complete":
61
- throw new UsageError(
62
- "run `asteroid complete <shell>` to print a completion script",
63
- );
64
- case "use":
65
- return use(deps, parsed.positionals[0]);
66
- default:
67
- return whoami(deps, parsed.org, parsed.json);
68
- }
69
- }
70
-
71
- async function credentialOrganization(
72
- deps: Deps,
73
- auth: Auth,
74
- ): Promise<string | undefined> {
75
- if (auth.org?.id) return auth.org.id;
76
- const response = await send(
77
- deps.env.apiUrl,
78
- auth,
79
- { method: "GET", path: "/users/v2/whoami", query: new URLSearchParams() },
80
- false,
81
- deps.fetch,
82
- );
83
- return (response.json as { organizationId?: string } | undefined)
84
- ?.organizationId;
85
- }
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
-
109
- async function runCommand(
110
- command: CliCommand,
111
- parsed: Parsed,
112
- deps: Deps,
113
- ): Promise<number> {
114
- const needsOrganization = command.body?.credentialOrganization === true;
115
- let request = needsOrganization
116
- ? undefined
117
- : await buildRequest(command, parsed);
118
- const auth = await authFor(parsed, deps);
119
- request ??= await buildRequest(command, parsed, {
120
- organizationId: await credentialOrganization(deps, auth),
121
- });
122
- announce(command, auth, deps);
123
-
124
- const response = await send(
125
- deps.env.apiUrl,
126
- auth,
127
- request,
128
- command.binary,
129
- deps.fetch,
130
- );
131
- if (response.bytes) {
132
- if (parsed.output) {
133
- writeFileSync(parsed.output, response.bytes);
134
- deps.stderr(
135
- `Wrote ${response.bytes.length} bytes to ${parsed.output}.\n`,
136
- );
137
- } else if (deps.stdoutIsTTY) {
138
- throw new UsageError(
139
- "refusing to print binary data to a terminal. Pass --output <file> or redirect stdout.",
140
- );
141
- } else deps.writeBytes(response.bytes);
142
- return 0;
143
- }
144
- if (response.status === 204) {
145
- if (!parsed.json) deps.stderr("Done.\n");
146
- return 0;
147
- }
148
- if (response.json !== undefined) {
149
- deps.stdout(
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`,
158
- );
159
- return 0;
160
- }
161
- if (response.text) deps.stdout(`${clean(response.text)}\n`);
162
- return 0;
163
- }
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
-
182
- const DESCRIPTION =
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.";
184
-
185
- export async function run(argv: string[], deps: Deps): Promise<number> {
186
- try {
187
- const invocation = await parseInvocation(argv, {
188
- commands: ALL_COMMANDS,
189
- builtins: BUILTINS,
190
- version: VERSION,
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.",
196
- });
197
- if (invocation.kind === "exit") {
198
- if (invocation.stdout) deps.stdout(invocation.stdout);
199
- if (invocation.stderr) deps.stderr(invocation.stderr);
200
- return invocation.code;
201
- }
202
- if (invocation.kind === "builtin") {
203
- return await runBuiltin(invocation.name, invocation.parsed, deps);
204
- }
205
- if (runsLocally(invocation.command, invocation.parsed)) {
206
- const auth = await authFor(invocation.parsed, deps);
207
- announce(invocation.command, auth, deps);
208
- return await runSync(invocation.command, invocation.parsed, {
209
- ...deps,
210
- remote: nodeRemote(deps.env.apiUrl, auth, deps.fetch),
211
- });
212
- }
213
- return await runCommand(invocation.command, invocation.parsed, deps);
214
- } catch (error) {
215
- if (error instanceof UsageError) {
216
- deps.stderr(`asteroid: ${clean(error.message)}\n`);
217
- return 2;
218
- }
219
- if (error instanceof ApiError) {
220
- deps.stderr(`asteroid: ${error.status}: ${clean(error.message)}\n`);
221
- return 1;
222
- }
223
- if (error instanceof AuthError || error instanceof WorkosError) {
224
- deps.stderr(`asteroid: ${clean(error.message)}\n`);
225
- return 1;
226
- }
227
- throw error;
228
- }
229
- }
@@ -1,80 +0,0 @@
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
- }
@@ -1,63 +0,0 @@
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 local = {
13
- method: "GET",
14
- path: "",
15
- binary: false,
16
- destructive: false,
17
- local: true,
18
- } as const;
19
-
20
- export const SYNC_COMMANDS: CliCommand[] = [
21
- {
22
- ...local,
23
- command: "execution pull",
24
- tool: "execution_pull",
25
- title: "Pull an execution to disk",
26
- description:
27
- "Write an execution to `<workflow-name>/executions/<id>/`: summary.json, activities.md (the step-by-step log), and the files the run produced.",
28
- readOnly: true,
29
- args: [
30
- {
31
- name: "executionId",
32
- flag: "execution-id",
33
- type: "string",
34
- required: true,
35
- },
36
- ],
37
- query: [
38
- option(
39
- "files",
40
- "string",
41
- "Only download files whose path matches this glob. --no-files skips downloads.",
42
- {
43
- negatable: true,
44
- },
45
- ),
46
- option(
47
- "summary-only",
48
- "boolean",
49
- "Write summary.json only: no activities, no files.",
50
- ),
51
- option(
52
- "max-file-size",
53
- "integer",
54
- "Skip files larger than this many bytes.",
55
- ),
56
- option(
57
- "dir",
58
- "string",
59
- "Write here instead of `<workflow-name>/executions/<id>/` under the current directory.",
60
- ),
61
- ],
62
- },
63
- ];
@@ -1,273 +0,0 @@
1
- import { type Activity, formatActivitiesMarkdown } from "./activities";
2
- import {
3
- executionDirName,
4
- isContained,
5
- join,
6
- matchesGlob,
7
- workflowRoot,
8
- } from "./paths";
9
- import { get, type Sync } from "./types";
10
-
11
- export interface Execution {
12
- id: string;
13
- status: string;
14
- workflowId: string;
15
- workflowVersionId?: string;
16
- workflowName?: string;
17
- inputs?: unknown;
18
- executionResult?: { outcome?: string; reasoning?: string } | null;
19
- createdAt: string;
20
- terminalAt?: string | null;
21
- duration?: number | null;
22
- scriptFailures?: { nodeName?: string; error?: string }[] | null;
23
- purgedAt?: string | null;
24
- }
25
-
26
- interface ExecutionFile {
27
- id: string;
28
- fileName: string;
29
- filePath: string;
30
- fileSize: number;
31
- directory: string;
32
- }
33
-
34
- const DIRECTORIES = ["workspace", "downloads", "shared"] as const;
35
-
36
- export interface ExecutionPullOptions {
37
- executionId: string;
38
- root: string;
39
- dir?: (execution: Execution) => string | undefined;
40
- files?: boolean;
41
- activities?: boolean;
42
- glob?: string;
43
- maxFileBytes?: number;
44
- }
45
-
46
- export interface ExecutionPullResult {
47
- execution: Execution;
48
- dir: string;
49
- written: string[];
50
- downloaded: number;
51
- candidates: number;
52
- skipped: string[];
53
- failed: string[];
54
- skippedBy?: string;
55
- }
56
-
57
- export function relativeFilePath(
58
- file: Pick<ExecutionFile, "filePath" | "directory">,
59
- ): string {
60
- const at = file.filePath.indexOf(`/${file.directory}/`);
61
- if (at === -1)
62
- return `${file.directory}/${file.filePath.split("/").pop() ?? "unknown"}`;
63
- return file.filePath
64
- .slice(at + 1)
65
- .replace(/^workspace\/\.execution-context\//, "");
66
- }
67
-
68
- function executionDir(
69
- options: ExecutionPullOptions,
70
- execution: Execution,
71
- ): string {
72
- const chosen = options.dir?.(execution);
73
- if (chosen) return chosen;
74
- const root = workflowRoot(
75
- options.root,
76
- execution.workflowName ?? execution.workflowId,
77
- );
78
- return join(root, "executions", executionDirName(execution.id));
79
- }
80
-
81
- async function writeText(sync: Sync, path: string, text: string) {
82
- await sync.files.write(path, Buffer.from(text, "utf8"));
83
- }
84
-
85
- async function writeActivities(sync: Sync, dir: string, execution: Execution) {
86
- try {
87
- const activities = (await sync.remote.json(
88
- get(`/agents/v2/executions/${execution.id}/activities`, { order: "asc" }),
89
- )) as Activity[];
90
- if (activities.length === 0) return false;
91
- const markdown = formatActivitiesMarkdown(
92
- activities,
93
- execution.status,
94
- execution.createdAt,
95
- );
96
- await writeText(sync, join(dir, "activities.md"), markdown);
97
- return true;
98
- } catch {
99
- return false;
100
- }
101
- }
102
-
103
- async function listFiles(
104
- sync: Sync,
105
- executionId: string,
106
- ): Promise<ExecutionFile[]> {
107
- const listing = (await sync.remote.json(
108
- get(`/agents/v2/executions/${executionId}/files`),
109
- )) as {
110
- directories: Partial<Record<string, ExecutionFile[]>>;
111
- };
112
- return DIRECTORIES.flatMap((name) => listing.directories[name] ?? []);
113
- }
114
-
115
- async function download(sync: Sync, executionId: string, file: ExecutionFile) {
116
- const path = `/agents/v2/executions/${executionId}/files/${file.id}/signed-url`;
117
- const { url } = (await sync.remote.json(get(path))) as { url: string };
118
- return sync.remote.download(url);
119
- }
120
-
121
- function selectFiles(
122
- files: ExecutionFile[],
123
- options: ExecutionPullOptions,
124
- skipped: string[],
125
- ) {
126
- return files.filter((file) => {
127
- const path = relativeFilePath(file);
128
- if (!isContained(path)) skipped.push(`${path} (unsafe path)`);
129
- else if (options.glob !== undefined && !matchesGlob(path, options.glob))
130
- skipped.push(`${path} (excluded by --files)`);
131
- else if (
132
- options.maxFileBytes !== undefined &&
133
- file.fileSize > options.maxFileBytes
134
- )
135
- skipped.push(`${path} (${file.fileSize} bytes, over the size limit)`);
136
- else return true;
137
- return false;
138
- });
139
- }
140
-
141
- async function pullFiles(
142
- sync: Sync,
143
- dir: string,
144
- options: ExecutionPullOptions,
145
- ) {
146
- const skipped: string[] = [];
147
- const failed: string[] = [];
148
- let files: ExecutionFile[];
149
- try {
150
- files = await listFiles(sync, options.executionId);
151
- } catch (error) {
152
- const reason = error instanceof Error ? error.message : String(error);
153
- return {
154
- downloaded: 0,
155
- candidates: 0,
156
- skipped,
157
- failed: [`file listing (${reason})`],
158
- };
159
- }
160
- let downloaded = 0;
161
- await Promise.all(
162
- selectFiles(files, options, skipped).map(async (file) => {
163
- try {
164
- const bytes = await download(sync, options.executionId, file);
165
- await sync.files.write(join(dir, relativeFilePath(file)), bytes);
166
- downloaded++;
167
- } catch (error) {
168
- failed.push(
169
- `${file.fileName} (${error instanceof Error ? error.message : String(error)})`,
170
- );
171
- }
172
- }),
173
- );
174
- return { downloaded, candidates: files.length, skipped, failed };
175
- }
176
-
177
- export async function pullExecution(
178
- sync: Sync,
179
- options: ExecutionPullOptions,
180
- ): Promise<ExecutionPullResult> {
181
- const execution = (await sync.remote.json(
182
- get(`/agents/v2/executions/${options.executionId}`),
183
- )) as Execution;
184
- const dir = executionDir(options, execution);
185
- await writeText(
186
- sync,
187
- join(dir, "summary.json"),
188
- JSON.stringify(execution, null, 2),
189
- );
190
- const result = {
191
- execution,
192
- dir,
193
- written: ["summary.json"],
194
- downloaded: 0,
195
- candidates: 0,
196
- skipped: [],
197
- failed: [],
198
- };
199
- if (execution.purgedAt) return result;
200
- if (
201
- options.activities !== false &&
202
- (await writeActivities(sync, dir, execution))
203
- )
204
- result.written.push("activities.md");
205
- if (options.files === false) {
206
- return {
207
- ...result,
208
- skippedBy: options.activities === false ? "--summary-only" : "--no-files",
209
- };
210
- }
211
- return { ...result, ...(await pullFiles(sync, dir, options)) };
212
- }
213
-
214
- function filesLine(result: ExecutionPullResult): string {
215
- const parts = [...result.written];
216
- if (result.downloaded > 0) parts.push(`${result.downloaded} downloaded`);
217
- const counted = `Files: ${result.written.length + result.downloaded} written (${parts.join(", ")})`;
218
- if (result.skippedBy)
219
- return `${counted}; downloads skipped (${result.skippedBy})`;
220
- if (result.candidates === 0) return `${counted}; no execution files matched`;
221
- return counted;
222
- }
223
-
224
- function failures(execution: Execution): string | null {
225
- const list = execution.scriptFailures ?? [];
226
- if (list.length === 0) return null;
227
- const lines = list.map(
228
- (f) =>
229
- ` - ${f.nodeName ?? "(unknown node)"}: ${(f.error ?? "").split("\n")[0]}`,
230
- );
231
- return `Script failures: ${list.length}\n${lines.join("\n")}`;
232
- }
233
-
234
- function purged(result: ExecutionPullResult): string {
235
- const { execution } = result;
236
- return [
237
- `Execution: ${execution.id}`,
238
- `Status: ${execution.status}`,
239
- `Purged: ${execution.purgedAt}`,
240
- "This run was retention-purged. Activities, files, inputs and result are gone. Do not diagnose it.",
241
- `Written to: ${result.dir}/`,
242
- ].join("\n");
243
- }
244
-
245
- export function formatExecutionPull(result: ExecutionPullResult): string {
246
- const { execution } = result;
247
- if (execution.purgedAt) return purged(result);
248
- const outcome = execution.executionResult;
249
- return [
250
- `Execution: ${execution.id}`,
251
- `Status: ${execution.status}`,
252
- `Workflow: ${execution.workflowName ?? execution.workflowId}`,
253
- execution.workflowVersionId
254
- ? `Workflow version: ${execution.workflowVersionId}`
255
- : null,
256
- execution.inputs ? `Inputs: ${JSON.stringify(execution.inputs)}` : null,
257
- outcome ? `Result: ${outcome.outcome}: ${outcome.reasoning}` : null,
258
- `Created: ${execution.createdAt}`,
259
- execution.terminalAt ? `Completed: ${execution.terminalAt}` : null,
260
- execution.duration != null
261
- ? `Duration: ${execution.duration.toFixed(1)}s`
262
- : null,
263
- failures(execution),
264
- filesLine(result),
265
- result.skipped.length > 0
266
- ? `Skipped:\n${result.skipped.map((s) => ` - ${s}`).join("\n")}`
267
- : null,
268
- result.failed.length > 0 ? `Failed: ${result.failed.join(", ")}` : null,
269
- `Written to: ${result.dir}/`,
270
- ]
271
- .filter(Boolean)
272
- .join("\n");
273
- }
@@ -1,47 +0,0 @@
1
- import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
- import { dirname, relative, resolve, sep } from "node:path";
3
-
4
- import type { LocalFiles } from "./types";
5
-
6
- function missing(error: unknown): boolean {
7
- return (error as NodeJS.ErrnoException).code === "ENOENT";
8
- }
9
-
10
- async function walk(root: string, dir: string, out: string[]): Promise<void> {
11
- for (const entry of await readdir(dir, { withFileTypes: true })) {
12
- const path = resolve(dir, entry.name);
13
- if (entry.isDirectory()) await walk(root, path, out);
14
- else if (entry.isFile())
15
- out.push(relative(root, path).split(sep).join("/"));
16
- }
17
- }
18
-
19
- export function nodeFiles(cwd: string): LocalFiles {
20
- const at = (path: string) => resolve(cwd, path);
21
- return {
22
- async read(path) {
23
- try {
24
- return new Uint8Array(await readFile(at(path)));
25
- } catch (error) {
26
- if (missing(error)) return undefined;
27
- throw error;
28
- }
29
- },
30
- async write(path, bytes) {
31
- await mkdir(dirname(at(path)), { recursive: true });
32
- await writeFile(at(path), bytes);
33
- },
34
- async remove(path) {
35
- await rm(at(path), { force: true });
36
- },
37
- async list(dir) {
38
- const out: string[] = [];
39
- try {
40
- await walk(at(dir), at(dir), out);
41
- } catch (error) {
42
- if (!missing(error)) throw error;
43
- }
44
- return out.sort();
45
- },
46
- };
47
- }