@buildinternet/uploads 0.7.0 → 0.8.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 +14 -3
- package/dist/cli-args.d.ts +17 -2
- package/dist/cli-args.js +50 -5
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +67 -24
- package/dist/client.d.ts +69 -0
- package/dist/client.js +99 -5
- package/dist/commands/install.js +110 -52
- package/dist/commands/invite.d.ts +10 -0
- package/dist/commands/invite.js +102 -0
- package/dist/commands/login.d.ts +15 -0
- package/dist/commands/login.js +17 -9
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +206 -16
- package/dist/config.js +6 -3
- package/dist/errors.d.ts +1 -1
- package/dist/github.d.ts +15 -0
- package/dist/github.js +31 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/mcp/args.d.ts +17 -0
- package/dist/mcp/args.js +42 -0
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tools.js +121 -21
- package/dist/metadata.d.ts +32 -0
- package/dist/metadata.js +102 -0
- package/dist/public-urls.d.ts +18 -0
- package/dist/public-urls.js +68 -0
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { createUploadsClient } from "./client.js";
|
|
4
|
-
import { parseCommandArgs, flagString, flagBool, flagInt, UsageError, } from "./cli-args.js";
|
|
4
|
+
import { parseCommandArgs, flagString, flagBool, flagInt, flagValues, UsageError, } from "./cli-args.js";
|
|
5
5
|
import { resolvePutDefaults, workspaceMismatch, workspaceFromToken, } from "./config.js";
|
|
6
6
|
import { buildMarkdown } from "./embed.js";
|
|
7
|
+
import { urlForGithubEmbed } from "./public-urls.js";
|
|
7
8
|
import { UploadsError } from "./errors.js";
|
|
8
9
|
import { writeJson, writeStdout } from "./io.js";
|
|
9
|
-
import {
|
|
10
|
+
import { parseMetaFlags, validateMetaMap } from "./metadata.js";
|
|
11
|
+
import { ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, attachmentsCommentBody, normalizeGithubCoordinate, } from "./github.js";
|
|
10
12
|
import { resolveRepo, resolveCurrentPullRequest, execRunner, upsertAttachmentsComment, } from "./github-gh.js";
|
|
11
13
|
import { resolvePutPrefix } from "./destinations.js";
|
|
12
14
|
import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
|
|
13
15
|
import { applyFrame, resolveFrameId } from "./frame.js";
|
|
14
16
|
import { buildCliProvenance } from "./provenance.js";
|
|
15
17
|
import { packageVersion } from "./package-version.js";
|
|
18
|
+
/** Read a local file (or `-` for stdin). Missing path → FILE_NOT_FOUND (exit 2). */
|
|
19
|
+
export function readFileArg(fileArg) {
|
|
20
|
+
try {
|
|
21
|
+
return new Uint8Array(readFileSync(fileArg === "-" ? 0 : fileArg));
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err?.code === "ENOENT") {
|
|
25
|
+
throw new UploadsError(`file not found: ${fileArg}`, "FILE_NOT_FOUND");
|
|
26
|
+
}
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
16
30
|
// --- put ---
|
|
17
31
|
const PUT_HELP = `uploads put <file> [options]
|
|
18
32
|
|
|
@@ -30,8 +44,12 @@ Uploads are public. --pr/--issue keys include the repo, number, and filename and
|
|
|
30
44
|
remain public even for private/internal GitHub repositories. Upload only media
|
|
31
45
|
that is safe at a predictable public URL.
|
|
32
46
|
|
|
47
|
+
Human/json output includes durable url and (when dual-host applies) embedUrl.
|
|
48
|
+
MARKDOWN prefers embedUrl for GitHub. Override: UPLOADS_EMBED_PUBLIC_BASE_URL.
|
|
49
|
+
|
|
33
50
|
Options:
|
|
34
51
|
--key <key> Object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>)
|
|
52
|
+
--name <leaf> Clean key leaf + default alt (no '/'); keeps --pr/default path. Not with --key
|
|
35
53
|
--destination <id> Typed root: screenshots | gh | f (sets --prefix)
|
|
36
54
|
--prefix <path> Key prefix (default: screenshots, or UPLOADS_DEFAULT_PREFIX)
|
|
37
55
|
--repo <owner/repo> Repo segment (default: git remote, or UPLOADS_DEFAULT_REPO)
|
|
@@ -53,13 +71,24 @@ Options:
|
|
|
53
71
|
--issue <num> Attach to an issue: key gh/<owner>/<repo>/issues/<num>/<name>
|
|
54
72
|
--comment With --pr/--issue: update one managed comment with attachments and linked galleries via local gh auth
|
|
55
73
|
--gallery <id> Add the uploaded object to this public gallery
|
|
74
|
+
--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
|
|
75
|
+
Re-uploading to an existing key WITH --meta replaces that file's
|
|
76
|
+
entire metadata set; without --meta the existing metadata is
|
|
77
|
+
preserved. Use "uploads meta set" to edit individual keys.
|
|
78
|
+
--dry-run Print key + public URL without uploading. Not with --comment/--gallery
|
|
79
|
+
|
|
80
|
+
Exit codes: 0 ok · 2 usage/token/file · 3 auth/policy · 4 network · 1 other.
|
|
81
|
+
Scripted formats (json|url|markdown) also print failures on stdout.
|
|
56
82
|
|
|
57
83
|
Examples:
|
|
58
84
|
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New cards" --width 700
|
|
59
85
|
uploads put ./mobile.png --frame phone
|
|
60
86
|
uploads put ./ui.png --frame browser --frame-url "https://app.example/settings"
|
|
61
87
|
uploads put ./shot.png --destination screenshots
|
|
88
|
+
uploads put ./capture-….webp --pr 128 --name hero.webp
|
|
89
|
+
uploads put ./shot.png --pr 128 --name hero.webp --dry-run --format url
|
|
62
90
|
uploads put ./after.png --gallery gal_example
|
|
91
|
+
uploads put ./shot.png --meta app=myapp --meta page=settings
|
|
63
92
|
`;
|
|
64
93
|
/**
|
|
65
94
|
* Turns a pr/issue pair (+ optional repo) into a GhTarget; undefined when
|
|
@@ -160,7 +189,7 @@ function frameOptionsFromFlags(flags) {
|
|
|
160
189
|
* fatal (`comment` command) or a warning (`put --comment`).
|
|
161
190
|
*/
|
|
162
191
|
export async function syncAttachmentsComment(client, target, run) {
|
|
163
|
-
const items = (await client.listAll({ prefix: ghKeyPrefix(target) })).map(({ key, url }) => ({ key, url }));
|
|
192
|
+
const items = (await client.listAll({ prefix: ghKeyPrefix(target) })).map(({ key, url, embedUrl }) => ({ key, url, embedUrl }));
|
|
164
193
|
const galleries = [];
|
|
165
194
|
let cursor;
|
|
166
195
|
do {
|
|
@@ -183,6 +212,7 @@ export async function syncAttachmentsComment(client, target, run) {
|
|
|
183
212
|
.slice(0, 3)
|
|
184
213
|
.map((item) => ({
|
|
185
214
|
url: item.url,
|
|
215
|
+
embedUrl: item.embedUrl,
|
|
186
216
|
alt: item.altText ?? item.objectKey,
|
|
187
217
|
itemUrl: item.pageUrl,
|
|
188
218
|
})),
|
|
@@ -226,12 +256,19 @@ Options:
|
|
|
226
256
|
--optimize-quality <1-100> WebP quality (default: 85)
|
|
227
257
|
--keep-exif Keep EXIF/XMP/ICC when optimizing (default: strip for privacy)
|
|
228
258
|
--workspace, -w <name> Override workspace
|
|
259
|
+
--meta <k=v> Extra queryable metadata (repeatable; value may contain "=").
|
|
260
|
+
gh.repo/gh.kind/gh.number/gh.ref are always set from the resolved
|
|
261
|
+
target — a --meta pair with the same key is overridden by it.
|
|
262
|
+
Because attach always sends its own gh.* pairs, re-attaching to
|
|
263
|
+
the same key always replaces that file's entire metadata set
|
|
264
|
+
(never preserves) — use "uploads meta set" to add to it instead.
|
|
229
265
|
|
|
230
266
|
Examples:
|
|
231
267
|
uploads attach ./before.png ./after.png
|
|
232
268
|
uploads attach ./mobile.png --frame phone
|
|
233
269
|
uploads attach ./shot.png --pr 123 --repo myorg/myapp
|
|
234
270
|
uploads attach ./artifact.zip --issue 45 --no-comment
|
|
271
|
+
uploads attach ./shot.png --meta app=myapp --meta page=settings
|
|
235
272
|
`;
|
|
236
273
|
export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
237
274
|
const parsed = parseCommandArgs(args);
|
|
@@ -253,6 +290,15 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
253
290
|
const optimizeOpts = optimizeOptionsFromFlags(parsed.flags, defaults);
|
|
254
291
|
const frameOpts = frameOptionsFromFlags(parsed.flags);
|
|
255
292
|
const contentTypeOverride = flagString(parsed.flags, "--content-type");
|
|
293
|
+
// User-supplied extras first, then the resolved target's gh.* — explicit
|
|
294
|
+
// target pairs always win over a same-named --meta extra (documented above).
|
|
295
|
+
// Validate the merged map (not just the extras) so the 24-key/8KB caps are
|
|
296
|
+
// enforced client-side even when extras alone are under the cap but extras
|
|
297
|
+
// + the 4 gh.* pairs push the merged map over it.
|
|
298
|
+
const metaExtras = parseMetaFlags(flagValues(parsed.flags, "--meta"));
|
|
299
|
+
const metadata = { ...metaExtras, ...ghMetadataFromTarget(target) };
|
|
300
|
+
if (Object.keys(metadata).length > 0)
|
|
301
|
+
validateMetaMap(metadata);
|
|
256
302
|
const results = [];
|
|
257
303
|
for (const file of parsed.positionals) {
|
|
258
304
|
if (file === "-")
|
|
@@ -260,7 +306,7 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
260
306
|
const sourceName = basename(file);
|
|
261
307
|
if (!ctx.quiet && !ctx.json)
|
|
262
308
|
process.stderr.write(`>> uploading ${file}\n`);
|
|
263
|
-
const prepared = await prepareImageForUpload(
|
|
309
|
+
const prepared = await prepareImageForUpload(readFileArg(file), sourceName, {
|
|
264
310
|
...frameOpts,
|
|
265
311
|
optimize: optimizeOpts,
|
|
266
312
|
});
|
|
@@ -280,10 +326,12 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
280
326
|
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
281
327
|
keepExif: optimizeOpts.keepExif === true,
|
|
282
328
|
}),
|
|
329
|
+
metadata,
|
|
283
330
|
});
|
|
331
|
+
const embedSrc = urlForGithubEmbed(result.url, result.embedUrl);
|
|
284
332
|
results.push({
|
|
285
333
|
...result,
|
|
286
|
-
markdown: buildMarkdown(
|
|
334
|
+
markdown: buildMarkdown(embedSrc, { alt: sourceName }),
|
|
287
335
|
optimize: {
|
|
288
336
|
optimized: prepared.optimized,
|
|
289
337
|
skippedReason: prepared.skippedReason,
|
|
@@ -310,7 +358,8 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
310
358
|
}
|
|
311
359
|
else {
|
|
312
360
|
for (const result of results) {
|
|
313
|
-
|
|
361
|
+
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
362
|
+
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${result.markdown}\n`);
|
|
314
363
|
}
|
|
315
364
|
if (!ctx.quiet && comment)
|
|
316
365
|
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
@@ -338,20 +387,41 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
338
387
|
const ghTarget = ghTargetFromFlags(parsed.flags, run);
|
|
339
388
|
const wantComment = parsed.flags.has("--comment");
|
|
340
389
|
const galleryId = flagString(parsed.flags, "--gallery");
|
|
390
|
+
const nameFlag = flagString(parsed.flags, "--name");
|
|
391
|
+
const dryRun = flagBool(parsed.flags, "--dry-run");
|
|
392
|
+
// Validate --meta up front (fail fast, before reading/optimizing the file).
|
|
393
|
+
const metadata = (() => {
|
|
394
|
+
const pairs = flagValues(parsed.flags, "--meta");
|
|
395
|
+
return pairs.length > 0 ? parseMetaFlags(pairs) : undefined;
|
|
396
|
+
})();
|
|
341
397
|
if (wantComment && typeof parsed.flags.get("--comment") === "string") {
|
|
342
398
|
throw new UsageError("--comment takes no value — place it after the file argument");
|
|
343
399
|
}
|
|
344
400
|
if (wantComment && !ghTarget)
|
|
345
401
|
throw new UsageError("--comment requires --pr or --issue");
|
|
346
402
|
if (ghTarget) {
|
|
347
|
-
if (keyHint)
|
|
348
|
-
throw new UsageError("--key cannot be combined with --pr/--issue");
|
|
403
|
+
if (keyHint) {
|
|
404
|
+
throw new UsageError("--key cannot be combined with --pr/--issue; use --name <leaf> to set a clean filename on the stable path");
|
|
405
|
+
}
|
|
349
406
|
if (flagString(parsed.flags, "--ref")) {
|
|
350
407
|
throw new UsageError("--ref cannot be combined with --pr/--issue");
|
|
351
408
|
}
|
|
352
409
|
if (prefixFlag)
|
|
353
410
|
throw new UsageError("--prefix cannot be combined with --pr/--issue");
|
|
354
411
|
}
|
|
412
|
+
if (nameFlag !== undefined) {
|
|
413
|
+
if (nameFlag === "" || nameFlag.includes("/")) {
|
|
414
|
+
throw new UsageError("--name must be a bare filename with no '/'");
|
|
415
|
+
}
|
|
416
|
+
if (keyHint)
|
|
417
|
+
throw new UsageError("--name cannot be combined with --key");
|
|
418
|
+
}
|
|
419
|
+
if (dryRun) {
|
|
420
|
+
if (wantComment)
|
|
421
|
+
throw new UsageError("--dry-run cannot be combined with --comment");
|
|
422
|
+
if (galleryId)
|
|
423
|
+
throw new UsageError("--dry-run cannot be combined with --gallery");
|
|
424
|
+
}
|
|
355
425
|
let resolvedPrefix;
|
|
356
426
|
try {
|
|
357
427
|
resolvedPrefix = resolvePutPrefix({
|
|
@@ -364,8 +434,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
364
434
|
catch (err) {
|
|
365
435
|
throw new UsageError(err instanceof Error ? err.message : String(err));
|
|
366
436
|
}
|
|
367
|
-
const bytes =
|
|
368
|
-
const sourceName = fileArg === "-" ? (keyHint ? basename(keyHint) : "stdin.bin") : basename(fileArg);
|
|
437
|
+
const bytes = readFileArg(fileArg);
|
|
438
|
+
const sourceName = nameFlag ?? (fileArg === "-" ? (keyHint ? basename(keyHint) : "stdin.bin") : basename(fileArg));
|
|
369
439
|
const format = ctx.json
|
|
370
440
|
? "json"
|
|
371
441
|
: (() => {
|
|
@@ -379,6 +449,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
379
449
|
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
380
450
|
const optimizeOpts = optimizeOptionsFromFlags(parsed.flags, defaults);
|
|
381
451
|
const frameOpts = frameOptionsFromFlags(parsed.flags);
|
|
452
|
+
// Optimize even on --dry-run so the preview key extension/hash match a real put.
|
|
382
453
|
const prepared = await prepareImageForUpload(bytes, sourceName, {
|
|
383
454
|
...frameOpts,
|
|
384
455
|
optimize: optimizeOpts,
|
|
@@ -395,7 +466,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
395
466
|
})()
|
|
396
467
|
: defaults.width;
|
|
397
468
|
if (!ctx.quiet && format === "human") {
|
|
398
|
-
process.stderr.write(`>> uploading ${fileArg === "-" ? "stdin" : fileArg}\n`);
|
|
469
|
+
process.stderr.write(`>> ${dryRun ? "dry run" : "uploading"} ${fileArg === "-" ? "stdin" : fileArg}\n`);
|
|
399
470
|
if (prepared.frame?.framed)
|
|
400
471
|
process.stderr.write(`>> framed with ${prepared.frame.frameId}\n`);
|
|
401
472
|
const note = formatOptimizeNote(prepared);
|
|
@@ -414,14 +485,17 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
414
485
|
ref: flagString(parsed.flags, "--ref") ?? defaults.ref,
|
|
415
486
|
contentType: prepared.optimized ? prepared.contentType : contentTypeOverride,
|
|
416
487
|
deriveRepoFromGit: !noGit,
|
|
488
|
+
dryRun,
|
|
417
489
|
provenance: buildCliProvenance({
|
|
418
490
|
sourceName,
|
|
419
491
|
optimized: prepared.optimized,
|
|
420
492
|
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
421
493
|
keepExif: optimizeOpts.keepExif === true,
|
|
422
494
|
}),
|
|
495
|
+
metadata,
|
|
423
496
|
});
|
|
424
|
-
const
|
|
497
|
+
const embedSrc = urlForGithubEmbed(result.url, result.embedUrl);
|
|
498
|
+
const markdown = buildMarkdown(embedSrc, { alt, width });
|
|
425
499
|
let gallery;
|
|
426
500
|
if (galleryId) {
|
|
427
501
|
try {
|
|
@@ -446,7 +520,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
446
520
|
filename: prepared.filename,
|
|
447
521
|
};
|
|
448
522
|
if (!ctx.quiet && format === "human") {
|
|
449
|
-
process.stderr.write(`>> key: ${result.key}\n\n`);
|
|
523
|
+
process.stderr.write(`>> key: ${result.key}${dryRun ? " (dry run — not uploaded)" : ""}\n\n`);
|
|
450
524
|
}
|
|
451
525
|
switch (format) {
|
|
452
526
|
case "json":
|
|
@@ -456,6 +530,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
456
530
|
optimize: optimizeMeta,
|
|
457
531
|
frame: prepared.frame,
|
|
458
532
|
gallery,
|
|
533
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
459
534
|
});
|
|
460
535
|
break;
|
|
461
536
|
case "url":
|
|
@@ -464,8 +539,10 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
464
539
|
case "markdown":
|
|
465
540
|
await writeStdout(`${markdown}\n`);
|
|
466
541
|
break;
|
|
467
|
-
default:
|
|
468
|
-
|
|
542
|
+
default: {
|
|
543
|
+
const embedLine = result.embedUrl ? `EMBED: ${result.embedUrl}\n` : "";
|
|
544
|
+
await writeStdout(`URL: ${result.url}\n${embedLine}MARKDOWN: ${markdown}${gallery?.url ? `\nGALLERY: ${gallery.url}` : ""}\n`);
|
|
545
|
+
}
|
|
469
546
|
}
|
|
470
547
|
if (gallery?.url && format !== "human") {
|
|
471
548
|
process.stderr.write(`gallery: ${gallery.url}\n`);
|
|
@@ -703,21 +780,51 @@ export async function runGallery(ctx, args, help = false) {
|
|
|
703
780
|
}
|
|
704
781
|
}
|
|
705
782
|
// --- list ---
|
|
706
|
-
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--workspace <name>]
|
|
783
|
+
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--meta <k=v>]... [--workspace <name>]
|
|
707
784
|
|
|
708
785
|
Default prefix: UPLOADS_DEFAULT_PREFIX (screenshots if unset).
|
|
709
786
|
|
|
787
|
+
--meta <k=v> (repeatable, ANDed) switches to the metadata filter endpoint —
|
|
788
|
+
returned items include their matched metadata. Combines with --prefix, not
|
|
789
|
+
with --pr/--issue/--all. See also: uploads find (positional-pair alias).
|
|
790
|
+
|
|
710
791
|
Examples:
|
|
711
792
|
uploads list --prefix screenshots/
|
|
712
793
|
uploads list --pr 123
|
|
713
794
|
uploads list --all --json
|
|
795
|
+
uploads list --meta gh.repo=buildinternet/uploads --meta gh.number=123
|
|
714
796
|
`;
|
|
797
|
+
/** `--meta k=v` (repeatable) filter path, shared by `runList` and `runFind`. */
|
|
798
|
+
async function runFindFiles(ctx, filters, flags) {
|
|
799
|
+
if (flagString(flags, "--cursor") !== undefined) {
|
|
800
|
+
throw new UsageError("--cursor is not supported with metadata filters");
|
|
801
|
+
}
|
|
802
|
+
const prefix = flagString(flags, "--prefix");
|
|
803
|
+
const limit = flagInt(flags, "--limit", "--limit");
|
|
804
|
+
const result = await ctx.client.findFiles(filters, { prefix, limit });
|
|
805
|
+
if (ctx.json)
|
|
806
|
+
await writeJson(result);
|
|
807
|
+
else
|
|
808
|
+
for (const item of result.items)
|
|
809
|
+
await writeStdout(`${item.key}${item.url ? ` ${item.url}` : ""}\n`);
|
|
810
|
+
return 0;
|
|
811
|
+
}
|
|
715
812
|
export async function runList(ctx, args, help = false, run = execRunner) {
|
|
716
813
|
const parsed = parseCommandArgs(args);
|
|
717
814
|
if (help || parsed.help) {
|
|
718
815
|
process.stderr.write(LIST_HELP);
|
|
719
816
|
return 0;
|
|
720
817
|
}
|
|
818
|
+
const metaPairs = flagValues(parsed.flags, "--meta");
|
|
819
|
+
if (metaPairs.length > 0) {
|
|
820
|
+
if (ghTargetFromFlags(parsed.flags, run)) {
|
|
821
|
+
throw new UsageError("--meta cannot be combined with --pr/--issue");
|
|
822
|
+
}
|
|
823
|
+
if (flagBool(parsed.flags, "--all")) {
|
|
824
|
+
throw new UsageError("--meta cannot be combined with --all");
|
|
825
|
+
}
|
|
826
|
+
return runFindFiles(ctx, parseMetaFlags(metaPairs), parsed.flags);
|
|
827
|
+
}
|
|
721
828
|
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
722
829
|
const prefixFlag = flagString(parsed.flags, "--prefix");
|
|
723
830
|
let prefix = prefixFlag ?? (defaults.prefix ? `${defaults.prefix}/` : undefined);
|
|
@@ -750,6 +857,89 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
750
857
|
}
|
|
751
858
|
return 0;
|
|
752
859
|
}
|
|
860
|
+
// --- find ---
|
|
861
|
+
const FIND_HELP = `uploads find k=v [k=v...] [--prefix <p>] [--limit <n>] [--workspace <name>]
|
|
862
|
+
|
|
863
|
+
Human-friendly alias for \`uploads list --meta k=v...\` — same metadata filter
|
|
864
|
+
(ANDed equality), same output; pairs are positional instead of repeated flags.
|
|
865
|
+
|
|
866
|
+
Examples:
|
|
867
|
+
uploads find gh.repo=buildinternet/uploads gh.number=123
|
|
868
|
+
uploads find app=myapp page=settings --prefix screenshots/
|
|
869
|
+
`;
|
|
870
|
+
export async function runFind(ctx, args, help = false) {
|
|
871
|
+
const parsed = parseCommandArgs(args);
|
|
872
|
+
if (help || parsed.help) {
|
|
873
|
+
process.stderr.write(FIND_HELP);
|
|
874
|
+
return 0;
|
|
875
|
+
}
|
|
876
|
+
if (parsed.positionals.length === 0) {
|
|
877
|
+
process.stderr.write(FIND_HELP);
|
|
878
|
+
return 2;
|
|
879
|
+
}
|
|
880
|
+
const filters = parseMetaFlags(parsed.positionals);
|
|
881
|
+
return runFindFiles(ctx, filters, parsed.flags);
|
|
882
|
+
}
|
|
883
|
+
// --- meta ---
|
|
884
|
+
const META_HELP = `uploads meta <command> [args]
|
|
885
|
+
|
|
886
|
+
Read/write an object's queryable custom metadata (D1-backed key-value pairs;
|
|
887
|
+
distinct from the R2 provenance headers put on upload).
|
|
888
|
+
|
|
889
|
+
Commands:
|
|
890
|
+
get <key> Show metadata for an object
|
|
891
|
+
set <key> k=v [k=v...] [--delete k]... Merge-set and/or delete pairs
|
|
892
|
+
|
|
893
|
+
Examples:
|
|
894
|
+
uploads meta get screenshots/myapp/42/shot.png
|
|
895
|
+
uploads meta set screenshots/myapp/42/shot.png app=myapp page=settings
|
|
896
|
+
uploads meta set screenshots/myapp/42/shot.png --delete app --delete page
|
|
897
|
+
`;
|
|
898
|
+
export async function runMeta(ctx, args, help = false) {
|
|
899
|
+
const parsed = parseCommandArgs(args);
|
|
900
|
+
const action = parsed.positionals[0];
|
|
901
|
+
if (help || parsed.help || !action) {
|
|
902
|
+
process.stderr.write(META_HELP);
|
|
903
|
+
return help || parsed.help ? 0 : 2;
|
|
904
|
+
}
|
|
905
|
+
switch (action) {
|
|
906
|
+
case "get": {
|
|
907
|
+
const key = parsed.positionals[1];
|
|
908
|
+
if (!key)
|
|
909
|
+
throw new UsageError("meta get requires an object key");
|
|
910
|
+
const result = await ctx.client.getMetadata(key);
|
|
911
|
+
if (ctx.json)
|
|
912
|
+
await writeJson(result);
|
|
913
|
+
else
|
|
914
|
+
for (const [k, v] of Object.entries(result.metadata))
|
|
915
|
+
await writeStdout(`${k}=${v}\n`);
|
|
916
|
+
return 0;
|
|
917
|
+
}
|
|
918
|
+
case "set": {
|
|
919
|
+
const key = parsed.positionals[1];
|
|
920
|
+
if (!key)
|
|
921
|
+
throw new UsageError("meta set requires an object key");
|
|
922
|
+
const pairs = parsed.positionals.slice(2);
|
|
923
|
+
const del = flagValues(parsed.flags, "--delete");
|
|
924
|
+
if (pairs.length === 0 && del.length === 0) {
|
|
925
|
+
throw new UsageError("meta set requires k=v pairs and/or --delete <key>");
|
|
926
|
+
}
|
|
927
|
+
const set = pairs.length > 0 ? parseMetaFlags(pairs) : undefined;
|
|
928
|
+
const result = await ctx.client.patchMetadata(key, {
|
|
929
|
+
set,
|
|
930
|
+
delete: del.length > 0 ? del : undefined,
|
|
931
|
+
});
|
|
932
|
+
if (ctx.json)
|
|
933
|
+
await writeJson(result);
|
|
934
|
+
else
|
|
935
|
+
for (const [k, v] of Object.entries(result.metadata))
|
|
936
|
+
await writeStdout(`${k}=${v}\n`);
|
|
937
|
+
return 0;
|
|
938
|
+
}
|
|
939
|
+
default:
|
|
940
|
+
throw new UsageError(`unknown meta command: ${action}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
753
943
|
// --- delete ---
|
|
754
944
|
const DELETE_HELP = `uploads delete <key> [--dry-run] [--workspace <name>]
|
|
755
945
|
|
package/dist/config.js
CHANGED
|
@@ -140,11 +140,14 @@ export function resolveConfig(flags) {
|
|
|
140
140
|
}
|
|
141
141
|
function missingTokenMessage(configPath) {
|
|
142
142
|
return [
|
|
143
|
-
"
|
|
144
|
-
"
|
|
143
|
+
"You're not signed in yet — one quick step and you're set:",
|
|
144
|
+
"",
|
|
145
|
+
" uploads login # open a browser and authorize this device",
|
|
146
|
+
"",
|
|
147
|
+
"Already have a token?",
|
|
145
148
|
` uploads setup --token <token> # guided setup → ${configPath}`,
|
|
146
149
|
` uploads config init --token <token> # writes ${configPath}`,
|
|
147
|
-
" or set UPLOADS_TOKEN
|
|
150
|
+
" or set UPLOADS_TOKEN / pass --token / use --env-file",
|
|
148
151
|
].join("\n");
|
|
149
152
|
}
|
|
150
153
|
/** Warn when an explicit workspace override may not match the token's embedded workspace. */
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_KEY" | "KEY_POLICY" | "STORAGE_QUOTA" | "UPLOAD_BUDGET" | "API_ERROR" | "NETWORK" | "USAGE";
|
|
1
|
+
export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "FILE_NOT_FOUND" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_KEY" | "KEY_POLICY" | "STORAGE_QUOTA" | "UPLOAD_BUDGET" | "API_ERROR" | "NETWORK" | "USAGE";
|
|
2
2
|
export declare class UploadsError extends Error {
|
|
3
3
|
readonly code: UploadsErrorCode;
|
|
4
4
|
readonly status?: number;
|
package/dist/github.d.ts
CHANGED
|
@@ -22,11 +22,25 @@ 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
|
+
* The four `gh.*` queryable-metadata pairs `uploads attach` writes
|
|
27
|
+
* automatically (`.context/2026-07-13-file-metadata-design.md`). `gh.kind`
|
|
28
|
+
* uses the API's singular vocabulary (`pull`/`issue`), distinct from
|
|
29
|
+
* `GhTarget.kind`'s URL-segment spelling (`pull`/`issues`). `gh.repo` and
|
|
30
|
+
* `gh.ref` are both lowercased so exact-match metadata search has one
|
|
31
|
+
* canonical spelling regardless of source casing (`--repo`, git remote, and
|
|
32
|
+
* `gh` output vary); `gh.ref` uses the same lowercased `owner/repo#number`
|
|
33
|
+
* coordinate as gallery GitHub references, so both surfaces resolve the same
|
|
34
|
+
* lookup key.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ghMetadataFromTarget(target: GhTarget): Record<string, string>;
|
|
25
37
|
/** Hidden marker identifying the one comment this CLI manages. Never change it — existing comments are found by exact match. */
|
|
26
38
|
export declare const ATTACHMENTS_MARKER = "<!-- uploads.sh:attachments -->";
|
|
27
39
|
export interface AttachmentItem {
|
|
28
40
|
key: string;
|
|
29
41
|
url: string | null;
|
|
42
|
+
/** Prefer for `<img src>` on GitHub (Camo-friendly host). Falls back to `url`. */
|
|
43
|
+
embedUrl?: string | null;
|
|
30
44
|
}
|
|
31
45
|
/** A public gallery linked to the PR or issue whose managed comment is syncing. */
|
|
32
46
|
export interface GalleryCommentItem {
|
|
@@ -37,6 +51,7 @@ export interface GalleryCommentItem {
|
|
|
37
51
|
previews?: {
|
|
38
52
|
url: string;
|
|
39
53
|
alt: string;
|
|
54
|
+
embedUrl?: string | null;
|
|
40
55
|
itemUrl?: string;
|
|
41
56
|
}[];
|
|
42
57
|
}
|
package/dist/github.js
CHANGED
|
@@ -63,6 +63,26 @@ export function ghKeyPrefix(target) {
|
|
|
63
63
|
export function ghAttachmentKey(target, filename) {
|
|
64
64
|
return `${ghKeyPrefix(target)}${sanitizeKeySegment(filename)}`;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The four `gh.*` queryable-metadata pairs `uploads attach` writes
|
|
68
|
+
* automatically (`.context/2026-07-13-file-metadata-design.md`). `gh.kind`
|
|
69
|
+
* uses the API's singular vocabulary (`pull`/`issue`), distinct from
|
|
70
|
+
* `GhTarget.kind`'s URL-segment spelling (`pull`/`issues`). `gh.repo` and
|
|
71
|
+
* `gh.ref` are both lowercased so exact-match metadata search has one
|
|
72
|
+
* canonical spelling regardless of source casing (`--repo`, git remote, and
|
|
73
|
+
* `gh` output vary); `gh.ref` uses the same lowercased `owner/repo#number`
|
|
74
|
+
* coordinate as gallery GitHub references, so both surfaces resolve the same
|
|
75
|
+
* lookup key.
|
|
76
|
+
*/
|
|
77
|
+
export function ghMetadataFromTarget(target) {
|
|
78
|
+
const repo = target.repo.toLowerCase();
|
|
79
|
+
return {
|
|
80
|
+
"gh.repo": repo,
|
|
81
|
+
"gh.kind": target.kind === "issues" ? "issue" : "pull",
|
|
82
|
+
"gh.number": String(target.num),
|
|
83
|
+
"gh.ref": `${repo}#${target.num}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
66
86
|
/** Hidden marker identifying the one comment this CLI manages. Never change it — existing comments are found by exact match. */
|
|
67
87
|
export const ATTACHMENTS_MARKER = "<!-- uploads.sh:attachments -->";
|
|
68
88
|
/** Default max width for images in the managed attachments comment (HTML img). */
|
|
@@ -107,7 +127,8 @@ export function attachmentsCommentBody(items, galleries = []) {
|
|
|
107
127
|
lines.push(`#### <a href="${href}">${escapeHtmlText(gallery.title)}</a>`);
|
|
108
128
|
for (const preview of gallery.previews ?? []) {
|
|
109
129
|
const previewHref = preview.itemUrl ? escapeHtmlAttr(preview.itemUrl) : href;
|
|
110
|
-
|
|
130
|
+
const previewSrc = escapeHtmlAttr(preview.embedUrl ?? preview.url);
|
|
131
|
+
lines.push(`<a href="${previewHref}"><img width="320" alt="${escapeHtmlAttr(preview.alt)}" src="${previewSrc}"></a>`);
|
|
111
132
|
}
|
|
112
133
|
lines.push(`<sub><a href="${href}">Open gallery</a></sub>`, "");
|
|
113
134
|
}
|
|
@@ -117,17 +138,20 @@ export function attachmentsCommentBody(items, galleries = []) {
|
|
|
117
138
|
lines.push("### 📎 Attachments", "");
|
|
118
139
|
for (const item of sorted) {
|
|
119
140
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
120
|
-
|
|
141
|
+
const stable = item.url;
|
|
142
|
+
const src = item.embedUrl ?? item.url;
|
|
143
|
+
if (src && inferContentType(name).startsWith("image/")) {
|
|
121
144
|
// Markdown ![]() has no width control — phone frames become full-column giants.
|
|
122
|
-
//
|
|
145
|
+
// img src uses embed host when available (Camo revalidates); click-through keeps stable URL.
|
|
123
146
|
const w = attachmentImageWidth(name);
|
|
124
147
|
const alt = escapeHtmlAttr(name);
|
|
125
|
-
const href = escapeHtmlAttr(
|
|
126
|
-
|
|
148
|
+
const href = escapeHtmlAttr(stable ?? src);
|
|
149
|
+
const imgSrc = escapeHtmlAttr(src);
|
|
150
|
+
lines.push(`<a href="${href}"><img width="${w}" alt="${alt}" src="${imgSrc}"></a>`);
|
|
127
151
|
lines.push("");
|
|
128
152
|
}
|
|
129
|
-
else if (
|
|
130
|
-
lines.push(`- [${name}](${
|
|
153
|
+
else if (stable) {
|
|
154
|
+
lines.push(`- [${name}](${stable})`);
|
|
131
155
|
}
|
|
132
156
|
else {
|
|
133
157
|
lines.push(`- ${name}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export { inferContentType, buildMarkdown } from "./embed.js";
|
|
2
|
+
export { DEFAULT_EMBED_PUBLIC_BASE_URL, embedBaseUrlFromEnv, embedUrlFromPublic, resolveEmbedBaseUrl, resolveEmbedUrl, urlForGithubEmbed, } from "./public-urls.js";
|
|
2
3
|
export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey } from "./keys.js";
|
|
3
4
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, type BuiltinDestinationId, } from "./destinations.js";
|
|
4
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";
|
|
5
6
|
export { UploadsError, type UploadsErrorCode } from "./errors.js";
|
|
6
|
-
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, } from "./client.js";
|
|
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";
|
|
7
8
|
export { buildCliProvenance } from "./provenance.js";
|
|
8
|
-
export {
|
|
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";
|
|
9
11
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, type OptimizeImageOptions, type OptimizeImageResult, type OptimizeOutputFormat, } from "./optimize.js";
|
|
10
12
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, type FrameFit, type FrameOptions, type FrameResult, } from "./frame.js";
|
|
11
13
|
export { execRunner, resolveRepo, upsertAttachmentsComment, type CommandRunner, } from "./github-gh.js";
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export { inferContentType, buildMarkdown } from "./embed.js";
|
|
2
|
+
export { DEFAULT_EMBED_PUBLIC_BASE_URL, embedBaseUrlFromEnv, embedUrlFromPublic, resolveEmbedBaseUrl, resolveEmbedUrl, urlForGithubEmbed, } from "./public-urls.js";
|
|
2
3
|
export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey } from "./keys.js";
|
|
3
4
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, } from "./destinations.js";
|
|
4
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";
|
|
5
6
|
export { UploadsError } from "./errors.js";
|
|
6
7
|
export { createUploadsClient, } from "./client.js";
|
|
7
8
|
export { buildCliProvenance } from "./provenance.js";
|
|
8
|
-
export {
|
|
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";
|
|
9
11
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, } from "./optimize.js";
|
|
10
12
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, } from "./frame.js";
|
|
11
13
|
export { execRunner, resolveRepo, upsertAttachmentsComment, } from "./github-gh.js";
|
package/dist/mcp/args.d.ts
CHANGED
|
@@ -2,3 +2,20 @@ export type ToolArgs = Record<string, unknown>;
|
|
|
2
2
|
export declare function usage(msg: string): never;
|
|
3
3
|
export declare function optString(args: ToolArgs, name: string): string | undefined;
|
|
4
4
|
export declare function optPosInt(args: ToolArgs, name: string): number | undefined;
|
|
5
|
+
/** A JSON-object argument of string→string pairs (e.g. a `metadata` or `filters` param). */
|
|
6
|
+
export declare function optStringRecord(args: ToolArgs, name: string): Record<string, string> | undefined;
|
|
7
|
+
/** A JSON-array argument of strings (e.g. a `delete` or `files` param). */
|
|
8
|
+
export declare function optStringArray(args: ToolArgs, name: string): string[] | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Shared tool-description text for the metadata-shaped `metadata`/`set`/
|
|
11
|
+
* `filters` params across the CLI/local MCP (put/attach/set_metadata/
|
|
12
|
+
* find_files) and the remote MCP worker (set_metadata/find_files).
|
|
13
|
+
*/
|
|
14
|
+
export declare const METADATA_DESCRIPTION = "Queryable custom metadata (key\u2192value), separate from provenance. Omit to leave any metadata already stored for this key untouched; pass an object (even {}) to fully replace it. Keys: lowercase, ^[a-z][a-z0-9._-]{0,63}$. Values: 1-512 printable ASCII characters. Caps: at most 24 keys, at most 8192 total key+value bytes. Suggested keys: app, url, page, device, resolution, commit, branch. `gh.*` is reserved by convention for GitHub PR/issue attachment context (repo/kind/number/ref).";
|
|
15
|
+
export declare const metadataProp: {
|
|
16
|
+
type: string;
|
|
17
|
+
additionalProperties: {
|
|
18
|
+
type: string;
|
|
19
|
+
};
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
package/dist/mcp/args.js
CHANGED
|
@@ -24,3 +24,45 @@ export function optPosInt(args, name) {
|
|
|
24
24
|
}
|
|
25
25
|
return v;
|
|
26
26
|
}
|
|
27
|
+
/** A JSON-object argument of string→string pairs (e.g. a `metadata` or `filters` param). */
|
|
28
|
+
export function optStringRecord(args, name) {
|
|
29
|
+
const v = args[name];
|
|
30
|
+
if (v === undefined || v === null)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (typeof v !== "object" || Array.isArray(v)) {
|
|
33
|
+
usage(`${name} must be an object of string values`);
|
|
34
|
+
}
|
|
35
|
+
// Object.create(null): a plain `{}` would silently drop a `__proto__` key
|
|
36
|
+
// (it hits the inherited setter instead of becoming an own property),
|
|
37
|
+
// turning a malicious/malformed key into a no-op rather than a rejected
|
|
38
|
+
// input. A null-prototype object makes every key a real own property so
|
|
39
|
+
// downstream validation (e.g. META_KEY_RE) sees and rejects it.
|
|
40
|
+
const result = Object.create(null);
|
|
41
|
+
for (const [key, value] of Object.entries(v)) {
|
|
42
|
+
if (typeof value !== "string")
|
|
43
|
+
usage(`${name}.${key} must be a string`);
|
|
44
|
+
result[key] = value;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
/** A JSON-array argument of strings (e.g. a `delete` or `files` param). */
|
|
49
|
+
export function optStringArray(args, name) {
|
|
50
|
+
const v = args[name];
|
|
51
|
+
if (v === undefined || v === null)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (!Array.isArray(v) || v.some((item) => typeof item !== "string")) {
|
|
54
|
+
usage(`${name} must be an array of strings`);
|
|
55
|
+
}
|
|
56
|
+
return v;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Shared tool-description text for the metadata-shaped `metadata`/`set`/
|
|
60
|
+
* `filters` params across the CLI/local MCP (put/attach/set_metadata/
|
|
61
|
+
* find_files) and the remote MCP worker (set_metadata/find_files).
|
|
62
|
+
*/
|
|
63
|
+
export const METADATA_DESCRIPTION = "Queryable custom metadata (key→value), separate from provenance. Omit to leave any metadata already stored for this key untouched; pass an object (even {}) to fully replace it. Keys: lowercase, ^[a-z][a-z0-9._-]{0,63}$. Values: 1-512 printable ASCII characters. Caps: at most 24 keys, at most 8192 total key+value bytes. Suggested keys: app, url, page, device, resolution, commit, branch. `gh.*` is reserved by convention for GitHub PR/issue attachment context (repo/kind/number/ref).";
|
|
64
|
+
export const metadataProp = {
|
|
65
|
+
type: "object",
|
|
66
|
+
additionalProperties: { type: "string" },
|
|
67
|
+
description: METADATA_DESCRIPTION,
|
|
68
|
+
};
|