@buildinternet/uploads 0.1.1 → 0.3.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 +21 -0
- package/README.md +46 -10
- package/dist/cli.js +39 -2
- package/dist/client.d.ts +48 -1
- package/dist/client.js +52 -12
- package/dist/commands/install.d.ts +8 -0
- package/dist/commands/install.js +133 -0
- package/dist/commands/mcp.d.ts +4 -0
- package/dist/commands/mcp.js +39 -0
- package/dist/commands.d.ts +63 -0
- package/dist/commands.js +337 -76
- package/dist/config-file.d.ts +5 -1
- package/dist/config-file.js +27 -2
- package/dist/destinations.d.ts +26 -0
- package/dist/destinations.js +47 -0
- package/dist/errors.d.ts +1 -1
- package/dist/frame.d.ts +36 -0
- package/dist/frame.js +245 -0
- package/dist/github.d.ts +11 -0
- package/dist/github.js +32 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/io.d.ts +3 -0
- package/dist/io.js +9 -0
- package/dist/mcp/args.d.ts +4 -0
- package/dist/mcp/args.js +26 -0
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +109 -0
- package/dist/mcp/stdio.d.ts +3 -0
- package/dist/mcp/stdio.js +14 -0
- package/dist/mcp/tools.d.ts +10 -0
- package/dist/mcp/tools.js +522 -0
- package/dist/optimize.d.ts +38 -0
- package/dist/optimize.js +177 -0
- package/package.json +15 -9
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
|
@@ -4,9 +4,7 @@ CLI and client for **uploads.sh** — upload files, get public URLs, and produce
|
|
|
4
4
|
|
|
5
5
|
## CLI
|
|
6
6
|
|
|
7
|
-
Binary:
|
|
8
|
-
|
|
9
|
-
Install globally or run a pinned version without installing:
|
|
7
|
+
Binary: **`uploads`**. Install globally (or use a pinned `npx` one-shot):
|
|
10
8
|
|
|
11
9
|
```bash
|
|
12
10
|
npm install --global @buildinternet/uploads
|
|
@@ -14,29 +12,65 @@ npx @buildinternet/uploads@0.1.0 --help
|
|
|
14
12
|
```
|
|
15
13
|
|
|
16
14
|
```bash
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
uploads setup
|
|
16
|
+
uploads attach ./before.png ./after.png
|
|
17
|
+
uploads put ./shot.png
|
|
18
|
+
uploads put ./shot.png --destination screenshots
|
|
19
|
+
uploads put ./shot.png --no-optimize
|
|
20
|
+
uploads put ./mobile.png --frame phone
|
|
21
|
+
uploads put ./ui.png --frame browser --frame-url "https://app.example"
|
|
22
|
+
uploads put ./after.png --pr 123 --comment
|
|
23
|
+
uploads doctor
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
Inside this monorepo only, `pnpm uploads …` builds the package first so you pick
|
|
27
|
+
up local source; product docs and PR “how to try it” examples should use the
|
|
28
|
+
global `uploads` form above.
|
|
29
|
+
|
|
30
|
+
Commands: `attach`, `put`, `comment`, `list`, `delete`, `usage`, `reconcile`,
|
|
31
|
+
`purge-expired`, `setup`, `install`, `config`, `doctor`, `health`, `mcp`.
|
|
25
32
|
|
|
26
33
|
`attach` is the agent-friendly default for GitHub media. It accepts one or more files,
|
|
27
34
|
infers the pull request for the current branch via `gh`, uploads stable URLs, and creates
|
|
28
35
|
or updates one managed attachments comment. Use `--pr`, `--issue`, and `--repo` to select
|
|
29
36
|
the target explicitly, or `--no-comment` to upload without changing GitHub comments.
|
|
30
37
|
|
|
38
|
+
**Keys / destinations:** default put uses the `screenshots` layout. Typed destinations
|
|
39
|
+
(`--destination screenshots|gh|f`, MCP `destination`) set the root; `--pr`/`--issue`
|
|
40
|
+
use `gh/…`. Workspaces may restrict put/sign to those roots via
|
|
41
|
+
`allowedKeyPrefixes` (see [workspaces](../../docs/workspaces.md)).
|
|
42
|
+
|
|
43
|
+
**Image optimization:** by default, still images are re-encoded to WebP (long edge
|
|
44
|
+
capped, high quality) before upload so GitHub embeds stay small, and **EXIF is
|
|
45
|
+
stripped**. Pass `--keep-exif` / `UPLOADS_KEEP_EXIF=1` to preserve image metadata, or
|
|
46
|
+
`--no-optimize` / `UPLOADS_NO_OPTIMIZE=1` to upload originals unchanged.
|
|
47
|
+
|
|
48
|
+
**Frames (opt-in):** `--frame phone|browser|iphone-16-pro` composites chrome
|
|
49
|
+
**before** optimize. `phone`/`browser` are procedural; `iphone-16-pro` fetches
|
|
50
|
+
community art from [device-frames-media](https://github.com/jonnyjackson26/device-frames-media)
|
|
51
|
+
into `~/.cache/uploads/frames` (not bundled).
|
|
52
|
+
|
|
31
53
|
Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
|
|
32
54
|
|
|
55
|
+
## MCP server
|
|
56
|
+
|
|
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).
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{ "command": "uploads", "args": ["--env-file", "/path/to/.env", "mcp"] }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
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
|
+
|
|
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.
|
|
66
|
+
|
|
33
67
|
## Programmatic use
|
|
34
68
|
|
|
35
69
|
```ts
|
|
36
70
|
import { createUploadsClient } from "@buildinternet/uploads";
|
|
37
71
|
```
|
|
38
72
|
|
|
39
|
-
Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools` for Workers).
|
|
73
|
+
Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools` for Workers); for local stdio MCP, use `uploads mcp` (above).
|
|
40
74
|
|
|
41
75
|
## Layout
|
|
42
76
|
|
|
@@ -44,6 +78,8 @@ Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools
|
|
|
44
78
|
src/
|
|
45
79
|
cli.ts Entry + help
|
|
46
80
|
commands.ts put, list, delete, comment, …
|
|
81
|
+
commands/mcp.ts `mcp` command entry
|
|
82
|
+
mcp/ Stdio MCP server (server.ts, tools.ts)
|
|
47
83
|
client.ts HTTP client for the API
|
|
48
84
|
github.ts PR/issue key paths + attachment comments
|
|
49
85
|
embed.ts Markdown image output
|
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,13 @@ 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
8
|
import { runLogin } from "./commands/login.js";
|
|
9
9
|
import { runAdmin } from "./commands/admin-enrollment.js";
|
|
10
|
+
import { runMcp } from "./commands/mcp.js";
|
|
11
|
+
import { runInstall } from "./commands/install.js";
|
|
10
12
|
const ROOT_HELP = `uploads — CLI for uploads.sh (GitHub image embeds)
|
|
11
13
|
|
|
12
14
|
Usage:
|
|
@@ -37,12 +39,17 @@ Commands:
|
|
|
37
39
|
comment Create/update a PR/issue attachments comment (via gh)
|
|
38
40
|
list List objects
|
|
39
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
|
|
40
45
|
setup Inspect/configure advanced CLI settings
|
|
46
|
+
install Install the agent skill + register the remote MCP server
|
|
41
47
|
login Exchange an enrollment code and configure credentials
|
|
42
48
|
admin Admin enrollment management
|
|
43
49
|
config Show path, init, or set shared config
|
|
44
50
|
doctor Health + auth + workspace checks
|
|
45
51
|
health API liveness (no auth)
|
|
52
|
+
mcp Serve MCP over stdio (tools mirror the CLI)
|
|
46
53
|
|
|
47
54
|
Put/list defaults (config file or env):
|
|
48
55
|
UPLOADS_DEFAULT_PREFIX, UPLOADS_DEFAULT_REPO, UPLOADS_DEFAULT_REF
|
|
@@ -55,7 +62,10 @@ Examples:
|
|
|
55
62
|
uploads put ./shot.png --ref 42
|
|
56
63
|
uploads doctor
|
|
57
64
|
|
|
58
|
-
Agent/MCP:
|
|
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.
|
|
59
69
|
`;
|
|
60
70
|
function createContext(globals, requireToken, commandArgs) {
|
|
61
71
|
const cmdWorkspace = commandWorkspace(parseCommandArgs(commandArgs).flags);
|
|
@@ -84,6 +94,9 @@ function exitCode(err) {
|
|
|
84
94
|
return 2;
|
|
85
95
|
case "UNAUTHORIZED":
|
|
86
96
|
case "NOT_FOUND":
|
|
97
|
+
case "KEY_POLICY":
|
|
98
|
+
case "STORAGE_QUOTA":
|
|
99
|
+
case "UPLOAD_BUDGET":
|
|
87
100
|
return 3;
|
|
88
101
|
case "NETWORK":
|
|
89
102
|
return 4;
|
|
@@ -107,6 +120,17 @@ function errorOut(err, json) {
|
|
|
107
120
|
process.stderr.write(`${msg}\n`);
|
|
108
121
|
else
|
|
109
122
|
process.stderr.write(`error: ${msg}\n`);
|
|
123
|
+
if (err instanceof UploadsError) {
|
|
124
|
+
if (err.code === "STORAGE_QUOTA" || err.code === "UPLOAD_BUDGET") {
|
|
125
|
+
process.stderr.write("hint: run `uploads usage` then delete objects or raise limits (`pnpm workspace:limits`)\n");
|
|
126
|
+
}
|
|
127
|
+
else if (err.code === "KEY_POLICY") {
|
|
128
|
+
process.stderr.write("hint: use a typed destination (`--destination screenshots|gh`) or an allowed prefix; operators set allowlists with `pnpm workspace:limits --allowed-prefixes`\n");
|
|
129
|
+
}
|
|
130
|
+
else if (err.status === 413 || err.message.toLowerCase().includes("too large")) {
|
|
131
|
+
process.stderr.write("hint: file exceeds workspace size policy (images vs video may differ); compress or raise --max-upload-bytes / --max-video-bytes\n");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
110
134
|
}
|
|
111
135
|
}
|
|
112
136
|
export async function runCli(argv) {
|
|
@@ -130,10 +154,17 @@ export async function runCli(argv) {
|
|
|
130
154
|
return runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
|
|
131
155
|
case "admin":
|
|
132
156
|
return runAdmin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
|
|
157
|
+
case "mcp":
|
|
158
|
+
return runMcp(cmdArgs, { globals: parsed.globals }, showHelp);
|
|
159
|
+
case "install":
|
|
160
|
+
return runInstall(cmdArgs, { globals: parsed.globals, json }, showHelp);
|
|
133
161
|
case "attach":
|
|
134
162
|
case "put":
|
|
135
163
|
case "list":
|
|
136
164
|
case "delete":
|
|
165
|
+
case "usage":
|
|
166
|
+
case "reconcile":
|
|
167
|
+
case "purge-expired":
|
|
137
168
|
case "doctor":
|
|
138
169
|
case "comment": {
|
|
139
170
|
const ctx = createContext(parsed.globals, !showHelp, cmdArgs);
|
|
@@ -148,6 +179,12 @@ export async function runCli(argv) {
|
|
|
148
179
|
return runList(ctx, cmdArgs, showHelp);
|
|
149
180
|
case "delete":
|
|
150
181
|
return runDelete(ctx, cmdArgs, showHelp);
|
|
182
|
+
case "usage":
|
|
183
|
+
return runUsage(ctx, cmdArgs, showHelp);
|
|
184
|
+
case "reconcile":
|
|
185
|
+
return runReconcile(ctx, cmdArgs, showHelp);
|
|
186
|
+
case "purge-expired":
|
|
187
|
+
return runPurgeExpired(ctx, cmdArgs, showHelp);
|
|
151
188
|
case "doctor":
|
|
152
189
|
return runDoctor(ctx, cmdArgs, showHelp);
|
|
153
190
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -43,6 +43,43 @@ 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
|
+
};
|
|
46
83
|
export interface EnrollmentExchangeResult {
|
|
47
84
|
apiUrl?: string;
|
|
48
85
|
workspace: string;
|
|
@@ -67,9 +104,19 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
67
104
|
put(body: Uint8Array, opts: PutOptions & {
|
|
68
105
|
filename: string;
|
|
69
106
|
}): Promise<PutResult>;
|
|
70
|
-
list(opts?: ListOptions)
|
|
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[]>;
|
|
71
112
|
delete(key: string): Promise<DeleteResult>;
|
|
72
113
|
head(key: string): Promise<HeadResult>;
|
|
73
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>;
|
|
74
121
|
};
|
|
75
122
|
export type UploadsClient = ReturnType<typeof createUploadsClient>;
|
package/dist/client.js
CHANGED
|
@@ -33,7 +33,10 @@ function encodeKeyPath(key) {
|
|
|
33
33
|
function filesBase(config) {
|
|
34
34
|
return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/files`;
|
|
35
35
|
}
|
|
36
|
-
function
|
|
36
|
+
function usageBase(config) {
|
|
37
|
+
return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/usage`;
|
|
38
|
+
}
|
|
39
|
+
function mapApiError(status, error, code) {
|
|
37
40
|
const normalized = error.toLowerCase();
|
|
38
41
|
if (status === 401 || normalized === "unauthorized") {
|
|
39
42
|
return new UploadsError(error, "UNAUTHORIZED", status);
|
|
@@ -44,6 +47,16 @@ function mapApiError(status, error) {
|
|
|
44
47
|
if (status === 400 && normalized === "invalid key") {
|
|
45
48
|
return new UploadsError(error, "INVALID_KEY", status);
|
|
46
49
|
}
|
|
50
|
+
if (code === "key_prefix_not_allowed" || code === "key_too_deep") {
|
|
51
|
+
return new UploadsError(error, "KEY_POLICY", status);
|
|
52
|
+
}
|
|
53
|
+
// Prefer stable body.code — bare 429 is also used for write rate limits.
|
|
54
|
+
if (status === 507 || code === "storage_quota_exceeded") {
|
|
55
|
+
return new UploadsError(error, "STORAGE_QUOTA", status);
|
|
56
|
+
}
|
|
57
|
+
if (code === "upload_budget_exceeded") {
|
|
58
|
+
return new UploadsError(error, "UPLOAD_BUDGET", status);
|
|
59
|
+
}
|
|
47
60
|
return new UploadsError(error, "API_ERROR", status);
|
|
48
61
|
}
|
|
49
62
|
async function parseErrorResponse(res) {
|
|
@@ -51,7 +64,10 @@ async function parseErrorResponse(res) {
|
|
|
51
64
|
const message = typeof body === "object" && body && "error" in body && typeof body.error === "string"
|
|
52
65
|
? body.error
|
|
53
66
|
: res.statusText || "request failed";
|
|
54
|
-
|
|
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);
|
|
55
71
|
}
|
|
56
72
|
export function createUploadsClient(config) {
|
|
57
73
|
async function request(method, path, opts) {
|
|
@@ -78,6 +94,17 @@ export function createUploadsClient(config) {
|
|
|
78
94
|
return undefined;
|
|
79
95
|
return (await res.json());
|
|
80
96
|
}
|
|
97
|
+
async function list(opts = {}) {
|
|
98
|
+
const params = new URLSearchParams();
|
|
99
|
+
if (opts.prefix)
|
|
100
|
+
params.set("prefix", opts.prefix);
|
|
101
|
+
if (opts.limit != null)
|
|
102
|
+
params.set("limit", String(opts.limit));
|
|
103
|
+
if (opts.cursor)
|
|
104
|
+
params.set("cursor", opts.cursor);
|
|
105
|
+
const qs = params.toString();
|
|
106
|
+
return request("GET", `${filesBase(config)}${qs ? `?${qs}` : ""}`);
|
|
107
|
+
}
|
|
81
108
|
return {
|
|
82
109
|
async put(body, opts) {
|
|
83
110
|
const key = opts.key ??
|
|
@@ -99,16 +126,17 @@ export function createUploadsClient(config) {
|
|
|
99
126
|
}
|
|
100
127
|
return { ...result, url: result.url };
|
|
101
128
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
list,
|
|
130
|
+
/** Follow cursors (optionally starting from one) and return every remaining item. */
|
|
131
|
+
async listAll(opts = {}) {
|
|
132
|
+
const items = [];
|
|
133
|
+
let cursor = opts.cursor;
|
|
134
|
+
do {
|
|
135
|
+
const page = await list({ ...opts, cursor });
|
|
136
|
+
items.push(...page.items);
|
|
137
|
+
cursor = page.cursor ?? undefined;
|
|
138
|
+
} while (cursor);
|
|
139
|
+
return items;
|
|
112
140
|
},
|
|
113
141
|
async delete(key) {
|
|
114
142
|
return request("DELETE", `${filesBase(config)}/${encodeKeyPath(key)}`);
|
|
@@ -119,5 +147,17 @@ export function createUploadsClient(config) {
|
|
|
119
147
|
async health() {
|
|
120
148
|
return request("GET", `${config.apiUrl}/health`, { auth: false });
|
|
121
149
|
},
|
|
150
|
+
/** Workspace storage / upload counters (+ limits when configured). */
|
|
151
|
+
async usage() {
|
|
152
|
+
return request("GET", usageBase(config));
|
|
153
|
+
},
|
|
154
|
+
/** Rebuild ledger bytes/objects from storage (source of truth). */
|
|
155
|
+
async reconcile() {
|
|
156
|
+
return request("POST", `${usageBase(config)}/reconcile`);
|
|
157
|
+
},
|
|
158
|
+
/** Delete objects past retentionDays (if set), then reconcile. */
|
|
159
|
+
async purgeExpired() {
|
|
160
|
+
return request("POST", `${usageBase(config)}/purge-expired`);
|
|
161
|
+
},
|
|
122
162
|
};
|
|
123
163
|
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { flagBool, flagString, parseCommandArgs, UsageError, } from "../cli-args.js";
|
|
2
|
+
import { resolveConfig } from "../config.js";
|
|
3
|
+
import { execRunner } from "../github-gh.js";
|
|
4
|
+
export const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
|
|
5
|
+
const SKILL_SOURCE = "buildinternet/uploads";
|
|
6
|
+
const SKILL_NAME = "uploads-cli";
|
|
7
|
+
const INSTALL_HELP = `uploads install — set up agent integrations (skill + remote MCP)
|
|
8
|
+
|
|
9
|
+
Installs the uploads-cli agent skill and registers the hosted MCP server
|
|
10
|
+
with Claude Code. The remote MCP endpoint infers your workspace from the
|
|
11
|
+
bearer token, so only the token is needed.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
uploads install [skill|mcp|all] (default: all)
|
|
15
|
+
|
|
16
|
+
What runs:
|
|
17
|
+
skill npx -y skills add ${SKILL_SOURCE} --skill ${SKILL_NAME}
|
|
18
|
+
mcp claude mcp add --transport http uploads ${DEFAULT_MCP_URL} \\
|
|
19
|
+
--header "Authorization: Bearer <token>"
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--url <endpoint> Remote MCP endpoint (default: ${DEFAULT_MCP_URL})
|
|
23
|
+
--name <name> MCP server name in the client (default: uploads)
|
|
24
|
+
--dry-run Print the commands without running them
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
uploads install
|
|
28
|
+
uploads install skill
|
|
29
|
+
uploads install mcp --dry-run
|
|
30
|
+
`;
|
|
31
|
+
/**
|
|
32
|
+
* Masks the configured token (and any Bearer credential) in text destined
|
|
33
|
+
* for stdout/stderr/JSON — command echoes, child-process output, and error
|
|
34
|
+
* messages can all embed it.
|
|
35
|
+
*/
|
|
36
|
+
function redactor(token) {
|
|
37
|
+
return (text) => {
|
|
38
|
+
let out = text.replace(/Bearer \S+/g, "Bearer ***");
|
|
39
|
+
if (token)
|
|
40
|
+
out = out.split(token).join("***");
|
|
41
|
+
return out;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function runStep(run, command) {
|
|
45
|
+
try {
|
|
46
|
+
const output = run(command[0], command.slice(1)).trim();
|
|
47
|
+
return { command, ok: true, output: output || undefined };
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
51
|
+
// execFileSync's ENOENT means the binary itself is missing.
|
|
52
|
+
const hint = err.code === "ENOENT"
|
|
53
|
+
? `${command[0]} not found on PATH — run manually: ${command.join(" ")}`
|
|
54
|
+
: message;
|
|
55
|
+
return { command, ok: false, error: hint };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function runInstall(args, opts, help = false) {
|
|
59
|
+
const parsed = parseCommandArgs(args);
|
|
60
|
+
if (help || parsed.help) {
|
|
61
|
+
process.stderr.write(INSTALL_HELP);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
const target = parsed.positionals[0] ?? "all";
|
|
65
|
+
if (!["skill", "mcp", "all"].includes(target)) {
|
|
66
|
+
throw new UsageError(`unknown install target: ${target} (expected skill, mcp, or all)`);
|
|
67
|
+
}
|
|
68
|
+
const url = flagString(parsed.flags, "--url") ?? DEFAULT_MCP_URL;
|
|
69
|
+
const name = flagString(parsed.flags, "--name") ?? "uploads";
|
|
70
|
+
const dryRun = flagBool(parsed.flags, "--dry-run");
|
|
71
|
+
const run = opts.runner ?? execRunner;
|
|
72
|
+
const results = {};
|
|
73
|
+
let redact = redactor(undefined);
|
|
74
|
+
if (target === "skill" || target === "all") {
|
|
75
|
+
const command = ["npx", "-y", "skills", "add", SKILL_SOURCE, "--skill", SKILL_NAME];
|
|
76
|
+
results.skill = dryRun ? { command, ok: true, skipped: "dry-run" } : runStep(run, command);
|
|
77
|
+
}
|
|
78
|
+
if (target === "mcp" || target === "all") {
|
|
79
|
+
const config = resolveConfig({
|
|
80
|
+
apiUrl: opts.globals.apiUrl,
|
|
81
|
+
workspace: opts.globals.workspace,
|
|
82
|
+
token: opts.globals.token,
|
|
83
|
+
envFile: opts.globals.envFile,
|
|
84
|
+
requireToken: !dryRun,
|
|
85
|
+
});
|
|
86
|
+
const bearer = config.token || "<token>";
|
|
87
|
+
redact = redactor(config.token || undefined);
|
|
88
|
+
const command = [
|
|
89
|
+
"claude",
|
|
90
|
+
"mcp",
|
|
91
|
+
"add",
|
|
92
|
+
"--transport",
|
|
93
|
+
"http",
|
|
94
|
+
name,
|
|
95
|
+
url,
|
|
96
|
+
"--header",
|
|
97
|
+
`Authorization: Bearer ${bearer}`,
|
|
98
|
+
];
|
|
99
|
+
results.mcp = dryRun ? { command, ok: true, skipped: "dry-run" } : runStep(run, command);
|
|
100
|
+
}
|
|
101
|
+
const failed = Object.values(results).some((r) => !r.ok);
|
|
102
|
+
if (opts.json) {
|
|
103
|
+
// Never echo the token in structured output — commands, child output,
|
|
104
|
+
// and error text can all embed it.
|
|
105
|
+
const redacted = Object.fromEntries(Object.entries(results).map(([key, r]) => [
|
|
106
|
+
key,
|
|
107
|
+
{
|
|
108
|
+
...r,
|
|
109
|
+
command: r.command.map(redact),
|
|
110
|
+
output: r.output === undefined ? undefined : redact(r.output),
|
|
111
|
+
error: r.error === undefined ? undefined : redact(r.error),
|
|
112
|
+
},
|
|
113
|
+
]));
|
|
114
|
+
process.stdout.write(JSON.stringify({ ok: !failed, steps: redacted }, null, 2) + "\n");
|
|
115
|
+
return failed ? 1 : 0;
|
|
116
|
+
}
|
|
117
|
+
for (const [step, r] of Object.entries(results)) {
|
|
118
|
+
const shown = redact(r.command.join(" "));
|
|
119
|
+
if (r.skipped)
|
|
120
|
+
process.stdout.write(`${step}: would run — ${shown}\n`);
|
|
121
|
+
else if (r.ok) {
|
|
122
|
+
process.stdout.write(`${step}: ok — ${shown}\n`);
|
|
123
|
+
if (r.output)
|
|
124
|
+
process.stdout.write(` ${redact(r.output).split("\n").join("\n ")}\n`);
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
process.stderr.write(`${step}: failed — ${redact(r.error ?? "")}\n`);
|
|
128
|
+
}
|
|
129
|
+
if (!failed && !dryRun) {
|
|
130
|
+
process.stderr.write("hint: restart your agent session to pick up the new skill/server\n");
|
|
131
|
+
}
|
|
132
|
+
return failed ? 1 : 0;
|
|
133
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { parseCommandArgs } from "../cli-args.js";
|
|
3
|
+
import { createMcpServer } from "../mcp/server.js";
|
|
4
|
+
import { serveStdio } from "../mcp/stdio.js";
|
|
5
|
+
import { createUploadsMcpTools } from "../mcp/tools.js";
|
|
6
|
+
const MCP_HELP = `uploads [globals] mcp
|
|
7
|
+
|
|
8
|
+
Serve the Model Context Protocol (MCP) over stdio for agent clients. Tools
|
|
9
|
+
mirror the CLI commands: put, attach, list, delete, usage, reconcile,
|
|
10
|
+
purge_expired, comment, health, doctor.
|
|
11
|
+
Global flags before "mcp" (--api-url, --token, --workspace, --env-file)
|
|
12
|
+
configure every tool call; a per-call "workspace" argument overrides
|
|
13
|
+
--workspace, like the CLI's per-command flag.
|
|
14
|
+
|
|
15
|
+
Example MCP client config:
|
|
16
|
+
{
|
|
17
|
+
"command": "uploads",
|
|
18
|
+
"args": ["--env-file", "/path/.env", "mcp"]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
uploads --env-file .env mcp
|
|
23
|
+
uploads --token up_default_… mcp
|
|
24
|
+
`;
|
|
25
|
+
// Same relative depth from src/commands/ and dist/commands/, so this works
|
|
26
|
+
// both under vitest (src) and at runtime (dist).
|
|
27
|
+
const { version } = createRequire(import.meta.url)("../../package.json");
|
|
28
|
+
export async function runMcp(args, opts, help = false) {
|
|
29
|
+
if (help || parseCommandArgs(args).help) {
|
|
30
|
+
process.stderr.write(MCP_HELP);
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
const server = createMcpServer({
|
|
34
|
+
serverInfo: { name: "uploads", version },
|
|
35
|
+
tools: createUploadsMcpTools({ globals: opts.globals }),
|
|
36
|
+
});
|
|
37
|
+
await serveStdio(server);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { type UploadsClient } from "./client.js";
|
|
2
|
+
import { type CommandFlags } from "./cli-args.js";
|
|
2
3
|
import { type ResolvedConfig } from "./config.js";
|
|
4
|
+
import { type GhTarget } from "./github.js";
|
|
3
5
|
import { type CommandRunner } from "./github-gh.js";
|
|
6
|
+
import { type OptimizeImageOptions, type OptimizeImageResult } from "./optimize.js";
|
|
7
|
+
import { type FrameResult } from "./frame.js";
|
|
8
|
+
import type { PutDefaults } from "./config-file.js";
|
|
4
9
|
export interface CliContext {
|
|
5
10
|
config: ResolvedConfig;
|
|
6
11
|
client: UploadsClient;
|
|
@@ -8,12 +13,70 @@ export interface CliContext {
|
|
|
8
13
|
quiet: boolean;
|
|
9
14
|
envFile?: string;
|
|
10
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Turns a pr/issue pair (+ optional repo) into a GhTarget; undefined when
|
|
18
|
+
* neither is present. Shared by the CLI flags and the MCP tool arguments.
|
|
19
|
+
*/
|
|
20
|
+
export declare function makeGhTarget(pr: number | undefined, issue: number | undefined, repoArg: string | undefined, run: CommandRunner): GhTarget | undefined;
|
|
21
|
+
/** Shared put/attach optimize flags + UPLOADS_NO_OPTIMIZE default. */
|
|
22
|
+
export declare function optimizeOptionsFromFlags(flags: CommandFlags["flags"], defaults: PutDefaults): OptimizeImageOptions;
|
|
23
|
+
export type PreparedUpload = OptimizeImageResult & {
|
|
24
|
+
frame?: Pick<FrameResult, "framed" | "frameId" | "skippedReason">;
|
|
25
|
+
};
|
|
26
|
+
/** Frame (optional) then optimize — shared by put/attach/MCP. */
|
|
27
|
+
export declare function prepareImageForUpload(bytes: Uint8Array, filename: string, opts: {
|
|
28
|
+
frameId?: string;
|
|
29
|
+
frameUrl?: string;
|
|
30
|
+
frameFit?: "cover" | "contain";
|
|
31
|
+
optimize: OptimizeImageOptions;
|
|
32
|
+
}): Promise<PreparedUpload>;
|
|
33
|
+
/**
|
|
34
|
+
* List every attachment under the target's prefix and create/update the
|
|
35
|
+
* managed comment. Throws on gh failure — callers decide whether that is
|
|
36
|
+
* fatal (`comment` command) or a warning (`put --comment`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function syncAttachmentsComment(client: UploadsClient, target: GhTarget, run: CommandRunner): Promise<{
|
|
39
|
+
action: "created" | "updated" | "skipped";
|
|
40
|
+
count: number;
|
|
41
|
+
}>;
|
|
11
42
|
export declare function runAttach(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
|
12
43
|
export declare function runPut(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
|
13
44
|
export declare function runList(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
|
14
45
|
export declare function runDelete(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
|
|
15
46
|
export declare function runComment(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
|
|
47
|
+
export declare function runUsage(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
|
|
48
|
+
export declare function runReconcile(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
|
|
49
|
+
export declare function runPurgeExpired(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
|
|
16
50
|
export declare function runHealth(ctx: Pick<CliContext, "json"> & {
|
|
17
51
|
apiUrl: string;
|
|
18
52
|
}, args: string[], help?: boolean): Promise<number>;
|
|
53
|
+
export interface DoctorReport {
|
|
54
|
+
ok: boolean;
|
|
55
|
+
apiUrl: string;
|
|
56
|
+
workspace: string;
|
|
57
|
+
workspaceSource: ResolvedConfig["workspaceSource"];
|
|
58
|
+
workspaceFromToken: string | undefined;
|
|
59
|
+
configPath: string;
|
|
60
|
+
configExists: boolean;
|
|
61
|
+
health: {
|
|
62
|
+
ok: boolean;
|
|
63
|
+
};
|
|
64
|
+
auth: {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
error: string | undefined;
|
|
67
|
+
};
|
|
68
|
+
/** Usage snapshot when auth works (optional fields when the endpoint fails). */
|
|
69
|
+
usage?: {
|
|
70
|
+
ok: boolean;
|
|
71
|
+
bytes?: number;
|
|
72
|
+
objects?: number;
|
|
73
|
+
uploadsInPeriod?: number;
|
|
74
|
+
error?: string;
|
|
75
|
+
};
|
|
76
|
+
/** Workspace/token mismatch warning (also present in hints). */
|
|
77
|
+
warning?: string;
|
|
78
|
+
hints: string[];
|
|
79
|
+
}
|
|
80
|
+
/** Doctor's health + auth + workspace checks, shared by the CLI and the MCP tool. */
|
|
81
|
+
export declare function buildDoctorReport(config: ResolvedConfig, client: UploadsClient): Promise<DoctorReport>;
|
|
19
82
|
export declare function runDoctor(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
|