@helmr/sdk 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1023 @@
1
+ // sdk/typescript/src/schema/task.ts
2
+ var TASK_ID_PATTERN = "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$";
3
+ var TASK_ID_MAX_LENGTH = 128;
4
+ var DEFAULT_MAX_DURATION_SECONDS = 900;
5
+ var MIN_MAX_DURATION_SECONDS = 5;
6
+ var MAX_DURATION_SECONDS = 86400;
7
+
8
+ class TaskIdError extends Error {
9
+ name = "TaskIdError";
10
+ value;
11
+ constructor(value) {
12
+ super(`task id must match ${TASK_ID_PATTERN}: ${JSON.stringify(value)}`);
13
+ this.value = value;
14
+ }
15
+ }
16
+ function validateTaskId(value) {
17
+ if (!isValidTaskId(value)) {
18
+ throw new TaskIdError(value);
19
+ }
20
+ }
21
+ function isValidTaskId(value) {
22
+ if (value.length === 0 || value.length > TASK_ID_MAX_LENGTH) {
23
+ return false;
24
+ }
25
+ const first = value.charCodeAt(0);
26
+ if (!isAsciiAlnum(first)) {
27
+ return false;
28
+ }
29
+ for (let index = 1;index < value.length; index += 1) {
30
+ const code = value.charCodeAt(index);
31
+ if (!(isAsciiAlnum(code) || code === 46 || code === 95 || code === 45)) {
32
+ return false;
33
+ }
34
+ }
35
+ return true;
36
+ }
37
+
38
+ class TaskMaxDurationError extends Error {
39
+ name = "TaskMaxDurationError";
40
+ value;
41
+ label;
42
+ constructor(value, label = "task maxDuration") {
43
+ super(`${label} must be an integer number of seconds between ${MIN_MAX_DURATION_SECONDS} and ${MAX_DURATION_SECONDS}`);
44
+ this.value = value;
45
+ this.label = label;
46
+ }
47
+ }
48
+ function readOptionalMaxDurationSeconds(value, label = "task maxDuration") {
49
+ if (value === undefined) {
50
+ return DEFAULT_MAX_DURATION_SECONDS;
51
+ }
52
+ if (typeof value === "number" && Number.isInteger(value) && Number.isFinite(value) && value >= MIN_MAX_DURATION_SECONDS && value <= MAX_DURATION_SECONDS) {
53
+ return value;
54
+ }
55
+ throw new TaskMaxDurationError(value, label);
56
+ }
57
+ function validateOptionalMaxDurationSeconds(value, label = "task maxDuration") {
58
+ readOptionalMaxDurationSeconds(value, label);
59
+ }
60
+ function isAsciiAlnum(code) {
61
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122;
62
+ }
63
+
64
+ // sdk/typescript/src/internal.ts
65
+ var approvalTimeoutErrorBrand = Symbol.for("helmr.sdk.ApprovalTimeoutError");
66
+ var messageTimeoutErrorBrand = Symbol.for("helmr.sdk.MessageTimeoutError");
67
+ var concurrentWaitErrorBrand = Symbol.for("helmr.sdk.ConcurrentWaitError");
68
+
69
+ class ApprovalTimeoutError extends Error {
70
+ constructor(message) {
71
+ super(message);
72
+ this.name = "ApprovalTimeoutError";
73
+ Object.defineProperty(this, approvalTimeoutErrorBrand, { value: true });
74
+ }
75
+ static [Symbol.hasInstance](value) {
76
+ return this === ApprovalTimeoutError && typeof value === "object" && value !== null && approvalTimeoutErrorBrand in value;
77
+ }
78
+ }
79
+
80
+ class MessageTimeoutError extends Error {
81
+ constructor(message) {
82
+ super(message);
83
+ this.name = "MessageTimeoutError";
84
+ Object.defineProperty(this, messageTimeoutErrorBrand, { value: true });
85
+ }
86
+ static [Symbol.hasInstance](value) {
87
+ return this === MessageTimeoutError && typeof value === "object" && value !== null && messageTimeoutErrorBrand in value;
88
+ }
89
+ }
90
+
91
+ class ConcurrentWaitError extends Error {
92
+ constructor(message) {
93
+ super(message);
94
+ this.name = "ConcurrentWaitError";
95
+ Object.defineProperty(this, concurrentWaitErrorBrand, { value: true });
96
+ }
97
+ static [Symbol.hasInstance](value) {
98
+ return this === ConcurrentWaitError && typeof value === "object" && value !== null && concurrentWaitErrorBrand in value;
99
+ }
100
+ }
101
+ function validateSecretName(name, label = "secret name") {
102
+ if (name.length === 0) {
103
+ throw new Error(`${label} must not be empty`);
104
+ }
105
+ if (name.length > 128) {
106
+ throw new Error(`${label} must be at most 128 characters`);
107
+ }
108
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
109
+ throw new Error(`${label} must match /^[A-Za-z_][A-Za-z0-9_]*$/`);
110
+ }
111
+ const upper = name.toUpperCase();
112
+ if (upper === "CON" || upper === "PRN" || upper === "AUX" || upper === "NUL" || /^COM[1-9]$/.test(upper) || /^LPT[1-9]$/.test(upper)) {
113
+ throw new Error(`${label} is reserved`);
114
+ }
115
+ }
116
+ function validateImageBuildSecretRef(name) {
117
+ validateSecretName(name, "image build secret ref");
118
+ return name;
119
+ }
120
+ var RESERVED_WORKSPACE_MOUNT_PATHS = [
121
+ "/dev",
122
+ "/opt/helmr",
123
+ "/proc",
124
+ "/run",
125
+ "/sys",
126
+ "/tmp",
127
+ "/.helmr-old-root"
128
+ ];
129
+ function normalizeWorkspaceMountPath(raw) {
130
+ if (raw.length === 0) {
131
+ throw new Error("sandbox.workspace() mountPath is empty");
132
+ }
133
+ if (raw.includes("\x00")) {
134
+ throw new Error("sandbox.workspace() mountPath contains NUL");
135
+ }
136
+ if (!raw.startsWith("/")) {
137
+ throw new Error(`sandbox.workspace() mountPath must be absolute: ${raw}`);
138
+ }
139
+ const parts = [];
140
+ for (const part of raw.split("/")) {
141
+ if (part === "" || part === ".")
142
+ continue;
143
+ if (part === "..") {
144
+ throw new Error(`sandbox.workspace() mountPath contains unsafe path components: ${raw}`);
145
+ }
146
+ parts.push(part);
147
+ }
148
+ if (parts.length === 0) {
149
+ throw new Error("sandbox.workspace() mountPath cannot be /");
150
+ }
151
+ const normalized = `/${parts.join("/")}`;
152
+ if (RESERVED_WORKSPACE_MOUNT_PATHS.some((reserved) => normalized === reserved || normalized.startsWith(`${reserved}/`))) {
153
+ throw new Error(`sandbox.workspace() mountPath conflicts with reserved runtime mount paths: ${normalized}`);
154
+ }
155
+ return normalized;
156
+ }
157
+ var taskBrand = Symbol.for("helmr.sdk.Task");
158
+ var taskOriginBrand = Symbol.for("helmr.sdk.TaskOrigin");
159
+ var configBrand = Symbol.for("helmr.sdk.Config");
160
+ var imageBuilderBrand = Symbol.for("helmr.sdk.ImageBuilder");
161
+ var sandboxBuilderBrand = Symbol.for("helmr.sdk.SandboxBuilder");
162
+ var sourceFileRefBrand = Symbol.for("helmr.sdk.SourceFileRef");
163
+ var sourceDirRefBrand = Symbol.for("helmr.sdk.SourceDirRef");
164
+ function markTask(config) {
165
+ validateTaskId(config.id);
166
+ validateOptionalMaxDurationSeconds(config.maxDuration);
167
+ Object.defineProperty(config, taskBrand, { value: true });
168
+ Object.defineProperty(config, taskOriginBrand, { value: captureTaskOrigin() });
169
+ return config;
170
+ }
171
+ function markConfig(config) {
172
+ Object.defineProperty(config, configBrand, { value: true });
173
+ return config;
174
+ }
175
+ function captureTaskOrigin() {
176
+ const stack = new Error().stack ?? "";
177
+ for (const line of stack.split(`
178
+ `).slice(1)) {
179
+ const file = stackFrameFile(line);
180
+ if (file === null || isSdkInternalFrame(file)) {
181
+ continue;
182
+ }
183
+ return file;
184
+ }
185
+ return "unknown";
186
+ }
187
+ function stackFrameFile(line) {
188
+ const match = /\(?((?:file:\/\/)?\/[^():]+):\d+:\d+\)?$/.exec(line.trim());
189
+ if (!match?.[1]) {
190
+ return null;
191
+ }
192
+ return match[1].startsWith("file://") ? decodeURIComponent(new URL(match[1]).pathname) : match[1];
193
+ }
194
+ function isSdkInternalFrame(file) {
195
+ return file.includes("/sdk/typescript/src/internal.ts") || file.includes("/sdk/typescript/src/task.ts") || file.includes("/sdk/typescript/src/index.ts") || file.includes("/runtime/typescript/src/");
196
+ }
197
+
198
+ class ImageBuilderImpl {
199
+ id;
200
+ steps;
201
+ constructor(id, steps = []) {
202
+ Object.defineProperty(this, imageBuilderBrand, { value: true });
203
+ this.id = id;
204
+ this.steps = steps;
205
+ }
206
+ from(ref) {
207
+ return new ImageBuilderImpl(this.id, [...this.steps, { kind: "from", ref }]);
208
+ }
209
+ run(argv, opts = {}) {
210
+ return new ImageBuilderImpl(this.id, [
211
+ ...this.steps,
212
+ {
213
+ kind: "run",
214
+ argv: [...argv],
215
+ cache: (opts.cache ?? []).map((binding) => ({
216
+ mountPath: binding.mountPath,
217
+ cache: { id: binding.cache.id }
218
+ })),
219
+ secrets: (opts.secrets ?? []).map((binding) => ({
220
+ mountPath: binding.mountPath,
221
+ secret: validateImageBuildSecretRef(binding.secret)
222
+ }))
223
+ }
224
+ ]);
225
+ }
226
+ copy(dest, src) {
227
+ return new ImageBuilderImpl(this.id, [...this.steps, { kind: "copy", dest, source: src }]);
228
+ }
229
+ copyFrom(dest, src, srcPath) {
230
+ if (!isImageBuilder(src)) {
231
+ throw new Error("image.copyFrom() requires an ImageBuilder created by image()");
232
+ }
233
+ return new ImageBuilderImpl(this.id, [
234
+ ...this.steps,
235
+ { kind: "copyFrom", dest, source: src, srcPath }
236
+ ]);
237
+ }
238
+ workdir(path) {
239
+ return new ImageBuilderImpl(this.id, [...this.steps, { kind: "workdir", path }]);
240
+ }
241
+ env(key, value) {
242
+ return new ImageBuilderImpl(this.id, [...this.steps, { kind: "env", key, value }]);
243
+ }
244
+ user(name) {
245
+ return new ImageBuilderImpl(this.id, [...this.steps, { kind: "user", name }]);
246
+ }
247
+ }
248
+
249
+ class SandboxBuilderImpl {
250
+ id;
251
+ imageBuilder;
252
+ workspaceBinding;
253
+ resourceSpec;
254
+ constructor(id, imageBuilder, workspaceBinding, resourceSpec) {
255
+ Object.defineProperty(this, sandboxBuilderBrand, { value: true });
256
+ this.id = id;
257
+ this.imageBuilder = imageBuilder;
258
+ this.workspaceBinding = workspaceBinding;
259
+ this.resourceSpec = resourceSpec;
260
+ }
261
+ image(img) {
262
+ if (!isImageBuilder(img)) {
263
+ throw new Error("sandbox.image() requires an ImageBuilder created by image()");
264
+ }
265
+ return new SandboxBuilderImpl(this.id, img, this.workspaceBinding, this.resourceSpec);
266
+ }
267
+ workspace(mountPath = "/workspace") {
268
+ return new SandboxBuilderImpl(this.id, this.imageBuilder, { mountPath: normalizeWorkspaceMountPath(mountPath) }, this.resourceSpec);
269
+ }
270
+ resources(opts) {
271
+ const resourceSpec = {
272
+ ...opts.cpu === undefined ? {} : { cpu: opts.cpu },
273
+ ...opts.memory === undefined ? {} : { memory: opts.memory }
274
+ };
275
+ return new SandboxBuilderImpl(this.id, this.imageBuilder, this.workspaceBinding, resourceSpec);
276
+ }
277
+ }
278
+
279
+ class SourceFileRefImpl {
280
+ path;
281
+ constructor(path) {
282
+ Object.defineProperty(this, sourceFileRefBrand, { value: true });
283
+ this.path = path;
284
+ }
285
+ }
286
+
287
+ class SourceDirRefImpl {
288
+ path;
289
+ ignore;
290
+ constructor(path, ignore) {
291
+ Object.defineProperty(this, sourceDirRefBrand, { value: true });
292
+ this.path = path;
293
+ this.ignore = ignore;
294
+ }
295
+ }
296
+ function isImageBuilder(value) {
297
+ return hasBrand(value, imageBuilderBrand);
298
+ }
299
+ function hasBrand(value, brand) {
300
+ return value !== null && typeof value === "object" && value[brand] === true;
301
+ }
302
+
303
+ // sdk/typescript/src/runtime/errors.ts
304
+ class RunNotFoundError extends Error {
305
+ runId;
306
+ constructor(runId) {
307
+ super(`run ${runId} was not found`);
308
+ this.name = "RunNotFoundError";
309
+ this.runId = runId;
310
+ }
311
+ }
312
+
313
+ class AuthError extends Error {
314
+ constructor(message) {
315
+ super(message);
316
+ this.name = "AuthError";
317
+ }
318
+ }
319
+
320
+ class TimeoutError extends Error {
321
+ constructor(message) {
322
+ super(message);
323
+ this.name = "TimeoutError";
324
+ }
325
+ }
326
+
327
+ class UnsupportedTransportError extends Error {
328
+ constructor(message) {
329
+ super(message);
330
+ this.name = "UnsupportedTransportError";
331
+ }
332
+ }
333
+
334
+ // sdk/typescript/src/runtime/run.ts
335
+ function runHandle(id, taskId) {
336
+ return { id, taskId };
337
+ }
338
+ function runSnapshot(snapshot) {
339
+ const status = runStatus(snapshot.status);
340
+ return {
341
+ id: snapshot.id,
342
+ taskId: snapshot.taskId,
343
+ status,
344
+ exitCode: snapshot.exitCode ?? null,
345
+ createdAt: snapshot.createdAt ?? null,
346
+ updatedAt: snapshot.updatedAt ?? null,
347
+ pendingWaitpoint: snapshot.pendingWaitpoint ?? null,
348
+ ...runStateBooleans(status),
349
+ ...snapshot.output === undefined ? {} : { output: snapshot.output }
350
+ };
351
+ }
352
+ function pendingWaitpointFromResponse(runId, wait) {
353
+ if (wait === undefined || wait === null)
354
+ return null;
355
+ if (wait.kind === "approval") {
356
+ return {
357
+ kind: "approval",
358
+ runId,
359
+ waitpointId: wait.waitpoint_id,
360
+ message: wait.message ?? "",
361
+ timeout: wait.timeout ?? null,
362
+ requestedAt: wait.requested_at
363
+ };
364
+ }
365
+ return {
366
+ kind: "message",
367
+ runId,
368
+ waitpointId: wait.waitpoint_id,
369
+ prompt: wait.prompt ?? null,
370
+ timeout: wait.timeout ?? null,
371
+ requestedAt: wait.requested_at
372
+ };
373
+ }
374
+ function isTerminalRunStatus(status) {
375
+ return status === "succeeded" || status === "failed" || status === "cancelled";
376
+ }
377
+ function runId(value) {
378
+ return typeof value === "string" ? value : value.id;
379
+ }
380
+ function runStateBooleans(status) {
381
+ return {
382
+ isQueued: status === "queued",
383
+ isRunning: status === "claimed" || status === "running",
384
+ isWaiting: status === "waiting",
385
+ isTerminal: isTerminalRunStatus(status),
386
+ isSuccess: status === "succeeded",
387
+ isFailed: status === "failed",
388
+ isCancelled: status === "cancelled"
389
+ };
390
+ }
391
+ function runStatus(status) {
392
+ switch (status) {
393
+ case "queued":
394
+ case "claimed":
395
+ case "running":
396
+ case "waiting":
397
+ case "succeeded":
398
+ case "failed":
399
+ case "cancelled":
400
+ return status;
401
+ default:
402
+ throw new Error(`unsupported run status ${JSON.stringify(status)}`);
403
+ }
404
+ }
405
+
406
+ // sdk/typescript/src/runtime/source.ts
407
+ function runWorkspaceFromSpec(spec) {
408
+ return {
409
+ repository: spec.repository,
410
+ ref: spec.ref,
411
+ ...spec.subpath === undefined ? {} : { subpath: spec.subpath }
412
+ };
413
+ }
414
+
415
+ // sdk/typescript/src/runtime/client.ts
416
+ class HelmrClient {
417
+ #baseUrl;
418
+ #apiKey;
419
+ constructor(options = {}) {
420
+ const rawUrl = options.url ?? process.env["HELMR_URL"];
421
+ if (rawUrl === undefined || rawUrl.trim() === "") {
422
+ throw new UnsupportedTransportError("HelmrClient requires a url option or HELMR_URL; no default transport is used");
423
+ }
424
+ const envApiKey = process.env["HELMR_API_KEY"];
425
+ const apiKey = options.apiKey ?? envApiKey;
426
+ let parsedUrl;
427
+ try {
428
+ parsedUrl = new URL(rawUrl);
429
+ } catch {
430
+ throw new UnsupportedTransportError("HelmrClient requires an http(s) URL");
431
+ }
432
+ if (parsedUrl.protocol === "https:") {
433
+ if (apiKey === undefined || apiKey === "") {
434
+ throw new AuthError("HelmrClient https:// transport requires apiKey or HELMR_API_KEY");
435
+ }
436
+ this.#baseUrl = normalizedBaseUrl(parsedUrl);
437
+ this.#apiKey = apiKey;
438
+ } else if (parsedUrl.protocol === "http:") {
439
+ if (!isLoopbackHost(parsedUrl.hostname)) {
440
+ throw new UnsupportedTransportError(`refusing to send credentials over plaintext non-loopback URL ${parsedUrl.toString()}`);
441
+ }
442
+ console.warn("HelmrClient http:// transport is plaintext and must be explicitly opted into; use https:// for remote services");
443
+ this.#baseUrl = normalizedBaseUrl(parsedUrl);
444
+ this.#apiKey = apiKey;
445
+ } else {
446
+ throw new UnsupportedTransportError(`unsupported HelmrClient transport scheme ${parsedUrl.protocol.replace(/:$/, "")}`);
447
+ }
448
+ }
449
+ tasks = {
450
+ trigger: async (task, opts) => {
451
+ const runWorkspace = runWorkspaceFromSpec(opts.workspace);
452
+ const maxDurationSeconds = readOptionalMaxDurationSeconds(task.maxDuration);
453
+ const response = await this.#fetch("/api/runs", {
454
+ method: "POST",
455
+ body: JSON.stringify({
456
+ task_id: task.id,
457
+ secrets: opts.secrets ?? {},
458
+ payload: opts.payload,
459
+ workspace: runWorkspace,
460
+ max_duration_seconds: maxDurationSeconds
461
+ }),
462
+ headers: { "content-type": "application/json" }
463
+ });
464
+ const run = await response.json();
465
+ return runHandle(run.id, run.task_id);
466
+ }
467
+ };
468
+ runs = {
469
+ retrieve: async (idOrHandle, opts = {}) => {
470
+ const response = await this.#json(`/api/runs/${encodeURIComponent(runId(idOrHandle))}`, requestSignal(opts.signal));
471
+ return runResponseToSnapshot(response);
472
+ },
473
+ wait: async (idOrHandle, opts = {}) => {
474
+ const id = runId(idOrHandle);
475
+ const timeoutMs = opts.timeoutMs;
476
+ const intervalMs = opts.intervalMs ?? 1000;
477
+ const started = Date.now();
478
+ for (;; ) {
479
+ throwIfAborted(opts.signal);
480
+ const run = await this.runs.retrieve(id, retrieveOptions(opts.signal));
481
+ if (isTerminalRunStatus(run.status)) {
482
+ return run;
483
+ }
484
+ if (timeoutMs !== undefined && Date.now() - started > timeoutMs) {
485
+ throw new TimeoutError(`run ${id} did not finish within ${timeoutMs}ms`);
486
+ }
487
+ await delay(intervalMs, opts.signal);
488
+ }
489
+ },
490
+ list: async (opts = {}) => {
491
+ const query = new URLSearchParams;
492
+ if (opts.status !== undefined)
493
+ query.set("status", opts.status);
494
+ if (opts.limit !== undefined)
495
+ query.set("limit", String(opts.limit));
496
+ if (opts.projectId !== undefined)
497
+ query.set("project_id", opts.projectId);
498
+ if (opts.environmentId !== undefined)
499
+ query.set("environment_id", opts.environmentId);
500
+ const suffix = query.size === 0 ? "" : `?${query}`;
501
+ const response = await this.#json(`/api/runs${suffix}`, requestSignal(opts.signal));
502
+ return response.runs.map((run) => runResponseToSnapshot(run));
503
+ },
504
+ logs: {
505
+ retrieve: async (idOrHandle, opts = {}) => {
506
+ return await this.#retrieveLogs(runId(idOrHandle), opts.signal);
507
+ }
508
+ },
509
+ events: {
510
+ list: async (idOrHandle, opts = {}) => {
511
+ return await this.#listEvents(runId(idOrHandle), opts);
512
+ },
513
+ subscribe: async (idOrHandle, opts = {}) => {
514
+ return await this.#subscribeEvents(runId(idOrHandle), opts);
515
+ }
516
+ }
517
+ };
518
+ waitpoints = {
519
+ approve: async (target, waitpointIdOrOpts, opts = {}) => {
520
+ const resolved = resolveWaitpointArgs(target, waitpointIdOrOpts, opts);
521
+ await this.#fetch(`/api/runs/${encodeURIComponent(resolved.runId)}/waitpoints/${encodeURIComponent(resolved.waitpointId)}/approve`, {
522
+ method: "POST",
523
+ body: JSON.stringify(approvalBody(resolved.opts)),
524
+ headers: { "content-type": "application/json" }
525
+ });
526
+ },
527
+ deny: async (target, waitpointIdOrOpts, opts = {}) => {
528
+ const resolved = resolveWaitpointArgs(target, waitpointIdOrOpts, opts);
529
+ await this.#fetch(`/api/runs/${encodeURIComponent(resolved.runId)}/waitpoints/${encodeURIComponent(resolved.waitpointId)}/deny`, {
530
+ method: "POST",
531
+ body: JSON.stringify(approvalBody(resolved.opts)),
532
+ headers: { "content-type": "application/json" }
533
+ });
534
+ },
535
+ reply: async (target, waitpointIdOrOpts, opts) => {
536
+ const resolved = resolveWaitpointArgs(target, waitpointIdOrOpts, opts);
537
+ await this.#fetch(`/api/runs/${encodeURIComponent(resolved.runId)}/waitpoints/${encodeURIComponent(resolved.waitpointId)}/message`, {
538
+ method: "POST",
539
+ body: JSON.stringify({ text: resolved.opts.text, attachments: [] }),
540
+ headers: { "content-type": "application/json" }
541
+ });
542
+ }
543
+ };
544
+ async#retrieveLogs(id, signal) {
545
+ const response = await this.#json(`/api/runs/${encodeURIComponent(id)}/logs`, requestSignal(signal));
546
+ return {
547
+ stdout: decodeBase64Text(response.stdout_base64),
548
+ stderr: decodeBase64Text(response.stderr_base64),
549
+ cursor: response.cursor,
550
+ truncated: response.truncated
551
+ };
552
+ }
553
+ async#listEvents(id, opts) {
554
+ const events = [];
555
+ let cursor = opts.cursor;
556
+ for (;; ) {
557
+ const query = new URLSearchParams;
558
+ if (cursor !== undefined)
559
+ query.set("cursor", String(cursor));
560
+ if (opts.pageSize !== undefined)
561
+ query.set("limit", String(opts.pageSize));
562
+ const suffix = query.size === 0 ? "" : `?${query}`;
563
+ const page = await this.#json(`/api/runs/${encodeURIComponent(id)}/events${suffix}`, requestSignal(opts.signal));
564
+ events.push(...page.events);
565
+ if (page.next_cursor === undefined || page.next_cursor === null) {
566
+ break;
567
+ }
568
+ cursor = page.next_cursor;
569
+ }
570
+ return events.map((event) => runEventRecordToRunEvent(event)).filter((event) => event !== undefined);
571
+ }
572
+ async#subscribeEvents(id, opts) {
573
+ const query = new URLSearchParams;
574
+ query.set("follow", "1");
575
+ if (opts.cursor !== undefined)
576
+ query.set("cursor", String(opts.cursor));
577
+ const response = await this.#fetch(`/api/runs/${encodeURIComponent(id)}/events?${query}`, {
578
+ headers: { accept: "text/event-stream" },
579
+ ...requestSignal(opts.signal)
580
+ });
581
+ return parseSse(response);
582
+ }
583
+ async#json(path, init = {}) {
584
+ return (await this.#fetch(path, init)).json();
585
+ }
586
+ async#fetch(path, init = {}) {
587
+ const headers = new Headers(init.headers);
588
+ if (this.#apiKey !== undefined) {
589
+ headers.set("authorization", `Bearer ${this.#apiKey}`);
590
+ }
591
+ const request = {
592
+ ...init,
593
+ headers
594
+ };
595
+ const response = await fetch(endpointUrl(this.#baseUrl, path), request);
596
+ if (response.status === 401) {
597
+ throw new AuthError("Helmr authentication failed");
598
+ }
599
+ if (!response.ok) {
600
+ throw new Error(`Helmr API ${response.status}: ${await response.text()}`);
601
+ }
602
+ return response;
603
+ }
604
+ }
605
+ function normalizedBaseUrl(url) {
606
+ if (url.search !== "" || url.hash !== "") {
607
+ throw new UnsupportedTransportError("HelmrClient URL must not include query or fragment");
608
+ }
609
+ return url;
610
+ }
611
+ function isLoopbackHost(hostname) {
612
+ const host = hostname.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
613
+ if (host === "localhost" || host === "::1") {
614
+ return true;
615
+ }
616
+ const ipv4 = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(host);
617
+ if (ipv4 === null) {
618
+ return false;
619
+ }
620
+ return ipv4[1] === "127" && ipv4.slice(2).every((part) => Number(part) >= 0 && Number(part) <= 255);
621
+ }
622
+ function endpointUrl(baseUrl, path) {
623
+ const endpoint = new URL(baseUrl.toString());
624
+ const queryStart = path.indexOf("?");
625
+ const pathOnly = queryStart === -1 ? path : path.slice(0, queryStart);
626
+ const query = queryStart === -1 ? "" : path.slice(queryStart + 1);
627
+ endpoint.pathname = joinUrlPath(endpoint.pathname, pathOnly);
628
+ endpoint.search = query;
629
+ endpoint.hash = "";
630
+ return endpoint;
631
+ }
632
+ function joinUrlPath(basePath, path) {
633
+ const base = basePath.replace(/\/+$/, "");
634
+ const suffix = `/${path.replace(/^\/+/, "")}`;
635
+ return base === "" ? suffix : `${base}${suffix}`;
636
+ }
637
+ function runResponseToSnapshot(response) {
638
+ return runSnapshot({
639
+ id: response.id,
640
+ taskId: response.task_id,
641
+ status: response.status,
642
+ exitCode: response.exit_code ?? null,
643
+ ...response.created_at === undefined ? {} : { createdAt: response.created_at },
644
+ ...response.updated_at === undefined ? {} : { updatedAt: response.updated_at },
645
+ pendingWaitpoint: pendingWaitpointFromResponse(response.id, response.pending_wait),
646
+ ..."output" in response ? { output: response.output } : {}
647
+ });
648
+ }
649
+ function approvalBody(opts) {
650
+ return opts.reason === undefined ? {} : { reason: opts.reason };
651
+ }
652
+ function resolveWaitpointArgs(target, waitpointIdOrOpts, opts) {
653
+ if (isWaitpointRef(target)) {
654
+ const resolvedOpts = waitpointIdOrOpts !== undefined && typeof waitpointIdOrOpts !== "string" ? waitpointIdOrOpts : opts;
655
+ return {
656
+ runId: target.runId,
657
+ waitpointId: target.waitpointId,
658
+ opts: resolvedOpts ?? {}
659
+ };
660
+ }
661
+ if (typeof waitpointIdOrOpts !== "string") {
662
+ throw new Error("waitpoint id is required when resolving a waitpoint by run id");
663
+ }
664
+ return {
665
+ runId: target,
666
+ waitpointId: waitpointIdOrOpts,
667
+ opts: opts ?? {}
668
+ };
669
+ }
670
+ function isWaitpointRef(value) {
671
+ if (value === null || typeof value !== "object")
672
+ return false;
673
+ const record = value;
674
+ return typeof record["runId"] === "string" && typeof record["waitpointId"] === "string";
675
+ }
676
+ function retrieveOptions(signal) {
677
+ return signal === undefined ? {} : { signal };
678
+ }
679
+ function requestSignal(signal) {
680
+ return signal === undefined ? {} : { signal };
681
+ }
682
+ function throwIfAborted(signal) {
683
+ if (signal?.aborted !== true)
684
+ return;
685
+ if (signal.reason instanceof Error) {
686
+ throw signal.reason;
687
+ }
688
+ throw new Error("operation aborted");
689
+ }
690
+ function delay(ms, signal) {
691
+ throwIfAborted(signal);
692
+ return new Promise((resolve, reject) => {
693
+ const cleanup = () => {
694
+ clearTimeout(timeout);
695
+ signal?.removeEventListener("abort", onAbort);
696
+ };
697
+ const timeout = setTimeout(() => {
698
+ cleanup();
699
+ resolve();
700
+ }, ms);
701
+ const onAbort = () => {
702
+ cleanup();
703
+ reject(signal?.reason instanceof Error ? signal.reason : new Error("operation aborted"));
704
+ };
705
+ signal?.addEventListener("abort", onAbort, { once: true });
706
+ });
707
+ }
708
+ async function* parseSse(response) {
709
+ const reader = response.body?.getReader();
710
+ if (reader === undefined) {
711
+ return;
712
+ }
713
+ const decoder = new TextDecoder;
714
+ let buffer = "";
715
+ for (;; ) {
716
+ const { value, done } = await reader.read();
717
+ if (done) {
718
+ return;
719
+ }
720
+ buffer += decoder.decode(value, { stream: true });
721
+ let boundary = findSseBoundary(buffer);
722
+ while (boundary !== -1) {
723
+ const delimiter = buffer.startsWith(`\r
724
+ \r
725
+ `, boundary) ? 4 : 2;
726
+ const raw = buffer.slice(0, boundary);
727
+ buffer = buffer.slice(boundary + delimiter);
728
+ const data = raw.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join(`
729
+ `);
730
+ if (data !== "") {
731
+ const event = runEventRecordToRunEvent(JSON.parse(data));
732
+ if (event !== undefined) {
733
+ yield event;
734
+ }
735
+ }
736
+ boundary = findSseBoundary(buffer);
737
+ }
738
+ }
739
+ }
740
+ function findSseBoundary(buffer) {
741
+ const lf = buffer.indexOf(`
742
+
743
+ `);
744
+ const crlf = buffer.indexOf(`\r
745
+ \r
746
+ `);
747
+ if (lf === -1)
748
+ return crlf;
749
+ if (crlf === -1)
750
+ return lf;
751
+ return Math.min(lf, crlf);
752
+ }
753
+ function runEventRecordToRunEvent(event) {
754
+ const attributes = objectRecord(event.attributes);
755
+ const runId2 = event.run_id ?? stringValue(attributes?.["run_id"]) ?? "";
756
+ if (event.message === "log.stdout" || event.message === "log.stderr") {
757
+ const stream = event.message === "log.stdout" ? "stdout" : "stderr";
758
+ return {
759
+ type: "log",
760
+ run_id: runId2,
761
+ stream,
762
+ bytes: numberValue(attributes?.["bytes"]) ?? 0,
763
+ observed_seq: numberValue(attributes?.["observed_seq"]) ?? 0,
764
+ at: event.at
765
+ };
766
+ }
767
+ if (event.message === "waitpoint.requested" && stringValue(attributes?.["kind"]) === "approval") {
768
+ const message = stringValue(attributes?.["display_text"]);
769
+ const waitpointId = stringValue(attributes?.["waitpoint_id"]);
770
+ if (waitpointId === undefined)
771
+ return;
772
+ return {
773
+ type: "approval_request",
774
+ run_id: runId2,
775
+ waitpoint_id: waitpointId,
776
+ message: message ?? "",
777
+ ...optionalNumber("timeout", attributes?.["timeout"]),
778
+ at: event.at
779
+ };
780
+ }
781
+ if (event.message === "waitpoint.resolved" && stringValue(attributes?.["kind"]) === "approval") {
782
+ const waitpointId = stringValue(attributes?.["waitpoint_id"]);
783
+ const resolution = stringValue(attributes?.["resolution_kind"]);
784
+ if (waitpointId === undefined)
785
+ return;
786
+ if (resolution !== "approved" && resolution !== "denied")
787
+ return;
788
+ return {
789
+ type: "approval_decided",
790
+ run_id: runId2,
791
+ waitpoint_id: waitpointId,
792
+ decision: resolution,
793
+ ...optionalString("reason", attributes?.["reason"]),
794
+ at: event.at
795
+ };
796
+ }
797
+ if (event.message === "waitpoint.requested" && stringValue(attributes?.["kind"]) === "message") {
798
+ const request = objectRecord(attributes?.["request"]);
799
+ const message = stringValue(attributes?.["display_text"]) ?? stringValue(request?.["prompt"]);
800
+ const waitpointId = stringValue(attributes?.["waitpoint_id"]);
801
+ if (waitpointId === undefined)
802
+ return;
803
+ return {
804
+ type: "message_request",
805
+ run_id: runId2,
806
+ waitpoint_id: waitpointId,
807
+ ...optionalString("prompt", message),
808
+ ...optionalNumber("timeout", attributes?.["timeout"]),
809
+ at: event.at
810
+ };
811
+ }
812
+ if (event.message === "waitpoint.resolved" && stringValue(attributes?.["kind"]) === "message") {
813
+ const result = objectRecord(attributes?.["result"]);
814
+ const text = stringValue(result?.["text"]);
815
+ const waitpointId = stringValue(attributes?.["waitpoint_id"]);
816
+ if (waitpointId === undefined)
817
+ return;
818
+ if (stringValue(attributes?.["resolution_kind"]) !== "replied")
819
+ return;
820
+ return {
821
+ type: "message_received",
822
+ run_id: runId2,
823
+ waitpoint_id: waitpointId,
824
+ text: text ?? "",
825
+ at: event.at
826
+ };
827
+ }
828
+ if (event.message.startsWith("emit.")) {
829
+ return {
830
+ type: "emit",
831
+ run_id: runId2,
832
+ event_type: stringValue(attributes?.["type"]) ?? event.message.slice("emit.".length),
833
+ content: attributes?.["content"],
834
+ at: event.at
835
+ };
836
+ }
837
+ if (event.message === "run.completed") {
838
+ return {
839
+ type: "task_complete",
840
+ run_id: runId2,
841
+ exit_code: numberValue(attributes?.["exit_code"]) ?? 0,
842
+ at: event.at
843
+ };
844
+ }
845
+ if (event.message === "run.failed") {
846
+ return {
847
+ type: "run_failed",
848
+ run_id: runId2,
849
+ failure_kind: stringValue(attributes?.["failure_kind"]) ?? "task_failed",
850
+ detail: attributes?.["detail"],
851
+ at: event.at
852
+ };
853
+ }
854
+ if (event.message === "run.timeout") {
855
+ return {
856
+ type: "run_timeout",
857
+ run_id: runId2,
858
+ elapsed_secs: numberValue(attributes?.["elapsed_active_secs"]) ?? 0,
859
+ limit_secs: numberValue(attributes?.["limit_secs"]) ?? 0,
860
+ at: event.at
861
+ };
862
+ }
863
+ if (event.message === "run.cancelled") {
864
+ return {
865
+ type: "run_cancelled",
866
+ run_id: runId2,
867
+ ...optionalString("reason", attributes?.["reason"]),
868
+ at: event.at
869
+ };
870
+ }
871
+ return;
872
+ }
873
+ function optionalString(key, value) {
874
+ const text = stringValue(value);
875
+ return text === undefined ? {} : { [key]: text };
876
+ }
877
+ function optionalNumber(key, value) {
878
+ return typeof value === "number" ? { [key]: value } : {};
879
+ }
880
+ function objectRecord(value) {
881
+ return value !== null && typeof value === "object" ? value : undefined;
882
+ }
883
+ function stringValue(value) {
884
+ return typeof value === "string" ? value : undefined;
885
+ }
886
+ function numberValue(value) {
887
+ return typeof value === "number" ? value : undefined;
888
+ }
889
+ function decodeBase64Text(value) {
890
+ const binary = atob(value);
891
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
892
+ return new TextDecoder().decode(bytes);
893
+ }
894
+
895
+ // sdk/typescript/src/sandbox.ts
896
+ var sandbox = (id) => new SandboxBuilderImpl(id);
897
+
898
+ // sdk/typescript/src/config.ts
899
+ function defineConfig(config) {
900
+ if (config === null || typeof config !== "object") {
901
+ throw new Error("defineConfig() requires an object");
902
+ }
903
+ if (!("dirs" in config)) {
904
+ throw new Error("defineConfig({ dirs }) requires a non-empty dirs array");
905
+ }
906
+ if (!Array.isArray(config.dirs) || config.dirs.length === 0) {
907
+ throw new Error("defineConfig({ dirs }) requires a non-empty dirs array");
908
+ }
909
+ if (config.project !== undefined) {
910
+ if (typeof config.project !== "string" || config.project.trim() === "") {
911
+ throw new Error("defineConfig({ project }) must be a non-empty string");
912
+ }
913
+ if (config.project.includes("\x00")) {
914
+ throw new Error("defineConfig({ project }) must not contain NUL");
915
+ }
916
+ }
917
+ for (const dir of config.dirs) {
918
+ if (typeof dir !== "string" || dir.trim() === "") {
919
+ throw new Error("defineConfig({ dirs }) entries must be non-empty strings");
920
+ }
921
+ if (dir.includes("\x00")) {
922
+ throw new Error("defineConfig({ dirs }) entries must not contain NUL");
923
+ }
924
+ }
925
+ if (config.ignorePatterns !== undefined) {
926
+ if (!Array.isArray(config.ignorePatterns)) {
927
+ throw new Error("defineConfig({ ignorePatterns }) must be an array of strings");
928
+ }
929
+ for (const pattern of config.ignorePatterns) {
930
+ if (typeof pattern !== "string" || pattern.trim() === "") {
931
+ throw new Error("defineConfig({ ignorePatterns }) entries must be non-empty strings");
932
+ }
933
+ if (pattern.includes("\x00")) {
934
+ throw new Error("defineConfig({ ignorePatterns }) entries must not contain NUL");
935
+ }
936
+ }
937
+ }
938
+ return markConfig({
939
+ ...config.project === undefined ? {} : { project: config.project },
940
+ dirs: [...config.dirs],
941
+ ...config.ignorePatterns === undefined ? {} : { ignorePatterns: [...config.ignorePatterns] }
942
+ });
943
+ }
944
+
945
+ // sdk/typescript/src/task.ts
946
+ function task(config) {
947
+ return markTask(config);
948
+ }
949
+
950
+ // sdk/typescript/src/trigger.ts
951
+ var defaultClient;
952
+ function getDefaultClient() {
953
+ defaultClient ??= new HelmrClient;
954
+ return defaultClient;
955
+ }
956
+ var tasks = {
957
+ trigger(task2, opts) {
958
+ return getDefaultClient().tasks.trigger(task2, opts);
959
+ }
960
+ };
961
+ // sdk/typescript/src/index.ts
962
+ var image = (id) => new ImageBuilderImpl(id);
963
+ var cache = (id) => ({ id });
964
+ var source = {
965
+ file(path) {
966
+ return new SourceFileRefImpl(path);
967
+ },
968
+ directory(path, opts) {
969
+ return new SourceDirRefImpl(path, opts?.ignore ? [...opts.ignore] : []);
970
+ }
971
+ };
972
+ var workspace = {
973
+ github(repo, opts) {
974
+ const [org, name, extra] = repo.split("/");
975
+ if (!org || !name || extra !== undefined) {
976
+ throw new Error('workspace.github() repo must be "org/repo"');
977
+ }
978
+ const ref = opts.ref.trim();
979
+ if (!ref) {
980
+ throw new Error("workspace.github() ref is required");
981
+ }
982
+ if (ref.includes("\x00")) {
983
+ throw new Error("workspace.github() ref must not contain NUL");
984
+ }
985
+ return {
986
+ kind: "github",
987
+ repository: repo,
988
+ ref,
989
+ ...opts.subpath === undefined ? {} : { subpath: opts.subpath }
990
+ };
991
+ }
992
+ };
993
+ var runs = new Proxy({}, {
994
+ get(_target, property, receiver) {
995
+ return Reflect.get(getDefaultClient().runs, property, receiver);
996
+ }
997
+ });
998
+ var waitpoints = new Proxy({}, {
999
+ get(_target, property, receiver) {
1000
+ return Reflect.get(getDefaultClient().waitpoints, property, receiver);
1001
+ }
1002
+ });
1003
+ export {
1004
+ workspace,
1005
+ waitpoints,
1006
+ validateSecretName,
1007
+ tasks,
1008
+ task,
1009
+ source,
1010
+ sandbox,
1011
+ runs,
1012
+ image,
1013
+ defineConfig,
1014
+ cache,
1015
+ UnsupportedTransportError,
1016
+ TimeoutError,
1017
+ RunNotFoundError,
1018
+ MessageTimeoutError,
1019
+ HelmrClient,
1020
+ ConcurrentWaitError,
1021
+ AuthError,
1022
+ ApprovalTimeoutError
1023
+ };