@buildinternet/uploads 0.52.1 → 0.53.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
@@ -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`, `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("&lt;", "<")
62
+ .replaceAll("&gt;", ">")
63
+ .replaceAll("&quot;", '"')
64
+ .replaceAll("&apos;", "'")
65
+ .replaceAll("&amp;", "&");
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
+ }
@@ -233,6 +233,10 @@ 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
+ },
236
240
  {
237
241
  name: "login",
238
242
  summary: "Sign in via browser (or an enrollment code) and save credentials",
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@ 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";
19
20
  import { runCompletion } from "./commands/completion.js";
20
21
  import { runLogout, runWhoami } from "./commands/session.js";
21
22
  import { runTelemetry } from "./commands/telemetry.js";
@@ -238,6 +239,10 @@ export async function runCli(argv) {
238
239
  case "health":
239
240
  code = await runHealth({ apiUrl, json }, cmdArgs, showHelp);
240
241
  break;
242
+ case "changelog":
243
+ // Public feed — no token, independent of the API origin.
244
+ code = await runChangelog(cmdArgs, { json }, showHelp);
245
+ break;
241
246
  case "config":
242
247
  code = await runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
243
248
  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
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ 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";
7
8
  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
9
  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
10
  export { buildCliProvenance } from "./provenance.js";
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ 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";
7
8
  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
9
  export { createUploadsClient, } from "./client.js";
9
10
  export { buildCliProvenance } from "./provenance.js";
@@ -21,6 +21,7 @@ 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;
24
25
  export declare const promoteToolResultSchema: JsonSchema;
25
26
  export declare const galleryResultSchema: JsonSchema;
26
27
  export declare const galleryFindResultSchema: JsonSchema;
@@ -248,6 +248,21 @@ 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"]);
251
266
  export const promoteToolResultSchema = objectSchema({
252
267
  // `promotion` is optional (issue #702): a `keys`-only call (no `branch`)
253
268
  // never runs the branch sweep, so there's nothing to report under it.
@@ -329,6 +344,7 @@ export const hostedOutputSchemas = {
329
344
  reconcile: reconcileResultSchema,
330
345
  purge_expired: purgeExpiredResultSchema,
331
346
  whoami: whoamiResultSchema,
347
+ changelog: changelogResultSchema,
332
348
  };
333
349
  /** Shared-shape stdio tools. Hosted-only tools (`promote`, `repo_link_status`) omitted. */
334
350
  export const stdioOutputSchemas = {
@@ -362,6 +378,7 @@ export const stdioOutputSchemas = {
362
378
  reconcile: reconcileResultSchema,
363
379
  purge_expired: purgeExpiredResultSchema,
364
380
  whoami: whoamiResultSchema,
381
+ changelog: changelogResultSchema,
365
382
  report: objectSchema({
366
383
  ok: { type: "boolean" },
367
384
  id: { type: "string" },
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MCP tool set mirroring the CLI commands (put, attach, list, delete,
3
- * usage, reconcile, purge_expired, comment, whoami, doctor). Config is
3
+ * usage, reconcile, purge_expired, comment, whoami, doctor, changelog). Config is
4
4
  * resolved fresh per tool call so a
5
5
  * per-call `workspace` argument behaves like the CLI's --workspace flag, and
6
6
  * a missing token surfaces as a tool error rather than a startup failure.
package/dist/mcp/tools.js CHANGED
@@ -14,6 +14,7 @@ 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";
17
18
  function mcpOptimizeOptions(args, defaults) {
18
19
  const quality = optPosInt(args, "optimizeQuality");
19
20
  if (quality !== undefined && quality > 100)
@@ -1552,6 +1553,33 @@ export function createUploadsMcpTools(opts) {
1552
1553
  return { ...target, ...result };
1553
1554
  },
1554
1555
  },
1556
+ {
1557
+ name: "changelog",
1558
+ title: "Product changelog",
1559
+ annotations: {
1560
+ readOnlyHint: true,
1561
+ destructiveHint: false,
1562
+ openWorldHint: true,
1563
+ },
1564
+ securitySchemes: mcpNoAuth,
1565
+ 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.",
1566
+ inputSchema: {
1567
+ type: "object",
1568
+ properties: {
1569
+ limit: {
1570
+ type: "number",
1571
+ description: `How many entries to return (default ${DEFAULT_CHANGELOG_LIMIT}, max ${MAX_CHANGELOG_LIMIT}).`,
1572
+ },
1573
+ },
1574
+ additionalProperties: false,
1575
+ },
1576
+ async handler(args) {
1577
+ const limit = optPosInt(args, "limit") ?? DEFAULT_CHANGELOG_LIMIT;
1578
+ if (limit > MAX_CHANGELOG_LIMIT)
1579
+ usage(`limit must be ${MAX_CHANGELOG_LIMIT} or less`);
1580
+ return fetchChangelog({ limit });
1581
+ },
1582
+ },
1555
1583
  {
1556
1584
  name: "whoami",
1557
1585
  title: "Who am I",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.52.1",
3
+ "version": "0.53.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.2.4"
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.2.5",
52
+ "files-sdk": "^2.3.1",
53
53
  "typescript": "^7.0.2",
54
54
  "vitest": "^4.1.10"
55
55
  },