@buildinternet/uploads 0.36.0 → 0.37.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/dist/cli-catalog.js +5 -3
- package/dist/client.d.ts +33 -4
- package/dist/client.js +15 -3
- package/dist/commands.d.ts +17 -2
- package/dist/commands.js +119 -28
- package/dist/github.d.ts +18 -10
- package/dist/github.js +101 -68
- package/dist/index.d.ts +1 -1
- package/dist/mcp/tools.js +38 -8
- package/package.json +1 -1
package/dist/cli-catalog.js
CHANGED
|
@@ -173,15 +173,17 @@ export const ROOT_COMMANDS = [
|
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
name: "find",
|
|
176
|
-
usage: "find k=v...",
|
|
177
|
-
summary: "
|
|
176
|
+
usage: "find [k=v...] [--name <term>]",
|
|
177
|
+
summary: "Find objects by metadata and/or filename substring",
|
|
178
178
|
},
|
|
179
179
|
{
|
|
180
180
|
name: "meta",
|
|
181
|
-
summary: "Get/set
|
|
181
|
+
summary: "Get/set object metadata; discover workspace keys/values",
|
|
182
182
|
subcommands: [
|
|
183
183
|
{ name: "get", summary: "Show metadata for an object" },
|
|
184
184
|
{ name: "set", summary: "Merge-set and/or delete metadata pairs" },
|
|
185
|
+
{ name: "keys", summary: "List distinct metadata keys in the workspace" },
|
|
186
|
+
{ name: "values", summary: "List distinct values for one metadata key" },
|
|
185
187
|
],
|
|
186
188
|
},
|
|
187
189
|
{
|
package/dist/client.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export interface ListOptions {
|
|
|
47
47
|
export interface FindFilesOptions {
|
|
48
48
|
prefix?: string;
|
|
49
49
|
limit?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Case-insensitive substring match on object keys (`?name=`).
|
|
52
|
+
* At least one of non-empty `filters` or `name` is required.
|
|
53
|
+
*/
|
|
54
|
+
name?: string;
|
|
50
55
|
}
|
|
51
56
|
export interface FindFilesItem {
|
|
52
57
|
key: string;
|
|
@@ -56,6 +61,24 @@ export interface FindFilesItem {
|
|
|
56
61
|
export interface FindFilesResult {
|
|
57
62
|
items: FindFilesItem[];
|
|
58
63
|
cursor: string | null;
|
|
64
|
+
/** Present when a `name` term was used — true if the underlying query hit its cap. */
|
|
65
|
+
truncated?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface MetadataKeysResult {
|
|
68
|
+
keys: Array<{
|
|
69
|
+
key: string;
|
|
70
|
+
count: number;
|
|
71
|
+
distinctValues: number;
|
|
72
|
+
}>;
|
|
73
|
+
truncated: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface MetadataValuesResult {
|
|
76
|
+
key: string;
|
|
77
|
+
values: Array<{
|
|
78
|
+
value: string;
|
|
79
|
+
count: number;
|
|
80
|
+
}>;
|
|
81
|
+
truncated: boolean;
|
|
59
82
|
}
|
|
60
83
|
export interface GetMetadataResult {
|
|
61
84
|
metadata: Record<string, string>;
|
|
@@ -220,7 +243,7 @@ export interface FindGalleriesByReferenceOptions {
|
|
|
220
243
|
* anyway, defeating the point of the server-side gate, so the CLI surfaces
|
|
221
244
|
* the decline instead.
|
|
222
245
|
*/
|
|
223
|
-
export type GithubCommentDeclineReason = "app_unconfigured" | "not_installed" | "forbidden" | "not_authorized" | "unavailable";
|
|
246
|
+
export type GithubCommentDeclineReason = "app_unconfigured" | "not_installed" | "forbidden" | "not_authorized" | "actor_not_authorized" | "unavailable";
|
|
224
247
|
export type GithubCommentResult = {
|
|
225
248
|
posted: true;
|
|
226
249
|
action: "created" | "updated" | "skipped";
|
|
@@ -523,10 +546,16 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
523
546
|
/** `PATCH /v1/:workspace/files/:key` — merge `set`/`delete`; returns the merged map. */
|
|
524
547
|
patchMetadata(key: string, opts: PatchMetadataOptions): Promise<GetMetadataResult>;
|
|
525
548
|
/**
|
|
526
|
-
* `GET /v1/:workspace/files?meta.<k>=<v
|
|
527
|
-
* queryable metadata
|
|
549
|
+
* `GET /v1/:workspace/files?meta.<k>=<v>&…&name=…` — ANDed equality filter
|
|
550
|
+
* over queryable metadata and/or a case-insensitive filename substring.
|
|
551
|
+
* At least one of non-empty `filters` or `opts.name` is required.
|
|
552
|
+
* `filters` must be pre-validated when present (see `metadata.ts`).
|
|
528
553
|
*/
|
|
529
|
-
findFiles(filters
|
|
554
|
+
findFiles(filters?: Record<string, string>, opts?: FindFilesOptions): Promise<FindFilesResult>;
|
|
555
|
+
/** `GET /v1/:workspace/files/facets` — workspace metadata key vocabulary. */
|
|
556
|
+
listMetadataKeys(): Promise<MetadataKeysResult>;
|
|
557
|
+
/** `GET /v1/:workspace/files/facets?key=` — distinct values for one key. */
|
|
558
|
+
listMetadataValues(key: string): Promise<MetadataValuesResult>;
|
|
530
559
|
head(key: string): Promise<HeadResult>;
|
|
531
560
|
createGallery(opts: CreateGalleryOptions): Promise<Gallery>;
|
|
532
561
|
getGallery(id: string): Promise<Gallery>;
|
package/dist/client.js
CHANGED
|
@@ -405,19 +405,31 @@ export function createUploadsClient(config) {
|
|
|
405
405
|
});
|
|
406
406
|
},
|
|
407
407
|
/**
|
|
408
|
-
* `GET /v1/:workspace/files?meta.<k>=<v
|
|
409
|
-
* queryable metadata
|
|
408
|
+
* `GET /v1/:workspace/files?meta.<k>=<v>&…&name=…` — ANDed equality filter
|
|
409
|
+
* over queryable metadata and/or a case-insensitive filename substring.
|
|
410
|
+
* At least one of non-empty `filters` or `opts.name` is required.
|
|
411
|
+
* `filters` must be pre-validated when present (see `metadata.ts`).
|
|
410
412
|
*/
|
|
411
|
-
async findFiles(filters, opts = {}) {
|
|
413
|
+
async findFiles(filters = {}, opts = {}) {
|
|
412
414
|
const params = new URLSearchParams();
|
|
413
415
|
for (const [k, v] of Object.entries(filters))
|
|
414
416
|
params.append(`meta.${k}`, v);
|
|
417
|
+
if (opts.name)
|
|
418
|
+
params.set("name", opts.name);
|
|
415
419
|
if (opts.prefix)
|
|
416
420
|
params.set("prefix", opts.prefix);
|
|
417
421
|
if (opts.limit != null)
|
|
418
422
|
params.set("limit", String(opts.limit));
|
|
419
423
|
return request("GET", `${filesBase(config)}?${params.toString()}`);
|
|
420
424
|
},
|
|
425
|
+
/** `GET /v1/:workspace/files/facets` — workspace metadata key vocabulary. */
|
|
426
|
+
async listMetadataKeys() {
|
|
427
|
+
return request("GET", `${filesBase(config)}/facets`);
|
|
428
|
+
},
|
|
429
|
+
/** `GET /v1/:workspace/files/facets?key=` — distinct values for one key. */
|
|
430
|
+
async listMetadataValues(key) {
|
|
431
|
+
return request("GET", `${filesBase(config)}/facets?${new URLSearchParams({ key })}`);
|
|
432
|
+
},
|
|
421
433
|
async head(key) {
|
|
422
434
|
const result = await request("GET", `${filesBase(config)}/${encodeKeyPath(key)}`);
|
|
423
435
|
return { ...result, embedUrl: resolveEmbedUrl(result.url, result.embedUrl) };
|
package/dist/commands.d.ts
CHANGED
|
@@ -165,8 +165,10 @@ export interface AttachmentsCommentResult {
|
|
|
165
165
|
export declare function commentViaSuffix(via: AttachmentsCommentResult["via"]): string;
|
|
166
166
|
/**
|
|
167
167
|
* Thrown by `syncAttachmentsComment` when the server declines with
|
|
168
|
-
* `not_authorized` (issue #297 baseline control
|
|
169
|
-
* different workspace
|
|
168
|
+
* `not_authorized` (issue #297 baseline control — this repo is bound to a
|
|
169
|
+
* different workspace) or `actor_not_authorized` (issue #297 control 2 — the
|
|
170
|
+
* workspace requires the caller to be on the target PR/issue thread).
|
|
171
|
+
* Deliberately not caught by the generic "bot endpoint
|
|
170
172
|
* unreachable" fallback below: falling back to gh here would let the
|
|
171
173
|
* human's own credentials post anyway, defeating the point of the
|
|
172
174
|
* server-side gate.
|
|
@@ -490,6 +492,19 @@ export interface DoctorReport {
|
|
|
490
492
|
scopes?: string[];
|
|
491
493
|
/** Workspace/token mismatch warning (also present in hints). */
|
|
492
494
|
warning?: string;
|
|
495
|
+
/**
|
|
496
|
+
* Bring-your-own-bucket storage status (issue #583 Phase 3). `GET
|
|
497
|
+
* /me/workspaces/:name/storage` is session-gated (Better Auth cookie or
|
|
498
|
+
* bearer via the AUTH service — see `session-auth.ts`); the CLI only ever
|
|
499
|
+
* holds a minted `up_<workspace>_…` workspace token, never a session
|
|
500
|
+
* bearer, so doctor cannot reach that route today. Until a token-authed
|
|
501
|
+
* read path exists, this is an honest "can't check from here" rather than
|
|
502
|
+
* a fabricated mode.
|
|
503
|
+
*/
|
|
504
|
+
storage: {
|
|
505
|
+
checked: false;
|
|
506
|
+
note: string;
|
|
507
|
+
};
|
|
493
508
|
hints: string[];
|
|
494
509
|
/** `screenshot`'s local-browser detection (fs scans only — never launches a browser). */
|
|
495
510
|
browser: {
|
package/dist/commands.js
CHANGED
|
@@ -447,8 +447,10 @@ export function commentViaSuffix(via) {
|
|
|
447
447
|
}
|
|
448
448
|
/**
|
|
449
449
|
* Thrown by `syncAttachmentsComment` when the server declines with
|
|
450
|
-
* `not_authorized` (issue #297 baseline control
|
|
451
|
-
* different workspace
|
|
450
|
+
* `not_authorized` (issue #297 baseline control — this repo is bound to a
|
|
451
|
+
* different workspace) or `actor_not_authorized` (issue #297 control 2 — the
|
|
452
|
+
* workspace requires the caller to be on the target PR/issue thread).
|
|
453
|
+
* Deliberately not caught by the generic "bot endpoint
|
|
452
454
|
* unreachable" fallback below: falling back to gh here would let the
|
|
453
455
|
* human's own credentials post anyway, defeating the point of the
|
|
454
456
|
* server-side gate.
|
|
@@ -485,6 +487,14 @@ export async function syncAttachmentsComment(client, target, run, workspace, opt
|
|
|
485
487
|
`Run \`uploads github link --status --repo ${target.repo}\` to see who owns the ` +
|
|
486
488
|
`binding, use that workspace instead, or post the comment manually with gh.`);
|
|
487
489
|
}
|
|
490
|
+
// Actor-on-PR gate (issue #297 control 2, workspace opt-in): same
|
|
491
|
+
// no-gh-fallback rule as not_authorized — the workspace explicitly asked
|
|
492
|
+
// the server to hold this line, so the CLI shouldn't route around it.
|
|
493
|
+
if (bot.reason === "actor_not_authorized") {
|
|
494
|
+
throw new GithubCommentAuthorizationError(`${bot.message ?? `You are not an actor on ${target.repo}#${target.num}.`}\n` +
|
|
495
|
+
`Ask an authorized thread participant to run this, or post the ` +
|
|
496
|
+
`comment manually with gh.`);
|
|
497
|
+
}
|
|
488
498
|
// Installed-but-unapproved is a fixable misconfiguration, not a silent
|
|
489
499
|
// degrade: tell the user (and how to fix it) before falling back to gh.
|
|
490
500
|
if (bot.reason === "forbidden" && bot.message) {
|
|
@@ -1384,7 +1394,7 @@ export function mergeStagingMeta(base, target) {
|
|
|
1384
1394
|
* for both the human-mode stderr line and the JSON `hint` field.
|
|
1385
1395
|
*/
|
|
1386
1396
|
export function putStagingNoteText(branch) {
|
|
1387
|
-
return (`note: staged for branch ${branch} — auto-
|
|
1397
|
+
return (`note: staged for branch ${branch} — auto-comments to pull request when opened ` +
|
|
1388
1398
|
`(or run: uploads attach --promote once it exists). Use --ref/--prefix for a plain dated upload.`);
|
|
1389
1399
|
}
|
|
1390
1400
|
/**
|
|
@@ -2134,31 +2144,45 @@ export async function runGallery(ctx, args, help = false) {
|
|
|
2134
2144
|
}
|
|
2135
2145
|
}
|
|
2136
2146
|
// --- list ---
|
|
2137
|
-
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--meta <k=v>]... [--workspace <name>]
|
|
2147
|
+
const LIST_HELP = `uploads list [--prefix <p>] [--pr <num> | --issue <num>] [--repo <owner/name>] [--limit <n>] [--cursor <c>] [--all] [--meta <k=v>]... [--name <term>] [--workspace <name>]
|
|
2138
2148
|
|
|
2139
2149
|
Default prefix: UPLOADS_DEFAULT_PREFIX (screenshots if unset).
|
|
2140
2150
|
|
|
2141
|
-
--meta <k=v> (repeatable, ANDed)
|
|
2142
|
-
returned items include their matched metadata. Combines with
|
|
2143
|
-
with --pr/--issue/--all.
|
|
2151
|
+
--meta <k=v> (repeatable, ANDed) and/or --name <term> switch to the search
|
|
2152
|
+
endpoint — returned items include their matched metadata. Combines with
|
|
2153
|
+
--prefix, not with --pr/--issue/--all. --name is a case-insensitive
|
|
2154
|
+
substring match on object keys. See also: uploads find.
|
|
2144
2155
|
|
|
2145
2156
|
Examples:
|
|
2146
2157
|
uploads list --prefix screenshots/
|
|
2147
2158
|
uploads list --pr 123
|
|
2148
2159
|
uploads list --all --json
|
|
2149
2160
|
uploads list --meta gh.repo=buildinternet/uploads --meta gh.number=123
|
|
2161
|
+
uploads list --name hero --meta app=web
|
|
2150
2162
|
`;
|
|
2151
|
-
/**
|
|
2152
|
-
|
|
2163
|
+
/** Human-mode stderr note when a paged API response was capped server-side. */
|
|
2164
|
+
function writeTruncatedNotice(truncated, quiet, detail) {
|
|
2165
|
+
if (truncated && !quiet) {
|
|
2166
|
+
process.stderr.write(`truncated: true (${detail})\n`);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
/** `--meta` / `--name` search path, shared by `runList` and `runFind`. */
|
|
2170
|
+
async function runFindFiles(ctx, filters, flags, name) {
|
|
2153
2171
|
if (flagString(flags, "--cursor") !== undefined) {
|
|
2154
|
-
throw new UsageError("--cursor is not supported with metadata filters");
|
|
2172
|
+
throw new UsageError("--cursor is not supported with metadata or name filters");
|
|
2155
2173
|
}
|
|
2156
2174
|
const prefix = flagString(flags, "--prefix");
|
|
2157
2175
|
const limit = flagInt(flags, "--limit", "--limit");
|
|
2158
|
-
const
|
|
2176
|
+
const nameTerm = name ?? flagString(flags, "--name");
|
|
2177
|
+
if (Object.keys(filters).length === 0 && !nameTerm) {
|
|
2178
|
+
throw new UsageError("find requires at least one k=v pair, --meta k=v, or --name <term>", {
|
|
2179
|
+
example: "uploads find path=/settings state=after",
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
const result = await ctx.client.findFiles(filters, { prefix, limit, name: nameTerm });
|
|
2159
2183
|
if (ctx.json)
|
|
2160
2184
|
await writeJson(result);
|
|
2161
|
-
else
|
|
2185
|
+
else {
|
|
2162
2186
|
for (const item of result.items) {
|
|
2163
2187
|
// LIST_HELP promises matched metadata in the output; render it inline
|
|
2164
2188
|
// (sorted for stable output) so human mode honors that, not just --json.
|
|
@@ -2168,6 +2192,8 @@ async function runFindFiles(ctx, filters, flags) {
|
|
|
2168
2192
|
.join(" ");
|
|
2169
2193
|
await writeStdout(`${item.key}${item.url ? ` ${item.url}` : ""}${meta ? ` ${meta}` : ""}\n`);
|
|
2170
2194
|
}
|
|
2195
|
+
writeTruncatedNotice(result.truncated, ctx.quiet, "more matches may exist beyond this page");
|
|
2196
|
+
}
|
|
2171
2197
|
return 0;
|
|
2172
2198
|
}
|
|
2173
2199
|
export async function runList(ctx, args, help = false, run = execRunner) {
|
|
@@ -2177,14 +2203,15 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
2177
2203
|
return 0;
|
|
2178
2204
|
}
|
|
2179
2205
|
const metaPairs = flagValues(parsed.flags, "--meta");
|
|
2180
|
-
|
|
2206
|
+
const nameFlag = flagString(parsed.flags, "--name");
|
|
2207
|
+
if (metaPairs.length > 0 || nameFlag !== undefined) {
|
|
2181
2208
|
if (ghTargetFromFlags(parsed.flags, run)) {
|
|
2182
|
-
throw new UsageError("--meta cannot be combined with --pr/--issue");
|
|
2209
|
+
throw new UsageError("--meta/--name cannot be combined with --pr/--issue");
|
|
2183
2210
|
}
|
|
2184
2211
|
if (flagBool(parsed.flags, "--all")) {
|
|
2185
|
-
throw new UsageError("--meta cannot be combined with --all");
|
|
2212
|
+
throw new UsageError("--meta/--name cannot be combined with --all");
|
|
2186
2213
|
}
|
|
2187
|
-
return runFindFiles(ctx, parseMetaFlags(metaPairs), parsed.flags);
|
|
2214
|
+
return runFindFiles(ctx, metaPairs.length > 0 ? parseMetaFlags(metaPairs) : {}, parsed.flags, nameFlag);
|
|
2188
2215
|
}
|
|
2189
2216
|
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
2190
2217
|
const prefixFlag = flagString(parsed.flags, "--prefix");
|
|
@@ -2219,15 +2246,21 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
2219
2246
|
return 0;
|
|
2220
2247
|
}
|
|
2221
2248
|
// --- find ---
|
|
2222
|
-
const FIND_HELP = `uploads find k=v [k=v...] [--prefix <p>] [--limit <n>] [--workspace <name>]
|
|
2249
|
+
const FIND_HELP = `uploads find [k=v...] [--meta k=v]... [--name <term>] [--prefix <p>] [--limit <n>] [--workspace <name>]
|
|
2250
|
+
|
|
2251
|
+
Find objects by queryable metadata (ANDed equality) and/or a case-insensitive
|
|
2252
|
+
filename substring. Same output as \`uploads list --meta\` / \`--name\`.
|
|
2223
2253
|
|
|
2224
|
-
|
|
2225
|
-
|
|
2254
|
+
Pairs are positional k=v, or spelled --meta k=v. A bare positional without
|
|
2255
|
+
\`=\` is treated as --name (e.g. \`uploads find hero\`). At least one of a
|
|
2256
|
+
meta pair or a name term is required.
|
|
2226
2257
|
|
|
2227
2258
|
Examples:
|
|
2228
2259
|
uploads find gh.repo=buildinternet/uploads gh.number=123
|
|
2229
2260
|
uploads find path=/settings state=after --prefix screenshots/
|
|
2230
2261
|
uploads find --meta path=/settings
|
|
2262
|
+
uploads find hero
|
|
2263
|
+
uploads find --name hero --meta app=web
|
|
2231
2264
|
`;
|
|
2232
2265
|
export async function runFind(ctx, args, help = false) {
|
|
2233
2266
|
const parsed = parseCommandArgs(args);
|
|
@@ -2237,24 +2270,43 @@ export async function runFind(ctx, args, help = false) {
|
|
|
2237
2270
|
}
|
|
2238
2271
|
// Same flag/positional symmetry as `meta set` (issue #545): `find` is the
|
|
2239
2272
|
// alias for `list --meta`, so `find --meta k=v` must not dead-end.
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2273
|
+
// Bare positionals without `=` are the filename term (issue #528) so
|
|
2274
|
+
// `uploads find hero` works without a flag. Empty input is rejected inside
|
|
2275
|
+
// `runFindFiles` (shared with `list --name` / `--meta`).
|
|
2276
|
+
const nameFlag = flagString(parsed.flags, "--name");
|
|
2277
|
+
const kvPositionals = [];
|
|
2278
|
+
const bareNames = [];
|
|
2279
|
+
for (const pos of parsed.positionals) {
|
|
2280
|
+
if (pos.includes("="))
|
|
2281
|
+
kvPositionals.push(pos);
|
|
2282
|
+
else
|
|
2283
|
+
bareNames.push(pos);
|
|
2284
|
+
}
|
|
2285
|
+
if (bareNames.length > 1) {
|
|
2286
|
+
throw new UsageError("find accepts at most one bare name term (or use --name)", {
|
|
2287
|
+
example: "uploads find hero --meta app=web",
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
if (bareNames.length === 1 && nameFlag !== undefined) {
|
|
2291
|
+
throw new UsageError("pass the name term either as a bare positional or --name, not both", {
|
|
2292
|
+
example: "uploads find hero",
|
|
2244
2293
|
});
|
|
2245
2294
|
}
|
|
2246
|
-
const
|
|
2247
|
-
return runFindFiles(ctx,
|
|
2295
|
+
const pairs = [...kvPositionals, ...flagValues(parsed.flags, "--meta")];
|
|
2296
|
+
return runFindFiles(ctx, pairs.length > 0 ? parseMetaFlags(pairs) : {}, parsed.flags, nameFlag ?? bareNames[0]);
|
|
2248
2297
|
}
|
|
2249
2298
|
// --- meta ---
|
|
2250
2299
|
const META_HELP = `uploads meta <command> [args]
|
|
2251
2300
|
|
|
2252
2301
|
Read/write an object's queryable custom metadata (D1-backed key-value pairs;
|
|
2253
|
-
distinct from the R2 provenance headers put on upload).
|
|
2302
|
+
distinct from the R2 provenance headers put on upload). Discover which keys
|
|
2303
|
+
and values exist in the workspace before filtering with find/list.
|
|
2254
2304
|
|
|
2255
2305
|
Commands:
|
|
2256
2306
|
get <key> Show metadata for an object
|
|
2257
2307
|
set <key> k=v [k=v...] [--delete k]... Merge-set and/or delete pairs
|
|
2308
|
+
keys List distinct metadata keys (with counts)
|
|
2309
|
+
values <meta-key> List distinct values for one key
|
|
2258
2310
|
|
|
2259
2311
|
Pairs take either form: positional k=v, or --meta k=v (same spelling as
|
|
2260
2312
|
put/screenshot/list). Both can appear in one call.
|
|
@@ -2264,6 +2316,8 @@ Examples:
|
|
|
2264
2316
|
uploads meta set screenshots/myapp/42/shot.png path=/settings state=after
|
|
2265
2317
|
uploads meta set screenshots/myapp/42/shot.png --meta path=/settings
|
|
2266
2318
|
uploads meta set screenshots/myapp/42/shot.png --delete path --delete state
|
|
2319
|
+
uploads meta keys
|
|
2320
|
+
uploads meta values app
|
|
2267
2321
|
`;
|
|
2268
2322
|
export async function runMeta(ctx, args, help = false) {
|
|
2269
2323
|
const parsed = parseCommandArgs(args);
|
|
@@ -2273,7 +2327,7 @@ export async function runMeta(ctx, args, help = false) {
|
|
|
2273
2327
|
return 0;
|
|
2274
2328
|
}
|
|
2275
2329
|
if (!action) {
|
|
2276
|
-
throw new UsageError("meta requires a subcommand: get or
|
|
2330
|
+
throw new UsageError("meta requires a subcommand: get, set, keys, or values", {
|
|
2277
2331
|
example: "uploads meta set screenshots/myapp/42/shot.png --meta path=/settings",
|
|
2278
2332
|
});
|
|
2279
2333
|
}
|
|
@@ -2330,8 +2384,40 @@ export async function runMeta(ctx, args, help = false) {
|
|
|
2330
2384
|
await resyncCommentAfterMetaSet(ctx, key, [...Object.keys(set ?? {}), ...del]);
|
|
2331
2385
|
return 0;
|
|
2332
2386
|
}
|
|
2387
|
+
case "keys": {
|
|
2388
|
+
// Workspace vocabulary discovery (issue #528) — which meta keys exist
|
|
2389
|
+
// and how common they are, before agents guess at find filters.
|
|
2390
|
+
const result = await ctx.client.listMetadataKeys();
|
|
2391
|
+
if (ctx.json)
|
|
2392
|
+
await writeJson(result);
|
|
2393
|
+
else {
|
|
2394
|
+
for (const row of result.keys) {
|
|
2395
|
+
await writeStdout(`${row.key} count=${row.count} distinct=${row.distinctValues}\n`);
|
|
2396
|
+
}
|
|
2397
|
+
writeTruncatedNotice(result.truncated, ctx.quiet, "more keys may exist beyond this page");
|
|
2398
|
+
}
|
|
2399
|
+
return 0;
|
|
2400
|
+
}
|
|
2401
|
+
case "values": {
|
|
2402
|
+
const key = parsed.positionals[1];
|
|
2403
|
+
if (!key) {
|
|
2404
|
+
throw new UsageError("meta values requires a metadata key", {
|
|
2405
|
+
example: "uploads meta values app",
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
const result = await ctx.client.listMetadataValues(key);
|
|
2409
|
+
if (ctx.json)
|
|
2410
|
+
await writeJson(result);
|
|
2411
|
+
else {
|
|
2412
|
+
for (const row of result.values) {
|
|
2413
|
+
await writeStdout(`${row.value} count=${row.count}\n`);
|
|
2414
|
+
}
|
|
2415
|
+
writeTruncatedNotice(result.truncated, ctx.quiet, "more values may exist beyond this page");
|
|
2416
|
+
}
|
|
2417
|
+
return 0;
|
|
2418
|
+
}
|
|
2333
2419
|
default:
|
|
2334
|
-
throw new UsageError(`unknown meta command: ${action} (expected get or
|
|
2420
|
+
throw new UsageError(`unknown meta command: ${action} (expected get, set, keys, or values)`, {
|
|
2335
2421
|
example: "uploads meta get screenshots/myapp/42/shot.png",
|
|
2336
2422
|
});
|
|
2337
2423
|
}
|
|
@@ -2845,6 +2931,10 @@ export async function buildDoctorReport(config, client, detectRoots) {
|
|
|
2845
2931
|
usage,
|
|
2846
2932
|
scopes,
|
|
2847
2933
|
warning: mismatch,
|
|
2934
|
+
storage: {
|
|
2935
|
+
checked: false,
|
|
2936
|
+
note: "not checked from the CLI — storage settings (shared vs. bring-your-own-bucket) live behind a signed-in session; sign in on the web (Account → workspace → Settings) to view mode and verification status",
|
|
2937
|
+
},
|
|
2848
2938
|
hints,
|
|
2849
2939
|
browser,
|
|
2850
2940
|
};
|
|
@@ -2881,6 +2971,7 @@ export async function runDoctor(ctx, args, help = false) {
|
|
|
2881
2971
|
else {
|
|
2882
2972
|
lines.push(`browser: ${report.browser.note ?? "not supported in this runtime"}`);
|
|
2883
2973
|
}
|
|
2974
|
+
lines.push(`storage: ${report.storage.note}`);
|
|
2884
2975
|
if (report.warning)
|
|
2885
2976
|
lines.push(`warning: ${report.warning}`);
|
|
2886
2977
|
for (const h of report.hints)
|
package/dist/github.d.ts
CHANGED
|
@@ -76,8 +76,8 @@ export declare function attachmentsMarker(workspace?: string): string;
|
|
|
76
76
|
export declare const MAX_INLINE_ATTACHMENT_IMAGES = 16;
|
|
77
77
|
/**
|
|
78
78
|
* Per-render knobs for the managed comment (issue #307), sourced from repo
|
|
79
|
-
* comment config. `imageWidth: "auto"`
|
|
80
|
-
*
|
|
79
|
+
* comment config. `imageWidth: "auto"` uses per-item filename heuristics plus
|
|
80
|
+
* density-aware sizing (solo/sparse/dense from the inlined count); `"full"`
|
|
81
81
|
* omits the `width` attribute entirely; a number overrides every width site.
|
|
82
82
|
*/
|
|
83
83
|
export interface CommentRenderOptions {
|
|
@@ -134,21 +134,29 @@ export interface GalleryCommentItem {
|
|
|
134
134
|
itemUrl?: string;
|
|
135
135
|
}[];
|
|
136
136
|
}
|
|
137
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* How crowded the managed comment is. Sparse comments (one shot, a single
|
|
139
|
+
* before/after) get larger embeds; dense comments keep compact historical sizes.
|
|
140
|
+
*/
|
|
141
|
+
export type AttachmentDensity = "solo" | "sparse" | "dense";
|
|
142
|
+
/** Dense (historical) default max width for images in the managed comment. */
|
|
138
143
|
export declare const ATTACHMENT_IMAGE_WIDTH_DEFAULT = 400;
|
|
139
144
|
/** Portrait / device mockups — keep phones readable, not full-column. */
|
|
140
145
|
export declare const ATTACHMENT_IMAGE_WIDTH_PORTRAIT = 280;
|
|
141
146
|
/** Wide UI / browser chrome. */
|
|
142
147
|
export declare const ATTACHMENT_IMAGE_WIDTH_WIDE = 640;
|
|
148
|
+
/** Dense pair-cell cap (side-by-side before/after). */
|
|
149
|
+
export declare const ATTACHMENT_IMAGE_WIDTH_PAIR = 320;
|
|
150
|
+
/** Map an inlined-media count onto a density tier. */
|
|
151
|
+
export declare function attachmentDensityForCount(inlinedCount: number): AttachmentDensity;
|
|
152
|
+
/** Pair-cell cap for the given density. */
|
|
153
|
+
export declare function attachmentPairWidth(density?: AttachmentDensity): number;
|
|
143
154
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
155
|
+
* Display width for a GitHub comment embed. Filenames are a weak but practical
|
|
156
|
+
* signal (we don't re-fetch dimensions when rebuilding the comment). `density`
|
|
157
|
+
* only affects managed-comment auto layout; other callers leave it `"dense"`.
|
|
146
158
|
*/
|
|
147
|
-
export declare function attachmentImageWidth(filename: string): number;
|
|
148
|
-
/** Max display width for one image inside a before/after pair row — smaller
|
|
149
|
-
* than a standalone image so two side by side stay under GitHub's comment
|
|
150
|
-
* column width (and don't overflow on mobile). */
|
|
151
|
-
export declare const ATTACHMENT_IMAGE_WIDTH_PAIR = 320;
|
|
159
|
+
export declare function attachmentImageWidth(filename: string, density?: AttachmentDensity): number;
|
|
152
160
|
/**
|
|
153
161
|
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
154
162
|
* intentionally preserves the legacy attachment-only body byte-for-byte.
|
package/dist/github.js
CHANGED
|
@@ -171,26 +171,52 @@ export const AUTO_RENDER_OPTIONS = {
|
|
|
171
171
|
metaState: true,
|
|
172
172
|
note: null,
|
|
173
173
|
};
|
|
174
|
-
/**
|
|
174
|
+
/** Dense (historical) default max width for images in the managed comment. */
|
|
175
175
|
export const ATTACHMENT_IMAGE_WIDTH_DEFAULT = 400;
|
|
176
176
|
/** Portrait / device mockups — keep phones readable, not full-column. */
|
|
177
177
|
export const ATTACHMENT_IMAGE_WIDTH_PORTRAIT = 280;
|
|
178
178
|
/** Wide UI / browser chrome. */
|
|
179
179
|
export const ATTACHMENT_IMAGE_WIDTH_WIDE = 640;
|
|
180
|
+
/** Dense pair-cell cap (side-by-side before/after). */
|
|
181
|
+
export const ATTACHMENT_IMAGE_WIDTH_PAIR = 320;
|
|
182
|
+
/** Per-density widths for `imageWidth: "auto"`. Dense reuses the exports above. */
|
|
183
|
+
const WIDTH_BY_DENSITY = {
|
|
184
|
+
solo: { default: 720, portrait: 360, wide: 800, pair: 400 },
|
|
185
|
+
sparse: { default: 560, portrait: 300, wide: 720, pair: 380 },
|
|
186
|
+
dense: {
|
|
187
|
+
default: ATTACHMENT_IMAGE_WIDTH_DEFAULT,
|
|
188
|
+
portrait: ATTACHMENT_IMAGE_WIDTH_PORTRAIT,
|
|
189
|
+
wide: ATTACHMENT_IMAGE_WIDTH_WIDE,
|
|
190
|
+
pair: ATTACHMENT_IMAGE_WIDTH_PAIR,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
/** Map an inlined-media count onto a density tier. */
|
|
194
|
+
export function attachmentDensityForCount(inlinedCount) {
|
|
195
|
+
if (inlinedCount <= 1)
|
|
196
|
+
return "solo";
|
|
197
|
+
if (inlinedCount <= 3)
|
|
198
|
+
return "sparse";
|
|
199
|
+
return "dense";
|
|
200
|
+
}
|
|
201
|
+
/** Pair-cell cap for the given density. */
|
|
202
|
+
export function attachmentPairWidth(density = "dense") {
|
|
203
|
+
return WIDTH_BY_DENSITY[density].pair;
|
|
204
|
+
}
|
|
180
205
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
206
|
+
* Display width for a GitHub comment embed. Filenames are a weak but practical
|
|
207
|
+
* signal (we don't re-fetch dimensions when rebuilding the comment). `density`
|
|
208
|
+
* only affects managed-comment auto layout; other callers leave it `"dense"`.
|
|
183
209
|
*/
|
|
184
|
-
export function attachmentImageWidth(filename) {
|
|
210
|
+
export function attachmentImageWidth(filename, density = "dense") {
|
|
211
|
+
const table = WIDTH_BY_DENSITY[density];
|
|
185
212
|
const n = filename.toLowerCase();
|
|
186
|
-
if (/(?:^|[-_.])(browser|desktop|dashboard|wide)(?:[-_.]|$)/.test(n))
|
|
187
|
-
return
|
|
188
|
-
}
|
|
213
|
+
if (/(?:^|[-_.])(browser|desktop|dashboard|wide)(?:[-_.]|$)/.test(n))
|
|
214
|
+
return table.wide;
|
|
189
215
|
if (/(?:^|[-_.])(phone|iphone|ipad|pixel|android|mobile|device)(?:[-_.]|$)/.test(n) ||
|
|
190
216
|
/iphone|pixel-?\d/.test(n)) {
|
|
191
|
-
return
|
|
217
|
+
return table.portrait;
|
|
192
218
|
}
|
|
193
|
-
return
|
|
219
|
+
return table.default;
|
|
194
220
|
}
|
|
195
221
|
/** `m:ss` under an hour, `h:mm:ss` at or above one. */
|
|
196
222
|
function formatDuration(seconds) {
|
|
@@ -205,19 +231,16 @@ function formatDuration(seconds) {
|
|
|
205
231
|
}
|
|
206
232
|
/**
|
|
207
233
|
* Display width for a video poster. Real dimensions only *select* among the
|
|
208
|
-
*
|
|
209
|
-
* result is capped at the real width so a small clip is never upscaled.
|
|
234
|
+
* density table's tiers — a raw 1920 would blow out the comment column — and
|
|
235
|
+
* the result is capped at the real width so a small clip is never upscaled.
|
|
210
236
|
*/
|
|
211
|
-
function posterImageWidth(videoMeta, filename) {
|
|
237
|
+
function posterImageWidth(videoMeta, filename, density = "dense") {
|
|
212
238
|
const w = videoMeta?.width ?? 0;
|
|
213
239
|
const h = videoMeta?.height ?? 0;
|
|
214
240
|
if (w <= 0 || h <= 0)
|
|
215
|
-
return attachmentImageWidth(filename);
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
: w / h >= 16 / 9
|
|
219
|
-
? ATTACHMENT_IMAGE_WIDTH_WIDE
|
|
220
|
-
: ATTACHMENT_IMAGE_WIDTH_DEFAULT;
|
|
241
|
+
return attachmentImageWidth(filename, density);
|
|
242
|
+
const table = WIDTH_BY_DENSITY[density];
|
|
243
|
+
const chosen = h > w ? table.portrait : w / h >= 16 / 9 ? table.wide : table.default;
|
|
221
244
|
return Math.min(chosen, w);
|
|
222
245
|
}
|
|
223
246
|
function escapeHtmlAttr(s) {
|
|
@@ -236,45 +259,37 @@ function escapeMarkdownText(s) {
|
|
|
236
259
|
return s.replace(/([\\`*_[\]~])/g, "\\$1");
|
|
237
260
|
}
|
|
238
261
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* metadata stays byte-identical to the pre-#365 render.
|
|
242
|
-
*
|
|
243
|
-
* Neither value is pre-sanitized: metadata values are printable ASCII up to
|
|
244
|
-
* 512 chars, and while the CLI validates `--state` against a closed enum,
|
|
245
|
-
* `PATCH /v1/:workspace/files/:key` can set any valid metadata value. A
|
|
246
|
-
* whitespace-only value passes that validation (length-1 printable ASCII), so
|
|
247
|
-
* treat it as absent rather than rendering a dangling separator.
|
|
248
|
-
*
|
|
249
|
-
* Bare `/` is stored/searchable but omitted from captions (issue #375) —
|
|
250
|
-
* alone it is a stray character, and as a prefix next to `state` it is
|
|
251
|
-
* noise. Only exact `/` after trim is suppressed.
|
|
262
|
+
* Collect path then state for a caption (issue #365). Bare `/` and
|
|
263
|
+
* whitespace-only values are omitted (issue #375). Empty when nothing usable.
|
|
252
264
|
*/
|
|
253
|
-
function
|
|
254
|
-
const
|
|
265
|
+
function metaCaptionValues(meta, options) {
|
|
266
|
+
const values = [];
|
|
255
267
|
const path = meta?.path?.trim();
|
|
256
268
|
if (options.metaPath && path && path !== "/")
|
|
257
|
-
|
|
269
|
+
values.push(path);
|
|
258
270
|
const state = meta?.state?.trim();
|
|
259
271
|
if (options.metaState && state)
|
|
260
|
-
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
/** `<sub>` caption body for an inline image, or null when there is nothing to say. */
|
|
264
|
-
function metaCaptionHtml(meta, options) {
|
|
265
|
-
const parts = metaCaptionParts(meta, options);
|
|
266
|
-
return parts.length > 0 ? parts.map(escapeHtmlText).join(" · ") : null;
|
|
272
|
+
values.push(state);
|
|
273
|
+
return values;
|
|
267
274
|
}
|
|
268
275
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
276
|
+
* Format path/state as code tokens. HTML → `<code>…</code>`; markdown →
|
|
277
|
+
* `` `…` `` (backslash-escape if the value itself contains a backtick).
|
|
278
|
+
* Returns `""` when there is nothing to say.
|
|
272
279
|
*/
|
|
273
|
-
function
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
280
|
+
function formatMetaCaption(meta, options, mode) {
|
|
281
|
+
const values = metaCaptionValues(meta, options);
|
|
282
|
+
if (values.length === 0)
|
|
276
283
|
return "";
|
|
277
|
-
|
|
284
|
+
if (mode === "html") {
|
|
285
|
+
return values.map((v) => `<code>${escapeHtmlText(v)}</code>`).join(" · ");
|
|
286
|
+
}
|
|
287
|
+
return values
|
|
288
|
+
.map((v) => {
|
|
289
|
+
const esc = escapeHtmlText(v);
|
|
290
|
+
return esc.includes("`") ? escapeMarkdownText(esc) : `\`${esc}\``;
|
|
291
|
+
})
|
|
292
|
+
.join(" · ");
|
|
278
293
|
}
|
|
279
294
|
/** Resolved pixel width for an image site, or `null` meaning "omit the width
|
|
280
295
|
* attribute". `"auto"` defers to the caller's per-item heuristic (`autoPx`);
|
|
@@ -388,28 +403,37 @@ function pairAttachments(items, isImageAt) {
|
|
|
388
403
|
}
|
|
389
404
|
return { partnerOf, roleOf };
|
|
390
405
|
}
|
|
391
|
-
|
|
392
|
-
* than a standalone image so two side by side stay under GitHub's comment
|
|
393
|
-
* column width (and don't overflow on mobile). */
|
|
394
|
-
export const ATTACHMENT_IMAGE_WIDTH_PAIR = 320;
|
|
395
|
-
function renderPairCell(item, label, options) {
|
|
406
|
+
function renderPairCell(item, label, options, density) {
|
|
396
407
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
397
408
|
const src = item.embedUrl ?? item.url;
|
|
398
409
|
const link = item.pageUrl ?? item.url;
|
|
399
|
-
const autoPx = Math.min(attachmentImageWidth(name),
|
|
410
|
+
const autoPx = Math.min(attachmentImageWidth(name, density), attachmentPairWidth(density));
|
|
400
411
|
const w = resolvedWidth(autoPx, options);
|
|
401
412
|
const alt = escapeHtmlAttr(name);
|
|
402
413
|
const href = escapeHtmlAttr((link ?? src));
|
|
403
414
|
const imgSrc = escapeHtmlAttr(src);
|
|
404
|
-
const caption =
|
|
415
|
+
const caption = formatMetaCaption(item.meta, options, "html");
|
|
405
416
|
const captionHtml = caption ? `<br><sub>${caption}</sub>` : "";
|
|
406
417
|
return `<td align="center"><sub><strong>${label}</strong></sub><br><a href="${href}">${imgTag(w, alt, imgSrc)}</a>${captionHtml}</td>`;
|
|
407
418
|
}
|
|
408
|
-
/** One side-by-side before/after row (issue #419)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
419
|
+
/** One side-by-side before/after row (issue #419). */
|
|
420
|
+
function renderPairRow(beforeItem, afterItem, options, density) {
|
|
421
|
+
return `<table><tr>${renderPairCell(beforeItem, "Before", options, density)}${renderPairCell(afterItem, "After", options, density)}</tr></table>`;
|
|
422
|
+
}
|
|
423
|
+
/** How many image/poster items will fit under `maxInlineImages` (for density). */
|
|
424
|
+
function countInlinableMedia(sorted, maxInlineImages) {
|
|
425
|
+
let count = 0;
|
|
426
|
+
for (const item of sorted) {
|
|
427
|
+
if (count >= maxInlineImages)
|
|
428
|
+
break;
|
|
429
|
+
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
430
|
+
const src = item.embedUrl ?? item.url;
|
|
431
|
+
const isImage = Boolean(src) && inferContentType(name).startsWith("image/");
|
|
432
|
+
const isPoster = Boolean(item.posterUrl) && inferContentType(name).startsWith("video/");
|
|
433
|
+
if (isImage || isPoster)
|
|
434
|
+
count++;
|
|
435
|
+
}
|
|
436
|
+
return count;
|
|
413
437
|
}
|
|
414
438
|
/**
|
|
415
439
|
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
@@ -444,6 +468,10 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
444
468
|
return Boolean(src) && inferContentType(name).startsWith("image/");
|
|
445
469
|
});
|
|
446
470
|
const { partnerOf, roleOf } = pairAttachments(sorted, isImageAt);
|
|
471
|
+
// One screenshot → large; a wall of shots → compact historical sizes.
|
|
472
|
+
const density = options.imageWidth === "auto"
|
|
473
|
+
? attachmentDensityForCount(countInlinableMedia(sorted, options.maxInlineImages))
|
|
474
|
+
: "dense";
|
|
447
475
|
const consumedByPair = new Set();
|
|
448
476
|
let inlinedImages = 0;
|
|
449
477
|
const overflowImages = [];
|
|
@@ -459,7 +487,7 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
459
487
|
consumedByPair.add(partnerIdx);
|
|
460
488
|
const beforeItem = roleOf.get(idx) === "before" ? item : partner;
|
|
461
489
|
const afterItem = roleOf.get(idx) === "before" ? partner : item;
|
|
462
|
-
lines.push(renderPairRow(beforeItem, afterItem, options), "");
|
|
490
|
+
lines.push(renderPairRow(beforeItem, afterItem, options, density), "");
|
|
463
491
|
continue;
|
|
464
492
|
}
|
|
465
493
|
// Cap already full for a two-image row — degrade this pair to two
|
|
@@ -483,7 +511,7 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
483
511
|
}
|
|
484
512
|
if (isPosterVideo) {
|
|
485
513
|
inlinedImages++;
|
|
486
|
-
const autoPx = posterImageWidth(item.videoMeta, name);
|
|
514
|
+
const autoPx = posterImageWidth(item.videoMeta, name, density);
|
|
487
515
|
const w = resolvedWidth(autoPx, options);
|
|
488
516
|
const href = escapeHtmlAttr(link ?? item.posterUrl);
|
|
489
517
|
lines.push(`<a href="${href}">${imgTag(w, escapeHtmlAttr(name), escapeHtmlAttr(item.posterUrl))}</a>`);
|
|
@@ -493,29 +521,33 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
493
521
|
if (item.videoMeta?.durationSeconds != null) {
|
|
494
522
|
parts.push(formatDuration(item.videoMeta.durationSeconds));
|
|
495
523
|
}
|
|
496
|
-
|
|
524
|
+
const metaCap = formatMetaCaption(item.meta, options, "html");
|
|
525
|
+
if (metaCap)
|
|
526
|
+
parts.push(metaCap);
|
|
497
527
|
lines.push(`<sub>${parts.join(" · ")}</sub>`, "");
|
|
498
528
|
}
|
|
499
529
|
else if (isImage) {
|
|
500
530
|
inlinedImages++;
|
|
501
531
|
// Markdown ![]() has no width control — phone frames become full-column giants.
|
|
502
532
|
// img src uses embed host when available (Camo revalidates); click-through prefers the file page.
|
|
503
|
-
const autoPx = attachmentImageWidth(name);
|
|
533
|
+
const autoPx = attachmentImageWidth(name, density);
|
|
504
534
|
const w = resolvedWidth(autoPx, options);
|
|
505
535
|
const alt = escapeHtmlAttr(name);
|
|
506
536
|
const href = escapeHtmlAttr(link ?? src);
|
|
507
537
|
const imgSrc = escapeHtmlAttr(src);
|
|
508
538
|
lines.push(`<a href="${href}">${imgTag(w, alt, imgSrc)}</a>`);
|
|
509
|
-
const caption =
|
|
539
|
+
const caption = formatMetaCaption(item.meta, options, "html");
|
|
510
540
|
if (caption)
|
|
511
541
|
lines.push(`<sub>${caption}</sub>`);
|
|
512
542
|
lines.push("");
|
|
513
543
|
}
|
|
514
544
|
else if (link) {
|
|
515
|
-
|
|
545
|
+
const cap = formatMetaCaption(item.meta, options, "markdown");
|
|
546
|
+
lines.push(`- [${name}](${link})${cap ? ` · ${cap}` : ""}`);
|
|
516
547
|
}
|
|
517
548
|
else {
|
|
518
|
-
|
|
549
|
+
const cap = formatMetaCaption(item.meta, options, "markdown");
|
|
550
|
+
lines.push(`- ${name}${cap ? ` · ${cap}` : ""}`);
|
|
519
551
|
}
|
|
520
552
|
}
|
|
521
553
|
if (overflowImages.length > 0) {
|
|
@@ -524,7 +556,8 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
524
556
|
for (const item of overflowImages) {
|
|
525
557
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
526
558
|
const link = item.pageUrl ?? item.url;
|
|
527
|
-
const
|
|
559
|
+
const cap = formatMetaCaption(item.meta, options, "markdown");
|
|
560
|
+
const suffix = cap ? ` · ${cap}` : "";
|
|
528
561
|
lines.push(link ? `- [${name}](${link})${suffix}` : `- ${name}${suffix}`);
|
|
529
562
|
}
|
|
530
563
|
lines.push("", "</details>", "");
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey
|
|
|
4
4
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, type BuiltinDestinationId, } from "./destinations.js";
|
|
5
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";
|
|
6
6
|
export { UploadsError, type UploadsErrorCode } from "./errors.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
|
+
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 MetadataKeysResult, type MetadataValuesResult, type GetMetadataResult, type PatchMetadataOptions, } from "./client.js";
|
|
8
8
|
export { buildCliProvenance } from "./provenance.js";
|
|
9
9
|
export { META_KEY_RE, META_VALUE_MAX, META_MAX_KEYS, META_MAX_TOTAL_BYTES, validateMetaEntry, parseMetaPair, parseMetaFlags, } from "./metadata.js";
|
|
10
10
|
export { ATTACHMENTS_MARKER, attachmentsCommentBody, GH_FALLBACK_AUTHOR_NOTE, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghBranchAttachmentKey, ghBranchKeyPrefix, ghMetadataForBranch, isValidRepo, parseRepoFromRemoteUrl, type AttachmentItem, type GhTarget, type GhTargetKind, } from "./github.js";
|
package/dist/mcp/tools.js
CHANGED
|
@@ -1075,34 +1075,64 @@ export function createUploadsMcpTools(opts) {
|
|
|
1075
1075
|
},
|
|
1076
1076
|
{
|
|
1077
1077
|
name: "find_files",
|
|
1078
|
-
description: "Find objects
|
|
1078
|
+
description: "Find objects whose queryable custom metadata matches ALL of `filters` (ANDed equality) and/or whose key contains `name` (case-insensitive substring). At least one of `filters` or `name` is required. Returns each match's key, public URL, full metadata map, and optional `truncated`. Same as `uploads find k=v...` / `uploads find --name <term>`.",
|
|
1079
1079
|
inputSchema: {
|
|
1080
1080
|
type: "object",
|
|
1081
1081
|
properties: {
|
|
1082
1082
|
filters: {
|
|
1083
1083
|
...metadataProp,
|
|
1084
|
-
description: "Metadata equality filters (
|
|
1084
|
+
description: "Metadata equality filters (optional when `name` is set). " + METADATA_DESCRIPTION,
|
|
1085
|
+
},
|
|
1086
|
+
name: {
|
|
1087
|
+
type: "string",
|
|
1088
|
+
description: "Case-insensitive substring match on object keys (1–128 chars). Optional when `filters` is non-empty.",
|
|
1089
|
+
},
|
|
1090
|
+
prefix: {
|
|
1091
|
+
type: "string",
|
|
1092
|
+
description: "Key prefix filter, combinable with filters/name.",
|
|
1085
1093
|
},
|
|
1086
|
-
prefix: { type: "string", description: "Key prefix filter, combinable with filters." },
|
|
1087
1094
|
limit: { type: "number", description: "Page size (default 50, max 500)." },
|
|
1088
1095
|
workspace: workspaceProp,
|
|
1089
1096
|
},
|
|
1090
|
-
required: ["filters"],
|
|
1091
1097
|
additionalProperties: false,
|
|
1092
1098
|
},
|
|
1093
1099
|
async handler(args) {
|
|
1094
|
-
const filters = optStringRecord(args, "filters");
|
|
1095
|
-
|
|
1096
|
-
|
|
1100
|
+
const filters = optStringRecord(args, "filters") ?? {};
|
|
1101
|
+
const name = optString(args, "name");
|
|
1102
|
+
const hasMeta = Object.keys(filters).length > 0;
|
|
1103
|
+
if (!hasMeta && !name) {
|
|
1104
|
+
usage("find_files requires filters and/or name");
|
|
1097
1105
|
}
|
|
1098
|
-
|
|
1106
|
+
if (hasMeta)
|
|
1107
|
+
validateMetaMap(filters);
|
|
1099
1108
|
const { client } = clientFor(args);
|
|
1100
1109
|
return client.findFiles(filters, {
|
|
1110
|
+
name,
|
|
1101
1111
|
prefix: optString(args, "prefix"),
|
|
1102
1112
|
limit: optPosInt(args, "limit"),
|
|
1103
1113
|
});
|
|
1104
1114
|
},
|
|
1105
1115
|
},
|
|
1116
|
+
{
|
|
1117
|
+
name: "list_metadata_keys",
|
|
1118
|
+
description: "List the distinct queryable metadata keys present in the workspace, with file counts and distinct-value counts. Use this to discover what is filterable before calling find_files — keys are user/agent-defined, not a fixed schema. Same as `uploads meta keys`. Pass optional `key` to list that key's values instead (`uploads meta values <key>`).",
|
|
1119
|
+
inputSchema: {
|
|
1120
|
+
type: "object",
|
|
1121
|
+
properties: {
|
|
1122
|
+
key: {
|
|
1123
|
+
type: "string",
|
|
1124
|
+
description: "When set, return distinct values for this metadata key (with counts) instead of the key list.",
|
|
1125
|
+
},
|
|
1126
|
+
workspace: workspaceProp,
|
|
1127
|
+
},
|
|
1128
|
+
additionalProperties: false,
|
|
1129
|
+
},
|
|
1130
|
+
async handler(args) {
|
|
1131
|
+
const { client } = clientFor(args);
|
|
1132
|
+
const key = optString(args, "key");
|
|
1133
|
+
return key ? client.listMetadataValues(key) : client.listMetadataKeys();
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1106
1136
|
{
|
|
1107
1137
|
name: "usage",
|
|
1108
1138
|
description: "Workspace storage and monthly upload counters (and remaining headroom when budgets are configured). Same as `uploads usage`.",
|