@buildinternet/uploads 0.5.0 → 0.7.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/dist/mcp/tools.js CHANGED
@@ -88,6 +88,23 @@ const frameProps = {
88
88
  function ghTargetFromArgs(args, run) {
89
89
  return makeGhTarget(optPosInt(args, "pr"), optPosInt(args, "issue"), optString(args, "repo"), run);
90
90
  }
91
+ function galleryId(args) {
92
+ const id = optString(args, "galleryId");
93
+ if (!id)
94
+ usage("galleryId is required");
95
+ return id;
96
+ }
97
+ function galleryReference(args) {
98
+ const provider = optString(args, "provider");
99
+ const coordinate = optString(args, "coordinate");
100
+ if (!provider)
101
+ usage("provider is required");
102
+ if (!coordinate)
103
+ usage("coordinate is required");
104
+ if (provider !== "github")
105
+ usage("provider must be github");
106
+ return { provider, coordinate };
107
+ }
91
108
  const workspaceProp = {
92
109
  type: "string",
93
110
  description: "Override the workspace for this call (like the CLI's --workspace flag).",
@@ -136,6 +153,127 @@ export function createUploadsMcpTools(opts) {
136
153
  return { comment, commentError };
137
154
  };
138
155
  return [
156
+ {
157
+ name: "gallery_create",
158
+ description: "Create a public ordered media gallery in the workspace. The returned canonical URL is safe to give users, but anyone who knows it can view the gallery and its media.",
159
+ inputSchema: {
160
+ type: "object",
161
+ properties: {
162
+ title: { type: "string", description: "Gallery title (1–120 characters)." },
163
+ description: { type: "string", description: "Optional public gallery description." },
164
+ workspace: workspaceProp,
165
+ },
166
+ required: ["title"],
167
+ additionalProperties: false,
168
+ },
169
+ async handler(args) {
170
+ const title = optString(args, "title");
171
+ if (!title)
172
+ usage("title is required");
173
+ const { client } = clientFor(args);
174
+ return client.createGallery({ title, description: optString(args, "description") });
175
+ },
176
+ },
177
+ {
178
+ name: "gallery_get",
179
+ description: "Get a workspace-owned gallery, including ordered media and its canonical public URL. Gallery media is public to anyone with the URL.",
180
+ inputSchema: {
181
+ type: "object",
182
+ properties: {
183
+ galleryId: { type: "string", description: "Opaque gallery ID." },
184
+ workspace: workspaceProp,
185
+ },
186
+ required: ["galleryId"],
187
+ additionalProperties: false,
188
+ },
189
+ async handler(args) {
190
+ const { client } = clientFor(args);
191
+ return client.getGallery(galleryId(args));
192
+ },
193
+ },
194
+ {
195
+ name: "gallery_add",
196
+ description: "Add one existing, publicly served workspace object to a gallery. Reads the latest gallery version before writing, so the optimistic API version is handled safely. Does not upload or delete the object.",
197
+ inputSchema: {
198
+ type: "object",
199
+ properties: {
200
+ galleryId: { type: "string", description: "Opaque gallery ID." },
201
+ objectKey: { type: "string", description: "Existing public object key to add." },
202
+ caption: { type: "string", description: "Optional public caption." },
203
+ altText: { type: "string", description: "Optional public alt text." },
204
+ workspace: workspaceProp,
205
+ },
206
+ required: ["galleryId", "objectKey"],
207
+ additionalProperties: false,
208
+ },
209
+ async handler(args) {
210
+ const objectKey = optString(args, "objectKey");
211
+ if (!objectKey)
212
+ usage("objectKey is required");
213
+ const { client } = clientFor(args);
214
+ const id = galleryId(args);
215
+ const current = await client.getGallery(id);
216
+ return client.addGalleryItem(id, objectKey, {
217
+ expectedVersion: current.version,
218
+ caption: optString(args, "caption"),
219
+ altText: optString(args, "altText"),
220
+ });
221
+ },
222
+ },
223
+ {
224
+ name: "gallery_link",
225
+ description: "Link a gallery to an external reference. References use provider-neutral fields; github currently accepts owner/repo#number or a strict GitHub issue/PR URL. No GitHub credentials or API calls are used.",
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ galleryId: { type: "string", description: "Opaque gallery ID." },
230
+ provider: { type: "string", description: "External provider (currently github)." },
231
+ coordinate: {
232
+ type: "string",
233
+ description: "Provider-native external reference coordinate.",
234
+ },
235
+ workspace: workspaceProp,
236
+ },
237
+ required: ["galleryId", "provider", "coordinate"],
238
+ additionalProperties: false,
239
+ },
240
+ async handler(args) {
241
+ const { client } = clientFor(args);
242
+ const id = galleryId(args);
243
+ const current = await client.getGallery(id);
244
+ return client.linkGalleryExternalReference(id, {
245
+ expectedVersion: current.version,
246
+ ...galleryReference(args),
247
+ });
248
+ },
249
+ },
250
+ {
251
+ name: "gallery_find_by_reference",
252
+ description: "Find workspace galleries linked to an external reference. Returns gallery summaries and canonical public URLs without contacting the provider.",
253
+ inputSchema: {
254
+ type: "object",
255
+ properties: {
256
+ provider: { type: "string", description: "External provider (currently github)." },
257
+ coordinate: {
258
+ type: "string",
259
+ description: "Provider-native external reference coordinate.",
260
+ },
261
+ limit: { type: "number", description: "Page size (default 50, max 100)." },
262
+ cursor: { type: "string", description: "Pagination cursor from a previous response." },
263
+ workspace: workspaceProp,
264
+ },
265
+ required: ["provider", "coordinate"],
266
+ additionalProperties: false,
267
+ },
268
+ async handler(args) {
269
+ const { client } = clientFor(args);
270
+ return client.findGalleriesByReference({
271
+ ...galleryReference(args),
272
+ limit: optPosInt(args, "limit"),
273
+ cursor: optString(args, "cursor"),
274
+ });
275
+ },
276
+ },
139
277
  {
140
278
  name: "put",
141
279
  description: "Upload a file to uploads.sh and get a public URL plus GitHub-ready embed markdown (the returned `markdown` is ready to paste into a PR or issue). Pass `file` (a local path) or `contentBase64` + `filename` for in-memory content; with `pr`/`issue` the key is stable (same filename → same URL) and `comment` syncs the managed attachments comment. All uploads are public; pr/issue keys are predictable and remain public for private/internal GitHub repositories, so upload only non-sensitive media.",
@@ -0,0 +1 @@
1
+ export declare function packageVersion(): string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Resolve the published package version for CLI headers, --version, and
3
+ * update checks. Reads package.json once per process.
4
+ */
5
+ import { createRequire } from "node:module";
6
+ let cachedVersion;
7
+ export function packageVersion() {
8
+ if (cachedVersion)
9
+ return cachedVersion;
10
+ try {
11
+ const require = createRequire(import.meta.url);
12
+ const pkg = require("../package.json");
13
+ cachedVersion = pkg.version ?? "0.0.0";
14
+ }
15
+ catch {
16
+ cachedVersion = "0.0.0";
17
+ }
18
+ return cachedVersion;
19
+ }
@@ -1,18 +1,4 @@
1
- import { createRequire } from "node:module";
2
- let cachedVersion;
3
- function packageVersion() {
4
- if (cachedVersion)
5
- return cachedVersion;
6
- try {
7
- const require = createRequire(import.meta.url);
8
- const pkg = require("../package.json");
9
- cachedVersion = pkg.version ?? "0.0.0";
10
- }
11
- catch {
12
- cachedVersion = "0.0.0";
13
- }
14
- return cachedVersion;
15
- }
1
+ import { packageVersion } from "./package-version.js";
16
2
  export function buildCliProvenance(opts) {
17
3
  const provenance = {
18
4
  client: opts.client ?? "uploads-cli",
@@ -0,0 +1,30 @@
1
+ export declare const PACKAGE_NAME = "@buildinternet/uploads";
2
+ export interface UpdateCache {
3
+ checkedAt: number;
4
+ latest: string;
5
+ current: string;
6
+ }
7
+ export interface UpdateCheckOptions {
8
+ quiet?: boolean;
9
+ /** mcp is always skipped (stdio purity). */
10
+ command?: string;
11
+ currentVersion?: string;
12
+ cachePath?: string;
13
+ now?: number;
14
+ /** Default 24h. Pass 0 in tests to force a network check. */
15
+ ttlMs?: number;
16
+ timeoutMs?: number;
17
+ fetchImpl?: typeof fetch;
18
+ write?: (text: string) => void;
19
+ }
20
+ /** Parse major.minor.patch (ignores pre-release). */
21
+ export declare function parseSemver(version: string): [number, number, number] | null;
22
+ /** True when `latest` is strictly greater than `current`. */
23
+ export declare function isNewerVersion(latest: string, current: string): boolean;
24
+ export declare function readUpdateCache(path: string): UpdateCache | undefined;
25
+ export declare function writeUpdateCache(path: string, cache: UpdateCache): void;
26
+ /**
27
+ * If a newer published version is known (or can be fetched within the timeout),
28
+ * write a one-line stderr hint. Always resolves; never throws.
29
+ */
30
+ export declare function maybeHintUpdate(opts?: UpdateCheckOptions): Promise<void>;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Optional npm update notifier for the CLI.
3
+ *
4
+ * Checks the registry at most once per day, never throws, never blocks longer
5
+ * than a short timeout, and writes only to stderr. Silence with --quiet,
6
+ * UPLOADS_NO_UPDATE=1, or NO_UPDATE_NOTIFIER=1.
7
+ */
8
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { homedir } from "node:os";
11
+ import { packageVersion } from "./package-version.js";
12
+ export const PACKAGE_NAME = "@buildinternet/uploads";
13
+ const REGISTRY_LATEST = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
14
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
15
+ const DEFAULT_TIMEOUT_MS = 1500;
16
+ function truthyEnv(name) {
17
+ const v = process.env[name];
18
+ if (!v)
19
+ return false;
20
+ const lower = v.toLowerCase();
21
+ return lower !== "0" && lower !== "false" && lower !== "no";
22
+ }
23
+ function defaultCachePath() {
24
+ const base = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
25
+ return join(base, "uploads", "version-check.json");
26
+ }
27
+ /** Parse major.minor.patch (ignores pre-release). */
28
+ export function parseSemver(version) {
29
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
30
+ if (!m)
31
+ return null;
32
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
33
+ }
34
+ /** True when `latest` is strictly greater than `current`. */
35
+ export function isNewerVersion(latest, current) {
36
+ const a = parseSemver(latest);
37
+ const b = parseSemver(current);
38
+ if (!a || !b)
39
+ return false;
40
+ for (let i = 0; i < 3; i++) {
41
+ if (a[i] > b[i])
42
+ return true;
43
+ if (a[i] < b[i])
44
+ return false;
45
+ }
46
+ return false;
47
+ }
48
+ export function readUpdateCache(path) {
49
+ try {
50
+ const raw = JSON.parse(readFileSync(path, "utf8"));
51
+ if (typeof raw.checkedAt !== "number" ||
52
+ typeof raw.latest !== "string" ||
53
+ typeof raw.current !== "string") {
54
+ return undefined;
55
+ }
56
+ return { checkedAt: raw.checkedAt, latest: raw.latest, current: raw.current };
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ }
62
+ export function writeUpdateCache(path, cache) {
63
+ try {
64
+ mkdirSync(dirname(path), { recursive: true });
65
+ writeFileSync(path, JSON.stringify(cache) + "\n", { mode: 0o600 });
66
+ }
67
+ catch {
68
+ // Best-effort — never fail the CLI for a cache write error.
69
+ }
70
+ }
71
+ /**
72
+ * If a newer published version is known (or can be fetched within the timeout),
73
+ * write a one-line stderr hint. Always resolves; never throws.
74
+ */
75
+ export async function maybeHintUpdate(opts = {}) {
76
+ try {
77
+ if (opts.quiet || opts.command === "mcp")
78
+ return;
79
+ if (truthyEnv("UPLOADS_NO_UPDATE") || truthyEnv("NO_UPDATE_NOTIFIER"))
80
+ return;
81
+ const current = opts.currentVersion ?? packageVersion();
82
+ const cachePath = opts.cachePath ?? defaultCachePath();
83
+ const now = opts.now ?? Date.now();
84
+ const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
85
+ const write = opts.write ?? ((text) => process.stderr.write(text));
86
+ const cached = readUpdateCache(cachePath);
87
+ let latest;
88
+ if (cached && now - cached.checkedAt < ttlMs && cached.current === current) {
89
+ latest = cached.latest;
90
+ }
91
+ else {
92
+ const fetched = await fetchLatestVersion(opts.fetchImpl, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
93
+ if (fetched) {
94
+ latest = fetched;
95
+ writeUpdateCache(cachePath, { checkedAt: now, latest: fetched, current });
96
+ }
97
+ else if (cached?.current === current) {
98
+ latest = cached.latest; // stale cache if network failed
99
+ }
100
+ }
101
+ if (latest && isNewerVersion(latest, current)) {
102
+ write(`hint: ${PACKAGE_NAME}@${latest} is available (you have ${current}). Update: npm i -g ${PACKAGE_NAME}\n`);
103
+ }
104
+ }
105
+ catch {
106
+ // Never surface update-check failures.
107
+ }
108
+ }
109
+ async function fetchLatestVersion(fetchImpl, timeoutMs) {
110
+ const fetchFn = fetchImpl ?? globalThis.fetch;
111
+ if (typeof fetchFn !== "function")
112
+ return undefined;
113
+ const controller = new AbortController();
114
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
115
+ try {
116
+ const res = await fetchFn(REGISTRY_LATEST, {
117
+ signal: controller.signal,
118
+ headers: {
119
+ accept: "application/json",
120
+ "user-agent": `${PACKAGE_NAME}/${packageVersion()} (update-check)`,
121
+ },
122
+ });
123
+ if (!res.ok)
124
+ return undefined;
125
+ const body = (await res.json());
126
+ return typeof body.version === "string" ? body.version : undefined;
127
+ }
128
+ catch {
129
+ return undefined;
130
+ }
131
+ finally {
132
+ clearTimeout(timer);
133
+ }
134
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
5
5
  "type": "module",
6
6
  "sideEffects": false,