@buildinternet/uploads 0.11.1 → 0.12.1
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 +12 -1
- package/dist/cli-catalog.d.ts +7 -0
- package/dist/cli-catalog.js +51 -0
- package/dist/cli.js +12 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +13 -7
- package/dist/commands/completion.js +15 -5
- package/dist/commands/screenshot.d.ts +6 -0
- package/dist/commands/screenshot.js +317 -0
- package/dist/commands.d.ts +71 -1
- package/dist/commands.js +114 -28
- package/dist/config-file.d.ts +28 -2
- package/dist/config-file.js +44 -6
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/github-gh.js +6 -0
- package/dist/mcp/tools.d.ts +7 -0
- package/dist/mcp/tools.js +250 -27
- package/dist/screenshot-local.d.ts +64 -0
- package/dist/screenshot-local.js +310 -0
- package/dist/screenshot-remote.d.ts +23 -0
- package/dist/screenshot-remote.js +79 -0
- package/dist/screenshot.d.ts +74 -0
- package/dist/screenshot.js +231 -0
- package/package.json +4 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { createUploadsClient } from "../client.js";
|
|
2
|
-
import { buildDoctorReport, makeGhTarget,
|
|
2
|
+
import { buildDoctorReport, makeGhTarget, syncAttachmentsComment, uploadAttachments, uploadPreparedImage, uploadPuts, } from "../commands.js";
|
|
3
3
|
import { resolveFrameId } from "../frame.js";
|
|
4
4
|
import { resolveConfig, resolvePutDefaults, } from "../config.js";
|
|
5
|
-
import { buildMarkdown } from "../embed.js";
|
|
6
|
-
import { urlForGithubEmbed } from "../public-urls.js";
|
|
7
5
|
import { resolvePutPrefix } from "../destinations.js";
|
|
8
|
-
import {
|
|
6
|
+
import { ghKeyPrefix, ghMetadataFromTarget } from "../github.js";
|
|
9
7
|
import { validateMetaMap } from "../metadata.js";
|
|
10
|
-
import { rewriteKeyExtension } from "../optimize.js";
|
|
11
|
-
import { buildCliProvenance } from "../provenance.js";
|
|
12
8
|
import { execRunner, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
13
9
|
import { METADATA_DESCRIPTION, metadataProp, optPosInt, optString, optStringArray, optStringRecord, usage, } from "./args.js";
|
|
14
10
|
import { batchFailureMessage, ToolBatchError } from "./server.js";
|
|
@@ -449,34 +445,20 @@ export function createUploadsMcpTools(opts) {
|
|
|
449
445
|
if (contentBase64 !== undefined) {
|
|
450
446
|
const sourceName = filenameArg;
|
|
451
447
|
const bytes = new Uint8Array(Buffer.from(contentBase64, "base64"));
|
|
452
|
-
const prepared = await
|
|
453
|
-
|
|
448
|
+
const { result, prepared, markdown } = await uploadPreparedImage(client, bytes, sourceName, {
|
|
449
|
+
frame: frameOpts,
|
|
454
450
|
optimize: optimizeOpts,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
let key = target ? ghAttachmentKey(target, filename) : keyArg;
|
|
458
|
-
if (key && prepared.optimized)
|
|
459
|
-
key = rewriteKeyExtension(key, filename);
|
|
460
|
-
const result = await client.put(prepared.bytes, {
|
|
461
|
-
filename,
|
|
462
|
-
key,
|
|
451
|
+
ghTarget: target,
|
|
452
|
+
key: keyArg,
|
|
463
453
|
prefix: resolvedPrefix ?? defaults.prefix,
|
|
464
454
|
repo: optString(args, "repo") ?? defaults.repo,
|
|
465
455
|
ref: refArg ?? defaults.ref,
|
|
466
|
-
contentType
|
|
456
|
+
contentType,
|
|
467
457
|
deriveRepoFromGit: !noGit,
|
|
468
458
|
dryRun,
|
|
469
|
-
provenance: buildCliProvenance({
|
|
470
|
-
sourceName,
|
|
471
|
-
client: "uploads-mcp",
|
|
472
|
-
optimized: prepared.optimized,
|
|
473
|
-
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
474
|
-
keepExif: optimizeOpts.keepExif === true,
|
|
475
|
-
}),
|
|
476
459
|
metadata,
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
alt: alt ?? sourceName,
|
|
460
|
+
provenanceClient: "uploads-mcp",
|
|
461
|
+
alt: () => alt ?? sourceName,
|
|
480
462
|
width,
|
|
481
463
|
});
|
|
482
464
|
const optimize = {
|
|
@@ -528,6 +510,247 @@ export function createUploadsMcpTools(opts) {
|
|
|
528
510
|
return flat;
|
|
529
511
|
},
|
|
530
512
|
},
|
|
513
|
+
{
|
|
514
|
+
name: "screenshot",
|
|
515
|
+
description: "Capture a URL or a local .html file and host it — a hosted, PR-embeddable image in one call. Backend `local` drives an already-installed Chrome/Chromium (dynamically loaded; unavailable in some runtimes); `remote` renders server-side via the workspace's render endpoint and counts against the monthly upload budget. Default via=auto prefers local when found, else remote. localhost/private-network URLs and .html files are local-only — via=remote (or auto falling back to remote) fails fast instead of a doomed request. Shares the put upload pipeline: optional frame, optimize-by-default, pr/issue attachment + comment, gallery, metadata. Uploads are public.",
|
|
516
|
+
inputSchema: {
|
|
517
|
+
type: "object",
|
|
518
|
+
properties: {
|
|
519
|
+
target: {
|
|
520
|
+
type: "string",
|
|
521
|
+
description: "http(s) URL, or a path to a local .html file.",
|
|
522
|
+
},
|
|
523
|
+
via: {
|
|
524
|
+
type: "string",
|
|
525
|
+
description: "Capture backend: auto (default) | local | remote.",
|
|
526
|
+
},
|
|
527
|
+
browser: {
|
|
528
|
+
type: "string",
|
|
529
|
+
description: "Explicit local browser executable path (local backend only).",
|
|
530
|
+
},
|
|
531
|
+
cdp: {
|
|
532
|
+
type: "string",
|
|
533
|
+
description: "Attach to a running Chrome via CDP instead of launching one (local backend only).",
|
|
534
|
+
},
|
|
535
|
+
viewport: {
|
|
536
|
+
type: "string",
|
|
537
|
+
description: "WIDTHxHEIGHT[@SCALEx], e.g. 1280x800@2x (default: 1280x800@2).",
|
|
538
|
+
},
|
|
539
|
+
selector: { type: "string", description: "Capture one element instead of the viewport." },
|
|
540
|
+
fullPage: { type: "boolean", description: "Capture the full scrollable page." },
|
|
541
|
+
colorScheme: {
|
|
542
|
+
type: "string",
|
|
543
|
+
description: "Emulate prefers-color-scheme: dark | light. Full media-query emulation requires via: \"local\" — the remote backend only sets the CSS color-scheme property and won't flip a page's own prefers-color-scheme queries.",
|
|
544
|
+
},
|
|
545
|
+
wait: {
|
|
546
|
+
type: "string",
|
|
547
|
+
description: 'Settle strategy: load (default) | domcontentloaded | networkidle | a millisecond count (millisecond counts are local-only — via: "local").',
|
|
548
|
+
},
|
|
549
|
+
key: {
|
|
550
|
+
type: "string",
|
|
551
|
+
description: "Explicit object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.png). Cannot be combined with pr/issue.",
|
|
552
|
+
},
|
|
553
|
+
destination: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "Typed destination root: screenshots | gh | f. With pr/issue must be gh or omitted.",
|
|
556
|
+
},
|
|
557
|
+
prefix: {
|
|
558
|
+
type: "string",
|
|
559
|
+
description: "Key prefix (default: screenshots, or UPLOADS_DEFAULT_PREFIX).",
|
|
560
|
+
},
|
|
561
|
+
...ghTargetProps("Attach to"),
|
|
562
|
+
repo: {
|
|
563
|
+
type: "string",
|
|
564
|
+
description: "owner/name repo segment (default: git remote, or UPLOADS_DEFAULT_REPO).",
|
|
565
|
+
},
|
|
566
|
+
ref: {
|
|
567
|
+
type: "string",
|
|
568
|
+
description: "PR/issue/branch key segment (default: today, or UPLOADS_DEFAULT_REF).",
|
|
569
|
+
},
|
|
570
|
+
alt: {
|
|
571
|
+
type: "string",
|
|
572
|
+
description: "Alt text for the markdown (default: derived filename).",
|
|
573
|
+
},
|
|
574
|
+
width: {
|
|
575
|
+
type: "number",
|
|
576
|
+
description: "Emit <img width=…> markdown instead of a plain embed.",
|
|
577
|
+
},
|
|
578
|
+
noOptimize: {
|
|
579
|
+
type: "boolean",
|
|
580
|
+
description: "Skip client-side image optimization (default: optimize to WebP).",
|
|
581
|
+
},
|
|
582
|
+
optimizeMaxEdge: {
|
|
583
|
+
type: "number",
|
|
584
|
+
description: "Max long edge in pixels when optimizing.",
|
|
585
|
+
},
|
|
586
|
+
optimizeQuality: { type: "number", description: "WebP quality 1-100 when optimizing." },
|
|
587
|
+
keepExif: { type: "boolean", description: "Keep EXIF/XMP/ICC when optimizing." },
|
|
588
|
+
...frameProps,
|
|
589
|
+
noGit: { type: "boolean", description: "Don't derive the repo segment from git." },
|
|
590
|
+
comment: {
|
|
591
|
+
type: "boolean",
|
|
592
|
+
description: "With pr/issue: create/update the managed attachments comment (best-effort).",
|
|
593
|
+
},
|
|
594
|
+
galleryId: {
|
|
595
|
+
type: "string",
|
|
596
|
+
description: "Add the uploaded object to this public gallery.",
|
|
597
|
+
},
|
|
598
|
+
dryRun: {
|
|
599
|
+
type: "boolean",
|
|
600
|
+
description: "Capture + resolve key/URL without uploading. Not with comment or galleryId.",
|
|
601
|
+
},
|
|
602
|
+
metadata: metadataProp,
|
|
603
|
+
workspace: workspaceProp,
|
|
604
|
+
},
|
|
605
|
+
required: ["target"],
|
|
606
|
+
additionalProperties: false,
|
|
607
|
+
},
|
|
608
|
+
async handler(args) {
|
|
609
|
+
const targetArg = optString(args, "target");
|
|
610
|
+
if (!targetArg)
|
|
611
|
+
usage("target is required");
|
|
612
|
+
const viaArg = optString(args, "via") ?? "auto";
|
|
613
|
+
if (viaArg !== "auto" && viaArg !== "local" && viaArg !== "remote") {
|
|
614
|
+
usage("via must be auto, local, or remote");
|
|
615
|
+
}
|
|
616
|
+
const colorSchemeArg = optString(args, "colorScheme");
|
|
617
|
+
if (colorSchemeArg && colorSchemeArg !== "dark" && colorSchemeArg !== "light") {
|
|
618
|
+
usage("colorScheme must be dark or light");
|
|
619
|
+
}
|
|
620
|
+
const target = ghTargetFromArgs(args, run);
|
|
621
|
+
const wantComment = optBool(args, "comment");
|
|
622
|
+
const dryRun = optBool(args, "dryRun");
|
|
623
|
+
const keyArg = optString(args, "key");
|
|
624
|
+
const destArg = optString(args, "destination");
|
|
625
|
+
const prefixArg = optString(args, "prefix");
|
|
626
|
+
const refArg = optString(args, "ref");
|
|
627
|
+
const galleryIdArg = optString(args, "galleryId");
|
|
628
|
+
if (wantComment && !target)
|
|
629
|
+
usage("comment requires pr or issue");
|
|
630
|
+
if (dryRun && wantComment)
|
|
631
|
+
usage("dryRun cannot be combined with comment");
|
|
632
|
+
if (dryRun && galleryIdArg)
|
|
633
|
+
usage("dryRun cannot be combined with galleryId");
|
|
634
|
+
if (target) {
|
|
635
|
+
if (keyArg)
|
|
636
|
+
usage("key cannot be combined with pr/issue");
|
|
637
|
+
if (refArg)
|
|
638
|
+
usage("ref cannot be combined with pr/issue");
|
|
639
|
+
if (prefixArg)
|
|
640
|
+
usage("prefix cannot be combined with pr/issue");
|
|
641
|
+
}
|
|
642
|
+
const metadata = optStringRecord(args, "metadata");
|
|
643
|
+
if (metadata)
|
|
644
|
+
validateMetaMap(metadata);
|
|
645
|
+
let resolvedPrefix;
|
|
646
|
+
try {
|
|
647
|
+
resolvedPrefix = resolvePutPrefix({
|
|
648
|
+
destination: destArg,
|
|
649
|
+
prefix: prefixArg,
|
|
650
|
+
key: keyArg,
|
|
651
|
+
ghAttachment: Boolean(target),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
usage(err instanceof Error ? err.message : String(err));
|
|
656
|
+
}
|
|
657
|
+
const { config, client } = clientFor(args);
|
|
658
|
+
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
659
|
+
const frameOpts = mcpFrameOptions(args);
|
|
660
|
+
const optimizeOpts = mcpOptimizeOptions(args, defaults);
|
|
661
|
+
const noGit = optBool(args, "noGit") || defaults.noGit === true;
|
|
662
|
+
const alt = optString(args, "alt");
|
|
663
|
+
const width = optPosInt(args, "width") ?? defaults.width;
|
|
664
|
+
// Dynamic import only: keeps mcp/tools.ts (and therefore anything
|
|
665
|
+
// that statically imports it) free of a static reference to the
|
|
666
|
+
// local-backend chain. If this fails, the runtime can't do Node-side
|
|
667
|
+
// capture at all — point the caller at the remote backend instead.
|
|
668
|
+
let screenshotModule;
|
|
669
|
+
try {
|
|
670
|
+
screenshotModule = await import("../screenshot.js");
|
|
671
|
+
}
|
|
672
|
+
catch (err) {
|
|
673
|
+
usage(`screenshot capture is unavailable in this runtime; try via: "remote" instead (${err instanceof Error ? err.message : String(err)})`);
|
|
674
|
+
}
|
|
675
|
+
let captured;
|
|
676
|
+
try {
|
|
677
|
+
captured = await screenshotModule.captureScreenshot({
|
|
678
|
+
target: targetArg,
|
|
679
|
+
via: viaArg,
|
|
680
|
+
browserPath: optString(args, "browser"),
|
|
681
|
+
cdp: optString(args, "cdp"),
|
|
682
|
+
viewport: screenshotModule.parseViewport(optString(args, "viewport")),
|
|
683
|
+
selector: optString(args, "selector"),
|
|
684
|
+
fullPage: optBool(args, "fullPage"),
|
|
685
|
+
colorScheme: colorSchemeArg,
|
|
686
|
+
waitUntil: screenshotModule.parseWaitUntil(optString(args, "wait")),
|
|
687
|
+
apiUrl: config.apiUrl,
|
|
688
|
+
token: config.token,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
if (err instanceof Error &&
|
|
693
|
+
"code" in err &&
|
|
694
|
+
err.code === "BROWSER_NOT_FOUND" &&
|
|
695
|
+
viaArg === "local") {
|
|
696
|
+
usage(`${err.message} — try via: "remote" instead`);
|
|
697
|
+
}
|
|
698
|
+
throw err;
|
|
699
|
+
}
|
|
700
|
+
const { result, prepared, markdown } = await uploadPreparedImage(client, captured.png, captured.filename, {
|
|
701
|
+
frame: frameOpts,
|
|
702
|
+
optimize: optimizeOpts,
|
|
703
|
+
ghTarget: target,
|
|
704
|
+
key: keyArg,
|
|
705
|
+
prefix: resolvedPrefix ?? defaults.prefix,
|
|
706
|
+
repo: optString(args, "repo") ?? defaults.repo,
|
|
707
|
+
ref: refArg ?? defaults.ref,
|
|
708
|
+
deriveRepoFromGit: !noGit,
|
|
709
|
+
dryRun,
|
|
710
|
+
metadata,
|
|
711
|
+
provenanceClient: "uploads-mcp-screenshot",
|
|
712
|
+
alt: (p) => alt ?? p.filename,
|
|
713
|
+
width,
|
|
714
|
+
});
|
|
715
|
+
let gallery;
|
|
716
|
+
if (galleryIdArg) {
|
|
717
|
+
try {
|
|
718
|
+
const current = await client.getGallery(galleryIdArg);
|
|
719
|
+
await client.addGalleryItem(galleryIdArg, result.key, {
|
|
720
|
+
expectedVersion: current.version,
|
|
721
|
+
altText: alt ?? prepared.filename,
|
|
722
|
+
});
|
|
723
|
+
gallery = { id: galleryIdArg, url: current.url };
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
gallery = {
|
|
727
|
+
id: galleryIdArg,
|
|
728
|
+
error: err instanceof Error ? err.message : String(err),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const flat = {
|
|
733
|
+
...result,
|
|
734
|
+
markdown,
|
|
735
|
+
backend: captured.backend,
|
|
736
|
+
optimize: {
|
|
737
|
+
optimized: prepared.optimized,
|
|
738
|
+
skippedReason: prepared.skippedReason,
|
|
739
|
+
originalBytes: prepared.originalBytes,
|
|
740
|
+
outputBytes: prepared.outputBytes,
|
|
741
|
+
filename: prepared.filename,
|
|
742
|
+
},
|
|
743
|
+
frame: prepared.frame,
|
|
744
|
+
gallery,
|
|
745
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
746
|
+
};
|
|
747
|
+
if (wantComment && target) {
|
|
748
|
+
const { comment, commentError } = await syncComment(client, target);
|
|
749
|
+
return { ...flat, comment, commentError };
|
|
750
|
+
}
|
|
751
|
+
return flat;
|
|
752
|
+
},
|
|
753
|
+
},
|
|
531
754
|
{
|
|
532
755
|
name: "attach",
|
|
533
756
|
description: "Upload one or more files as stable PR/issue attachments (in parallel) and maintain a managed GitHub comment. Returns `uploads` and `failures` (one bad file does not abort the batch). Each success has `url`, `embedUrl`, and `markdown` (prefer embedUrl for GitHub). With no pr/issue, targets the current branch PR. Attachments are public and keys are predictable; upload only non-sensitive media.",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type BrowserCandidateSource = "env" | "system" | "playwright-cache" | "puppeteer-cache";
|
|
2
|
+
export interface BrowserCandidate {
|
|
3
|
+
source: BrowserCandidateSource;
|
|
4
|
+
/** e.g. "chrome", "chromium", "chromium_headless_shell", "edge". */
|
|
5
|
+
kind: string;
|
|
6
|
+
executablePath: string;
|
|
7
|
+
/** Cache revision/build string, when applicable. */
|
|
8
|
+
revision?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DetectRoots {
|
|
11
|
+
platform?: NodeJS.Platform;
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
playwrightCacheDir?: string;
|
|
14
|
+
puppeteerCacheDir?: string;
|
|
15
|
+
/** Injectable for tests: overrides the fixed system-install paths. */
|
|
16
|
+
systemCandidates?: readonly {
|
|
17
|
+
kind: string;
|
|
18
|
+
path: string;
|
|
19
|
+
}[];
|
|
20
|
+
/** Injectable for tests: replaces fs.existsSync. */
|
|
21
|
+
exists?: (path: string) => boolean;
|
|
22
|
+
/** Injectable for tests: replaces fs.readdirSync. */
|
|
23
|
+
readdir?: (path: string) => string[];
|
|
24
|
+
}
|
|
25
|
+
export interface DetectResult {
|
|
26
|
+
/** Explicit --browser / UPLOADS_CHROME_PATH / CHROME_PATH override, if any. */
|
|
27
|
+
envOverride?: string;
|
|
28
|
+
candidates: BrowserCandidate[];
|
|
29
|
+
/** Best candidate by the documented ranking, or undefined if none found. */
|
|
30
|
+
winner?: BrowserCandidate;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Scan for a usable local Chromium-family executable. Pure fs/env — never
|
|
34
|
+
* launches a browser. Roots are injectable so tests can fake a cache layout.
|
|
35
|
+
*/
|
|
36
|
+
export declare function detectLocalBrowser(roots?: DetectRoots): DetectResult;
|
|
37
|
+
export interface LocalCaptureOptions {
|
|
38
|
+
/** URL to navigate to, or a `file://` URL for local .html targets. */
|
|
39
|
+
url: string;
|
|
40
|
+
/** Explicit browser executable (--browser / UPLOADS_CHROME_PATH / CHROME_PATH). */
|
|
41
|
+
browserPath?: string;
|
|
42
|
+
/** Attach to a running Chrome instead of launching one. */
|
|
43
|
+
cdp?: string;
|
|
44
|
+
viewport: {
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
deviceScaleFactor: number;
|
|
48
|
+
};
|
|
49
|
+
selector?: string;
|
|
50
|
+
fullPage?: boolean;
|
|
51
|
+
colorScheme?: "dark" | "light";
|
|
52
|
+
/** "load" | "domcontentloaded" | "networkidle", or a millisecond settle delay. */
|
|
53
|
+
waitUntil: "load" | "domcontentloaded" | "networkidle" | number;
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
detectRoots?: DetectRoots;
|
|
56
|
+
/**
|
|
57
|
+
* Pre-computed detection result from a caller that already scanned the
|
|
58
|
+
* filesystem (e.g. `auto`-routing's probe) — avoids re-running
|
|
59
|
+
* `detectLocalBrowser` a second time for the same capture.
|
|
60
|
+
*/
|
|
61
|
+
detectResult?: DetectResult;
|
|
62
|
+
}
|
|
63
|
+
/** Capture a PNG screenshot using a local (already-installed) browser. */
|
|
64
|
+
export declare function captureLocal(opts: LocalCaptureOptions): Promise<Uint8Array>;
|