@buildinternet/uploads 0.13.1 → 0.15.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/cli-catalog.js +7 -0
- package/dist/cli.js +6 -2
- package/dist/client.d.ts +66 -0
- package/dist/client.js +37 -0
- package/dist/commands/screenshot.js +39 -9
- package/dist/commands.d.ts +53 -4
- package/dist/commands.js +387 -33
- package/dist/github-gh.d.ts +28 -1
- package/dist/github-gh.js +89 -6
- package/dist/github.d.ts +35 -1
- package/dist/github.js +88 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/tools.js +15 -15
- package/dist/metadata.d.ts +8 -0
- package/dist/metadata.js +10 -0
- package/package.json +1 -1
package/dist/github-gh.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { UsageError } from "./cli-args.js";
|
|
3
|
-
import { ATTACHMENTS_MARKER, isValidRepo, parseRepoFromRemoteUrl, } from "./github.js";
|
|
3
|
+
import { ATTACHMENTS_MARKER, ghMetadataFromTarget, isValidRepo, parseRepoFromRemoteUrl, } from "./github.js";
|
|
4
|
+
import { META_VALUE_MAX, isMetaValueSafe } from "./metadata.js";
|
|
4
5
|
export const execRunner = (cmd, args, input) => execFileSync(cmd, args, { encoding: "utf8", input, stdio: ["pipe", "pipe", "pipe"] });
|
|
5
6
|
/**
|
|
6
7
|
* Resolve "owner/name". Order: explicit --repo (validated) → `gh repo view`
|
|
@@ -67,6 +68,20 @@ export function resolveCurrentPullRequest(repo, run = execRunner) {
|
|
|
67
68
|
}
|
|
68
69
|
throw new UsageError("could not infer a pull request for the current branch — pass --pr <num> or --issue <num>");
|
|
69
70
|
}
|
|
71
|
+
/** Resolve the current git branch (`--branch` with no value). Throws UsageError on detached HEAD or outside a git repo. */
|
|
72
|
+
export function resolveCurrentBranch(run = execRunner) {
|
|
73
|
+
let branch;
|
|
74
|
+
try {
|
|
75
|
+
branch = run("git", ["rev-parse", "--abbrev-ref", "HEAD"]).trim();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new UsageError("could not determine the current git branch — pass --branch <name> or run inside a git repo");
|
|
79
|
+
}
|
|
80
|
+
if (branch === "" || branch === "HEAD") {
|
|
81
|
+
throw new UsageError("could not determine the current branch (detached HEAD) — pass --branch <name>");
|
|
82
|
+
}
|
|
83
|
+
return branch;
|
|
84
|
+
}
|
|
70
85
|
/**
|
|
71
86
|
* Classify a bare PR/issue number via the GitHub API so the default `put`
|
|
72
87
|
* path can stamp the right `gh.kind`. Returns undefined on any failure (gh
|
|
@@ -91,22 +106,90 @@ export function classifyGhNumber(repo, num, run = execRunner) {
|
|
|
91
106
|
}
|
|
92
107
|
return undefined;
|
|
93
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Best-effort PR/issue title lookup via local `gh`. Returns undefined on any
|
|
111
|
+
* failure (gh missing, unauthenticated, network, 404) — mirrors
|
|
112
|
+
* `resolveCurrentPullRequest`/`classifyGhNumber`'s degrade-don't-throw
|
|
113
|
+
* pattern. A title is a nice-to-have annotation, never a blocker: callers
|
|
114
|
+
* must never let this failure abort an upload.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveGhTitle(target, run = execRunner) {
|
|
117
|
+
try {
|
|
118
|
+
const out = run("gh", [
|
|
119
|
+
target.kind === "pull" ? "pr" : "issue",
|
|
120
|
+
"view",
|
|
121
|
+
String(target.num),
|
|
122
|
+
"--repo",
|
|
123
|
+
target.repo,
|
|
124
|
+
"--json",
|
|
125
|
+
"title",
|
|
126
|
+
"--jq",
|
|
127
|
+
".title",
|
|
128
|
+
]).trim();
|
|
129
|
+
return out.length > 0 ? out : undefined;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// gh missing / unauthenticated / not found / network — caller skips
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* `ghMetadataFromTarget`'s 4 pairs, plus a best-effort `gh.title` (issue #267)
|
|
138
|
+
* when `resolveGhTitle` yields one that also satisfies the metadata-value
|
|
139
|
+
* rule every other pair follows (1-512 printable ASCII — `metadata.ts`'s
|
|
140
|
+
* `META_VALUE_MAX`/`isMetaValueSafe`). Truncated to `META_VALUE_MAX` first;
|
|
141
|
+
* a title left empty or unsafe by truncation (e.g. non-ASCII — real titles
|
|
142
|
+
* often contain emoji or curly quotes) is silently omitted rather than
|
|
143
|
+
* sanitized, matching `resolveGhTitle`'s own "degrade, don't fail the
|
|
144
|
+
* upload" contract.
|
|
145
|
+
*/
|
|
146
|
+
export function ghMetadataFromTargetWithTitle(target, run = execRunner) {
|
|
147
|
+
const base = ghMetadataFromTarget(target);
|
|
148
|
+
const title = resolveGhTitle(target, run);
|
|
149
|
+
if (title === undefined)
|
|
150
|
+
return base;
|
|
151
|
+
const truncated = title.length > META_VALUE_MAX ? title.slice(0, META_VALUE_MAX) : title;
|
|
152
|
+
return isMetaValueSafe(truncated) ? { ...base, "gh.title": truncated } : base;
|
|
153
|
+
}
|
|
94
154
|
/**
|
|
95
155
|
* PR comments live on the issues endpoint, so one path covers PRs and issues.
|
|
96
|
-
*
|
|
156
|
+
* `--paginate` follows Link headers and merges every page into one array, so the
|
|
157
|
+
* marker comment is found even on threads past 100 comments.
|
|
158
|
+
*
|
|
159
|
+
* Hunts for `marker` (the namespaced, per-workspace marker) first; when none
|
|
160
|
+
* is found, falls back to a comment carrying the shared legacy
|
|
161
|
+
* `ATTACHMENTS_MARKER` (pre-4b, unnamespaced) so it can be adopted and
|
|
162
|
+
* migrated in place. When `marker` IS the legacy marker (no workspace to
|
|
163
|
+
* namespace with) this collapses to a single hunt, unchanged from pre-4b
|
|
164
|
+
* behavior.
|
|
97
165
|
*/
|
|
98
|
-
function findManagedComment(target, run) {
|
|
99
|
-
const raw = run("gh", [
|
|
166
|
+
function findManagedComment(target, run, marker) {
|
|
167
|
+
const raw = run("gh", [
|
|
168
|
+
"api",
|
|
169
|
+
`repos/${target.repo}/issues/${target.num}/comments?per_page=100`,
|
|
170
|
+
"--paginate",
|
|
171
|
+
]);
|
|
100
172
|
const comments = JSON.parse(raw);
|
|
173
|
+
const namespacedHit = comments.find((c) => typeof c.body === "string" && c.body.includes(marker));
|
|
174
|
+
if (namespacedHit)
|
|
175
|
+
return namespacedHit;
|
|
176
|
+
if (marker === ATTACHMENTS_MARKER)
|
|
177
|
+
return undefined;
|
|
101
178
|
return comments.find((c) => typeof c.body === "string" && c.body.includes(ATTACHMENTS_MARKER));
|
|
102
179
|
}
|
|
103
180
|
/**
|
|
104
181
|
* Create the managed attachments comment, or edit it in place if it already
|
|
105
182
|
* exists. Never touches any other comment. Body is passed via stdin
|
|
106
183
|
* (`-F body=@-`) so it is never shell-interpolated.
|
|
184
|
+
*
|
|
185
|
+
* `marker` identifies which comment to hunt for (see `findManagedComment`);
|
|
186
|
+
* `body` is expected to already carry that same marker as its first line
|
|
187
|
+
* (built via `attachmentsCommentBody(items, galleries, marker)`), so patching
|
|
188
|
+
* an adopted legacy comment migrates it to the namespaced marker in place.
|
|
189
|
+
* Defaults to the shared legacy marker for backward compatibility.
|
|
107
190
|
*/
|
|
108
|
-
export function upsertAttachmentsComment(target, body, run = execRunner) {
|
|
109
|
-
const existing = findManagedComment(target, run);
|
|
191
|
+
export function upsertAttachmentsComment(target, body, run = execRunner, marker = ATTACHMENTS_MARKER) {
|
|
192
|
+
const existing = findManagedComment(target, run, marker);
|
|
110
193
|
if (existing) {
|
|
111
194
|
run("gh", [
|
|
112
195
|
"api",
|
package/dist/github.d.ts
CHANGED
|
@@ -22,6 +22,26 @@ export declare function ghKeyPrefix(target: GhTarget): string;
|
|
|
22
22
|
* (unlike buildScreenshotKey).
|
|
23
23
|
*/
|
|
24
24
|
export declare function ghAttachmentKey(target: GhTarget, filename: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Branch-staged attachment key prefix: `gh/<owner>/<repo>/branch/<branch>/`.
|
|
27
|
+
* Pre-PR staging (Phase 1 of branch-staged attachments) — no PR/issue number
|
|
28
|
+
* exists yet. `branch` goes through `sanitizeKeySegment` like every other key
|
|
29
|
+
* segment, so e.g. `feature/x` becomes `feature-x`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ghBranchKeyPrefix(repo: string, branch: string): string;
|
|
32
|
+
/** Branch-staged attachment key: `ghBranchKeyPrefix` + the sanitized filename. */
|
|
33
|
+
export declare function ghBranchAttachmentKey(repo: string, branch: string, filename: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* `gh.*` metadata for a branch-staged attach: `gh.repo`, `gh.kind=branch`,
|
|
36
|
+
* `gh.branch` (lowercased), and `gh.staged-at` (ISO 8601 UTC, no fractional
|
|
37
|
+
* seconds). No `gh.number`/`gh.ref`/`gh.title` — there is no PR/issue yet.
|
|
38
|
+
* Throws `UsageError` when the lowercased branch name fails the metadata
|
|
39
|
+
* value rule (printable ASCII, 1-512 chars) — unlike `gh.title`'s best-effort
|
|
40
|
+
* degrade, a branch name the caller explicitly chose (via `--branch` or the
|
|
41
|
+
* current git branch) is worth failing loudly on rather than silently
|
|
42
|
+
* dropping from the metadata set.
|
|
43
|
+
*/
|
|
44
|
+
export declare function ghMetadataForBranch(repo: string, branch: string, now?: Date): Record<string, string>;
|
|
25
45
|
/**
|
|
26
46
|
* The four `gh.*` queryable-metadata pairs `uploads attach` writes
|
|
27
47
|
* automatically (`.context/2026-07-13-file-metadata-design.md`). `gh.kind`
|
|
@@ -36,11 +56,25 @@ export declare function ghAttachmentKey(target: GhTarget, filename: string): str
|
|
|
36
56
|
export declare function ghMetadataFromTarget(target: GhTarget): Record<string, string>;
|
|
37
57
|
/** Hidden marker identifying the one comment this CLI manages. Never change it — existing comments are found by exact match. */
|
|
38
58
|
export declare const ATTACHMENTS_MARKER = "<!-- uploads.sh:attachments -->";
|
|
59
|
+
/**
|
|
60
|
+
* Per-workspace marker (`<!-- uploads.sh:attachments ws=<workspace> -->`) so
|
|
61
|
+
* two workspaces managing the same repo don't clobber each other's comment.
|
|
62
|
+
* Falls back to the shared legacy marker when `workspace` is missing or does
|
|
63
|
+
* not look like a safe slug — degrade, don't guess or risk breaking the
|
|
64
|
+
* comment's HTML.
|
|
65
|
+
*/
|
|
66
|
+
export declare function attachmentsMarker(workspace?: string): string;
|
|
67
|
+
/** Max attachments embedded as inline `<img>` tags before the rest collapse
|
|
68
|
+
* into a `<details>` link list. Keeps very large threads from becoming a wall
|
|
69
|
+
* of images. */
|
|
70
|
+
export declare const MAX_INLINE_ATTACHMENT_IMAGES = 16;
|
|
39
71
|
export interface AttachmentItem {
|
|
40
72
|
key: string;
|
|
41
73
|
url: string | null;
|
|
42
74
|
/** Prefer for `<img src>` on GitHub (Camo-friendly host). Falls back to `url`. */
|
|
43
75
|
embedUrl?: string | null;
|
|
76
|
+
/** Canonical `/f/` file-page URL (server-computed). Preferred click-through target; falls back to `url`. */
|
|
77
|
+
pageUrl?: string | null;
|
|
44
78
|
}
|
|
45
79
|
/** A public gallery linked to the PR or issue whose managed comment is syncing. */
|
|
46
80
|
export interface GalleryCommentItem {
|
|
@@ -70,4 +104,4 @@ export declare function attachmentImageWidth(filename: string): number;
|
|
|
70
104
|
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
71
105
|
* intentionally preserves the legacy attachment-only body byte-for-byte.
|
|
72
106
|
*/
|
|
73
|
-
export declare function attachmentsCommentBody(items: AttachmentItem[], galleries?: GalleryCommentItem[]): string;
|
|
107
|
+
export declare function attachmentsCommentBody(items: AttachmentItem[], galleries?: GalleryCommentItem[], marker?: string): string;
|
package/dist/github.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { inferContentType } from "./embed.js";
|
|
2
2
|
import { sanitizeKeySegment } from "./keys.js";
|
|
3
|
+
import { UsageError } from "./cli-args.js";
|
|
4
|
+
import { isMetaValueSafe } from "./metadata.js";
|
|
3
5
|
const REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
4
6
|
export function isValidRepo(repo) {
|
|
5
7
|
return REPO_RE.test(repo);
|
|
@@ -63,6 +65,42 @@ export function ghKeyPrefix(target) {
|
|
|
63
65
|
export function ghAttachmentKey(target, filename) {
|
|
64
66
|
return `${ghKeyPrefix(target)}${sanitizeKeySegment(filename)}`;
|
|
65
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Branch-staged attachment key prefix: `gh/<owner>/<repo>/branch/<branch>/`.
|
|
70
|
+
* Pre-PR staging (Phase 1 of branch-staged attachments) — no PR/issue number
|
|
71
|
+
* exists yet. `branch` goes through `sanitizeKeySegment` like every other key
|
|
72
|
+
* segment, so e.g. `feature/x` becomes `feature-x`.
|
|
73
|
+
*/
|
|
74
|
+
export function ghBranchKeyPrefix(repo, branch) {
|
|
75
|
+
const [owner, name] = repo.split("/");
|
|
76
|
+
return `gh/${sanitizeKeySegment(owner)}/${sanitizeKeySegment(name)}/branch/${sanitizeKeySegment(branch)}/`;
|
|
77
|
+
}
|
|
78
|
+
/** Branch-staged attachment key: `ghBranchKeyPrefix` + the sanitized filename. */
|
|
79
|
+
export function ghBranchAttachmentKey(repo, branch, filename) {
|
|
80
|
+
return `${ghBranchKeyPrefix(repo, branch)}${sanitizeKeySegment(filename)}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `gh.*` metadata for a branch-staged attach: `gh.repo`, `gh.kind=branch`,
|
|
84
|
+
* `gh.branch` (lowercased), and `gh.staged-at` (ISO 8601 UTC, no fractional
|
|
85
|
+
* seconds). No `gh.number`/`gh.ref`/`gh.title` — there is no PR/issue yet.
|
|
86
|
+
* Throws `UsageError` when the lowercased branch name fails the metadata
|
|
87
|
+
* value rule (printable ASCII, 1-512 chars) — unlike `gh.title`'s best-effort
|
|
88
|
+
* degrade, a branch name the caller explicitly chose (via `--branch` or the
|
|
89
|
+
* current git branch) is worth failing loudly on rather than silently
|
|
90
|
+
* dropping from the metadata set.
|
|
91
|
+
*/
|
|
92
|
+
export function ghMetadataForBranch(repo, branch, now = new Date()) {
|
|
93
|
+
const branchLower = branch.toLowerCase();
|
|
94
|
+
if (!isMetaValueSafe(branchLower)) {
|
|
95
|
+
throw new UsageError(`invalid --branch: "${branch}" must be printable ASCII to stage as gh.branch metadata`);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
"gh.repo": repo.toLowerCase(),
|
|
99
|
+
"gh.kind": "branch",
|
|
100
|
+
"gh.branch": branchLower,
|
|
101
|
+
"gh.staged-at": now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
66
104
|
/**
|
|
67
105
|
* The four `gh.*` queryable-metadata pairs `uploads attach` writes
|
|
68
106
|
* automatically (`.context/2026-07-13-file-metadata-design.md`). `gh.kind`
|
|
@@ -85,6 +123,28 @@ export function ghMetadataFromTarget(target) {
|
|
|
85
123
|
}
|
|
86
124
|
/** Hidden marker identifying the one comment this CLI manages. Never change it — existing comments are found by exact match. */
|
|
87
125
|
export const ATTACHMENTS_MARKER = "<!-- uploads.sh:attachments -->";
|
|
126
|
+
/** Workspace slugs are `[a-z0-9-]`-ish; only markers built from a slug matching
|
|
127
|
+
* this are trusted as a distinct namespace — anything else (empty, unsafe
|
|
128
|
+
* chars) degrades to the shared legacy marker rather than emitting untrusted
|
|
129
|
+
* text into comment HTML. */
|
|
130
|
+
const WORKSPACE_SLUG_RE = /^[a-z0-9-]{1,64}$/;
|
|
131
|
+
/**
|
|
132
|
+
* Per-workspace marker (`<!-- uploads.sh:attachments ws=<workspace> -->`) so
|
|
133
|
+
* two workspaces managing the same repo don't clobber each other's comment.
|
|
134
|
+
* Falls back to the shared legacy marker when `workspace` is missing or does
|
|
135
|
+
* not look like a safe slug — degrade, don't guess or risk breaking the
|
|
136
|
+
* comment's HTML.
|
|
137
|
+
*/
|
|
138
|
+
export function attachmentsMarker(workspace) {
|
|
139
|
+
if (workspace && WORKSPACE_SLUG_RE.test(workspace)) {
|
|
140
|
+
return `<!-- uploads.sh:attachments ws=${workspace} -->`;
|
|
141
|
+
}
|
|
142
|
+
return ATTACHMENTS_MARKER;
|
|
143
|
+
}
|
|
144
|
+
/** Max attachments embedded as inline `<img>` tags before the rest collapse
|
|
145
|
+
* into a `<details>` link list. Keeps very large threads from becoming a wall
|
|
146
|
+
* of images. */
|
|
147
|
+
export const MAX_INLINE_ATTACHMENT_IMAGES = 16;
|
|
88
148
|
/** Default max width for images in the managed attachments comment (HTML img). */
|
|
89
149
|
export const ATTACHMENT_IMAGE_WIDTH_DEFAULT = 400;
|
|
90
150
|
/** Portrait / device mockups — keep phones readable, not full-column. */
|
|
@@ -116,10 +176,10 @@ function escapeHtmlText(s) {
|
|
|
116
176
|
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
117
177
|
* intentionally preserves the legacy attachment-only body byte-for-byte.
|
|
118
178
|
*/
|
|
119
|
-
export function attachmentsCommentBody(items, galleries = []) {
|
|
179
|
+
export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMENTS_MARKER) {
|
|
120
180
|
const sorted = items.toSorted((a, b) => a.key.localeCompare(b.key));
|
|
121
181
|
const sortedGalleries = galleries.toSorted((a, b) => a.title.localeCompare(b.title) || a.url.localeCompare(b.url));
|
|
122
|
-
const lines = [
|
|
182
|
+
const lines = [marker];
|
|
123
183
|
if (sortedGalleries.length > 0) {
|
|
124
184
|
lines.push("### 🖼️ Galleries", "");
|
|
125
185
|
for (const gallery of sortedGalleries) {
|
|
@@ -136,27 +196,48 @@ export function attachmentsCommentBody(items, galleries = []) {
|
|
|
136
196
|
}
|
|
137
197
|
if (sorted.length > 0 || sortedGalleries.length === 0)
|
|
138
198
|
lines.push("### 📎 Attachments", "");
|
|
199
|
+
let inlinedImages = 0;
|
|
200
|
+
const overflowImages = [];
|
|
139
201
|
for (const item of sorted) {
|
|
140
202
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
141
203
|
const stable = item.url;
|
|
142
204
|
const src = item.embedUrl ?? item.url;
|
|
143
|
-
|
|
205
|
+
const link = item.pageUrl ?? stable; // click-through: file page when known, else raw
|
|
206
|
+
const isImage = Boolean(src) && inferContentType(name).startsWith("image/");
|
|
207
|
+
if (isImage && inlinedImages >= MAX_INLINE_ATTACHMENT_IMAGES) {
|
|
208
|
+
// Cap hit — defer to the collapsed overflow list below rather than
|
|
209
|
+
// embedding every remaining image inline.
|
|
210
|
+
overflowImages.push(item);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (isImage) {
|
|
214
|
+
inlinedImages++;
|
|
144
215
|
// Markdown ![]() has no width control — phone frames become full-column giants.
|
|
145
|
-
// img src uses embed host when available (Camo revalidates); click-through
|
|
216
|
+
// img src uses embed host when available (Camo revalidates); click-through prefers the file page.
|
|
146
217
|
const w = attachmentImageWidth(name);
|
|
147
218
|
const alt = escapeHtmlAttr(name);
|
|
148
|
-
const href = escapeHtmlAttr(
|
|
219
|
+
const href = escapeHtmlAttr(link ?? src);
|
|
149
220
|
const imgSrc = escapeHtmlAttr(src);
|
|
150
221
|
lines.push(`<a href="${href}"><img width="${w}" alt="${alt}" src="${imgSrc}"></a>`);
|
|
151
222
|
lines.push("");
|
|
152
223
|
}
|
|
153
|
-
else if (
|
|
154
|
-
lines.push(`- [${name}](${
|
|
224
|
+
else if (link) {
|
|
225
|
+
lines.push(`- [${name}](${link})`);
|
|
155
226
|
}
|
|
156
227
|
else {
|
|
157
228
|
lines.push(`- ${name}`);
|
|
158
229
|
}
|
|
159
230
|
}
|
|
231
|
+
if (overflowImages.length > 0) {
|
|
232
|
+
const n = overflowImages.length;
|
|
233
|
+
lines.push(`<details><summary>${n} more attachment${n === 1 ? "" : "s"}</summary>`, "");
|
|
234
|
+
for (const item of overflowImages) {
|
|
235
|
+
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
236
|
+
const link = item.pageUrl ?? item.url;
|
|
237
|
+
lines.push(link ? `- [${name}](${link})` : `- ${name}`);
|
|
238
|
+
}
|
|
239
|
+
lines.push("", "</details>", "");
|
|
240
|
+
}
|
|
160
241
|
lines.push('<sub>Maintained by <a href="https://uploads.sh">uploads.sh</a> — re-uploading a file with the same name updates it everywhere it is embedded.</sub>');
|
|
161
242
|
return lines.join("\n");
|
|
162
243
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { UploadsError, type UploadsErrorCode } from "./errors.js";
|
|
|
7
7
|
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 GetMetadataResult, type PatchMetadataOptions, } from "./client.js";
|
|
8
8
|
export { buildCliProvenance } from "./provenance.js";
|
|
9
9
|
export { META_KEY_RE, META_VALUE_MAX, META_MAX_KEYS, META_MAX_TOTAL_BYTES, validateMetaEntry, parseMetaPair, parseMetaFlags, } from "./metadata.js";
|
|
10
|
-
export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, isValidRepo, parseRepoFromRemoteUrl, type AttachmentItem, type GhTarget, type GhTargetKind, } from "./github.js";
|
|
10
|
+
export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghBranchAttachmentKey, ghBranchKeyPrefix, ghMetadataForBranch, isValidRepo, parseRepoFromRemoteUrl, type AttachmentItem, type GhTarget, type GhTargetKind, } from "./github.js";
|
|
11
11
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, type OptimizeImageOptions, type OptimizeImageResult, type OptimizeOutputFormat, } from "./optimize.js";
|
|
12
12
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, type FrameFit, type FrameOptions, type FrameResult, } from "./frame.js";
|
|
13
13
|
export { execRunner, resolveRepo, upsertAttachmentsComment, type CommandRunner, } from "./github-gh.js";
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { UploadsError } from "./errors.js";
|
|
|
7
7
|
export { createUploadsClient, } from "./client.js";
|
|
8
8
|
export { buildCliProvenance } from "./provenance.js";
|
|
9
9
|
export { META_KEY_RE, META_VALUE_MAX, META_MAX_KEYS, META_MAX_TOTAL_BYTES, validateMetaEntry, parseMetaPair, parseMetaFlags, } from "./metadata.js";
|
|
10
|
-
export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, isValidRepo, parseRepoFromRemoteUrl, } from "./github.js";
|
|
10
|
+
export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghBranchAttachmentKey, ghBranchKeyPrefix, ghMetadataForBranch, isValidRepo, parseRepoFromRemoteUrl, } from "./github.js";
|
|
11
11
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, } from "./optimize.js";
|
|
12
12
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, } from "./frame.js";
|
|
13
13
|
export { execRunner, resolveRepo, upsertAttachmentsComment, } from "./github-gh.js";
|
package/dist/mcp/tools.js
CHANGED
|
@@ -3,9 +3,9 @@ import { buildDoctorReport, makeGhTarget, syncAttachmentsComment, uploadAttachme
|
|
|
3
3
|
import { resolveFrameId } from "../frame.js";
|
|
4
4
|
import { resolveConfig, resolvePutDefaults, } from "../config.js";
|
|
5
5
|
import { resolvePutPrefix } from "../destinations.js";
|
|
6
|
-
import { ghKeyPrefix
|
|
6
|
+
import { ghKeyPrefix } from "../github.js";
|
|
7
7
|
import { validateMetaMap } from "../metadata.js";
|
|
8
|
-
import { execRunner, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
8
|
+
import { execRunner, ghMetadataFromTargetWithTitle, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
9
9
|
import { METADATA_DESCRIPTION, metadataProp, optPosInt, optString, optStringArray, optStringRecord, usage, } from "./args.js";
|
|
10
10
|
import { batchFailureMessage, ToolBatchError } from "./server.js";
|
|
11
11
|
import { attachmentFromText, buildReportPayload, parseReportType, REPORT_TYPES, submitReport, validateReportMessage, } from "../report.js";
|
|
@@ -123,11 +123,11 @@ export function createUploadsMcpTools(opts) {
|
|
|
123
123
|
});
|
|
124
124
|
return { config, client: clientFactory(config) };
|
|
125
125
|
}
|
|
126
|
-
const syncComment = async (client, target) => {
|
|
126
|
+
const syncComment = async (client, target, workspace) => {
|
|
127
127
|
let comment;
|
|
128
128
|
let commentError;
|
|
129
129
|
try {
|
|
130
|
-
comment = await syncAttachmentsComment(client, target, run);
|
|
130
|
+
comment = await syncAttachmentsComment(client, target, run, workspace);
|
|
131
131
|
}
|
|
132
132
|
catch (err) {
|
|
133
133
|
// Uploads already succeeded; the comment is best-effort by design.
|
|
@@ -402,7 +402,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
402
402
|
catch (err) {
|
|
403
403
|
usage(err instanceof Error ? err.message : String(err));
|
|
404
404
|
}
|
|
405
|
-
const { client } = clientFor(args);
|
|
405
|
+
const { config, client } = clientFor(args);
|
|
406
406
|
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
407
407
|
const frameOpts = mcpFrameOptions(args);
|
|
408
408
|
const optimizeOpts = mcpOptimizeOptions(args, defaults);
|
|
@@ -436,7 +436,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
436
436
|
throw new ToolBatchError(batchFailureMessage(failures), { uploads, failures });
|
|
437
437
|
}
|
|
438
438
|
if (wantComment && target && uploads.length > 0) {
|
|
439
|
-
const { comment, commentError } = await syncComment(client, target);
|
|
439
|
+
const { comment, commentError } = await syncComment(client, target, config.workspace);
|
|
440
440
|
return { uploads, failures, comment, commentError };
|
|
441
441
|
}
|
|
442
442
|
return { uploads, failures };
|
|
@@ -469,7 +469,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
469
469
|
filename: prepared.filename,
|
|
470
470
|
};
|
|
471
471
|
if (wantComment && target) {
|
|
472
|
-
const { comment, commentError } = await syncComment(client, target);
|
|
472
|
+
const { comment, commentError } = await syncComment(client, target, config.workspace);
|
|
473
473
|
return { ...result, markdown, optimize, frame: prepared.frame, comment, commentError };
|
|
474
474
|
}
|
|
475
475
|
return {
|
|
@@ -504,7 +504,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
504
504
|
...(dryRun ? { dryRun: true } : {}),
|
|
505
505
|
};
|
|
506
506
|
if (wantComment && target) {
|
|
507
|
-
const { comment, commentError } = await syncComment(client, target);
|
|
507
|
+
const { comment, commentError } = await syncComment(client, target, config.workspace);
|
|
508
508
|
return { ...flat, comment, commentError };
|
|
509
509
|
}
|
|
510
510
|
return flat;
|
|
@@ -761,7 +761,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
761
761
|
...(dryRun ? { dryRun: true } : {}),
|
|
762
762
|
};
|
|
763
763
|
if (wantComment && target) {
|
|
764
|
-
const { comment, commentError } = await syncComment(client, target);
|
|
764
|
+
const { comment, commentError } = await syncComment(client, target, config.workspace);
|
|
765
765
|
return { ...flat, comment, commentError };
|
|
766
766
|
}
|
|
767
767
|
return flat;
|
|
@@ -821,7 +821,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
821
821
|
const explicitTarget = ghTargetFromArgs(args, run);
|
|
822
822
|
const target = explicitTarget ??
|
|
823
823
|
resolveCurrentPullRequest(resolveRepo(optString(args, "repo"), run), run);
|
|
824
|
-
const { client } = clientFor(args);
|
|
824
|
+
const { config, client } = clientFor(args);
|
|
825
825
|
const contentType = optString(args, "contentType");
|
|
826
826
|
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
827
827
|
const frameOpts = mcpFrameOptions(args);
|
|
@@ -830,10 +830,10 @@ export function createUploadsMcpTools(opts) {
|
|
|
830
830
|
// explicit target pairs always win over a same-named metadata extra
|
|
831
831
|
// (mirrors runAttach in ../commands.js). Validate the merged map (not
|
|
832
832
|
// just the extras) so the 24-key/8KB caps are enforced client-side —
|
|
833
|
-
// extras alone might pass while extras + the
|
|
833
|
+
// extras alone might pass while extras + the gh.* pairs exceed the
|
|
834
834
|
// cap, which would otherwise only be caught server-side after upload.
|
|
835
835
|
const metaExtras = optStringRecord(args, "metadata") ?? {};
|
|
836
|
-
const metadata = { ...metaExtras, ...
|
|
836
|
+
const metadata = { ...metaExtras, ...ghMetadataFromTargetWithTitle(target, run) };
|
|
837
837
|
if (Object.keys(metadata).length > 0)
|
|
838
838
|
validateMetaMap(metadata);
|
|
839
839
|
const { uploads, failures } = await uploadAttachments({
|
|
@@ -856,7 +856,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
856
856
|
}
|
|
857
857
|
if (optBool(args, "noComment"))
|
|
858
858
|
return { target, uploads, failures };
|
|
859
|
-
const { comment, commentError } = await syncComment(client, target);
|
|
859
|
+
const { comment, commentError } = await syncComment(client, target, config.workspace);
|
|
860
860
|
return { target, uploads, failures, comment, commentError };
|
|
861
861
|
},
|
|
862
862
|
},
|
|
@@ -1062,8 +1062,8 @@ export function createUploadsMcpTools(opts) {
|
|
|
1062
1062
|
const target = ghTargetFromArgs(args, run);
|
|
1063
1063
|
if (!target)
|
|
1064
1064
|
usage("comment requires pr or issue");
|
|
1065
|
-
const { client } = clientFor(args);
|
|
1066
|
-
const result = await syncAttachmentsComment(client, target, run);
|
|
1065
|
+
const { config, client } = clientFor(args);
|
|
1066
|
+
const result = await syncAttachmentsComment(client, target, run, config.workspace);
|
|
1067
1067
|
return { ...target, ...result };
|
|
1068
1068
|
},
|
|
1069
1069
|
},
|
package/dist/metadata.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export declare const META_MAX_KEYS = 24;
|
|
|
8
8
|
export declare const META_MAX_TOTAL_BYTES = 8192;
|
|
9
9
|
/** Throws `UsageError` with a readable message if `key`/`value` violate the metadata rules. */
|
|
10
10
|
export declare function validateMetaEntry(key: string, value: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Non-throwing version of `validateMetaEntry`'s value check (length +
|
|
13
|
+
* printable-ASCII), for callers that want to silently drop a value that
|
|
14
|
+
* doesn't fit rather than reject the whole request — e.g. a best-effort
|
|
15
|
+
* `gh.title` derived from a real-world PR/issue title, which may contain
|
|
16
|
+
* unicode (emoji, curly quotes) that the metadata value rule disallows.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isMetaValueSafe(value: string): boolean;
|
|
11
19
|
/**
|
|
12
20
|
* Split `k=v` on the FIRST "=" (so values may themselves contain "="), then
|
|
13
21
|
* validate the pair. Throws `UsageError` on malformed input.
|
package/dist/metadata.js
CHANGED
|
@@ -38,6 +38,16 @@ export function validateMetaEntry(key, value) {
|
|
|
38
38
|
throw new UsageError(`invalid metadata value for key "${key}": must be 1-${META_VALUE_MAX} printable ASCII characters`);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Non-throwing version of `validateMetaEntry`'s value check (length +
|
|
43
|
+
* printable-ASCII), for callers that want to silently drop a value that
|
|
44
|
+
* doesn't fit rather than reject the whole request — e.g. a best-effort
|
|
45
|
+
* `gh.title` derived from a real-world PR/issue title, which may contain
|
|
46
|
+
* unicode (emoji, curly quotes) that the metadata value rule disallows.
|
|
47
|
+
*/
|
|
48
|
+
export function isMetaValueSafe(value) {
|
|
49
|
+
return value.length >= 1 && value.length <= META_VALUE_MAX && VALUE_SAFE_RE.test(value);
|
|
50
|
+
}
|
|
41
51
|
/**
|
|
42
52
|
* Split `k=v` on the FIRST "=" (so values may themselves contain "="), then
|
|
43
53
|
* validate the pair. Throws `UsageError` on malformed input.
|