antpath 0.7.0 → 0.8.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.
@@ -22,6 +22,19 @@ export declare function createOutputLink(http: HttpClient, runId: string, output
22
22
  export declare function cancelRun(http: HttpClient, runId: string): Promise<void>;
23
23
  export declare function deleteRun(http: HttpClient, runId: string): Promise<void>;
24
24
  export declare function whoami(http: HttpClient): Promise<WhoAmI>;
25
+ /**
26
+ * Stream the per-run archive zip from the BFF. Returns the raw
27
+ * `Response` so callers can pipe the body to disk without buffering
28
+ * the whole archive in memory.
29
+ *
30
+ * The archive lifecycle contract lives in
31
+ * `apps/dashboard/src/server/run-archive.ts` and
32
+ * `packages/sdk/docs/outputs.md`. Pre-session runs reject with HTTP
33
+ * 409 `run_not_started`; mid-session and terminal both produce the
34
+ * same archive layout and differ only in `manifest.json`'s `source`
35
+ * + `partial` fields.
36
+ */
37
+ export declare function downloadRunArchive(http: HttpClient, runId: string): Promise<Response>;
25
38
  export declare function submitRunFlat(http: HttpClient, request: PlatformFlatRunSubmissionInput): Promise<Run>;
26
39
  /**
27
40
  * Multipart variant of `submitRunFlat` for runs that carry transient
@@ -59,3 +72,23 @@ export declare function createSkillBundle(http: HttpClient, args: {
59
72
  export declare function listSkills(http: HttpClient): Promise<readonly Skill[]>;
60
73
  export declare function getSkill(http: HttpClient, skillId: string): Promise<Skill>;
61
74
  export declare function deleteSkill(http: HttpClient, skillId: string): Promise<void>;
75
+ /**
76
+ * Lookup a live workspace skill by `(name, contentHash)`. Returns the
77
+ * matching `Skill` record or null when no live row carries that hash.
78
+ *
79
+ * `contentHash` is the wire format `sha256:<hex>` as returned by
80
+ * `hashSkillBundle`. This powers `Skill.uploadIfChanged` — the SDK
81
+ * computes the hash locally and calls this function to skip the upload
82
+ * when the bytes already exist.
83
+ */
84
+ export declare function findSkillByHash(http: HttpClient, args: {
85
+ readonly name: string;
86
+ readonly contentHash: string;
87
+ }): Promise<Skill | null>;
88
+ /**
89
+ * Lookup a live workspace skill by `name`. Returns the matching `Skill`
90
+ * record or null when no live row carries that name. Implemented as a
91
+ * list-and-filter on the existing `/api/skills` endpoint — the
92
+ * indexed by-hash route is reserved for `uploadIfChanged`.
93
+ */
94
+ export declare function findSkillByName(http: HttpClient, name: string): Promise<Skill | null>;
@@ -41,6 +41,22 @@ export async function deleteRun(http, runId) {
41
41
  export async function whoami(http) {
42
42
  return http.request("/api/whoami");
43
43
  }
44
+ /**
45
+ * Stream the per-run archive zip from the BFF. Returns the raw
46
+ * `Response` so callers can pipe the body to disk without buffering
47
+ * the whole archive in memory.
48
+ *
49
+ * The archive lifecycle contract lives in
50
+ * `apps/dashboard/src/server/run-archive.ts` and
51
+ * `packages/sdk/docs/outputs.md`. Pre-session runs reject with HTTP
52
+ * 409 `run_not_started`; mid-session and terminal both produce the
53
+ * same archive layout and differ only in `manifest.json`'s `source`
54
+ * + `partial` fields.
55
+ */
56
+ export async function downloadRunArchive(http, runId) {
57
+ const { response } = await http.download(`/api/runs/${encodeURIComponent(runId)}/download`);
58
+ return response;
59
+ }
44
60
  // ===========================================================================
45
61
  // Flat (Skill / McpServer / Blueprint) operations
46
62
  // ===========================================================================
@@ -125,6 +141,33 @@ export async function deleteSkill(http, skillId) {
125
141
  method: "DELETE"
126
142
  });
127
143
  }
144
+ /**
145
+ * Lookup a live workspace skill by `(name, contentHash)`. Returns the
146
+ * matching `Skill` record or null when no live row carries that hash.
147
+ *
148
+ * `contentHash` is the wire format `sha256:<hex>` as returned by
149
+ * `hashSkillBundle`. This powers `Skill.uploadIfChanged` — the SDK
150
+ * computes the hash locally and calls this function to skip the upload
151
+ * when the bytes already exist.
152
+ */
153
+ export async function findSkillByHash(http, args) {
154
+ const params = new URLSearchParams({
155
+ name: args.name,
156
+ content_hash: args.contentHash
157
+ });
158
+ const result = await http.request(`/api/skills/by-hash?${params.toString()}`);
159
+ return result.skill ?? null;
160
+ }
161
+ /**
162
+ * Lookup a live workspace skill by `name`. Returns the matching `Skill`
163
+ * record or null when no live row carries that name. Implemented as a
164
+ * list-and-filter on the existing `/api/skills` endpoint — the
165
+ * indexed by-hash route is reserved for `uploadIfChanged`.
166
+ */
167
+ export async function findSkillByName(http, name) {
168
+ const skills = await listSkills(http);
169
+ return skills.find((skill) => skill.name === name) ?? null;
170
+ }
128
171
  function unwrapSkill(result) {
129
172
  if (result && typeof result === "object" && "skill" in result) {
130
173
  return result.skill;
@@ -67,8 +67,16 @@ export interface ProxyIndexEntry {
67
67
  * The actual auth value lives in the run's Vault bundle under
68
68
  * `secrets.proxyEndpointAuth[i].value` and is never reflected back
69
69
  * into the container or index file.
70
+ *
71
+ * The `none` variant declares an upstream that takes no auth (public
72
+ * APIs like Wikimedia Commons or NASA Images). It still routes through
73
+ * the proxy for unified egress, audit, and budget enforcement, but
74
+ * carries no `proxyEndpointAuth[]` entry and the BFF injects no
75
+ * header or query value.
70
76
  */
71
77
  export type ProxyAuthShape = {
78
+ readonly type: "none";
79
+ } | {
72
80
  readonly type: "bearer";
73
81
  } | {
74
82
  readonly type: "basic";
@@ -82,7 +90,7 @@ export type ProxyAuthShape = {
82
90
  export type ProxyAuthType = ProxyAuthShape["type"];
83
91
  /**
84
92
  * Header name (lowercase) that an upstream auth shape uses as its
85
- * carrier. Returns `undefined` for query-based auth.
93
+ * carrier. Returns `undefined` for query-based and keyless auth.
86
94
  *
87
95
  * Used by the submission parser to forbid `allowHeaders` from listing
88
96
  * the auth header (avoids leaks via caller-supplied headers), and by
@@ -92,7 +100,7 @@ export type ProxyAuthType = ProxyAuthShape["type"];
92
100
  export declare function authShapeHeaderName(shape: ProxyAuthShape): string | undefined;
93
101
  /**
94
102
  * Query-string key that an upstream query-based auth shape uses as its
95
- * carrier. Returns `undefined` for non-query shapes.
103
+ * carrier. Returns `undefined` for non-query shapes (including "none").
96
104
  */
97
105
  export declare function authShapeQueryName(shape: ProxyAuthShape): string | undefined;
98
106
  /**
@@ -66,7 +66,7 @@ export const PROXY_ERROR_CODES = [
66
66
  ];
67
67
  /**
68
68
  * Header name (lowercase) that an upstream auth shape uses as its
69
- * carrier. Returns `undefined` for query-based auth.
69
+ * carrier. Returns `undefined` for query-based and keyless auth.
70
70
  *
71
71
  * Used by the submission parser to forbid `allowHeaders` from listing
72
72
  * the auth header (avoids leaks via caller-supplied headers), and by
@@ -81,12 +81,13 @@ export function authShapeHeaderName(shape) {
81
81
  case "header":
82
82
  return shape.name.toLowerCase();
83
83
  case "query":
84
+ case "none":
84
85
  return undefined;
85
86
  }
86
87
  }
87
88
  /**
88
89
  * Query-string key that an upstream query-based auth shape uses as its
89
- * carrier. Returns `undefined` for non-query shapes.
90
+ * carrier. Returns `undefined` for non-query shapes (including "none").
90
91
  */
91
92
  export function authShapeQueryName(shape) {
92
93
  return shape.type === "query" ? shape.name : undefined;
@@ -71,6 +71,25 @@ export interface WhoAmI {
71
71
  readonly tokenId?: string;
72
72
  readonly tokenName?: string | null;
73
73
  readonly scopes?: readonly string[];
74
+ /**
75
+ * Workspace-level caps the BFF will enforce on subsequent calls.
76
+ * Surfaced so consumers (e.g. broll's app-side admission gate) can
77
+ * decide whether to keep their own gate or rely on platform headers.
78
+ * All fields optional — older BFFs may omit. Numbers are concrete
79
+ * snapshots at the time of the `whoami` call.
80
+ */
81
+ readonly caps?: {
82
+ /** Token-bucket cap on POST /api/runs per minute, per workspace. */
83
+ readonly runSubmitPerMinute?: number;
84
+ /** Hard cap on concurrent non-terminal runs the workspace may hold. */
85
+ readonly maxConcurrentRuns?: number;
86
+ /** Storage cap (bytes) on captured output objects, workspace-wide. */
87
+ readonly storageCapBytes?: number;
88
+ /** Current captured-output usage in bytes. */
89
+ readonly storageUsedBytes?: number;
90
+ /** Wall-clock ceiling on a single run before forced termination. */
91
+ readonly maxRunDurationMs?: number;
92
+ };
74
93
  readonly [key: string]: unknown;
75
94
  }
76
95
  /**
@@ -175,6 +175,27 @@ export interface PlatformFlatSubmission {
175
175
  readonly mcpServers: readonly McpServerRef[];
176
176
  readonly environment?: PlatformTemplateEnvironment;
177
177
  readonly metadata?: Record<string, JsonValue>;
178
+ /**
179
+ * Opt-in container paths to capture as `output_objects` at session
180
+ * terminal. When omitted, the worker still persists run metadata
181
+ * (status, events, snapshots, cleanup state) but does not capture
182
+ * any container file bytes. When present, the worker drives a
183
+ * synthetic agent turn at session terminal that instructs the agent
184
+ * to register every file under these paths via the Anthropic Files
185
+ * API, then walks the resulting list and copies bytes into private
186
+ * Supabase Storage.
187
+ *
188
+ * Validation:
189
+ * - Absolute UNIX paths only (starts with `/`).
190
+ * - No `..` segments, no NUL bytes, no embedded newlines.
191
+ * - Max 32 entries.
192
+ * - Max 512 bytes per entry.
193
+ *
194
+ * Entries are normalised (collapse `/+`, drop trailing `/` except
195
+ * for `/`) and deduplicated. The normalised list is what travels in
196
+ * the idempotency hash and the run snapshot.
197
+ */
198
+ readonly outputDirs?: readonly string[];
178
199
  }
179
200
  export interface PlatformFlatRunSubmissionRequest {
180
201
  readonly workspaceId: string;
@@ -272,6 +272,9 @@ function parseProxyAuthShape(input, field) {
272
272
  const value = requireRecord(input, field);
273
273
  const type = requireString(value.type, `${field}.type`);
274
274
  switch (type) {
275
+ case "none":
276
+ assertOnlyKeys(value, field, ["type"]);
277
+ return { type: "none" };
275
278
  case "bearer":
276
279
  assertOnlyKeys(value, field, ["type"]);
277
280
  return { type: "bearer" };
@@ -293,7 +296,7 @@ function parseProxyAuthShape(input, field) {
293
296
  return { type: "query", name };
294
297
  }
295
298
  default:
296
- throw new Error(`${field}.type must be one of: bearer, basic, header, query`);
299
+ throw new Error(`${field}.type must be one of: none, bearer, basic, header, query`);
297
300
  }
298
301
  }
299
302
  function parseProxyMethods(input, field) {
@@ -382,6 +385,16 @@ function crossValidateProxyEndpointsAndAuth(endpoints, auth) {
382
385
  const authByName = new Map(authList.map((a) => [a.name, a]));
383
386
  for (const endpoint of endpointsList) {
384
387
  const authEntry = authByName.get(endpoint.name);
388
+ if (endpoint.authShape.type === "none") {
389
+ // Keyless endpoints carry no auth value. Reject any matching
390
+ // auth entry so callers don't accidentally ship a secret bound
391
+ // to a "none" endpoint (which would be silently ignored at
392
+ // request time — confusing and a leak risk).
393
+ if (authEntry) {
394
+ throw new Error(`proxyEndpoints[${endpoint.name}] has authShape "none" but a matching secrets.proxyEndpointAuth entry was supplied; remove the auth entry`);
395
+ }
396
+ continue;
397
+ }
385
398
  if (!authEntry) {
386
399
  throw new Error(`proxyEndpoints[${endpoint.name}] has no matching secrets.proxyEndpointAuth entry`);
387
400
  }
@@ -742,7 +755,8 @@ function parseFlatSubmission(input) {
742
755
  "skills",
743
756
  "mcpServers",
744
757
  "environment",
745
- "metadata"
758
+ "metadata",
759
+ "outputDirs"
746
760
  ]);
747
761
  for (const key of Object.keys(value)) {
748
762
  if (!allowed.has(key)) {
@@ -756,6 +770,7 @@ function parseFlatSubmission(input) {
756
770
  const mcpServers = parseFlatMcpServers(value.mcpServers);
757
771
  const environment = parseTemplateEnvironment(value.environment);
758
772
  const metadata = optionalJsonRecord(value.metadata, "submission.metadata");
773
+ const outputDirs = parseOutputDirs(value.outputDirs);
759
774
  return {
760
775
  model,
761
776
  ...(system ? { system } : {}),
@@ -763,9 +778,84 @@ function parseFlatSubmission(input) {
763
778
  skills,
764
779
  mcpServers,
765
780
  ...(environment ? { environment } : {}),
766
- ...(metadata ? { metadata } : {})
781
+ ...(metadata ? { metadata } : {}),
782
+ ...(outputDirs ? { outputDirs } : {})
767
783
  };
768
784
  }
785
+ /**
786
+ * Maximum number of `outputDirs` entries accepted per submission.
787
+ *
788
+ * 32 is enough room for the typical "one or two capture roots" pattern
789
+ * plus a generous margin for legitimate multi-root use cases (per-tool
790
+ * output directory + scratch state + logs, repeated across a few
791
+ * subdirectories), without inviting abuse of the synthetic-turn path
792
+ * the worker drives at session terminal.
793
+ */
794
+ const MAX_OUTPUT_DIRS = 32;
795
+ /**
796
+ * Maximum byte length of a single `outputDirs` entry (after UTF-8
797
+ * encoding). 512 bytes comfortably covers `/very/long/nested/path`
798
+ * style entries without letting a misuse smuggle large blobs through
799
+ * the field.
800
+ */
801
+ const MAX_OUTPUT_DIR_BYTES = 512;
802
+ function parseOutputDirs(input) {
803
+ if (input === undefined) {
804
+ return undefined;
805
+ }
806
+ if (!Array.isArray(input)) {
807
+ throw new Error("submission.outputDirs must be an array of absolute UNIX paths");
808
+ }
809
+ if (input.length === 0) {
810
+ // Treat an empty array as omission so the idempotency hash matches
811
+ // the "no outputDirs" case.
812
+ return undefined;
813
+ }
814
+ if (input.length > MAX_OUTPUT_DIRS) {
815
+ throw new Error(`submission.outputDirs has ${input.length} entries; max is ${MAX_OUTPUT_DIRS}`);
816
+ }
817
+ const seen = new Set();
818
+ const normalised = [];
819
+ for (let i = 0; i < input.length; i++) {
820
+ const item = input[i];
821
+ if (typeof item !== "string") {
822
+ throw new Error(`submission.outputDirs[${i}] must be a string`);
823
+ }
824
+ if (item.length === 0) {
825
+ throw new Error(`submission.outputDirs[${i}] must be a non-empty absolute UNIX path`);
826
+ }
827
+ const bytes = new TextEncoder().encode(item).length;
828
+ if (bytes > MAX_OUTPUT_DIR_BYTES) {
829
+ throw new Error(`submission.outputDirs[${i}] exceeds ${MAX_OUTPUT_DIR_BYTES} bytes (got ${bytes})`);
830
+ }
831
+ if (!item.startsWith("/")) {
832
+ throw new Error(`submission.outputDirs[${i}] must be an absolute UNIX path (start with '/')`);
833
+ }
834
+ if (item.includes("\0")) {
835
+ throw new Error(`submission.outputDirs[${i}] must not contain NUL bytes`);
836
+ }
837
+ if (item.includes("\n") || item.includes("\r")) {
838
+ throw new Error(`submission.outputDirs[${i}] must not contain newline characters`);
839
+ }
840
+ const segments = item.split("/");
841
+ if (segments.includes("..")) {
842
+ throw new Error(`submission.outputDirs[${i}] must not contain '..' segments`);
843
+ }
844
+ const collapsed = segments
845
+ .filter((seg, idx) => seg.length > 0 || idx === 0)
846
+ .join("/");
847
+ const stripped = collapsed.length > 1 && collapsed.endsWith("/")
848
+ ? collapsed.slice(0, -1)
849
+ : collapsed;
850
+ const canonical = stripped.length === 0 ? "/" : stripped;
851
+ if (seen.has(canonical)) {
852
+ continue;
853
+ }
854
+ seen.add(canonical);
855
+ normalised.push(canonical);
856
+ }
857
+ return normalised;
858
+ }
769
859
  function parseFlatPrompt(input) {
770
860
  if (typeof input === "string") {
771
861
  if (input.length === 0) {
package/dist/cli.mjs CHANGED
@@ -6,7 +6,12 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // dist/cli.js
9
- import { readFile as readFile2, writeFile } from "node:fs/promises";
9
+ import { readFile as readFile2, writeFile, readdir as readdir2, stat as stat2 } from "node:fs/promises";
10
+ import { resolve as resolvePath4 } from "node:path";
11
+
12
+ // dist/internal.js
13
+ var ANTPATH_INDEX_PATH = "/antpath/index.json";
14
+ var ANTPATH_RUN_TOKEN_PATH = "/antpath/run-token";
10
15
 
11
16
  // ../shared/dist/config.js
12
17
  var DEFAULT_CAPS = {
@@ -527,6 +532,9 @@ __export(operations_exports, {
527
532
  createSkillBundle: () => createSkillBundle,
528
533
  deleteRun: () => deleteRun,
529
534
  deleteSkill: () => deleteSkill,
535
+ downloadRunArchive: () => downloadRunArchive,
536
+ findSkillByHash: () => findSkillByHash,
537
+ findSkillByName: () => findSkillByName,
530
538
  getRun: () => getRun,
531
539
  getSkill: () => getSkill,
532
540
  listOutputs: () => listOutputs,
@@ -567,6 +575,10 @@ async function deleteRun(http, runId) {
567
575
  async function whoami(http) {
568
576
  return http.request("/api/whoami");
569
577
  }
578
+ async function downloadRunArchive(http, runId) {
579
+ const { response } = await http.download(`/api/runs/${encodeURIComponent(runId)}/download`);
580
+ return response;
581
+ }
570
582
  async function submitRunFlat(http, request) {
571
583
  return http.request("/api/runs", {
572
584
  method: "POST",
@@ -623,6 +635,18 @@ async function deleteSkill(http, skillId) {
623
635
  method: "DELETE"
624
636
  });
625
637
  }
638
+ async function findSkillByHash(http, args) {
639
+ const params = new URLSearchParams({
640
+ name: args.name,
641
+ content_hash: args.contentHash
642
+ });
643
+ const result = await http.request(`/api/skills/by-hash?${params.toString()}`);
644
+ return result.skill ?? null;
645
+ }
646
+ async function findSkillByName(http, name) {
647
+ const skills = await listSkills(http);
648
+ return skills.find((skill) => skill.name === name) ?? null;
649
+ }
626
650
  function unwrapSkill(result) {
627
651
  if (result && typeof result === "object" && "skill" in result) {
628
652
  return result.skill;
@@ -669,10 +693,6 @@ function validateProxyAuth(endpoints, auth) {
669
693
  }
670
694
  }
671
695
 
672
- // dist/internal.js
673
- var ANTPATH_INDEX_PATH = "/antpath/index.json";
674
- var ANTPATH_RUN_TOKEN_PATH = "/antpath/run-token";
675
-
676
696
  // dist/host/common.js
677
697
  var SUCCESS = { code: 0 };
678
698
  var USAGE_ERR = { code: 2 };
@@ -830,6 +850,52 @@ function takeBooleanFlag(rest, flag) {
830
850
  return { present, remaining };
831
851
  }
832
852
 
853
+ // dist/outputs-sync.js
854
+ async function runOutputsSyncCmd(io2, dirs) {
855
+ if (dirs.length === 0) {
856
+ io2.stderr("usage: antpath outputs sync <dir> [<dir> ...]\n");
857
+ return USAGE_ERR;
858
+ }
859
+ try {
860
+ await io2.readFile(ANTPATH_INDEX_PATH);
861
+ } catch {
862
+ io2.stderr("`antpath outputs sync` is an in-container internal command and cannot run on the host.\n");
863
+ return USAGE_ERR;
864
+ }
865
+ if (!io2.walkDirectory) {
866
+ io2.stderr("antpath outputs sync: walkDirectory IO is not available\n");
867
+ return RUNTIME_ERR;
868
+ }
869
+ let scanned = 0;
870
+ let missing = 0;
871
+ for (const dir of dirs) {
872
+ if (!dir.startsWith("/")) {
873
+ io2.stderr(JSON.stringify({ dir, error: "non_absolute_path", message: "skipping non-absolute output dir" }) + "\n");
874
+ missing++;
875
+ continue;
876
+ }
877
+ let entries;
878
+ try {
879
+ entries = await io2.walkDirectory(dir);
880
+ } catch (err2) {
881
+ io2.stderr(JSON.stringify({ dir, error: "walk_failed", message: err2.message ?? "walk failed" }) + "\n");
882
+ missing++;
883
+ continue;
884
+ }
885
+ if (entries === null) {
886
+ io2.stderr(JSON.stringify({ dir, error: "missing_or_unreadable" }) + "\n");
887
+ missing++;
888
+ continue;
889
+ }
890
+ for (const entry of entries) {
891
+ io2.stdout(JSON.stringify({ dir, path: entry.path, sizeBytes: entry.sizeBytes }) + "\n");
892
+ scanned++;
893
+ }
894
+ }
895
+ io2.stdout(JSON.stringify({ summary: { dirs: dirs.length, files: scanned, missing } }) + "\n");
896
+ return SUCCESS;
897
+ }
898
+
833
899
  // dist/proxy.js
834
900
  function parseProxyFlags(rest) {
835
901
  let endpointName = null;
@@ -2644,7 +2710,7 @@ async function runOutputsCmd(io2, argv) {
2644
2710
  }
2645
2711
 
2646
2712
  // dist/host/download.js
2647
- import { resolve as resolvePath3, basename as basename2 } from "node:path";
2713
+ import { resolve as resolvePath3 } from "node:path";
2648
2714
  async function runDownloadCmd(io2, argv) {
2649
2715
  if (await refuseInsideManagedRun(io2, "download"))
2650
2716
  return USAGE_ERR;
@@ -2661,51 +2727,38 @@ async function runDownloadCmd(io2, argv) {
2661
2727
  return USAGE_ERR;
2662
2728
  }
2663
2729
  const positional = outFlag.remaining.filter((arg) => !arg.startsWith("--"));
2664
- if (positional.length !== 2) {
2665
- io2.stderr("usage: antpath download <run-id> <output-id> [--out path] [common flags]\n");
2730
+ if (positional.length !== 1) {
2731
+ io2.stderr("usage: antpath download <run-id> [--out path] [common flags]\n");
2666
2732
  return USAGE_ERR;
2667
2733
  }
2668
2734
  const runId = positional[0];
2669
- const outputId = positional[1];
2670
2735
  const http = makeHttpClient(io2, common.flags);
2671
- let link;
2736
+ let response;
2672
2737
  try {
2673
- link = await operations_exports.createOutputLink(http, runId, outputId);
2738
+ response = await operations_exports.downloadRunArchive(http, runId);
2674
2739
  } catch (err2) {
2675
- return emitJsonError(io2, "link_failed", err2.message ?? "create link failed", { runId, outputId });
2740
+ return emitJsonError(io2, "download_failed", err2.message ?? "download failed", { runId });
2676
2741
  }
2677
- let response;
2742
+ let bytes;
2678
2743
  try {
2679
- response = await io2.fetchImpl(link.url, { method: "GET", redirect: "follow" });
2744
+ bytes = new Uint8Array(await response.arrayBuffer());
2680
2745
  } catch (err2) {
2681
- return emitJsonError(io2, "download_failed", `download fetch failed: ${err2.message}`, { runId, outputId });
2682
- }
2683
- if (!response.ok) {
2684
- return emitJsonError(io2, "download_failed", `download HTTP ${response.status}`, { runId, outputId });
2746
+ return emitJsonError(io2, "download_failed", `download read failed: ${err2.message}`, { runId });
2685
2747
  }
2686
- const buffer = new Uint8Array(await response.arrayBuffer());
2687
- const destination = resolveDestination(io2, outFlag.value, outputId, link.url);
2748
+ const destination = resolveDestination(io2, outFlag.value, runId);
2688
2749
  try {
2689
- await io2.writeFile(destination, buffer);
2750
+ await io2.writeFile(destination, bytes);
2690
2751
  } catch (err2) {
2691
- return emitJsonError(io2, "write_failed", `failed to write output: ${err2.message}`, { destination });
2752
+ return emitJsonError(io2, "write_failed", `failed to write archive: ${err2.message}`, { destination });
2692
2753
  }
2693
- io2.stdout(JSON.stringify({ runId, outputId, path: destination, bytes: buffer.byteLength }) + "\n");
2754
+ io2.stdout(JSON.stringify({ runId, path: destination, bytes: bytes.byteLength }) + "\n");
2694
2755
  return SUCCESS;
2695
2756
  }
2696
- function resolveDestination(io2, out, outputId, signedUrl) {
2757
+ function resolveDestination(io2, out, runId) {
2697
2758
  if (out) {
2698
2759
  return resolvePath3(io2.cwd(), out);
2699
2760
  }
2700
- let fileName = `${outputId}`;
2701
- try {
2702
- const url = new URL(signedUrl);
2703
- const tail = basename2(url.pathname);
2704
- if (tail)
2705
- fileName = tail;
2706
- } catch {
2707
- }
2708
- return resolvePath3(io2.cwd(), fileName);
2761
+ return resolvePath3(io2.cwd(), `antpath-run-${runId}.zip`);
2709
2762
  }
2710
2763
 
2711
2764
  // dist/host/cancel.js
@@ -2815,6 +2868,9 @@ async function dispatch(io2, args) {
2815
2868
  case "events":
2816
2869
  return runEventsCmd(io2, rest);
2817
2870
  case "outputs":
2871
+ if (rest[0] === "sync") {
2872
+ return runOutputsSyncCmd(io2, rest.slice(1));
2873
+ }
2818
2874
  return runOutputsCmd(io2, rest);
2819
2875
  case "download":
2820
2876
  return runDownloadCmd(io2, rest);
@@ -2864,7 +2920,7 @@ Protocol version: ${manifest.protocolVersion}
2864
2920
  io2.stdout(" antpath status <run-id> --api-token T\n");
2865
2921
  io2.stdout(" antpath events <run-id> [--follow] --api-token T\n");
2866
2922
  io2.stdout(" antpath outputs <run-id> --api-token T\n");
2867
- io2.stdout(" antpath download <run-id> <output-id> [--out path] --api-token T\n");
2923
+ io2.stdout(" antpath download <run-id> [--out path] --api-token T\n");
2868
2924
  io2.stdout(" antpath cancel <run-id> --api-token T\n");
2869
2925
  io2.stdout(" antpath delete <run-id> --api-token T\n");
2870
2926
  io2.stdout(" antpath whoami --api-token T\n");
@@ -2892,6 +2948,30 @@ Protocol version: ${manifest.protocolVersion}
2892
2948
  }
2893
2949
 
2894
2950
  // dist/cli.js
2951
+ async function walkDirectory(root) {
2952
+ try {
2953
+ const rootStat = await stat2(root);
2954
+ if (!rootStat.isDirectory())
2955
+ return null;
2956
+ } catch {
2957
+ return null;
2958
+ }
2959
+ const out = [];
2960
+ async function visit(dir) {
2961
+ const entries = await readdir2(dir, { withFileTypes: true });
2962
+ for (const entry of entries) {
2963
+ const full = resolvePath4(dir, entry.name);
2964
+ if (entry.isDirectory()) {
2965
+ await visit(full);
2966
+ } else if (entry.isFile()) {
2967
+ const s = await stat2(full);
2968
+ out.push({ path: full, sizeBytes: s.size });
2969
+ }
2970
+ }
2971
+ }
2972
+ await visit(root);
2973
+ return out;
2974
+ }
2895
2975
  var io = {
2896
2976
  readFile: (path) => readFile2(path, "utf8"),
2897
2977
  writeFile: (path, data) => writeFile(path, data),
@@ -2900,6 +2980,7 @@ var io = {
2900
2980
  stderr: (chunk) => process.stderr.write(chunk),
2901
2981
  exit: (code) => process.exit(code),
2902
2982
  argv: process.argv,
2903
- cwd: () => process.cwd()
2983
+ cwd: () => process.cwd(),
2984
+ walkDirectory
2904
2985
  };
2905
2986
  await runCli(io);
@@ -1 +1 @@
1
- 974924653a05ddcd4ac437ebc2c1e38f3ddc833c56c113f838f1c6df235b0473 cli.mjs
1
+ e7ee04a6c02d7ddfef1546abddda3b7031c92b8f21471a1e1cec10478a30fa50 cli.mjs