@buildinternet/uploads 0.45.0 → 0.46.1
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/commands/install.d.ts +13 -1
- package/dist/commands/install.js +192 -87
- package/dist/commands/login.js +1 -1
- package/dist/commands/screenshot.js +12 -1
- package/dist/commands/update.js +3 -2
- package/dist/screenshot-local.d.ts +9 -0
- package/dist/screenshot-local.js +20 -1
- package/dist/screenshot.d.ts +8 -0
- package/dist/screenshot.js +7 -0
- package/package.json +1 -1
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")}
|
|
@@ -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
|
}
|
|
@@ -82,7 +82,16 @@ Options:
|
|
|
82
82
|
--no-hide-dev-tools Don't auto-hide framework dev toolbars (auto-hidden on localhost/private)
|
|
83
83
|
--reduced-motion Emulate prefers-reduced-motion: reduce so animations settle (best-effort
|
|
84
84
|
on --via remote — neutralizes animations via injected CSS)
|
|
85
|
-
--
|
|
85
|
+
--wait-for <js> Poll this JS expression in the page until truthy before --eval and
|
|
86
|
+
capture (--via local only). Bridges framework hydration: load/
|
|
87
|
+
networkidle settle before React/Next attach handlers, so a synthetic
|
|
88
|
+
click in --eval hits the inert server-rendered DOM. Express the app's
|
|
89
|
+
own "interactive" signal, e.g. --wait-for 'window.__hydrated===true' or
|
|
90
|
+
--wait-for 'document.querySelector("[data-hydrated]")'. Times out with
|
|
91
|
+
the capture timeout if it never becomes truthy.
|
|
92
|
+
--eval <js> Run JS in the page after settle, before capture (--via local only).
|
|
93
|
+
Note: synthetic events (el.click()) won't reach framework handlers
|
|
94
|
+
until the app hydrates — pair with --wait-for on React/Next apps.
|
|
86
95
|
--init-script <file> Inject a JS file before navigation (--via local only)
|
|
87
96
|
--annotate <file|-> Bake hand-drawn boxes, arrows, labels, and redactions from a JSON
|
|
88
97
|
annotation spec onto the capture before upload (file path or - for
|
|
@@ -216,6 +225,7 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
216
225
|
// lets captureScreenshot apply its localhost-aware default.
|
|
217
226
|
const hideDevTools = flagBool(parsed.flags, "--no-hide-dev-tools") ? false : undefined;
|
|
218
227
|
const reducedMotion = flagBool(parsed.flags, "--reduced-motion");
|
|
228
|
+
const waitForExpr = flagString(parsed.flags, "--wait-for");
|
|
219
229
|
const evalJs = flagString(parsed.flags, "--eval");
|
|
220
230
|
const initScriptPath = flagString(parsed.flags, "--init-script");
|
|
221
231
|
let initScript;
|
|
@@ -445,6 +455,7 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
445
455
|
hide,
|
|
446
456
|
hideDevTools,
|
|
447
457
|
reducedMotion,
|
|
458
|
+
waitForExpr,
|
|
448
459
|
evalJs,
|
|
449
460
|
initScript,
|
|
450
461
|
// Skip folding when an explicit --key was given — --key sets the whole
|
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]
|
|
@@ -62,6 +62,15 @@ export interface LocalCaptureOptions {
|
|
|
62
62
|
hide?: string[];
|
|
63
63
|
/** Emulate prefers-reduced-motion: reduce so animations settle deterministically. */
|
|
64
64
|
reducedMotion?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* JS expression polled in the page (page.waitForFunction) after settle and
|
|
67
|
+
* before `evalJs`/capture — the caller's "app is interactive" signal. Lets a
|
|
68
|
+
* synthetic click in `evalJs` land after a framework (React/Next/…) has
|
|
69
|
+
* hydrated and attached its handlers, instead of firing on the still-inert
|
|
70
|
+
* server-rendered DOM. Throws `RENDER_FAILED` if it never becomes truthy
|
|
71
|
+
* within the capture timeout.
|
|
72
|
+
*/
|
|
73
|
+
waitForExpr?: string;
|
|
65
74
|
/** JS run via page.evaluate after settle, before capture. */
|
|
66
75
|
evalJs?: string;
|
|
67
76
|
/** JS injected via addInitScript before navigation. */
|
package/dist/screenshot-local.js
CHANGED
|
@@ -346,9 +346,28 @@ export async function captureLocal(opts) {
|
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
const waitUntil = typeof opts.waitUntil === "string" ? opts.waitUntil : "load";
|
|
349
|
-
|
|
349
|
+
const timeoutMs = opts.timeoutMs ?? 30_000;
|
|
350
|
+
await page.goto(opts.url, { waitUntil, timeout: timeoutMs });
|
|
350
351
|
if (typeof opts.waitUntil === "number")
|
|
351
352
|
await page.waitForTimeout(opts.waitUntil);
|
|
353
|
+
// Hydration-aware gate (issue #715): poll the caller's "app is interactive"
|
|
354
|
+
// predicate before running any eval or capturing. The load/networkidle
|
|
355
|
+
// settle strategies fire before a framework hydrates, so a synthetic
|
|
356
|
+
// click in `evalJs` would hit the inert server-rendered DOM with no
|
|
357
|
+
// handler attached. Waiting for the caller's own signal (e.g.
|
|
358
|
+
// `window.__hydrated === true`, or a class/attribute the app sets once
|
|
359
|
+
// interactive) closes that gap. A timeout means the predicate never
|
|
360
|
+
// became truthy — surface it clearly rather than capturing the un-ready
|
|
361
|
+
// page silently.
|
|
362
|
+
if (opts.waitForExpr) {
|
|
363
|
+
try {
|
|
364
|
+
await page.waitForFunction(opts.waitForExpr, undefined, { timeout: timeoutMs });
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
throw new UploadsError(`--wait-for expression never became truthy within ${timeoutMs}ms: ${opts.waitForExpr}` +
|
|
368
|
+
` (${err instanceof Error ? err.message : String(err)})`, "RENDER_FAILED");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
352
371
|
// Hide overlays first, then run any user eval (which may depend on, or
|
|
353
372
|
// deliberately override, the hidden state).
|
|
354
373
|
if (opts.hide && opts.hide.length > 0) {
|
package/dist/screenshot.d.ts
CHANGED
|
@@ -99,6 +99,13 @@ export interface CaptureScreenshotOptions {
|
|
|
99
99
|
hideDevTools?: boolean;
|
|
100
100
|
/** Emulate prefers-reduced-motion: reduce so CSS/JS animations settle. */
|
|
101
101
|
reducedMotion?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* JS expression polled in the page until truthy after settle, before
|
|
104
|
+
* `evalJs` and capture — the caller's "app is interactive" signal so a
|
|
105
|
+
* synthetic click in `evalJs` lands after framework hydration (issue #715).
|
|
106
|
+
* Local backend only — throws if the resolved backend is remote.
|
|
107
|
+
*/
|
|
108
|
+
waitForExpr?: string;
|
|
102
109
|
/** Run this JS in the page after settle, before capture (local backend only). */
|
|
103
110
|
evalJs?: string;
|
|
104
111
|
/** Inject this JS as an init script before navigation (local backend only). */
|
|
@@ -127,6 +134,7 @@ export interface CaptureScreenshotOptions {
|
|
|
127
134
|
waitUntil: WaitUntil;
|
|
128
135
|
hide?: string[];
|
|
129
136
|
reducedMotion?: boolean;
|
|
137
|
+
waitForExpr?: string;
|
|
130
138
|
evalJs?: string;
|
|
131
139
|
initScript?: string;
|
|
132
140
|
measureSelectors?: string[];
|
package/dist/screenshot.js
CHANGED
|
@@ -290,6 +290,12 @@ export async function captureScreenshot(opts) {
|
|
|
290
290
|
if (backend === "remote" && (opts.evalJs !== undefined || opts.initScript !== undefined)) {
|
|
291
291
|
throw new UploadsError("--eval and --init-script are local-only — use --via local", "USAGE");
|
|
292
292
|
}
|
|
293
|
+
// --wait-for polls a JS predicate via the live local page (page.waitForFunction);
|
|
294
|
+
// the remote renderer has no eval escape hatch to evaluate it. Fail fast
|
|
295
|
+
// rather than silently ignore the caller's readiness signal.
|
|
296
|
+
if (backend === "remote" && opts.waitForExpr !== undefined) {
|
|
297
|
+
throw new UploadsError("--wait-for is local-only — use --via local", "USAGE");
|
|
298
|
+
}
|
|
293
299
|
// Selector-based annotation measurement needs a live local page — the
|
|
294
300
|
// remote render endpoint has no eval escape hatch to run
|
|
295
301
|
// getBoundingClientRect. Covers both explicit --via remote and auto
|
|
@@ -315,6 +321,7 @@ export async function captureScreenshot(opts) {
|
|
|
315
321
|
waitUntil,
|
|
316
322
|
hide,
|
|
317
323
|
reducedMotion: opts.reducedMotion,
|
|
324
|
+
waitForExpr: opts.waitForExpr,
|
|
318
325
|
evalJs: opts.evalJs,
|
|
319
326
|
initScript: opts.initScript,
|
|
320
327
|
measureSelectors: opts.measureSelectors,
|