@buildinternet/uploads 0.52.0 → 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 +1 -1
- package/dist/changelog.d.ts +36 -0
- package/dist/changelog.js +240 -0
- package/dist/cli-catalog.js +4 -0
- package/dist/cli.js +5 -0
- package/dist/commands/changelog.d.ts +7 -0
- package/dist/commands/changelog.js +49 -0
- package/dist/commands.js +2 -1
- package/dist/comment-render.generated.d.ts +4 -0
- package/dist/comment-render.generated.js +77 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/output-schemas.d.ts +1 -0
- package/dist/mcp/output-schemas.js +17 -0
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.js +28 -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`, `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,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/commands.js
CHANGED
|
@@ -655,7 +655,7 @@ export async function syncAttachmentsComment(client, target, run, workspace, opt
|
|
|
655
655
|
target: { kind: target.kind, num: target.num },
|
|
656
656
|
});
|
|
657
657
|
const prefixes = ghListPrefixes(ghKeyPrefix(target), ghPrefix, (id) => ghPrivateKeyPrefix(id, target));
|
|
658
|
-
const items = await ghMergedList(prefixes, undefined, async (prefix) => (await client.listAll({ prefix, metadata: true })).map(({ key, url, embedUrl, pageUrl, metadata }) => {
|
|
658
|
+
const items = await ghMergedList(prefixes, undefined, async (prefix) => (await client.listAll({ prefix, metadata: true })).map(({ key, url, embedUrl, pageUrl, size, metadata }) => {
|
|
659
659
|
// The list endpoint returns every metadata key; the comment
|
|
660
660
|
// renders only these two. Narrowing here keeps both render paths
|
|
661
661
|
// byte-identical.
|
|
@@ -673,6 +673,7 @@ export async function syncAttachmentsComment(client, target, run, workspace, opt
|
|
|
673
673
|
url,
|
|
674
674
|
embedUrl,
|
|
675
675
|
pageUrl,
|
|
676
|
+
...(size != null ? { size } : {}),
|
|
676
677
|
...(path || state
|
|
677
678
|
? { meta: { ...(path ? { path } : {}), ...(state ? { state } : {}) } }
|
|
678
679
|
: {}),
|
|
@@ -120,6 +120,10 @@ export interface AttachmentItem {
|
|
|
120
120
|
width?: number;
|
|
121
121
|
height?: number;
|
|
122
122
|
};
|
|
123
|
+
/** Object size in bytes, when known — drives the non-media file table's Size column. */
|
|
124
|
+
size?: number;
|
|
125
|
+
/** Stored content type, when known — preferred over name-based inference for file classification and the table's Type column. */
|
|
126
|
+
contentType?: string;
|
|
123
127
|
}
|
|
124
128
|
/** A public gallery linked to the PR or issue whose managed comment is syncing. */
|
|
125
129
|
export interface GalleryCommentItem {
|
|
@@ -285,6 +285,47 @@ function formatMetaCaption(meta, options, mode) {
|
|
|
285
285
|
})
|
|
286
286
|
.join(" · ");
|
|
287
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Decimal-SI byte size for the non-media file table's Size column: whole
|
|
290
|
+
* bytes below 1000, one decimal place at KB/MB/GB and above. `"—"` when the
|
|
291
|
+
* size is unknown.
|
|
292
|
+
*/
|
|
293
|
+
function formatBytes(bytes) {
|
|
294
|
+
if (bytes == null || !Number.isFinite(bytes) || bytes < 0)
|
|
295
|
+
return "—";
|
|
296
|
+
if (bytes < 1000)
|
|
297
|
+
return `${Math.round(bytes)} B`;
|
|
298
|
+
const units = [
|
|
299
|
+
[1e9, "GB"],
|
|
300
|
+
[1e6, "MB"],
|
|
301
|
+
[1e3, "KB"],
|
|
302
|
+
];
|
|
303
|
+
for (const [threshold, label] of units) {
|
|
304
|
+
if (bytes >= threshold)
|
|
305
|
+
return `${(bytes / threshold).toFixed(1)} ${label}`;
|
|
306
|
+
}
|
|
307
|
+
return `${Math.round(bytes)} B`;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Type label for the non-media file table: uppercase filename extension
|
|
311
|
+
* first, then the content type's subtype, then a bare "FILE" fallback.
|
|
312
|
+
*/
|
|
313
|
+
function fileTypeLabel(name, contentType) {
|
|
314
|
+
const dot = name.lastIndexOf(".");
|
|
315
|
+
if (dot !== -1 && dot < name.length - 1)
|
|
316
|
+
return name.slice(dot + 1).toUpperCase();
|
|
317
|
+
if (contentType) {
|
|
318
|
+
const slash = contentType.indexOf("/");
|
|
319
|
+
if (slash !== -1 && slash < contentType.length - 1) {
|
|
320
|
+
return contentType.slice(slash + 1).toUpperCase();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return "FILE";
|
|
324
|
+
}
|
|
325
|
+
/** Escape `|` so a filename can't break out of a markdown table cell. */
|
|
326
|
+
function escapeTableCell(s) {
|
|
327
|
+
return s.replace(/\|/g, "\\|");
|
|
328
|
+
}
|
|
288
329
|
/** Resolved pixel width for an image site, or `null` meaning "omit the width
|
|
289
330
|
* attribute". `"auto"` defers to the caller's per-item heuristic (`autoPx`);
|
|
290
331
|
* `"full"` always omits; a number always wins. */
|
|
@@ -485,6 +526,10 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
485
526
|
const consumedByPair = new Set();
|
|
486
527
|
let inlinedImages = 0;
|
|
487
528
|
const overflowImages = [];
|
|
529
|
+
// Non-media attachments (PDFs, archives, text/data files) never inline and
|
|
530
|
+
// never overflow into the <details> link list — they render as one table
|
|
531
|
+
// after the image/video section instead (issue #946).
|
|
532
|
+
const fileItems = [];
|
|
488
533
|
for (let idx = 0; idx < sorted.length; idx++) {
|
|
489
534
|
if (consumedByPair.has(idx))
|
|
490
535
|
continue;
|
|
@@ -510,8 +555,21 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
510
555
|
const stable = item.url;
|
|
511
556
|
const src = item.embedUrl ?? item.url;
|
|
512
557
|
const link = item.pageUrl ?? stable; // click-through: file page when known, else raw
|
|
513
|
-
|
|
514
|
-
|
|
558
|
+
// "application/octet-stream" is the server's generic fallback for an
|
|
559
|
+
// object stored without an explicit content type — not a real signal —
|
|
560
|
+
// so it defers to the filename the same as an absent `contentType`.
|
|
561
|
+
const effectiveType = item.contentType && item.contentType !== "application/octet-stream"
|
|
562
|
+
? item.contentType
|
|
563
|
+
: inferContentType(name);
|
|
564
|
+
const isImage = Boolean(src) && effectiveType.startsWith("image/");
|
|
565
|
+
const isPosterVideo = Boolean(item.posterUrl) && effectiveType.startsWith("video/");
|
|
566
|
+
if (!effectiveType.startsWith("image/") && !effectiveType.startsWith("video/")) {
|
|
567
|
+
// Neither an image nor a video by content type — a non-media
|
|
568
|
+
// attachment goes into the file table, never the bullet list or
|
|
569
|
+
// overflow details.
|
|
570
|
+
fileItems.push(item);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
515
573
|
const inlines = isImage || isPosterVideo;
|
|
516
574
|
if (inlines && inlinedImages >= options.maxInlineImages) {
|
|
517
575
|
// Cap hit — defer to the collapsed overflow list below rather than
|
|
@@ -604,6 +662,23 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
604
662
|
lines.push(`- ${name}${cap ? ` · ${cap}` : ""}`);
|
|
605
663
|
}
|
|
606
664
|
}
|
|
665
|
+
if (fileItems.length > 0) {
|
|
666
|
+
lines.push("| File | Type | Size |", "| --- | --- | --- |");
|
|
667
|
+
for (const item of fileItems) {
|
|
668
|
+
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
669
|
+
const escapedName = escapeTableCell(name);
|
|
670
|
+
let fileCell = item.url ? `[${escapedName}](${item.url})` : escapedName;
|
|
671
|
+
if (item.pageUrl)
|
|
672
|
+
fileCell += ` · [page](${item.pageUrl})`;
|
|
673
|
+
const cap = formatMetaCaption(item.meta, options, "markdown");
|
|
674
|
+
if (cap)
|
|
675
|
+
fileCell += ` · ${cap}`;
|
|
676
|
+
const typeLabel = fileTypeLabel(name, item.contentType);
|
|
677
|
+
const sizeLabel = formatBytes(item.size);
|
|
678
|
+
lines.push(`| ${fileCell} | ${typeLabel} | ${sizeLabel} |`);
|
|
679
|
+
}
|
|
680
|
+
lines.push("");
|
|
681
|
+
}
|
|
607
682
|
if (overflowImages.length > 0) {
|
|
608
683
|
const n = overflowImages.length;
|
|
609
684
|
lines.push(`<details><summary>${n} more attachment${n === 1 ? "" : "s"}</summary>`, "");
|
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" },
|
package/dist/mcp/tools.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
},
|