@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 +26 -4
- package/dist/cli-catalog.js +15 -0
- package/dist/cli-help.js +1 -0
- package/dist/cli.js +10 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.js +24 -0
- package/dist/commands/docs.d.ts +8 -0
- package/dist/commands/docs.js +67 -0
- package/dist/commands/feed.d.ts +2 -0
- package/dist/commands/feed.js +133 -0
- package/dist/comment-render.generated.d.ts +1 -1
- package/dist/docs.d.ts +52 -0
- package/dist/docs.js +439 -0
- package/dist/github.d.ts +12 -0
- package/dist/github.js +40 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/mcp/docs-tool.d.ts +4 -0
- package/dist/mcp/docs-tool.js +35 -0
- package/dist/mcp/output-schemas.d.ts +2 -0
- package/dist/mcp/output-schemas.js +53 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/tools.d.ts +2 -1
- package/dist/mcp/tools.js +108 -1
- package/package.json +1 -1
package/dist/docs.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch and search the public uploads.sh docs catalog for `uploads docs`
|
|
3
|
+
* and the MCP `search_docs` tool.
|
|
4
|
+
*/
|
|
5
|
+
import { UploadsError } from "./errors.js";
|
|
6
|
+
import { packageVersion } from "./package-version.js";
|
|
7
|
+
export const DOCS_HUB_URL = "https://uploads.sh/docs";
|
|
8
|
+
export const DOCS_JSON_URL = "https://uploads.sh/docs.json";
|
|
9
|
+
export const DEFAULT_DOCS_LIMIT = 5;
|
|
10
|
+
export const MAX_DOCS_LIMIT = 50;
|
|
11
|
+
export const DOCS_SNIPPET_MAX_CHARS = 1500;
|
|
12
|
+
export const DOCS_PAGE_MAX_CHARS = 50_000;
|
|
13
|
+
const FETCH_TIMEOUT_MS = 8000;
|
|
14
|
+
const SITE_ORIGIN = "https://uploads.sh";
|
|
15
|
+
const STOP_WORDS = new Set([
|
|
16
|
+
"a",
|
|
17
|
+
"an",
|
|
18
|
+
"and",
|
|
19
|
+
"at",
|
|
20
|
+
"do",
|
|
21
|
+
"for",
|
|
22
|
+
"how",
|
|
23
|
+
"i",
|
|
24
|
+
"in",
|
|
25
|
+
"is",
|
|
26
|
+
"my",
|
|
27
|
+
"of",
|
|
28
|
+
"on",
|
|
29
|
+
"or",
|
|
30
|
+
"the",
|
|
31
|
+
"to",
|
|
32
|
+
"we",
|
|
33
|
+
"your",
|
|
34
|
+
]);
|
|
35
|
+
/** Built-in catalog used when /docs.json is unreachable. */
|
|
36
|
+
export const FALLBACK_DOCS_CATALOG = {
|
|
37
|
+
url: DOCS_HUB_URL,
|
|
38
|
+
pages: [
|
|
39
|
+
{
|
|
40
|
+
page: "docs",
|
|
41
|
+
title: "Docs",
|
|
42
|
+
url: "https://uploads.sh/docs",
|
|
43
|
+
summary: "Install the uploads CLI, attach screenshots and video to GitHub PRs, issues, and code reviews, and set up your agent.",
|
|
44
|
+
aliases: ["overview", "hub", "install"],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
page: "attach-pull-request-images",
|
|
48
|
+
title: "Attach & share",
|
|
49
|
+
url: "https://uploads.sh/docs/attach-pull-request-images",
|
|
50
|
+
summary: "Attach screenshots and video to GitHub PRs and issues, get a public URL for any accepted file, capture a screenshot, and bake callouts or redactions onto it with the uploads CLI.",
|
|
51
|
+
aliases: [
|
|
52
|
+
"attach",
|
|
53
|
+
"stage",
|
|
54
|
+
"staging",
|
|
55
|
+
"screenshot",
|
|
56
|
+
"annotate",
|
|
57
|
+
"pr",
|
|
58
|
+
"issue",
|
|
59
|
+
"before",
|
|
60
|
+
"after",
|
|
61
|
+
"put",
|
|
62
|
+
"share",
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
page: "galleries",
|
|
67
|
+
title: "Galleries",
|
|
68
|
+
url: "https://uploads.sh/docs/galleries",
|
|
69
|
+
summary: "Create an ordered set of media behind one public link at /g/<id>, add files with uploads put --gallery, and link the gallery to a PR or issue.",
|
|
70
|
+
aliases: ["gallery", "collection"],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
page: "github-app",
|
|
74
|
+
title: "GitHub App",
|
|
75
|
+
url: "https://uploads.sh/docs/github-app",
|
|
76
|
+
summary: "Install the uploads-sh GitHub App so attachment comments post as the bot, private-repo PR and issue titles show on file pages, and titles stay current.",
|
|
77
|
+
aliases: ["bot", "webhook", "promote", "app", "ingest"],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
page: "comment-config",
|
|
81
|
+
title: "Comment config",
|
|
82
|
+
url: "https://uploads.sh/docs/comment-config",
|
|
83
|
+
summary: "Reference for .uploads.yml — a repo-committed file that controls image width, inline-image caps, caption metadata, and an optional note on the managed PR/issue comment.",
|
|
84
|
+
aliases: ["uploads.yml", "comment", "yaml", "yml", "width"],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
page: "agents",
|
|
88
|
+
title: "Set up your agent",
|
|
89
|
+
url: "https://uploads.sh/docs/agents",
|
|
90
|
+
summary: "Install the uploads agent skill and MCP server so your coding agent can attach screenshots to GitHub on its own.",
|
|
91
|
+
aliases: ["mcp", "skill", "plugin", "claude", "codex"],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
page: "reference",
|
|
95
|
+
title: "Reference",
|
|
96
|
+
url: "https://uploads.sh/docs/reference",
|
|
97
|
+
summary: "Manage uploaded files with the uploads CLI (list, delete, usage), plus what to know about visibility, access, and stability.",
|
|
98
|
+
aliases: ["list", "delete", "usage", "visibility"],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
page: "limits",
|
|
102
|
+
title: "Plans & limits",
|
|
103
|
+
url: "https://uploads.sh/docs/limits",
|
|
104
|
+
summary: "Storage, file size, and member limits for uploads.sh free and pro workspaces, and how they compare with GitHub's own attachment caps.",
|
|
105
|
+
aliases: ["plan", "plans", "pricing", "quota", "storage", "pro", "free"],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
page: "byo-bucket",
|
|
109
|
+
title: "Bring your own bucket",
|
|
110
|
+
url: "https://uploads.sh/docs/byo-bucket",
|
|
111
|
+
summary: "Point a workspace at your own Cloudflare R2 or S3-compatible bucket instead of hosted storage on storage.uploads.sh.",
|
|
112
|
+
aliases: ["r2", "s3", "byo", "bucket", "bring-your-own"],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
page: "github-screenshots",
|
|
116
|
+
title: "How to get agents to upload screenshots & video to GitHub",
|
|
117
|
+
url: "https://uploads.sh/github-screenshots",
|
|
118
|
+
summary: "One command for Claude Code, CI jobs, and scripts that captures, hosts, and posts screenshots and video to PRs and issues, before or after the PR exists.",
|
|
119
|
+
aliases: ["walkthrough", "agent-guide", "how-to"],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
page: "changelog",
|
|
123
|
+
title: "Changelog",
|
|
124
|
+
url: "https://uploads.sh/changelog",
|
|
125
|
+
summary: "Platform updates and CLI releases, newest first.",
|
|
126
|
+
aliases: ["updates", "release", "whats-new"],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
page: "auth",
|
|
130
|
+
title: "Auth for agents",
|
|
131
|
+
url: "https://uploads.sh/auth.md",
|
|
132
|
+
summary: "Device sign-in, workspace bearer tokens, and hosted MCP OAuth.",
|
|
133
|
+
aliases: ["login", "token", "oauth", "bearer"],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
function asString(value) {
|
|
138
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
139
|
+
}
|
|
140
|
+
function asHttpUrl(value) {
|
|
141
|
+
const raw = asString(value);
|
|
142
|
+
if (!raw)
|
|
143
|
+
return undefined;
|
|
144
|
+
try {
|
|
145
|
+
const parsed = new URL(raw);
|
|
146
|
+
if (parsed.protocol === "https:" || parsed.protocol === "http:")
|
|
147
|
+
return raw;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
function asStringArray(value) {
|
|
155
|
+
if (!Array.isArray(value))
|
|
156
|
+
return [];
|
|
157
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
158
|
+
}
|
|
159
|
+
function parsePage(raw) {
|
|
160
|
+
if (!raw || typeof raw !== "object")
|
|
161
|
+
return undefined;
|
|
162
|
+
const rec = raw;
|
|
163
|
+
const page = asString(rec.page);
|
|
164
|
+
const title = asString(rec.title);
|
|
165
|
+
const url = asHttpUrl(rec.url);
|
|
166
|
+
const summary = asString(rec.summary);
|
|
167
|
+
if (!page || !title || !url || !summary)
|
|
168
|
+
return undefined;
|
|
169
|
+
return { page, title, url, summary, aliases: asStringArray(rec.aliases) };
|
|
170
|
+
}
|
|
171
|
+
export function parseDocsCatalog(raw) {
|
|
172
|
+
if (!raw || typeof raw !== "object") {
|
|
173
|
+
throw new UploadsError("docs catalog was not a JSON object", "API_ERROR");
|
|
174
|
+
}
|
|
175
|
+
const rec = raw;
|
|
176
|
+
if (!Array.isArray(rec.pages)) {
|
|
177
|
+
throw new UploadsError("docs catalog is missing a pages array", "API_ERROR");
|
|
178
|
+
}
|
|
179
|
+
const pages = rec.pages.map(parsePage).filter((p) => p !== undefined);
|
|
180
|
+
if (pages.length === 0) {
|
|
181
|
+
throw new UploadsError("docs catalog had no usable pages", "API_ERROR");
|
|
182
|
+
}
|
|
183
|
+
return { url: asHttpUrl(rec.url) ?? DOCS_HUB_URL, pages };
|
|
184
|
+
}
|
|
185
|
+
function clampDocsLimit(limit, fallback = DEFAULT_DOCS_LIMIT) {
|
|
186
|
+
const n = limit ?? fallback;
|
|
187
|
+
if (n > MAX_DOCS_LIMIT)
|
|
188
|
+
return MAX_DOCS_LIMIT;
|
|
189
|
+
if (n < 1)
|
|
190
|
+
return fallback;
|
|
191
|
+
return n;
|
|
192
|
+
}
|
|
193
|
+
function tokenizeDocsQuery(query) {
|
|
194
|
+
return query
|
|
195
|
+
.toLowerCase()
|
|
196
|
+
.split(/[^a-z0-9]+/)
|
|
197
|
+
.filter((token) => token.length > 1 && !STOP_WORDS.has(token));
|
|
198
|
+
}
|
|
199
|
+
function normalizeDocsKey(value) {
|
|
200
|
+
return value
|
|
201
|
+
.trim()
|
|
202
|
+
.toLowerCase()
|
|
203
|
+
.replace(/^https:\/\/uploads\.sh/i, "")
|
|
204
|
+
.replace(/^\/+/, "")
|
|
205
|
+
.replace(/\/+$/, "");
|
|
206
|
+
}
|
|
207
|
+
function pageKeys(page) {
|
|
208
|
+
const keys = [page.page, ...page.aliases];
|
|
209
|
+
try {
|
|
210
|
+
const path = new URL(page.url).pathname.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
211
|
+
keys.push(path);
|
|
212
|
+
const leaf = path.split("/").pop();
|
|
213
|
+
if (leaf)
|
|
214
|
+
keys.push(leaf);
|
|
215
|
+
if (path.startsWith("docs/"))
|
|
216
|
+
keys.push(path.slice("docs/".length));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Catalog URLs are validated on parse; ignore a malformed fallback entry.
|
|
220
|
+
}
|
|
221
|
+
return keys.map(normalizeDocsKey).filter(Boolean);
|
|
222
|
+
}
|
|
223
|
+
export function resolveDocsPage(catalog, raw) {
|
|
224
|
+
const query = normalizeDocsKey(raw);
|
|
225
|
+
if (!query)
|
|
226
|
+
return undefined;
|
|
227
|
+
const stripped = query.startsWith("docs/") ? query.slice("docs/".length) : query;
|
|
228
|
+
for (const page of catalog.pages) {
|
|
229
|
+
const keys = pageKeys(page);
|
|
230
|
+
if (keys.includes(query) || keys.includes(stripped))
|
|
231
|
+
return page;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
function searchableText(page) {
|
|
236
|
+
return `${page.page} ${page.title} ${page.summary} ${page.aliases.join(" ")}`.toLowerCase();
|
|
237
|
+
}
|
|
238
|
+
function scoreDocsPage(page, tokens) {
|
|
239
|
+
if (tokens.length === 0)
|
|
240
|
+
return 0;
|
|
241
|
+
const text = searchableText(page);
|
|
242
|
+
let matched = 0;
|
|
243
|
+
for (const token of tokens) {
|
|
244
|
+
if (text.includes(token))
|
|
245
|
+
matched += 1;
|
|
246
|
+
}
|
|
247
|
+
if (matched === 0)
|
|
248
|
+
return 0;
|
|
249
|
+
return matched * 10 + (matched === tokens.length ? 40 : 0);
|
|
250
|
+
}
|
|
251
|
+
export function rankDocsPages(catalog, query) {
|
|
252
|
+
const tokens = tokenizeDocsQuery(query);
|
|
253
|
+
const needle = query.trim().toLowerCase();
|
|
254
|
+
const scored = catalog.pages
|
|
255
|
+
.map((page) => {
|
|
256
|
+
const tokenScore = tokens.length > 0 ? scoreDocsPage(page, tokens) : 0;
|
|
257
|
+
const substringScore = tokenScore === 0 && needle.length > 0 && searchableText(page).includes(needle) ? 5 : 0;
|
|
258
|
+
return { page, score: tokenScore + substringScore };
|
|
259
|
+
})
|
|
260
|
+
.filter((row) => row.score > 0)
|
|
261
|
+
.sort((a, b) => b.score - a.score || a.page.title.localeCompare(b.page.title));
|
|
262
|
+
return scored.map((row) => row.page);
|
|
263
|
+
}
|
|
264
|
+
export function truncateDocsSnippet(text, maxChars = DOCS_SNIPPET_MAX_CHARS) {
|
|
265
|
+
const trimmed = text.trim();
|
|
266
|
+
if (trimmed.length <= maxChars)
|
|
267
|
+
return trimmed;
|
|
268
|
+
const cut = trimmed.slice(0, maxChars - 1);
|
|
269
|
+
const atSpace = cut.lastIndexOf(" ");
|
|
270
|
+
return `${atSpace > 40 ? cut.slice(0, atSpace) : cut}…`;
|
|
271
|
+
}
|
|
272
|
+
function hitFromPage(page, body) {
|
|
273
|
+
if (body === undefined) {
|
|
274
|
+
return {
|
|
275
|
+
title: page.title,
|
|
276
|
+
url: page.url,
|
|
277
|
+
page: page.page,
|
|
278
|
+
snippet: page.summary,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const clipped = body.length > DOCS_PAGE_MAX_CHARS ? body.slice(0, DOCS_PAGE_MAX_CHARS) : body;
|
|
282
|
+
const snippet = truncateDocsSnippet(clipped);
|
|
283
|
+
return {
|
|
284
|
+
title: page.title,
|
|
285
|
+
url: page.url,
|
|
286
|
+
page: page.page,
|
|
287
|
+
snippet,
|
|
288
|
+
body: clipped,
|
|
289
|
+
...(snippet.length < clipped.trim().length || body.length > DOCS_PAGE_MAX_CHARS
|
|
290
|
+
? { truncated: true }
|
|
291
|
+
: {}),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function indent(text, prefix = " ") {
|
|
295
|
+
return text
|
|
296
|
+
.split("\n")
|
|
297
|
+
.map((line) => (line.length === 0 ? prefix.trimEnd() : `${prefix}${line}`))
|
|
298
|
+
.join("\n");
|
|
299
|
+
}
|
|
300
|
+
/** Human terminal output for catalog/search hits. A fetched page prints its body. */
|
|
301
|
+
export function formatDocsHuman(doc) {
|
|
302
|
+
const lines = [];
|
|
303
|
+
const only = doc.results.length === 1 ? doc.results[0] : undefined;
|
|
304
|
+
if (only?.body) {
|
|
305
|
+
lines.push(only.title);
|
|
306
|
+
lines.push(only.url);
|
|
307
|
+
lines.push("");
|
|
308
|
+
lines.push(only.body.trimEnd());
|
|
309
|
+
return `${lines.join("\n")}\n`;
|
|
310
|
+
}
|
|
311
|
+
lines.push("Docs", "");
|
|
312
|
+
if (doc.results.length === 0) {
|
|
313
|
+
lines.push("No matching docs.");
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
for (const hit of doc.results) {
|
|
317
|
+
lines.push(hit.title);
|
|
318
|
+
lines.push(hit.url);
|
|
319
|
+
lines.push("");
|
|
320
|
+
lines.push(indent(hit.snippet));
|
|
321
|
+
lines.push("");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
lines.push(`See all docs: ${doc.url}`);
|
|
325
|
+
return `${lines.join("\n")}\n`;
|
|
326
|
+
}
|
|
327
|
+
function isUploadsDocsUrl(url) {
|
|
328
|
+
try {
|
|
329
|
+
const parsed = new URL(url);
|
|
330
|
+
return parsed.origin === SITE_ORIGIN;
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async function fetchUrl(fetchImpl, url, headers, timeoutMs) {
|
|
337
|
+
const controller = new AbortController();
|
|
338
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
339
|
+
try {
|
|
340
|
+
return await fetchImpl(url, { signal: controller.signal, headers });
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
const aborted = err instanceof Error && err.name === "AbortError";
|
|
344
|
+
throw new UploadsError(aborted
|
|
345
|
+
? `timed out fetching docs (${timeoutMs}ms)`
|
|
346
|
+
: `couldn't reach docs (${err instanceof Error ? err.message : String(err)})`, "NETWORK");
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
export async function loadDocsCatalog(opts = {}) {
|
|
353
|
+
if (opts.catalog)
|
|
354
|
+
return opts.catalog;
|
|
355
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
356
|
+
const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
357
|
+
const userAgent = opts.userAgent ?? `uploads/${packageVersion()}`;
|
|
358
|
+
const jsonUrl = opts.url ?? DOCS_JSON_URL;
|
|
359
|
+
try {
|
|
360
|
+
const res = await fetchUrl(fetchImpl, jsonUrl, { "user-agent": userAgent, accept: "application/json" }, timeoutMs);
|
|
361
|
+
if (res.ok) {
|
|
362
|
+
try {
|
|
363
|
+
const payload = await res.json();
|
|
364
|
+
return parseDocsCatalog(payload);
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
if (opts.url) {
|
|
368
|
+
throw err instanceof UploadsError
|
|
369
|
+
? err
|
|
370
|
+
: new UploadsError("docs catalog was not valid JSON", "API_ERROR", res.status);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else if (opts.url) {
|
|
375
|
+
throw new UploadsError(`docs catalog returned HTTP ${res.status}; see ${DOCS_HUB_URL}`, "API_ERROR", res.status);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
if (opts.url)
|
|
380
|
+
throw err;
|
|
381
|
+
}
|
|
382
|
+
return FALLBACK_DOCS_CATALOG;
|
|
383
|
+
}
|
|
384
|
+
async function fetchPageMarkdown(page, opts) {
|
|
385
|
+
if (!isUploadsDocsUrl(page.url)) {
|
|
386
|
+
throw new UploadsError(`refusing to fetch docs outside ${SITE_ORIGIN}`, "USAGE");
|
|
387
|
+
}
|
|
388
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
389
|
+
const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
390
|
+
const userAgent = opts.userAgent ?? `uploads/${packageVersion()}`;
|
|
391
|
+
const res = await fetchUrl(fetchImpl, page.url, {
|
|
392
|
+
"user-agent": userAgent,
|
|
393
|
+
accept: "text/markdown, text/plain;q=0.9",
|
|
394
|
+
}, timeoutMs);
|
|
395
|
+
if (!res.ok) {
|
|
396
|
+
throw new UploadsError(`docs page returned HTTP ${res.status}; see ${page.url}`, "API_ERROR", res.status);
|
|
397
|
+
}
|
|
398
|
+
const body = await res.text();
|
|
399
|
+
return hitFromPage(page, body);
|
|
400
|
+
}
|
|
401
|
+
export async function searchDocs(opts = {}) {
|
|
402
|
+
const catalog = await loadDocsCatalog(opts);
|
|
403
|
+
const pageArg = opts.page?.trim();
|
|
404
|
+
const query = opts.query?.trim();
|
|
405
|
+
if (pageArg) {
|
|
406
|
+
const page = resolveDocsPage(catalog, pageArg);
|
|
407
|
+
if (!page) {
|
|
408
|
+
throw new UploadsError(`unknown docs page: ${pageArg}`, "USAGE");
|
|
409
|
+
}
|
|
410
|
+
const hit = await fetchPageMarkdown(page, opts);
|
|
411
|
+
return {
|
|
412
|
+
url: catalog.url,
|
|
413
|
+
query: pageArg,
|
|
414
|
+
results: [hit],
|
|
415
|
+
total: 1,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (query) {
|
|
419
|
+
const exact = resolveDocsPage(catalog, query);
|
|
420
|
+
if (exact && !/\s/.test(query)) {
|
|
421
|
+
const hit = await fetchPageMarkdown(exact, opts);
|
|
422
|
+
return { url: catalog.url, query, results: [hit], total: 1 };
|
|
423
|
+
}
|
|
424
|
+
const ranked = rankDocsPages(catalog, query);
|
|
425
|
+
const sliced = ranked.slice(0, clampDocsLimit(opts.limit));
|
|
426
|
+
return {
|
|
427
|
+
url: catalog.url,
|
|
428
|
+
query,
|
|
429
|
+
results: sliced.map((page) => hitFromPage(page)),
|
|
430
|
+
total: ranked.length,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const sliced = opts.limit === undefined ? catalog.pages : catalog.pages.slice(0, clampDocsLimit(opts.limit));
|
|
434
|
+
return {
|
|
435
|
+
url: catalog.url,
|
|
436
|
+
results: sliced.map((page) => hitFromPage(page)),
|
|
437
|
+
total: catalog.pages.length,
|
|
438
|
+
};
|
|
439
|
+
}
|
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
|
@@ -5,11 +5,12 @@ export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, reso
|
|
|
5
5
|
export { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, defaultConfigPath, resolveConfigPath, loadConfigFile, loadEnvFile, resolveApiUrl, resolveConfig, describeConfigSources, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, workspaceFromToken, workspaceMismatch, type UploadsClientConfig, type ResolvedConfig, type WorkspaceSource, type ConfigValueSource, type ConfigSources, type UploadsConfigKey, type UploadsConfigValues, type PutDefaults, } from "./config.js";
|
|
6
6
|
export { UploadsError, type UploadsErrorCode } from "./errors.js";
|
|
7
7
|
export { CHANGELOG_JSON_URL, CHANGELOG_PAGE_URL, CHANGELOG_XML_URL, DEFAULT_CHANGELOG_LIMIT, MAX_CHANGELOG_LIMIT, fetchChangelog, formatChangelogHuman, parseChangelogAtom, parseChangelogJson, selectChangelogEntries, type ChangelogDocument, type ChangelogJsonEntry, type FetchChangelogOptions, } from "./changelog.js";
|
|
8
|
+
export { DEFAULT_DOCS_LIMIT, DOCS_HUB_URL, DOCS_JSON_URL, MAX_DOCS_LIMIT, formatDocsHuman, searchDocs, type DocsCatalog, type DocsSearchDocument, type SearchDocsOptions, } from "./docs.js";
|
|
8
9
|
export { assertFetchableUploadUrl, fetchUploadSource, filenameFromUploadUrl, resolveUploadFilename, FETCH_UPLOAD_SOURCE_DEFAULT_MAX_BYTES, FETCH_UPLOAD_SOURCE_MAX_REDIRECTS, FETCH_UPLOAD_SOURCE_TIMEOUT_MS, } from "./fetch-upload-source.js";
|
|
9
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";
|
|
10
11
|
export { buildCliProvenance } from "./provenance.js";
|
|
11
12
|
export { META_KEY_RE, META_VALUE_MAX, META_MAX_KEYS, META_MAX_TOTAL_BYTES, validateMetaEntry, parseMetaPair, parseMetaFlags, } from "./metadata.js";
|
|
12
|
-
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";
|
|
13
14
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, type OptimizeImageOptions, type OptimizeImageResult, type OptimizeOutputFormat, } from "./optimize.js";
|
|
14
15
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, type FrameFit, type FrameOptions, type FrameResult, } from "./frame.js";
|
|
15
16
|
export { execRunner, resolveRepo, upsertAttachmentsComment, type CommandRunner, } from "./github-gh.js";
|
package/dist/index.js
CHANGED
|
@@ -5,11 +5,12 @@ export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, reso
|
|
|
5
5
|
export { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, defaultConfigPath, resolveConfigPath, loadConfigFile, loadEnvFile, resolveApiUrl, resolveConfig, describeConfigSources, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, workspaceFromToken, workspaceMismatch, } from "./config.js";
|
|
6
6
|
export { UploadsError } from "./errors.js";
|
|
7
7
|
export { CHANGELOG_JSON_URL, CHANGELOG_PAGE_URL, CHANGELOG_XML_URL, DEFAULT_CHANGELOG_LIMIT, MAX_CHANGELOG_LIMIT, fetchChangelog, formatChangelogHuman, parseChangelogAtom, parseChangelogJson, selectChangelogEntries, } from "./changelog.js";
|
|
8
|
+
export { DEFAULT_DOCS_LIMIT, DOCS_HUB_URL, DOCS_JSON_URL, MAX_DOCS_LIMIT, formatDocsHuman, searchDocs, } from "./docs.js";
|
|
8
9
|
export { assertFetchableUploadUrl, fetchUploadSource, filenameFromUploadUrl, resolveUploadFilename, FETCH_UPLOAD_SOURCE_DEFAULT_MAX_BYTES, FETCH_UPLOAD_SOURCE_MAX_REDIRECTS, FETCH_UPLOAD_SOURCE_TIMEOUT_MS, } from "./fetch-upload-source.js";
|
|
9
10
|
export { createUploadsClient, } from "./client.js";
|
|
10
11
|
export { buildCliProvenance } from "./provenance.js";
|
|
11
12
|
export { META_KEY_RE, META_VALUE_MAX, META_MAX_KEYS, META_MAX_TOTAL_BYTES, validateMetaEntry, parseMetaPair, parseMetaFlags, } from "./metadata.js";
|
|
12
|
-
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,
|
|
13
14
|
// Private-repo randomized-prefix builders (issue #631) — needed by the
|
|
14
15
|
// hosted MCP (apps/mcp), which builds keys in-process rather than via
|
|
15
16
|
// the CLI's own commands.ts.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ToolArgs } from "./args.js";
|
|
2
|
+
export declare const SEARCH_DOCS_DESCRIPTION = "Search the public uploads.sh documentation. Use this to answer questions about uploads.sh product workflows, including: attaching screenshots and video to GitHub PRs and issues; staging files before a PR exists; galleries; the GitHub App; comment config (.uploads.yml); hosted MCP and agent setup; screenshot capture and annotate; plans and limits; bring-your-own bucket. Same as `uploads docs`. Returns titles, URLs, and snippets. Pass `page` to fetch the full markdown of one page (slug, path, or URL). Omit `query` to list the catalog.";
|
|
3
|
+
export declare const SEARCH_DOCS_INPUT_SCHEMA: Record<string, unknown>;
|
|
4
|
+
export declare function runSearchDocsTool(args: ToolArgs): Promise<import("../docs.js").DocsSearchDocument>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared MCP `search_docs` surface for stdio and hosted servers.
|
|
3
|
+
*/
|
|
4
|
+
import { DEFAULT_DOCS_LIMIT, MAX_DOCS_LIMIT, searchDocs } from "../docs.js";
|
|
5
|
+
import { optPosInt, optString, usage } from "./args.js";
|
|
6
|
+
export const SEARCH_DOCS_DESCRIPTION = "Search the public uploads.sh documentation. Use this to answer questions about uploads.sh product workflows, including: attaching screenshots and video to GitHub PRs and issues; staging files before a PR exists; galleries; the GitHub App; comment config (.uploads.yml); hosted MCP and agent setup; screenshot capture and annotate; plans and limits; bring-your-own bucket. Same as `uploads docs`. Returns titles, URLs, and snippets. Pass `page` to fetch the full markdown of one page (slug, path, or URL). Omit `query` to list the catalog.";
|
|
7
|
+
export const SEARCH_DOCS_INPUT_SCHEMA = {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
query: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Search query (e.g. 'stage before a PR'). A single slug like 'attach' fetches that page.",
|
|
13
|
+
},
|
|
14
|
+
page: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Fetch one page as markdown. Accepts a slug (`attach`), path (`/docs/agents`), or https://uploads.sh URL.",
|
|
17
|
+
},
|
|
18
|
+
limit: {
|
|
19
|
+
type: "number",
|
|
20
|
+
description: `How many search hits to return (default ${DEFAULT_DOCS_LIMIT}, max ${MAX_DOCS_LIMIT}). Ignored when fetching one page.`,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
};
|
|
25
|
+
export async function runSearchDocsTool(args) {
|
|
26
|
+
const query = optString(args, "query");
|
|
27
|
+
const page = optString(args, "page");
|
|
28
|
+
if (query && page)
|
|
29
|
+
usage("pass query or page, not both");
|
|
30
|
+
const limit = optPosInt(args, "limit");
|
|
31
|
+
if (limit !== undefined && limit > MAX_DOCS_LIMIT) {
|
|
32
|
+
usage(`limit must be ${MAX_DOCS_LIMIT} or less`);
|
|
33
|
+
}
|
|
34
|
+
return searchDocs({ query, page, limit });
|
|
35
|
+
}
|
|
@@ -22,9 +22,11 @@ export declare const reconcileResultSchema: JsonSchema;
|
|
|
22
22
|
export declare const purgeExpiredResultSchema: JsonSchema;
|
|
23
23
|
export declare const whoamiResultSchema: JsonSchema;
|
|
24
24
|
export declare const changelogResultSchema: JsonSchema;
|
|
25
|
+
export declare const searchDocsResultSchema: JsonSchema;
|
|
25
26
|
export declare const promoteToolResultSchema: JsonSchema;
|
|
26
27
|
export declare const galleryResultSchema: JsonSchema;
|
|
27
28
|
export declare const galleryFindResultSchema: JsonSchema;
|
|
29
|
+
export declare const feedResultSchema: JsonSchema;
|
|
28
30
|
/** Hosted catalog — every tool must have an entry. */
|
|
29
31
|
export declare const hostedOutputSchemas: Record<string, JsonSchema>;
|
|
30
32
|
/** Shared-shape stdio tools. Hosted-only tools (`promote`, `repo_link_status`) omitted. */
|