@buildinternet/uploads 0.55.0 → 0.55.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/dist/cli.js CHANGED
@@ -402,7 +402,7 @@ export async function runCli(argv) {
402
402
  return 2;
403
403
  }
404
404
  }
405
- // Best-effort; skipped for mcp, --quiet/--json, and opt-out env vars.
405
+ // Best-effort; skipped for mcp, update, --quiet/--json, and opt-out env vars.
406
406
  if (code === 0 && !showHelp) {
407
407
  await maybeHintUpdate({ quiet: quiet || json, command: parsed.command });
408
408
  }
@@ -31,6 +31,12 @@ export declare function npmTooOldHint(version: string): string;
31
31
  */
32
32
  export declare function probeSkillTooling(run: CommandRunner): string | undefined;
33
33
  export declare function runStep(run: CommandRunner, command: string[]): StepResult;
34
+ /**
35
+ * True when `mcp list` output already names this server.
36
+ * Matches a name token (Claude's `uploads: url` form, tables, JSON) and not
37
+ * a substring of a host like `agents.uploads.sh`.
38
+ */
39
+ export declare function mcpListIncludesName(output: string, name: string): boolean;
34
40
  export declare function runInstall(args: string[], opts: {
35
41
  globals: GlobalFlags;
36
42
  json?: boolean;
@@ -1,6 +1,6 @@
1
1
  import { flagBool, flagString, parseCommandArgs, UsageError, } from "../cli-args.js";
2
2
  import { resolveConfig } from "../config.js";
3
- import { execRunner } from "../github-gh.js";
3
+ import { execRunner, timedExecRunner } from "../github-gh.js";
4
4
  import { writeCommandHelp } from "../cli-style.js";
5
5
  import { HOOK_COMMAND, HOOK_INVOCATION, installHookManifests, } from "../hooks-install.js";
6
6
  export const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
@@ -36,7 +36,9 @@ export const MCP_CLIENTS = [
36
36
  // Codex HTTP MCP has no --header; auth is OAuth on first use (same as the
37
37
  // plugin's .mcp.json). Passing --bearer-token-env-var UPLOADS_TOKEN would
38
38
  // break machines that signed in via `uploads login` (token lives in the
39
- // config file, not the environment).
39
+ // config file, not the environment). A second `mcp add` re-opens Codex's
40
+ // localhost callback ("Authentication complete. You may close this
41
+ // window.") — probe `mcp list` first so a refresh does not do that.
40
42
  command: (name, url) => ["codex", "mcp", "add", name, "--url", url],
41
43
  },
42
44
  {
@@ -58,9 +60,10 @@ workspace from the bearer token, so only the token is needed.
58
60
  Claude Code and Codex ship the same reminder via their plugins (same command:
59
61
  \`${HOOK_INVOCATION}\`) — install those plugins instead of relying on this step.
60
62
 
61
- Safe to re-run. An MCP server already registered under this name is reported
62
- as \`already configured\` and left as-is — including the token it was created
63
- with. To point it at a new token: \`<cli> mcp remove <name>\` first
63
+ Safe to re-run. Before \`mcp add\`, install checks \`mcp list\`. A server that is
64
+ already registered is reported as \`already configured\` and left as-is —
65
+ including the token it was created with — so a refresh does not open a browser
66
+ for OAuth. To point it at a new token: \`<cli> mcp remove <name>\` first
64
67
  (e.g. \`claude mcp remove uploads\`).
65
68
 
66
69
  Usage:
@@ -187,6 +190,28 @@ function skillCommand(skill) {
187
190
  function isAlreadyConfigured(error) {
188
191
  return /already exists|already (configured|registered|present)|duplicate/i.test(error);
189
192
  }
193
+ /**
194
+ * True when `mcp list` output already names this server.
195
+ * Matches a name token (Claude's `uploads: url` form, tables, JSON) and not
196
+ * a substring of a host like `agents.uploads.sh`.
197
+ */
198
+ export function mcpListIncludesName(output, name) {
199
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
200
+ const token = new RegExp(`(?:^|[\\s"'\\[{,])${escaped}(?:[:\\s"',\\]}]|$)`, "m");
201
+ return token.test(output);
202
+ }
203
+ /** Best-effort `mcp list` probe. Never opens a browser; times out on the default runner. */
204
+ const MCP_LIST_TIMEOUT_MS = 8_000;
205
+ function probeMcpAlreadyConfigured(run, client, name) {
206
+ const probe = run === execRunner ? timedExecRunner(MCP_LIST_TIMEOUT_MS) : run;
207
+ try {
208
+ const output = probe(client.id, ["mcp", "list"]);
209
+ return mcpListIncludesName(output, name) ? "yes" : "no";
210
+ }
211
+ catch (err) {
212
+ return isEnoent(err) ? "missing" : "no";
213
+ }
214
+ }
190
215
  function mcpStepKey(client) {
191
216
  return `mcp:${client.id}`;
192
217
  }
@@ -215,8 +240,25 @@ function partitionSteps(results) {
215
240
  }
216
241
  return { skills, mcp, other };
217
242
  }
218
- /** Run one client's `mcp add`; missing binaries skip, duplicates are already-configured. */
219
- function runMcpClientStep(run, command) {
243
+ /**
244
+ * Run one client's `mcp add`. Probe `mcp list` first so a refresh does not
245
+ * re-run add — Codex (and some other clients) treat a second add as success
246
+ * and open a localhost OAuth callback even when the server is already there.
247
+ */
248
+ function runMcpClientStep(run, client, name, url, bearer) {
249
+ const command = client.command(name, url, bearer);
250
+ const listed = probeMcpAlreadyConfigured(run, client, name);
251
+ if (listed === "yes") {
252
+ return { command, ok: true, skipped: "already-configured" };
253
+ }
254
+ if (listed === "missing") {
255
+ return {
256
+ command,
257
+ ok: true,
258
+ skipped: "missing-cli",
259
+ error: `${client.id} not found on PATH`,
260
+ };
261
+ }
220
262
  try {
221
263
  const output = run(command[0], command.slice(1)).trim();
222
264
  return { command, ok: true, output: output || undefined };
@@ -412,7 +454,7 @@ export async function runInstall(args, opts, help = false) {
412
454
  results[key] = { command, ok: true, skipped: "dry-run" };
413
455
  }
414
456
  else {
415
- results[key] = runMcpClientStep(run, command);
457
+ results[key] = runMcpClientStep(run, client, name, url, bearer);
416
458
  }
417
459
  }
418
460
  }
@@ -11,9 +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\` (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.
14
+ registered is left as-is (\`already configured\`) — install checks \`mcp list\`
15
+ first so \`mcp add\` (and Codex's OAuth browser) does not run again. A missing
16
+ agent CLI is skipped so it does not fail the rest of the refresh.
17
17
 
18
18
  Usage:
19
19
  uploads update [options]
@@ -6,7 +6,10 @@ export interface UpdateCache {
6
6
  }
7
7
  export interface UpdateCheckOptions {
8
8
  quiet?: boolean;
9
- /** mcp is always skipped (stdio purity). */
9
+ /**
10
+ * `mcp` is always skipped (stdio purity). `update` is skipped so a successful
11
+ * upgrade does not immediately hint the old in-process version.
12
+ */
10
13
  command?: string;
11
14
  currentVersion?: string;
12
15
  cachePath?: string;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Checks the registry at most once per day, never throws, never blocks longer
5
5
  * than a short timeout, and writes only to stderr. Silence with --quiet,
6
- * UPLOADS_NO_UPDATE=1, or NO_UPDATE_NOTIFIER=1.
6
+ * `uploads update`, `uploads mcp`, UPLOADS_NO_UPDATE=1, or NO_UPDATE_NOTIFIER=1.
7
7
  */
8
8
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
@@ -75,7 +75,7 @@ export function writeUpdateCache(path, cache) {
75
75
  export async function checkForUpdate(opts = {}) {
76
76
  const current = opts.currentVersion ?? packageVersion();
77
77
  try {
78
- if (opts.quiet || opts.command === "mcp") {
78
+ if (opts.quiet || opts.command === "mcp" || opts.command === "update") {
79
79
  return { current, updateAvailable: false };
80
80
  }
81
81
  if (truthyEnv("UPLOADS_NO_UPDATE") || truthyEnv("NO_UPDATE_NOTIFIER")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.55.0",
3
+ "version": "0.55.1",
4
4
  "mcpName": "sh.uploads/mcp",
5
5
  "description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
6
6
  "type": "module",