@buildinternet/uploads 0.1.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Build Internet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,64 +1,79 @@
1
- # @buildinternet/uploads
2
-
3
- CLI and client for **uploads.sh** — upload files, get public URLs, and produce GitHub-ready markdown. Successor to the R2 scripts in `buildinternet-skills/github-screenshots`.
4
-
5
- ## CLI
6
-
7
- Binary: `uploads` (also `pnpm uploads` from repo root after `pnpm install`).
8
-
9
- Install globally or run a pinned version without installing:
10
-
11
- ```bash
12
- npm install --global @buildinternet/uploads
13
- npx @buildinternet/uploads@0.1.0 --help
14
- ```
15
-
16
- ```bash
17
- pnpm uploads setup --env-file .env
18
- pnpm uploads attach ./before.png ./after.png --env-file .env
19
- pnpm uploads put ./shot.png --env-file .env
20
- pnpm uploads put ./after.png --pr 123 --comment --env-file .env
21
- pnpm uploads doctor --env-file .env
22
- ```
23
-
24
- Commands: `attach`, `put`, `comment`, `list`, `delete`, `setup`, `config`, `doctor`, `health`.
25
-
26
- `attach` is the agent-friendly default for GitHub media. It accepts one or more files,
27
- infers the pull request for the current branch via `gh`, uploads stable URLs, and creates
28
- or updates one managed attachments comment. Use `--pr`, `--issue`, and `--repo` to select
29
- the target explicitly, or `--no-comment` to upload without changing GitHub comments.
30
-
31
- Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
32
-
33
- ## Programmatic use
34
-
35
- ```ts
36
- import { createUploadsClient } from "@buildinternet/uploads";
37
- ```
38
-
39
- Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools` for Workers).
40
-
41
- ## Layout
42
-
43
- ```
44
- src/
45
- cli.ts Entry + help
46
- commands.ts put, list, delete, comment, …
47
- client.ts HTTP client for the API
48
- github.ts PR/issue key paths + attachment comments
49
- embed.ts Markdown image output
50
- bin/uploads.js Bin shim
51
- ```
52
-
53
- ## Commands
54
-
55
- ```bash
56
- pnpm build # tsc → dist/
57
- pnpm typecheck
58
- pnpm test
59
- pnpm pack:check # verify the npm tarball contents
60
- ```
61
-
62
- Maintainer release instructions: [`docs/releasing.md`](../../docs/releasing.md).
63
-
64
- Agent-oriented usage: [`skills/uploads-cli/SKILL.md`](../../skills/uploads-cli/SKILL.md). REST details: [`docs/api.md`](../../docs/api.md).
1
+ # @buildinternet/uploads
2
+
3
+ CLI and client for **uploads.sh** — upload files, get public URLs, and produce GitHub-ready markdown. Successor to the R2 scripts in `buildinternet-skills/github-screenshots`.
4
+
5
+ ## CLI
6
+
7
+ Binary: `uploads` (also `pnpm uploads` from repo root after `pnpm install`).
8
+
9
+ Install globally or run a pinned version without installing:
10
+
11
+ ```bash
12
+ npm install --global @buildinternet/uploads
13
+ npx @buildinternet/uploads@0.1.0 --help
14
+ ```
15
+
16
+ ```bash
17
+ pnpm uploads setup --env-file .env
18
+ pnpm uploads attach ./before.png ./after.png --env-file .env
19
+ pnpm uploads put ./shot.png --env-file .env
20
+ pnpm uploads put ./after.png --pr 123 --comment --env-file .env
21
+ pnpm uploads doctor --env-file .env
22
+ ```
23
+
24
+ Commands: `attach`, `put`, `comment`, `list`, `delete`, `usage`, `reconcile`,
25
+ `purge-expired`, `setup`, `install`, `config`, `doctor`, `health`, `mcp`.
26
+
27
+ `attach` is the agent-friendly default for GitHub media. It accepts one or more files,
28
+ infers the pull request for the current branch via `gh`, uploads stable URLs, and creates
29
+ or updates one managed attachments comment. Use `--pr`, `--issue`, and `--repo` to select
30
+ the target explicitly, or `--no-comment` to upload without changing GitHub comments.
31
+
32
+ Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
33
+
34
+ ## MCP server
35
+
36
+ `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).
37
+
38
+ ```json
39
+ { "command": "uploads", "args": ["--env-file", "/path/to/.env", "mcp"] }
40
+ ```
41
+
42
+ 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`.
43
+
44
+ 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.
45
+
46
+ ## Programmatic use
47
+
48
+ ```ts
49
+ import { createUploadsClient } from "@buildinternet/uploads";
50
+ ```
51
+
52
+ Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools` for Workers); for local stdio MCP, use `uploads mcp` (above).
53
+
54
+ ## Layout
55
+
56
+ ```
57
+ src/
58
+ cli.ts Entry + help
59
+ commands.ts put, list, delete, comment,
60
+ commands/mcp.ts `mcp` command entry
61
+ mcp/ Stdio MCP server (server.ts, tools.ts)
62
+ client.ts HTTP client for the API
63
+ github.ts PR/issue key paths + attachment comments
64
+ embed.ts Markdown image output
65
+ bin/uploads.js Bin shim
66
+ ```
67
+
68
+ ## Commands
69
+
70
+ ```bash
71
+ pnpm build # tsc → dist/
72
+ pnpm typecheck
73
+ pnpm test
74
+ pnpm pack:check # verify the npm tarball contents
75
+ ```
76
+
77
+ Maintainer release instructions: [`docs/releasing.md`](../../docs/releasing.md).
78
+
79
+ Agent-oriented usage: [`skills/uploads-cli/SKILL.md`](../../skills/uploads-cli/SKILL.md). REST details: [`docs/api.md`](../../docs/api.md).
package/bin/uploads.js CHANGED
File without changes
package/dist/cli.js CHANGED
@@ -2,56 +2,70 @@ 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, } from "./commands.js";
5
+ import { runPut, runAttach, runList, runDelete, runHealth, runDoctor, runComment, runUsage, runReconcile, runPurgeExpired, } from "./commands.js";
6
6
  import { runConfig } from "./commands/config.js";
7
7
  import { runSetup } from "./commands/setup.js";
8
- const ROOT_HELP = `uploads CLI for uploads.sh (GitHub image embeds)
9
-
10
- Usage:
11
- uploads [globals] <command> [args]
12
-
13
- Config (first match wins, per key):
14
- CLI flags --api-url, --token, --workspace
15
- environment UPLOADS_API_URL, UPLOADS_TOKEN, UPLOADS_WORKSPACE
16
- --env-file <path>
17
- $BUILDINTERNET_CONFIG
18
- ~/.config/buildinternet/config (shared with github-screenshots)
19
-
20
- Workspace (within config layers):
21
- --workspace, -w override — global (before command) or per-command (after)
22
- UPLOADS_WORKSPACE env / config file
23
- (else inferred from token up_<name>_…, else "default")
24
-
25
- Other globals (before command):
26
- --api-url <url> default: https://api.uploads.sh
27
- --token <token> or UPLOADS_TOKEN
28
- --env-file <path>
29
- --json JSON on stdout
30
- --quiet
31
-
32
- Commands:
33
- attach <file...> Attach media to the current PR (stable URLs + managed comment)
34
- put <file> Upload (+ URL + markdown for GitHub)
35
- comment Create/update a PR/issue attachments comment (via gh)
36
- list List objects
37
- delete <key> Delete object
38
- setup Guided config + token minting steps
39
- config Show path, init, or set shared config
40
- doctor Health + auth + workspace checks
41
- health API liveness (no auth)
42
-
43
- Put/list defaults (config file or env):
44
- UPLOADS_DEFAULT_PREFIX, UPLOADS_DEFAULT_REPO, UPLOADS_DEFAULT_REF
45
- UPLOADS_DEFAULT_WIDTH, UPLOADS_NO_GIT
46
-
47
- Examples:
48
- uploads setup
49
- uploads setup --token up_default_… --repo myorg/myapp
50
- uploads attach ./before.png ./after.png
51
- uploads put ./shot.png --ref 42
52
- uploads doctor
53
-
54
- Agent/MCP: use createUploadsWorkerFileTools() from @buildinternet/uploads/agent on the Worker.
8
+ import { runLogin } from "./commands/login.js";
9
+ import { runAdmin } from "./commands/admin-enrollment.js";
10
+ import { runMcp } from "./commands/mcp.js";
11
+ import { runInstall } from "./commands/install.js";
12
+ const ROOT_HELP = `uploads — CLI for uploads.sh (GitHub image embeds)
13
+
14
+ Usage:
15
+ uploads [globals] <command> [args]
16
+
17
+ Config (first match wins, per key):
18
+ CLI flags --api-url, --token, --workspace
19
+ environment UPLOADS_API_URL, UPLOADS_TOKEN, UPLOADS_WORKSPACE
20
+ --env-file <path>
21
+ $BUILDINTERNET_CONFIG
22
+ ~/.config/buildinternet/config (shared with github-screenshots)
23
+
24
+ Workspace (within config layers):
25
+ --workspace, -w override — global (before command) or per-command (after)
26
+ UPLOADS_WORKSPACE env / config file
27
+ (else inferred from token up_<name>_…, else "default")
28
+
29
+ Other globals (before command):
30
+ --api-url <url> default: https://api.uploads.sh
31
+ --token <token> or UPLOADS_TOKEN
32
+ --env-file <path>
33
+ --json JSON on stdout
34
+ --quiet
35
+
36
+ Commands:
37
+ attach <file...> Attach media to the current PR (stable URLs + managed comment)
38
+ put <file> Upload (+ URL + markdown for GitHub)
39
+ comment Create/update a PR/issue attachments comment (via gh)
40
+ list List objects
41
+ delete <key> Delete object
42
+ usage Workspace storage / upload counters
43
+ reconcile Rebuild usage ledger from storage
44
+ purge-expired Delete objects past retentionDays
45
+ setup Inspect/configure advanced CLI settings
46
+ install Install the agent skill + register the remote MCP server
47
+ login Exchange an enrollment code and configure credentials
48
+ admin Admin enrollment management
49
+ config Show path, init, or set shared config
50
+ doctor Health + auth + workspace checks
51
+ health API liveness (no auth)
52
+ mcp Serve MCP over stdio (tools mirror the CLI)
53
+
54
+ Put/list defaults (config file or env):
55
+ UPLOADS_DEFAULT_PREFIX, UPLOADS_DEFAULT_REPO, UPLOADS_DEFAULT_REF
56
+ UPLOADS_DEFAULT_WIDTH, UPLOADS_NO_GIT
57
+
58
+ Examples:
59
+ uploads setup
60
+ uploads setup --token up_default_… --repo myorg/myapp
61
+ uploads attach ./before.png ./after.png
62
+ uploads put ./shot.png --ref 42
63
+ uploads doctor
64
+
65
+ Agent/MCP: \`uploads install\` sets up the agent skill and the hosted MCP server
66
+ (https://agents.uploads.sh/mcp, workspace inferred from the token). Run
67
+ \`uploads mcp\` for local stdio, or use createUploadsWorkerFileTools()
68
+ from @buildinternet/uploads/agent on the Worker.
55
69
  `;
56
70
  function createContext(globals, requireToken, commandArgs) {
57
71
  const cmdWorkspace = commandWorkspace(parseCommandArgs(commandArgs).flags);
@@ -80,6 +94,8 @@ function exitCode(err) {
80
94
  return 2;
81
95
  case "UNAUTHORIZED":
82
96
  case "NOT_FOUND":
97
+ case "STORAGE_QUOTA":
98
+ case "UPLOAD_BUDGET":
83
99
  return 3;
84
100
  case "NETWORK":
85
101
  return 4;
@@ -122,10 +138,21 @@ export async function runCli(argv) {
122
138
  return runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
123
139
  case "setup":
124
140
  return runSetup(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
141
+ case "login":
142
+ return runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
143
+ case "admin":
144
+ return runAdmin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
145
+ case "mcp":
146
+ return runMcp(cmdArgs, { globals: parsed.globals }, showHelp);
147
+ case "install":
148
+ return runInstall(cmdArgs, { globals: parsed.globals, json }, showHelp);
125
149
  case "attach":
126
150
  case "put":
127
151
  case "list":
128
152
  case "delete":
153
+ case "usage":
154
+ case "reconcile":
155
+ case "purge-expired":
129
156
  case "doctor":
130
157
  case "comment": {
131
158
  const ctx = createContext(parsed.globals, !showHelp, cmdArgs);
@@ -140,6 +167,12 @@ export async function runCli(argv) {
140
167
  return runList(ctx, cmdArgs, showHelp);
141
168
  case "delete":
142
169
  return runDelete(ctx, cmdArgs, showHelp);
170
+ case "usage":
171
+ return runUsage(ctx, cmdArgs, showHelp);
172
+ case "reconcile":
173
+ return runReconcile(ctx, cmdArgs, showHelp);
174
+ case "purge-expired":
175
+ return runPurgeExpired(ctx, cmdArgs, showHelp);
143
176
  case "doctor":
144
177
  return runDoctor(ctx, cmdArgs, showHelp);
145
178
  }
package/dist/client.d.ts CHANGED
@@ -43,13 +43,80 @@ export interface DeleteResult {
43
43
  export interface HealthResult {
44
44
  ok: boolean;
45
45
  }
46
+ export interface UsageResult {
47
+ workspace: string;
48
+ bytes: number;
49
+ objects: number;
50
+ uploadsInPeriod: number;
51
+ periodStart: string;
52
+ updatedAt: string;
53
+ maxStorageBytes?: number;
54
+ storageRemainingBytes?: number;
55
+ maxUploadsPerPeriod?: number;
56
+ uploadsRemaining?: number;
57
+ }
58
+ export interface ReconcileResult {
59
+ workspace: string;
60
+ bytes: number;
61
+ objects: number;
62
+ previous: {
63
+ bytes: number;
64
+ objects: number;
65
+ };
66
+ changed: boolean;
67
+ usage: UsageResult;
68
+ }
69
+ export interface PurgeExpiredResult {
70
+ workspace: string;
71
+ retentionDays: number;
72
+ cutoff: string;
73
+ deleted: number;
74
+ freedBytes: number;
75
+ keys: string[];
76
+ keysTruncated: boolean;
77
+ reconcile: ReconcileResult;
78
+ }
79
+ export type PurgeExpiredResponse = PurgeExpiredResult | {
80
+ skipped: true;
81
+ reason: string;
82
+ };
83
+ export interface EnrollmentExchangeResult {
84
+ apiUrl?: string;
85
+ workspace: string;
86
+ token: string;
87
+ scopes?: Array<"files:read" | "files:write" | "files:delete">;
88
+ expiresAt?: string;
89
+ }
90
+ export interface EnrollmentCreateResult {
91
+ code: string;
92
+ expiresAt: string;
93
+ tokenExpiresAt: string;
94
+ }
95
+ export declare function exchangeEnrollment(apiUrl: string, code: string): Promise<EnrollmentExchangeResult>;
96
+ export declare function createEnrollment(apiUrl: string, adminToken: string, input: {
97
+ workspace?: string;
98
+ label?: string;
99
+ enrollmentSeconds?: number;
100
+ tokenExpiresInSeconds?: number;
101
+ scopes?: Array<"files:read" | "files:write" | "files:delete">;
102
+ }): Promise<EnrollmentCreateResult>;
46
103
  export declare function createUploadsClient(config: UploadsClientConfig): {
47
104
  put(body: Uint8Array, opts: PutOptions & {
48
105
  filename: string;
49
106
  }): Promise<PutResult>;
50
- list(opts?: ListOptions): Promise<ListResult>;
107
+ list: (opts?: ListOptions) => Promise<ListResult>;
108
+ /** Follow cursors (optionally starting from one) and return every remaining item. */
109
+ listAll(opts?: Omit<ListOptions, "cursor"> & {
110
+ cursor?: string;
111
+ }): Promise<ListItem[]>;
51
112
  delete(key: string): Promise<DeleteResult>;
52
113
  head(key: string): Promise<HeadResult>;
53
114
  health(): Promise<HealthResult>;
115
+ /** Workspace storage / upload counters (+ limits when configured). */
116
+ usage(): Promise<UsageResult>;
117
+ /** Rebuild ledger bytes/objects from storage (source of truth). */
118
+ reconcile(): Promise<ReconcileResult>;
119
+ /** Delete objects past retentionDays (if set), then reconcile. */
120
+ purgeExpired(): Promise<PurgeExpiredResponse>;
54
121
  };
55
122
  export type UploadsClient = ReturnType<typeof createUploadsClient>;
package/dist/client.js CHANGED
@@ -1,13 +1,42 @@
1
1
  import { inferContentType } from "./embed.js";
2
2
  import { UploadsError } from "./errors.js";
3
3
  import { buildScreenshotKey } from "./keys.js";
4
+ async function jsonRequest(url, init) {
5
+ let res;
6
+ try {
7
+ res = await fetch(url, init);
8
+ }
9
+ catch (err) {
10
+ throw new UploadsError(err instanceof Error ? err.message : "network request failed", "NETWORK");
11
+ }
12
+ if (!res.ok)
13
+ throw await parseErrorResponse(res);
14
+ return (await res.json());
15
+ }
16
+ export function exchangeEnrollment(apiUrl, code) {
17
+ return jsonRequest(`${apiUrl.replace(/\/$/, "")}/auth/enrollments/exchange`, {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({ code }),
21
+ });
22
+ }
23
+ export function createEnrollment(apiUrl, adminToken, input) {
24
+ return jsonRequest(`${apiUrl.replace(/\/$/, "")}/admin/enrollments`, {
25
+ method: "POST",
26
+ headers: { Authorization: `Bearer ${adminToken}`, "Content-Type": "application/json" },
27
+ body: JSON.stringify(input),
28
+ });
29
+ }
4
30
  function encodeKeyPath(key) {
5
31
  return key.split("/").map(encodeURIComponent).join("/");
6
32
  }
7
33
  function filesBase(config) {
8
34
  return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/files`;
9
35
  }
10
- function mapApiError(status, error) {
36
+ function usageBase(config) {
37
+ return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/usage`;
38
+ }
39
+ function mapApiError(status, error, code) {
11
40
  const normalized = error.toLowerCase();
12
41
  if (status === 401 || normalized === "unauthorized") {
13
42
  return new UploadsError(error, "UNAUTHORIZED", status);
@@ -18,6 +47,13 @@ function mapApiError(status, error) {
18
47
  if (status === 400 && normalized === "invalid key") {
19
48
  return new UploadsError(error, "INVALID_KEY", status);
20
49
  }
50
+ // Prefer stable body.code — bare 429 is also used for write rate limits.
51
+ if (status === 507 || code === "storage_quota_exceeded") {
52
+ return new UploadsError(error, "STORAGE_QUOTA", status);
53
+ }
54
+ if (code === "upload_budget_exceeded") {
55
+ return new UploadsError(error, "UPLOAD_BUDGET", status);
56
+ }
21
57
  return new UploadsError(error, "API_ERROR", status);
22
58
  }
23
59
  async function parseErrorResponse(res) {
@@ -25,7 +61,10 @@ async function parseErrorResponse(res) {
25
61
  const message = typeof body === "object" && body && "error" in body && typeof body.error === "string"
26
62
  ? body.error
27
63
  : res.statusText || "request failed";
28
- return mapApiError(res.status, message);
64
+ const code = typeof body === "object" && body && "code" in body && typeof body.code === "string"
65
+ ? body.code
66
+ : undefined;
67
+ return mapApiError(res.status, message, code);
29
68
  }
30
69
  export function createUploadsClient(config) {
31
70
  async function request(method, path, opts) {
@@ -52,6 +91,17 @@ export function createUploadsClient(config) {
52
91
  return undefined;
53
92
  return (await res.json());
54
93
  }
94
+ async function list(opts = {}) {
95
+ const params = new URLSearchParams();
96
+ if (opts.prefix)
97
+ params.set("prefix", opts.prefix);
98
+ if (opts.limit != null)
99
+ params.set("limit", String(opts.limit));
100
+ if (opts.cursor)
101
+ params.set("cursor", opts.cursor);
102
+ const qs = params.toString();
103
+ return request("GET", `${filesBase(config)}${qs ? `?${qs}` : ""}`);
104
+ }
55
105
  return {
56
106
  async put(body, opts) {
57
107
  const key = opts.key ??
@@ -73,16 +123,17 @@ export function createUploadsClient(config) {
73
123
  }
74
124
  return { ...result, url: result.url };
75
125
  },
76
- async list(opts = {}) {
77
- const params = new URLSearchParams();
78
- if (opts.prefix)
79
- params.set("prefix", opts.prefix);
80
- if (opts.limit != null)
81
- params.set("limit", String(opts.limit));
82
- if (opts.cursor)
83
- params.set("cursor", opts.cursor);
84
- const qs = params.toString();
85
- return request("GET", `${filesBase(config)}${qs ? `?${qs}` : ""}`);
126
+ list,
127
+ /** Follow cursors (optionally starting from one) and return every remaining item. */
128
+ async listAll(opts = {}) {
129
+ const items = [];
130
+ let cursor = opts.cursor;
131
+ do {
132
+ const page = await list({ ...opts, cursor });
133
+ items.push(...page.items);
134
+ cursor = page.cursor ?? undefined;
135
+ } while (cursor);
136
+ return items;
86
137
  },
87
138
  async delete(key) {
88
139
  return request("DELETE", `${filesBase(config)}/${encodeKeyPath(key)}`);
@@ -93,5 +144,17 @@ export function createUploadsClient(config) {
93
144
  async health() {
94
145
  return request("GET", `${config.apiUrl}/health`, { auth: false });
95
146
  },
147
+ /** Workspace storage / upload counters (+ limits when configured). */
148
+ async usage() {
149
+ return request("GET", usageBase(config));
150
+ },
151
+ /** Rebuild ledger bytes/objects from storage (source of truth). */
152
+ async reconcile() {
153
+ return request("POST", `${usageBase(config)}/reconcile`);
154
+ },
155
+ /** Delete objects past retentionDays (if set), then reconcile. */
156
+ async purgeExpired() {
157
+ return request("POST", `${usageBase(config)}/purge-expired`);
158
+ },
96
159
  };
97
160
  }
@@ -0,0 +1,7 @@
1
+ type FileScope = "files:read" | "files:write" | "files:delete";
2
+ export declare function parseScopes(raw: string | undefined): FileScope[] | undefined;
3
+ export declare function runAdmin(args: string[], opts: {
4
+ json?: boolean;
5
+ apiUrl?: string;
6
+ }, help?: boolean): Promise<number>;
7
+ export {};
@@ -0,0 +1,59 @@
1
+ import { createEnrollment } from "../client.js";
2
+ import { flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
3
+ const HELP = `uploads admin enrollment create [options]
4
+
5
+ Admin-only: create a short-lived, one-time enrollment code.
6
+
7
+ Options:
8
+ --admin-token <token> Or ADMIN_TOKEN (UPLOADS_ADMIN_TOKEN is a legacy alias)
9
+ --workspace <name> Default: default
10
+ --label <label>
11
+ --expires-in <seconds> Default: server policy
12
+ --token-expires-in <seconds> Upload token lifetime (default: server policy)
13
+ --scopes <list> Comma-separated files:read,files:write,files:delete
14
+ --api-url <url> Default: https://api.uploads.sh
15
+ `;
16
+ const FILE_SCOPES = new Set(["files:read", "files:write", "files:delete"]);
17
+ export function parseScopes(raw) {
18
+ if (raw === undefined)
19
+ return undefined;
20
+ const scopes = raw
21
+ .split(",")
22
+ .map((scope) => scope.trim())
23
+ .filter(Boolean);
24
+ if (scopes.length === 0)
25
+ throw new UsageError("--scopes must contain at least one scope");
26
+ const invalid = scopes.find((scope) => !FILE_SCOPES.has(scope));
27
+ if (invalid)
28
+ throw new UsageError(`invalid scope: ${invalid} (expected files:read, files:write, or files:delete)`);
29
+ return [...new Set(scopes)];
30
+ }
31
+ export async function runAdmin(args, opts, help = false) {
32
+ const parsed = parseCommandArgs(args);
33
+ if (help || parsed.help) {
34
+ process.stderr.write(HELP);
35
+ return 0;
36
+ }
37
+ if (parsed.positionals[0] !== "enrollment" || parsed.positionals[1] !== "create")
38
+ throw new UsageError("expected: uploads admin enrollment create");
39
+ const adminToken = flagString(parsed.flags, "--admin-token") ??
40
+ process.env.ADMIN_TOKEN ??
41
+ process.env.UPLOADS_ADMIN_TOKEN;
42
+ if (!adminToken)
43
+ throw new UsageError("ADMIN_TOKEN is required for admin enrollment creation");
44
+ const apiUrl = flagString(parsed.flags, "--api-url") ?? opts.apiUrl ?? "https://api.uploads.sh";
45
+ const workspace = flagString(parsed.flags, "--workspace") ?? "default";
46
+ const label = flagString(parsed.flags, "--label");
47
+ const result = await createEnrollment(apiUrl, adminToken, {
48
+ workspace,
49
+ label,
50
+ enrollmentSeconds: flagInt(parsed.flags, "--expires-in", "--expires-in"),
51
+ tokenExpiresInSeconds: flagInt(parsed.flags, "--token-expires-in", "--token-expires-in"),
52
+ scopes: parseScopes(flagString(parsed.flags, "--scopes")),
53
+ });
54
+ if (opts.json)
55
+ process.stdout.write(JSON.stringify({ workspace, label: label ?? null, ...result }, null, 2) + "\n");
56
+ else
57
+ process.stdout.write(`Enrollment code (share once): ${result.code}\nworkspace: ${workspace}\nexpires: ${result.expiresAt}\n`);
58
+ return 0;
59
+ }
@@ -0,0 +1,8 @@
1
+ import { type GlobalFlags } from "../cli-args.js";
2
+ import { type CommandRunner } from "../github-gh.js";
3
+ export declare const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
4
+ export declare function runInstall(args: string[], opts: {
5
+ globals: GlobalFlags;
6
+ json?: boolean;
7
+ runner?: CommandRunner;
8
+ }, help?: boolean): Promise<number>;