@buildinternet/uploads 0.10.1 → 0.11.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 +18 -4
- package/dist/async.d.ts +5 -0
- package/dist/async.js +21 -0
- package/dist/cli-catalog.js +18 -4
- package/dist/cli-help.js +1 -1
- package/dist/cli.js +59 -7
- package/dist/commands/install.js +33 -15
- package/dist/commands/mcp.js +2 -0
- package/dist/commands/report.d.ts +4 -0
- package/dist/commands/report.js +164 -0
- package/dist/commands/telemetry.d.ts +4 -0
- package/dist/commands/telemetry.js +91 -0
- package/dist/commands.d.ts +92 -1
- package/dist/commands.js +364 -150
- package/dist/format-usage.d.ts +38 -0
- package/dist/format-usage.js +125 -0
- package/dist/mcp/batch-error.d.ts +16 -0
- package/dist/mcp/batch-error.js +24 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +33 -1
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.js +254 -94
- package/dist/report.d.ts +68 -0
- package/dist/report.js +152 -0
- package/dist/telemetry.d.ts +60 -0
- package/dist/telemetry.js +290 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,8 @@ global `uploads` form above.
|
|
|
39
39
|
|
|
40
40
|
Commands: `attach`, `put`, `gallery`, `comment`, `list`, `find`, `meta`, `delete`, `usage`,
|
|
41
41
|
`reconcile`, `purge-expired`, `setup`, `install`, `login`, `whoami` (alias `status`),
|
|
42
|
-
`logout`, `invite`, `admin`, `config`, `doctor`, `health`, `mcp`,
|
|
42
|
+
`logout`, `invite`, `admin`, `config`, `telemetry`, `report`, `doctor`, `health`, `mcp`,
|
|
43
|
+
`completion`.
|
|
43
44
|
|
|
44
45
|
**Help:** bare `uploads` / `uploads help` / `--help` shows essentials; use
|
|
45
46
|
`uploads help --all` (or `--help --all`) for the full command list. Per-command:
|
|
@@ -57,6 +58,19 @@ newer npm release is available (at most once/day, `~/.cache/uploads/`). Silence
|
|
|
57
58
|
with `--quiet`, `--json`, `UPLOADS_NO_UPDATE=1`, or `NO_UPDATE_NOTIFIER=1`. Not used
|
|
58
59
|
for `uploads mcp`.
|
|
59
60
|
|
|
61
|
+
**Telemetry:** the CLI and MCP server send anonymous usage pings (command name,
|
|
62
|
+
version, OS/arch, exit code, duration, optional error code) to
|
|
63
|
+
`POST /v1/telemetry` on the configured API. No arguments, paths, tokens, workspace
|
|
64
|
+
names, or file content. Opt out with `UPLOADS_TELEMETRY_DISABLED=1`,
|
|
65
|
+
`DO_NOT_TRACK=1`, or `uploads telemetry disable`. First interactive run prints a
|
|
66
|
+
one-line notice.
|
|
67
|
+
|
|
68
|
+
**Diagnostic reports (opt-in):** `uploads report "what broke"` sends a short
|
|
69
|
+
message to the team. Attach a log with `--file ./trace.log`, or pipe:
|
|
70
|
+
`uploads doctor --json 2>&1 | uploads report "doctor failed"`. Never automatic —
|
|
71
|
+
also available as the MCP `report` tool when the user asks to submit feedback.
|
|
72
|
+
Attachments are text-only (max 256 KiB) and stored under an unguessable R2 key.
|
|
73
|
+
|
|
60
74
|
**Exit codes:** `0` ok, `2` usage/token/file, `3` auth/policy, `4` network, `1` other.
|
|
61
75
|
Failures go to stderr; under `--format json|url|markdown` they also go to stdout so
|
|
62
76
|
piped runs stay self-diagnosing. Prefer JSON `code` over message text. `put --dry-run`
|
|
@@ -114,7 +128,7 @@ Config layers (first match wins): CLI flags → env vars → `--env-file` → `~
|
|
|
114
128
|
|
|
115
129
|
## MCP server
|
|
116
130
|
|
|
117
|
-
`uploads mcp` serves the Model Context Protocol over stdio (newline-delimited JSON-RPC, no extra dependencies). Tools include file operations plus public gallery workflows: `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`. Gallery tools return API-provided canonical URLs and never need GitHub credentials. The remaining stdio tools are `put`, `attach`, `list`, `delete`, `set_metadata`, `find_files`, `usage`, `reconcile`, `purge_expired`, `comment`, `health`, and `doctor` — with the same config resolution and defaults, plus a per-call `workspace` argument. `put` and `attach` accept a `metadata` param (same `gh.*` auto-injection as the CLI's `attach`); `set_metadata
|
|
131
|
+
`uploads mcp` serves the Model Context Protocol over stdio (newline-delimited JSON-RPC, no extra dependencies). Tools include file operations plus public gallery workflows: `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`. Gallery tools return API-provided canonical URLs and never need GitHub credentials. The remaining stdio tools are `put`, `attach`, `list`, `delete`, `get_metadata`, `set_metadata`, `find_files`, `usage`, `reconcile`, `purge_expired`, `comment`, `health`, and `doctor` — with the same config resolution and defaults, plus a per-call `workspace` argument. `put` and `attach` accept a `metadata` param (same `gh.*` auto-injection as the CLI's `attach`); `get_metadata`, `set_metadata`, and `find_files` mirror `uploads meta get` / `meta set` / `find`. 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).
|
|
118
132
|
|
|
119
133
|
```json
|
|
120
134
|
{ "command": "uploads", "args": ["--env-file", "/path/to/.env", "mcp"] }
|
|
@@ -122,7 +136,7 @@ Config layers (first match wins): CLI flags → env vars → `--env-file` → `~
|
|
|
122
136
|
|
|
123
137
|
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`.
|
|
124
138
|
|
|
125
|
-
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 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
|
|
139
|
+
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 (short progress; `--verbose` for underlying output). 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.
|
|
126
140
|
|
|
127
141
|
## Programmatic use
|
|
128
142
|
|
|
@@ -159,4 +173,4 @@ pnpm pack:check # verify the npm tarball contents
|
|
|
159
173
|
|
|
160
174
|
Maintainer release instructions: [`docs/releasing.md`](../../docs/releasing.md).
|
|
161
175
|
|
|
162
|
-
Agent-oriented usage: [`skills/uploads-cli/SKILL.md`](../../skills/uploads-cli/SKILL.md). REST details: [`docs/api.md`](../../docs/api.md).
|
|
176
|
+
Agent-oriented usage: [`skills/uploads-cli/SKILL.md`](../../skills/uploads-cli/SKILL.md) (full CLI reference) and [`skills/github-screenshots/SKILL.md`](../../skills/github-screenshots/SKILL.md) (visuals into PRs/issues). REST details: [`docs/api.md`](../../docs/api.md).
|
package/dist/async.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `fn` over `values` with bounded concurrency, preserving input order
|
|
3
|
+
* in the returned array. Used by multi-file attach (and similar fan-outs).
|
|
4
|
+
*/
|
|
5
|
+
export declare function mapBounded<T, R>(values: readonly T[], concurrency: number, fn: (value: T, index: number) => Promise<R>): Promise<R[]>;
|
package/dist/async.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `fn` over `values` with bounded concurrency, preserving input order
|
|
3
|
+
* in the returned array. Used by multi-file attach (and similar fan-outs).
|
|
4
|
+
*/
|
|
5
|
+
export async function mapBounded(values, concurrency, fn) {
|
|
6
|
+
if (values.length === 0)
|
|
7
|
+
return [];
|
|
8
|
+
const limit = Math.max(1, Math.min(concurrency, values.length));
|
|
9
|
+
const result = Array.from({ length: values.length });
|
|
10
|
+
let next = 0;
|
|
11
|
+
async function worker() {
|
|
12
|
+
for (;;) {
|
|
13
|
+
const index = next++;
|
|
14
|
+
if (index >= values.length)
|
|
15
|
+
return;
|
|
16
|
+
result[index] = await fn(values[index], index);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
await Promise.all(Array.from({ length: limit }, worker));
|
|
20
|
+
return result;
|
|
21
|
+
}
|
package/dist/cli-catalog.js
CHANGED
|
@@ -59,7 +59,7 @@ export const ROOT_COMMANDS = [
|
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
name: "put",
|
|
62
|
-
usage: "put <file
|
|
62
|
+
usage: "put <file...>",
|
|
63
63
|
summary: "Upload (+ URL + markdown for GitHub)",
|
|
64
64
|
essential: true,
|
|
65
65
|
},
|
|
@@ -110,12 +110,12 @@ export const ROOT_COMMANDS = [
|
|
|
110
110
|
{ name: "setup", summary: "Inspect/configure advanced CLI settings" },
|
|
111
111
|
{
|
|
112
112
|
name: "install",
|
|
113
|
-
summary: "Install the agent
|
|
113
|
+
summary: "Install the agent skills + register the remote MCP server",
|
|
114
114
|
essential: true,
|
|
115
115
|
subcommands: [
|
|
116
|
-
{ name: "skill", summary: "Install the agent
|
|
116
|
+
{ name: "skill", summary: "Install the agent skills only" },
|
|
117
117
|
{ name: "mcp", summary: "Register the remote MCP server only" },
|
|
118
|
-
{ name: "all", summary: "Install
|
|
118
|
+
{ name: "all", summary: "Install skills and MCP (default)" },
|
|
119
119
|
],
|
|
120
120
|
},
|
|
121
121
|
{
|
|
@@ -154,6 +154,20 @@ export const ROOT_COMMANDS = [
|
|
|
154
154
|
{ name: "set", summary: "Set one UPLOADS_* key" },
|
|
155
155
|
],
|
|
156
156
|
},
|
|
157
|
+
{
|
|
158
|
+
name: "telemetry",
|
|
159
|
+
summary: "Manage anonymous usage telemetry (status / enable / disable)",
|
|
160
|
+
subcommands: [
|
|
161
|
+
{ name: "status", summary: "Show whether telemetry is enabled" },
|
|
162
|
+
{ name: "enable", summary: "Enable anonymous usage telemetry" },
|
|
163
|
+
{ name: "disable", summary: "Disable anonymous usage telemetry" },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "report",
|
|
168
|
+
usage: "report [message]",
|
|
169
|
+
summary: "Send a diagnostic report (optional log attachment; explicit opt-in)",
|
|
170
|
+
},
|
|
157
171
|
{
|
|
158
172
|
name: "doctor",
|
|
159
173
|
summary: "Health + auth + workspace checks",
|
package/dist/cli-help.js
CHANGED
|
@@ -147,7 +147,7 @@ ${section(style, "Examples:")}
|
|
|
147
147
|
${style.command("uploads logout")}
|
|
148
148
|
${style.command("uploads --version")}
|
|
149
149
|
|
|
150
|
-
${section(style, "Agent/MCP:")} ${style.body("`uploads install` sets up the agent
|
|
150
|
+
${section(style, "Agent/MCP:")} ${style.body("`uploads install` sets up the agent skills and the hosted MCP server")}
|
|
151
151
|
${style.body("(https://agents.uploads.sh/mcp, workspace inferred from the token). Run")}
|
|
152
152
|
${style.body("`uploads mcp` for local stdio, or use createUploadsWorkerFileTools()")}
|
|
153
153
|
${style.body("from @buildinternet/uploads/agent on the Worker.")}
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createUploadsClient } from "./client.js";
|
|
2
|
-
import { resolveApiUrl, resolveConfig } from "./config.js";
|
|
2
|
+
import { DEFAULT_API_URL, resolveApiUrl, resolveConfig } from "./config.js";
|
|
3
3
|
import { UploadsError } from "./errors.js";
|
|
4
4
|
import { commandWorkspace, flagString, isHelpFlag, parseArgv, parseCommandArgs, UsageError, } from "./cli-args.js";
|
|
5
5
|
import { formatRootHelp, wantsFullHelp } from "./cli-help.js";
|
|
@@ -14,8 +14,11 @@ import { runMcp } from "./commands/mcp.js";
|
|
|
14
14
|
import { runInstall } from "./commands/install.js";
|
|
15
15
|
import { runCompletion } from "./commands/completion.js";
|
|
16
16
|
import { runLogout, runWhoami } from "./commands/session.js";
|
|
17
|
+
import { runTelemetry } from "./commands/telemetry.js";
|
|
18
|
+
import { runReport } from "./commands/report.js";
|
|
17
19
|
import { packageVersion } from "./package-version.js";
|
|
18
20
|
import { checkForUpdate, maybeHintUpdate } from "./update-check.js";
|
|
21
|
+
import { errorCodeFromUnknown, maybeShowFirstRunNotice, recordEvent, telemetryCommandName, } from "./telemetry.js";
|
|
19
22
|
async function writeRootHelp(options = {}) {
|
|
20
23
|
// Best-effort version check so the help header can show a banner when outdated.
|
|
21
24
|
// Short timeout; cache still applies (once/day). Never blocks help on network.
|
|
@@ -144,10 +147,47 @@ function usageHint(argv) {
|
|
|
144
147
|
}
|
|
145
148
|
}
|
|
146
149
|
export async function runCli(argv) {
|
|
150
|
+
const telemetryStart = Date.now();
|
|
151
|
+
const telemetryCmd = telemetryCommandName(argv);
|
|
152
|
+
// Prefer --api-url from argv early so every exit path can pass it to telemetry.
|
|
153
|
+
let apiUrl = DEFAULT_API_URL;
|
|
154
|
+
try {
|
|
155
|
+
apiUrl = resolveApiUrl(parseArgv(argv).globals);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Usage errors while parsing globals fall through to the main try/catch.
|
|
159
|
+
}
|
|
160
|
+
const skipTelemetry = argv.includes("--version") ||
|
|
161
|
+
argv.includes("-V") ||
|
|
162
|
+
telemetryCmd === "telemetry" ||
|
|
163
|
+
telemetryCmd.startsWith("telemetry ") ||
|
|
164
|
+
// Report is itself a deliberate submit; skip the automatic usage ping.
|
|
165
|
+
telemetryCmd === "report";
|
|
166
|
+
// One-time notice for interactive humans (never MCP / json / quiet).
|
|
167
|
+
if (!skipTelemetry) {
|
|
168
|
+
const wantsQuiet = argv.includes("--json") || argv.includes("--quiet") || argv[2] === "mcp";
|
|
169
|
+
maybeShowFirstRunNotice({ interactive: wantsQuiet ? false : undefined });
|
|
170
|
+
}
|
|
171
|
+
const flushTelemetry = (code, err) => {
|
|
172
|
+
if (skipTelemetry)
|
|
173
|
+
return;
|
|
174
|
+
// Long-lived MCP process: per-tool events come from the MCP server; skip
|
|
175
|
+
// a process-level "mcp" ping so we don't double-count or hang exit.
|
|
176
|
+
if (telemetryCmd === "mcp")
|
|
177
|
+
return;
|
|
178
|
+
recordEvent({
|
|
179
|
+
surface: "cli",
|
|
180
|
+
command: telemetryCmd,
|
|
181
|
+
exitCode: code,
|
|
182
|
+
durationMs: Date.now() - telemetryStart,
|
|
183
|
+
errorCode: err !== undefined ? errorCodeFromUnknown(err) : undefined,
|
|
184
|
+
}, { apiUrl });
|
|
185
|
+
};
|
|
147
186
|
try {
|
|
148
187
|
const parsed = parseArgv(argv);
|
|
149
188
|
const json = parsed.globals.json ?? false;
|
|
150
189
|
const quiet = parsed.globals.quiet ?? false;
|
|
190
|
+
apiUrl = resolveApiUrl(parsed.globals);
|
|
151
191
|
if (parsed.globals.version) {
|
|
152
192
|
process.stdout.write(`${packageVersion()}\n`);
|
|
153
193
|
return 0;
|
|
@@ -163,14 +203,16 @@ export async function runCli(argv) {
|
|
|
163
203
|
envFile: parsed.globals.envFile,
|
|
164
204
|
});
|
|
165
205
|
// Explicit help exits 0; bare `uploads` is usage → 2.
|
|
166
|
-
|
|
206
|
+
const code = parsed.help || isHelpCommand ? 0 : 2;
|
|
207
|
+
flushTelemetry(code);
|
|
208
|
+
return code;
|
|
167
209
|
}
|
|
168
210
|
const cmdArgs = parsed.rest.slice(1);
|
|
169
211
|
const showHelp = parsed.help || cmdArgs.some(isHelpFlag);
|
|
170
212
|
let code;
|
|
171
213
|
switch (parsed.command) {
|
|
172
214
|
case "health":
|
|
173
|
-
code = await runHealth({ apiUrl
|
|
215
|
+
code = await runHealth({ apiUrl, json }, cmdArgs, showHelp);
|
|
174
216
|
break;
|
|
175
217
|
case "config":
|
|
176
218
|
code = await runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
|
|
@@ -179,7 +221,7 @@ export async function runCli(argv) {
|
|
|
179
221
|
code = await runSetup(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
|
|
180
222
|
break;
|
|
181
223
|
case "login":
|
|
182
|
-
code = await runLogin(cmdArgs, { json, apiUrl
|
|
224
|
+
code = await runLogin(cmdArgs, { json, apiUrl }, showHelp);
|
|
183
225
|
break;
|
|
184
226
|
case "whoami":
|
|
185
227
|
case "status":
|
|
@@ -195,10 +237,16 @@ export async function runCli(argv) {
|
|
|
195
237
|
code = await runLogout(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
|
|
196
238
|
break;
|
|
197
239
|
case "invite":
|
|
198
|
-
code = await runInvite(cmdArgs, { json, apiUrl
|
|
240
|
+
code = await runInvite(cmdArgs, { json, apiUrl }, showHelp);
|
|
199
241
|
break;
|
|
200
242
|
case "admin":
|
|
201
|
-
code = await runAdmin(cmdArgs, { json, apiUrl
|
|
243
|
+
code = await runAdmin(cmdArgs, { json, apiUrl }, showHelp);
|
|
244
|
+
break;
|
|
245
|
+
case "telemetry":
|
|
246
|
+
code = await runTelemetry(cmdArgs, { json, apiUrl }, showHelp);
|
|
247
|
+
break;
|
|
248
|
+
case "report":
|
|
249
|
+
code = await runReport(cmdArgs, { json, apiUrl }, showHelp);
|
|
202
250
|
break;
|
|
203
251
|
case "mcp":
|
|
204
252
|
code = await runMcp(cmdArgs, { globals: parsed.globals }, showHelp);
|
|
@@ -271,6 +319,7 @@ export async function runCli(argv) {
|
|
|
271
319
|
token: parsed.globals.token,
|
|
272
320
|
envFile: parsed.globals.envFile,
|
|
273
321
|
});
|
|
322
|
+
flushTelemetry(2);
|
|
274
323
|
return 2;
|
|
275
324
|
}
|
|
276
325
|
}
|
|
@@ -278,6 +327,7 @@ export async function runCli(argv) {
|
|
|
278
327
|
if (code === 0 && !showHelp) {
|
|
279
328
|
await maybeHintUpdate({ quiet: quiet || json, command: parsed.command });
|
|
280
329
|
}
|
|
330
|
+
flushTelemetry(code);
|
|
281
331
|
return code;
|
|
282
332
|
}
|
|
283
333
|
catch (err) {
|
|
@@ -285,6 +335,8 @@ export async function runCli(argv) {
|
|
|
285
335
|
errorOut(err, format);
|
|
286
336
|
if (err instanceof UsageError && format !== "json")
|
|
287
337
|
usageHint(argv);
|
|
288
|
-
|
|
338
|
+
const code = exitCode(err);
|
|
339
|
+
flushTelemetry(code, err);
|
|
340
|
+
return code;
|
|
289
341
|
}
|
|
290
342
|
}
|
package/dist/commands/install.js
CHANGED
|
@@ -4,22 +4,24 @@ import { execRunner } from "../github-gh.js";
|
|
|
4
4
|
import { writeCommandHelp } from "../cli-style.js";
|
|
5
5
|
export const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
|
|
6
6
|
const SKILL_SOURCE = "buildinternet/uploads";
|
|
7
|
-
const
|
|
8
|
-
const INSTALL_HELP = `uploads install — set up agent integrations (
|
|
7
|
+
const SKILL_NAMES = ["uploads-cli", "github-screenshots"];
|
|
8
|
+
const INSTALL_HELP = `uploads install — set up agent integrations (skills + remote MCP)
|
|
9
9
|
|
|
10
|
-
Installs the uploads-cli agent
|
|
11
|
-
with Claude Code. The remote MCP endpoint infers your
|
|
12
|
-
bearer token, so only the token is needed.
|
|
10
|
+
Installs the github-screenshots and uploads-cli agent skills and registers
|
|
11
|
+
the hosted MCP server with Claude Code. The remote MCP endpoint infers your
|
|
12
|
+
workspace from the bearer token, so only the token is needed.
|
|
13
13
|
|
|
14
14
|
Usage:
|
|
15
15
|
uploads install [skill|mcp|all] (default: all)
|
|
16
16
|
|
|
17
17
|
What it does:
|
|
18
|
-
skill Agent
|
|
18
|
+
skill Agent skills (via npx skills) — github-screenshots: visuals into
|
|
19
|
+
PRs/issues; uploads-cli: full CLI reference
|
|
19
20
|
mcp Hosted MCP server in Claude Code — put, list, attach, galleries
|
|
20
21
|
|
|
21
22
|
What runs under the hood:
|
|
22
|
-
skill npx -y skills add ${SKILL_SOURCE} --skill
|
|
23
|
+
skill npx -y skills add ${SKILL_SOURCE} --skill <name> -g -y -a '*'
|
|
24
|
+
(once per skill: ${SKILL_NAMES.join(", ")})
|
|
23
25
|
mcp claude mcp add --transport http uploads ${DEFAULT_MCP_URL} \\
|
|
24
26
|
--header "Authorization: Bearer <token>"
|
|
25
27
|
|
|
@@ -57,9 +59,9 @@ function runStep(run, command) {
|
|
|
57
59
|
return { command, ok: false, error: hint };
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
|
-
function skillCommand() {
|
|
62
|
+
function skillCommand(skill) {
|
|
61
63
|
// -g global, -y non-interactive, -a '*' every agent (skips the multi-select TUI)
|
|
62
|
-
return ["npx", "-y", "skills", "add", SKILL_SOURCE, "--skill",
|
|
64
|
+
return ["npx", "-y", "skills", "add", SKILL_SOURCE, "--skill", skill, "-g", "-y", "-a", "*"];
|
|
63
65
|
}
|
|
64
66
|
function mcpCommand(name, url, bearer) {
|
|
65
67
|
return [
|
|
@@ -142,10 +144,14 @@ export async function runInstall(args, opts, help = false) {
|
|
|
142
144
|
const redact = redactor(token);
|
|
143
145
|
const results = {};
|
|
144
146
|
if (target === "skill" || target === "all") {
|
|
145
|
-
const command = skillCommand();
|
|
146
147
|
if (human)
|
|
147
|
-
process.stdout.write("Installing
|
|
148
|
-
|
|
148
|
+
process.stdout.write("Installing skills…\n");
|
|
149
|
+
for (const skill of SKILL_NAMES) {
|
|
150
|
+
const command = skillCommand(skill);
|
|
151
|
+
results[`skill:${skill}`] = dryRun
|
|
152
|
+
? { command, ok: true, skipped: "dry-run" }
|
|
153
|
+
: runStep(run, command);
|
|
154
|
+
}
|
|
149
155
|
}
|
|
150
156
|
if (target === "mcp" || target === "all") {
|
|
151
157
|
if (!dryRun && !token) {
|
|
@@ -179,14 +185,26 @@ export async function runInstall(args, opts, help = false) {
|
|
|
179
185
|
return failed ? 1 : 0;
|
|
180
186
|
}
|
|
181
187
|
printHumanSteps(results, redact, verbose);
|
|
188
|
+
const skillResults = Object.entries(results)
|
|
189
|
+
.filter(([step]) => step.startsWith("skill:"))
|
|
190
|
+
.map(([, r]) => r);
|
|
191
|
+
const skillsOk = skillResults.length > 0 && skillResults.every((r) => r.ok);
|
|
192
|
+
const skillsFailed = skillResults.some((r) => !r.ok);
|
|
182
193
|
if (!failed && !dryRun) {
|
|
183
|
-
|
|
194
|
+
const stepLabels = [
|
|
195
|
+
...new Set(Object.keys(results).map((k) => (k.startsWith("skill:") ? "skills" : k))),
|
|
196
|
+
];
|
|
197
|
+
printSuccessFooter(stepLabels, signedIn);
|
|
184
198
|
}
|
|
185
|
-
else if (failed && !dryRun &&
|
|
199
|
+
else if (failed && !dryRun && skillsOk && results.mcp && !results.mcp.ok) {
|
|
186
200
|
const next = results.mcp.skipped === "sign-in"
|
|
187
201
|
? "Sign in with `uploads login`, then re-run `uploads install mcp`."
|
|
188
202
|
: "Fix the MCP step above, then re-run `uploads install mcp`.";
|
|
189
|
-
process.stdout.write(`\
|
|
203
|
+
process.stdout.write(`\nSkills are installed. ${next}\n`);
|
|
204
|
+
}
|
|
205
|
+
else if (failed && !dryRun && skillsFailed) {
|
|
206
|
+
// Mixed or total skill failure used to print only per-step lines (#191).
|
|
207
|
+
process.stdout.write("\nSkill install incomplete. Fix the errors above, then re-run `uploads install skill`.\n");
|
|
190
208
|
}
|
|
191
209
|
return failed ? 1 : 0;
|
|
192
210
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseCommandArgs } from "../cli-args.js";
|
|
2
|
+
import { resolveApiUrl } from "../config.js";
|
|
2
3
|
import { createMcpServer } from "../mcp/server.js";
|
|
3
4
|
import { serveStdio } from "../mcp/stdio.js";
|
|
4
5
|
import { createUploadsMcpTools } from "../mcp/tools.js";
|
|
@@ -31,6 +32,7 @@ export async function runMcp(args, opts, help = false) {
|
|
|
31
32
|
const server = createMcpServer({
|
|
32
33
|
serverInfo: { name: "uploads", version: packageVersion() },
|
|
33
34
|
tools: createUploadsMcpTools({ globals: opts.globals }),
|
|
35
|
+
apiUrl: resolveApiUrl(opts.globals),
|
|
34
36
|
});
|
|
35
37
|
await serveStdio(server);
|
|
36
38
|
return 0;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uploads report` — explicit opt-in diagnostic message (+ optional log).
|
|
3
|
+
*/
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { flagBool, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
6
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
7
|
+
import { writeJson } from "../io.js";
|
|
8
|
+
import { attachmentFromText, buildReportPayload, loadReportAttachment, MAX_REPORT_ATTACHMENT_BYTES, MAX_REPORT_MESSAGE, parseReportType, reportFallbackHint, REPORT_TYPES, submitReport, validateReportMessage, } from "../report.js";
|
|
9
|
+
const REPORT_HELP = `uploads report [message] — send a diagnostic report (explicit opt-in)
|
|
10
|
+
|
|
11
|
+
Nothing is sent unless you run this (or the MCP report tool).
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--file <path> Attach a text log/trace (max 256 KiB)
|
|
15
|
+
--type <t> bug | error | idea | other (default: other)
|
|
16
|
+
--contact <value> Optional email/handle for follow-up
|
|
17
|
+
--command <name> Command that failed (e.g. put) — no args/paths
|
|
18
|
+
--error-code <code> Optional UploadsError code (e.g. KEY_POLICY)
|
|
19
|
+
--dry-run Print the payload without sending
|
|
20
|
+
--json JSON on stdout
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
uploads report "put fails with KEY_POLICY on custom prefixes"
|
|
24
|
+
uploads report --type bug --file ./trace.log "crash during optimize"
|
|
25
|
+
uploads doctor --json 2>&1 | uploads report "doctor failed"
|
|
26
|
+
`;
|
|
27
|
+
/**
|
|
28
|
+
* Bound stdin read. Stops once maxBytes is exceeded (returns what was read
|
|
29
|
+
* plus a flag so callers can reject oversized input).
|
|
30
|
+
*/
|
|
31
|
+
async function readStdinBounded(maxBytes) {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let total = 0;
|
|
34
|
+
let exceeded = false;
|
|
35
|
+
for await (const chunk of process.stdin) {
|
|
36
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
37
|
+
total += buf.byteLength;
|
|
38
|
+
if (total > maxBytes) {
|
|
39
|
+
exceeded = true;
|
|
40
|
+
const keep = maxBytes - (total - buf.byteLength);
|
|
41
|
+
if (keep > 0)
|
|
42
|
+
chunks.push(buf.subarray(0, keep));
|
|
43
|
+
break; // stop reading; peer close drains the rest
|
|
44
|
+
}
|
|
45
|
+
chunks.push(buf);
|
|
46
|
+
}
|
|
47
|
+
return { text: Buffer.concat(chunks).toString("utf8"), exceeded };
|
|
48
|
+
}
|
|
49
|
+
export async function runReport(args, opts = {}, help = false) {
|
|
50
|
+
const parsed = parseCommandArgs(args);
|
|
51
|
+
if (help || parsed.help) {
|
|
52
|
+
writeCommandHelp(REPORT_HELP);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
const typeFlag = flagString(parsed.flags, "--type");
|
|
56
|
+
if (typeFlag && !parseReportType(typeFlag)) {
|
|
57
|
+
throw new UsageError(`--type must be one of: ${REPORT_TYPES.join(", ")}`);
|
|
58
|
+
}
|
|
59
|
+
const type = parseReportType(typeFlag) ?? "other";
|
|
60
|
+
const dryRun = flagBool(parsed.flags, "--dry-run");
|
|
61
|
+
const json = opts.json || flagBool(parsed.flags, "--json");
|
|
62
|
+
const filePath = flagString(parsed.flags, "--file");
|
|
63
|
+
const positional = parsed.positionals.length > 0 ? parsed.positionals.join(" ") : undefined;
|
|
64
|
+
const hasPositional = Boolean(positional?.trim());
|
|
65
|
+
const stdinIsPipe = !process.stdin.isTTY;
|
|
66
|
+
// Only consume stdin when we still need a message or a piped attachment.
|
|
67
|
+
// Skip when --file already supplies the log and a positional message exists.
|
|
68
|
+
const needStdinAsMessage = stdinIsPipe && !hasPositional;
|
|
69
|
+
const needStdinAsAttachment = stdinIsPipe && hasPositional && !filePath;
|
|
70
|
+
let piped = "";
|
|
71
|
+
let pipedExceeded = false;
|
|
72
|
+
if (needStdinAsMessage || needStdinAsAttachment) {
|
|
73
|
+
const max = needStdinAsAttachment ? MAX_REPORT_ATTACHMENT_BYTES : MAX_REPORT_MESSAGE + 1; // +1 so we can detect "too long"
|
|
74
|
+
const result = await readStdinBounded(max);
|
|
75
|
+
piped = result.text;
|
|
76
|
+
pipedExceeded = result.exceeded;
|
|
77
|
+
}
|
|
78
|
+
const pipedTrimmed = piped.trim();
|
|
79
|
+
let raw = null;
|
|
80
|
+
if (hasPositional)
|
|
81
|
+
raw = positional;
|
|
82
|
+
else if (pipedTrimmed)
|
|
83
|
+
raw = pipedTrimmed;
|
|
84
|
+
else if (process.stdin.isTTY) {
|
|
85
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
86
|
+
try {
|
|
87
|
+
const answer = await rl.question("What's going wrong? (blank to cancel)\n> ");
|
|
88
|
+
raw = answer.trim() || null;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
rl.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (raw === null) {
|
|
95
|
+
if (json)
|
|
96
|
+
await writeJson({ ok: false, cancelled: true });
|
|
97
|
+
else
|
|
98
|
+
process.stderr.write("Cancelled — no report sent.\n");
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
if (needStdinAsMessage && (pipedExceeded || raw.length > MAX_REPORT_MESSAGE)) {
|
|
102
|
+
throw new UsageError('piped input is too long for the message — use: `… | uploads report "summary"`');
|
|
103
|
+
}
|
|
104
|
+
const validated = validateReportMessage(raw);
|
|
105
|
+
if (!validated.ok)
|
|
106
|
+
throw new UsageError(validated.error);
|
|
107
|
+
let attachment;
|
|
108
|
+
try {
|
|
109
|
+
if (filePath)
|
|
110
|
+
attachment = loadReportAttachment(filePath);
|
|
111
|
+
else if (needStdinAsAttachment && pipedTrimmed) {
|
|
112
|
+
if (pipedExceeded) {
|
|
113
|
+
throw new Error(`attachment exceeds ${MAX_REPORT_ATTACHMENT_BYTES} bytes`);
|
|
114
|
+
}
|
|
115
|
+
attachment = attachmentFromText(piped, "stdin.log");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
throw new UsageError(err instanceof Error ? err.message : String(err));
|
|
120
|
+
}
|
|
121
|
+
const payload = buildReportPayload(validated.message, {
|
|
122
|
+
type,
|
|
123
|
+
contact: flagString(parsed.flags, "--contact")?.trim() || undefined,
|
|
124
|
+
surface: "cli",
|
|
125
|
+
command: flagString(parsed.flags, "--command"),
|
|
126
|
+
errorCode: flagString(parsed.flags, "--error-code"),
|
|
127
|
+
attachment,
|
|
128
|
+
});
|
|
129
|
+
if (dryRun) {
|
|
130
|
+
const preview = {
|
|
131
|
+
...payload,
|
|
132
|
+
attachment: payload.attachment
|
|
133
|
+
? {
|
|
134
|
+
filename: payload.attachment.filename,
|
|
135
|
+
contentType: payload.attachment.contentType,
|
|
136
|
+
bytes: new TextEncoder().encode(payload.attachment.body).byteLength,
|
|
137
|
+
body: "[omitted in dry-run]",
|
|
138
|
+
}
|
|
139
|
+
: undefined,
|
|
140
|
+
};
|
|
141
|
+
if (json)
|
|
142
|
+
await writeJson({ dryRun: true, payload: preview });
|
|
143
|
+
else
|
|
144
|
+
process.stdout.write(`[dry-run] would POST:\n${JSON.stringify(preview, null, 2)}\n`);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
const result = await submitReport(payload, { apiUrl: opts.apiUrl });
|
|
148
|
+
if (result.ok) {
|
|
149
|
+
if (json) {
|
|
150
|
+
await writeJson({ ok: true, id: result.id, hasAttachment: result.hasAttachment });
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
process.stdout.write(`Thanks — report received (id: ${result.id}${result.hasAttachment ? ", with attachment" : ""})\n`);
|
|
154
|
+
}
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
if (json)
|
|
158
|
+
await writeJson({ ok: false, error: result.error });
|
|
159
|
+
else {
|
|
160
|
+
process.stderr.write(`error: couldn't send report: ${result.error}\n`);
|
|
161
|
+
process.stderr.write(`hint: ${reportFallbackHint()}\n`);
|
|
162
|
+
}
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uploads telemetry` — status / enable / disable for anonymous usage pings.
|
|
3
|
+
*/
|
|
4
|
+
import { flagBool, parseCommandArgs } from "../cli-args.js";
|
|
5
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
6
|
+
import { writeJson } from "../io.js";
|
|
7
|
+
import { setTelemetryEnabled, telemetryStatus, defaultTelemetryDataDir } from "../telemetry.js";
|
|
8
|
+
const TELEMETRY_HELP = `uploads telemetry — manage anonymous usage telemetry
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
status Show whether telemetry is enabled and where events go
|
|
12
|
+
enable Enable anonymous usage telemetry
|
|
13
|
+
disable Disable anonymous usage telemetry
|
|
14
|
+
|
|
15
|
+
What is collected (when enabled):
|
|
16
|
+
command name, CLI version, OS/arch, runtime, exit code, duration,
|
|
17
|
+
client kind/agent, anonymous id, optional allowlisted error code.
|
|
18
|
+
Never arguments, paths, tokens, workspace names, or file content.
|
|
19
|
+
|
|
20
|
+
Opt out without this command:
|
|
21
|
+
UPLOADS_TELEMETRY_DISABLED=1
|
|
22
|
+
DO_NOT_TRACK=1
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
uploads telemetry status
|
|
26
|
+
uploads telemetry status --json
|
|
27
|
+
uploads telemetry disable
|
|
28
|
+
uploads telemetry enable
|
|
29
|
+
`;
|
|
30
|
+
export async function runTelemetry(args, opts = {}, help = false) {
|
|
31
|
+
const parsed = parseCommandArgs(args);
|
|
32
|
+
if (help || parsed.help || parsed.positionals.length === 0) {
|
|
33
|
+
writeCommandHelp(TELEMETRY_HELP);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
const sub = parsed.positionals[0];
|
|
37
|
+
const json = opts.json || flagBool(parsed.flags, "--json");
|
|
38
|
+
switch (sub) {
|
|
39
|
+
case "status": {
|
|
40
|
+
const s = telemetryStatus({ apiUrl: opts.apiUrl });
|
|
41
|
+
if (json) {
|
|
42
|
+
await writeJson({
|
|
43
|
+
enabled: s.enabled,
|
|
44
|
+
reason: s.reason ?? null,
|
|
45
|
+
anonId: s.anonId,
|
|
46
|
+
clientKind: s.clientKind,
|
|
47
|
+
agentName: s.agentName ?? null,
|
|
48
|
+
endpoint: s.endpoint,
|
|
49
|
+
dataDir: defaultTelemetryDataDir(),
|
|
50
|
+
});
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
process.stdout.write(`Telemetry: ${s.enabled ? "enabled" : "disabled"}\n`);
|
|
54
|
+
if (!s.enabled && s.reason)
|
|
55
|
+
process.stdout.write(`Reason: ${s.reason}\n`);
|
|
56
|
+
process.stdout.write(`Anon ID: ${s.anonId}\n`);
|
|
57
|
+
process.stdout.write(`Kind: ${s.clientKind}${s.agentName ? ` (${s.agentName})` : ""}\n`);
|
|
58
|
+
process.stdout.write(`Endpoint: ${s.endpoint}\n`);
|
|
59
|
+
process.stdout.write("\nAnonymous non-PII usage only (no paths, tokens, or content).\n");
|
|
60
|
+
process.stdout.write("See `uploads telemetry --help` for what is sent and how to opt out.\n");
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
case "enable": {
|
|
64
|
+
setTelemetryEnabled(true);
|
|
65
|
+
const s = telemetryStatus({ apiUrl: opts.apiUrl });
|
|
66
|
+
if (json) {
|
|
67
|
+
await writeJson({ enabled: s.enabled, reason: s.reason ?? null });
|
|
68
|
+
}
|
|
69
|
+
else if (s.enabled) {
|
|
70
|
+
process.stdout.write("Telemetry enabled.\n");
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
process.stdout.write(`Telemetry still disabled (${s.reason ?? "opt-out active"}).\n`);
|
|
74
|
+
process.stdout.write("Cleared the local disable file; env opt-outs still apply.\n");
|
|
75
|
+
}
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
case "disable": {
|
|
79
|
+
setTelemetryEnabled(false);
|
|
80
|
+
if (json)
|
|
81
|
+
await writeJson({ enabled: false });
|
|
82
|
+
else
|
|
83
|
+
process.stdout.write("Telemetry disabled.\n");
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
process.stderr.write(`error: unknown telemetry subcommand: ${sub}\n`);
|
|
88
|
+
process.stderr.write("hint: uploads telemetry --help\n");
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
}
|