@wji.ai/cli 0.1.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.
@@ -0,0 +1,381 @@
1
+ # wji CLI — full command reference
2
+
3
+ Global flags (accepted before any subcommand):
4
+
5
+ | Flag | Meaning |
6
+ | --- | --- |
7
+ | `--profile <name>` | config profile to use |
8
+ | `--endpoint <url>` | API base URL (default `https://api.wji.ai`) |
9
+ | `--token <token>` | bearer token or `sk_` key for this call only |
10
+ | `--org <orgId>` | organization context (`X-Org-Id`) |
11
+ | `--no-org` | force personal context for this call |
12
+ | `--locale <locale>` | response language: `cht`, `chs`, `en` |
13
+ | `--json` | print the raw API response instead of tables |
14
+
15
+ Value conventions: `@file` reads a file, `-` reads stdin, `--set key=value`
16
+ repeats and nests on dots, references are `assetId[:label[:weight]]`.
17
+
18
+ Exit codes: `0` ok, `1` error, `77` auth/permission failure.
19
+
20
+ Destructive commands — marked `[-y]` below — ask `[y/N]` with the target's
21
+ name. Without a terminal they refuse to run unless `-y/--yes` is passed.
22
+ `canvas gen` asks the same way, showing the credit cost.
23
+
24
+ Token rotation: every refresh issues a new refresh token and invalidates the
25
+ old one. Presenting an already-used refresh token makes the server revoke
26
+ **all** of that user's sessions. The CLI takes a lock file next to the config
27
+ (`.refresh-<profile>.lock`) and re-reads the config before refreshing, so
28
+ parallel commands share one refresh. Access tokens are not revoked by logout;
29
+ they stay valid until they expire.
30
+
31
+ ## auth
32
+
33
+ | Command | API |
34
+ | --- | --- |
35
+ | `auth login [-e email] [-p password]` | `POST /auth/signin` |
36
+ | `auth signup -e -n -c [-p]` | `POST /auth/signup` (needs an emailed code) |
37
+ | `auth token` | prints the active bearer token |
38
+ | `auth whoami` | `GET /user/me` + local context |
39
+ | `auth refresh` | `POST /auth/refresh` `{refreshToken}` — rotates the pair |
40
+ | `auth logout` | `POST /auth/signout` `{refreshToken}` (best effort) and clears local credentials |
41
+ | `auth key list` | `GET /api-key/search` |
42
+ | `auth key create <name> [--expires] [--save]` | `POST /api-key/create` — secret shown once |
43
+ | `auth key revoke <id> [-y]` | `POST /api-key/list/:id/revoke` — immediate; anything using the key breaks |
44
+ | `auth key rm <id> [-y]` | `DELETE /api-key/list/:id` |
45
+ | `auth key use <secret>` | stores an existing `sk_` key |
46
+
47
+ ## config
48
+
49
+ `config show` · `config set <key> <value>` · `config unset <key>` ·
50
+ `config profiles` · `config use <profile>` · `config rm <profile> [-y]` · `config path`
51
+
52
+ Settable keys: `endpoint`, `orgId`, `workspace`, `locale`, `apiKey`, `email`, `name`.
53
+
54
+ ## org
55
+
56
+ `org list` · `org use <orgId>` · `org personal` · `org current` ·
57
+ `org members` · `org create <name>` (at most 3 organizations per account; name ≤200)
58
+
59
+ Maps to `GET /organization/mine`, `/organization/me`, `/organization/members`,
60
+ `POST /organization/create`. Context is the `X-Org-Id` header; there is no
61
+ switch endpoint.
62
+
63
+ ## ws (workspace)
64
+
65
+ | Command | API |
66
+ | --- | --- |
67
+ | `ws list [-k keyword] [-s all\|me\|shared\|favorite] [--page] [--limit]` | `GET /workspace/search` — `--limit` max 100, `-1` returns at most 500 |
68
+ | `ws get [id]` | `GET /workspace/list/:id` |
69
+ | `ws use <id>` | stores the default workspace locally |
70
+ | `ws create <name> [-d] [-r ratio] [-l lang] [-t type] [-a app...]` | `POST /workspace/create` |
71
+ | `ws update [id] [-n] [-d] [-r] [-l] [--set]` | `PUT /workspace/list/:id` |
72
+ | `ws rm <id> [-y]` | `DELETE /workspace/list/:id` (owner only) |
73
+ | `ws favorite <id> [--off]` | `PUT /workspace/list/:id/favorite` |
74
+ | `ws fs [id] [-a appId]` | `GET /workspace/list/:id/fs[/:appid]` |
75
+ | `ws app list\|add <app> [-n]\|rm <appId> [-y]` | `POST/DELETE /workspace/list/:id/apps` |
76
+ | `ws member list\|add <userId> [-r role]\|role <memberId> <role>\|rm <memberId> [-y]` | `/workspace/list/:id/member/*` |
77
+ | `ws quota [id] [--set k=v]` | `GET .../quota-usage`, `PUT .../quota` |
78
+ | `ws stats [id]` | `GET /workspace/list/:id/stats` |
79
+ | `ws templates` / `ws clone <templateId>` | `GET /workspace/start/templates`, `POST /workspace/create/:id` |
80
+
81
+ Caps: 50 workspaces per account (`ws create` and `ws clone`), names ≤200
82
+ characters, descriptions ≤5,000.
83
+
84
+ App tokens for `-a` / `ws app add`: `dataset`, `datasetimagegeneration`,
85
+ `canvas` (InfiniteCanvas), `document`, `draw`, `files`, `playscript`, `batch`,
86
+ `digitalhost`.
87
+
88
+ Restriction from the backend: `Dataset`, `Draw`, `DatasetImageGeneration` and
89
+ `Files` may exist at most once per workspace.
90
+
91
+ ## fs (fsnode)
92
+
93
+ | Command | API |
94
+ | --- | --- |
95
+ | `fs root [key] [-d depth]` | `GET /fsnode/root[/:key]` — key defaults to your drive; pass a workspace id for its root |
96
+ | `fs ls <id> [--limit] [--scope]` | `GET /fsnode/list/:id/children` |
97
+ | `fs tree <id> [-d depth]` | `GET /fsnode/list/:id/tree` |
98
+ | `fs get <id> [--scope]` | `GET /fsnode/list/:id` (comma-separated ids allowed) |
99
+ | `fs path <id>` | `GET /fsnode/list/:id/path` |
100
+ | `fs mkdir <parent> <name> [--nickname] [--content]` | `POST /fsnode/create` type=folder |
101
+ | `fs touch <parent> <name> [--mimetype] [--asset] [--content]` | `POST /fsnode/create` type=file |
102
+ | `fs set <id> [-n] [--nickname] [--content] [--tags] [--images] [--icon] [--asset] [--sort] [--set]` | `PATCH /fsnode/list/:id` |
103
+ | `fs describe <id> <text>` | merges `content.description` |
104
+ | `fs rm <id> [-y]` | `DELETE /fsnode/list/:id` (soft delete, whole subtree) |
105
+ | `fs sort <parent> <ids...>` | `PATCH /fsnode/list/:id/children/sort` |
106
+ | `fs batch <json>` | `PATCH /fsnode/batch` — `[{id, name?, content?, sort?, file?}]` |
107
+ | `fs used-space` | `GET /fsnode/drive/used-space` |
108
+
109
+ `--content` replaces the entire content object. Names are capped at 500
110
+ characters, nicknames at 200, and `fs batch` at 100 items per call.
111
+
112
+ ## dataset
113
+
114
+ | Command | Notes |
115
+ | --- | --- |
116
+ | `dataset new <parent> <name> [-t type] [-d description] [--images] [--nickname]` | creates an fsnode with mimetype `workspace/dataset` |
117
+ | `dataset list <folderId>` | dataset nodes in a folder |
118
+ | `dataset get <key>` | `GET /dataset/list/:key` (vector-store record) |
119
+ | `dataset rm <key> [-y]` | `DELETE /dataset/list/:key` |
120
+
121
+ Types (as the web app defines them): `outline`, `screenplay`, `character`,
122
+ `location`, `prop`, `outfit`, `scene`, `shot`, `other`. A character dataset
123
+ should carry a headshot, a character sheet and a voice sample (see SKILL.md).
124
+
125
+ Semantic dataset folders created with the Dataset app: `story`, `character`,
126
+ `location`, `prop`, `shot`, `other` (identified by `content.semanticKey`).
127
+
128
+ ## asset
129
+
130
+ | Command | API |
131
+ | --- | --- |
132
+ | `asset upload <file...> [-w] [--store] [--vectorize] [--width] [--height] [--duration]` | `POST /assets/v2/upload` (multipart) |
133
+ | `asset import <url> [-w] [-n] [--type] [--raw]` | `POST /assets/create-from-url` (or `/import-from-url`) — http/https only; private, loopback, link-local and metadata hosts are rejected, including via redirect |
134
+ | `asset list [--store] [--page] [--limit]` | `GET /assets/search` — only `store` filters server-side; the endpoint otherwise returns your own assets |
135
+ | `asset get <id>` | `GET /assets/list/:id` |
136
+ | `asset status <ids...>` | `POST /assets/check-status` — batched 100 per request; returns only assets you created or in your workspaces |
137
+ | `asset update <id> [-n] [--tags]` | `PATCH /assets/list/:id` |
138
+ | `asset rm <id> [-y]` | `DELETE /assets/list/:id` |
139
+ | `asset download <ids...> [-o path]` | fetches `asset.url`; into a directory, each file is named after the asset and keeps the URL's extension |
140
+ | `asset resolve <url> [-w]` | `GET /assets/resolve-by-url` |
141
+ | `asset summary <id...>` | `POST /assets/list/:id/summary` or `/assets/summary/batch` — at most 100 ids, your own assets only |
142
+
143
+ Asset states: `0` pending, `1` ready, `-1` cancelled.
144
+
145
+ Uploads of `.html`, `.xhtml`, `.svg`, `.js` and `.xml` are stored as
146
+ `application/octet-stream` with `Content-Disposition: attachment`.
147
+
148
+ ## canvas
149
+
150
+ Targets the canvas linked from the browser (top-bar link button) unless
151
+ `-c/--canvas <id>` is given. Every write is a plain fsnode call; an open canvas
152
+ updates live.
153
+
154
+ | Command | API / behaviour |
155
+ | --- | --- |
156
+ | `canvas status` (`link`) | `GET /canvas-link`, then the selected nodes read fresh from the canvas |
157
+ | `canvas unlink` | `DELETE /canvas-link`; the tab turns its button off at its next heartbeat (≤ 60s) |
158
+ | `canvas list [-w]` | canvases under each InfiniteCanvas app: `GET /workspace/list/:id/fs/:appId` → `workspace-v2/canvases` children |
159
+ | `canvas nodes [-c] [-k kind]` | `GET /fsnode/list/:canvasId/children?limit=-1` + `POST /assets/check-status` |
160
+ | `canvas get <nodeId> [-c]` | same load; one node with references (`@image1 ← id`) and frame slots |
161
+ | `canvas edges [-c] [--node]` | the `canvas/edge` children |
162
+ | `canvas add <kind> [-c] [-n] [-t\|--text-file] [--asset] [-p\|--prompt-file] [-m] [-r] [--resolution] [-d] [--set] [--from ids…] [--at x,y]` | `POST /fsnode/create` (`canvas/node/<kind>`), then one `canvas/edge` per `--from`. Kinds: text, title, comment, image, video, audio |
163
+ | `canvas set <nodeId> [-c] [-n] [-t] [--move x,y] [-p] [-m] [-r] [--resolution] [-d] [--set]` | read → merge → `PATCH /fsnode/list/:id` |
164
+ | `canvas connect <from> <to> [-c]` | `POST /fsnode/create` (`canvas/edge`); a first/last-frame video model gets `videoFrameSlot` start, then end |
165
+ | `canvas disconnect <from> <to> [-c]` | `DELETE /fsnode/list/:edgeId` |
166
+ | `canvas rm <nodeIds...> [-c] [-y]` | deletes the nodes' edges, then the nodes; refuses nodes still generating |
167
+ | `canvas gen <nodeId> [-c] [-y] [-w] [--timeout] [--interval]` | `POST /ai/assets/preComputeCost` → confirm → `POST /ai/assets/history/<kind>/generations` (`fsnode`, `scope: canvas`, `scopeId`) → PATCH the node's `file` and `generate.historyId/pendingAssetId` |
168
+
169
+ Storage format (what the web canvas writes):
170
+
171
+ - Node: `mimetype canvas/node/<kind>`, `content.position {x,y}`; text kinds
172
+ `content.text`; media `content.mode` `display` (asset in `file`) or
173
+ `generate` with `content.generate {prompt, model, ratio, resolution,
174
+ duration, values, improvePromptEnabled, historyId?, pendingAssetId?}`. Width
175
+ and height are not stored.
176
+ - Edge: `mimetype canvas/edge`, name `连线`, `content {fromNodeId, toNodeId,
177
+ sourceHandle, targetHandle, videoFrameSlot?}`. References are incoming edges
178
+ sorted by `sort` then id, numbered per source kind.
179
+ - `PATCH` replaces `content` whole, so the CLI always reads and merges first.
180
+ - Canvas apps skip the unique-sibling-name check, so names may repeat.
181
+
182
+ `add`/`set` validate ratio, resolution, duration and `--set` keys against the
183
+ model's form, fill its defaults, drop unknown parameters with a warning, and
184
+ set `autoDuration=false` when `-d` is passed. `-p` and `-t` are literal text
185
+ (`@image1` is not a file).
186
+
187
+ `canvas gen` builds the same request as the canvas Generate button: image,
188
+ video and audio references in order; text-node references prefixed to the
189
+ prompt as `[name]\ntext`; `startImage`/`endImage` from frame slots; Seedance
190
+ ref auto-duration sent as `-1`. References to dataset, bundle, table, YouTube,
191
+ sprite, layer or panorama nodes are refused — generate those from the browser.
192
+
193
+ Link state (`/canvas-link`, one per user, Redis TTL 300s): the tab `PUT`s
194
+ `{sessionId, takeover?, workspaceId, canvasId, selectedNodeIds, selectedNodes,
195
+ viewport, url}` on toggle (`takeover: true`), on selection change and every
196
+ 60s. A non-takeover `PUT` from a tab that no longer owns the link gets
197
+ `409 canvas-link-gone`, which switches that tab's button off.
198
+
199
+ ## gen
200
+
201
+ Shared flags: `-m/--model` (required), `-p/--prompt`, `-n/--n`, `-w/--workspace`,
202
+ `--fsnode`, `--scope`, `--scope-id`, `-r/--ratio`, `--tags`, `--values`,
203
+ `--set`, `--no-history`, `--wait`, `--timeout` (seconds, default 900),
204
+ `--interval` (seconds, default 5).
205
+
206
+ | Command | Extra flags | API |
207
+ | --- | --- | --- |
208
+ | `gen image` | `--ref`, `--omni`, `--style-ref`, `--reference-weight`, `--stylize`, `--model-options` | `POST /ai/assets/history/image/generations` |
209
+ | `gen video` | `-d/--duration`, `--start-image`, `--end-image`, `--ref`, `--ref-video`, `--ref-audio` | `POST /ai/assets/history/video/generations` |
210
+ | `gen audio` | `-d/--duration`, `--ref-audio` | `POST /ai/assets/history/audio/generations` |
211
+ | `gen cost -t <type> -m <model>` | `-n`, `-p`, `-d`, `-r`, `--refs`, `--values` | `POST /ai/assets/preComputeCost` |
212
+
213
+ `--no-history` calls `/ai/assets/<type>/generations`, which returns assets
214
+ directly and cannot be polled by history id.
215
+
216
+ Admission limits, per user and API key (changing IP does not reset them):
217
+
218
+ - Submissions: image 30/min, video 10/min, audio 20/min → HTTP 429.
219
+ - Queued + running jobs: FREE 3, VIP 10, SVIP 25 (admins can override per user)
220
+ → HTTP 429 `排隊中的任務已達上限(N),請等待現有任務完成後再試`. Checked before anything is charged.
221
+ - Insufficient balance is rejected up front; there is no partial charge.
222
+
223
+ Server-owned `values` keys are stripped before billing: `num_images`,
224
+ `referenceImageCount`, `referenceVideoTotalDuration`, `hasReferenceVideo`,
225
+ `output_resolution`, `output_billing_key` and every `*Info` key. Numeric form
226
+ parameters are range-checked against the model's `min`/`max`.
227
+
228
+ `gen cost` validation: `n` 1–32, `duration` −1–600 (−1 = automatic),
229
+ `prompt` ≤10,000 characters, `aspectRatio` ≤32 characters,
230
+ `referenceImageCount` 0–64.
231
+
232
+ ## history
233
+
234
+ `history list [-w] [--mode image|video|audio] [--fsnode] [--scope] [--scope-id] [--tags] [--bookmarked]` →
235
+ `GET /ai/assets/history/search`
236
+
237
+ `history get <id> [--wait] [--timeout] [--interval]` ·
238
+ `history bookmark <id> [--off]` · `history rm <id> [-y]` ·
239
+ `history download <id> [-o dir]`
240
+
241
+ ## models
242
+
243
+ `models list [-f feature...] [-w] [-k keyword] [--all]` →
244
+ `GET /ai/model/list/:features`
245
+
246
+ The endpoint matches with `features.some(...)`, so an empty feature list returns
247
+ nothing; with no `-f` the CLI asks for every feature instead.
248
+
249
+ The response is a curated view — no `id`, `rate`, `tags` or `protocol` — and it
250
+ is de-duplicated by **`nickname`**, which is therefore the unique selector.
251
+ `name` is not unique (`seedance-2.0` exists twice under different providers).
252
+ Pass the `MODEL` column (the nickname) to `-m`.
253
+
254
+ `models get <name>` matches nickname first, then name, and warns when several
255
+ models share the name. It prints ratios, batch range, reference support,
256
+ duration range, accepted/required props, and the `form.other` parameters
257
+ reachable through `--set`.
258
+ `models features` lists the flags accepted by `-f`.
259
+
260
+ Features: `text`, `image`, `video`, `audio`, `tts`, `web`, `thinking`,
261
+ `function_calling`, `image_vision`, `video_vision`, `structured_output`,
262
+ `digital_host`, `upscale_video`.
263
+
264
+ ## agent
265
+
266
+ `agent list` (public marketplace) · `agent mine` · `agent get <id>` ·
267
+ `agent create <name> -d [-i instructions] [-m model] [--visibility] [-c] [-t] [--greeting]` ·
268
+ `agent update <id> …` · `agent rm <id> [-y]` · `agent files <id>` ·
269
+ `agent categories [--mine]` · `agent tags [--mine]` · `agent recommend`
270
+
271
+ Skills & versions:
272
+ `agent skills <id>` · `agent skill-add <id> <ref...>` · `agent skill-remove <id> <ref...>` ·
273
+ `agent versions <id>` · `agent restore <id> <versionId>`. `agent create` also
274
+ takes `--skill <ref...>`.
275
+
276
+ - Skill refs: `system:<key>` (e.g. `system:image`) or `user:<skillId>` (a bare
277
+ value is treated as `user:`). They map to the agent's `skills: SkillRef[]`.
278
+ - Any edit that changes the agent's snapshot auto-records a version
279
+ (`GET /ai/agent/list/:id/versions`); `restore`
280
+ (`POST /ai/agent/list/:id/versions/:versionId/restore`) reverts to one and
281
+ itself records a new version.
282
+
283
+ Visibility: `private`, `unlist`, `public`. Instructions are only returned to
284
+ the agent's creator.
285
+
286
+ Field caps: name ≤200, model ≤100, description and greeting ≤5,000,
287
+ instructions ≤200,000 characters.
288
+
289
+ `-n/--name`, `-c/--category` and `-t/--tag` are **exact** server-side matches,
290
+ not fuzzy search — `--tag` matches agents whose tag array contains that tag.
291
+ There is no keyword search on agents; filter client-side with `--json | jq` if
292
+ you need substring matching.
293
+
294
+ ## preset
295
+
296
+ `preset list` (usable) · `preset mine` · `preset public` ·
297
+ `preset workspace [id]` — all take `-k`, `-t tags`, `-s sort`, `--page`, `--limit`.
298
+
299
+ `preset get <id>` · `preset create <name> -p prompt [-d] [-c category] [-t] [--visibility] [-w]` ·
300
+ `preset update <id> …` · `preset rm <id> [-y]` · `preset fork <id>` ·
301
+ `preset copy <id> [workspaceId]` · `preset tags -s my|public|workspace`
302
+
303
+ `PUT /prompt-preset/list/:id` rebuilds every field from the body, so an omitted
304
+ field arrives as `undefined` and the request 500s. `preset update` therefore
305
+ reads the current preset (bypassing the 10s cache) and merges, which keeps
306
+ partial updates working.
307
+
308
+ Field caps: name ≤200, prompt ≤50,000, description ≤5,000 characters; at most
309
+ 50 tags of ≤64 characters each.
310
+
311
+ Categories: `character`, `storyboard`, `multi_angle`, `composition`, `utility`.
312
+ Sorts: `weight`, `popular`, `latest`, `updated`. `-t` takes comma-separated
313
+ tags that must all match.
314
+
315
+ ## skill
316
+
317
+ Skills are SKILL.md bundles stored server-side as `files: [{path, content}]`.
318
+ Only `.md` files persist (the importer drops everything else); scripts belong to
319
+ *system* skills, which are built-in and read-only.
320
+
321
+ | Command | API |
322
+ | --- | --- |
323
+ | `skill system` | `GET /ai/skill/system` — built-in skills `{kind:'system', key, name, description, runtime, script}` |
324
+ | `skill list [--page] [--limit]` | `GET /ai/skill/search` — your user skills |
325
+ | `skill get <id>` | `GET /ai/skill/:id` — metadata + file list (full content with `--json`) |
326
+ | `skill cat <id> [path]` | prints one file (defaults to SKILL.md) from the `get` response |
327
+ | `skill import <zip>` | `POST /ai/skill/import` (multipart `file`) — a zip whose root holds SKILL.md |
328
+ | `skill push <dir>` | zips a local folder's `.md` files and imports it (ergonomic `import`) |
329
+ | `skill pull <id> [dir] [-f]` | writes the bundle's files to disk |
330
+ | `skill update <id> [-n] [-d] [--visibility] [--dir] [-y]` | `PUT /ai/skill/:id` — metadata, or replace **all** files from `--dir` (confirms first) |
331
+ | `skill rm <id> [-y]` | `DELETE /ai/skill/:id` |
332
+
333
+ Import rules: the zip must contain `SKILL.md` at its root (or one folder deep — a
334
+ single common root folder is stripped). `SKILL.md` frontmatter `name:` is
335
+ required; `description:` is read too. Visibility is `private` (default) or
336
+ `unlist`.
337
+
338
+ Import limits (checked locally by `skill push` / `skill import` before upload):
339
+ zip ≤5MB, ≤200 entries, each `.md` ≤2MB uncompressed, ≤20MB total; 50 user
340
+ skills per account.
341
+
342
+ `skill update --dir` sends `files` in the JSON body instead, where the entity
343
+ caps each file at **200,000 characters** and a path at 512 (≤200 files; name
344
+ ≤200, description ≤5,000). Use `skill push` for larger files.
345
+
346
+ ## profile
347
+
348
+ `profile show` · `profile update [-n] [--handle] [--bio] [--email] [--contact-email] [--phone] [--website] [--avatar] [--banner]` ·
349
+ `profile password [-p]` · `profile logins` · `profile wallet [--limit]` ·
350
+ `profile search <keyword>`
351
+
352
+ `profile search` needs a keyword of 2–64 characters and `--limit` 1–20. It
353
+ fuzzy-matches name and handle, matches an email only exactly, and never
354
+ returns email addresses.
355
+
356
+ Handle rules: 3–24 chars, starts with a letter, lowercase letters/digits/underscore.
357
+ Pass `null` to clear a nullable field. Password minimum is 6 characters.
358
+
359
+ ## API notes
360
+
361
+ - Responses are unwrapped JSON. Errors are `{ message, path, method, timestamp }`
362
+ with a matching HTTP status.
363
+ - An image history's `images` array carries **only `{id}`** — no status, no url.
364
+ Real state comes from `POST /assets/check-status`; the CLI does this for you
365
+ in `gen --wait`, `history get` and `history download`. Polling the history
366
+ alone would report success the moment the entry exists.
367
+ - Asset states: `status` `0` pending, `1` ready, `-1` cancelled; a `Completed`
368
+ asset carrying `error` is also a failure.
369
+ - Some GETs are cached per user for 10s, keyed on the full URL including the
370
+ query string.
371
+ - Paginated endpoints return `{ data, pagination: { total, page, limit, next,
372
+ hasNext, hasPrev, totalPages } }`. An explicit `limit` is capped at **100**.
373
+ `limit: -1` returns everything only on endpoints that opt in (`fs ls` does);
374
+ everywhere else it returns at most **500**. Check `pagination.hasNext`.
375
+ - Batch id arrays (`check-status`, `summary/batch`, `fs batch`) accept at most
376
+ 100 entries, and a single malformed id fails the whole request.
377
+ - Anonymous auth routes are rate-limited per client IP: signin and refresh
378
+ 10/min, signup 5/min, forgot-password 5/min. Authenticated routes are limited
379
+ per user or API key (default 200/min per route).
380
+ - Query strings use `qs` with `allowDots`, so `--set a.b=1` becomes `a.b=1`.
381
+ - Ids are Mongo ObjectIds.