@buildinternet/uploads 0.4.0 → 0.6.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/README.md CHANGED
@@ -4,15 +4,16 @@ CLI and client for **uploads.sh** — upload files, get public URLs, and produce
4
4
 
5
5
  ## CLI
6
6
 
7
- Binary: **`uploads`**. Install globally (or use a pinned `npx` one-shot):
7
+ Binary: **`uploads`**. Install globally (or use an `npx` one-shot):
8
8
 
9
9
  ```bash
10
10
  npm install --global @buildinternet/uploads
11
- npx @buildinternet/uploads@0.1.0 --help
11
+ npx @buildinternet/uploads --help
12
12
  ```
13
13
 
14
14
  ```bash
15
15
  uploads setup
16
+ uploads --version
16
17
  uploads attach ./before.png ./after.png
17
18
  uploads put ./shot.png
18
19
  uploads put ./shot.png --destination screenshots
@@ -20,6 +21,8 @@ uploads put ./shot.png --no-optimize
20
21
  uploads put ./mobile.png --frame phone
21
22
  uploads put ./ui.png --frame browser --frame-url "https://app.example"
22
23
  uploads put ./after.png --pr 123 --comment
24
+ uploads gallery create --title "Release screenshots"
25
+ uploads put ./after.png --gallery gal_example
23
26
  uploads doctor
24
27
  ```
25
28
 
@@ -27,12 +30,20 @@ Inside this monorepo only, `pnpm uploads …` builds the package first so you pi
27
30
  up local source; product docs and PR “how to try it” examples should use the
28
31
  global `uploads` form above.
29
32
 
30
- Commands: `attach`, `put`, `comment`, `list`, `delete`, `usage`, `reconcile`,
33
+ Commands: `attach`, `put`, `gallery`, `comment`, `list`, `delete`, `usage`, `reconcile`,
31
34
  `purge-expired`, `setup`, `install`, `config`, `doctor`, `health`, `mcp`.
32
35
 
36
+ **Globals (before the command):** `--api-url`, `--token`, `--workspace` / `-w`,
37
+ `--env-file`, `--json`, `--quiet`, `--version` / `-V`, `-h` / `--help`.
38
+
39
+ **Update hints:** after a successful run the CLI may print one stderr line when a
40
+ newer npm release is available (at most once/day, `~/.cache/uploads/`). Silence
41
+ with `--quiet`, `--json`, `UPLOADS_NO_UPDATE=1`, or `NO_UPDATE_NOTIFIER=1`. Not used
42
+ for `uploads mcp`.
43
+
33
44
  `attach` is the agent-friendly default for GitHub media. It accepts one or more files,
34
45
  infers the pull request for the current branch via `gh`, uploads stable URLs, and creates
35
- or updates one managed attachments comment. Use `--pr`, `--issue`, and `--repo` to select
46
+ or updates one marker-owned GitHub comment. It keeps loose `gh/...` attachments and linked public galleries in distinct sections, shows up to three available gallery images inline, and updates that same comment in place on every sync. Use `--pr`, `--issue`, and `--repo` to select
36
47
  the target explicitly, or `--no-comment` to upload without changing GitHub comments.
37
48
 
38
49
  **Keys / destinations:** default put uses the `screenshots` layout. Typed destinations
@@ -50,11 +61,33 @@ stripped**. Pass `--keep-exif` / `UPLOADS_KEEP_EXIF=1` to preserve image metadat
50
61
  community art from [device-frames-media](https://github.com/jonnyjackson26/device-frames-media)
51
62
  into `~/.cache/uploads/frames` (not bundled).
52
63
 
64
+ ## Public galleries
65
+
66
+ Create an ordered gallery, then add existing uploads by key. The API returns the canonical
67
+ public URL; the CLI never constructs it. **Anyone who knows that URL can view the gallery and
68
+ its media**—GitHub or repository visibility does not restrict it. Deleting a gallery removes
69
+ only the gallery record, not its uploaded objects or their retention policy. A workspace can hold up to 100 active galleries, each with up to 100 items and 20 external references.
70
+
71
+ ```bash
72
+ uploads gallery create --title "Release screenshots"
73
+ uploads gallery add gal_example screenshots/myapp/42/after.webp --alt "Updated dashboard"
74
+ uploads put ./before.png --gallery gal_example
75
+ uploads gallery show gal_example
76
+ uploads gallery link gal_example --github buildinternet/uploads#58
77
+ uploads gallery list --github https://github.com/buildinternet/uploads/pull/58
78
+ ```
79
+
80
+ When adding several keys, `uploads gallery add` processes them sequentially and reports any
81
+ individual failures in `--json` output. Gallery item updates use the API's current version to
82
+ avoid overwriting concurrent changes.
83
+
84
+ Link a gallery to a GitHub issue or pull request with `gallery link --github`. Run `uploads comment --pr <number>` (or use `put --comment`) to refresh that target’s one managed comment with every linked gallery and loose attachment. Coordinates and strict `https://github.com/<owner>/<repo>/issues|pull/<number>` URLs are accepted; `gallery list --github` performs the authenticated reverse lookup. Links never change gallery identity, and GitHub repository visibility does not make the public gallery private.
85
+
53
86
  Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
54
87
 
55
88
  ## MCP server
56
89
 
57
- `uploads mcp` serves the Model Context Protocol over stdio (newline-delimited JSON-RPC, no extra dependencies). Tools mirror the CLI commands one-to-one `put`, `attach`, `list`, `delete`, `usage`, `reconcile`, `purge_expired`, `comment`, `health`, `doctor` — with the same config resolution and defaults, plus a per-call `workspace` argument. Interactive/credential commands (`setup`, `login`, `admin`, `config`) are not exposed. A token isn't required to start the server; auth errors surface per tool call (`health` needs no auth).
90
+ `uploads mcp` serves the Model Context Protocol over stdio (newline-delimited JSON-RPC, no extra dependencies). Tools include file operations plus public gallery workflows: `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`. Gallery tools return API-provided canonical URLs and never need GitHub credentials. The remaining stdio tools are `put`, `attach`, `list`, `delete`, `usage`, `reconcile`, `purge_expired`, `comment`, `health`, and `doctor` — with the same config resolution and defaults, plus a per-call `workspace` argument. Interactive/credential commands (`setup`, `login`, `admin`, `config`) are not exposed. A token isn't required to start the server; auth errors surface per tool call (`health` needs no auth).
58
91
 
59
92
  ```json
60
93
  { "command": "uploads", "args": ["--env-file", "/path/to/.env", "mcp"] }
@@ -62,7 +95,7 @@ Config layers (first match wins): CLI flags → env vars → `--env-file` → `~
62
95
 
63
96
  Or with `UPLOADS_TOKEN`/`UPLOADS_WORKSPACE` in the environment or user config. Claude Code: `claude mcp add uploads -- uploads --env-file /path/to/.env mcp`.
64
97
 
65
- For HTTP clients there's also a hosted variant at `https://agents.uploads.sh/mcp` — the workspace is inferred from the bearer token, so only the URL and token are needed (`https://agents.uploads.sh/<workspace>/mcp` and the `mcp.uploads.sh` hostname also work). Tools: put/list/delete/health, same bearer tokens as the REST API — see `apps/mcp` in the repo. `uploads install` registers it with Claude Code (and installs the agent skill) in one step. Its `put` takes no content type: the stored type is sniffed server-side from the bytes and checked against the workspace allowlist, and writes are rate limited per workspace.
98
+ For HTTP clients there's also a hosted variant at `https://agents.uploads.sh/mcp` — the workspace is inferred from the bearer token, so only the URL and token are needed (`https://agents.uploads.sh/<workspace>/mcp` and the `mcp.uploads.sh` hostname also work). Tools: file operations plus `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`; all use the same bearer-token workspace scopes and gallery URLs come from the API — see `apps/mcp` in the repo. `uploads install` registers it with Claude Code (and installs the agent skill) in one step. Its `put` takes no content type: the stored type is sniffed server-side from the bytes and checked against the workspace allowlist, and writes are rate limited per workspace.
66
99
 
67
100
  ## Programmatic use
68
101
 
@@ -76,14 +109,16 @@ Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools
76
109
 
77
110
  ```
78
111
  src/
79
- cli.ts Entry + help
80
- commands.ts put, list, delete, comment,
81
- commands/mcp.ts `mcp` command entry
82
- mcp/ Stdio MCP server (server.ts, tools.ts)
83
- client.ts HTTP client for the API
84
- github.ts PR/issue key paths + attachment comments
85
- embed.ts Markdown image output
86
- bin/uploads.js Bin shim
112
+ cli.ts Entry + help
113
+ package-version.ts Shared version for --version / doctor / headers
114
+ update-check.ts Optional npm update hint (stderr)
115
+ commands.ts put, list, delete, comment,
116
+ commands/mcp.ts `mcp` command entry
117
+ mcp/ Stdio MCP server (server.ts, tools.ts)
118
+ client.ts HTTP client for the API
119
+ github.ts PR/issue key paths + attachment comments
120
+ embed.ts Markdown image output
121
+ bin/uploads.js Bin shim
87
122
  ```
88
123
 
89
124
  ## Commands
package/dist/agent.js CHANGED
@@ -15,7 +15,7 @@ export function createUploadsWorkerFileTools(files, opts = {}) {
15
15
  },
16
16
  overrides: {
17
17
  uploadFile: {
18
- description: "Upload a file for public hosting (e.g. GitHub embeds). Prefer keys under screenshots/.",
18
+ description: "Upload a non-sensitive file for public hosting (e.g. GitHub embeds). GitHub repository visibility does not restrict access; prefer keys under screenshots/.",
19
19
  },
20
20
  ...overrides,
21
21
  },
@@ -5,6 +5,8 @@ export interface GlobalFlags {
5
5
  envFile?: string;
6
6
  json?: boolean;
7
7
  quiet?: boolean;
8
+ /** `--version` / `-V` — print package version and exit. */
9
+ version?: boolean;
8
10
  }
9
11
  export interface ParsedArgv {
10
12
  globals: GlobalFlags;
package/dist/cli-args.js CHANGED
@@ -28,6 +28,11 @@ export function parseArgv(argv) {
28
28
  i++;
29
29
  continue;
30
30
  }
31
+ if (arg === "--version" || arg === "-V") {
32
+ globals.version = true;
33
+ i++;
34
+ continue;
35
+ }
31
36
  if (VALUE_GLOBALS.has(arg)) {
32
37
  const value = args[i + 1];
33
38
  if (!value || value.startsWith("-")) {
package/dist/cli.js CHANGED
@@ -2,13 +2,15 @@ import { createUploadsClient } from "./client.js";
2
2
  import { resolveApiUrl, resolveConfig } from "./config.js";
3
3
  import { UploadsError } from "./errors.js";
4
4
  import { commandWorkspace, isHelpFlag, parseArgv, parseCommandArgs, UsageError, } from "./cli-args.js";
5
- import { runPut, runAttach, runList, runDelete, runHealth, runDoctor, runComment, runUsage, runReconcile, runPurgeExpired, } from "./commands.js";
5
+ import { runPut, runAttach, runList, runDelete, runHealth, runDoctor, runComment, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
6
6
  import { runConfig } from "./commands/config.js";
7
7
  import { runSetup } from "./commands/setup.js";
8
8
  import { runLogin } from "./commands/login.js";
9
9
  import { runAdmin } from "./commands/admin-enrollment.js";
10
10
  import { runMcp } from "./commands/mcp.js";
11
11
  import { runInstall } from "./commands/install.js";
12
+ import { packageVersion } from "./package-version.js";
13
+ import { maybeHintUpdate } from "./update-check.js";
12
14
  const ROOT_HELP = `uploads — CLI for uploads.sh (GitHub image embeds)
13
15
 
14
16
  Usage:
@@ -31,11 +33,13 @@ Other globals (before command):
31
33
  --token <token> or UPLOADS_TOKEN
32
34
  --env-file <path>
33
35
  --json JSON on stdout
34
- --quiet
36
+ --quiet Suppress stderr progress and update hints
37
+ --version, -V Print package version and exit
35
38
 
36
39
  Commands:
37
40
  attach <file...> Attach media to the current PR (stable URLs + managed comment)
38
41
  put <file> Upload (+ URL + markdown for GitHub)
42
+ gallery Create and organize public media galleries
39
43
  comment Create/update a PR/issue attachments comment (via gh)
40
44
  list List objects
41
45
  delete <key> Delete object
@@ -45,7 +49,7 @@ Commands:
45
49
  setup Inspect/configure advanced CLI settings
46
50
  install Install the agent skill + register the remote MCP server
47
51
  login Exchange an enrollment code and configure credentials
48
- admin Admin enrollment management
52
+ admin Admin invitation management
49
53
  config Show path, init, or set shared config
50
54
  doctor Health + auth + workspace checks
51
55
  health API liveness (no auth)
@@ -55,12 +59,16 @@ Put/list defaults (config file or env):
55
59
  UPLOADS_DEFAULT_PREFIX, UPLOADS_DEFAULT_REPO, UPLOADS_DEFAULT_REF
56
60
  UPLOADS_DEFAULT_WIDTH, UPLOADS_NO_GIT
57
61
 
62
+ Update hints (stderr, once/day): silence with --quiet / UPLOADS_NO_UPDATE=1 / NO_UPDATE_NOTIFIER=1
63
+
58
64
  Examples:
59
65
  uploads setup
60
66
  uploads setup --token up_default_… --repo myorg/myapp
61
67
  uploads attach ./before.png ./after.png
62
68
  uploads put ./shot.png --ref 42
69
+ uploads gallery create --title "Release screenshots"
63
70
  uploads doctor
71
+ uploads --version
64
72
 
65
73
  Agent/MCP: \`uploads install\` sets up the agent skill and the hosted MCP server
66
74
  (https://agents.uploads.sh/mcp, workspace inferred from the token). Run
@@ -133,33 +141,57 @@ function errorOut(err, json) {
133
141
  }
134
142
  }
135
143
  }
144
+ /** Point agents at layered --help instead of dumping the full root manual. */
145
+ function usageHint(argv) {
146
+ try {
147
+ const cmd = parseArgv(argv).command;
148
+ process.stderr.write(cmd ? `hint: uploads ${cmd} --help\n` : "hint: uploads --help\n");
149
+ }
150
+ catch {
151
+ process.stderr.write("hint: uploads --help\n");
152
+ }
153
+ }
136
154
  export async function runCli(argv) {
137
155
  try {
138
156
  const parsed = parseArgv(argv);
139
157
  const json = parsed.globals.json ?? false;
158
+ const quiet = parsed.globals.quiet ?? false;
159
+ if (parsed.globals.version) {
160
+ process.stdout.write(`${packageVersion()}\n`);
161
+ return 0;
162
+ }
140
163
  if (!parsed.command) {
141
164
  process.stderr.write(ROOT_HELP);
142
165
  return parsed.help ? 0 : 2;
143
166
  }
144
167
  const cmdArgs = parsed.rest.slice(1);
145
168
  const showHelp = parsed.help || cmdArgs.some(isHelpFlag);
169
+ let code;
146
170
  switch (parsed.command) {
147
171
  case "health":
148
- return runHealth({ apiUrl: resolveApiUrl(parsed.globals), json }, cmdArgs, showHelp);
172
+ code = await runHealth({ apiUrl: resolveApiUrl(parsed.globals), json }, cmdArgs, showHelp);
173
+ break;
149
174
  case "config":
150
- return runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
175
+ code = await runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
176
+ break;
151
177
  case "setup":
152
- return runSetup(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
178
+ code = await runSetup(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
179
+ break;
153
180
  case "login":
154
- return runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
181
+ code = await runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
182
+ break;
155
183
  case "admin":
156
- return runAdmin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
184
+ code = await runAdmin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
185
+ break;
157
186
  case "mcp":
158
- return runMcp(cmdArgs, { globals: parsed.globals }, showHelp);
187
+ code = await runMcp(cmdArgs, { globals: parsed.globals }, showHelp);
188
+ break;
159
189
  case "install":
160
- return runInstall(cmdArgs, { globals: parsed.globals, json }, showHelp);
190
+ code = await runInstall(cmdArgs, { globals: parsed.globals, json }, showHelp);
191
+ break;
161
192
  case "attach":
162
193
  case "put":
194
+ case "gallery":
163
195
  case "list":
164
196
  case "delete":
165
197
  case "usage":
@@ -170,34 +202,52 @@ export async function runCli(argv) {
170
202
  const ctx = createContext(parsed.globals, !showHelp, cmdArgs);
171
203
  switch (parsed.command) {
172
204
  case "attach":
173
- return runAttach(ctx, cmdArgs, showHelp);
205
+ code = await runAttach(ctx, cmdArgs, showHelp);
206
+ break;
174
207
  case "put":
175
- return runPut(ctx, cmdArgs, showHelp);
208
+ code = await runPut(ctx, cmdArgs, showHelp);
209
+ break;
210
+ case "gallery":
211
+ code = await runGallery(ctx, cmdArgs, showHelp);
212
+ break;
176
213
  case "comment":
177
- return runComment(ctx, cmdArgs, showHelp);
214
+ code = await runComment(ctx, cmdArgs, showHelp);
215
+ break;
178
216
  case "list":
179
- return runList(ctx, cmdArgs, showHelp);
217
+ code = await runList(ctx, cmdArgs, showHelp);
218
+ break;
180
219
  case "delete":
181
- return runDelete(ctx, cmdArgs, showHelp);
220
+ code = await runDelete(ctx, cmdArgs, showHelp);
221
+ break;
182
222
  case "usage":
183
- return runUsage(ctx, cmdArgs, showHelp);
223
+ code = await runUsage(ctx, cmdArgs, showHelp);
224
+ break;
184
225
  case "reconcile":
185
- return runReconcile(ctx, cmdArgs, showHelp);
226
+ code = await runReconcile(ctx, cmdArgs, showHelp);
227
+ break;
186
228
  case "purge-expired":
187
- return runPurgeExpired(ctx, cmdArgs, showHelp);
229
+ code = await runPurgeExpired(ctx, cmdArgs, showHelp);
230
+ break;
188
231
  case "doctor":
189
- return runDoctor(ctx, cmdArgs, showHelp);
232
+ code = await runDoctor(ctx, cmdArgs, showHelp);
233
+ break;
190
234
  }
235
+ break;
191
236
  }
192
237
  default:
193
238
  process.stderr.write(`unknown command: ${parsed.command}\n\n${ROOT_HELP}`);
194
239
  return 2;
195
240
  }
241
+ // Best-effort; skipped for mcp, --quiet/--json, and opt-out env vars.
242
+ if (code === 0 && !showHelp) {
243
+ await maybeHintUpdate({ quiet: quiet || json, command: parsed.command });
244
+ }
245
+ return code;
196
246
  }
197
247
  catch (err) {
198
248
  errorOut(err, argv.includes("--json"));
199
249
  if (err instanceof UsageError && !argv.includes("--json"))
200
- process.stderr.write(`\n${ROOT_HELP}`);
250
+ usageHint(argv);
201
251
  return exitCode(err);
202
252
  }
203
253
  }
package/dist/client.d.ts CHANGED
@@ -53,6 +53,79 @@ export interface DeleteResult {
53
53
  key: string;
54
54
  deleted: boolean;
55
55
  }
56
+ /** A workspace-owned, publicly visible ordered media gallery. */
57
+ export interface GalleryItem {
58
+ id: string;
59
+ objectKey: string;
60
+ position: number;
61
+ caption: string | null;
62
+ altText: string | null;
63
+ createdAt: string;
64
+ status: "available" | "missing";
65
+ url: string | null;
66
+ contentType: string | null;
67
+ size: number | null;
68
+ }
69
+ export interface Gallery {
70
+ id: string;
71
+ /** Canonical public URL returned by the API; clients must not construct it. */
72
+ url: string;
73
+ workspace: string;
74
+ title: string;
75
+ description: string | null;
76
+ visibility: "public";
77
+ coverItemId: string | null;
78
+ version: number;
79
+ createdAt: string;
80
+ updatedAt: string;
81
+ items: GalleryItem[];
82
+ }
83
+ export type GallerySummary = Omit<Gallery, "items">;
84
+ export interface GalleryListOptions {
85
+ limit?: number;
86
+ cursor?: string;
87
+ }
88
+ export interface GalleryListResult {
89
+ galleries: GallerySummary[];
90
+ nextCursor: string | null;
91
+ }
92
+ export interface CreateGalleryOptions {
93
+ title: string;
94
+ description?: string | null;
95
+ }
96
+ export interface AddGalleryItemOptions {
97
+ expectedVersion: number;
98
+ caption?: string | null;
99
+ altText?: string | null;
100
+ }
101
+ export interface DeleteGalleryOptions {
102
+ expectedVersion: number;
103
+ }
104
+ export interface GalleryExternalReference {
105
+ id: string;
106
+ provider: "github";
107
+ resourceType: "item";
108
+ coordinate: string;
109
+ canonicalUrl: string | null;
110
+ createdAt: string;
111
+ }
112
+ export interface GalleryExternalReferenceListResult {
113
+ references: GalleryExternalReference[];
114
+ }
115
+ export interface LinkGalleryExternalReferenceOptions {
116
+ expectedVersion: number;
117
+ provider: "github";
118
+ coordinate: string;
119
+ }
120
+ export interface UnlinkGalleryExternalReferenceOptions {
121
+ expectedVersion: number;
122
+ }
123
+ export interface FindGalleriesByReferenceOptions {
124
+ provider: "github";
125
+ coordinate: string;
126
+ limit?: number;
127
+ cursor?: string;
128
+ }
56
129
  export interface HealthResult {
57
130
  ok: boolean;
58
131
  }
@@ -101,14 +174,17 @@ export interface EnrollmentExchangeResult {
101
174
  expiresAt?: string;
102
175
  }
103
176
  export interface EnrollmentCreateResult {
177
+ pageId: string;
104
178
  code: string;
105
179
  expiresAt: string;
106
180
  tokenExpiresAt: string;
181
+ emailed?: boolean;
107
182
  }
108
183
  export declare function exchangeEnrollment(apiUrl: string, code: string): Promise<EnrollmentExchangeResult>;
109
184
  export declare function createEnrollment(apiUrl: string, adminToken: string, input: {
110
185
  workspace?: string;
111
186
  label?: string;
187
+ email?: string;
112
188
  enrollmentSeconds?: number;
113
189
  tokenExpiresInSeconds?: number;
114
190
  scopes?: Array<"files:read" | "files:write" | "files:delete">;
@@ -124,6 +200,21 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
124
200
  }): Promise<ListItem[]>;
125
201
  delete(key: string): Promise<DeleteResult>;
126
202
  head(key: string): Promise<HeadResult>;
203
+ createGallery(opts: CreateGalleryOptions): Promise<Gallery>;
204
+ getGallery(id: string): Promise<Gallery>;
205
+ listGalleries(opts?: GalleryListOptions): Promise<GalleryListResult>;
206
+ deleteGallery(id: string, opts: DeleteGalleryOptions): Promise<{
207
+ deleted: boolean;
208
+ id: string;
209
+ }>;
210
+ addGalleryItem(id: string, objectKey: string, opts: AddGalleryItemOptions): Promise<GalleryItem>;
211
+ listGalleryExternalReferences(id: string): Promise<GalleryExternalReferenceListResult>;
212
+ linkGalleryExternalReference(id: string, opts: LinkGalleryExternalReferenceOptions): Promise<GalleryExternalReference>;
213
+ unlinkGalleryExternalReference(id: string, referenceId: string, opts: UnlinkGalleryExternalReferenceOptions): Promise<{
214
+ deleted: boolean;
215
+ id: string;
216
+ }>;
217
+ findGalleriesByReference(opts: FindGalleriesByReferenceOptions): Promise<GalleryListResult>;
127
218
  health(): Promise<HealthResult>;
128
219
  /** Workspace storage / upload counters (+ limits when configured). */
129
220
  usage(): Promise<UsageResult>;
package/dist/client.js CHANGED
@@ -36,21 +36,24 @@ function filesBase(config) {
36
36
  function usageBase(config) {
37
37
  return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/usage`;
38
38
  }
39
+ function galleriesBase(config) {
40
+ return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/galleries`;
41
+ }
39
42
  function mapApiError(status, error, code) {
40
43
  const normalized = error.toLowerCase();
41
- if (status === 401 || normalized === "unauthorized") {
44
+ if (status === 401 || code === "unauthorized" || normalized === "unauthorized") {
42
45
  return new UploadsError(error, "UNAUTHORIZED", status);
43
46
  }
44
- if (status === 404 || normalized === "not found") {
47
+ if (status === 404 || code === "not_found" || normalized === "not found") {
45
48
  return new UploadsError(error, "NOT_FOUND", status);
46
49
  }
47
- if (status === 400 && normalized === "invalid key") {
50
+ if (code === "invalid_key" || (status === 400 && normalized === "invalid key")) {
48
51
  return new UploadsError(error, "INVALID_KEY", status);
49
52
  }
50
53
  if (code === "key_prefix_not_allowed" || code === "key_too_deep") {
51
54
  return new UploadsError(error, "KEY_POLICY", status);
52
55
  }
53
- // Prefer stable body.code — bare 429 is also used for write rate limits.
56
+ // Prefer stable body code — bare 429 is also used for write rate limits.
54
57
  if (status === 507 || code === "storage_quota_exceeded") {
55
58
  return new UploadsError(error, "STORAGE_QUOTA", status);
56
59
  }
@@ -59,15 +62,34 @@ function mapApiError(status, error, code) {
59
62
  }
60
63
  return new UploadsError(error, "API_ERROR", status);
61
64
  }
65
+ /**
66
+ * Parse API error bodies. Prefers the nested envelope
67
+ * `{ error: { code, type, message, details? } }`; still accepts the legacy
68
+ * flat `{ error: string, code?: string }` shape.
69
+ */
70
+ function extractErrorFields(body) {
71
+ if (typeof body === "object" && body && "error" in body) {
72
+ const err = body.error;
73
+ if (typeof err === "object" && err && "message" in err) {
74
+ const nested = err;
75
+ return {
76
+ message: typeof nested.message === "string" ? nested.message : "request failed",
77
+ code: typeof nested.code === "string" ? nested.code : undefined,
78
+ };
79
+ }
80
+ if (typeof err === "string") {
81
+ const code = "code" in body && typeof body.code === "string"
82
+ ? body.code
83
+ : undefined;
84
+ return { message: err, code };
85
+ }
86
+ }
87
+ return { message: "request failed" };
88
+ }
62
89
  async function parseErrorResponse(res) {
63
90
  const body = await res.json().catch(() => ({}));
64
- const message = typeof body === "object" && body && "error" in body && typeof body.error === "string"
65
- ? body.error
66
- : res.statusText || "request failed";
67
- const code = typeof body === "object" && body && "code" in body && typeof body.code === "string"
68
- ? body.code
69
- : undefined;
70
- return mapApiError(res.status, message, code);
91
+ const { message, code } = extractErrorFields(body);
92
+ return mapApiError(res.status, message || res.statusText || "request failed", code);
71
93
  }
72
94
  export function createUploadsClient(config) {
73
95
  async function request(method, path, opts) {
@@ -105,6 +127,9 @@ export function createUploadsClient(config) {
105
127
  const qs = params.toString();
106
128
  return request("GET", `${filesBase(config)}${qs ? `?${qs}` : ""}`);
107
129
  }
130
+ async function getGallery(id) {
131
+ return request("GET", `${galleriesBase(config)}/${encodeURIComponent(id)}`);
132
+ }
108
133
  return {
109
134
  async put(body, opts) {
110
135
  const key = opts.key ??
@@ -151,6 +176,63 @@ export function createUploadsClient(config) {
151
176
  async head(key) {
152
177
  return request("GET", `${filesBase(config)}/${encodeKeyPath(key)}`);
153
178
  },
179
+ async createGallery(opts) {
180
+ return request("POST", galleriesBase(config), {
181
+ body: new TextEncoder().encode(JSON.stringify(opts)),
182
+ headers: { "Content-Type": "application/json" },
183
+ });
184
+ },
185
+ async getGallery(id) {
186
+ return getGallery(id);
187
+ },
188
+ async listGalleries(opts = {}) {
189
+ const params = new URLSearchParams();
190
+ if (opts.limit != null)
191
+ params.set("limit", String(opts.limit));
192
+ if (opts.cursor)
193
+ params.set("cursor", opts.cursor);
194
+ const qs = params.toString();
195
+ return request("GET", `${galleriesBase(config)}${qs ? `?${qs}` : ""}`);
196
+ },
197
+ async deleteGallery(id, opts) {
198
+ return request("DELETE", `${galleriesBase(config)}/${encodeURIComponent(id)}`, {
199
+ body: new TextEncoder().encode(JSON.stringify(opts)),
200
+ headers: { "Content-Type": "application/json" },
201
+ });
202
+ },
203
+ async addGalleryItem(id, objectKey, opts) {
204
+ return request("POST", `${galleriesBase(config)}/${encodeURIComponent(id)}/items`, {
205
+ body: new TextEncoder().encode(JSON.stringify({ objectKey, ...opts })),
206
+ headers: { "Content-Type": "application/json" },
207
+ });
208
+ },
209
+ async listGalleryExternalReferences(id) {
210
+ return request("GET", galleriesBase(config) + "/" + encodeURIComponent(id) + "/external-references");
211
+ },
212
+ async linkGalleryExternalReference(id, opts) {
213
+ return request("POST", galleriesBase(config) + "/" + encodeURIComponent(id) + "/external-references", {
214
+ body: new TextEncoder().encode(JSON.stringify(opts)),
215
+ headers: { "Content-Type": "application/json" },
216
+ });
217
+ },
218
+ async unlinkGalleryExternalReference(id, referenceId, opts) {
219
+ return request("DELETE", galleriesBase(config) +
220
+ "/" +
221
+ encodeURIComponent(id) +
222
+ "/external-references/" +
223
+ encodeURIComponent(referenceId), {
224
+ body: new TextEncoder().encode(JSON.stringify(opts)),
225
+ headers: { "Content-Type": "application/json" },
226
+ });
227
+ },
228
+ async findGalleriesByReference(opts) {
229
+ const params = new URLSearchParams({ provider: opts.provider, coordinate: opts.coordinate });
230
+ if (opts.limit != null)
231
+ params.set("limit", String(opts.limit));
232
+ if (opts.cursor)
233
+ params.set("cursor", opts.cursor);
234
+ return request("GET", galleriesBase(config) + "/by-reference?" + params);
235
+ },
154
236
  async health() {
155
237
  return request("GET", `${config.apiUrl}/health`, { auth: false });
156
238
  },
@@ -1,4 +1,6 @@
1
1
  type FileScope = "files:read" | "files:write" | "files:delete";
2
+ export declare function invitePageUrl(apiUrl: string, pageId: string, webUrl?: string): string;
3
+ export declare function inviteMagicLink(pageUrl: string, code: string): string;
2
4
  export declare function parseScopes(raw: string | undefined): FileScope[] | undefined;
3
5
  export declare function runAdmin(args: string[], opts: {
4
6
  json?: boolean;