antpath 0.7.0 → 0.9.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.
@@ -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,7 +532,11 @@ __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,
539
+ getRunUnit: () => getRunUnit,
531
540
  getSkill: () => getSkill,
532
541
  listOutputs: () => listOutputs,
533
542
  listRunEvents: () => listRunEvents,
@@ -547,6 +556,9 @@ async function getRun(http, runId) {
547
556
  const result = await http.request(`/api/runs/${encodeURIComponent(runId)}`);
548
557
  return hasRun(result) ? result.run : result;
549
558
  }
559
+ async function getRunUnit(http, runId) {
560
+ return http.request(`/api/runs/${encodeURIComponent(runId)}`);
561
+ }
550
562
  async function listRunEvents(http, runId) {
551
563
  const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/events`);
552
564
  return result.events;
@@ -567,6 +579,10 @@ async function deleteRun(http, runId) {
567
579
  async function whoami(http) {
568
580
  return http.request("/api/whoami");
569
581
  }
582
+ async function downloadRunArchive(http, runId) {
583
+ const { response } = await http.download(`/api/runs/${encodeURIComponent(runId)}/download`);
584
+ return response;
585
+ }
570
586
  async function submitRunFlat(http, request) {
571
587
  return http.request("/api/runs", {
572
588
  method: "POST",
@@ -623,6 +639,18 @@ async function deleteSkill(http, skillId) {
623
639
  method: "DELETE"
624
640
  });
625
641
  }
642
+ async function findSkillByHash(http, args) {
643
+ const params = new URLSearchParams({
644
+ name: args.name,
645
+ content_hash: args.contentHash
646
+ });
647
+ const result = await http.request(`/api/skills/by-hash?${params.toString()}`);
648
+ return result.skill ?? null;
649
+ }
650
+ async function findSkillByName(http, name) {
651
+ const skills = await listSkills(http);
652
+ return skills.find((skill) => skill.name === name) ?? null;
653
+ }
626
654
  function unwrapSkill(result) {
627
655
  if (result && typeof result === "object" && "skill" in result) {
628
656
  return result.skill;
@@ -669,10 +697,6 @@ function validateProxyAuth(endpoints, auth) {
669
697
  }
670
698
  }
671
699
 
672
- // dist/internal.js
673
- var ANTPATH_INDEX_PATH = "/antpath/index.json";
674
- var ANTPATH_RUN_TOKEN_PATH = "/antpath/run-token";
675
-
676
700
  // dist/host/common.js
677
701
  var SUCCESS = { code: 0 };
678
702
  var USAGE_ERR = { code: 2 };
@@ -830,6 +854,52 @@ function takeBooleanFlag(rest, flag) {
830
854
  return { present, remaining };
831
855
  }
832
856
 
857
+ // dist/outputs-sync.js
858
+ async function runOutputsSyncCmd(io2, dirs) {
859
+ if (dirs.length === 0) {
860
+ io2.stderr("usage: antpath outputs sync <dir> [<dir> ...]\n");
861
+ return USAGE_ERR;
862
+ }
863
+ try {
864
+ await io2.readFile(ANTPATH_INDEX_PATH);
865
+ } catch {
866
+ io2.stderr("`antpath outputs sync` is an in-container internal command and cannot run on the host.\n");
867
+ return USAGE_ERR;
868
+ }
869
+ if (!io2.walkDirectory) {
870
+ io2.stderr("antpath outputs sync: walkDirectory IO is not available\n");
871
+ return RUNTIME_ERR;
872
+ }
873
+ let scanned = 0;
874
+ let missing = 0;
875
+ for (const dir of dirs) {
876
+ if (!dir.startsWith("/")) {
877
+ io2.stderr(JSON.stringify({ dir, error: "non_absolute_path", message: "skipping non-absolute output dir" }) + "\n");
878
+ missing++;
879
+ continue;
880
+ }
881
+ let entries;
882
+ try {
883
+ entries = await io2.walkDirectory(dir);
884
+ } catch (err2) {
885
+ io2.stderr(JSON.stringify({ dir, error: "walk_failed", message: err2.message ?? "walk failed" }) + "\n");
886
+ missing++;
887
+ continue;
888
+ }
889
+ if (entries === null) {
890
+ io2.stderr(JSON.stringify({ dir, error: "missing_or_unreadable" }) + "\n");
891
+ missing++;
892
+ continue;
893
+ }
894
+ for (const entry of entries) {
895
+ io2.stdout(JSON.stringify({ dir, path: entry.path, sizeBytes: entry.sizeBytes }) + "\n");
896
+ scanned++;
897
+ }
898
+ }
899
+ io2.stdout(JSON.stringify({ summary: { dirs: dirs.length, files: scanned, missing } }) + "\n");
900
+ return SUCCESS;
901
+ }
902
+
833
903
  // dist/proxy.js
834
904
  function parseProxyFlags(rest) {
835
905
  let endpointName = null;
@@ -2644,7 +2714,7 @@ async function runOutputsCmd(io2, argv) {
2644
2714
  }
2645
2715
 
2646
2716
  // dist/host/download.js
2647
- import { resolve as resolvePath3, basename as basename2 } from "node:path";
2717
+ import { resolve as resolvePath3 } from "node:path";
2648
2718
  async function runDownloadCmd(io2, argv) {
2649
2719
  if (await refuseInsideManagedRun(io2, "download"))
2650
2720
  return USAGE_ERR;
@@ -2661,51 +2731,38 @@ async function runDownloadCmd(io2, argv) {
2661
2731
  return USAGE_ERR;
2662
2732
  }
2663
2733
  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");
2734
+ if (positional.length !== 1) {
2735
+ io2.stderr("usage: antpath download <run-id> [--out path] [common flags]\n");
2666
2736
  return USAGE_ERR;
2667
2737
  }
2668
2738
  const runId = positional[0];
2669
- const outputId = positional[1];
2670
2739
  const http = makeHttpClient(io2, common.flags);
2671
- let link;
2740
+ let response;
2672
2741
  try {
2673
- link = await operations_exports.createOutputLink(http, runId, outputId);
2742
+ response = await operations_exports.downloadRunArchive(http, runId);
2674
2743
  } catch (err2) {
2675
- return emitJsonError(io2, "link_failed", err2.message ?? "create link failed", { runId, outputId });
2744
+ return emitJsonError(io2, "download_failed", err2.message ?? "download failed", { runId });
2676
2745
  }
2677
- let response;
2746
+ let bytes;
2678
2747
  try {
2679
- response = await io2.fetchImpl(link.url, { method: "GET", redirect: "follow" });
2748
+ bytes = new Uint8Array(await response.arrayBuffer());
2680
2749
  } 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 });
2750
+ return emitJsonError(io2, "download_failed", `download read failed: ${err2.message}`, { runId });
2685
2751
  }
2686
- const buffer = new Uint8Array(await response.arrayBuffer());
2687
- const destination = resolveDestination(io2, outFlag.value, outputId, link.url);
2752
+ const destination = resolveDestination(io2, outFlag.value, runId);
2688
2753
  try {
2689
- await io2.writeFile(destination, buffer);
2754
+ await io2.writeFile(destination, bytes);
2690
2755
  } catch (err2) {
2691
- return emitJsonError(io2, "write_failed", `failed to write output: ${err2.message}`, { destination });
2756
+ return emitJsonError(io2, "write_failed", `failed to write archive: ${err2.message}`, { destination });
2692
2757
  }
2693
- io2.stdout(JSON.stringify({ runId, outputId, path: destination, bytes: buffer.byteLength }) + "\n");
2758
+ io2.stdout(JSON.stringify({ runId, path: destination, bytes: bytes.byteLength }) + "\n");
2694
2759
  return SUCCESS;
2695
2760
  }
2696
- function resolveDestination(io2, out, outputId, signedUrl) {
2761
+ function resolveDestination(io2, out, runId) {
2697
2762
  if (out) {
2698
2763
  return resolvePath3(io2.cwd(), out);
2699
2764
  }
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);
2765
+ return resolvePath3(io2.cwd(), `antpath-run-${runId}.zip`);
2709
2766
  }
2710
2767
 
2711
2768
  // dist/host/cancel.js
@@ -2815,6 +2872,9 @@ async function dispatch(io2, args) {
2815
2872
  case "events":
2816
2873
  return runEventsCmd(io2, rest);
2817
2874
  case "outputs":
2875
+ if (rest[0] === "sync") {
2876
+ return runOutputsSyncCmd(io2, rest.slice(1));
2877
+ }
2818
2878
  return runOutputsCmd(io2, rest);
2819
2879
  case "download":
2820
2880
  return runDownloadCmd(io2, rest);
@@ -2864,7 +2924,7 @@ Protocol version: ${manifest.protocolVersion}
2864
2924
  io2.stdout(" antpath status <run-id> --api-token T\n");
2865
2925
  io2.stdout(" antpath events <run-id> [--follow] --api-token T\n");
2866
2926
  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");
2927
+ io2.stdout(" antpath download <run-id> [--out path] --api-token T\n");
2868
2928
  io2.stdout(" antpath cancel <run-id> --api-token T\n");
2869
2929
  io2.stdout(" antpath delete <run-id> --api-token T\n");
2870
2930
  io2.stdout(" antpath whoami --api-token T\n");
@@ -2892,6 +2952,30 @@ Protocol version: ${manifest.protocolVersion}
2892
2952
  }
2893
2953
 
2894
2954
  // dist/cli.js
2955
+ async function walkDirectory(root) {
2956
+ try {
2957
+ const rootStat = await stat2(root);
2958
+ if (!rootStat.isDirectory())
2959
+ return null;
2960
+ } catch {
2961
+ return null;
2962
+ }
2963
+ const out = [];
2964
+ async function visit(dir) {
2965
+ const entries = await readdir2(dir, { withFileTypes: true });
2966
+ for (const entry of entries) {
2967
+ const full = resolvePath4(dir, entry.name);
2968
+ if (entry.isDirectory()) {
2969
+ await visit(full);
2970
+ } else if (entry.isFile()) {
2971
+ const s = await stat2(full);
2972
+ out.push({ path: full, sizeBytes: s.size });
2973
+ }
2974
+ }
2975
+ }
2976
+ await visit(root);
2977
+ return out;
2978
+ }
2895
2979
  var io = {
2896
2980
  readFile: (path) => readFile2(path, "utf8"),
2897
2981
  writeFile: (path, data) => writeFile(path, data),
@@ -2900,6 +2984,7 @@ var io = {
2900
2984
  stderr: (chunk) => process.stderr.write(chunk),
2901
2985
  exit: (code) => process.exit(code),
2902
2986
  argv: process.argv,
2903
- cwd: () => process.cwd()
2987
+ cwd: () => process.cwd(),
2988
+ walkDirectory
2904
2989
  };
2905
2990
  await runCli(io);
@@ -1 +1 @@
1
- 974924653a05ddcd4ac437ebc2c1e38f3ddc833c56c113f838f1c6df235b0473 cli.mjs
1
+ c4520d53e89222d7073d467e376774789532d5b9a932c7ae6821e527cb718e0e cli.mjs
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { HttpClient, type FetchLike, type Output, type PlatformFlatRunSubmissionInput, type PlatformFlatSubmission, type PlatformInlineSecrets, type PlatformProxyEndpoint, type PlatformProxyEndpointAuth, type Run, type RunEvent, type SignedOutputLink, type Skill as SkillRecord, type WhoAmI } from "./_shared/index.js";
1
+ import { HttpClient, type FetchLike, type Output, type PlatformFlatRunSubmissionInput, type PlatformFlatSubmission, type PlatformInlineSecrets, type PlatformProxyEndpoint, type PlatformProxyEndpointAuth, type Run, type RunEvent, type RunUnit, type SignedOutputLink, type Skill as SkillRecord, type WhoAmI } from "./_shared/index.js";
2
2
  import type { Blueprint } from "./blueprint.js";
3
3
  import { McpServer } from "./mcp-server.js";
4
4
  import { ProxyEndpoint } from "./proxy-endpoint.js";
@@ -51,6 +51,21 @@ export interface SubmitRunOptions {
51
51
  readonly metadata?: PlatformFlatSubmission["metadata"];
52
52
  readonly cleanup?: PlatformFlatRunSubmissionInput["cleanup"];
53
53
  readonly proxyEndpoints?: readonly ProxyEndpoint[];
54
+ /**
55
+ * Container paths to capture as `output_objects` at session terminal.
56
+ *
57
+ * - Omitted: run metadata (status/events/snapshots/cleanup state) is
58
+ * still persisted, but no container file bytes are captured.
59
+ * - Present: the worker drives an agent-side sync at session terminal
60
+ * that registers every file under these paths via the Anthropic
61
+ * Files API. The bytes land in private Supabase Storage and can be
62
+ * retrieved via `RunRef.outputs()` / `RunRef.download()`.
63
+ *
64
+ * Paths are absolute UNIX paths (start with `/`), max 32 entries,
65
+ * max 512 bytes per entry, no `..` segments, no NUL bytes. See
66
+ * `packages/sdk/docs/outputs.md` for the full contract.
67
+ */
68
+ readonly outputDirs?: readonly string[];
54
69
  readonly secrets: PlatformInlineSecrets;
55
70
  readonly idempotencyKey?: string;
56
71
  readonly signal?: AbortSignal;
@@ -74,10 +89,34 @@ export declare class RunRef {
74
89
  readonly runId: string;
75
90
  constructor(client: AntpathClient, runId: string);
76
91
  get(): Promise<Run>;
92
+ /** Convenience wrapper for `AntpathClient.getRunUnit`. */
93
+ getUnit(): Promise<RunUnit>;
77
94
  events(): Promise<readonly RunEvent[]>;
78
95
  stream(options?: StreamEventsOptions): AsyncIterable<RunEvent>;
79
96
  wait(options?: WaitForRunOptions): Promise<Run>;
80
97
  outputs(): Promise<readonly Output[]>;
98
+ /**
99
+ * Download the per-run archive zip. Lifecycle-aware in one method:
100
+ *
101
+ * - Pre-session (run still `queued`): rejects with a 409
102
+ * `run_not_started` error. Poll `RunRef.get()` until the status
103
+ * leaves `queued` before retrying.
104
+ * - Mid-session: returns whatever has been captured so far. The
105
+ * archive's `manifest.json` carries `partial: true` and a
106
+ * `missing[]` entry advising the caller to redownload after
107
+ * terminal for the full archive.
108
+ * - Terminal: returns the complete archive sourced from durable
109
+ * Supabase Storage. `manifest.json` carries `partial: false`.
110
+ *
111
+ * Pass `to` to write the archive zip to a file path; omit it to
112
+ * receive the raw streaming `Response` (use this for piping to a
113
+ * custom sink). `to` resolves through Node's `fs/promises` and is
114
+ * not available in browsers — browser callers omit `to` and pipe
115
+ * `Response.body` themselves.
116
+ */
117
+ download(options?: {
118
+ readonly to?: string;
119
+ }): Promise<Response>;
81
120
  cancel(): Promise<void>;
82
121
  delete(): Promise<void>;
83
122
  }
@@ -99,6 +138,31 @@ export declare class SkillsClient {
99
138
  list(): Promise<readonly SkillRecord[]>;
100
139
  get(skillId: string): Promise<SkillRecord>;
101
140
  delete(skillId: string): Promise<void>;
141
+ /**
142
+ * Lookup a live workspace skill by `(name, contentHash)`.
143
+ *
144
+ * Returns the matching `Skill` record or `null` when no live row
145
+ * carries that hash. The `contentHash` is the wire format
146
+ * `sha256:<hex>` returned by `hashSkillBundle` (and stored verbatim
147
+ * on every skill row). The hash space is unique enough that one
148
+ * row at most can match, so this is a single keyed lookup.
149
+ *
150
+ * Powers `Skill.uploadIfChanged(client)` internally; consumers can
151
+ * also call it directly when they already have a hash in hand and
152
+ * want to know whether the skill is already persisted.
153
+ */
154
+ findByHash(args: {
155
+ readonly name: string;
156
+ readonly contentHash: string;
157
+ }): Promise<SkillRecord | null>;
158
+ /**
159
+ * Lookup a live workspace skill by `name`. Returns the matching
160
+ * `Skill` record or `null` when no live row carries that name.
161
+ * Implemented as a list-and-filter against the existing `/api/skills`
162
+ * endpoint — typical workspace skill counts are small enough that
163
+ * the cost is negligible.
164
+ */
165
+ findByName(name: string): Promise<SkillRecord | null>;
102
166
  /**
103
167
  * Internal: post a pre-bundled skill zip to the BFF. Only
104
168
  * `Skill.upload` calls this. NOT part of the public API.
@@ -157,6 +221,16 @@ export declare class AntpathClient {
157
221
  */
158
222
  submitRun(options: SubmitRunOptions): Promise<RunRef>;
159
223
  getRun(runId: string): Promise<Run>;
224
+ /**
225
+ * Fetch the self-contained `RunUnit`: parsed submission inputs,
226
+ * attempts, indexed events (inline + cursor for the tail), raw
227
+ * provider-event Storage manifest, outputs, capture failures,
228
+ * proxy-call audit, pinned workspace skills, provider skills,
229
+ * transient skills. Backed by the same endpoint as `getRun` but
230
+ * typed against the full wire shape — use this when you need
231
+ * fields beyond `{id, status, timestamps, usage}`.
232
+ */
233
+ getRunUnit(runId: string): Promise<RunUnit>;
160
234
  listEvents(runId: string): Promise<readonly RunEvent[]>;
161
235
  /**
162
236
  * Poll the events endpoint and yield new events as they arrive. Stops
@@ -174,5 +248,12 @@ export declare class AntpathClient {
174
248
  cancelRun(runId: string): Promise<void>;
175
249
  deleteRun(runId: string): Promise<void>;
176
250
  whoami(): Promise<WhoAmI>;
251
+ /**
252
+ * Stream the per-run archive zip body. Returned `Response` body is
253
+ * `application/zip` and may be piped directly to disk; the SDK never
254
+ * buffers the archive. Callers requiring a "write to a path" verb
255
+ * should use `RunRef.download({ to })`.
256
+ */
257
+ downloadRunArchive(runId: string): Promise<Response>;
177
258
  }
178
259
  export type { Blueprint, PlatformProxyEndpoint, PlatformProxyEndpointAuth };
package/dist/client.js CHANGED
@@ -17,6 +17,10 @@ export class RunRef {
17
17
  get() {
18
18
  return this.#client.getRun(this.runId);
19
19
  }
20
+ /** Convenience wrapper for `AntpathClient.getRunUnit`. */
21
+ getUnit() {
22
+ return this.#client.getRunUnit(this.runId);
23
+ }
20
24
  events() {
21
25
  return this.#client.listEvents(this.runId);
22
26
  }
@@ -29,6 +33,34 @@ export class RunRef {
29
33
  outputs() {
30
34
  return this.#client.listOutputs(this.runId);
31
35
  }
36
+ /**
37
+ * Download the per-run archive zip. Lifecycle-aware in one method:
38
+ *
39
+ * - Pre-session (run still `queued`): rejects with a 409
40
+ * `run_not_started` error. Poll `RunRef.get()` until the status
41
+ * leaves `queued` before retrying.
42
+ * - Mid-session: returns whatever has been captured so far. The
43
+ * archive's `manifest.json` carries `partial: true` and a
44
+ * `missing[]` entry advising the caller to redownload after
45
+ * terminal for the full archive.
46
+ * - Terminal: returns the complete archive sourced from durable
47
+ * Supabase Storage. `manifest.json` carries `partial: false`.
48
+ *
49
+ * Pass `to` to write the archive zip to a file path; omit it to
50
+ * receive the raw streaming `Response` (use this for piping to a
51
+ * custom sink). `to` resolves through Node's `fs/promises` and is
52
+ * not available in browsers — browser callers omit `to` and pipe
53
+ * `Response.body` themselves.
54
+ */
55
+ async download(options) {
56
+ const response = await this.#client.downloadRunArchive(this.runId);
57
+ if (options?.to !== undefined) {
58
+ const buffer = new Uint8Array(await response.arrayBuffer());
59
+ const { writeFile } = await import("node:fs/promises");
60
+ await writeFile(options.to, buffer);
61
+ }
62
+ return response;
63
+ }
32
64
  cancel() {
33
65
  return this.#client.cancelRun(this.runId);
34
66
  }
@@ -62,6 +94,32 @@ export class SkillsClient {
62
94
  delete(skillId) {
63
95
  return operations.deleteSkill(this.#http, skillId);
64
96
  }
97
+ /**
98
+ * Lookup a live workspace skill by `(name, contentHash)`.
99
+ *
100
+ * Returns the matching `Skill` record or `null` when no live row
101
+ * carries that hash. The `contentHash` is the wire format
102
+ * `sha256:<hex>` returned by `hashSkillBundle` (and stored verbatim
103
+ * on every skill row). The hash space is unique enough that one
104
+ * row at most can match, so this is a single keyed lookup.
105
+ *
106
+ * Powers `Skill.uploadIfChanged(client)` internally; consumers can
107
+ * also call it directly when they already have a hash in hand and
108
+ * want to know whether the skill is already persisted.
109
+ */
110
+ findByHash(args) {
111
+ return operations.findSkillByHash(this.#http, args);
112
+ }
113
+ /**
114
+ * Lookup a live workspace skill by `name`. Returns the matching
115
+ * `Skill` record or `null` when no live row carries that name.
116
+ * Implemented as a list-and-filter against the existing `/api/skills`
117
+ * endpoint — typical workspace skill counts are small enough that
118
+ * the cost is negligible.
119
+ */
120
+ findByName(name) {
121
+ return operations.findSkillByName(this.#http, name);
122
+ }
65
123
  /**
66
124
  * Internal: post a pre-bundled skill zip to the BFF. Only
67
125
  * `Skill.upload` calls this. NOT part of the public API.
@@ -156,7 +214,10 @@ export class AntpathClient {
156
214
  skills: skillRefs,
157
215
  mcpServers: submissionMcpServers,
158
216
  ...(options.environment ? { environment: options.environment } : {}),
159
- ...(options.metadata ? { metadata: options.metadata } : {})
217
+ ...(options.metadata ? { metadata: options.metadata } : {}),
218
+ ...(options.outputDirs && options.outputDirs.length > 0
219
+ ? { outputDirs: options.outputDirs }
220
+ : {})
160
221
  };
161
222
  const secrets = {
162
223
  ...options.secrets,
@@ -180,6 +241,18 @@ export class AntpathClient {
180
241
  getRun(runId) {
181
242
  return operations.getRun(this.#http, runId);
182
243
  }
244
+ /**
245
+ * Fetch the self-contained `RunUnit`: parsed submission inputs,
246
+ * attempts, indexed events (inline + cursor for the tail), raw
247
+ * provider-event Storage manifest, outputs, capture failures,
248
+ * proxy-call audit, pinned workspace skills, provider skills,
249
+ * transient skills. Backed by the same endpoint as `getRun` but
250
+ * typed against the full wire shape — use this when you need
251
+ * fields beyond `{id, status, timestamps, usage}`.
252
+ */
253
+ getRunUnit(runId) {
254
+ return operations.getRunUnit(this.#http, runId);
255
+ }
183
256
  listEvents(runId) {
184
257
  return operations.listRunEvents(this.#http, runId);
185
258
  }
@@ -241,6 +314,15 @@ export class AntpathClient {
241
314
  whoami() {
242
315
  return operations.whoami(this.#http);
243
316
  }
317
+ /**
318
+ * Stream the per-run archive zip body. Returned `Response` body is
319
+ * `application/zip` and may be piped directly to disk; the SDK never
320
+ * buffers the archive. Callers requiring a "write to a path" verb
321
+ * should use `RunRef.download({ to })`.
322
+ */
323
+ downloadRunArchive(runId) {
324
+ return operations.downloadRunArchive(this.#http, runId);
325
+ }
244
326
  }
245
327
  const TERMINAL_STATUSES = new Set([
246
328
  "succeeded",