@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 +21 -0
- package/README.md +17 -2
- package/dist/cli.js +27 -2
- package/dist/client.d.ts +48 -1
- package/dist/client.js +49 -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 +47 -0
- package/dist/commands.js +150 -57
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- 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 +386 -0
- package/package.json +12 -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
|
@@ -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`, `
|
|
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:
|
|
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)
|
|
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,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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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,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,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>;
|