@buildinternet/uploads 0.13.0 → 0.14.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/client.d.ts +30 -5
- package/dist/client.js +14 -1
- package/dist/commands/screenshot.js +7 -6
- package/dist/commands.d.ts +13 -4
- package/dist/commands.js +38 -17
- package/dist/github-gh.d.ts +19 -0
- package/dist/github-gh.js +54 -3
- package/dist/mcp/tools.js +4 -4
- package/dist/metadata.d.ts +8 -0
- package/dist/metadata.js +10 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { UploadsClientConfig } from "./config.js";
|
|
2
|
+
/** Scoped-operator-token permission scope. */
|
|
3
|
+
export type TokenScope = "files:read" | "files:write" | "files:delete" | "operator:read" | "operator:write" | "workspace:invite" | "workspace:manage";
|
|
2
4
|
/** Allowlisted object provenance (maps to X-Uploads-Meta-* on put). */
|
|
3
5
|
export type ProvenanceInput = {
|
|
4
6
|
client?: string;
|
|
@@ -168,6 +170,17 @@ export interface FindGalleriesByReferenceOptions {
|
|
|
168
170
|
limit?: number;
|
|
169
171
|
cursor?: string;
|
|
170
172
|
}
|
|
173
|
+
/** Reasons the bot did not post; the CLI treats any of them as "fall back to gh". */
|
|
174
|
+
export type GithubCommentDeclineReason = "app_unconfigured" | "not_installed" | "forbidden" | "unavailable";
|
|
175
|
+
export type GithubCommentResult = {
|
|
176
|
+
posted: true;
|
|
177
|
+
action: "created" | "updated" | "skipped";
|
|
178
|
+
count: number;
|
|
179
|
+
commentUrl?: string;
|
|
180
|
+
} | {
|
|
181
|
+
posted: false;
|
|
182
|
+
reason: GithubCommentDeclineReason;
|
|
183
|
+
};
|
|
171
184
|
export interface HealthResult {
|
|
172
185
|
ok: boolean;
|
|
173
186
|
}
|
|
@@ -212,7 +225,7 @@ export interface EnrollmentExchangeResult {
|
|
|
212
225
|
apiUrl?: string;
|
|
213
226
|
workspace: string;
|
|
214
227
|
token: string;
|
|
215
|
-
scopes?: Array<
|
|
228
|
+
scopes?: Array<TokenScope>;
|
|
216
229
|
expiresAt?: string;
|
|
217
230
|
}
|
|
218
231
|
export interface EnrollmentCreateResult {
|
|
@@ -229,9 +242,16 @@ export declare function createEnrollment(apiUrl: string, adminToken: string, inp
|
|
|
229
242
|
email?: string;
|
|
230
243
|
enrollmentSeconds?: number;
|
|
231
244
|
tokenExpiresInSeconds?: number;
|
|
232
|
-
scopes?: Array<
|
|
245
|
+
scopes?: Array<TokenScope>;
|
|
233
246
|
}): Promise<EnrollmentCreateResult>;
|
|
234
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* OAuth client id for the device flow. Registered server-side as a managed
|
|
249
|
+
* official `oauth_client` row (seeded by apps/auth migration
|
|
250
|
+
* 20260719000000_seed_cli_oauth_client.sql — issue #251): public PKCE client,
|
|
251
|
+
* no secret, device-code grant only. The auth worker's device endpoints
|
|
252
|
+
* validate this id against that table, so operators can disable it from
|
|
253
|
+
* /admin/oauth. The literal must match the seeded row's client_id.
|
|
254
|
+
*/
|
|
235
255
|
export declare const DEVICE_CLIENT_ID = "uploads-cli";
|
|
236
256
|
/**
|
|
237
257
|
* User-Agent for device-flow requests. Stored on the Better Auth session row
|
|
@@ -302,7 +322,7 @@ export declare function createWorkspaceRequest(apiUrl: string, accessToken: stri
|
|
|
302
322
|
export interface MintTokenResult {
|
|
303
323
|
token: string;
|
|
304
324
|
workspace: string;
|
|
305
|
-
scopes: Array<
|
|
325
|
+
scopes: Array<TokenScope>;
|
|
306
326
|
label: string | null;
|
|
307
327
|
expiresAt: string | null;
|
|
308
328
|
}
|
|
@@ -332,7 +352,7 @@ export declare function createWorkspaceInvite(apiUrl: string, accessToken: strin
|
|
|
332
352
|
*/
|
|
333
353
|
export declare function mintWorkspaceToken(apiUrl: string, accessToken: string, input: {
|
|
334
354
|
workspace: string;
|
|
335
|
-
scopes?: Array<
|
|
355
|
+
scopes?: Array<TokenScope>;
|
|
336
356
|
label?: string;
|
|
337
357
|
ttlSeconds?: number;
|
|
338
358
|
}): Promise<MintTokenResult>;
|
|
@@ -387,6 +407,11 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
387
407
|
id: string;
|
|
388
408
|
}>;
|
|
389
409
|
findGalleriesByReference(opts: FindGalleriesByReferenceOptions): Promise<GalleryListResult>;
|
|
410
|
+
upsertGithubComment(opts: {
|
|
411
|
+
repo: string;
|
|
412
|
+
num: number;
|
|
413
|
+
kind: "pull" | "issues";
|
|
414
|
+
}): Promise<GithubCommentResult>;
|
|
390
415
|
health(): Promise<HealthResult>;
|
|
391
416
|
/** Workspace storage / upload counters (+ limits when configured). */
|
|
392
417
|
usage(): Promise<UsageResult>;
|
package/dist/client.js
CHANGED
|
@@ -37,7 +37,14 @@ export function createEnrollment(apiUrl, adminToken, input) {
|
|
|
37
37
|
// D5). Better Auth's `device.code`/`device.token` endpoints take
|
|
38
38
|
// `application/json` bodies, NOT the RFC's form-encoding — the JSON shapes
|
|
39
39
|
// below are what the worker expects.
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* OAuth client id for the device flow. Registered server-side as a managed
|
|
42
|
+
* official `oauth_client` row (seeded by apps/auth migration
|
|
43
|
+
* 20260719000000_seed_cli_oauth_client.sql — issue #251): public PKCE client,
|
|
44
|
+
* no secret, device-code grant only. The auth worker's device endpoints
|
|
45
|
+
* validate this id against that table, so operators can disable it from
|
|
46
|
+
* /admin/oauth. The literal must match the seeded row's client_id.
|
|
47
|
+
*/
|
|
41
48
|
export const DEVICE_CLIENT_ID = "uploads-cli";
|
|
42
49
|
/**
|
|
43
50
|
* User-Agent for device-flow requests. Stored on the Better Auth session row
|
|
@@ -445,6 +452,12 @@ export function createUploadsClient(config) {
|
|
|
445
452
|
params.set("cursor", opts.cursor);
|
|
446
453
|
return request("GET", galleriesBase(config) + "/by-reference?" + params);
|
|
447
454
|
},
|
|
455
|
+
async upsertGithubComment(opts) {
|
|
456
|
+
return request("POST", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/comment`, {
|
|
457
|
+
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
458
|
+
headers: { "Content-Type": "application/json" },
|
|
459
|
+
});
|
|
460
|
+
},
|
|
448
461
|
async health() {
|
|
449
462
|
return request("GET", `${config.apiUrl}/health`, { auth: false });
|
|
450
463
|
},
|
|
@@ -2,12 +2,11 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { flagBool, flagInt, flagString, flagValues, parseCommandArgs, UsageError, } from "../cli-args.js";
|
|
4
4
|
import { writeCommandHelp } from "../cli-style.js";
|
|
5
|
-
import { frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, syncAttachmentsComment, uploadPreparedImage, } from "../commands.js";
|
|
5
|
+
import { frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, } from "../commands.js";
|
|
6
6
|
import { resolvePutDefaults } from "../config.js";
|
|
7
7
|
import { loadDefaultsRaw, resolveScreenshotDefaults } from "../config-file.js";
|
|
8
8
|
import { resolvePutPrefix } from "../destinations.js";
|
|
9
|
-
import {
|
|
10
|
-
import { execRunner } from "../github-gh.js";
|
|
9
|
+
import { execRunner, ghMetadataFromTargetWithTitle } from "../github-gh.js";
|
|
11
10
|
import { parseMetaFlags, validateMetaMap } from "../metadata.js";
|
|
12
11
|
import { writeJson, writeStdout } from "../io.js";
|
|
13
12
|
import { assertHideSelector, captureScreenshot, parseViewport, parseWaitUntil, } from "../screenshot.js";
|
|
@@ -67,7 +66,9 @@ Options:
|
|
|
67
66
|
--keep-exif Keep EXIF/XMP/ICC when optimizing
|
|
68
67
|
--pr <num> Attach to a pull request (stable URL, no hash)
|
|
69
68
|
--issue <num> Attach to an issue
|
|
70
|
-
--comment With --pr/--issue: update the managed attachments comment
|
|
69
|
+
--comment With --pr/--issue: update the managed attachments comment.
|
|
70
|
+
Posts as uploads-sh[bot] when the GitHub App is installed;
|
|
71
|
+
otherwise via local gh.
|
|
71
72
|
--gallery <id> Add the uploaded object to this public gallery
|
|
72
73
|
--meta <k=v> Queryable custom metadata (repeatable)
|
|
73
74
|
--workspace, -w <name> Override workspace
|
|
@@ -214,7 +215,7 @@ captureImpl = captureScreenshot) {
|
|
|
214
215
|
const metaExtras = parseMetaFlags(flagValues(parsed.flags, "--meta"));
|
|
215
216
|
let metadata = metaExtras;
|
|
216
217
|
if (ghTarget) {
|
|
217
|
-
metadata = { ...metaExtras, ...
|
|
218
|
+
metadata = { ...metaExtras, ...ghMetadataFromTargetWithTitle(ghTarget, run) };
|
|
218
219
|
validateMetaMap(metadata);
|
|
219
220
|
}
|
|
220
221
|
else if (Object.keys(metaExtras).length > 0) {
|
|
@@ -296,7 +297,7 @@ captureImpl = captureScreenshot) {
|
|
|
296
297
|
try {
|
|
297
298
|
comment = await syncAttachmentsComment(ctx.client, ghTarget, run);
|
|
298
299
|
if (logHuman)
|
|
299
|
-
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
300
|
+
process.stderr.write(`>> attachments comment ${comment.action}${commentViaSuffix(comment.via)}\n`);
|
|
300
301
|
}
|
|
301
302
|
catch (err) {
|
|
302
303
|
commentError = err instanceof Error ? err.message : String(err);
|
package/dist/commands.d.ts
CHANGED
|
@@ -89,13 +89,22 @@ export declare function frameOptionsFromFlags(flags: CommandFlags["flags"]): {
|
|
|
89
89
|
};
|
|
90
90
|
/**
|
|
91
91
|
* List every attachment under the target's prefix and create/update the
|
|
92
|
-
* managed comment.
|
|
93
|
-
*
|
|
92
|
+
* managed comment. Prefers the server-side bot endpoint (`uploads-sh[bot]`,
|
|
93
|
+
* rendered from this workspace's own data); any failure to post that way —
|
|
94
|
+
* not installed, declined, self-hosted 404, network error — falls through to
|
|
95
|
+
* the local-`gh` path so self-hosters keep working unchanged. Throws on gh
|
|
96
|
+
* failure — callers decide whether that is fatal (`comment` command) or a
|
|
97
|
+
* warning (`put --comment`).
|
|
94
98
|
*/
|
|
95
|
-
export
|
|
99
|
+
export interface AttachmentsCommentResult {
|
|
96
100
|
action: "created" | "updated" | "skipped";
|
|
97
101
|
count: number;
|
|
98
|
-
|
|
102
|
+
/** Who posted the comment: the GitHub App bot, or the local `gh` fallback. */
|
|
103
|
+
via: "bot" | "gh";
|
|
104
|
+
}
|
|
105
|
+
/** Human-mode suffix noting who posted the managed comment. */
|
|
106
|
+
export declare function commentViaSuffix(via: AttachmentsCommentResult["via"]): string;
|
|
107
|
+
export declare function syncAttachmentsComment(client: UploadsClient, target: GhTarget, run: CommandRunner): Promise<AttachmentsCommentResult>;
|
|
99
108
|
export type AttachUploadItem = PutResult & {
|
|
100
109
|
file: string;
|
|
101
110
|
markdown: string;
|
package/dist/commands.js
CHANGED
|
@@ -10,7 +10,7 @@ import { UploadsError } from "./errors.js";
|
|
|
10
10
|
import { writeJson, writeStdout } from "./io.js";
|
|
11
11
|
import { parseMetaFlags, validateMetaMap } from "./metadata.js";
|
|
12
12
|
import { ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, attachmentsCommentBody, normalizeGithubCoordinate, } from "./github.js";
|
|
13
|
-
import { resolveRepo, resolveCurrentPullRequest, classifyGhNumber, execRunner, upsertAttachmentsComment, } from "./github-gh.js";
|
|
13
|
+
import { resolveRepo, resolveCurrentPullRequest, classifyGhNumber, execRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, } from "./github-gh.js";
|
|
14
14
|
import { resolvePutPrefix } from "./destinations.js";
|
|
15
15
|
import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
|
|
16
16
|
import { applyFrame, resolveFrameId } from "./frame.js";
|
|
@@ -89,7 +89,9 @@ Options:
|
|
|
89
89
|
--format human|url|markdown|json
|
|
90
90
|
--pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
|
|
91
91
|
--issue <num> Attach to an issue: key gh/<owner>/<repo>/issues/<num>/<name>
|
|
92
|
-
--comment With --pr/--issue: update one managed comment with
|
|
92
|
+
--comment With --pr/--issue: update one managed comment with
|
|
93
|
+
attachments and linked galleries. Posts as uploads-sh[bot]
|
|
94
|
+
when the GitHub App is installed; otherwise via local gh.
|
|
93
95
|
--gallery <id> Add the uploaded object(s) to this public gallery
|
|
94
96
|
--meta <k=v> Queryable custom metadata (repeatable; value may contain "="): key ^[a-z][a-z0-9._-]{0,63}$, value 1-512 printable ASCII, max 24 pairs
|
|
95
97
|
Re-uploading to an existing key WITH --meta replaces that file's
|
|
@@ -272,12 +274,25 @@ export function frameOptionsFromFlags(flags) {
|
|
|
272
274
|
throw new UsageError("--frame-url requires --frame");
|
|
273
275
|
return { frameId, frameUrl, frameFit };
|
|
274
276
|
}
|
|
275
|
-
/**
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
*/
|
|
277
|
+
/** Human-mode suffix noting who posted the managed comment. */
|
|
278
|
+
export function commentViaSuffix(via) {
|
|
279
|
+
return via === "bot" ? " (uploads-sh[bot])" : " (via gh)";
|
|
280
|
+
}
|
|
280
281
|
export async function syncAttachmentsComment(client, target, run) {
|
|
282
|
+
try {
|
|
283
|
+
const bot = await client.upsertGithubComment({
|
|
284
|
+
repo: target.repo,
|
|
285
|
+
num: target.num,
|
|
286
|
+
kind: target.kind,
|
|
287
|
+
});
|
|
288
|
+
if (bot.posted)
|
|
289
|
+
return { action: bot.action, count: bot.count, via: "bot" };
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// Endpoint absent/unreachable (self-hosted, network, older worker) — fall
|
|
293
|
+
// through to the gh path below.
|
|
294
|
+
}
|
|
295
|
+
// gh fallback: gather from this workspace's own data and post via local `gh`.
|
|
281
296
|
const items = (await client.listAll({ prefix: ghKeyPrefix(target) })).map(({ key, url, embedUrl }) => ({ key, url, embedUrl }));
|
|
282
297
|
const galleries = [];
|
|
283
298
|
let cursor;
|
|
@@ -313,10 +328,14 @@ export async function syncAttachmentsComment(client, target, run) {
|
|
|
313
328
|
}
|
|
314
329
|
}));
|
|
315
330
|
if (items.length === 0 && previewGalleries.length === 0)
|
|
316
|
-
return { action: "skipped", count: 0 };
|
|
331
|
+
return { action: "skipped", count: 0, via: "gh" };
|
|
317
332
|
const body = attachmentsCommentBody(items, previewGalleries);
|
|
318
333
|
const { created } = upsertAttachmentsComment(target, body, run);
|
|
319
|
-
return {
|
|
334
|
+
return {
|
|
335
|
+
action: created ? "created" : "updated",
|
|
336
|
+
count: items.length + previewGalleries.length,
|
|
337
|
+
via: "gh",
|
|
338
|
+
};
|
|
320
339
|
}
|
|
321
340
|
// --- attach ---
|
|
322
341
|
const ATTACH_HELP = `uploads attach <file...> [options]
|
|
@@ -532,9 +551,9 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
532
551
|
// target pairs always win over a same-named --meta extra (documented above).
|
|
533
552
|
// Validate the merged map (not just the extras) so the 24-key/8KB caps are
|
|
534
553
|
// enforced client-side even when extras alone are under the cap but extras
|
|
535
|
-
// + the
|
|
554
|
+
// + the gh.* pairs push the merged map over it.
|
|
536
555
|
const metaExtras = parseMetaFlags(flagValues(parsed.flags, "--meta"));
|
|
537
|
-
const metadata = { ...metaExtras, ...
|
|
556
|
+
const metadata = { ...metaExtras, ...ghMetadataFromTargetWithTitle(target, run) };
|
|
538
557
|
if (Object.keys(metadata).length > 0)
|
|
539
558
|
validateMetaMap(metadata);
|
|
540
559
|
const logHuman = !ctx.quiet && !ctx.json;
|
|
@@ -588,7 +607,7 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
588
607
|
process.stderr.write(`warning: could not upload ${failure.file}: ${failure.error.message}\n`);
|
|
589
608
|
}
|
|
590
609
|
if (!ctx.quiet && comment)
|
|
591
|
-
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
610
|
+
process.stderr.write(`>> attachments comment ${comment.action}${commentViaSuffix(comment.via)}\n`);
|
|
592
611
|
if (!ctx.quiet && uploads.length > 0) {
|
|
593
612
|
const ref = ghMetadataFromTarget(target)["gh.ref"];
|
|
594
613
|
process.stderr.write(`>> find these later: uploads find gh.ref=${ref}\n`);
|
|
@@ -711,7 +730,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
711
730
|
let metadata = userMeta;
|
|
712
731
|
let attachedRef;
|
|
713
732
|
if (ghTarget) {
|
|
714
|
-
const merged = { ...userMeta, ...
|
|
733
|
+
const merged = { ...userMeta, ...ghMetadataFromTargetWithTitle(ghTarget, run) };
|
|
715
734
|
validateMetaMap(merged); // enforce 24-key/8KB caps on the merged map (matches attach)
|
|
716
735
|
metadata = merged;
|
|
717
736
|
attachedRef = merged["gh.ref"];
|
|
@@ -723,7 +742,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
723
742
|
if (autoEnabled) {
|
|
724
743
|
const autoTarget = resolveAutoGhTarget(flagString(parsed.flags, "--repo") ?? defaults.repo, flagString(parsed.flags, "--ref") ?? defaults.ref, run);
|
|
725
744
|
if (autoTarget) {
|
|
726
|
-
const autoMeta =
|
|
745
|
+
const autoMeta = ghMetadataFromTargetWithTitle(autoTarget, run);
|
|
727
746
|
const merged = { ...autoMeta, ...userMeta };
|
|
728
747
|
// Auto resolution must never fail the upload: if merging the gh.* pairs
|
|
729
748
|
// would exceed the metadata caps, drop them and upload with --meta only.
|
|
@@ -798,7 +817,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
798
817
|
try {
|
|
799
818
|
comment = await syncAttachmentsComment(ctx.client, ghTarget, run);
|
|
800
819
|
if (logHuman)
|
|
801
|
-
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
820
|
+
process.stderr.write(`>> attachments comment ${comment.action}${commentViaSuffix(comment.via)}\n`);
|
|
802
821
|
}
|
|
803
822
|
catch (err) {
|
|
804
823
|
commentError = err instanceof Error ? err.message : String(err);
|
|
@@ -1322,7 +1341,8 @@ export async function runDelete(ctx, args, help = false) {
|
|
|
1322
1341
|
const COMMENT_HELP = `uploads comment (--pr <num> | --issue <num>) [--repo <owner/name>] [--workspace <name>]
|
|
1323
1342
|
|
|
1324
1343
|
Create or update the managed attachments comment on a GitHub PR or issue,
|
|
1325
|
-
listing everything uploaded for it.
|
|
1344
|
+
listing everything uploaded for it. Posts as uploads-sh[bot] when the GitHub
|
|
1345
|
+
App is installed on the repo; otherwise via your local gh auth. Finds its own
|
|
1326
1346
|
prior comment via a hidden marker and edits it in place; never touches other
|
|
1327
1347
|
comments or the description.
|
|
1328
1348
|
|
|
@@ -1344,9 +1364,10 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
|
|
|
1344
1364
|
await writeJson({ ...target, ...result });
|
|
1345
1365
|
}
|
|
1346
1366
|
else if (!ctx.quiet) {
|
|
1367
|
+
const via = commentViaSuffix(result.via);
|
|
1347
1368
|
process.stderr.write(result.action === "skipped"
|
|
1348
1369
|
? `no attachments under ${ghKeyPrefix(target)} — nothing to do\n`
|
|
1349
|
-
: `${result.action} attachments comment on ${target.repo}#${target.num} (${result.count} file${result.count === 1 ? "" : "s"})\n`);
|
|
1370
|
+
: `${result.action} attachments comment on ${target.repo}#${target.num} (${result.count} file${result.count === 1 ? "" : "s"})${via}\n`);
|
|
1350
1371
|
}
|
|
1351
1372
|
return 0;
|
|
1352
1373
|
}
|
package/dist/github-gh.d.ts
CHANGED
|
@@ -16,6 +16,25 @@ export declare function resolveCurrentPullRequest(repo: string, run?: CommandRun
|
|
|
16
16
|
* uploads without metadata.
|
|
17
17
|
*/
|
|
18
18
|
export declare function classifyGhNumber(repo: string, num: number, run?: CommandRunner): GhTarget | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Best-effort PR/issue title lookup via local `gh`. Returns undefined on any
|
|
21
|
+
* failure (gh missing, unauthenticated, network, 404) — mirrors
|
|
22
|
+
* `resolveCurrentPullRequest`/`classifyGhNumber`'s degrade-don't-throw
|
|
23
|
+
* pattern. A title is a nice-to-have annotation, never a blocker: callers
|
|
24
|
+
* must never let this failure abort an upload.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveGhTitle(target: GhTarget, run?: CommandRunner): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* `ghMetadataFromTarget`'s 4 pairs, plus a best-effort `gh.title` (issue #267)
|
|
29
|
+
* when `resolveGhTitle` yields one that also satisfies the metadata-value
|
|
30
|
+
* rule every other pair follows (1-512 printable ASCII — `metadata.ts`'s
|
|
31
|
+
* `META_VALUE_MAX`/`isMetaValueSafe`). Truncated to `META_VALUE_MAX` first;
|
|
32
|
+
* a title left empty or unsafe by truncation (e.g. non-ASCII — real titles
|
|
33
|
+
* often contain emoji or curly quotes) is silently omitted rather than
|
|
34
|
+
* sanitized, matching `resolveGhTitle`'s own "degrade, don't fail the
|
|
35
|
+
* upload" contract.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ghMetadataFromTargetWithTitle(target: GhTarget, run?: CommandRunner): Record<string, string>;
|
|
19
38
|
/**
|
|
20
39
|
* Create the managed attachments comment, or edit it in place if it already
|
|
21
40
|
* exists. Never touches any other comment. Body is passed via stdin
|
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`
|
|
@@ -91,12 +92,62 @@ export function classifyGhNumber(repo, num, run = execRunner) {
|
|
|
91
92
|
}
|
|
92
93
|
return undefined;
|
|
93
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Best-effort PR/issue title lookup via local `gh`. Returns undefined on any
|
|
97
|
+
* failure (gh missing, unauthenticated, network, 404) — mirrors
|
|
98
|
+
* `resolveCurrentPullRequest`/`classifyGhNumber`'s degrade-don't-throw
|
|
99
|
+
* pattern. A title is a nice-to-have annotation, never a blocker: callers
|
|
100
|
+
* must never let this failure abort an upload.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveGhTitle(target, run = execRunner) {
|
|
103
|
+
try {
|
|
104
|
+
const out = run("gh", [
|
|
105
|
+
target.kind === "pull" ? "pr" : "issue",
|
|
106
|
+
"view",
|
|
107
|
+
String(target.num),
|
|
108
|
+
"--repo",
|
|
109
|
+
target.repo,
|
|
110
|
+
"--json",
|
|
111
|
+
"title",
|
|
112
|
+
"--jq",
|
|
113
|
+
".title",
|
|
114
|
+
]).trim();
|
|
115
|
+
return out.length > 0 ? out : undefined;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// gh missing / unauthenticated / not found / network — caller skips
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* `ghMetadataFromTarget`'s 4 pairs, plus a best-effort `gh.title` (issue #267)
|
|
124
|
+
* when `resolveGhTitle` yields one that also satisfies the metadata-value
|
|
125
|
+
* rule every other pair follows (1-512 printable ASCII — `metadata.ts`'s
|
|
126
|
+
* `META_VALUE_MAX`/`isMetaValueSafe`). Truncated to `META_VALUE_MAX` first;
|
|
127
|
+
* a title left empty or unsafe by truncation (e.g. non-ASCII — real titles
|
|
128
|
+
* often contain emoji or curly quotes) is silently omitted rather than
|
|
129
|
+
* sanitized, matching `resolveGhTitle`'s own "degrade, don't fail the
|
|
130
|
+
* upload" contract.
|
|
131
|
+
*/
|
|
132
|
+
export function ghMetadataFromTargetWithTitle(target, run = execRunner) {
|
|
133
|
+
const base = ghMetadataFromTarget(target);
|
|
134
|
+
const title = resolveGhTitle(target, run);
|
|
135
|
+
if (title === undefined)
|
|
136
|
+
return base;
|
|
137
|
+
const truncated = title.length > META_VALUE_MAX ? title.slice(0, META_VALUE_MAX) : title;
|
|
138
|
+
return isMetaValueSafe(truncated) ? { ...base, "gh.title": truncated } : base;
|
|
139
|
+
}
|
|
94
140
|
/**
|
|
95
141
|
* PR comments live on the issues endpoint, so one path covers PRs and issues.
|
|
96
|
-
*
|
|
142
|
+
* `--paginate` follows Link headers and merges every page into one array, so the
|
|
143
|
+
* marker comment is found even on threads past 100 comments.
|
|
97
144
|
*/
|
|
98
145
|
function findManagedComment(target, run) {
|
|
99
|
-
const raw = run("gh", [
|
|
146
|
+
const raw = run("gh", [
|
|
147
|
+
"api",
|
|
148
|
+
`repos/${target.repo}/issues/${target.num}/comments?per_page=100`,
|
|
149
|
+
"--paginate",
|
|
150
|
+
]);
|
|
100
151
|
const comments = JSON.parse(raw);
|
|
101
152
|
return comments.find((c) => typeof c.body === "string" && c.body.includes(ATTACHMENTS_MARKER));
|
|
102
153
|
}
|
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";
|
|
@@ -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({
|
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.
|