@aexhq/sdk 0.40.6 → 0.40.8

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 (53) hide show
  1. package/README.md +159 -159
  2. package/dist/_contracts/asset-upload-helper.d.ts +38 -0
  3. package/dist/_contracts/asset-upload-helper.js +111 -0
  4. package/dist/_contracts/internal.d.ts +5 -9
  5. package/dist/_contracts/internal.js +5 -79
  6. package/dist/_contracts/operations.d.ts +9 -2
  7. package/dist/_contracts/operations.js +141 -16
  8. package/dist/_contracts/retry-core.d.ts +27 -0
  9. package/dist/_contracts/retry-core.js +75 -0
  10. package/dist/_contracts/runtime-types.d.ts +6 -0
  11. package/dist/_contracts/stable.d.ts +10 -5
  12. package/dist/_contracts/stable.js +10 -5
  13. package/dist/asset-upload.d.ts +2 -1
  14. package/dist/asset-upload.js +28 -61
  15. package/dist/asset-upload.js.map +1 -1
  16. package/dist/cli.mjs +297 -80
  17. package/dist/cli.mjs.sha256 +1 -1
  18. package/dist/client.d.ts +5 -0
  19. package/dist/client.js +8 -13
  20. package/dist/client.js.map +1 -1
  21. package/dist/retry.js +13 -41
  22. package/dist/retry.js.map +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/dist/version.js +1 -1
  25. package/docs/authentication.md +125 -125
  26. package/docs/billing.md +118 -118
  27. package/docs/concepts/agent-tools.md +66 -66
  28. package/docs/concepts/composition.md +37 -37
  29. package/docs/concepts/providers-and-runtimes.md +54 -54
  30. package/docs/concepts/runs.md +49 -49
  31. package/docs/concepts/subagents.md +88 -88
  32. package/docs/credentials.md +125 -125
  33. package/docs/defaults.md +64 -64
  34. package/docs/errors.md +196 -196
  35. package/docs/events.md +243 -243
  36. package/docs/limits-and-quotas.md +144 -144
  37. package/docs/limits.md +50 -50
  38. package/docs/mcp.md +44 -44
  39. package/docs/networking.md +163 -163
  40. package/docs/outputs.md +267 -267
  41. package/docs/public-surface.json +77 -77
  42. package/docs/quickstart.md +119 -119
  43. package/docs/release.md +38 -38
  44. package/docs/retries.md +129 -129
  45. package/docs/run-config.md +58 -58
  46. package/docs/run-record.md +55 -55
  47. package/docs/secrets.md +125 -125
  48. package/docs/testing.md +35 -35
  49. package/docs/vision-skills.md +91 -91
  50. package/docs/webhooks.md +127 -127
  51. package/examples/feature-tour.ts +282 -282
  52. package/examples/spike-settle-latency.ts +125 -125
  53. package/package.json +1 -1
@@ -1,282 +1,282 @@
1
- /**
2
- * SDK feature tour: one managed session that uses typed model/runtime constants,
3
- * inline AGENTS.md guidance, uploaded files, a custom tool bundle, selected
4
- * built-in tools, runtime env vars/secrets, streamed events, output reads, and
5
- * a follow-up session turn.
6
- *
7
- * Run from the repository root after building the workspace package:
8
- *
9
- * AEX_API_KEY=... DEEPSEEK_API_KEY=... bun packages/sdk/examples/feature-tour.ts
10
- *
11
- * Optional:
12
- *
13
- * AEX_API_URL=https://api.aex.dev
14
- * AEX_FEATURE_TOUR_DOWNLOAD=./feature-tour-session.zip
15
- * AEX_DEMO_RUNTIME_SECRET=... # mounted as a runtime secret; never printed
16
- * AEX_DEMO_MCP_URL=https://... # declares an optional remote MCP server
17
- * AEX_DEMO_MCP_TOKEN=... # optional bearer auth for that MCP server
18
- */
19
- import {
20
- AgentsMd,
21
- Aex,
22
- BuiltinTools,
23
- File,
24
- isRateLimited,
25
- McpServer,
26
- Models,
27
- Providers,
28
- Secret,
29
- Sizes,
30
- Tool
31
- } from "@aexhq/sdk";
32
-
33
- process.on("uncaughtException", handleFatal);
34
- process.on("unhandledRejection", handleFatal);
35
-
36
- const apiKey = required("AEX_API_KEY");
37
- const deepseekKey = required("DEEPSEEK_API_KEY");
38
- const apiUrl = process.env.AEX_API_URL;
39
- const demoMcpUrl = process.env.AEX_DEMO_MCP_URL;
40
- const demoMcpToken = process.env.AEX_DEMO_MCP_TOKEN;
41
- const demoRuntimeSecret = process.env.AEX_DEMO_RUNTIME_SECRET;
42
- const downloadPath = process.env.AEX_FEATURE_TOUR_DOWNLOAD;
43
- const textEncoder = new TextEncoder();
44
-
45
- const aex = new Aex({
46
- apiKey,
47
- ...(apiUrl ? { baseUrl: apiUrl } : {}),
48
- retry: {
49
- maxAttempts: 4,
50
- initialDelayMs: 500,
51
- maxDelayMs: 10_000,
52
- maxElapsedMs: 90_000
53
- }
54
- });
55
-
56
- const metricLookup = await Tool.fromFiles({
57
- name: "metric_lookup",
58
- description: "Looks up normalized demo metrics for one product line.",
59
- inputSchema: {
60
- type: "object",
61
- additionalProperties: false,
62
- properties: {
63
- product: {
64
- type: "string",
65
- enum: ["atlas", "beacon", "cinder"],
66
- description: "Product line to inspect."
67
- }
68
- },
69
- required: ["product"]
70
- },
71
- entry: "index.js",
72
- files: {
73
- "index.js": `
74
- const DATA = {
75
- atlas: { customerCount: 118, activationHealthPct: 72.4, supportTickets: 11 },
76
- beacon: { customerCount: 74, activationHealthPct: 65.1, supportTickets: 19 },
77
- cinder: { customerCount: 43, activationHealthPct: 58.8, supportTickets: 7 }
78
- };
79
-
80
- export default async function ({ input }) {
81
- const key = String(input.product ?? "").toLowerCase();
82
- const row = DATA[key];
83
- if (!row) {
84
- return { content: [{ type: "text", text: \`unknown product: \${key}\` }], is_error: true };
85
- }
86
- return { content: [{ type: "text", text: JSON.stringify({ product: key, ...row }) }] };
87
- }
88
- `
89
- }
90
- });
91
-
92
- const demoCsv = [
93
- "product,region,q1_revenue_usd,q2_revenue_usd,activation_rate",
94
- "atlas,na,120000,139500,0.84",
95
- "beacon,emea,98000,104300,0.79",
96
- "cinder,apac,67000,81000,0.91"
97
- ].join("\n");
98
-
99
- const attachedFile = await File.fromBytes({
100
- name: "quarterly-metrics.csv",
101
- bytes: textEncoder.encode(`${demoCsv}\n`),
102
- mountPath: "/workspace/input"
103
- });
104
-
105
- const runRules = await AgentsMd.fromContent(
106
- [
107
- "# Feature tour rules",
108
- "- Use `/workspace/input/quarterly-metrics.csv` as the source table.",
109
- "- Call `metric_lookup` for atlas, beacon, and cinder before writing conclusions.",
110
- "- Write final artifacts under `/workspace/outputs`.",
111
- "- Never print runtime secret values or provider keys."
112
- ].join("\n"),
113
- { name: "feature-tour-rules" }
114
- );
115
-
116
- const mcpServers = demoMcpUrl
117
- ? [
118
- McpServer.remote({
119
- name: "demo-mcp",
120
- url: demoMcpUrl,
121
- ...(demoMcpToken
122
- ? { headers: { Authorization: `Bearer ${demoMcpToken}` } }
123
- : {})
124
- })
125
- ]
126
- : [];
127
-
128
- const environmentSecrets = demoRuntimeSecret
129
- ? { DEMO_RUNTIME_SECRET: Secret.value(demoRuntimeSecret) }
130
- : undefined;
131
-
132
- console.log("creating feature-tour session...");
133
- console.log(`optional mcp: ${mcpServers.length > 0 ? "enabled" : "disabled"}`);
134
- console.log(`optional runtime secret: ${environmentSecrets ? "enabled" : "disabled"}`);
135
-
136
- const session = await aex.openSession({
137
- provider: Providers.DEEPSEEK,
138
- model: Models.DEEPSEEK_V4_FLASH,
139
- system: [
140
- "You are a concise analytics agent.",
141
- "Prefer exact calculations and write durable files for the caller."
142
- ].join(" "),
143
- agentsMd: [runRules],
144
- files: [attachedFile],
145
- includeBuiltinTools: false,
146
- tools: [
147
- BuiltinTools.read_file,
148
- BuiltinTools.write_file,
149
- BuiltinTools.bash,
150
- BuiltinTools.grep,
151
- BuiltinTools.code_execution,
152
- metricLookup
153
- ],
154
- mcpServers,
155
- environment: {
156
- networking: { mode: "open" },
157
- variables: {
158
- FEATURE_TOUR: "true",
159
- REPORT_DIR: "/workspace/outputs"
160
- },
161
- ...(environmentSecrets ? { secrets: environmentSecrets } : {})
162
- },
163
- outputs: {
164
- allowedDirs: ["/workspace/outputs"],
165
- deniedDirs: ["*.tmp"],
166
- maxFiles: 10,
167
- maxFileBytes: 1_000_000
168
- },
169
- outputMode: "stream",
170
- runtime: Sizes.SHARED_0_25X_1GB,
171
- metadata: {
172
- example: "sdk-feature-tour",
173
- sdkSurface: "public"
174
- },
175
- overrides: {
176
- idleTtl: "5m",
177
- timeout: "10m",
178
- maxSpendUsd: 2
179
- },
180
- apiKeys: { deepseek: deepseekKey }
181
- });
182
-
183
- console.log(`session: ${session.id}`);
184
-
185
- const prompt = [
186
- "Analyze the attached quarterly metrics.",
187
- "Call metric_lookup for atlas, beacon, and cinder.",
188
- "Create /workspace/outputs/feature-tour-report.md with a short table, a ranking by q2_revenue_usd, and two risks.",
189
- "Create /workspace/outputs/summary.json with keys topProduct, totalQ2RevenueUsd, highestActivationProduct, and riskCount."
190
- ].join(" ");
191
-
192
- const firstTurn = session.send(prompt);
193
- const firstTurnIterator = firstTurn[Symbol.asyncIterator]();
194
- let result: Awaited<ReturnType<typeof firstTurn.done>> | undefined;
195
- for (;;) {
196
- const next = await firstTurnIterator.next();
197
- if (next.done) {
198
- result = next.value as Awaited<ReturnType<typeof firstTurn.done>>;
199
- break;
200
- }
201
- const event = next.value;
202
- if (event.isTextMessage()) {
203
- process.stdout.write(event.data.text);
204
- } else if (event.isToolCallStart()) {
205
- process.stdout.write(`\n[tool:start] ${event.data.name}\n`);
206
- } else if (event.isToolCallResult()) {
207
- process.stdout.write("[tool:result]\n");
208
- }
209
- }
210
-
211
- if (!result) {
212
- throw new Error("first turn stream ended without a result");
213
- }
214
- console.log(`\nfirst turn parked with status: ${result.status}`);
215
-
216
- const followUp = await session
217
- .send("Read summary.json back and answer with one sentence confirming the top product and total Q2 revenue.")
218
- .done();
219
- console.log(`follow-up status: ${followUp.status}`);
220
- if (followUp.text) {
221
- console.log(`follow-up text: ${followUp.text.trim()}`);
222
- }
223
-
224
- const parked = await session.wait({ timeoutMs: 60_000, intervalMs: 2_000 });
225
- console.log(`settled session status: ${parked.status}`);
226
-
227
- const messages = await session.messages().list();
228
- console.log(`assistant messages: ${messages.length}`);
229
-
230
- const events = await session.events().list();
231
- console.log(`captured events: ${events.length}`);
232
-
233
- const outputs = await session.outputs().list();
234
- console.log("outputs:");
235
- for (const output of outputs) {
236
- console.log(`- ${output.filename ?? output.id} (${output.contentType ?? "unknown"})`);
237
- }
238
-
239
- const summary = await session.outputs().read(
240
- { path: "summary.json", match: "suffix" },
241
- { maxBytes: 20_000 }
242
- );
243
- console.log("summary.json:");
244
- console.log(summary.text);
245
-
246
- const report = await session.outputs().findOne({
247
- filename: "feature-tour-report.md"
248
- });
249
- if (report) {
250
- const reportPreview = await session.outputs().read(report, {
251
- maxBytes: 4_000,
252
- grep: "risk"
253
- });
254
- console.log("report risk lines:");
255
- console.log(reportPreview.text || "(no risk lines found)");
256
- }
257
-
258
- const reopenedOutputs = await aex.sessions.outputs(session.id).list();
259
- console.log(`outputs via aex.sessions.outputs(...): ${reopenedOutputs.length}`);
260
-
261
- if (downloadPath) {
262
- const bytes = await session.download({ to: downloadPath });
263
- console.log(`downloaded session archive: ${downloadPath} (${bytes.byteLength} bytes)`);
264
- }
265
-
266
- function required(name: string): string {
267
- const value = process.env[name];
268
- if (!value) {
269
- console.error(`Missing env var ${name}`);
270
- process.exit(1);
271
- }
272
- return value;
273
- }
274
-
275
- function handleFatal(err: unknown): void {
276
- if (isRateLimited(err)) {
277
- console.error(`rate limited after ${err.attempts} attempts; retry after ${err.retryAfterMs ?? "unknown"}ms`);
278
- process.exit(1);
279
- }
280
- console.error(err instanceof Error ? err.stack ?? err.message : String(err));
281
- process.exit(1);
282
- }
1
+ /**
2
+ * SDK feature tour: one managed session that uses typed model/runtime constants,
3
+ * inline AGENTS.md guidance, uploaded files, a custom tool bundle, selected
4
+ * built-in tools, runtime env vars/secrets, streamed events, output reads, and
5
+ * a follow-up session turn.
6
+ *
7
+ * Run from the repository root after building the workspace package:
8
+ *
9
+ * AEX_API_KEY=... DEEPSEEK_API_KEY=... bun packages/sdk/examples/feature-tour.ts
10
+ *
11
+ * Optional:
12
+ *
13
+ * AEX_API_URL=https://api.aex.dev
14
+ * AEX_FEATURE_TOUR_DOWNLOAD=./feature-tour-session.zip
15
+ * AEX_DEMO_RUNTIME_SECRET=... # mounted as a runtime secret; never printed
16
+ * AEX_DEMO_MCP_URL=https://... # declares an optional remote MCP server
17
+ * AEX_DEMO_MCP_TOKEN=... # optional bearer auth for that MCP server
18
+ */
19
+ import {
20
+ AgentsMd,
21
+ Aex,
22
+ BuiltinTools,
23
+ File,
24
+ isRateLimited,
25
+ McpServer,
26
+ Models,
27
+ Providers,
28
+ Secret,
29
+ Sizes,
30
+ Tool
31
+ } from "@aexhq/sdk";
32
+
33
+ process.on("uncaughtException", handleFatal);
34
+ process.on("unhandledRejection", handleFatal);
35
+
36
+ const apiKey = required("AEX_API_KEY");
37
+ const deepseekKey = required("DEEPSEEK_API_KEY");
38
+ const apiUrl = process.env.AEX_API_URL;
39
+ const demoMcpUrl = process.env.AEX_DEMO_MCP_URL;
40
+ const demoMcpToken = process.env.AEX_DEMO_MCP_TOKEN;
41
+ const demoRuntimeSecret = process.env.AEX_DEMO_RUNTIME_SECRET;
42
+ const downloadPath = process.env.AEX_FEATURE_TOUR_DOWNLOAD;
43
+ const textEncoder = new TextEncoder();
44
+
45
+ const aex = new Aex({
46
+ apiKey,
47
+ ...(apiUrl ? { baseUrl: apiUrl } : {}),
48
+ retry: {
49
+ maxAttempts: 4,
50
+ initialDelayMs: 500,
51
+ maxDelayMs: 10_000,
52
+ maxElapsedMs: 90_000
53
+ }
54
+ });
55
+
56
+ const metricLookup = await Tool.fromFiles({
57
+ name: "metric_lookup",
58
+ description: "Looks up normalized demo metrics for one product line.",
59
+ inputSchema: {
60
+ type: "object",
61
+ additionalProperties: false,
62
+ properties: {
63
+ product: {
64
+ type: "string",
65
+ enum: ["atlas", "beacon", "cinder"],
66
+ description: "Product line to inspect."
67
+ }
68
+ },
69
+ required: ["product"]
70
+ },
71
+ entry: "index.js",
72
+ files: {
73
+ "index.js": `
74
+ const DATA = {
75
+ atlas: { customerCount: 118, activationHealthPct: 72.4, supportTickets: 11 },
76
+ beacon: { customerCount: 74, activationHealthPct: 65.1, supportTickets: 19 },
77
+ cinder: { customerCount: 43, activationHealthPct: 58.8, supportTickets: 7 }
78
+ };
79
+
80
+ export default async function ({ input }) {
81
+ const key = String(input.product ?? "").toLowerCase();
82
+ const row = DATA[key];
83
+ if (!row) {
84
+ return { content: [{ type: "text", text: \`unknown product: \${key}\` }], is_error: true };
85
+ }
86
+ return { content: [{ type: "text", text: JSON.stringify({ product: key, ...row }) }] };
87
+ }
88
+ `
89
+ }
90
+ });
91
+
92
+ const demoCsv = [
93
+ "product,region,q1_revenue_usd,q2_revenue_usd,activation_rate",
94
+ "atlas,na,120000,139500,0.84",
95
+ "beacon,emea,98000,104300,0.79",
96
+ "cinder,apac,67000,81000,0.91"
97
+ ].join("\n");
98
+
99
+ const attachedFile = await File.fromBytes({
100
+ name: "quarterly-metrics.csv",
101
+ bytes: textEncoder.encode(`${demoCsv}\n`),
102
+ mountPath: "/workspace/input"
103
+ });
104
+
105
+ const runRules = await AgentsMd.fromContent(
106
+ [
107
+ "# Feature tour rules",
108
+ "- Use `/workspace/input/quarterly-metrics.csv` as the source table.",
109
+ "- Call `metric_lookup` for atlas, beacon, and cinder before writing conclusions.",
110
+ "- Write final artifacts under `/workspace/outputs`.",
111
+ "- Never print runtime secret values or provider keys."
112
+ ].join("\n"),
113
+ { name: "feature-tour-rules" }
114
+ );
115
+
116
+ const mcpServers = demoMcpUrl
117
+ ? [
118
+ McpServer.remote({
119
+ name: "demo-mcp",
120
+ url: demoMcpUrl,
121
+ ...(demoMcpToken
122
+ ? { headers: { Authorization: `Bearer ${demoMcpToken}` } }
123
+ : {})
124
+ })
125
+ ]
126
+ : [];
127
+
128
+ const environmentSecrets = demoRuntimeSecret
129
+ ? { DEMO_RUNTIME_SECRET: Secret.value(demoRuntimeSecret) }
130
+ : undefined;
131
+
132
+ console.log("creating feature-tour session...");
133
+ console.log(`optional mcp: ${mcpServers.length > 0 ? "enabled" : "disabled"}`);
134
+ console.log(`optional runtime secret: ${environmentSecrets ? "enabled" : "disabled"}`);
135
+
136
+ const session = await aex.openSession({
137
+ provider: Providers.DEEPSEEK,
138
+ model: Models.DEEPSEEK_V4_FLASH,
139
+ system: [
140
+ "You are a concise analytics agent.",
141
+ "Prefer exact calculations and write durable files for the caller."
142
+ ].join(" "),
143
+ agentsMd: [runRules],
144
+ files: [attachedFile],
145
+ includeBuiltinTools: false,
146
+ tools: [
147
+ BuiltinTools.read_file,
148
+ BuiltinTools.write_file,
149
+ BuiltinTools.bash,
150
+ BuiltinTools.grep,
151
+ BuiltinTools.code_execution,
152
+ metricLookup
153
+ ],
154
+ mcpServers,
155
+ environment: {
156
+ networking: { mode: "open" },
157
+ variables: {
158
+ FEATURE_TOUR: "true",
159
+ REPORT_DIR: "/workspace/outputs"
160
+ },
161
+ ...(environmentSecrets ? { secrets: environmentSecrets } : {})
162
+ },
163
+ outputs: {
164
+ allowedDirs: ["/workspace/outputs"],
165
+ deniedDirs: ["*.tmp"],
166
+ maxFiles: 10,
167
+ maxFileBytes: 1_000_000
168
+ },
169
+ outputMode: "stream",
170
+ runtime: Sizes.SHARED_0_25X_1GB,
171
+ metadata: {
172
+ example: "sdk-feature-tour",
173
+ sdkSurface: "public"
174
+ },
175
+ overrides: {
176
+ idleTtl: "5m",
177
+ timeout: "10m",
178
+ maxSpendUsd: 2
179
+ },
180
+ apiKeys: { deepseek: deepseekKey }
181
+ });
182
+
183
+ console.log(`session: ${session.id}`);
184
+
185
+ const prompt = [
186
+ "Analyze the attached quarterly metrics.",
187
+ "Call metric_lookup for atlas, beacon, and cinder.",
188
+ "Create /workspace/outputs/feature-tour-report.md with a short table, a ranking by q2_revenue_usd, and two risks.",
189
+ "Create /workspace/outputs/summary.json with keys topProduct, totalQ2RevenueUsd, highestActivationProduct, and riskCount."
190
+ ].join(" ");
191
+
192
+ const firstTurn = session.send(prompt);
193
+ const firstTurnIterator = firstTurn[Symbol.asyncIterator]();
194
+ let result: Awaited<ReturnType<typeof firstTurn.done>> | undefined;
195
+ for (;;) {
196
+ const next = await firstTurnIterator.next();
197
+ if (next.done) {
198
+ result = next.value as Awaited<ReturnType<typeof firstTurn.done>>;
199
+ break;
200
+ }
201
+ const event = next.value;
202
+ if (event.isTextMessage()) {
203
+ process.stdout.write(event.data.text);
204
+ } else if (event.isToolCallStart()) {
205
+ process.stdout.write(`\n[tool:start] ${event.data.name}\n`);
206
+ } else if (event.isToolCallResult()) {
207
+ process.stdout.write("[tool:result]\n");
208
+ }
209
+ }
210
+
211
+ if (!result) {
212
+ throw new Error("first turn stream ended without a result");
213
+ }
214
+ console.log(`\nfirst turn parked with status: ${result.status}`);
215
+
216
+ const followUp = await session
217
+ .send("Read summary.json back and answer with one sentence confirming the top product and total Q2 revenue.")
218
+ .done();
219
+ console.log(`follow-up status: ${followUp.status}`);
220
+ if (followUp.text) {
221
+ console.log(`follow-up text: ${followUp.text.trim()}`);
222
+ }
223
+
224
+ const parked = await session.wait({ timeoutMs: 60_000, intervalMs: 2_000 });
225
+ console.log(`settled session status: ${parked.status}`);
226
+
227
+ const messages = await session.messages().list();
228
+ console.log(`assistant messages: ${messages.length}`);
229
+
230
+ const events = await session.events().list();
231
+ console.log(`captured events: ${events.length}`);
232
+
233
+ const outputs = await session.outputs().list();
234
+ console.log("outputs:");
235
+ for (const output of outputs) {
236
+ console.log(`- ${output.filename ?? output.id} (${output.contentType ?? "unknown"})`);
237
+ }
238
+
239
+ const summary = await session.outputs().read(
240
+ { path: "summary.json", match: "suffix" },
241
+ { maxBytes: 20_000 }
242
+ );
243
+ console.log("summary.json:");
244
+ console.log(summary.text);
245
+
246
+ const report = await session.outputs().findOne({
247
+ filename: "feature-tour-report.md"
248
+ });
249
+ if (report) {
250
+ const reportPreview = await session.outputs().read(report, {
251
+ maxBytes: 4_000,
252
+ grep: "risk"
253
+ });
254
+ console.log("report risk lines:");
255
+ console.log(reportPreview.text || "(no risk lines found)");
256
+ }
257
+
258
+ const reopenedOutputs = await aex.sessions.outputs(session.id).list();
259
+ console.log(`outputs via aex.sessions.outputs(...): ${reopenedOutputs.length}`);
260
+
261
+ if (downloadPath) {
262
+ const bytes = await session.download({ to: downloadPath });
263
+ console.log(`downloaded session archive: ${downloadPath} (${bytes.byteLength} bytes)`);
264
+ }
265
+
266
+ function required(name: string): string {
267
+ const value = process.env[name];
268
+ if (!value) {
269
+ console.error(`Missing env var ${name}`);
270
+ process.exit(1);
271
+ }
272
+ return value;
273
+ }
274
+
275
+ function handleFatal(err: unknown): void {
276
+ if (isRateLimited(err)) {
277
+ console.error(`rate limited after ${err.attempts} attempts; retry after ${err.retryAfterMs ?? "unknown"}ms`);
278
+ process.exit(1);
279
+ }
280
+ console.error(err instanceof Error ? err.stack ?? err.message : String(err));
281
+ process.exit(1);
282
+ }