@buildinternet/uploads 0.46.0 → 0.46.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli-catalog.js +1 -1
- package/dist/cli-help.js +4 -3
- package/dist/client.js +95 -15
- package/dist/commands/install.d.ts +13 -1
- package/dist/commands/install.js +192 -87
- package/dist/commands/login.js +1 -1
- package/dist/commands/update.js +3 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -204,7 +204,7 @@ Config layers (first match wins): CLI flags → env vars → `--env-file` → `~
|
|
|
204
204
|
|
|
205
205
|
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`.
|
|
206
206
|
|
|
207
|
-
For HTTP clients there's also a hosted variant at `https://agents.uploads.sh/mcp` — the workspace is inferred from the bearer token, so only the URL and token are needed (`https://agents.uploads.sh/<workspace>/mcp` and the `mcp.uploads.sh` hostname also work). Tools: file operations (including `get_metadata` / `set_metadata` / `find_files`) plus `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`; all use the same bearer-token workspace scopes and gallery URLs come from the API — see `apps/mcp` in the repo. The hosted `put` also accepts a `metadata` param. `uploads install` registers the skills + hosted MCP + Grok/Cursor hooks (short progress; `--verbose` for underlying output). Claude and Codex use their plugins for the same pre-PR screenshot reminder (`uploads hook pre-pr-screenshot`). 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.
|
|
207
|
+
For HTTP clients there's also a hosted variant at `https://agents.uploads.sh/mcp` — the workspace is inferred from the bearer token, so only the URL and token are needed (`https://agents.uploads.sh/<workspace>/mcp` and the `mcp.uploads.sh` hostname also work). Tools: file operations (including `get_metadata` / `set_metadata` / `find_files`) plus `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`; all use the same bearer-token workspace scopes and gallery URLs come from the API — see `apps/mcp` in the repo. The hosted `put` also accepts a `metadata` param. `uploads install` registers the skills + hosted MCP with whichever of Claude Code, Codex, and Grok are on PATH (a missing CLI is skipped) + Grok/Cursor hooks (short progress; `--verbose` for underlying output). Claude and Codex use their plugins for the same pre-PR screenshot reminder (`uploads hook pre-pr-screenshot`). 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.
|
|
208
208
|
|
|
209
209
|
## Programmatic use
|
|
210
210
|
|
package/dist/cli-catalog.js
CHANGED
|
@@ -212,7 +212,7 @@ export const ROOT_COMMANDS = [
|
|
|
212
212
|
essential: true,
|
|
213
213
|
subcommands: [
|
|
214
214
|
{ name: "skill", summary: "Install the agent skills only" },
|
|
215
|
-
{ name: "mcp", summary: "Register the remote MCP server
|
|
215
|
+
{ name: "mcp", summary: "Register the remote MCP server (skips missing agent CLIs)" },
|
|
216
216
|
{ name: "hooks", summary: "Install PR screenshot hooks for Grok/Cursor" },
|
|
217
217
|
{ name: "all", summary: "Install skills, MCP, and hooks (default)" },
|
|
218
218
|
],
|
package/dist/cli-help.js
CHANGED
|
@@ -167,9 +167,10 @@ ${section(style, "Examples:")}
|
|
|
167
167
|
${style.command("uploads logout")}
|
|
168
168
|
${style.command("uploads --version")}
|
|
169
169
|
|
|
170
|
-
${section(style, "Agent/MCP:")} ${style.body("`uploads install` sets up skills, hosted MCP
|
|
171
|
-
${style.body("
|
|
172
|
-
${style.body("hook. Run `uploads mcp`
|
|
170
|
+
${section(style, "Agent/MCP:")} ${style.body("`uploads install` sets up skills, hosted MCP (Claude/Codex/Grok;")}
|
|
171
|
+
${style.body("skips any CLI that is not on PATH), and hooks for Grok/Cursor. Claude and")}
|
|
172
|
+
${style.body("Codex use their plugins for the same PR-screenshot hook. Run `uploads mcp`")}
|
|
173
|
+
${style.body("for local stdio.")}
|
|
173
174
|
|
|
174
175
|
${style.muted("Tip: uploads help essentials only")}
|
|
175
176
|
${style.muted(" uploads help --all this full listing")}
|
package/dist/client.js
CHANGED
|
@@ -4,14 +4,104 @@ import { UploadsError } from "./errors.js";
|
|
|
4
4
|
import { buildScreenshotKey } from "./keys.js";
|
|
5
5
|
import { packageVersion } from "./package-version.js";
|
|
6
6
|
import { resolveEmbedUrl } from "./public-urls.js";
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// --- Request resilience (issue #809) ---------------------------------------
|
|
8
|
+
//
|
|
9
|
+
// Bare `fetch` has no timeout — undici's default header timeout is ~5
|
|
10
|
+
// minutes, so a backend stall (see the 2026-08-23 D1 stall incident, #808)
|
|
11
|
+
// hangs the CLI silently for that whole window instead of failing fast like
|
|
12
|
+
// the workers now do (#805-#807). Every core API call routes through
|
|
13
|
+
// `resilientFetch` for a bounded timeout and a single bounded retry.
|
|
14
|
+
//
|
|
15
|
+
// Timeouts: short for JSON control calls, longer for the one content-bytes
|
|
16
|
+
// call (file `put`), matching the side-channel calls' pattern (telemetry.ts,
|
|
17
|
+
// update-check.ts already carry AbortController timeouts — this closes the
|
|
18
|
+
// gap on the core path).
|
|
19
|
+
const JSON_TIMEOUT_MS = 15_000;
|
|
20
|
+
const CONTENT_TIMEOUT_MS = 60_000;
|
|
21
|
+
// At most one retry. Retried on network errors, 503, and 429 — and only for
|
|
22
|
+
// GET/PUT, since a `put` is byte-idempotent (same key, same bytes) but a
|
|
23
|
+
// POST/DELETE/PATCH might not be, and telling "no bytes sent" apart from "the
|
|
24
|
+
// mutation already landed" isn't reliable enough to risk a double-apply.
|
|
25
|
+
const MAX_ATTEMPTS = 2;
|
|
26
|
+
const RETRYABLE_METHODS = new Set(["GET", "PUT"]);
|
|
27
|
+
const RETRYABLE_STATUSES = new Set([429, 503]);
|
|
28
|
+
const DEFAULT_RETRY_DELAY_MS = 2_000;
|
|
29
|
+
// Cap an honored X-Retry-After so a large server-suggested backoff can't
|
|
30
|
+
// stall a hook/CI step for minutes.
|
|
31
|
+
const MAX_RETRY_AFTER_DELAY_MS = 10_000;
|
|
32
|
+
function sleep(ms) {
|
|
33
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
}
|
|
35
|
+
/** Seconds-valued retry delay from the response, capped; falls back to the default backoff. */
|
|
36
|
+
function retryDelayMs(res) {
|
|
37
|
+
const raw = res?.headers.get("x-retry-after") ?? res?.headers.get("retry-after");
|
|
38
|
+
const seconds = raw ? Number(raw) : NaN;
|
|
39
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
40
|
+
return Math.min(seconds * 1000, MAX_RETRY_AFTER_DELAY_MS);
|
|
41
|
+
}
|
|
42
|
+
return DEFAULT_RETRY_DELAY_MS;
|
|
43
|
+
}
|
|
44
|
+
/** One-line stderr notice so a hook/CI log explains the pause (issue #809). */
|
|
45
|
+
function printRetryNotice(label, delayMs) {
|
|
46
|
+
const seconds = delayMs % 1000 === 0 ? `${delayMs / 1000}s` : `${Math.round(delayMs) / 1000}s`;
|
|
47
|
+
process.stderr.write(`warning: uploads.sh ${label}, retrying in ${seconds}…\n`);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* fetch with a per-request AbortController timeout. A timeout (or any other
|
|
51
|
+
* fetch rejection) surfaces as `UploadsError` code `NETWORK`, naming the
|
|
52
|
+
* timeout so hook/CI logs are diagnosable.
|
|
53
|
+
*/
|
|
54
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
9
57
|
try {
|
|
10
|
-
|
|
58
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
11
59
|
}
|
|
12
60
|
catch (err) {
|
|
61
|
+
if (controller.signal.aborted) {
|
|
62
|
+
throw new UploadsError(`request timed out after ${timeoutMs}ms`, "NETWORK");
|
|
63
|
+
}
|
|
13
64
|
throw new UploadsError(err instanceof Error ? err.message : "network request failed", "NETWORK");
|
|
14
65
|
}
|
|
66
|
+
finally {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The shared resilience core for both `jsonRequest` and
|
|
72
|
+
* `createUploadsClient`'s `request`: bounded timeout + bounded retry on
|
|
73
|
+
* network errors, 503, and 429, honoring `X-Retry-After` when present.
|
|
74
|
+
* Never retries a non-idempotent method (see `RETRYABLE_METHODS` above).
|
|
75
|
+
* Returns the raw `Response` on the final attempt — callers still handle
|
|
76
|
+
* `!res.ok` themselves via `parseErrorResponse`.
|
|
77
|
+
*/
|
|
78
|
+
async function resilientFetch(method, url, init, timeoutMs) {
|
|
79
|
+
const retryable = RETRYABLE_METHODS.has(method.toUpperCase());
|
|
80
|
+
for (let attempt = 1;; attempt++) {
|
|
81
|
+
let res;
|
|
82
|
+
let networkErr;
|
|
83
|
+
try {
|
|
84
|
+
res = await fetchWithTimeout(url, init, timeoutMs);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
networkErr = err;
|
|
88
|
+
}
|
|
89
|
+
const canRetry = retryable &&
|
|
90
|
+
attempt < MAX_ATTEMPTS &&
|
|
91
|
+
(networkErr !== undefined || (res !== undefined && RETRYABLE_STATUSES.has(res.status)));
|
|
92
|
+
if (!canRetry) {
|
|
93
|
+
if (networkErr)
|
|
94
|
+
throw networkErr;
|
|
95
|
+
return res;
|
|
96
|
+
}
|
|
97
|
+
const delayMs = retryDelayMs(res);
|
|
98
|
+
printRetryNotice(res ? `responded ${res.status}` : "request failed", delayMs);
|
|
99
|
+
await sleep(delayMs);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function jsonRequest(url, init) {
|
|
103
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
104
|
+
const res = await resilientFetch(method, url, init, JSON_TIMEOUT_MS);
|
|
15
105
|
if (!res.ok)
|
|
16
106
|
throw await parseErrorResponse(res);
|
|
17
107
|
return (await res.json());
|
|
@@ -280,18 +370,7 @@ export function createUploadsClient(config) {
|
|
|
280
370
|
if (opts?.auth !== false) {
|
|
281
371
|
headers.Authorization = `Bearer ${config.token}`;
|
|
282
372
|
}
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
res = await fetch(path, {
|
|
286
|
-
method,
|
|
287
|
-
headers,
|
|
288
|
-
body: opts?.body,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
catch (err) {
|
|
292
|
-
const message = err instanceof Error ? err.message : "network request failed";
|
|
293
|
-
throw new UploadsError(message, "NETWORK");
|
|
294
|
-
}
|
|
373
|
+
const res = await resilientFetch(method, path, { method, headers, body: opts?.body }, opts?.longTimeout ? CONTENT_TIMEOUT_MS : JSON_TIMEOUT_MS);
|
|
295
374
|
if (!res.ok) {
|
|
296
375
|
throw await parseErrorResponse(res);
|
|
297
376
|
}
|
|
@@ -383,6 +462,7 @@ export function createUploadsClient(config) {
|
|
|
383
462
|
const result = await request("PUT", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}`, {
|
|
384
463
|
body,
|
|
385
464
|
headers,
|
|
465
|
+
longTimeout: true,
|
|
386
466
|
});
|
|
387
467
|
if (result.url == null) {
|
|
388
468
|
throw new UploadsError("upload succeeded but workspace has no publicBaseUrl", "NO_PUBLIC_URL", 201);
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { type GlobalFlags } from "../cli-args.js";
|
|
2
2
|
import { type CommandRunner } from "../github-gh.js";
|
|
3
3
|
export declare const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
|
|
4
|
+
type McpClientId = "claude" | "codex" | "grok";
|
|
5
|
+
interface McpClient {
|
|
6
|
+
id: McpClientId;
|
|
7
|
+
label: string;
|
|
8
|
+
command: (name: string, url: string, bearer: string) => string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Agent CLIs that can register the hosted MCP server. Each is attempted
|
|
12
|
+
* independently; a missing binary is skipped so the others can still install.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MCP_CLIENTS: readonly McpClient[];
|
|
4
15
|
export interface StepResult {
|
|
5
16
|
command: string[];
|
|
6
17
|
ok: boolean;
|
|
7
|
-
skipped?: "dry-run" | "sign-in" | "already-configured";
|
|
18
|
+
skipped?: "dry-run" | "sign-in" | "already-configured" | "missing-cli";
|
|
8
19
|
error?: string;
|
|
9
20
|
output?: string;
|
|
10
21
|
}
|
|
@@ -27,3 +38,4 @@ export declare function runInstall(args: string[], opts: {
|
|
|
27
38
|
/** Override home for hook installs (tests). */
|
|
28
39
|
home?: string;
|
|
29
40
|
}, help?: boolean): Promise<number>;
|
|
41
|
+
export {};
|
package/dist/commands/install.js
CHANGED
|
@@ -6,20 +6,62 @@ import { HOOK_COMMAND, HOOK_INVOCATION, installHookManifests, } from "../hooks-i
|
|
|
6
6
|
export const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
|
|
7
7
|
const SKILL_SOURCE = "buildinternet/uploads";
|
|
8
8
|
const SKILL_NAMES = ["uploads-cli", "github-screenshots", "annotate-screenshots"];
|
|
9
|
+
/** `mcp add --transport http` with an Authorization header. */
|
|
10
|
+
function httpMcpAdd(binary, name, url, bearer) {
|
|
11
|
+
return [
|
|
12
|
+
binary,
|
|
13
|
+
"mcp",
|
|
14
|
+
"add",
|
|
15
|
+
"--transport",
|
|
16
|
+
"http",
|
|
17
|
+
name,
|
|
18
|
+
url,
|
|
19
|
+
"--header",
|
|
20
|
+
`Authorization: Bearer ${bearer}`,
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Agent CLIs that can register the hosted MCP server. Each is attempted
|
|
25
|
+
* independently; a missing binary is skipped so the others can still install.
|
|
26
|
+
*/
|
|
27
|
+
export const MCP_CLIENTS = [
|
|
28
|
+
{
|
|
29
|
+
id: "claude",
|
|
30
|
+
label: "Claude Code",
|
|
31
|
+
command: (name, url, bearer) => httpMcpAdd("claude", name, url, bearer),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "codex",
|
|
35
|
+
label: "Codex",
|
|
36
|
+
// Codex HTTP MCP has no --header; auth is OAuth on first use (same as the
|
|
37
|
+
// plugin's .mcp.json). Passing --bearer-token-env-var UPLOADS_TOKEN would
|
|
38
|
+
// break machines that signed in via `uploads login` (token lives in the
|
|
39
|
+
// config file, not the environment).
|
|
40
|
+
command: (name, url) => ["codex", "mcp", "add", name, "--url", url],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "grok",
|
|
44
|
+
label: "Grok",
|
|
45
|
+
command: (name, url, bearer) => httpMcpAdd("grok", name, url, bearer),
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
const MCP_CLIENT_BINARIES = MCP_CLIENTS.map((c) => c.id).join(", ");
|
|
9
49
|
const INSTALL_HELP = `uploads install — set up agent integrations (skills + remote MCP + hooks)
|
|
10
50
|
|
|
11
51
|
Installs the github-screenshots, uploads-cli, and annotate-screenshots agent
|
|
12
|
-
skills, registers the
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
52
|
+
skills, registers the hosted MCP server with whichever of Claude Code, Codex,
|
|
53
|
+
and Grok are on PATH, and installs the PR screenshot reminder hook for
|
|
54
|
+
Grok / Cursor when those tools are present. A missing agent CLI is skipped —
|
|
55
|
+
it does not fail the rest of the install. The remote MCP endpoint infers your
|
|
56
|
+
workspace from the bearer token, so only the token is needed.
|
|
16
57
|
|
|
17
58
|
Claude Code and Codex ship the same reminder via their plugins (same command:
|
|
18
59
|
\`${HOOK_INVOCATION}\`) — install those plugins instead of relying on this step.
|
|
19
60
|
|
|
20
61
|
Safe to re-run. An MCP server already registered under this name is reported
|
|
21
62
|
as \`already configured\` and left as-is — including the token it was created
|
|
22
|
-
with. To point it at a new token:
|
|
63
|
+
with. To point it at a new token: \`<cli> mcp remove <name>\` first
|
|
64
|
+
(e.g. \`claude mcp remove uploads\`).
|
|
23
65
|
|
|
24
66
|
Usage:
|
|
25
67
|
uploads install [skill|mcp|hooks|all] (default: all)
|
|
@@ -28,7 +70,8 @@ What it does:
|
|
|
28
70
|
skill Agent skills (via npx skills) — github-screenshots: visuals into
|
|
29
71
|
PRs/issues; uploads-cli: full CLI reference; annotate-screenshots:
|
|
30
72
|
hand-drawn callouts and redaction on screenshots
|
|
31
|
-
mcp Hosted MCP server in Claude Code —
|
|
73
|
+
mcp Hosted MCP server in Claude Code, Codex, and Grok — each CLI that
|
|
74
|
+
is installed is registered; missing ones are skipped
|
|
32
75
|
hooks PR screenshot reminder for Grok / Cursor (user-global manifests)
|
|
33
76
|
|
|
34
77
|
What runs under the hood:
|
|
@@ -37,7 +80,10 @@ What runs under the hood:
|
|
|
37
80
|
with npx on PATH — missing tooling fails once with install guidance)
|
|
38
81
|
mcp claude mcp add --transport http uploads ${DEFAULT_MCP_URL} \\
|
|
39
82
|
--header "Authorization: Bearer <token>"
|
|
40
|
-
|
|
83
|
+
codex mcp add uploads --url ${DEFAULT_MCP_URL}
|
|
84
|
+
grok mcp add --transport http uploads ${DEFAULT_MCP_URL} \\
|
|
85
|
+
--header "Authorization: Bearer <token>"
|
|
86
|
+
(each is skipped when that CLI is not on PATH)
|
|
41
87
|
hooks write/merge ~/.grok/hooks/… and ~/.cursor/hooks.json when present
|
|
42
88
|
|
|
43
89
|
Options:
|
|
@@ -76,9 +122,15 @@ export function missingBinaryHint(binary) {
|
|
|
76
122
|
`Install Node 22+ from https://nodejs.org (or your package manager), open a new shell, ` +
|
|
77
123
|
`confirm \`${binary} --version\` works, then re-run \`uploads install skill\`.`);
|
|
78
124
|
case "claude":
|
|
79
|
-
return (`claude not found on PATH — MCP install needs the Claude Code CLI. ` +
|
|
125
|
+
return (`claude not found on PATH — MCP install for Claude Code needs the Claude Code CLI. ` +
|
|
80
126
|
`Install it from https://docs.anthropic.com/en/docs/claude-code, ensure \`claude\` is on PATH, ` +
|
|
81
|
-
`then re-run \`uploads install mcp\`.
|
|
127
|
+
`then re-run \`uploads install mcp\`. Other agent CLIs (and skills/hooks) still work without it.`);
|
|
128
|
+
case "codex":
|
|
129
|
+
return (`codex not found on PATH — MCP install for Codex needs the Codex CLI. ` +
|
|
130
|
+
`Install it, ensure \`codex\` is on PATH, then re-run \`uploads install mcp\`.`);
|
|
131
|
+
case "grok":
|
|
132
|
+
return (`grok not found on PATH — MCP install for Grok needs the Grok CLI. ` +
|
|
133
|
+
`Install it, ensure \`grok\` is on PATH, then re-run \`uploads install mcp\`.`);
|
|
82
134
|
default:
|
|
83
135
|
return `${binary} not found on PATH — install it and ensure it is available in this shell.`;
|
|
84
136
|
}
|
|
@@ -131,28 +183,59 @@ function skillCommand(skill) {
|
|
|
131
183
|
// -g global, -y non-interactive, -a '*' every agent (skips the multi-select TUI)
|
|
132
184
|
return ["npx", "-y", "skills", "add", SKILL_SOURCE, "--skill", skill, "-g", "-y", "-a", "*"];
|
|
133
185
|
}
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
* state for anyone re-running `uploads install`, not a failure — recognize it
|
|
138
|
-
* so the run stays green and the footer tells them how to re-add if they want
|
|
139
|
-
* a fresh token in the header.
|
|
140
|
-
*/
|
|
141
|
-
function alreadyConfigured(result) {
|
|
142
|
-
return !result.ok && /already exists/i.test(result.error ?? "");
|
|
186
|
+
/** `mcp add` exits non-zero when the name is already registered; treat that as success. */
|
|
187
|
+
function isAlreadyConfigured(error) {
|
|
188
|
+
return /already exists|already (configured|registered|present)|duplicate/i.test(error);
|
|
143
189
|
}
|
|
144
|
-
function
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
190
|
+
function mcpStepKey(client) {
|
|
191
|
+
return `mcp:${client.id}`;
|
|
192
|
+
}
|
|
193
|
+
function mcpClientForStep(step) {
|
|
194
|
+
return MCP_CLIENTS.find((c) => mcpStepKey(c) === step);
|
|
195
|
+
}
|
|
196
|
+
function stepFamily(key) {
|
|
197
|
+
if (key.startsWith("skill:"))
|
|
198
|
+
return "skills";
|
|
199
|
+
if (key.startsWith("mcp:"))
|
|
200
|
+
return "mcp";
|
|
201
|
+
return key;
|
|
202
|
+
}
|
|
203
|
+
function partitionSteps(results) {
|
|
204
|
+
const skills = [];
|
|
205
|
+
const mcp = [];
|
|
206
|
+
const other = [];
|
|
207
|
+
for (const entry of Object.entries(results)) {
|
|
208
|
+
const key = entry[0];
|
|
209
|
+
if (key.startsWith("skill:"))
|
|
210
|
+
skills.push(entry);
|
|
211
|
+
else if (key.startsWith("mcp:"))
|
|
212
|
+
mcp.push(entry);
|
|
213
|
+
else
|
|
214
|
+
other.push(entry);
|
|
215
|
+
}
|
|
216
|
+
return { skills, mcp, other };
|
|
217
|
+
}
|
|
218
|
+
/** Run one client's `mcp add`; missing binaries skip, duplicates are already-configured. */
|
|
219
|
+
function runMcpClientStep(run, command) {
|
|
220
|
+
try {
|
|
221
|
+
const output = run(command[0], command.slice(1)).trim();
|
|
222
|
+
return { command, ok: true, output: output || undefined };
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
if (isEnoent(err)) {
|
|
226
|
+
return {
|
|
227
|
+
command,
|
|
228
|
+
ok: true,
|
|
229
|
+
skipped: "missing-cli",
|
|
230
|
+
error: `${command[0]} not found on PATH`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
234
|
+
if (isAlreadyConfigured(message)) {
|
|
235
|
+
return { command, ok: true, skipped: "already-configured", output: message };
|
|
236
|
+
}
|
|
237
|
+
return { command, ok: false, error: message };
|
|
238
|
+
}
|
|
156
239
|
}
|
|
157
240
|
function peekToken(globals) {
|
|
158
241
|
try {
|
|
@@ -171,27 +254,35 @@ function peekToken(globals) {
|
|
|
171
254
|
}
|
|
172
255
|
function printOneHumanStep(step, r, redact, verbose, mcpName) {
|
|
173
256
|
const cmd = redact(r.command.join(" "));
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
257
|
+
switch (r.skipped) {
|
|
258
|
+
case "dry-run":
|
|
259
|
+
process.stdout.write(`${step}: would run — ${cmd}\n`);
|
|
260
|
+
return;
|
|
261
|
+
case "sign-in":
|
|
262
|
+
process.stdout.write(`${step}: skipped — ${redact(r.error ?? "needs sign-in")}\n`);
|
|
263
|
+
return;
|
|
264
|
+
case "missing-cli":
|
|
265
|
+
process.stdout.write(`${step}: skipped — ${r.error ?? `${r.command[0]} not found on PATH`}\n`);
|
|
266
|
+
return;
|
|
267
|
+
case "already-configured": {
|
|
268
|
+
const client = mcpClientForStep(step);
|
|
269
|
+
const label = client?.label ?? "the client";
|
|
270
|
+
const remove = `${client?.id ?? "<cli>"} mcp remove ${mcpName}`;
|
|
271
|
+
process.stdout.write(`${step}: already configured — "${mcpName}" is registered in ${label} (nothing to do)\n` +
|
|
272
|
+
` To re-register (e.g. with a new token): ${remove} && uploads install mcp\n`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
183
275
|
}
|
|
184
|
-
|
|
276
|
+
if (r.ok) {
|
|
185
277
|
process.stdout.write(`${step}: ok\n`);
|
|
186
278
|
if (verbose && r.output) {
|
|
187
279
|
process.stdout.write(` ${redact(r.output).split("\n").join("\n ")}\n`);
|
|
188
280
|
}
|
|
281
|
+
return;
|
|
189
282
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
process.stderr.write(` command: ${cmd}\n`);
|
|
194
|
-
}
|
|
283
|
+
process.stderr.write(`${step}: failed — ${redact(r.error ?? "")}\n`);
|
|
284
|
+
if (verbose)
|
|
285
|
+
process.stderr.write(` command: ${cmd}\n`);
|
|
195
286
|
}
|
|
196
287
|
/** Shared error when every skill step failed identically; otherwise undefined. */
|
|
197
288
|
function identicalSkillFailure(skillEntries) {
|
|
@@ -203,27 +294,41 @@ function identicalSkillFailure(skillEntries) {
|
|
|
203
294
|
const allSame = skillEntries.every(([, r]) => !r.ok && !r.skipped && r.error === error);
|
|
204
295
|
return allSame ? error : undefined;
|
|
205
296
|
}
|
|
297
|
+
function printMcpHumanSteps(mcp, redact, verbose, mcpName) {
|
|
298
|
+
const first = mcp[0];
|
|
299
|
+
if (!first)
|
|
300
|
+
return;
|
|
301
|
+
if (mcp.every(([, r]) => r.skipped === "sign-in")) {
|
|
302
|
+
printOneHumanStep("mcp", first[1], redact, verbose, mcpName);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (mcp.every(([, r]) => r.skipped === "missing-cli")) {
|
|
306
|
+
process.stdout.write(`mcp: skipped — no agent CLI on PATH (${MCP_CLIENT_BINARIES}). Skills and hooks still work.\n`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
for (const [step, r] of mcp) {
|
|
310
|
+
printOneHumanStep(step, r, redact, verbose, mcpName);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
206
313
|
/** Collapse identical skill failures to one `skills:` line (missing npx, old npm, …). */
|
|
207
314
|
function printHumanSteps(results, redact, verbose, mcpName) {
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
const sharedError = identicalSkillFailure(skillEntries);
|
|
315
|
+
const { skills, mcp, other } = partitionSteps(results);
|
|
316
|
+
const sharedError = identicalSkillFailure(skills);
|
|
211
317
|
if (sharedError !== undefined) {
|
|
212
318
|
process.stderr.write(`skills: failed — ${redact(sharedError)}\n`);
|
|
213
319
|
if (verbose) {
|
|
214
|
-
for (const [step, r] of
|
|
320
|
+
for (const [step, r] of skills) {
|
|
215
321
|
process.stderr.write(` ${step}: ${redact(r.command.join(" "))}\n`);
|
|
216
322
|
}
|
|
217
323
|
}
|
|
218
324
|
}
|
|
219
325
|
else {
|
|
220
|
-
for (const [step, r] of
|
|
326
|
+
for (const [step, r] of skills) {
|
|
221
327
|
printOneHumanStep(step, r, redact, verbose, mcpName);
|
|
222
328
|
}
|
|
223
329
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
continue;
|
|
330
|
+
printMcpHumanSteps(mcp, redact, verbose, mcpName);
|
|
331
|
+
for (const [step, r] of other) {
|
|
227
332
|
printOneHumanStep(step, r, redact, verbose, mcpName);
|
|
228
333
|
}
|
|
229
334
|
}
|
|
@@ -288,24 +393,27 @@ export async function runInstall(args, opts, help = false) {
|
|
|
288
393
|
}
|
|
289
394
|
}
|
|
290
395
|
if (target === "mcp" || target === "all") {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
396
|
+
const bearer = token || "<token>";
|
|
397
|
+
const skipSignIn = !dryRun && !token;
|
|
398
|
+
if (human && !skipSignIn)
|
|
399
|
+
process.stdout.write("Installing MCP server…\n");
|
|
400
|
+
for (const client of MCP_CLIENTS) {
|
|
401
|
+
const command = client.command(name, url, bearer);
|
|
402
|
+
const key = mcpStepKey(client);
|
|
403
|
+
if (skipSignIn) {
|
|
404
|
+
results[key] = {
|
|
405
|
+
command,
|
|
406
|
+
ok: false,
|
|
407
|
+
skipped: "sign-in",
|
|
408
|
+
error: "needs sign-in — run `uploads login`, then `uploads install mcp`",
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
else if (dryRun) {
|
|
412
|
+
results[key] = { command, ok: true, skipped: "dry-run" };
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
results[key] = runMcpClientStep(run, command);
|
|
416
|
+
}
|
|
309
417
|
}
|
|
310
418
|
}
|
|
311
419
|
if (target === "hooks" || target === "all") {
|
|
@@ -345,28 +453,25 @@ export async function runInstall(args, opts, help = false) {
|
|
|
345
453
|
(dryRun || (human && (verbose || hookWrites.some((w) => w.action !== "skipped"))))) {
|
|
346
454
|
printHookResults(hookWrites);
|
|
347
455
|
}
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
.map(([, r]) => r);
|
|
456
|
+
const { skills: skillEntries, mcp: mcpEntries } = partitionSteps(results);
|
|
457
|
+
const skillResults = skillEntries.map(([, r]) => r);
|
|
351
458
|
const skillsOk = skillResults.length > 0 && skillResults.every((r) => r.ok);
|
|
352
459
|
const skillsFailed = skillResults.some((r) => !r.ok);
|
|
460
|
+
const mcpResults = mcpEntries.map(([, r]) => r);
|
|
461
|
+
const mcpFailed = mcpResults.some((r) => !r.ok);
|
|
353
462
|
if (!failed && !dryRun) {
|
|
354
463
|
const stepLabels = [
|
|
355
|
-
...new Set(Object.
|
|
464
|
+
...new Set(Object.entries(results)
|
|
465
|
+
.filter(([, r]) => r.ok && r.skipped !== "missing-cli" && r.skipped !== "sign-in")
|
|
466
|
+
.map(([k]) => stepFamily(k))),
|
|
356
467
|
];
|
|
357
|
-
|
|
468
|
+
if (stepLabels.length > 0)
|
|
469
|
+
printSuccessFooter(stepLabels, signedIn);
|
|
358
470
|
}
|
|
359
|
-
else if (failed && !dryRun && skillsOk &&
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
else if (results.mcp.error?.includes("not found on PATH")) {
|
|
365
|
-
next = "Install the Claude Code CLI (or skip MCP), then re-run `uploads install mcp`.";
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
next = "Fix the MCP step above, then re-run `uploads install mcp`.";
|
|
369
|
-
}
|
|
471
|
+
else if (failed && !dryRun && skillsOk && mcpFailed) {
|
|
472
|
+
const next = mcpResults.every((r) => r.skipped === "sign-in")
|
|
473
|
+
? "Sign in with `uploads login`, then re-run `uploads install mcp`."
|
|
474
|
+
: "Fix the MCP step above, then re-run `uploads install mcp`.";
|
|
370
475
|
process.stdout.write(`\nSkills are installed. ${next}\n`);
|
|
371
476
|
}
|
|
372
477
|
else if (failed && !dryRun && skillsFailed) {
|
package/dist/commands/login.js
CHANGED
|
@@ -459,7 +459,7 @@ export async function runLogin(args, opts, help = false, deviceIo = defaultDevic
|
|
|
459
459
|
process.stdout.write(`saved credentials to ${path}\napi: ${savedApiUrl}\nworkspace: ${result.workspace}\ntoken: ${redactToken(result.token)}\n`);
|
|
460
460
|
process[doctor.ok ? "stdout" : "stderr"].write(`doctor: ${checked ? (doctor.ok ? "ok" : `failed — ${doctor.error}`) : "skipped"}\n`);
|
|
461
461
|
if (doctor.ok)
|
|
462
|
-
process.stdout.write("\nusing a coding agent? run `uploads install` to add the uploads skill + MCP server
|
|
462
|
+
process.stdout.write("\nusing a coding agent? run `uploads install` to add the uploads skill + MCP server\n");
|
|
463
463
|
}
|
|
464
464
|
return doctor.ok ? 0 : 1;
|
|
465
465
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -11,8 +11,9 @@ const UPDATE_HELP = `uploads update — update the CLI and refresh agent integra
|
|
|
11
11
|
Upgrades the globally installed npm package, then re-runs \`uploads install\` so
|
|
12
12
|
the agent skills match the new version. Skills drift on their own, so this
|
|
13
13
|
refreshes them even when the CLI is already current. An MCP server already
|
|
14
|
-
registered is left as-is (\`already configured\`) — \`claude mcp add\`
|
|
15
|
-
|
|
14
|
+
registered is left as-is (\`already configured\`) — \`claude mcp add\` (and the
|
|
15
|
+
Codex/Grok equivalents) never overwrite an existing entry. A missing agent CLI
|
|
16
|
+
is skipped so it does not fail the rest of the refresh.
|
|
16
17
|
|
|
17
18
|
Usage:
|
|
18
19
|
uploads update [options]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildinternet/uploads",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.2",
|
|
4
4
|
"description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^26.1.0",
|
|
50
50
|
"ai": "^6.0.0",
|
|
51
|
-
"files-sdk": "^2.2.
|
|
51
|
+
"files-sdk": "^2.2.5",
|
|
52
52
|
"typescript": "^7.0.2",
|
|
53
53
|
"vitest": "^4.1.10"
|
|
54
54
|
},
|