@buildinternet/uploads 0.46.2 → 0.47.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/dist/client.d.ts +46 -3
- package/dist/client.js +92 -30
- package/dist/commands/login.d.ts +4 -4
- package/dist/commands/login.js +5 -5
- package/dist/commands.js +21 -10
- package/dist/config.d.ts +8 -1
- package/dist/config.js +10 -3
- package/dist/mcp/tools.js +17 -3
- package/dist/session-cli-version.js +1 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -36,6 +36,13 @@ export interface PutOptions {
|
|
|
36
36
|
* `KEY_EXISTS`.
|
|
37
37
|
*/
|
|
38
38
|
replace?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Reuse this key to safely retry the same upload request (issue #829). Not
|
|
41
|
+
* generated automatically — unlike `createGallery`, a `put` is only
|
|
42
|
+
* retried by the client (see `resilientFetch`) or is server-replay-safe
|
|
43
|
+
* when the caller supplies one; omit it and no header is sent.
|
|
44
|
+
*/
|
|
45
|
+
idempotencyKey?: string;
|
|
39
46
|
}
|
|
40
47
|
export interface ListOptions {
|
|
41
48
|
prefix?: string;
|
|
@@ -44,6 +51,12 @@ export interface ListOptions {
|
|
|
44
51
|
/** Hydrate each row's queryable D1 metadata (`?metadata=1`). */
|
|
45
52
|
metadata?: boolean;
|
|
46
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Default page cap for `findFilesAll` / `uploads find --all`. Search is a
|
|
56
|
+
* bounded read on the server, so following its cursor is bounded on the client
|
|
57
|
+
* too rather than draining an unknown number of pages (issue #829 §4).
|
|
58
|
+
*/
|
|
59
|
+
export declare const FIND_FILES_MAX_PAGES = 20;
|
|
47
60
|
export interface FindFilesOptions {
|
|
48
61
|
prefix?: string;
|
|
49
62
|
limit?: number;
|
|
@@ -52,6 +65,13 @@ export interface FindFilesOptions {
|
|
|
52
65
|
* At least one of non-empty `filters` or `name` is required.
|
|
53
66
|
*/
|
|
54
67
|
name?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Opaque continuation from a previous result's `cursor`. Pass it back
|
|
70
|
+
* unchanged, with the same filters/name/prefix, to fetch the next page
|
|
71
|
+
* (issue #829 §4). A cursor minted for one search path is rejected by the
|
|
72
|
+
* other, so do not hand-build one.
|
|
73
|
+
*/
|
|
74
|
+
cursor?: string;
|
|
55
75
|
}
|
|
56
76
|
export interface FindFilesItem {
|
|
57
77
|
key: string;
|
|
@@ -201,6 +221,8 @@ export interface GalleryListResult {
|
|
|
201
221
|
export interface CreateGalleryOptions {
|
|
202
222
|
title: string;
|
|
203
223
|
description?: string | null;
|
|
224
|
+
/** Reuse this key to safely retry the same create request. Generated when omitted. */
|
|
225
|
+
idempotencyKey?: string;
|
|
204
226
|
}
|
|
205
227
|
export interface AddGalleryItemOptions {
|
|
206
228
|
expectedVersion: number;
|
|
@@ -592,6 +614,13 @@ export declare function mintWorkspaceToken(apiUrl: string, accessToken: string,
|
|
|
592
614
|
scopes?: Array<TokenScope>;
|
|
593
615
|
label?: string;
|
|
594
616
|
ttlSeconds?: number | null;
|
|
617
|
+
/**
|
|
618
|
+
* Optional retry key. Reusing it (same effective request, within 24h)
|
|
619
|
+
* replays the original response — including the one-time plaintext token —
|
|
620
|
+
* instead of minting a second token. A changed request with the same key
|
|
621
|
+
* returns 409.
|
|
622
|
+
*/
|
|
623
|
+
idempotencyKey?: string;
|
|
595
624
|
}): Promise<MintTokenResult>;
|
|
596
625
|
/**
|
|
597
626
|
* Parse API error bodies. Prefers the nested envelope
|
|
@@ -632,10 +661,24 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
632
661
|
* ANDed equality filter over queryable metadata and/or a case-insensitive
|
|
633
662
|
* filename substring. At least one of non-empty `filters` or `opts.name`
|
|
634
663
|
* is required. `filters` must be pre-validated when present (see
|
|
635
|
-
* `metadata.ts`). The
|
|
636
|
-
*
|
|
664
|
+
* `metadata.ts`). The page is capped server-side (100, narrowable via
|
|
665
|
+
* `limit`); when more matches exist the result carries an opaque `cursor`
|
|
666
|
+
* to pass back as `opts.cursor` for the next page, and null when the last
|
|
667
|
+
* page has been reached.
|
|
668
|
+
*/
|
|
669
|
+
findFiles: (filters?: Record<string, string>, opts?: FindFilesOptions) => Promise<FindFilesResult>;
|
|
670
|
+
/**
|
|
671
|
+
* `findFiles` followed through its `cursor`, up to `maxPages` requests
|
|
672
|
+
* (default `FIND_FILES_MAX_PAGES`). Bounded on purpose: search pages are
|
|
673
|
+
* the expensive read path, so draining is capped rather than open-ended.
|
|
674
|
+
* The returned `cursor` is non-null when the cap stopped the drain early —
|
|
675
|
+
* pass it back to continue from there.
|
|
676
|
+
*
|
|
677
|
+
* `maxPages` must be a finite positive number. Anything else (`Infinity`,
|
|
678
|
+
* `NaN`, zero, a negative) falls back to the default rather than removing
|
|
679
|
+
* the bound or silently fetching nothing.
|
|
637
680
|
*/
|
|
638
|
-
|
|
681
|
+
findFilesAll(filters?: Record<string, string>, opts?: FindFilesOptions, maxPages?: number): Promise<FindFilesResult>;
|
|
639
682
|
/** `GET /v1/:workspace/files/facets` — workspace metadata key vocabulary. */
|
|
640
683
|
listMetadataKeys(): Promise<MetadataKeysResult>;
|
|
641
684
|
/** `GET /v1/:workspace/files/facets?key=` — distinct values for one key. */
|
package/dist/client.js
CHANGED
|
@@ -4,6 +4,12 @@ import { UploadsError } from "./errors.js";
|
|
|
4
4
|
import { buildScreenshotKey } from "./keys.js";
|
|
5
5
|
import { packageVersion } from "./package-version.js";
|
|
6
6
|
import { resolveEmbedUrl } from "./public-urls.js";
|
|
7
|
+
/**
|
|
8
|
+
* Default page cap for `findFilesAll` / `uploads find --all`. Search is a
|
|
9
|
+
* bounded read on the server, so following its cursor is bounded on the client
|
|
10
|
+
* too rather than draining an unknown number of pages (issue #829 §4).
|
|
11
|
+
*/
|
|
12
|
+
export const FIND_FILES_MAX_PAGES = 20;
|
|
7
13
|
// --- Request resilience (issue #809) ---------------------------------------
|
|
8
14
|
//
|
|
9
15
|
// Bare `fetch` has no timeout — undici's default header timeout is ~5
|
|
@@ -18,12 +24,17 @@ import { resolveEmbedUrl } from "./public-urls.js";
|
|
|
18
24
|
// gap on the core path).
|
|
19
25
|
const JSON_TIMEOUT_MS = 15_000;
|
|
20
26
|
const CONTENT_TIMEOUT_MS = 60_000;
|
|
21
|
-
// At most one retry. Retried on network errors, 503, and 429 —
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
27
|
+
// At most one retry. Retried on network errors, 503, and 429 — GET always.
|
|
28
|
+
// POST and PUT are retryable only when the request carries an
|
|
29
|
+
// Idempotency-Key (gallery-create's POST, and an upload PUT that opts in per
|
|
30
|
+
// issue #829) — a bare PUT is no longer assumed byte-idempotent, since a
|
|
31
|
+
// strict (non-`gh/`) key without a caller-supplied key can 409 `key_exists`
|
|
32
|
+
// on replay. DELETE/PATCH remain single-attempt because telling "no bytes
|
|
33
|
+
// sent" apart from "the mutation already landed" isn't reliable enough to
|
|
34
|
+
// risk a double-apply.
|
|
25
35
|
const MAX_ATTEMPTS = 2;
|
|
26
|
-
const RETRYABLE_METHODS = new Set(["GET"
|
|
36
|
+
const RETRYABLE_METHODS = new Set(["GET"]);
|
|
37
|
+
const IDEMPOTENCY_KEYED_METHODS = new Set(["POST", "PUT"]);
|
|
27
38
|
const RETRYABLE_STATUSES = new Set([429, 503]);
|
|
28
39
|
const DEFAULT_RETRY_DELAY_MS = 2_000;
|
|
29
40
|
// Cap an honored X-Retry-After so a large server-suggested backoff can't
|
|
@@ -76,7 +87,10 @@ async function fetchWithTimeout(url, init, timeoutMs) {
|
|
|
76
87
|
* `!res.ok` themselves via `parseErrorResponse`.
|
|
77
88
|
*/
|
|
78
89
|
async function resilientFetch(method, url, init, timeoutMs) {
|
|
79
|
-
const
|
|
90
|
+
const normalizedMethod = method.toUpperCase();
|
|
91
|
+
const retryable = RETRYABLE_METHODS.has(normalizedMethod) ||
|
|
92
|
+
(IDEMPOTENCY_KEYED_METHODS.has(normalizedMethod) &&
|
|
93
|
+
new Headers(init.headers).has("Idempotency-Key"));
|
|
80
94
|
for (let attempt = 1;; attempt++) {
|
|
81
95
|
let res;
|
|
82
96
|
let networkErr;
|
|
@@ -265,7 +279,11 @@ export function createWorkspaceInvite(apiUrl, accessToken, workspace, input) {
|
|
|
265
279
|
export function mintWorkspaceToken(apiUrl, accessToken, input) {
|
|
266
280
|
return jsonRequest(`${apiUrl.replace(/\/$/, "")}/v1/tokens`, {
|
|
267
281
|
method: "POST",
|
|
268
|
-
headers: {
|
|
282
|
+
headers: {
|
|
283
|
+
Authorization: `Bearer ${accessToken}`,
|
|
284
|
+
"Content-Type": "application/json",
|
|
285
|
+
...(input.idempotencyKey ? { "Idempotency-Key": input.idempotencyKey } : {}),
|
|
286
|
+
},
|
|
269
287
|
body: JSON.stringify({
|
|
270
288
|
grants: [{ workspace: input.workspace, ...(input.scopes ? { scopes: input.scopes } : {}) }],
|
|
271
289
|
...(input.label ? { label: input.label } : {}),
|
|
@@ -386,14 +404,17 @@ export function createUploadsClient(config) {
|
|
|
386
404
|
params.set("limit", String(opts.limit));
|
|
387
405
|
if (opts.cursor)
|
|
388
406
|
params.set("cursor", opts.cursor);
|
|
389
|
-
|
|
390
|
-
|
|
407
|
+
// The canonical route hydrates D1 metadata by default (issue #613 — the
|
|
408
|
+
// session shape won the reconciliation); when the caller didn't ask for
|
|
409
|
+
// `opts.metadata` this client discards it anyway below, so `metadata=0`
|
|
410
|
+
// opts out of the hydration pass server-side instead of paying for work
|
|
411
|
+
// whose result is thrown away (issue #829 §5).
|
|
412
|
+
params.set("metadata", opts.metadata ? "1" : "0");
|
|
391
413
|
const qs = params.toString();
|
|
392
|
-
// Canonical list envelope is `{files, prefixes, cursor}
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
// hydrated maps when the caller didn't ask for them.
|
|
414
|
+
// Canonical list envelope is `{files, prefixes, cursor}`. This client
|
|
415
|
+
// keeps its historical `{items, cursor}` contract: rename the array and
|
|
416
|
+
// honor `opts.metadata` by stripping the hydrated maps when the caller
|
|
417
|
+
// didn't ask for them.
|
|
397
418
|
const page = await request("GET", `${canonicalFilesBase(config)}${qs ? `?${qs}` : ""}`);
|
|
398
419
|
return {
|
|
399
420
|
cursor: page.cursor,
|
|
@@ -407,6 +428,25 @@ export function createUploadsClient(config) {
|
|
|
407
428
|
}),
|
|
408
429
|
};
|
|
409
430
|
}
|
|
431
|
+
async function findFiles(filters = {}, opts = {}) {
|
|
432
|
+
const params = new URLSearchParams();
|
|
433
|
+
for (const [k, v] of Object.entries(filters))
|
|
434
|
+
params.append(`meta.${k}`, v);
|
|
435
|
+
if (opts.name)
|
|
436
|
+
params.set("name", opts.name);
|
|
437
|
+
if (opts.prefix)
|
|
438
|
+
params.set("prefix", opts.prefix);
|
|
439
|
+
if (opts.limit != null)
|
|
440
|
+
params.set("limit", String(opts.limit));
|
|
441
|
+
if (opts.cursor)
|
|
442
|
+
params.set("cursor", opts.cursor);
|
|
443
|
+
const result = await request("GET", `${canonicalFilesBase(config)}/search?${params.toString()}`);
|
|
444
|
+
return {
|
|
445
|
+
items: result.items,
|
|
446
|
+
cursor: result.cursor ?? null,
|
|
447
|
+
truncated: result.truncated,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
410
450
|
async function getGallery(id) {
|
|
411
451
|
return request("GET", `${galleriesBase(config)}/${encodeURIComponent(id)}`);
|
|
412
452
|
}
|
|
@@ -445,6 +485,8 @@ export function createUploadsClient(config) {
|
|
|
445
485
|
const headers = { "Content-Type": contentType };
|
|
446
486
|
if (opts.replace)
|
|
447
487
|
headers["X-Uploads-Replace"] = "1";
|
|
488
|
+
if (opts.idempotencyKey)
|
|
489
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
448
490
|
if (opts.provenance) {
|
|
449
491
|
for (const [k, v] of Object.entries(opts.provenance)) {
|
|
450
492
|
if (v !== undefined && v !== "")
|
|
@@ -505,21 +547,37 @@ export function createUploadsClient(config) {
|
|
|
505
547
|
* ANDed equality filter over queryable metadata and/or a case-insensitive
|
|
506
548
|
* filename substring. At least one of non-empty `filters` or `opts.name`
|
|
507
549
|
* is required. `filters` must be pre-validated when present (see
|
|
508
|
-
* `metadata.ts`). The
|
|
509
|
-
*
|
|
550
|
+
* `metadata.ts`). The page is capped server-side (100, narrowable via
|
|
551
|
+
* `limit`); when more matches exist the result carries an opaque `cursor`
|
|
552
|
+
* to pass back as `opts.cursor` for the next page, and null when the last
|
|
553
|
+
* page has been reached.
|
|
510
554
|
*/
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
555
|
+
findFiles,
|
|
556
|
+
/**
|
|
557
|
+
* `findFiles` followed through its `cursor`, up to `maxPages` requests
|
|
558
|
+
* (default `FIND_FILES_MAX_PAGES`). Bounded on purpose: search pages are
|
|
559
|
+
* the expensive read path, so draining is capped rather than open-ended.
|
|
560
|
+
* The returned `cursor` is non-null when the cap stopped the drain early —
|
|
561
|
+
* pass it back to continue from there.
|
|
562
|
+
*
|
|
563
|
+
* `maxPages` must be a finite positive number. Anything else (`Infinity`,
|
|
564
|
+
* `NaN`, zero, a negative) falls back to the default rather than removing
|
|
565
|
+
* the bound or silently fetching nothing.
|
|
566
|
+
*/
|
|
567
|
+
async findFilesAll(filters = {}, opts = {}, maxPages = FIND_FILES_MAX_PAGES) {
|
|
568
|
+
const items = [];
|
|
569
|
+
let cursor = opts.cursor;
|
|
570
|
+
let truncated;
|
|
571
|
+
const pages = Number.isFinite(maxPages) && maxPages >= 1 ? Math.floor(maxPages) : FIND_FILES_MAX_PAGES;
|
|
572
|
+
for (let page = 0; page < pages; page += 1) {
|
|
573
|
+
const result = await findFiles(filters, { ...opts, cursor });
|
|
574
|
+
items.push(...result.items);
|
|
575
|
+
truncated = result.truncated;
|
|
576
|
+
cursor = result.cursor ?? undefined;
|
|
577
|
+
if (!cursor)
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
return { items, cursor: cursor ?? null, ...(truncated === undefined ? {} : { truncated }) };
|
|
523
581
|
},
|
|
524
582
|
/** `GET /v1/:workspace/files/facets` — workspace metadata key vocabulary. */
|
|
525
583
|
async listMetadataKeys() {
|
|
@@ -534,9 +592,13 @@ export function createUploadsClient(config) {
|
|
|
534
592
|
return { ...result, embedUrl: resolveEmbedUrl(result.url, result.embedUrl) };
|
|
535
593
|
},
|
|
536
594
|
async createGallery(opts) {
|
|
595
|
+
const { idempotencyKey = crypto.randomUUID(), ...body } = opts;
|
|
537
596
|
return request("POST", galleriesBase(config), {
|
|
538
|
-
body: new TextEncoder().encode(JSON.stringify(
|
|
539
|
-
headers: {
|
|
597
|
+
body: new TextEncoder().encode(JSON.stringify(body)),
|
|
598
|
+
headers: {
|
|
599
|
+
"Content-Type": "application/json",
|
|
600
|
+
"Idempotency-Key": idempotencyKey,
|
|
601
|
+
},
|
|
540
602
|
});
|
|
541
603
|
},
|
|
542
604
|
async getGallery(id) {
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -17,10 +17,10 @@ export declare function resolveEnrollmentCode(parsed: ReturnType<typeof parseCom
|
|
|
17
17
|
hiddenPrompt: () => Promise<string>;
|
|
18
18
|
}): Promise<string>;
|
|
19
19
|
/**
|
|
20
|
-
* Auth
|
|
21
|
-
*
|
|
22
|
-
* auth runs on a different loopback port
|
|
23
|
-
* --auth-url / UPLOADS_AUTH_URL.
|
|
20
|
+
* Auth base URL: explicit flag > UPLOADS_AUTH_URL > derived from the API base
|
|
21
|
+
* (`api.<domain>` → `<domain>`, same-origin auth since #731) > the production
|
|
22
|
+
* default. Local multi-worker dev (where auth runs on a different loopback port
|
|
23
|
+
* than the API) needs an explicit --auth-url / UPLOADS_AUTH_URL.
|
|
24
24
|
*/
|
|
25
25
|
export declare function resolveAuthUrl(parsed: ReturnType<typeof parseCommandArgs>, apiUrl: string): string;
|
|
26
26
|
export interface DeviceLoginIo {
|
package/dist/commands/login.js
CHANGED
|
@@ -25,7 +25,7 @@ Options:
|
|
|
25
25
|
--scopes <list> Comma-separated scopes (default:
|
|
26
26
|
files:read,files:write,files:delete)
|
|
27
27
|
--label <text> Token label (default: this machine's hostname)
|
|
28
|
-
--auth-url <url> Auth base (default: https://
|
|
28
|
+
--auth-url <url> Auth base (default: https://uploads.sh)
|
|
29
29
|
--no-open Don't try to open a browser automatically
|
|
30
30
|
--code <code> Fallback: use a pre-existing enrollment code instead of
|
|
31
31
|
device login (visible in shell history)
|
|
@@ -160,10 +160,10 @@ function hasEnrollmentSource(parsed) {
|
|
|
160
160
|
Boolean(process.env.UPLOADS_ENROLLMENT_CODE));
|
|
161
161
|
}
|
|
162
162
|
/**
|
|
163
|
-
* Auth
|
|
164
|
-
*
|
|
165
|
-
* auth runs on a different loopback port
|
|
166
|
-
* --auth-url / UPLOADS_AUTH_URL.
|
|
163
|
+
* Auth base URL: explicit flag > UPLOADS_AUTH_URL > derived from the API base
|
|
164
|
+
* (`api.<domain>` → `<domain>`, same-origin auth since #731) > the production
|
|
165
|
+
* default. Local multi-worker dev (where auth runs on a different loopback port
|
|
166
|
+
* than the API) needs an explicit --auth-url / UPLOADS_AUTH_URL.
|
|
167
167
|
*/
|
|
168
168
|
export function resolveAuthUrl(parsed, apiUrl) {
|
|
169
169
|
const explicit = flagString(parsed.flags, "--auth-url") ?? process.env.UPLOADS_AUTH_URL;
|
package/dist/commands.js
CHANGED
|
@@ -2574,8 +2574,10 @@ Default prefix: UPLOADS_DEFAULT_PREFIX (screenshots if unset).
|
|
|
2574
2574
|
|
|
2575
2575
|
--meta <k=v> (repeatable, ANDed) and/or --name <term> switch to the search
|
|
2576
2576
|
endpoint — returned items include their matched metadata. Combines with
|
|
2577
|
-
--prefix, not with --pr/--issue
|
|
2578
|
-
substring match on object keys.
|
|
2577
|
+
--prefix, not with --pr/--issue. --name is a case-insensitive
|
|
2578
|
+
substring match on object keys. Search pages are continued with --cursor
|
|
2579
|
+
(opaque, from the previous page); --all follows it for up to 20 pages and
|
|
2580
|
+
prints the next cursor if more remain. See also: uploads find.
|
|
2579
2581
|
|
|
2580
2582
|
Examples:
|
|
2581
2583
|
uploads list --prefix screenshots/
|
|
@@ -2592,18 +2594,21 @@ function writeTruncatedNotice(truncated, quiet, detail) {
|
|
|
2592
2594
|
}
|
|
2593
2595
|
/** `--meta` / `--name` search path, shared by `runList` and `runFind`. */
|
|
2594
2596
|
async function runFindFiles(ctx, filters, flags, name) {
|
|
2595
|
-
if (flagString(flags, "--cursor") !== undefined) {
|
|
2596
|
-
throw new UsageError("--cursor is not supported with metadata or name filters");
|
|
2597
|
-
}
|
|
2598
2597
|
const prefix = flagString(flags, "--prefix");
|
|
2599
2598
|
const limit = flagInt(flags, "--limit", "--limit");
|
|
2599
|
+
const cursor = flagString(flags, "--cursor");
|
|
2600
2600
|
const nameTerm = name ?? flagString(flags, "--name");
|
|
2601
2601
|
if (Object.keys(filters).length === 0 && !nameTerm) {
|
|
2602
2602
|
throw new UsageError("find requires at least one k=v pair, --meta k=v, or --name <term>", {
|
|
2603
2603
|
example: "uploads find path=/settings state=after",
|
|
2604
2604
|
});
|
|
2605
2605
|
}
|
|
2606
|
-
|
|
2606
|
+
// `--all` follows the search cursor, but only up to FIND_FILES_MAX_PAGES —
|
|
2607
|
+
// never an unbounded drain. `--cursor` resumes a specific page by hand.
|
|
2608
|
+
const searchOpts = { prefix, limit, name: nameTerm, cursor };
|
|
2609
|
+
const result = flagBool(flags, "--all")
|
|
2610
|
+
? await ctx.client.findFilesAll(filters, searchOpts)
|
|
2611
|
+
: await ctx.client.findFiles(filters, searchOpts);
|
|
2607
2612
|
if (ctx.json)
|
|
2608
2613
|
await writeJson(result);
|
|
2609
2614
|
else {
|
|
@@ -2617,6 +2622,9 @@ async function runFindFiles(ctx, filters, flags, name) {
|
|
|
2617
2622
|
await writeStdout(`${item.key}${item.url ? ` ${item.url}` : ""}${meta ? ` ${meta}` : ""}\n`);
|
|
2618
2623
|
}
|
|
2619
2624
|
writeTruncatedNotice(result.truncated, ctx.quiet, "more matches may exist beyond this page");
|
|
2625
|
+
// Human mode surfaces the continuation the same way `uploads list` does.
|
|
2626
|
+
if (result.cursor)
|
|
2627
|
+
process.stderr.write(`cursor: ${result.cursor}\n`);
|
|
2620
2628
|
}
|
|
2621
2629
|
return 0;
|
|
2622
2630
|
}
|
|
@@ -2632,9 +2640,6 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
2632
2640
|
if (ghTargetFromFlags(parsed.flags, run)) {
|
|
2633
2641
|
throw new UsageError("--meta/--name cannot be combined with --pr/--issue");
|
|
2634
2642
|
}
|
|
2635
|
-
if (flagBool(parsed.flags, "--all")) {
|
|
2636
|
-
throw new UsageError("--meta/--name cannot be combined with --all");
|
|
2637
|
-
}
|
|
2638
2643
|
return runFindFiles(ctx, metaPairs.length > 0 ? parseMetaFlags(metaPairs) : {}, parsed.flags, nameFlag);
|
|
2639
2644
|
}
|
|
2640
2645
|
const defaults = resolvePutDefaults({ envFile: ctx.envFile });
|
|
@@ -2694,11 +2699,16 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
2694
2699
|
return 0;
|
|
2695
2700
|
}
|
|
2696
2701
|
// --- find ---
|
|
2697
|
-
const FIND_HELP = `uploads find [k=v...] [--meta k=v]... [--name <term>] [--prefix <p>] [--limit <n>] [--workspace <name>]
|
|
2702
|
+
const FIND_HELP = `uploads find [k=v...] [--meta k=v]... [--name <term>] [--prefix <p>] [--limit <n>] [--cursor <c>] [--all] [--workspace <name>]
|
|
2698
2703
|
|
|
2699
2704
|
Find objects by queryable metadata (ANDed equality) and/or a case-insensitive
|
|
2700
2705
|
filename substring. Same output as \`uploads list --meta\` / \`--name\`.
|
|
2701
2706
|
|
|
2707
|
+
Results are paged. When more matches exist the next page's opaque cursor is
|
|
2708
|
+
printed to stderr (and carried in --json as \`cursor\`); pass it back with
|
|
2709
|
+
--cursor. --all follows the cursor for you, up to 20 pages, then prints the
|
|
2710
|
+
cursor to resume from.
|
|
2711
|
+
|
|
2702
2712
|
Pairs are positional k=v, or spelled --meta k=v. A bare positional without
|
|
2703
2713
|
\`=\` is treated as --name (e.g. \`uploads find hero\`). At least one of a
|
|
2704
2714
|
meta pair or a name term is required.
|
|
@@ -2709,6 +2719,7 @@ Examples:
|
|
|
2709
2719
|
uploads find --meta path=/settings
|
|
2710
2720
|
uploads find hero
|
|
2711
2721
|
uploads find --name hero --meta app=web
|
|
2722
|
+
uploads find app=web --all --json
|
|
2712
2723
|
`;
|
|
2713
2724
|
export async function runFind(ctx, args, help = false) {
|
|
2714
2725
|
const parsed = parseCommandArgs(args);
|
package/dist/config.d.ts
CHANGED
|
@@ -4,7 +4,14 @@ export interface UploadsClientConfig {
|
|
|
4
4
|
workspace: string;
|
|
5
5
|
token: string;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Derive the auth base URL from an API base. Since #731 the auth endpoints are
|
|
9
|
+
* served same-origin on the app origin (`<origin>/api/auth`, web proxies to the
|
|
10
|
+
* auth worker), not an `auth.` subdomain — so the canonical `api.<domain>`
|
|
11
|
+
* shape maps to its parent (`api.uploads.sh` → `uploads.sh`). Falls back to the
|
|
12
|
+
* production origin. Local multi-worker dev, where auth listens on a different
|
|
13
|
+
* loopback port, still needs an explicit `--auth-url` / `UPLOADS_AUTH_URL`.
|
|
14
|
+
*/
|
|
8
15
|
export declare function authUrlFromApi(apiUrl: string): string;
|
|
9
16
|
export declare const DEFAULT_API_URL = "https://api.uploads.sh";
|
|
10
17
|
export declare const DEFAULT_WORKSPACE = "default";
|
package/dist/config.js
CHANGED
|
@@ -2,19 +2,26 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { loadConfigFile, resolveConfigPath } from "./config-file.js";
|
|
3
3
|
import { UploadsError } from "./errors.js";
|
|
4
4
|
export { defaultConfigPath, resolveConfigPath, loadConfigFile, redactToken, writeConfigKeys, removeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, resolveScreenshotDefaults, UPLOADS_CONFIG_KEYS, } from "./config-file.js";
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Derive the auth base URL from an API base. Since #731 the auth endpoints are
|
|
7
|
+
* served same-origin on the app origin (`<origin>/api/auth`, web proxies to the
|
|
8
|
+
* auth worker), not an `auth.` subdomain — so the canonical `api.<domain>`
|
|
9
|
+
* shape maps to its parent (`api.uploads.sh` → `uploads.sh`). Falls back to the
|
|
10
|
+
* production origin. Local multi-worker dev, where auth listens on a different
|
|
11
|
+
* loopback port, still needs an explicit `--auth-url` / `UPLOADS_AUTH_URL`.
|
|
12
|
+
*/
|
|
6
13
|
export function authUrlFromApi(apiUrl) {
|
|
7
14
|
try {
|
|
8
15
|
const url = new URL(apiUrl);
|
|
9
16
|
if (url.hostname.startsWith("api.")) {
|
|
10
|
-
url.hostname =
|
|
17
|
+
url.hostname = url.hostname.slice(4);
|
|
11
18
|
return url.origin;
|
|
12
19
|
}
|
|
13
20
|
}
|
|
14
21
|
catch {
|
|
15
22
|
// fall through
|
|
16
23
|
}
|
|
17
|
-
return "https://
|
|
24
|
+
return "https://uploads.sh";
|
|
18
25
|
}
|
|
19
26
|
export const DEFAULT_API_URL = "https://api.uploads.sh";
|
|
20
27
|
export const DEFAULT_WORKSPACE = "default";
|
package/dist/mcp/tools.js
CHANGED
|
@@ -1298,7 +1298,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
1298
1298
|
title: "Find files",
|
|
1299
1299
|
annotations: mcpRead,
|
|
1300
1300
|
securitySchemes: mcpOAuthRead,
|
|
1301
|
-
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,
|
|
1301
|
+
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, optional `truncated`, and a `cursor` to pass back for the next page (null when there are none; set `all` to follow it for you). Same as `uploads find k=v...` / `uploads find --name <term>`.",
|
|
1302
1302
|
inputSchema: {
|
|
1303
1303
|
type: "object",
|
|
1304
1304
|
properties: {
|
|
@@ -1315,6 +1315,14 @@ export function createUploadsMcpTools(opts) {
|
|
|
1315
1315
|
description: "Key prefix filter, combinable with filters/name.",
|
|
1316
1316
|
},
|
|
1317
1317
|
limit: { type: "number", description: "Page size (default 50, max 500)." },
|
|
1318
|
+
cursor: {
|
|
1319
|
+
type: "string",
|
|
1320
|
+
description: "Opaque continuation from a previous call's `cursor`. Pass it back unchanged with the same filters/name to get the next page; a null `cursor` means there are no more pages.",
|
|
1321
|
+
},
|
|
1322
|
+
all: {
|
|
1323
|
+
type: "boolean",
|
|
1324
|
+
description: "Follow the cursor and return every page, up to a bounded number of requests. A non-null `cursor` in the result means that bound was reached before the end — pass it back to continue.",
|
|
1325
|
+
},
|
|
1318
1326
|
workspace: workspaceProp,
|
|
1319
1327
|
},
|
|
1320
1328
|
additionalProperties: false,
|
|
@@ -1329,11 +1337,17 @@ export function createUploadsMcpTools(opts) {
|
|
|
1329
1337
|
if (hasMeta)
|
|
1330
1338
|
validateMetaMap(filters);
|
|
1331
1339
|
const { client } = await clientFor(args);
|
|
1332
|
-
|
|
1340
|
+
const opts = {
|
|
1333
1341
|
name,
|
|
1334
1342
|
prefix: optString(args, "prefix"),
|
|
1335
1343
|
limit: optPosInt(args, "limit"),
|
|
1336
|
-
|
|
1344
|
+
cursor: optString(args, "cursor"),
|
|
1345
|
+
};
|
|
1346
|
+
// `all` drains through findFilesAll, which caps its own page count —
|
|
1347
|
+
// unlike `list`'s `all`, this never runs unbounded.
|
|
1348
|
+
return optBool(args, "all")
|
|
1349
|
+
? client.findFilesAll(filters, opts)
|
|
1350
|
+
: client.findFiles(filters, opts);
|
|
1337
1351
|
},
|
|
1338
1352
|
},
|
|
1339
1353
|
{
|
|
@@ -47,7 +47,7 @@ export async function syncSessionCliVersion(opts = {}) {
|
|
|
47
47
|
const apiUrl = opts.apiUrl ?? fromFile.UPLOADS_API_URL ?? process.env.UPLOADS_API_URL;
|
|
48
48
|
const authUrl = (opts.authUrl ??
|
|
49
49
|
process.env.UPLOADS_AUTH_URL ??
|
|
50
|
-
(apiUrl ? authUrlFromApi(apiUrl) : "https://
|
|
50
|
+
(apiUrl ? authUrlFromApi(apiUrl) : "https://uploads.sh")).replace(/\/$/, "");
|
|
51
51
|
const controller = new AbortController();
|
|
52
52
|
const timer = setTimeout(() => controller.abort(), POST_TIMEOUT_MS);
|
|
53
53
|
try {
|