@buildinternet/uploads 0.8.0 → 0.9.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 +2 -2
- package/dist/commands.js +84 -4
- package/dist/config-file.d.ts +3 -1
- package/dist/config-file.js +10 -0
- package/dist/github-gh.d.ts +7 -0
- package/dist/github-gh.js +24 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ Config layers (first match wins): CLI flags → env vars → `--env-file` → `~
|
|
|
98
98
|
|
|
99
99
|
## MCP server
|
|
100
100
|
|
|
101
|
-
`uploads mcp` serves the Model Context Protocol over stdio (newline-delimited JSON-RPC, no extra dependencies). Tools include file operations plus public gallery workflows: `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`. Gallery tools return API-provided canonical URLs and never need GitHub credentials. The remaining stdio tools are `put`, `attach`, `list`, `delete`, `usage`, `reconcile`, `purge_expired`, `comment`, `health`, and `doctor` — with the same config resolution and defaults, plus a per-call `workspace` argument. Interactive/credential commands (`setup`, `login`, `admin`, `config`) are not exposed. A token isn't required to start the server; auth errors surface per tool call (`health` needs no auth).
|
|
101
|
+
`uploads mcp` serves the Model Context Protocol over stdio (newline-delimited JSON-RPC, no extra dependencies). Tools include file operations plus public gallery workflows: `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`. Gallery tools return API-provided canonical URLs and never need GitHub credentials. The remaining stdio tools are `put`, `attach`, `list`, `delete`, `set_metadata`, `find_files`, `usage`, `reconcile`, `purge_expired`, `comment`, `health`, and `doctor` — with the same config resolution and defaults, plus a per-call `workspace` argument. `put` and `attach` accept a `metadata` param (same `gh.*` auto-injection as the CLI's `attach`); `set_metadata` and `find_files` mirror `uploads meta set` and `uploads find`. Interactive/credential commands (`setup`, `login`, `admin`, `config`) are not exposed. A token isn't required to start the server; auth errors surface per tool call (`health` needs no auth).
|
|
102
102
|
|
|
103
103
|
```json
|
|
104
104
|
{ "command": "uploads", "args": ["--env-file", "/path/to/.env", "mcp"] }
|
|
@@ -106,7 +106,7 @@ Config layers (first match wins): CLI flags → env vars → `--env-file` → `~
|
|
|
106
106
|
|
|
107
107
|
Or with `UPLOADS_TOKEN`/`UPLOADS_WORKSPACE` in the environment or user config. Claude Code: `claude mcp add uploads -- uploads --env-file /path/to/.env mcp`.
|
|
108
108
|
|
|
109
|
-
For HTTP clients there's also a hosted variant at `https://agents.uploads.sh/mcp` — the workspace is inferred from the bearer token, so only the URL and token are needed (`https://agents.uploads.sh/<workspace>/mcp` and the `mcp.uploads.sh` hostname also work). Tools: file operations plus `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`; all use the same bearer-token workspace scopes and gallery URLs come from the API — see `apps/mcp` in the repo. `uploads install` registers the skill + hosted MCP (short progress; `--verbose` for underlying output). Its `put` takes no content type: the stored type is sniffed server-side from the bytes and checked against the workspace allowlist, and writes are rate limited per workspace.
|
|
109
|
+
For HTTP clients there's also a hosted variant at `https://agents.uploads.sh/mcp` — the workspace is inferred from the bearer token, so only the URL and token are needed (`https://agents.uploads.sh/<workspace>/mcp` and the `mcp.uploads.sh` hostname also work). Tools: file operations plus `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`; all use the same bearer-token workspace scopes and gallery URLs come from the API — see `apps/mcp` in the repo. The hosted `put` also accepts a `metadata` param. `uploads install` registers the skill + hosted MCP (short progress; `--verbose` for underlying output). Its `put` takes no content type: the stored type is sniffed server-side from the bytes and checked against the workspace allowlist, and writes are rate limited per workspace.
|
|
110
110
|
|
|
111
111
|
## Programmatic use
|
|
112
112
|
|
package/dist/commands.js
CHANGED
|
@@ -9,7 +9,7 @@ import { UploadsError } from "./errors.js";
|
|
|
9
9
|
import { writeJson, writeStdout } from "./io.js";
|
|
10
10
|
import { parseMetaFlags, validateMetaMap } from "./metadata.js";
|
|
11
11
|
import { ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, attachmentsCommentBody, normalizeGithubCoordinate, } from "./github.js";
|
|
12
|
-
import { resolveRepo, resolveCurrentPullRequest, execRunner, upsertAttachmentsComment, } from "./github-gh.js";
|
|
12
|
+
import { resolveRepo, resolveCurrentPullRequest, classifyGhNumber, execRunner, upsertAttachmentsComment, } from "./github-gh.js";
|
|
13
13
|
import { resolvePutPrefix } from "./destinations.js";
|
|
14
14
|
import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
|
|
15
15
|
import { applyFrame, resolveFrameId } from "./frame.js";
|
|
@@ -65,6 +65,8 @@ Options:
|
|
|
65
65
|
--optimize-quality <1-100> WebP quality (default: 85)
|
|
66
66
|
--keep-exif Keep EXIF/XMP/ICC when optimizing (default: strip for privacy)
|
|
67
67
|
--no-git Don't derive --repo from git (or UPLOADS_NO_GIT=1)
|
|
68
|
+
--auto Resolve current PR/issue and stamp gh.* metadata (default on)
|
|
69
|
+
--no-auto Skip gh.* auto-resolution (also skipped by --no-git or UPLOADS_NO_AUTO_META=1)
|
|
68
70
|
--workspace, -w <name> Override workspace (wins over UPLOADS_WORKSPACE and token inference)
|
|
69
71
|
--format human|url|markdown|json
|
|
70
72
|
--pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
|
|
@@ -107,6 +109,24 @@ export function makeGhTarget(pr, issue, repoArg, run) {
|
|
|
107
109
|
function ghTargetFromFlags(flags, run) {
|
|
108
110
|
return makeGhTarget(flagInt(flags, "--pr", "--pr"), flagInt(flags, "--issue", "--issue"), flagString(flags, "--repo"), run);
|
|
109
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Best-effort GitHub target for the default put path (no --pr/--issue). A
|
|
114
|
+
* numeric --ref is classified as pull vs issue; otherwise the current branch's
|
|
115
|
+
* PR is resolved. Never throws — any failure yields undefined so the upload
|
|
116
|
+
* proceeds without gh metadata.
|
|
117
|
+
*/
|
|
118
|
+
function resolveAutoGhTarget(repoArg, ref, run) {
|
|
119
|
+
try {
|
|
120
|
+
const repo = resolveRepo(repoArg, run);
|
|
121
|
+
if (ref !== undefined && /^\d+$/.test(ref) && Number(ref) > 0) {
|
|
122
|
+
return classifyGhNumber(repo, Number.parseInt(ref, 10), run);
|
|
123
|
+
}
|
|
124
|
+
return resolveCurrentPullRequest(repo, run);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
110
130
|
/** Shared put/attach optimize flags + UPLOADS_NO_OPTIMIZE default. */
|
|
111
131
|
export function optimizeOptionsFromFlags(flags, defaults) {
|
|
112
132
|
if (flags.has("--no-optimize") && typeof flags.get("--no-optimize") === "string") {
|
|
@@ -363,6 +383,11 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
363
383
|
}
|
|
364
384
|
if (!ctx.quiet && comment)
|
|
365
385
|
process.stderr.write(`>> attachments comment ${comment.action}\n`);
|
|
386
|
+
// attach auto-writes gh.* metadata; point the user at how to find it later.
|
|
387
|
+
if (!ctx.quiet) {
|
|
388
|
+
const ref = ghMetadataFromTarget(target)["gh.ref"];
|
|
389
|
+
process.stderr.write(`>> find these later: uploads find gh.ref=${ref}\n`);
|
|
390
|
+
}
|
|
366
391
|
}
|
|
367
392
|
return 0;
|
|
368
393
|
}
|
|
@@ -390,13 +415,19 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
390
415
|
const nameFlag = flagString(parsed.flags, "--name");
|
|
391
416
|
const dryRun = flagBool(parsed.flags, "--dry-run");
|
|
392
417
|
// Validate --meta up front (fail fast, before reading/optimizing the file).
|
|
393
|
-
const
|
|
418
|
+
const userMeta = (() => {
|
|
394
419
|
const pairs = flagValues(parsed.flags, "--meta");
|
|
395
420
|
return pairs.length > 0 ? parseMetaFlags(pairs) : undefined;
|
|
396
421
|
})();
|
|
397
422
|
if (wantComment && typeof parsed.flags.get("--comment") === "string") {
|
|
398
423
|
throw new UsageError("--comment takes no value — place it after the file argument");
|
|
399
424
|
}
|
|
425
|
+
if (parsed.flags.has("--auto") && typeof parsed.flags.get("--auto") === "string") {
|
|
426
|
+
throw new UsageError("--auto takes no value");
|
|
427
|
+
}
|
|
428
|
+
if (parsed.flags.has("--no-auto") && typeof parsed.flags.get("--no-auto") === "string") {
|
|
429
|
+
throw new UsageError("--no-auto takes no value");
|
|
430
|
+
}
|
|
400
431
|
if (wantComment && !ghTarget)
|
|
401
432
|
throw new UsageError("--comment requires --pr or --issue");
|
|
402
433
|
if (ghTarget) {
|
|
@@ -474,6 +505,43 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
474
505
|
process.stderr.write(`>> ${note}\n`);
|
|
475
506
|
}
|
|
476
507
|
const noGit = flagBool(parsed.flags, "--no-git") || defaults.noGit === true;
|
|
508
|
+
// gh.* metadata: explicit --pr/--issue target wins over --meta; otherwise
|
|
509
|
+
// best-effort auto resolution (on by default) where --meta wins. --no-git,
|
|
510
|
+
// --no-auto, or UPLOADS_NO_AUTO_META disable auto; --auto forces past the
|
|
511
|
+
// config default but never past --no-git (no repo to resolve).
|
|
512
|
+
let metadata = userMeta;
|
|
513
|
+
let attachedRef;
|
|
514
|
+
if (ghTarget) {
|
|
515
|
+
const merged = { ...userMeta, ...ghMetadataFromTarget(ghTarget) };
|
|
516
|
+
validateMetaMap(merged); // enforce 24-key/8KB caps on the merged map (matches attach)
|
|
517
|
+
metadata = merged;
|
|
518
|
+
attachedRef = merged["gh.ref"];
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
const autoEnabled = !noGit &&
|
|
522
|
+
!flagBool(parsed.flags, "--no-auto") &&
|
|
523
|
+
(flagBool(parsed.flags, "--auto") || defaults.noAutoMeta !== true);
|
|
524
|
+
if (autoEnabled) {
|
|
525
|
+
const autoTarget = resolveAutoGhTarget(flagString(parsed.flags, "--repo") ?? defaults.repo, flagString(parsed.flags, "--ref") ?? defaults.ref, run);
|
|
526
|
+
if (autoTarget) {
|
|
527
|
+
const autoMeta = ghMetadataFromTarget(autoTarget);
|
|
528
|
+
const merged = { ...autoMeta, ...userMeta };
|
|
529
|
+
// Auto resolution must never fail the upload: if merging the gh.* pairs
|
|
530
|
+
// would exceed the metadata caps, drop them and upload with --meta only.
|
|
531
|
+
try {
|
|
532
|
+
validateMetaMap(merged);
|
|
533
|
+
metadata = merged;
|
|
534
|
+
attachedRef = merged["gh.ref"];
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// keep metadata = userMeta (already validated); skip auto gh.*
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (attachedRef && !ctx.quiet && format === "human") {
|
|
543
|
+
process.stderr.write(`>> attached to ${attachedRef}\n`);
|
|
544
|
+
}
|
|
477
545
|
let key = ghTarget ? ghAttachmentKey(ghTarget, filename) : keyHint;
|
|
478
546
|
if (key && prepared.optimized)
|
|
479
547
|
key = rewriteKeyExtension(key, filename);
|
|
@@ -805,8 +873,15 @@ async function runFindFiles(ctx, filters, flags) {
|
|
|
805
873
|
if (ctx.json)
|
|
806
874
|
await writeJson(result);
|
|
807
875
|
else
|
|
808
|
-
for (const item of result.items)
|
|
809
|
-
|
|
876
|
+
for (const item of result.items) {
|
|
877
|
+
// LIST_HELP promises matched metadata in the output; render it inline
|
|
878
|
+
// (sorted for stable output) so human mode honors that, not just --json.
|
|
879
|
+
const meta = Object.entries(item.metadata)
|
|
880
|
+
.toSorted(([a], [b]) => a.localeCompare(b))
|
|
881
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
882
|
+
.join(" ");
|
|
883
|
+
await writeStdout(`${item.key}${item.url ? ` ${item.url}` : ""}${meta ? ` ${meta}` : ""}\n`);
|
|
884
|
+
}
|
|
810
885
|
return 0;
|
|
811
886
|
}
|
|
812
887
|
export async function runList(ctx, args, help = false, run = execRunner) {
|
|
@@ -910,6 +985,11 @@ export async function runMeta(ctx, args, help = false) {
|
|
|
910
985
|
const result = await ctx.client.getMetadata(key);
|
|
911
986
|
if (ctx.json)
|
|
912
987
|
await writeJson(result);
|
|
988
|
+
else if (Object.keys(result.metadata).length === 0) {
|
|
989
|
+
// Empty stdout reads as failure; a stderr note keeps stdout parseable.
|
|
990
|
+
if (!ctx.quiet)
|
|
991
|
+
process.stderr.write("(no metadata)\n");
|
|
992
|
+
}
|
|
913
993
|
else
|
|
914
994
|
for (const [k, v] of Object.entries(result.metadata))
|
|
915
995
|
await writeStdout(`${k}=${v}\n`);
|
package/dist/config-file.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UploadsClientConfig } from "./config.js";
|
|
2
|
-
export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN", "UPLOADS_DEFAULT_PREFIX", "UPLOADS_DEFAULT_REPO", "UPLOADS_DEFAULT_REF", "UPLOADS_DEFAULT_WIDTH", "UPLOADS_NO_GIT", "UPLOADS_NO_OPTIMIZE", "UPLOADS_KEEP_EXIF"];
|
|
2
|
+
export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN", "UPLOADS_DEFAULT_PREFIX", "UPLOADS_DEFAULT_REPO", "UPLOADS_DEFAULT_REF", "UPLOADS_DEFAULT_WIDTH", "UPLOADS_NO_GIT", "UPLOADS_NO_OPTIMIZE", "UPLOADS_KEEP_EXIF", "UPLOADS_NO_AUTO_META"];
|
|
3
3
|
export type UploadsConfigKey = (typeof UPLOADS_CONFIG_KEYS)[number];
|
|
4
4
|
export type UploadsConfigValues = Partial<Record<UploadsConfigKey, string>>;
|
|
5
5
|
export interface PutDefaults {
|
|
@@ -12,6 +12,8 @@ export interface PutDefaults {
|
|
|
12
12
|
noOptimize?: boolean;
|
|
13
13
|
/** When true, optimize keeps EXIF/XMP/ICC (default strips). */
|
|
14
14
|
keepExif?: boolean;
|
|
15
|
+
/** When true, `put` does NOT auto-resolve/stamp gh.* on the default path. */
|
|
16
|
+
noAutoMeta?: boolean;
|
|
15
17
|
}
|
|
16
18
|
declare const PUT_DEFAULT_KEY_MAP: Record<keyof PutDefaults, UploadsConfigKey>;
|
|
17
19
|
export declare function putDefaultsToConfigValues(defaults: PutDefaults): UploadsConfigValues;
|
package/dist/config-file.js
CHANGED
|
@@ -12,6 +12,7 @@ export const UPLOADS_CONFIG_KEYS = [
|
|
|
12
12
|
"UPLOADS_NO_GIT",
|
|
13
13
|
"UPLOADS_NO_OPTIMIZE",
|
|
14
14
|
"UPLOADS_KEEP_EXIF",
|
|
15
|
+
"UPLOADS_NO_AUTO_META",
|
|
15
16
|
];
|
|
16
17
|
const PUT_DEFAULT_KEY_MAP = {
|
|
17
18
|
prefix: "UPLOADS_DEFAULT_PREFIX",
|
|
@@ -21,6 +22,7 @@ const PUT_DEFAULT_KEY_MAP = {
|
|
|
21
22
|
noGit: "UPLOADS_NO_GIT",
|
|
22
23
|
noOptimize: "UPLOADS_NO_OPTIMIZE",
|
|
23
24
|
keepExif: "UPLOADS_KEEP_EXIF",
|
|
25
|
+
noAutoMeta: "UPLOADS_NO_AUTO_META",
|
|
24
26
|
};
|
|
25
27
|
function isTruthyConfigFlag(value) {
|
|
26
28
|
if (!value)
|
|
@@ -44,6 +46,8 @@ export function putDefaultsToConfigValues(defaults) {
|
|
|
44
46
|
out.UPLOADS_NO_OPTIMIZE = "1";
|
|
45
47
|
if (defaults.keepExif)
|
|
46
48
|
out.UPLOADS_KEEP_EXIF = "1";
|
|
49
|
+
if (defaults.noAutoMeta)
|
|
50
|
+
out.UPLOADS_NO_AUTO_META = "1";
|
|
47
51
|
return out;
|
|
48
52
|
}
|
|
49
53
|
function parsePutDefaultsFromRaw(raw) {
|
|
@@ -65,6 +69,8 @@ function parsePutDefaultsFromRaw(raw) {
|
|
|
65
69
|
out.noOptimize = true;
|
|
66
70
|
if (isTruthyConfigFlag(raw.UPLOADS_KEEP_EXIF))
|
|
67
71
|
out.keepExif = true;
|
|
72
|
+
if (isTruthyConfigFlag(raw.UPLOADS_NO_AUTO_META))
|
|
73
|
+
out.noAutoMeta = true;
|
|
68
74
|
return out;
|
|
69
75
|
}
|
|
70
76
|
function parsePutDefaultsFromEnv() {
|
|
@@ -83,6 +89,8 @@ function parsePutDefaultsFromEnv() {
|
|
|
83
89
|
raw.UPLOADS_NO_OPTIMIZE = process.env.UPLOADS_NO_OPTIMIZE;
|
|
84
90
|
if (process.env.UPLOADS_KEEP_EXIF)
|
|
85
91
|
raw.UPLOADS_KEEP_EXIF = process.env.UPLOADS_KEEP_EXIF;
|
|
92
|
+
if (process.env.UPLOADS_NO_AUTO_META)
|
|
93
|
+
raw.UPLOADS_NO_AUTO_META = process.env.UPLOADS_NO_AUTO_META;
|
|
86
94
|
return parsePutDefaultsFromRaw(raw);
|
|
87
95
|
}
|
|
88
96
|
/** XDG default shared across buildinternet skills (github-screenshots, uploads, …). */
|
|
@@ -152,6 +160,8 @@ export function mergePutDefaults(...layers) {
|
|
|
152
160
|
out.noOptimize = layer.noOptimize;
|
|
153
161
|
if (layer.keepExif != null)
|
|
154
162
|
out.keepExif = layer.keepExif;
|
|
163
|
+
if (layer.noAutoMeta != null)
|
|
164
|
+
out.noAutoMeta = layer.noAutoMeta;
|
|
155
165
|
}
|
|
156
166
|
return out;
|
|
157
167
|
}
|
package/dist/github-gh.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ export declare const execRunner: CommandRunner;
|
|
|
9
9
|
export declare function resolveRepo(explicit: string | undefined, run?: CommandRunner): string;
|
|
10
10
|
/** Resolve the pull request associated with the current branch. */
|
|
11
11
|
export declare function resolveCurrentPullRequest(repo: string, run?: CommandRunner): GhTarget;
|
|
12
|
+
/**
|
|
13
|
+
* Classify a bare PR/issue number via the GitHub API so the default `put`
|
|
14
|
+
* path can stamp the right `gh.kind`. Returns undefined on any failure (gh
|
|
15
|
+
* missing, 404, network) — the caller treats that as "no gh context" and
|
|
16
|
+
* uploads without metadata.
|
|
17
|
+
*/
|
|
18
|
+
export declare function classifyGhNumber(repo: string, num: number, run?: CommandRunner): GhTarget | undefined;
|
|
12
19
|
/**
|
|
13
20
|
* Create the managed attachments comment, or edit it in place if it already
|
|
14
21
|
* exists. Never touches any other comment. Body is passed via stdin
|
package/dist/github-gh.js
CHANGED
|
@@ -61,6 +61,30 @@ export function resolveCurrentPullRequest(repo, run = execRunner) {
|
|
|
61
61
|
}
|
|
62
62
|
throw new UsageError("could not infer a pull request for the current branch — pass --pr <num> or --issue <num>");
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Classify a bare PR/issue number via the GitHub API so the default `put`
|
|
66
|
+
* path can stamp the right `gh.kind`. Returns undefined on any failure (gh
|
|
67
|
+
* missing, 404, network) — the caller treats that as "no gh context" and
|
|
68
|
+
* uploads without metadata.
|
|
69
|
+
*/
|
|
70
|
+
export function classifyGhNumber(repo, num, run = execRunner) {
|
|
71
|
+
try {
|
|
72
|
+
const out = run("gh", [
|
|
73
|
+
"api",
|
|
74
|
+
`repos/${repo}/issues/${num}`,
|
|
75
|
+
"--jq",
|
|
76
|
+
'if .pull_request then "pull" else "issue" end',
|
|
77
|
+
]).trim();
|
|
78
|
+
if (out === "pull")
|
|
79
|
+
return { repo, kind: "pull", num };
|
|
80
|
+
if (out === "issue")
|
|
81
|
+
return { repo, kind: "issues", num };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// gh missing / not found / network — caller skips
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
64
88
|
/**
|
|
65
89
|
* PR comments live on the issues endpoint, so one path covers PRs and issues.
|
|
66
90
|
* Only the first 100 comments are searched (accepted v1 limitation).
|