@buildinternet/uploads 0.1.1 → 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
@@ -21,7 +21,8 @@ pnpm uploads put ./after.png --pr 123 --comment --env-file .env
21
21
  pnpm uploads doctor --env-file .env
22
22
  ```
23
23
 
24
- Commands: `attach`, `put`, `comment`, `list`, `delete`, `setup`, `config`, `doctor`, `health`.
24
+ Commands: `attach`, `put`, `comment`, `list`, `delete`, `usage`, `reconcile`,
25
+ `purge-expired`, `setup`, `install`, `config`, `doctor`, `health`, `mcp`.
25
26
 
26
27
  `attach` is the agent-friendly default for GitHub media. It accepts one or more files,
27
28
  infers the pull request for the current branch via `gh`, uploads stable URLs, and creates
@@ -30,13 +31,25 @@ the target explicitly, or `--no-comment` to upload without changing GitHub comme
30
31
 
31
32
  Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
32
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
+
33
46
  ## Programmatic use
34
47
 
35
48
  ```ts
36
49
  import { createUploadsClient } from "@buildinternet/uploads";
37
50
  ```
38
51
 
39
- Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools` for Workers).
52
+ Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools` for Workers); for local stdio MCP, use `uploads mcp` (above).
40
53
 
41
54
  ## Layout
42
55
 
@@ -44,6 +57,8 @@ Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools
44
57
  src/
45
58
  cli.ts Entry + help
46
59
  commands.ts put, list, delete, comment, …
60
+ commands/mcp.ts `mcp` command entry
61
+ mcp/ Stdio MCP server (server.ts, tools.ts)
47
62
  client.ts HTTP client for the API
48
63
  github.ts PR/issue key paths + attachment comments
49
64
  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: use createUploadsWorkerFileTools() from @buildinternet/uploads/agent on the Worker.
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,8 @@ function exitCode(err) {
84
94
  return 2;
85
95
  case "UNAUTHORIZED":
86
96
  case "NOT_FOUND":
97
+ case "STORAGE_QUOTA":
98
+ case "UPLOAD_BUDGET":
87
99
  return 3;
88
100
  case "NETWORK":
89
101
  return 4;
@@ -130,10 +142,17 @@ export async function runCli(argv) {
130
142
  return runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
131
143
  case "admin":
132
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);
133
149
  case "attach":
134
150
  case "put":
135
151
  case "list":
136
152
  case "delete":
153
+ case "usage":
154
+ case "reconcile":
155
+ case "purge-expired":
137
156
  case "doctor":
138
157
  case "comment": {
139
158
  const ctx = createContext(parsed.globals, !showHelp, cmdArgs);
@@ -148,6 +167,12 @@ export async function runCli(argv) {
148
167
  return runList(ctx, cmdArgs, showHelp);
149
168
  case "delete":
150
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);
151
176
  case "doctor":
152
177
  return runDoctor(ctx, cmdArgs, showHelp);
153
178
  }
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): 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[]>;
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 mapApiError(status, error) {
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,13 @@ function mapApiError(status, error) {
44
47
  if (status === 400 && normalized === "invalid key") {
45
48
  return new UploadsError(error, "INVALID_KEY", status);
46
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
+ }
47
57
  return new UploadsError(error, "API_ERROR", status);
48
58
  }
49
59
  async function parseErrorResponse(res) {
@@ -51,7 +61,10 @@ async function parseErrorResponse(res) {
51
61
  const message = typeof body === "object" && body && "error" in body && typeof body.error === "string"
52
62
  ? body.error
53
63
  : res.statusText || "request failed";
54
- 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);
55
68
  }
56
69
  export function createUploadsClient(config) {
57
70
  async function request(method, path, opts) {
@@ -78,6 +91,17 @@ export function createUploadsClient(config) {
78
91
  return undefined;
79
92
  return (await res.json());
80
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
+ }
81
105
  return {
82
106
  async put(body, opts) {
83
107
  const key = opts.key ??
@@ -99,16 +123,17 @@ export function createUploadsClient(config) {
99
123
  }
100
124
  return { ...result, url: result.url };
101
125
  },
102
- async list(opts = {}) {
103
- const params = new URLSearchParams();
104
- if (opts.prefix)
105
- params.set("prefix", opts.prefix);
106
- if (opts.limit != null)
107
- params.set("limit", String(opts.limit));
108
- if (opts.cursor)
109
- params.set("cursor", opts.cursor);
110
- const qs = params.toString();
111
- 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;
112
137
  },
113
138
  async delete(key) {
114
139
  return request("DELETE", `${filesBase(config)}/${encodeKeyPath(key)}`);
@@ -119,5 +144,17 @@ export function createUploadsClient(config) {
119
144
  async health() {
120
145
  return request("GET", `${config.apiUrl}/health`, { auth: false });
121
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
+ },
122
159
  };
123
160
  }
@@ -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,4 @@
1
+ import { type GlobalFlags } from "../cli-args.js";
2
+ export declare function runMcp(args: string[], opts: {
3
+ globals: GlobalFlags;
4
+ }, help?: boolean): Promise<number>;
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  import { type UploadsClient } from "./client.js";
2
2
  import { type ResolvedConfig } from "./config.js";
3
+ import { type GhTarget } from "./github.js";
3
4
  import { type CommandRunner } from "./github-gh.js";
4
5
  export interface CliContext {
5
6
  config: ResolvedConfig;
@@ -8,12 +9,58 @@ export interface CliContext {
8
9
  quiet: boolean;
9
10
  envFile?: string;
10
11
  }
12
+ /**
13
+ * Turns a pr/issue pair (+ optional repo) into a GhTarget; undefined when
14
+ * neither is present. Shared by the CLI flags and the MCP tool arguments.
15
+ */
16
+ export declare function makeGhTarget(pr: number | undefined, issue: number | undefined, repoArg: string | undefined, run: CommandRunner): GhTarget | undefined;
17
+ /**
18
+ * List every attachment under the target's prefix and create/update the
19
+ * managed comment. Throws on gh failure — callers decide whether that is
20
+ * fatal (`comment` command) or a warning (`put --comment`).
21
+ */
22
+ export declare function syncAttachmentsComment(client: UploadsClient, target: GhTarget, run: CommandRunner): Promise<{
23
+ action: "created" | "updated" | "skipped";
24
+ count: number;
25
+ }>;
11
26
  export declare function runAttach(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
12
27
  export declare function runPut(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
13
28
  export declare function runList(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
14
29
  export declare function runDelete(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
15
30
  export declare function runComment(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
31
+ export declare function runUsage(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
32
+ export declare function runReconcile(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
33
+ export declare function runPurgeExpired(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
16
34
  export declare function runHealth(ctx: Pick<CliContext, "json"> & {
17
35
  apiUrl: string;
18
36
  }, args: string[], help?: boolean): Promise<number>;
37
+ export interface DoctorReport {
38
+ ok: boolean;
39
+ apiUrl: string;
40
+ workspace: string;
41
+ workspaceSource: ResolvedConfig["workspaceSource"];
42
+ workspaceFromToken: string | undefined;
43
+ configPath: string;
44
+ configExists: boolean;
45
+ health: {
46
+ ok: boolean;
47
+ };
48
+ auth: {
49
+ ok: boolean;
50
+ error: string | undefined;
51
+ };
52
+ /** Usage snapshot when auth works (optional fields when the endpoint fails). */
53
+ usage?: {
54
+ ok: boolean;
55
+ bytes?: number;
56
+ objects?: number;
57
+ uploadsInPeriod?: number;
58
+ error?: string;
59
+ };
60
+ /** Workspace/token mismatch warning (also present in hints). */
61
+ warning?: string;
62
+ hints: string[];
63
+ }
64
+ /** Doctor's health + auth + workspace checks, shared by the CLI and the MCP tool. */
65
+ export declare function buildDoctorReport(config: ResolvedConfig, client: UploadsClient): Promise<DoctorReport>;
19
66
  export declare function runDoctor(ctx: CliContext, args: string[], help?: boolean): Promise<number>;