@buildinternet/uploads 0.36.0 → 0.37.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.
@@ -173,15 +173,17 @@ export const ROOT_COMMANDS = [
173
173
  },
174
174
  {
175
175
  name: "find",
176
- usage: "find k=v...",
177
- summary: "List objects matching metadata (alias for list --meta)",
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 an object's queryable metadata",
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>;
@@ -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>&…` — ANDed equality filter over
527
- * queryable metadata. `filters` must be pre-validated (see `metadata.ts`).
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: Record<string, string>, opts?: FindFilesOptions): Promise<FindFilesResult>;
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>&…` — ANDed equality filter over
409
- * queryable metadata. `filters` must be pre-validated (see `metadata.ts`).
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.js CHANGED
@@ -2134,31 +2134,45 @@ export async function runGallery(ctx, args, help = false) {
2134
2134
  }
2135
2135
  }
2136
2136
  // --- 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>]
2137
+ 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
2138
 
2139
2139
  Default prefix: UPLOADS_DEFAULT_PREFIX (screenshots if unset).
2140
2140
 
2141
- --meta <k=v> (repeatable, ANDed) switches to the metadata filter endpoint
2142
- returned items include their matched metadata. Combines with --prefix, not
2143
- with --pr/--issue/--all. See also: uploads find (positional-pair alias).
2141
+ --meta <k=v> (repeatable, ANDed) and/or --name <term> switch to the search
2142
+ endpoint — returned items include their matched metadata. Combines with
2143
+ --prefix, not with --pr/--issue/--all. --name is a case-insensitive
2144
+ substring match on object keys. See also: uploads find.
2144
2145
 
2145
2146
  Examples:
2146
2147
  uploads list --prefix screenshots/
2147
2148
  uploads list --pr 123
2148
2149
  uploads list --all --json
2149
2150
  uploads list --meta gh.repo=buildinternet/uploads --meta gh.number=123
2151
+ uploads list --name hero --meta app=web
2150
2152
  `;
2151
- /** `--meta k=v` (repeatable) filter path, shared by `runList` and `runFind`. */
2152
- async function runFindFiles(ctx, filters, flags) {
2153
+ /** Human-mode stderr note when a paged API response was capped server-side. */
2154
+ function writeTruncatedNotice(truncated, quiet, detail) {
2155
+ if (truncated && !quiet) {
2156
+ process.stderr.write(`truncated: true (${detail})\n`);
2157
+ }
2158
+ }
2159
+ /** `--meta` / `--name` search path, shared by `runList` and `runFind`. */
2160
+ async function runFindFiles(ctx, filters, flags, name) {
2153
2161
  if (flagString(flags, "--cursor") !== undefined) {
2154
- throw new UsageError("--cursor is not supported with metadata filters");
2162
+ throw new UsageError("--cursor is not supported with metadata or name filters");
2155
2163
  }
2156
2164
  const prefix = flagString(flags, "--prefix");
2157
2165
  const limit = flagInt(flags, "--limit", "--limit");
2158
- const result = await ctx.client.findFiles(filters, { prefix, limit });
2166
+ const nameTerm = name ?? flagString(flags, "--name");
2167
+ if (Object.keys(filters).length === 0 && !nameTerm) {
2168
+ throw new UsageError("find requires at least one k=v pair, --meta k=v, or --name <term>", {
2169
+ example: "uploads find path=/settings state=after",
2170
+ });
2171
+ }
2172
+ const result = await ctx.client.findFiles(filters, { prefix, limit, name: nameTerm });
2159
2173
  if (ctx.json)
2160
2174
  await writeJson(result);
2161
- else
2175
+ else {
2162
2176
  for (const item of result.items) {
2163
2177
  // LIST_HELP promises matched metadata in the output; render it inline
2164
2178
  // (sorted for stable output) so human mode honors that, not just --json.
@@ -2168,6 +2182,8 @@ async function runFindFiles(ctx, filters, flags) {
2168
2182
  .join(" ");
2169
2183
  await writeStdout(`${item.key}${item.url ? ` ${item.url}` : ""}${meta ? ` ${meta}` : ""}\n`);
2170
2184
  }
2185
+ writeTruncatedNotice(result.truncated, ctx.quiet, "more matches may exist beyond this page");
2186
+ }
2171
2187
  return 0;
2172
2188
  }
2173
2189
  export async function runList(ctx, args, help = false, run = execRunner) {
@@ -2177,14 +2193,15 @@ export async function runList(ctx, args, help = false, run = execRunner) {
2177
2193
  return 0;
2178
2194
  }
2179
2195
  const metaPairs = flagValues(parsed.flags, "--meta");
2180
- if (metaPairs.length > 0) {
2196
+ const nameFlag = flagString(parsed.flags, "--name");
2197
+ if (metaPairs.length > 0 || nameFlag !== undefined) {
2181
2198
  if (ghTargetFromFlags(parsed.flags, run)) {
2182
- throw new UsageError("--meta cannot be combined with --pr/--issue");
2199
+ throw new UsageError("--meta/--name cannot be combined with --pr/--issue");
2183
2200
  }
2184
2201
  if (flagBool(parsed.flags, "--all")) {
2185
- throw new UsageError("--meta cannot be combined with --all");
2202
+ throw new UsageError("--meta/--name cannot be combined with --all");
2186
2203
  }
2187
- return runFindFiles(ctx, parseMetaFlags(metaPairs), parsed.flags);
2204
+ return runFindFiles(ctx, metaPairs.length > 0 ? parseMetaFlags(metaPairs) : {}, parsed.flags, nameFlag);
2188
2205
  }
2189
2206
  const defaults = resolvePutDefaults({ envFile: ctx.envFile });
2190
2207
  const prefixFlag = flagString(parsed.flags, "--prefix");
@@ -2219,15 +2236,21 @@ export async function runList(ctx, args, help = false, run = execRunner) {
2219
2236
  return 0;
2220
2237
  }
2221
2238
  // --- find ---
2222
- const FIND_HELP = `uploads find k=v [k=v...] [--prefix <p>] [--limit <n>] [--workspace <name>]
2239
+ const FIND_HELP = `uploads find [k=v...] [--meta k=v]... [--name <term>] [--prefix <p>] [--limit <n>] [--workspace <name>]
2223
2240
 
2224
- Human-friendly alias for \`uploads list --meta k=v...\` same metadata filter
2225
- (ANDed equality), same output; pairs are positional, or spelled --meta k=v.
2241
+ Find objects by queryable metadata (ANDed equality) and/or a case-insensitive
2242
+ filename substring. Same output as \`uploads list --meta\` / \`--name\`.
2243
+
2244
+ Pairs are positional k=v, or spelled --meta k=v. A bare positional without
2245
+ \`=\` is treated as --name (e.g. \`uploads find hero\`). At least one of a
2246
+ meta pair or a name term is required.
2226
2247
 
2227
2248
  Examples:
2228
2249
  uploads find gh.repo=buildinternet/uploads gh.number=123
2229
2250
  uploads find path=/settings state=after --prefix screenshots/
2230
2251
  uploads find --meta path=/settings
2252
+ uploads find hero
2253
+ uploads find --name hero --meta app=web
2231
2254
  `;
2232
2255
  export async function runFind(ctx, args, help = false) {
2233
2256
  const parsed = parseCommandArgs(args);
@@ -2237,24 +2260,43 @@ export async function runFind(ctx, args, help = false) {
2237
2260
  }
2238
2261
  // Same flag/positional symmetry as `meta set` (issue #545): `find` is the
2239
2262
  // alias for `list --meta`, so `find --meta k=v` must not dead-end.
2240
- const pairs = [...parsed.positionals, ...flagValues(parsed.flags, "--meta")];
2241
- if (pairs.length === 0) {
2242
- throw new UsageError("find requires at least one k=v pair (or --meta k=v)", {
2243
- example: "uploads find path=/settings state=after",
2263
+ // Bare positionals without `=` are the filename term (issue #528) so
2264
+ // `uploads find hero` works without a flag. Empty input is rejected inside
2265
+ // `runFindFiles` (shared with `list --name` / `--meta`).
2266
+ const nameFlag = flagString(parsed.flags, "--name");
2267
+ const kvPositionals = [];
2268
+ const bareNames = [];
2269
+ for (const pos of parsed.positionals) {
2270
+ if (pos.includes("="))
2271
+ kvPositionals.push(pos);
2272
+ else
2273
+ bareNames.push(pos);
2274
+ }
2275
+ if (bareNames.length > 1) {
2276
+ throw new UsageError("find accepts at most one bare name term (or use --name)", {
2277
+ example: "uploads find hero --meta app=web",
2278
+ });
2279
+ }
2280
+ if (bareNames.length === 1 && nameFlag !== undefined) {
2281
+ throw new UsageError("pass the name term either as a bare positional or --name, not both", {
2282
+ example: "uploads find hero",
2244
2283
  });
2245
2284
  }
2246
- const filters = parseMetaFlags(pairs);
2247
- return runFindFiles(ctx, filters, parsed.flags);
2285
+ const pairs = [...kvPositionals, ...flagValues(parsed.flags, "--meta")];
2286
+ return runFindFiles(ctx, pairs.length > 0 ? parseMetaFlags(pairs) : {}, parsed.flags, nameFlag ?? bareNames[0]);
2248
2287
  }
2249
2288
  // --- meta ---
2250
2289
  const META_HELP = `uploads meta <command> [args]
2251
2290
 
2252
2291
  Read/write an object's queryable custom metadata (D1-backed key-value pairs;
2253
- distinct from the R2 provenance headers put on upload).
2292
+ distinct from the R2 provenance headers put on upload). Discover which keys
2293
+ and values exist in the workspace before filtering with find/list.
2254
2294
 
2255
2295
  Commands:
2256
2296
  get <key> Show metadata for an object
2257
2297
  set <key> k=v [k=v...] [--delete k]... Merge-set and/or delete pairs
2298
+ keys List distinct metadata keys (with counts)
2299
+ values <meta-key> List distinct values for one key
2258
2300
 
2259
2301
  Pairs take either form: positional k=v, or --meta k=v (same spelling as
2260
2302
  put/screenshot/list). Both can appear in one call.
@@ -2264,6 +2306,8 @@ Examples:
2264
2306
  uploads meta set screenshots/myapp/42/shot.png path=/settings state=after
2265
2307
  uploads meta set screenshots/myapp/42/shot.png --meta path=/settings
2266
2308
  uploads meta set screenshots/myapp/42/shot.png --delete path --delete state
2309
+ uploads meta keys
2310
+ uploads meta values app
2267
2311
  `;
2268
2312
  export async function runMeta(ctx, args, help = false) {
2269
2313
  const parsed = parseCommandArgs(args);
@@ -2273,7 +2317,7 @@ export async function runMeta(ctx, args, help = false) {
2273
2317
  return 0;
2274
2318
  }
2275
2319
  if (!action) {
2276
- throw new UsageError("meta requires a subcommand: get or set", {
2320
+ throw new UsageError("meta requires a subcommand: get, set, keys, or values", {
2277
2321
  example: "uploads meta set screenshots/myapp/42/shot.png --meta path=/settings",
2278
2322
  });
2279
2323
  }
@@ -2330,8 +2374,40 @@ export async function runMeta(ctx, args, help = false) {
2330
2374
  await resyncCommentAfterMetaSet(ctx, key, [...Object.keys(set ?? {}), ...del]);
2331
2375
  return 0;
2332
2376
  }
2377
+ case "keys": {
2378
+ // Workspace vocabulary discovery (issue #528) — which meta keys exist
2379
+ // and how common they are, before agents guess at find filters.
2380
+ const result = await ctx.client.listMetadataKeys();
2381
+ if (ctx.json)
2382
+ await writeJson(result);
2383
+ else {
2384
+ for (const row of result.keys) {
2385
+ await writeStdout(`${row.key} count=${row.count} distinct=${row.distinctValues}\n`);
2386
+ }
2387
+ writeTruncatedNotice(result.truncated, ctx.quiet, "more keys may exist beyond this page");
2388
+ }
2389
+ return 0;
2390
+ }
2391
+ case "values": {
2392
+ const key = parsed.positionals[1];
2393
+ if (!key) {
2394
+ throw new UsageError("meta values requires a metadata key", {
2395
+ example: "uploads meta values app",
2396
+ });
2397
+ }
2398
+ const result = await ctx.client.listMetadataValues(key);
2399
+ if (ctx.json)
2400
+ await writeJson(result);
2401
+ else {
2402
+ for (const row of result.values) {
2403
+ await writeStdout(`${row.value} count=${row.count}\n`);
2404
+ }
2405
+ writeTruncatedNotice(result.truncated, ctx.quiet, "more values may exist beyond this page");
2406
+ }
2407
+ return 0;
2408
+ }
2333
2409
  default:
2334
- throw new UsageError(`unknown meta command: ${action} (expected get or set)`, {
2410
+ throw new UsageError(`unknown meta command: ${action} (expected get, set, keys, or values)`, {
2335
2411
  example: "uploads meta get screenshots/myapp/42/shot.png",
2336
2412
  });
2337
2413
  }
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 in the workspace whose queryable custom metadata matches ALL of `filters` (ANDed equality). Returns each match's key, public URL, and full metadata map. Same as `uploads find k=v...` / `uploads list --meta k=v`.",
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 (at least one pair). " + METADATA_DESCRIPTION,
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
- if (!filters || Object.keys(filters).length === 0) {
1096
- usage("filters must have at least one key");
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
- validateMetaMap(filters);
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`.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
5
5
  "type": "module",
6
6
  "sideEffects": false,