@buildinternet/uploads 0.53.0 → 0.55.0

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 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,9 +45,9 @@ 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
- `logout`, `invite`, `admin`, `config`, `telemetry`, `report`, `doctor`, `health`, `changelog`, `mcp`,
50
+ `logout`, `invite`, `admin`, `config`, `telemetry`, `report`, `doctor`, `health`, `changelog`, `docs`, `mcp`,
49
51
  `completion`.
50
52
 
51
53
  **Help:** bare `uploads` / `uploads help` / `--help` shows essentials; use
@@ -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: `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`, `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).
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 `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.
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
 
@@ -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)",
@@ -237,6 +247,11 @@ export const ROOT_COMMANDS = [
237
247
  name: "changelog",
238
248
  summary: "Show recent product updates (and a link to the full changelog)",
239
249
  },
250
+ {
251
+ name: "docs",
252
+ summary: "Search public uploads.sh docs (or fetch one page as markdown)",
253
+ usage: "docs [query]",
254
+ },
240
255
  {
241
256
  name: "login",
242
257
  summary: "Sign in via browser (or an enrollment code) and save credentials",
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";
@@ -17,6 +18,7 @@ import { runInstall } from "./commands/install.js";
17
18
  import { runHook } from "./commands/hook.js";
18
19
  import { runUpdate } from "./commands/update.js";
19
20
  import { runChangelog } from "./commands/changelog.js";
21
+ import { runDocs } from "./commands/docs.js";
20
22
  import { runCompletion } from "./commands/completion.js";
21
23
  import { runLogout, runWhoami } from "./commands/session.js";
22
24
  import { runTelemetry } from "./commands/telemetry.js";
@@ -243,6 +245,10 @@ export async function runCli(argv) {
243
245
  // Public feed — no token, independent of the API origin.
244
246
  code = await runChangelog(cmdArgs, { json }, showHelp);
245
247
  break;
248
+ case "docs":
249
+ // Public catalog — no token, independent of the API origin.
250
+ code = await runDocs(cmdArgs, { json }, showHelp);
251
+ break;
246
252
  case "config":
247
253
  code = await runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
248
254
  break;
@@ -304,6 +310,7 @@ export async function runCli(argv) {
304
310
  case "staged":
305
311
  case "screenshot":
306
312
  case "gallery":
313
+ case "feed":
307
314
  case "list":
308
315
  case "find":
309
316
  case "meta":
@@ -332,6 +339,9 @@ export async function runCli(argv) {
332
339
  case "gallery":
333
340
  code = await runGallery(ctx, cmdArgs, showHelp);
334
341
  break;
342
+ case "feed":
343
+ code = await runFeed(ctx, cmdArgs, showHelp);
344
+ break;
335
345
  case "comment":
336
346
  code = await runComment(ctx, cmdArgs, showHelp);
337
347
  break;
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,8 @@
1
+ import { type SearchDocsOptions } from "../docs.js";
2
+ export interface RunDocsOptions {
3
+ json?: boolean;
4
+ fetch?: SearchDocsOptions["fetchImpl"];
5
+ url?: string;
6
+ catalog?: SearchDocsOptions["catalog"];
7
+ }
8
+ export declare function runDocs(args: string[], opts?: RunDocsOptions, help?: boolean): Promise<number>;
@@ -0,0 +1,67 @@
1
+ import { flagBool, flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
2
+ import { UploadsError } from "../errors.js";
3
+ import { DEFAULT_DOCS_LIMIT, MAX_DOCS_LIMIT, formatDocsHuman, searchDocs, } from "../docs.js";
4
+ import { writeCommandHelp } from "../cli-style.js";
5
+ import { writeJson, writeStdout } from "../io.js";
6
+ const DOCS_HELP = `uploads docs — search public uploads.sh documentation
7
+
8
+ Lists the docs catalog, searches by topic, or fetches one page as markdown.
9
+
10
+ Usage:
11
+ uploads docs [query…] [options]
12
+
13
+ Options:
14
+ --page <slug> Fetch the full markdown of one page (slug, path, or URL)
15
+ --limit <n> Number of search hits to show (default ${DEFAULT_DOCS_LIMIT}, max ${MAX_DOCS_LIMIT})
16
+ --json JSON on stdout (also accepts global --json)
17
+
18
+ Examples:
19
+ uploads docs
20
+ uploads docs stage before a PR
21
+ uploads docs attach
22
+ uploads docs --page agents
23
+ uploads docs --json "github app"
24
+ `;
25
+ export async function runDocs(args, opts = {}, help = false) {
26
+ const parsed = parseCommandArgs(args);
27
+ if (help || parsed.help) {
28
+ writeCommandHelp(DOCS_HELP);
29
+ return 0;
30
+ }
31
+ const json = Boolean(opts.json) || flagBool(parsed.flags, "--json");
32
+ const page = flagString(parsed.flags, "--page");
33
+ const limit = flagInt(parsed.flags, "--limit", "--limit");
34
+ if (limit !== undefined && limit > MAX_DOCS_LIMIT) {
35
+ throw new UsageError(`--limit must be ${MAX_DOCS_LIMIT} or less (got ${limit})`, {
36
+ example: `uploads docs --limit ${MAX_DOCS_LIMIT}`,
37
+ });
38
+ }
39
+ const query = parsed.positionals.join(" ").trim();
40
+ if (page && query) {
41
+ throw new UsageError("pass a search query or --page, not both", {
42
+ example: "uploads docs --page attach",
43
+ });
44
+ }
45
+ let doc;
46
+ try {
47
+ doc = await searchDocs({
48
+ query: query || undefined,
49
+ page: page || undefined,
50
+ limit,
51
+ fetchImpl: opts.fetch,
52
+ url: opts.url,
53
+ catalog: opts.catalog,
54
+ });
55
+ }
56
+ catch (err) {
57
+ if (err instanceof UploadsError && err.code === "USAGE") {
58
+ throw new UsageError(err.message, { example: "uploads docs --page attach" });
59
+ }
60
+ throw err;
61
+ }
62
+ if (json)
63
+ await writeJson(doc);
64
+ else
65
+ await writeStdout(formatDocsHuman(doc));
66
+ return 0;
67
+ }
@@ -0,0 +1,2 @@
1
+ import type { CliContext } from "../commands.js";
2
+ export declare function runFeed(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
@@ -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
+ }
@@ -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
- /** Canonical `/f/` file-page URL (server-computed). Preferred click-through target; falls back to `url`. */
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/docs.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ export declare const DOCS_HUB_URL = "https://uploads.sh/docs";
2
+ export declare const DOCS_JSON_URL = "https://uploads.sh/docs.json";
3
+ export declare const DEFAULT_DOCS_LIMIT = 5;
4
+ export declare const MAX_DOCS_LIMIT = 50;
5
+ export declare const DOCS_SNIPPET_MAX_CHARS = 1500;
6
+ export declare const DOCS_PAGE_MAX_CHARS = 50000;
7
+ export type DocsCatalogPage = {
8
+ page: string;
9
+ title: string;
10
+ url: string;
11
+ summary: string;
12
+ aliases: string[];
13
+ };
14
+ export type DocsCatalog = {
15
+ url: string;
16
+ pages: DocsCatalogPage[];
17
+ };
18
+ export type DocsSearchHit = {
19
+ title: string;
20
+ url: string;
21
+ page: string;
22
+ snippet: string;
23
+ body?: string;
24
+ truncated?: boolean;
25
+ };
26
+ export type DocsSearchDocument = {
27
+ url: string;
28
+ query?: string;
29
+ results: DocsSearchHit[];
30
+ total: number;
31
+ };
32
+ export type SearchDocsOptions = {
33
+ query?: string;
34
+ page?: string;
35
+ limit?: number;
36
+ url?: string;
37
+ fetchImpl?: typeof fetch;
38
+ timeoutMs?: number;
39
+ userAgent?: string;
40
+ /** Skip the network catalog fetch (tests). */
41
+ catalog?: DocsCatalog;
42
+ };
43
+ /** Built-in catalog used when /docs.json is unreachable. */
44
+ export declare const FALLBACK_DOCS_CATALOG: DocsCatalog;
45
+ export declare function parseDocsCatalog(raw: unknown): DocsCatalog;
46
+ export declare function resolveDocsPage(catalog: DocsCatalog, raw: string): DocsCatalogPage | undefined;
47
+ export declare function rankDocsPages(catalog: DocsCatalog, query: string): DocsCatalogPage[];
48
+ export declare function truncateDocsSnippet(text: string, maxChars?: number): string;
49
+ /** Human terminal output for catalog/search hits. A fetched page prints its body. */
50
+ export declare function formatDocsHuman(doc: DocsSearchDocument): string;
51
+ export declare function loadDocsCatalog(opts?: SearchDocsOptions): Promise<DocsCatalog>;
52
+ export declare function searchDocs(opts?: SearchDocsOptions): Promise<DocsSearchDocument>;