@buildinternet/uploads 0.52.1 → 0.54.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 +1 -1
- package/dist/changelog.d.ts +36 -0
- package/dist/changelog.js +240 -0
- package/dist/cli-catalog.js +9 -0
- package/dist/cli.js +10 -0
- package/dist/commands/changelog.d.ts +7 -0
- package/dist/commands/changelog.js +49 -0
- package/dist/commands/docs.d.ts +8 -0
- package/dist/commands/docs.js +67 -0
- package/dist/docs.d.ts +52 -0
- package/dist/docs.js +439 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- 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 +33 -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 +42 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ global `uploads` form above.
|
|
|
45
45
|
|
|
46
46
|
Commands: `attach`, `put`, `screenshot`, `annotate`, `gallery`, `comment`, `list`, `find`, `meta`, `delete`, `usage`,
|
|
47
47
|
`reconcile`, `purge-expired`, `setup`, `install`, `login`, `whoami` (alias `status`),
|
|
48
|
-
`logout`, `invite`, `admin`, `config`, `telemetry`, `report`, `doctor`, `health`, `mcp`,
|
|
48
|
+
`logout`, `invite`, `admin`, `config`, `telemetry`, `report`, `doctor`, `health`, `changelog`, `docs`, `mcp`,
|
|
49
49
|
`completion`.
|
|
50
50
|
|
|
51
51
|
**Help:** bare `uploads` / `uploads help` / `--help` shows essentials; use
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const CHANGELOG_PAGE_URL = "https://uploads.sh/changelog";
|
|
2
|
+
export declare const CHANGELOG_JSON_URL = "https://uploads.sh/changelog.json";
|
|
3
|
+
export declare const CHANGELOG_XML_URL = "https://uploads.sh/changelog.xml";
|
|
4
|
+
export declare const DEFAULT_CHANGELOG_LIMIT = 5;
|
|
5
|
+
export declare const MAX_CHANGELOG_LIMIT = 50;
|
|
6
|
+
export type ChangelogKind = "platform" | "cli";
|
|
7
|
+
export type ChangelogJsonEntry = {
|
|
8
|
+
id: string;
|
|
9
|
+
kind: ChangelogKind;
|
|
10
|
+
title: string;
|
|
11
|
+
date: string;
|
|
12
|
+
url: string;
|
|
13
|
+
tags: string[];
|
|
14
|
+
summary: string;
|
|
15
|
+
body: string;
|
|
16
|
+
};
|
|
17
|
+
export type ChangelogDocument = {
|
|
18
|
+
url: string;
|
|
19
|
+
feed?: string;
|
|
20
|
+
entries: ChangelogJsonEntry[];
|
|
21
|
+
};
|
|
22
|
+
export type FetchChangelogOptions = {
|
|
23
|
+
limit?: number;
|
|
24
|
+
url?: string;
|
|
25
|
+
fetchImpl?: typeof fetch;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
userAgent?: string;
|
|
28
|
+
};
|
|
29
|
+
/** Parse the Atom twin at /changelog.xml (fallback when JSON is not deployed yet). */
|
|
30
|
+
export declare function parseChangelogAtom(xml: string): ChangelogDocument;
|
|
31
|
+
export declare function parseChangelogJson(raw: unknown): ChangelogDocument;
|
|
32
|
+
export declare function clampChangelogLimit(limit: number | undefined): number;
|
|
33
|
+
export declare function selectChangelogEntries(doc: ChangelogDocument, limit?: number): ChangelogDocument;
|
|
34
|
+
/** Human terminal output: recent titles + summaries, then a link to the page. */
|
|
35
|
+
export declare function formatChangelogHuman(doc: ChangelogDocument): string;
|
|
36
|
+
export declare function fetchChangelog(opts?: FetchChangelogOptions): Promise<ChangelogDocument>;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch and format the public uploads.sh changelog for `uploads changelog`
|
|
3
|
+
* and the MCP `changelog` tool.
|
|
4
|
+
*/
|
|
5
|
+
import { UploadsError } from "./errors.js";
|
|
6
|
+
import { packageVersion } from "./package-version.js";
|
|
7
|
+
export const CHANGELOG_PAGE_URL = "https://uploads.sh/changelog";
|
|
8
|
+
export const CHANGELOG_JSON_URL = "https://uploads.sh/changelog.json";
|
|
9
|
+
export const CHANGELOG_XML_URL = "https://uploads.sh/changelog.xml";
|
|
10
|
+
export const DEFAULT_CHANGELOG_LIMIT = 5;
|
|
11
|
+
export const MAX_CHANGELOG_LIMIT = 50;
|
|
12
|
+
const FETCH_TIMEOUT_MS = 8000;
|
|
13
|
+
function isKind(value) {
|
|
14
|
+
return value === "platform" || value === "cli";
|
|
15
|
+
}
|
|
16
|
+
function asString(value) {
|
|
17
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
18
|
+
}
|
|
19
|
+
function asHttpUrl(value) {
|
|
20
|
+
const raw = asString(value);
|
|
21
|
+
if (!raw)
|
|
22
|
+
return undefined;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(raw);
|
|
25
|
+
if (parsed.protocol === "https:" || parsed.protocol === "http:")
|
|
26
|
+
return raw;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function parseEntry(raw) {
|
|
34
|
+
if (!raw || typeof raw !== "object")
|
|
35
|
+
return undefined;
|
|
36
|
+
const rec = raw;
|
|
37
|
+
const id = asString(rec.id);
|
|
38
|
+
const title = asString(rec.title);
|
|
39
|
+
const date = asString(rec.date);
|
|
40
|
+
const url = asHttpUrl(rec.url);
|
|
41
|
+
const summary = asString(rec.summary);
|
|
42
|
+
if (!id || !title || !date || !url || !summary)
|
|
43
|
+
return undefined;
|
|
44
|
+
const kind = isKind(rec.kind) ? rec.kind : "platform";
|
|
45
|
+
const tags = Array.isArray(rec.tags)
|
|
46
|
+
? rec.tags.filter((t) => typeof t === "string" && t.length > 0)
|
|
47
|
+
: [];
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
kind,
|
|
51
|
+
title,
|
|
52
|
+
date,
|
|
53
|
+
url,
|
|
54
|
+
tags,
|
|
55
|
+
summary,
|
|
56
|
+
body: typeof rec.body === "string" ? rec.body : "",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function decodeXml(value) {
|
|
60
|
+
return value
|
|
61
|
+
.replaceAll("<", "<")
|
|
62
|
+
.replaceAll(">", ">")
|
|
63
|
+
.replaceAll(""", '"')
|
|
64
|
+
.replaceAll("'", "'")
|
|
65
|
+
.replaceAll("&", "&");
|
|
66
|
+
}
|
|
67
|
+
function stripHtml(html) {
|
|
68
|
+
return html
|
|
69
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
70
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
71
|
+
.replace(/<[^>]+>/g, " ")
|
|
72
|
+
.replace(/\s+/g, " ")
|
|
73
|
+
.trim();
|
|
74
|
+
}
|
|
75
|
+
function tagMatch(block, tag) {
|
|
76
|
+
const m = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`));
|
|
77
|
+
return m?.[1] === undefined ? undefined : decodeXml(m[1]).trim();
|
|
78
|
+
}
|
|
79
|
+
function attrMatch(block, tag, attr) {
|
|
80
|
+
const m = block.match(new RegExp(`<${tag}[^>]*\\s${attr}="([^"]+)"`));
|
|
81
|
+
return m?.[1] === undefined ? undefined : decodeXml(m[1]).trim();
|
|
82
|
+
}
|
|
83
|
+
function truncateSummary(text, maxChars = 280) {
|
|
84
|
+
if (text.length <= maxChars)
|
|
85
|
+
return text;
|
|
86
|
+
const cut = text.slice(0, maxChars - 1);
|
|
87
|
+
const atSpace = cut.lastIndexOf(" ");
|
|
88
|
+
return `${atSpace > 40 ? cut.slice(0, atSpace) : cut}…`;
|
|
89
|
+
}
|
|
90
|
+
/** Parse the Atom twin at /changelog.xml (fallback when JSON is not deployed yet). */
|
|
91
|
+
export function parseChangelogAtom(xml) {
|
|
92
|
+
const entries = [];
|
|
93
|
+
const entryRe = /<entry>([\s\S]*?)<\/entry>/g;
|
|
94
|
+
let match;
|
|
95
|
+
while ((match = entryRe.exec(xml))) {
|
|
96
|
+
const block = match[1] ?? "";
|
|
97
|
+
const title = tagMatch(block, "title");
|
|
98
|
+
const date = tagMatch(block, "updated");
|
|
99
|
+
const url = attrMatch(block, "link", "href") ?? tagMatch(block, "id");
|
|
100
|
+
const idHref = tagMatch(block, "id") ?? url ?? "";
|
|
101
|
+
const hash = idHref.indexOf("#");
|
|
102
|
+
const id = hash >= 0 ? idHref.slice(hash + 1) : undefined;
|
|
103
|
+
const tags = [...block.matchAll(/<category\s+term="([^"]+)"/g)].map((m) => decodeXml(m[1] ?? ""));
|
|
104
|
+
const html = tagMatch(block, "content") ?? "";
|
|
105
|
+
const plain = stripHtml(html);
|
|
106
|
+
if (!title || !date || !url || !id || !plain)
|
|
107
|
+
continue;
|
|
108
|
+
const kind = tags.includes("cli") && !tags.includes("platform") ? "cli" : "platform";
|
|
109
|
+
entries.push({
|
|
110
|
+
id,
|
|
111
|
+
kind,
|
|
112
|
+
title,
|
|
113
|
+
date,
|
|
114
|
+
url,
|
|
115
|
+
tags,
|
|
116
|
+
summary: truncateSummary(plain),
|
|
117
|
+
body: plain,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (entries.length === 0) {
|
|
121
|
+
throw new UploadsError("changelog Atom feed had no usable entries", "API_ERROR");
|
|
122
|
+
}
|
|
123
|
+
return { url: CHANGELOG_PAGE_URL, feed: CHANGELOG_XML_URL, entries };
|
|
124
|
+
}
|
|
125
|
+
export function parseChangelogJson(raw) {
|
|
126
|
+
if (!raw || typeof raw !== "object") {
|
|
127
|
+
throw new UploadsError("changelog response was not a JSON object", "API_ERROR");
|
|
128
|
+
}
|
|
129
|
+
const rec = raw;
|
|
130
|
+
if (!Array.isArray(rec.entries)) {
|
|
131
|
+
throw new UploadsError("changelog response is missing an entries array", "API_ERROR");
|
|
132
|
+
}
|
|
133
|
+
const entries = rec.entries
|
|
134
|
+
.map(parseEntry)
|
|
135
|
+
.filter((e) => e !== undefined);
|
|
136
|
+
if (rec.entries.length > 0 && entries.length === 0) {
|
|
137
|
+
throw new UploadsError("changelog response had no usable entries", "API_ERROR");
|
|
138
|
+
}
|
|
139
|
+
const feed = asHttpUrl(rec.feed);
|
|
140
|
+
return {
|
|
141
|
+
url: asHttpUrl(rec.url) ?? CHANGELOG_PAGE_URL,
|
|
142
|
+
...(feed ? { feed } : {}),
|
|
143
|
+
entries,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export function clampChangelogLimit(limit) {
|
|
147
|
+
const n = limit ?? DEFAULT_CHANGELOG_LIMIT;
|
|
148
|
+
if (n > MAX_CHANGELOG_LIMIT)
|
|
149
|
+
return MAX_CHANGELOG_LIMIT;
|
|
150
|
+
if (n < 1)
|
|
151
|
+
return DEFAULT_CHANGELOG_LIMIT;
|
|
152
|
+
return n;
|
|
153
|
+
}
|
|
154
|
+
export function selectChangelogEntries(doc, limit) {
|
|
155
|
+
const n = clampChangelogLimit(limit);
|
|
156
|
+
return { ...doc, entries: doc.entries.slice(0, n) };
|
|
157
|
+
}
|
|
158
|
+
function formatDate(iso) {
|
|
159
|
+
const d = new Date(iso);
|
|
160
|
+
if (Number.isNaN(d.getTime()))
|
|
161
|
+
return iso;
|
|
162
|
+
return d.toLocaleDateString("en-US", {
|
|
163
|
+
year: "numeric",
|
|
164
|
+
month: "short",
|
|
165
|
+
day: "numeric",
|
|
166
|
+
timeZone: "UTC",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function indent(text, prefix = " ") {
|
|
170
|
+
return text
|
|
171
|
+
.split("\n")
|
|
172
|
+
.map((line) => (line.length === 0 ? prefix.trimEnd() : `${prefix}${line}`))
|
|
173
|
+
.join("\n");
|
|
174
|
+
}
|
|
175
|
+
/** Human terminal output: recent titles + summaries, then a link to the page. */
|
|
176
|
+
export function formatChangelogHuman(doc) {
|
|
177
|
+
const lines = ["What's new", ""];
|
|
178
|
+
if (doc.entries.length === 0) {
|
|
179
|
+
lines.push("No changelog entries.");
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
for (const entry of doc.entries) {
|
|
183
|
+
lines.push(entry.title);
|
|
184
|
+
lines.push(`${formatDate(entry.date)} · ${entry.url}`);
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push(indent(entry.summary));
|
|
187
|
+
lines.push("");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
lines.push(`See all updates: ${doc.url}`);
|
|
191
|
+
return `${lines.join("\n")}\n`;
|
|
192
|
+
}
|
|
193
|
+
async function fetchUrl(fetchImpl, url, headers, timeoutMs) {
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
196
|
+
try {
|
|
197
|
+
return await fetchImpl(url, { signal: controller.signal, headers });
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
const aborted = err instanceof Error && err.name === "AbortError";
|
|
201
|
+
throw new UploadsError(aborted
|
|
202
|
+
? `timed out fetching changelog (${timeoutMs}ms)`
|
|
203
|
+
: `couldn't reach changelog (${err instanceof Error ? err.message : String(err)})`, "NETWORK");
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export async function fetchChangelog(opts = {}) {
|
|
210
|
+
const explicitUrl = opts.url;
|
|
211
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
212
|
+
const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
213
|
+
const userAgent = opts.userAgent ?? `uploads/${packageVersion()}`;
|
|
214
|
+
const headers = { "user-agent": userAgent };
|
|
215
|
+
const jsonUrl = explicitUrl ?? CHANGELOG_JSON_URL;
|
|
216
|
+
const jsonRes = await fetchUrl(fetchImpl, jsonUrl, { ...headers, accept: "application/json" }, timeoutMs);
|
|
217
|
+
if (jsonRes.ok) {
|
|
218
|
+
try {
|
|
219
|
+
const payload = await jsonRes.json();
|
|
220
|
+
return selectChangelogEntries(parseChangelogJson(payload), opts.limit);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
if (explicitUrl) {
|
|
224
|
+
throw err instanceof UploadsError
|
|
225
|
+
? err
|
|
226
|
+
: new UploadsError("changelog response was not valid JSON", "API_ERROR", jsonRes.status);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (explicitUrl) {
|
|
231
|
+
throw new UploadsError(`changelog returned HTTP ${jsonRes.status}; see ${CHANGELOG_PAGE_URL}`, "API_ERROR", jsonRes.status);
|
|
232
|
+
}
|
|
233
|
+
// JSON twin is new; fall back to the Atom feed that already ships.
|
|
234
|
+
const xmlRes = await fetchUrl(fetchImpl, CHANGELOG_XML_URL, { ...headers, accept: "application/atom+xml, application/xml, text/xml" }, timeoutMs);
|
|
235
|
+
if (!xmlRes.ok) {
|
|
236
|
+
throw new UploadsError(`changelog returned HTTP ${xmlRes.status}; see ${CHANGELOG_PAGE_URL}`, "API_ERROR", xmlRes.status);
|
|
237
|
+
}
|
|
238
|
+
const xml = await xmlRes.text();
|
|
239
|
+
return selectChangelogEntries(parseChangelogAtom(xml), opts.limit);
|
|
240
|
+
}
|
package/dist/cli-catalog.js
CHANGED
|
@@ -233,6 +233,15 @@ export const ROOT_COMMANDS = [
|
|
|
233
233
|
summary: "Update the CLI, then refresh the agent skills + MCP registration",
|
|
234
234
|
essential: true,
|
|
235
235
|
},
|
|
236
|
+
{
|
|
237
|
+
name: "changelog",
|
|
238
|
+
summary: "Show recent product updates (and a link to the full changelog)",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "docs",
|
|
242
|
+
summary: "Search public uploads.sh docs (or fetch one page as markdown)",
|
|
243
|
+
usage: "docs [query]",
|
|
244
|
+
},
|
|
236
245
|
{
|
|
237
246
|
name: "login",
|
|
238
247
|
summary: "Sign in via browser (or an enrollment code) and save credentials",
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,8 @@ import { runMcp } from "./commands/mcp.js";
|
|
|
16
16
|
import { runInstall } from "./commands/install.js";
|
|
17
17
|
import { runHook } from "./commands/hook.js";
|
|
18
18
|
import { runUpdate } from "./commands/update.js";
|
|
19
|
+
import { runChangelog } from "./commands/changelog.js";
|
|
20
|
+
import { runDocs } from "./commands/docs.js";
|
|
19
21
|
import { runCompletion } from "./commands/completion.js";
|
|
20
22
|
import { runLogout, runWhoami } from "./commands/session.js";
|
|
21
23
|
import { runTelemetry } from "./commands/telemetry.js";
|
|
@@ -238,6 +240,14 @@ export async function runCli(argv) {
|
|
|
238
240
|
case "health":
|
|
239
241
|
code = await runHealth({ apiUrl, json }, cmdArgs, showHelp);
|
|
240
242
|
break;
|
|
243
|
+
case "changelog":
|
|
244
|
+
// Public feed — no token, independent of the API origin.
|
|
245
|
+
code = await runChangelog(cmdArgs, { json }, showHelp);
|
|
246
|
+
break;
|
|
247
|
+
case "docs":
|
|
248
|
+
// Public catalog — no token, independent of the API origin.
|
|
249
|
+
code = await runDocs(cmdArgs, { json }, showHelp);
|
|
250
|
+
break;
|
|
241
251
|
case "config":
|
|
242
252
|
code = await runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
|
|
243
253
|
break;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type FetchChangelogOptions } from "../changelog.js";
|
|
2
|
+
export interface RunChangelogOptions {
|
|
3
|
+
json?: boolean;
|
|
4
|
+
fetch?: FetchChangelogOptions["fetchImpl"];
|
|
5
|
+
url?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function runChangelog(args: string[], opts?: RunChangelogOptions, help?: boolean): Promise<number>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { flagBool, flagInt, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
2
|
+
import { DEFAULT_CHANGELOG_LIMIT, MAX_CHANGELOG_LIMIT, fetchChangelog, formatChangelogHuman, } from "../changelog.js";
|
|
3
|
+
import { writeCommandHelp } from "../cli-style.js";
|
|
4
|
+
import { writeJson, writeStdout } from "../io.js";
|
|
5
|
+
const CHANGELOG_HELP = `uploads changelog — recent product updates
|
|
6
|
+
|
|
7
|
+
Prints the latest updates from uploads.sh, then a link to the full changelog.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
uploads changelog [options]
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--limit <n> Number of entries to show (default: ${DEFAULT_CHANGELOG_LIMIT}, max: ${MAX_CHANGELOG_LIMIT})
|
|
14
|
+
--json JSON on stdout (also accepts global --json)
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
uploads changelog
|
|
18
|
+
uploads changelog --limit 10
|
|
19
|
+
uploads changelog --json
|
|
20
|
+
`;
|
|
21
|
+
export async function runChangelog(args, opts = {}, help = false) {
|
|
22
|
+
const parsed = parseCommandArgs(args);
|
|
23
|
+
if (help || parsed.help) {
|
|
24
|
+
writeCommandHelp(CHANGELOG_HELP);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
if (parsed.positionals.length > 0) {
|
|
28
|
+
throw new UsageError(`changelog takes no arguments (got ${parsed.positionals[0]})`, {
|
|
29
|
+
example: "uploads changelog",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const json = Boolean(opts.json) || flagBool(parsed.flags, "--json");
|
|
33
|
+
const limit = flagInt(parsed.flags, "--limit", "--limit") ?? DEFAULT_CHANGELOG_LIMIT;
|
|
34
|
+
if (limit > MAX_CHANGELOG_LIMIT) {
|
|
35
|
+
throw new UsageError(`--limit must be ${MAX_CHANGELOG_LIMIT} or less (got ${limit})`, {
|
|
36
|
+
example: `uploads changelog --limit ${MAX_CHANGELOG_LIMIT}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const doc = await fetchChangelog({
|
|
40
|
+
limit,
|
|
41
|
+
fetchImpl: opts.fetch,
|
|
42
|
+
url: opts.url,
|
|
43
|
+
});
|
|
44
|
+
if (json)
|
|
45
|
+
await writeJson(doc);
|
|
46
|
+
else
|
|
47
|
+
await writeStdout(formatChangelogHuman(doc));
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
@@ -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
|
+
}
|
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>;
|
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/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey
|
|
|
4
4
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, type BuiltinDestinationId, } from "./destinations.js";
|
|
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
|
+
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";
|
|
7
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";
|
|
8
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";
|
|
9
11
|
export { buildCliProvenance } from "./provenance.js";
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey
|
|
|
4
4
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, } from "./destinations.js";
|
|
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
|
+
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";
|
|
7
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";
|
|
8
10
|
export { createUploadsClient, } from "./client.js";
|
|
9
11
|
export { buildCliProvenance } from "./provenance.js";
|
|
@@ -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
|
+
}
|
|
@@ -21,6 +21,8 @@ export declare const usageResultSchema: JsonSchema;
|
|
|
21
21
|
export declare const reconcileResultSchema: JsonSchema;
|
|
22
22
|
export declare const purgeExpiredResultSchema: JsonSchema;
|
|
23
23
|
export declare const whoamiResultSchema: JsonSchema;
|
|
24
|
+
export declare const changelogResultSchema: JsonSchema;
|
|
25
|
+
export declare const searchDocsResultSchema: JsonSchema;
|
|
24
26
|
export declare const promoteToolResultSchema: JsonSchema;
|
|
25
27
|
export declare const galleryResultSchema: JsonSchema;
|
|
26
28
|
export declare const galleryFindResultSchema: JsonSchema;
|
|
@@ -248,6 +248,35 @@ export const whoamiResultSchema = objectSchema({
|
|
|
248
248
|
signedIn: { type: "boolean" },
|
|
249
249
|
apiUrl: { type: "string" },
|
|
250
250
|
}, ["ok", "workspace"]);
|
|
251
|
+
const changelogEntrySchema = objectSchema({
|
|
252
|
+
id: { type: "string" },
|
|
253
|
+
kind: { type: "string", enum: ["platform", "cli"] },
|
|
254
|
+
title: { type: "string" },
|
|
255
|
+
date: { type: "string" },
|
|
256
|
+
url: { type: "string" },
|
|
257
|
+
tags: { type: "array", items: { type: "string" } },
|
|
258
|
+
summary: { type: "string" },
|
|
259
|
+
body: { type: "string" },
|
|
260
|
+
}, ["id", "kind", "title", "date", "url", "tags", "summary", "body"]);
|
|
261
|
+
export const changelogResultSchema = objectSchema({
|
|
262
|
+
url: { type: "string" },
|
|
263
|
+
feed: { type: "string" },
|
|
264
|
+
entries: { type: "array", items: changelogEntrySchema },
|
|
265
|
+
}, ["url", "entries"]);
|
|
266
|
+
const docsSearchHitSchema = objectSchema({
|
|
267
|
+
title: { type: "string" },
|
|
268
|
+
url: { type: "string" },
|
|
269
|
+
page: { type: "string" },
|
|
270
|
+
snippet: { type: "string" },
|
|
271
|
+
body: { type: "string" },
|
|
272
|
+
truncated: { type: "boolean" },
|
|
273
|
+
}, ["title", "url", "page", "snippet"]);
|
|
274
|
+
export const searchDocsResultSchema = objectSchema({
|
|
275
|
+
url: { type: "string" },
|
|
276
|
+
query: { type: "string" },
|
|
277
|
+
results: { type: "array", items: docsSearchHitSchema },
|
|
278
|
+
total: { type: "number" },
|
|
279
|
+
}, ["url", "results", "total"]);
|
|
251
280
|
export const promoteToolResultSchema = objectSchema({
|
|
252
281
|
// `promotion` is optional (issue #702): a `keys`-only call (no `branch`)
|
|
253
282
|
// never runs the branch sweep, so there's nothing to report under it.
|
|
@@ -329,6 +358,8 @@ export const hostedOutputSchemas = {
|
|
|
329
358
|
reconcile: reconcileResultSchema,
|
|
330
359
|
purge_expired: purgeExpiredResultSchema,
|
|
331
360
|
whoami: whoamiResultSchema,
|
|
361
|
+
changelog: changelogResultSchema,
|
|
362
|
+
search_docs: searchDocsResultSchema,
|
|
332
363
|
};
|
|
333
364
|
/** Shared-shape stdio tools. Hosted-only tools (`promote`, `repo_link_status`) omitted. */
|
|
334
365
|
export const stdioOutputSchemas = {
|
|
@@ -362,6 +393,8 @@ export const stdioOutputSchemas = {
|
|
|
362
393
|
reconcile: reconcileResultSchema,
|
|
363
394
|
purge_expired: purgeExpiredResultSchema,
|
|
364
395
|
whoami: whoamiResultSchema,
|
|
396
|
+
changelog: changelogResultSchema,
|
|
397
|
+
search_docs: searchDocsResultSchema,
|
|
365
398
|
report: objectSchema({
|
|
366
399
|
ok: { type: "boolean" },
|
|
367
400
|
id: { type: "string" },
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { McpServer, type Implementation, type jsonSchemaValidator } from "@modelcontextprotocol/server";
|
|
21
21
|
export { appProp, canonicalMetaFromArgs, METADATA_DESCRIPTION, METADATA_PATH_CUE, MCP_EXAMPLE_PNG_BASE64, metadataArgWithCanonical, metadataProp, stateProp, optBool, optPosInt, optString, optStringArray, optStringRecord, usage, type ToolArgs, } from "./args.js";
|
|
22
22
|
export { ToolBatchError, batchFailureMessage } from "./batch-error.js";
|
|
23
|
+
export { SEARCH_DOCS_DESCRIPTION, SEARCH_DOCS_INPUT_SCHEMA, runSearchDocsTool, } from "./docs-tool.js";
|
|
23
24
|
export { mapBounded } from "../async.js";
|
|
24
25
|
export { McpServer, type jsonSchemaValidator };
|
|
25
26
|
export { commentResultSchema, deleteResultSchema, findFilesResultSchema, galleryFindResultSchema, galleryResultSchema, hostedOutputSchemas, listResultSchema, metadataFacetsResultSchema, metadataResultSchema, promoteToolResultSchema, purgeExpiredResultSchema, putResultSchema, reconcileResultSchema, repoLinkStatusResultSchema, stdioOutputSchemas, usageResultSchema, withOutputSchemas, } from "./output-schemas.js";
|
package/dist/mcp/server.js
CHANGED
|
@@ -23,6 +23,7 @@ import { errorCodeFromUnknown, recordEvent } from "../telemetry.js";
|
|
|
23
23
|
import { ToolBatchError } from "./batch-error.js";
|
|
24
24
|
export { appProp, canonicalMetaFromArgs, METADATA_DESCRIPTION, METADATA_PATH_CUE, MCP_EXAMPLE_PNG_BASE64, metadataArgWithCanonical, metadataProp, stateProp, optBool, optPosInt, optString, optStringArray, optStringRecord, usage, } from "./args.js";
|
|
25
25
|
export { ToolBatchError, batchFailureMessage } from "./batch-error.js";
|
|
26
|
+
export { SEARCH_DOCS_DESCRIPTION, SEARCH_DOCS_INPUT_SCHEMA, runSearchDocsTool, } from "./docs-tool.js";
|
|
26
27
|
export { mapBounded } from "../async.js";
|
|
27
28
|
export { McpServer };
|
|
28
29
|
export { commentResultSchema, deleteResultSchema, findFilesResultSchema, galleryFindResultSchema, galleryResultSchema, hostedOutputSchemas, listResultSchema, metadataFacetsResultSchema, metadataResultSchema, promoteToolResultSchema, purgeExpiredResultSchema, putResultSchema, reconcileResultSchema, repoLinkStatusResultSchema, stdioOutputSchemas, usageResultSchema, withOutputSchemas, } from "./output-schemas.js";
|
package/dist/mcp/tools.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP tool set mirroring the CLI commands (put, attach, list, delete,
|
|
3
|
-
* usage, reconcile, purge_expired, comment, whoami, doctor
|
|
3
|
+
* usage, reconcile, purge_expired, comment, whoami, doctor, changelog,
|
|
4
|
+
* search_docs). Config is
|
|
4
5
|
* resolved fresh per tool call so a
|
|
5
6
|
* per-call `workspace` argument behaves like the CLI's --workspace flag, and
|
|
6
7
|
* a missing token surfaces as a tool error rather than a startup failure.
|
package/dist/mcp/tools.js
CHANGED
|
@@ -14,6 +14,8 @@ import { appProp, canonicalMetaFromArgs, METADATA_PATH_CUE, metadataArgWithCanon
|
|
|
14
14
|
import { batchFailureMessage, mcpDestroyPublic, mcpNoAuth, mcpOAuthAny, mcpOAuthDelete, mcpOAuthRead, mcpOAuthWrite, mcpRead, mcpWriteInternal, mcpWritePublic, stdioOutputSchemas, withOutputSchemas, ToolBatchError, } from "./server.js";
|
|
15
15
|
import { attachmentFromText, buildReportPayload, parseReportType, REPORT_TYPES, submitReport, validateReportMessage, } from "../report.js";
|
|
16
16
|
import { resolveApiUrl } from "../config.js";
|
|
17
|
+
import { DEFAULT_CHANGELOG_LIMIT, MAX_CHANGELOG_LIMIT, fetchChangelog } from "../changelog.js";
|
|
18
|
+
import { SEARCH_DOCS_DESCRIPTION, SEARCH_DOCS_INPUT_SCHEMA, runSearchDocsTool, } from "./docs-tool.js";
|
|
17
19
|
function mcpOptimizeOptions(args, defaults) {
|
|
18
20
|
const quality = optPosInt(args, "optimizeQuality");
|
|
19
21
|
if (quality !== undefined && quality > 100)
|
|
@@ -1552,6 +1554,46 @@ export function createUploadsMcpTools(opts) {
|
|
|
1552
1554
|
return { ...target, ...result };
|
|
1553
1555
|
},
|
|
1554
1556
|
},
|
|
1557
|
+
{
|
|
1558
|
+
name: "search_docs",
|
|
1559
|
+
title: "Search uploads.sh docs",
|
|
1560
|
+
annotations: {
|
|
1561
|
+
readOnlyHint: true,
|
|
1562
|
+
destructiveHint: false,
|
|
1563
|
+
openWorldHint: true,
|
|
1564
|
+
},
|
|
1565
|
+
securitySchemes: mcpNoAuth,
|
|
1566
|
+
description: SEARCH_DOCS_DESCRIPTION,
|
|
1567
|
+
inputSchema: SEARCH_DOCS_INPUT_SCHEMA,
|
|
1568
|
+
handler: runSearchDocsTool,
|
|
1569
|
+
},
|
|
1570
|
+
{
|
|
1571
|
+
name: "changelog",
|
|
1572
|
+
title: "Product changelog",
|
|
1573
|
+
annotations: {
|
|
1574
|
+
readOnlyHint: true,
|
|
1575
|
+
destructiveHint: false,
|
|
1576
|
+
openWorldHint: true,
|
|
1577
|
+
},
|
|
1578
|
+
securitySchemes: mcpNoAuth,
|
|
1579
|
+
description: "Read recent uploads.sh product updates (platform and CLI). Returns the latest entries with titles, dates, summaries, and a link to the full changelog at https://uploads.sh/changelog. Same as `uploads changelog`. Use this to discover new features before recommending uploads.sh workflows.",
|
|
1580
|
+
inputSchema: {
|
|
1581
|
+
type: "object",
|
|
1582
|
+
properties: {
|
|
1583
|
+
limit: {
|
|
1584
|
+
type: "number",
|
|
1585
|
+
description: `How many entries to return (default ${DEFAULT_CHANGELOG_LIMIT}, max ${MAX_CHANGELOG_LIMIT}).`,
|
|
1586
|
+
},
|
|
1587
|
+
},
|
|
1588
|
+
additionalProperties: false,
|
|
1589
|
+
},
|
|
1590
|
+
async handler(args) {
|
|
1591
|
+
const limit = optPosInt(args, "limit") ?? DEFAULT_CHANGELOG_LIMIT;
|
|
1592
|
+
if (limit > MAX_CHANGELOG_LIMIT)
|
|
1593
|
+
usage(`limit must be ${MAX_CHANGELOG_LIMIT} or less`);
|
|
1594
|
+
return fetchChangelog({ limit });
|
|
1595
|
+
},
|
|
1596
|
+
},
|
|
1555
1597
|
{
|
|
1556
1598
|
name: "whoami",
|
|
1557
1599
|
title: "Who am I",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildinternet/uploads",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"mcpName": "sh.uploads/mcp",
|
|
5
5
|
"description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
|
|
6
6
|
"type": "module",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"node": ">=22"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"files-sdk": "^2.
|
|
42
|
+
"files-sdk": "^2.3.1"
|
|
43
43
|
},
|
|
44
44
|
"peerDependenciesMeta": {
|
|
45
45
|
"files-sdk": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^26.1.0",
|
|
51
51
|
"ai": "^6.0.0",
|
|
52
|
-
"files-sdk": "^2.
|
|
52
|
+
"files-sdk": "^2.3.1",
|
|
53
53
|
"typescript": "^7.0.2",
|
|
54
54
|
"vitest": "^4.1.10"
|
|
55
55
|
},
|