@buildinternet/uploads 0.54.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/README.md +25 -3
- package/dist/cli-catalog.js +10 -0
- package/dist/cli-help.js +1 -0
- package/dist/cli.js +6 -1
- package/dist/client.d.ts +60 -0
- package/dist/client.js +24 -0
- package/dist/commands/feed.d.ts +2 -0
- package/dist/commands/feed.js +133 -0
- package/dist/commands/install.d.ts +6 -0
- package/dist/commands/install.js +50 -8
- package/dist/commands/update.js +3 -3
- package/dist/comment-render.generated.d.ts +1 -1
- package/dist/github.d.ts +12 -0
- package/dist/github.js +40 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/output-schemas.d.ts +1 -0
- package/dist/mcp/output-schemas.js +37 -0
- package/dist/mcp/tools.js +94 -1
- package/dist/update-check.d.ts +4 -1
- package/dist/update-check.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ uploads put --url https://cdn.example/shot.png --pr 123
|
|
|
31
31
|
uploads put --url http://localhost:4321/shot.png
|
|
32
32
|
uploads gallery create --title "Release screenshots"
|
|
33
33
|
uploads put ./after.png --gallery gal_example
|
|
34
|
+
uploads feed create --repo owner/repo
|
|
35
|
+
uploads feed create --repo owner/repo --pr 123
|
|
34
36
|
# custom metadata (queryable): page URL, in-app path, which surface
|
|
35
37
|
uploads put ./shot.png --meta url=https://app.example/settings --meta path=/settings --meta app=web
|
|
36
38
|
uploads meta get screenshots/myapp/42/shot.webp
|
|
@@ -43,7 +45,7 @@ Inside this monorepo only, `pnpm uploads …` builds the package first so you pi
|
|
|
43
45
|
up local source; product docs and PR “how to try it” examples should use the
|
|
44
46
|
global `uploads` form above.
|
|
45
47
|
|
|
46
|
-
Commands: `attach`, `put`, `screenshot`, `annotate`, `gallery`, `comment`, `list`, `find`, `meta`, `delete`, `usage`,
|
|
48
|
+
Commands: `attach`, `put`, `screenshot`, `annotate`, `gallery`, `feed`, `comment`, `list`, `find`, `meta`, `delete`, `usage`,
|
|
47
49
|
`reconcile`, `purge-expired`, `setup`, `install`, `login`, `whoami` (alias `status`),
|
|
48
50
|
`logout`, `invite`, `admin`, `config`, `telemetry`, `report`, `doctor`, `health`, `changelog`, `docs`, `mcp`,
|
|
49
51
|
`completion`.
|
|
@@ -192,13 +194,33 @@ When adding several keys, `uploads gallery add` processes them sequentially and
|
|
|
192
194
|
individual failures in `--json` output. Gallery item updates use the API's current version to
|
|
193
195
|
avoid overwriting concurrent changes.
|
|
194
196
|
|
|
197
|
+
## Change feeds
|
|
198
|
+
|
|
199
|
+
A feed is a public newest-first page of screenshots already tagged with a GitHub `owner/repo`,
|
|
200
|
+
or with one pull request or issue. Same product — `--pr` / `--issue` / `--github` is one extra
|
|
201
|
+
filter. It is a live query, not a curated gallery. Use a gallery when you pick the files; use
|
|
202
|
+
a repo feed for the latest shots across a repo; use a PR feed when reviewers should see only
|
|
203
|
+
that pull request. The API returns the canonical public URL.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
uploads feed create --repo owner/repo
|
|
207
|
+
uploads feed create --repo owner/repo --pr 123
|
|
208
|
+
uploads feed create --github owner/repo#123
|
|
209
|
+
uploads feed create --repo owner/repo --path /settings
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Creating the same scope again returns the existing URL. Anyone who knows that URL can
|
|
213
|
+
view the feed. Each shot also has `/c/<id>/<item>` with previous / next. Syncing a
|
|
214
|
+
managed PR comment creates the PR feed if needed and points image clicks at that pager.
|
|
215
|
+
MCP: `feed_create` (`repo`, plus `pr` / `issue` / `github` / `path`) and `feed_get`.
|
|
216
|
+
|
|
195
217
|
Link a gallery to a GitHub issue or pull request with `gallery link --github`. Run `uploads comment --pr <number>` to refresh that target’s one managed comment with every linked gallery and loose attachment. Coordinates and strict `https://github.com/<owner>/<repo>/issues|pull/<number>` URLs are accepted; `gallery list --github` performs the authenticated reverse lookup. Links never change gallery identity, and GitHub repository visibility does not make the public gallery private.
|
|
196
218
|
|
|
197
219
|
Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
|
|
198
220
|
|
|
199
221
|
## MCP server
|
|
200
222
|
|
|
201
|
-
`uploads mcp` serves the Model Context Protocol over stdio (newline-delimited JSON-RPC, no extra dependencies). Tools include file operations plus public gallery workflows
|
|
223
|
+
`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`, `gallery_find_by_reference`) and change feeds (`feed_create`, `feed_get` — pass `pr` or `github` to scope a feed to one pull request). Gallery and feed 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`, `whoami`, 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 (`whoami` needs no auth).
|
|
202
224
|
|
|
203
225
|
```json
|
|
204
226
|
{ "command": "uploads", "args": ["--env-file", "/path/to/.env", "mcp"] }
|
|
@@ -208,7 +230,7 @@ Or with `UPLOADS_TOKEN`/`UPLOADS_WORKSPACE` in the environment or user config. C
|
|
|
208
230
|
|
|
209
231
|
The MCP Registry lists this server as `sh.uploads/mcp`.
|
|
210
232
|
|
|
211
|
-
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 `
|
|
233
|
+
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`, `gallery_find_by_reference`, `feed_create`, and `feed_get`; all use the same bearer-token workspace scopes and gallery/feed 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.
|
|
212
234
|
|
|
213
235
|
## Programmatic use
|
|
214
236
|
|
package/dist/cli-catalog.js
CHANGED
|
@@ -156,6 +156,16 @@ export const ROOT_COMMANDS = [
|
|
|
156
156
|
{ name: "unlink", summary: "Unlink a gallery from GitHub" },
|
|
157
157
|
],
|
|
158
158
|
},
|
|
159
|
+
{
|
|
160
|
+
name: "feed",
|
|
161
|
+
summary: "Create a public newest-first screenshot feed for a GitHub repo or pull request",
|
|
162
|
+
subcommands: [
|
|
163
|
+
{ name: "create", summary: "Create or reuse a repo or PR change feed" },
|
|
164
|
+
{ name: "show", summary: "Show a feed" },
|
|
165
|
+
{ name: "list", summary: "List feeds" },
|
|
166
|
+
{ name: "delete", summary: "Delete a feed record" },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
159
169
|
{
|
|
160
170
|
name: "comment",
|
|
161
171
|
summary: "Create/update a PR/issue attachments comment (via gh)",
|
package/dist/cli-help.js
CHANGED
|
@@ -165,6 +165,7 @@ ${section(style, "Examples:")}
|
|
|
165
165
|
${style.command("uploads attach")} ./artifact.zip --issue 45 --no-comment
|
|
166
166
|
${style.command("uploads attach")} ./shot.png --meta path=/settings --state after
|
|
167
167
|
${style.command("uploads gallery")} create --title "Release screenshots"
|
|
168
|
+
${style.command("uploads feed")} create --repo owner/repo
|
|
168
169
|
${style.command("uploads doctor")}
|
|
169
170
|
${style.command("uploads logout")}
|
|
170
171
|
${style.command("uploads --version")}
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { commandSummary, suggestCommand } from "./cli-suggest.js";
|
|
|
7
7
|
import { writeJson } from "./io.js";
|
|
8
8
|
import { colorEnabled, createStyle } from "./cli-style.js";
|
|
9
9
|
import { runPut, runAttach, runStaged, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runGithub, runIngest, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
|
|
10
|
+
import { runFeed } from "./commands/feed.js";
|
|
10
11
|
import { runConfig } from "./commands/config.js";
|
|
11
12
|
import { runSetup } from "./commands/setup.js";
|
|
12
13
|
import { runLogin } from "./commands/login.js";
|
|
@@ -309,6 +310,7 @@ export async function runCli(argv) {
|
|
|
309
310
|
case "staged":
|
|
310
311
|
case "screenshot":
|
|
311
312
|
case "gallery":
|
|
313
|
+
case "feed":
|
|
312
314
|
case "list":
|
|
313
315
|
case "find":
|
|
314
316
|
case "meta":
|
|
@@ -337,6 +339,9 @@ export async function runCli(argv) {
|
|
|
337
339
|
case "gallery":
|
|
338
340
|
code = await runGallery(ctx, cmdArgs, showHelp);
|
|
339
341
|
break;
|
|
342
|
+
case "feed":
|
|
343
|
+
code = await runFeed(ctx, cmdArgs, showHelp);
|
|
344
|
+
break;
|
|
340
345
|
case "comment":
|
|
341
346
|
code = await runComment(ctx, cmdArgs, showHelp);
|
|
342
347
|
break;
|
|
@@ -397,7 +402,7 @@ export async function runCli(argv) {
|
|
|
397
402
|
return 2;
|
|
398
403
|
}
|
|
399
404
|
}
|
|
400
|
-
// 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.
|
|
401
406
|
if (code === 0 && !showHelp) {
|
|
402
407
|
await maybeHintUpdate({ quiet: quiet || json, command: parsed.command });
|
|
403
408
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -257,6 +257,59 @@ export interface FindGalleriesByReferenceOptions {
|
|
|
257
257
|
limit?: number;
|
|
258
258
|
cursor?: string;
|
|
259
259
|
}
|
|
260
|
+
/** A workspace-owned, query-backed public repo change feed. */
|
|
261
|
+
export interface FeedItem {
|
|
262
|
+
id: string;
|
|
263
|
+
objectKey: string;
|
|
264
|
+
filename: string;
|
|
265
|
+
status: "available" | "missing" | "withheld";
|
|
266
|
+
url: string | null;
|
|
267
|
+
embedUrl: string | null;
|
|
268
|
+
/** Canonical `/c/<id>/<item>` page when the API computed one. */
|
|
269
|
+
pageUrl?: string;
|
|
270
|
+
contentType: string | null;
|
|
271
|
+
size: number | null;
|
|
272
|
+
uploaded: string | null;
|
|
273
|
+
modified: string | null;
|
|
274
|
+
path: string | null;
|
|
275
|
+
state: string | null;
|
|
276
|
+
posterUrl?: string;
|
|
277
|
+
videoDimensions?: {
|
|
278
|
+
width: number;
|
|
279
|
+
height: number;
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
export interface Feed {
|
|
283
|
+
id: string;
|
|
284
|
+
/** Canonical public URL returned by the API; clients must not construct it. */
|
|
285
|
+
url: string;
|
|
286
|
+
workspace: string;
|
|
287
|
+
repo: string;
|
|
288
|
+
path: string | null;
|
|
289
|
+
number: number | null;
|
|
290
|
+
kind: "pull" | "issue" | null;
|
|
291
|
+
title: string;
|
|
292
|
+
createdAt: string;
|
|
293
|
+
updatedAt: string;
|
|
294
|
+
items: FeedItem[];
|
|
295
|
+
}
|
|
296
|
+
export type FeedSummary = Omit<Feed, "items">;
|
|
297
|
+
export interface FeedListOptions {
|
|
298
|
+
limit?: number;
|
|
299
|
+
cursor?: string;
|
|
300
|
+
}
|
|
301
|
+
export interface FeedListResult {
|
|
302
|
+
feeds: FeedSummary[];
|
|
303
|
+
nextCursor: string | null;
|
|
304
|
+
}
|
|
305
|
+
export interface CreateFeedOptions {
|
|
306
|
+
repo: string;
|
|
307
|
+
path?: string | null;
|
|
308
|
+
number?: number | null;
|
|
309
|
+
kind?: "pull" | "issue" | null;
|
|
310
|
+
pr?: number | null;
|
|
311
|
+
issue?: number | null;
|
|
312
|
+
}
|
|
260
313
|
/**
|
|
261
314
|
* Reasons the bot did not post. The CLI falls back to the local `gh` path
|
|
262
315
|
* for all of these except `not_authorized` (issue #297 baseline control):
|
|
@@ -734,6 +787,13 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
734
787
|
id: string;
|
|
735
788
|
}>;
|
|
736
789
|
findGalleriesByReference(opts: FindGalleriesByReferenceOptions): Promise<GalleryListResult>;
|
|
790
|
+
createFeed(opts: CreateFeedOptions): Promise<Feed>;
|
|
791
|
+
getFeed(id: string): Promise<Feed>;
|
|
792
|
+
listFeeds(opts?: FeedListOptions): Promise<FeedListResult>;
|
|
793
|
+
deleteFeed(id: string): Promise<{
|
|
794
|
+
deleted: boolean;
|
|
795
|
+
id: string;
|
|
796
|
+
}>;
|
|
737
797
|
/**
|
|
738
798
|
* Upsert the managed attachments comment. `resync: true` marks an
|
|
739
799
|
* explicit "make the comment state correct" call (`uploads comment`), so
|
package/dist/client.js
CHANGED
|
@@ -307,6 +307,9 @@ function usageBase(config) {
|
|
|
307
307
|
function galleriesBase(config) {
|
|
308
308
|
return `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/galleries`;
|
|
309
309
|
}
|
|
310
|
+
function feedsBase(config) {
|
|
311
|
+
return `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/feeds`;
|
|
312
|
+
}
|
|
310
313
|
function mapApiError(status, error, code, requiredScope, existingUrl) {
|
|
311
314
|
const normalized = error.toLowerCase();
|
|
312
315
|
if (status === 401 || code === "unauthorized" || normalized === "unauthorized") {
|
|
@@ -652,6 +655,27 @@ export function createUploadsClient(config) {
|
|
|
652
655
|
params.set("cursor", opts.cursor);
|
|
653
656
|
return request("GET", galleriesBase(config) + "/by-reference?" + params);
|
|
654
657
|
},
|
|
658
|
+
async createFeed(opts) {
|
|
659
|
+
return request("POST", feedsBase(config), {
|
|
660
|
+
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
661
|
+
headers: { "Content-Type": "application/json" },
|
|
662
|
+
});
|
|
663
|
+
},
|
|
664
|
+
async getFeed(id) {
|
|
665
|
+
return request("GET", `${feedsBase(config)}/${encodeURIComponent(id)}`);
|
|
666
|
+
},
|
|
667
|
+
async listFeeds(opts = {}) {
|
|
668
|
+
const params = new URLSearchParams();
|
|
669
|
+
if (opts.limit != null)
|
|
670
|
+
params.set("limit", String(opts.limit));
|
|
671
|
+
if (opts.cursor)
|
|
672
|
+
params.set("cursor", opts.cursor);
|
|
673
|
+
const qs = params.toString();
|
|
674
|
+
return request("GET", `${feedsBase(config)}${qs ? `?${qs}` : ""}`);
|
|
675
|
+
},
|
|
676
|
+
async deleteFeed(id) {
|
|
677
|
+
return request("DELETE", `${feedsBase(config)}/${encodeURIComponent(id)}`);
|
|
678
|
+
},
|
|
655
679
|
/**
|
|
656
680
|
* Upsert the managed attachments comment. `resync: true` marks an
|
|
657
681
|
* explicit "make the comment state correct" call (`uploads comment`), so
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
2
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
3
|
+
import { parseGithubIssueRef } from "../github.js";
|
|
4
|
+
import { resolveRepo } from "../github-gh.js";
|
|
5
|
+
import { writeJson, writeStdout } from "../io.js";
|
|
6
|
+
const FEED_HELP = `uploads feed <command> [args]
|
|
7
|
+
|
|
8
|
+
A public newest-first feed of screenshots tagged with a GitHub owner/repo,
|
|
9
|
+
or one pull request / issue. Same product — pass --pr, --issue, or --github
|
|
10
|
+
to scope it. A gallery is a hand-picked list; a feed is live. Anyone who
|
|
11
|
+
knows the URL can view it. Creating the same scope again returns the
|
|
12
|
+
existing feed.
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
create [--repo <owner/repo>] [--pr <n> | --issue <n> | --github <ref>] [--path <page-path>]
|
|
16
|
+
show <feed-id>
|
|
17
|
+
list [--limit <n>] [--cursor <c>]
|
|
18
|
+
delete <feed-id>
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
uploads feed create
|
|
22
|
+
uploads feed create --repo acme/app
|
|
23
|
+
uploads feed create --repo acme/app --pr 123
|
|
24
|
+
uploads feed create --github acme/app#123
|
|
25
|
+
uploads feed create --repo acme/app --path /settings
|
|
26
|
+
uploads feed show feed_example
|
|
27
|
+
`;
|
|
28
|
+
function resolveCreateOptions(parsed) {
|
|
29
|
+
const github = flagString(parsed.flags, "--github");
|
|
30
|
+
const pr = flagInt(parsed.flags, "--pr", "--pr");
|
|
31
|
+
const issue = flagInt(parsed.flags, "--issue", "--issue");
|
|
32
|
+
const path = flagString(parsed.flags, "--path");
|
|
33
|
+
const repoFlag = flagString(parsed.flags, "--repo");
|
|
34
|
+
if (pr != null && issue != null) {
|
|
35
|
+
throw new UsageError("--pr and --issue are mutually exclusive", {
|
|
36
|
+
example: "uploads feed create --repo owner/repo --pr 123",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (github && (pr != null || issue != null)) {
|
|
40
|
+
throw new UsageError("--github cannot be combined with --pr or --issue", {
|
|
41
|
+
example: "uploads feed create --github owner/repo#123",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (github) {
|
|
45
|
+
const ref = parseGithubIssueRef(github);
|
|
46
|
+
if (!ref) {
|
|
47
|
+
throw new UsageError("--github must be owner/repo#number or a GitHub issue/PR URL", {
|
|
48
|
+
example: "uploads feed create --github owner/repo#123",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (repoFlag && repoFlag.trim().toLowerCase() !== ref.repo) {
|
|
52
|
+
throw new UsageError("--repo does not match --github", {
|
|
53
|
+
example: "uploads feed create --github owner/repo#123",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
repo: ref.repo,
|
|
58
|
+
path,
|
|
59
|
+
number: ref.number,
|
|
60
|
+
kind: ref.kind,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
repo: resolveRepo(repoFlag).toLowerCase(),
|
|
65
|
+
path,
|
|
66
|
+
...(pr != null ? { number: pr, kind: "pull" } : {}),
|
|
67
|
+
...(issue != null ? { number: issue, kind: "issue" } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function runFeed(ctx, args, help = false) {
|
|
71
|
+
const parsed = parseCommandArgs(args);
|
|
72
|
+
const action = parsed.positionals[0];
|
|
73
|
+
if (help || parsed.help) {
|
|
74
|
+
writeCommandHelp(FEED_HELP);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
if (!action) {
|
|
78
|
+
throw new UsageError("feed requires a subcommand: create, show, list, or delete", {
|
|
79
|
+
example: "uploads feed create --repo owner/repo --pr 123",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
switch (action) {
|
|
83
|
+
case "create": {
|
|
84
|
+
const feed = await ctx.client.createFeed(resolveCreateOptions(parsed));
|
|
85
|
+
if (ctx.json)
|
|
86
|
+
await writeJson(feed);
|
|
87
|
+
else
|
|
88
|
+
await writeStdout(`${feed.url}\n`);
|
|
89
|
+
if (!ctx.quiet && !ctx.json)
|
|
90
|
+
process.stderr.write("warning: feeds are public to anyone with the URL\n");
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
case "show": {
|
|
94
|
+
const id = parsed.positionals[1];
|
|
95
|
+
if (!id)
|
|
96
|
+
throw new UsageError("feed show requires a feed ID");
|
|
97
|
+
const feed = await ctx.client.getFeed(id);
|
|
98
|
+
if (ctx.json)
|
|
99
|
+
await writeJson(feed);
|
|
100
|
+
else
|
|
101
|
+
await writeStdout(`${feed.url}\n`);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
case "list": {
|
|
105
|
+
const page = await ctx.client.listFeeds({
|
|
106
|
+
limit: flagInt(parsed.flags, "--limit", "--limit"),
|
|
107
|
+
cursor: flagString(parsed.flags, "--cursor"),
|
|
108
|
+
});
|
|
109
|
+
if (ctx.json)
|
|
110
|
+
await writeJson(page);
|
|
111
|
+
else {
|
|
112
|
+
for (const feed of page.feeds)
|
|
113
|
+
await writeStdout(`${feed.id} ${feed.url} ${feed.title}\n`);
|
|
114
|
+
if (page.nextCursor)
|
|
115
|
+
process.stderr.write(`cursor: ${page.nextCursor}\n`);
|
|
116
|
+
}
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
case "delete": {
|
|
120
|
+
const id = parsed.positionals[1];
|
|
121
|
+
if (!id)
|
|
122
|
+
throw new UsageError("feed delete requires a feed ID");
|
|
123
|
+
const result = await ctx.client.deleteFeed(id);
|
|
124
|
+
if (ctx.json)
|
|
125
|
+
await writeJson(result);
|
|
126
|
+
else if (!ctx.quiet)
|
|
127
|
+
process.stderr.write(`deleted feed ${result.id}\n`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
throw new UsageError(`unknown feed command: ${action} (expected create, show, list, or delete)`, { example: "uploads feed create --repo owner/repo --pr 123" });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -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;
|
package/dist/commands/install.js
CHANGED
|
@@ -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.
|
|
62
|
-
as \`already configured\` and left as-is —
|
|
63
|
-
|
|
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
|
-
/**
|
|
219
|
-
|
|
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,
|
|
457
|
+
results[key] = runMcpClientStep(run, client, name, url, bearer);
|
|
416
458
|
}
|
|
417
459
|
}
|
|
418
460
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -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\`) — \`
|
|
15
|
-
Codex
|
|
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]
|
|
@@ -90,7 +90,7 @@ export interface AttachmentItem {
|
|
|
90
90
|
url: string | null;
|
|
91
91
|
/** Prefer for `<img src>` on GitHub (Camo-friendly host). Falls back to `url`. */
|
|
92
92
|
embedUrl?: string | null;
|
|
93
|
-
/**
|
|
93
|
+
/** Preferred click-through (PR feed item, `/f/` file page, or equivalent). Falls back to `url`. */
|
|
94
94
|
pageUrl?: string | null;
|
|
95
95
|
/**
|
|
96
96
|
* The only canonical metadata the managed comment renders (issue #365).
|
package/dist/github.d.ts
CHANGED
|
@@ -5,9 +5,21 @@ export interface GithubCoordinate {
|
|
|
5
5
|
coordinate: string;
|
|
6
6
|
canonicalUrl: string;
|
|
7
7
|
}
|
|
8
|
+
/** Parsed `owner/repo#number` or a strict GitHub issue/PR URL. */
|
|
9
|
+
export interface GithubIssueRef {
|
|
10
|
+
repo: string;
|
|
11
|
+
number: number;
|
|
12
|
+
kind?: "pull" | "issue";
|
|
13
|
+
}
|
|
8
14
|
export declare function isValidRepo(repo: string): boolean;
|
|
9
15
|
/** Parse "owner/name" from a git remote URL (SSH or HTTPS), else undefined. */
|
|
10
16
|
export declare function parseRepoFromRemoteUrl(url: string): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Parse `owner/repo#number` or a strict
|
|
19
|
+
* `https://github.com/<owner>/<repo>/issues|pull/<number>` URL. Kind is set
|
|
20
|
+
* only when the URL path names `/pull/` or `/issues/`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseGithubIssueRef(value: string): GithubIssueRef | undefined;
|
|
11
23
|
/** Normalize a GitHub issue or pull-request coordinate for gallery linking. */
|
|
12
24
|
export declare function normalizeGithubCoordinate(value: string): GithubCoordinate | undefined;
|
|
13
25
|
/**
|
package/dist/github.js
CHANGED
|
@@ -13,6 +13,46 @@ export function parseRepoFromRemoteUrl(url) {
|
|
|
13
13
|
const repo = match?.[1];
|
|
14
14
|
return repo && isValidRepo(repo) ? repo : undefined;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse `owner/repo#number` or a strict
|
|
18
|
+
* `https://github.com/<owner>/<repo>/issues|pull/<number>` URL. Kind is set
|
|
19
|
+
* only when the URL path names `/pull/` or `/issues/`.
|
|
20
|
+
*/
|
|
21
|
+
export function parseGithubIssueRef(value) {
|
|
22
|
+
const input = value.trim();
|
|
23
|
+
const coordinate = /^([^/\s#]+)\/([^/\s#]+)#([1-9][0-9]*)$/.exec(input);
|
|
24
|
+
if (coordinate)
|
|
25
|
+
return githubIssueRef(coordinate[1], coordinate[2], coordinate[3]);
|
|
26
|
+
try {
|
|
27
|
+
const url = new URL(input);
|
|
28
|
+
if (url.protocol !== "https:" ||
|
|
29
|
+
url.hostname.toLowerCase() !== "github.com" ||
|
|
30
|
+
url.port ||
|
|
31
|
+
url.username ||
|
|
32
|
+
url.password ||
|
|
33
|
+
url.search ||
|
|
34
|
+
url.hash)
|
|
35
|
+
return undefined;
|
|
36
|
+
const match = /^\/([^/]+)\/([^/]+)\/(issues|pull)\/([1-9][0-9]*)\/?$/.exec(url.pathname);
|
|
37
|
+
if (!match)
|
|
38
|
+
return undefined;
|
|
39
|
+
return githubIssueRef(match[1], match[2], match[4], match[3] === "issues" ? "issue" : "pull");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function githubIssueRef(ownerRaw, repositoryRaw, numberRaw, kind) {
|
|
46
|
+
const repo = ownerRaw + "/" + repositoryRaw;
|
|
47
|
+
const number = Number(numberRaw);
|
|
48
|
+
if (!isValidRepo(repo) || !Number.isSafeInteger(number))
|
|
49
|
+
return undefined;
|
|
50
|
+
return {
|
|
51
|
+
repo: ownerRaw.toLowerCase() + "/" + repositoryRaw.toLowerCase(),
|
|
52
|
+
number,
|
|
53
|
+
...(kind ? { kind } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
16
56
|
/** Normalize a GitHub issue or pull-request coordinate for gallery linking. */
|
|
17
57
|
export function normalizeGithubCoordinate(value) {
|
|
18
58
|
const input = value.trim();
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { assertFetchableUploadUrl, fetchUploadSource, filenameFromUploadUrl, res
|
|
|
10
10
|
export { createUploadsClient, type UploadsClient, type PutOptions, type ProvenanceInput, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type GalleryItem, type Gallery, type GallerySummary, type GalleryListOptions, type GalleryListResult, type CreateGalleryOptions, type AddGalleryItemOptions, type DeleteGalleryOptions, type HealthResult, type UsageResult, type ReconcileResult, type PurgeExpiredResult, type PurgeExpiredResponse, type FindFilesOptions, type FindFilesItem, type FindFilesResult, type MetadataKeysResult, type MetadataValuesResult, type GetMetadataResult, type PatchMetadataOptions, type ResolveGhPrefixOptions, type ResolveGhPrefixResult, } from "./client.js";
|
|
11
11
|
export { buildCliProvenance } from "./provenance.js";
|
|
12
12
|
export { META_KEY_RE, META_VALUE_MAX, META_MAX_KEYS, META_MAX_TOTAL_BYTES, validateMetaEntry, parseMetaPair, parseMetaFlags, } from "./metadata.js";
|
|
13
|
-
export { ATTACHMENTS_MARKER, attachmentsCommentBody, GH_FALLBACK_AUTHOR_NOTE, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghBranchAttachmentKey, ghBranchKeyPrefix, ghMetadataForBranch, isValidRepo, parseRepoFromRemoteUrl, GH_PRIVATE_ROOT, ghPrivateKeyPrefix, ghPrivateAttachmentKey, ghPrivateBranchKeyPrefix, ghPrivateBranchAttachmentKey, ghAttachmentKeyForMode, ghBranchAttachmentKeyForMode, parseGhKey, parseGhPrivateKey, type AttachmentItem, type GhTarget, type GhTargetKind, type GhKeyMode, } from "./github.js";
|
|
13
|
+
export { ATTACHMENTS_MARKER, attachmentsCommentBody, GH_FALLBACK_AUTHOR_NOTE, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghBranchAttachmentKey, ghBranchKeyPrefix, ghMetadataForBranch, isValidRepo, parseGithubIssueRef, parseRepoFromRemoteUrl, GH_PRIVATE_ROOT, ghPrivateKeyPrefix, ghPrivateAttachmentKey, ghPrivateBranchKeyPrefix, ghPrivateBranchAttachmentKey, ghAttachmentKeyForMode, ghBranchAttachmentKeyForMode, parseGhKey, parseGhPrivateKey, type AttachmentItem, type GithubIssueRef, type GhTarget, type GhTargetKind, type GhKeyMode, } from "./github.js";
|
|
14
14
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, type OptimizeImageOptions, type OptimizeImageResult, type OptimizeOutputFormat, } from "./optimize.js";
|
|
15
15
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, type FrameFit, type FrameOptions, type FrameResult, } from "./frame.js";
|
|
16
16
|
export { execRunner, resolveRepo, upsertAttachmentsComment, type CommandRunner, } from "./github-gh.js";
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { assertFetchableUploadUrl, fetchUploadSource, filenameFromUploadUrl, res
|
|
|
10
10
|
export { createUploadsClient, } from "./client.js";
|
|
11
11
|
export { buildCliProvenance } from "./provenance.js";
|
|
12
12
|
export { META_KEY_RE, META_VALUE_MAX, META_MAX_KEYS, META_MAX_TOTAL_BYTES, validateMetaEntry, parseMetaPair, parseMetaFlags, } from "./metadata.js";
|
|
13
|
-
export { ATTACHMENTS_MARKER, attachmentsCommentBody, GH_FALLBACK_AUTHOR_NOTE, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghBranchAttachmentKey, ghBranchKeyPrefix, ghMetadataForBranch, isValidRepo, parseRepoFromRemoteUrl,
|
|
13
|
+
export { ATTACHMENTS_MARKER, attachmentsCommentBody, GH_FALLBACK_AUTHOR_NOTE, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghBranchAttachmentKey, ghBranchKeyPrefix, ghMetadataForBranch, isValidRepo, parseGithubIssueRef, parseRepoFromRemoteUrl,
|
|
14
14
|
// Private-repo randomized-prefix builders (issue #631) — needed by the
|
|
15
15
|
// hosted MCP (apps/mcp), which builds keys in-process rather than via
|
|
16
16
|
// the CLI's own commands.ts.
|
|
@@ -26,6 +26,7 @@ export declare const searchDocsResultSchema: JsonSchema;
|
|
|
26
26
|
export declare const promoteToolResultSchema: JsonSchema;
|
|
27
27
|
export declare const galleryResultSchema: JsonSchema;
|
|
28
28
|
export declare const galleryFindResultSchema: JsonSchema;
|
|
29
|
+
export declare const feedResultSchema: JsonSchema;
|
|
29
30
|
/** Hosted catalog — every tool must have an entry. */
|
|
30
31
|
export declare const hostedOutputSchemas: Record<string, JsonSchema>;
|
|
31
32
|
/** Shared-shape stdio tools. Hosted-only tools (`promote`, `repo_link_status`) omitted. */
|
|
@@ -337,6 +337,39 @@ export const galleryFindResultSchema = objectSchema({
|
|
|
337
337
|
galleries: { type: "array", items: galleryResultSchema },
|
|
338
338
|
nextCursor: nullableString,
|
|
339
339
|
});
|
|
340
|
+
const feedItemSchema = objectSchema({
|
|
341
|
+
id: { type: "string" },
|
|
342
|
+
objectKey: { type: "string" },
|
|
343
|
+
filename: { type: "string" },
|
|
344
|
+
status: { type: "string" },
|
|
345
|
+
url: nullableString,
|
|
346
|
+
embedUrl: nullableString,
|
|
347
|
+
pageUrl: { type: "string" },
|
|
348
|
+
contentType: nullableString,
|
|
349
|
+
size: { type: ["number", "null"] },
|
|
350
|
+
uploaded: nullableString,
|
|
351
|
+
modified: nullableString,
|
|
352
|
+
path: nullableString,
|
|
353
|
+
state: nullableString,
|
|
354
|
+
posterUrl: { type: "string" },
|
|
355
|
+
videoDimensions: {
|
|
356
|
+
type: "object",
|
|
357
|
+
additionalProperties: true,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
export const feedResultSchema = objectSchema({
|
|
361
|
+
id: { type: "string" },
|
|
362
|
+
url: { type: "string" },
|
|
363
|
+
workspace: { type: "string" },
|
|
364
|
+
repo: { type: "string" },
|
|
365
|
+
path: nullableString,
|
|
366
|
+
number: { type: ["number", "null"] },
|
|
367
|
+
kind: nullableString,
|
|
368
|
+
title: { type: "string" },
|
|
369
|
+
createdAt: { type: "string" },
|
|
370
|
+
updatedAt: { type: "string" },
|
|
371
|
+
items: { type: "array", items: feedItemSchema },
|
|
372
|
+
});
|
|
340
373
|
/** Hosted catalog — every tool must have an entry. */
|
|
341
374
|
export const hostedOutputSchemas = {
|
|
342
375
|
gallery_create: galleryResultSchema,
|
|
@@ -344,6 +377,8 @@ export const hostedOutputSchemas = {
|
|
|
344
377
|
gallery_add: galleryItemSchema,
|
|
345
378
|
gallery_link: galleryReferenceSchema,
|
|
346
379
|
gallery_find_by_reference: galleryFindResultSchema,
|
|
380
|
+
feed_create: feedResultSchema,
|
|
381
|
+
feed_get: feedResultSchema,
|
|
347
382
|
put: putResultSchema,
|
|
348
383
|
list: listResultSchema,
|
|
349
384
|
delete: deleteResultSchema,
|
|
@@ -368,6 +403,8 @@ export const stdioOutputSchemas = {
|
|
|
368
403
|
gallery_add: galleryItemSchema,
|
|
369
404
|
gallery_link: galleryReferenceSchema,
|
|
370
405
|
gallery_find_by_reference: galleryFindResultSchema,
|
|
406
|
+
feed_create: feedResultSchema,
|
|
407
|
+
feed_get: feedResultSchema,
|
|
371
408
|
put: putResultSchema,
|
|
372
409
|
list: listResultSchema,
|
|
373
410
|
delete: deleteResultSchema,
|
package/dist/mcp/tools.js
CHANGED
|
@@ -4,7 +4,7 @@ import { resolveFrameId } from "../frame.js";
|
|
|
4
4
|
import { resolveConfig, resolvePutDefaults, } from "../config.js";
|
|
5
5
|
import { resolvePutPrefix } from "../destinations.js";
|
|
6
6
|
import { fetchUploadSource, resolveUploadFilename } from "../fetch-upload-source.js";
|
|
7
|
-
import { ghKeyPrefix, ghPrivateKeyPrefix } from "../github.js";
|
|
7
|
+
import { ghKeyPrefix, ghPrivateKeyPrefix, parseGithubIssueRef } from "../github.js";
|
|
8
8
|
import { safeCaptureFacts } from "../capture-facts.js";
|
|
9
9
|
import { deriveRepoSlugFromGit } from "../keys.js";
|
|
10
10
|
import { validateMetaMap } from "../metadata.js";
|
|
@@ -75,6 +75,12 @@ function galleryId(args) {
|
|
|
75
75
|
usage("galleryId is required");
|
|
76
76
|
return id;
|
|
77
77
|
}
|
|
78
|
+
function feedId(args) {
|
|
79
|
+
const id = optString(args, "feedId");
|
|
80
|
+
if (!id)
|
|
81
|
+
usage("feedId is required");
|
|
82
|
+
return id;
|
|
83
|
+
}
|
|
78
84
|
function galleryReference(args) {
|
|
79
85
|
const provider = optString(args, "provider");
|
|
80
86
|
const coordinate = optString(args, "coordinate");
|
|
@@ -86,6 +92,33 @@ function galleryReference(args) {
|
|
|
86
92
|
usage("provider must be github");
|
|
87
93
|
return { provider, coordinate };
|
|
88
94
|
}
|
|
95
|
+
function feedCreateOptions(args) {
|
|
96
|
+
const github = optString(args, "github");
|
|
97
|
+
const repo = optString(args, "repo");
|
|
98
|
+
const path = optString(args, "path");
|
|
99
|
+
const pr = optPosInt(args, "pr");
|
|
100
|
+
const issue = optPosInt(args, "issue");
|
|
101
|
+
if (pr != null && issue != null)
|
|
102
|
+
usage("pr and issue are mutually exclusive");
|
|
103
|
+
if (github && (pr != null || issue != null))
|
|
104
|
+
usage("github cannot be combined with pr or issue");
|
|
105
|
+
if (github) {
|
|
106
|
+
const ref = parseGithubIssueRef(github);
|
|
107
|
+
if (!ref)
|
|
108
|
+
usage("github must be owner/repo#number or a GitHub issue/PR URL");
|
|
109
|
+
if (repo && repo.trim().toLowerCase() !== ref.repo)
|
|
110
|
+
usage("repo does not match github");
|
|
111
|
+
return { repo: ref.repo, path, number: ref.number, kind: ref.kind };
|
|
112
|
+
}
|
|
113
|
+
if (!repo)
|
|
114
|
+
usage("repo is required");
|
|
115
|
+
return {
|
|
116
|
+
repo,
|
|
117
|
+
path,
|
|
118
|
+
...(pr != null ? { number: pr, kind: "pull" } : {}),
|
|
119
|
+
...(issue != null ? { number: issue, kind: "issue" } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
89
122
|
const workspaceProp = {
|
|
90
123
|
type: "string",
|
|
91
124
|
description: "Override the workspace for this call (like the CLI's --workspace flag).",
|
|
@@ -289,6 +322,66 @@ export function createUploadsMcpTools(opts) {
|
|
|
289
322
|
});
|
|
290
323
|
},
|
|
291
324
|
},
|
|
325
|
+
{
|
|
326
|
+
name: "feed_create",
|
|
327
|
+
title: "Create change feed",
|
|
328
|
+
annotations: mcpWritePublic,
|
|
329
|
+
securitySchemes: mcpOAuthWrite,
|
|
330
|
+
description: "Create a public newest-first screenshot feed (same product as CLI `uploads feed create`). Pass `repo` alone for the whole GitHub repo. Pass `pr` plus `repo` — or `github` as owner/repo#123 / a GitHub PR URL — to scope that same feed to one pull request. `issue` does the same for an issue. Optional `path` filters by page-path metadata. Creating the same scope again returns the existing URL. This is a live query, not a curated gallery. Anyone who knows the URL can view it.",
|
|
331
|
+
inputSchema: {
|
|
332
|
+
type: "object",
|
|
333
|
+
properties: {
|
|
334
|
+
repo: {
|
|
335
|
+
type: "string",
|
|
336
|
+
description: "GitHub owner/repo, for example acme/app. Required unless github is set.",
|
|
337
|
+
},
|
|
338
|
+
pr: {
|
|
339
|
+
type: "integer",
|
|
340
|
+
minimum: 1,
|
|
341
|
+
description: "Scope the feed to this pull request. Same product as a repo feed — one extra filter. Mutually exclusive with issue and github.",
|
|
342
|
+
},
|
|
343
|
+
issue: {
|
|
344
|
+
type: "integer",
|
|
345
|
+
minimum: 1,
|
|
346
|
+
description: "Scope the feed to this issue. Mutually exclusive with pr and github.",
|
|
347
|
+
},
|
|
348
|
+
github: {
|
|
349
|
+
type: "string",
|
|
350
|
+
description: "Scope via owner/repo#number or a GitHub issue/PR URL. Supplies repo when omitted. Mutually exclusive with pr and issue.",
|
|
351
|
+
},
|
|
352
|
+
path: {
|
|
353
|
+
type: "string",
|
|
354
|
+
description: "Optional exact page-path metadata filter, for example /settings.",
|
|
355
|
+
},
|
|
356
|
+
workspace: workspaceProp,
|
|
357
|
+
},
|
|
358
|
+
additionalProperties: false,
|
|
359
|
+
},
|
|
360
|
+
async handler(args) {
|
|
361
|
+
const { client } = await clientFor(args);
|
|
362
|
+
return client.createFeed(feedCreateOptions(args));
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: "feed_get",
|
|
367
|
+
title: "Get change feed",
|
|
368
|
+
annotations: mcpRead,
|
|
369
|
+
securitySchemes: mcpOAuthRead,
|
|
370
|
+
description: "Get a workspace-owned change feed by ID, including its current newest-first screenshots and the canonical public /c/<id> URL. Each item includes pageUrl (`/c/<id>/<item>`) for the pager. The feed may be repo-wide or scoped to one pull request / issue (see `number` and `kind` on the result). Anyone with the URL can view the media.",
|
|
371
|
+
inputSchema: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
feedId: { type: "string", description: "Opaque feed ID." },
|
|
375
|
+
workspace: workspaceProp,
|
|
376
|
+
},
|
|
377
|
+
required: ["feedId"],
|
|
378
|
+
additionalProperties: false,
|
|
379
|
+
},
|
|
380
|
+
async handler(args) {
|
|
381
|
+
const { client } = await clientFor(args);
|
|
382
|
+
return client.getFeed(feedId(args));
|
|
383
|
+
},
|
|
384
|
+
},
|
|
292
385
|
{
|
|
293
386
|
name: "put",
|
|
294
387
|
title: "Upload file",
|
package/dist/update-check.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ export interface UpdateCache {
|
|
|
6
6
|
}
|
|
7
7
|
export interface UpdateCheckOptions {
|
|
8
8
|
quiet?: boolean;
|
|
9
|
-
/**
|
|
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;
|
package/dist/update-check.js
CHANGED
|
@@ -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")) {
|