@worldlabsai/cli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Use the World Labs Developer API from a terminal or coding agent with a manually issued API key. The published package has zero npm runtime dependencies; the public SDK is bundled.
4
4
 
5
- With Node 24 or newer, install the CLI and configure your key:
5
+ With Node 22 or newer, install the CLI and configure your key:
6
6
 
7
7
  ```sh
8
8
  npm install -g @worldlabsai/cli && worldlabs setup
@@ -10,7 +10,7 @@ npm install -g @worldlabsai/cli && worldlabs setup
10
10
 
11
11
  On Windows, run these commands in Command Prompt.
12
12
 
13
- `setup` opens the API keys page, prompts you to paste a new key without echoing it, validates it with `assets.list`, and saves it in a plaintext file in your user configuration directory. It can also install the general skill for Codex or Claude Code; you can skip skill installation. Use `worldlabs setup --help` for options.
13
+ `setup` opens the API keys page, prompts you to paste a new key without echoing it, validates it with `assets.list`, and saves it in a plaintext file in your user configuration directory. It can also install the World Labs skill for a coding agent. Use `worldlabs setup --help` for options.
14
14
 
15
15
  ## Onboarding
16
16
 
@@ -21,7 +21,7 @@ flowchart TD
21
21
  C --> D[Paste key into hidden prompt]
22
22
  D --> E[Validate with assets.list]
23
23
  E --> F[Save credentials file]
24
- F --> G[Optionally install Codex or Claude Code skill]
24
+ F --> G[Optionally install the World Labs skill for your coding agent]
25
25
  F --> H[Upload assets and run tasks]
26
26
  ```
27
27
 
@@ -29,20 +29,20 @@ On the API keys page, choose only the scopes required for your work: task submis
29
29
 
30
30
  ## Commands
31
31
 
32
- | Command | Purpose |
33
- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
34
- | `setup [--client codex\|claude-code]` | Configure the API key and optionally install the agent skill. Without `--client`, noninteractive setup skips skill installation. |
35
- | `auth login [--no-browser]` | Open the API keys page, or print its URL for manual opening. |
36
- | `auth status` | Check the key through `assets.list` without printing it. This does not validate every generation scope. |
37
- | `auth logout` | Remove the locally stored key. Revoke the key on the website to invalidate it. |
38
- | `tasks list` | List tasks available through the public SDK. |
39
- | `tasks run <name> --request file.json [--idempotency-key key] [--wait] [--timeout seconds]` | Submit a task request; optionally wait for its operation. Some SDK tasks require beta entitlement. |
40
- | `assets upload file [--mime type]` | Upload a local file. |
41
- | `assets download id --out file` | Download an asset to a local path. |
42
- | `assets list [--page-token token]` | List accessible assets. |
43
- | `operations get id` | Read an operation. |
44
- | `operations wait id [--timeout seconds]` | Poll an operation until it finishes or the timeout expires. The default timeout is 600 seconds. |
45
- | `skills install\|uninstall --client codex\|claude-code` | Install or remove the general agent skill. |
32
+ | Command | Purpose |
33
+ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
34
+ | `setup [--client <agent>] [--api-key-stdin] [--yes]` | Configure the API key and optionally install the World Labs skill. `--yes` accepts installer prompts. |
35
+ | `auth login [--no-browser] [--api-key-stdin]` | Open the API keys page, or print its URL for manual opening. `--api-key-stdin` reads a piped key. |
36
+ | `auth status` | Check the key through `assets.list` without printing it. This does not validate every generation scope. |
37
+ | `auth logout` | Remove the locally stored key. Revoke the key on the website to invalidate it. |
38
+ | `tasks list` | List tasks available through the public SDK. |
39
+ | `tasks run <name> --request file.json [--idempotency-key key] [--wait] [--timeout seconds]` | Submit a task request; optionally wait for its operation. Some SDK tasks require beta entitlement. |
40
+ | `assets upload file [--mime type]` | Upload a local file. |
41
+ | `assets download id --out file` | Download an asset to a local path. |
42
+ | `assets list [--page-token token]` | List accessible assets. |
43
+ | `operations get id` | Read an operation. |
44
+ | `operations wait id [--timeout seconds]` | Poll an operation until it finishes or the timeout expires. The default timeout is 600 seconds. |
45
+ | `skills install\|uninstall [--client <agent>] [--yes]` | Install or remove the World Labs skill globally. `--yes` accepts installer prompts. |
46
46
 
47
47
  Use `--request -` to read task JSON from standard input. Results go to stdout; diagnostics go to stderr, so a result can be saved without mixing in progress messages:
48
48
 
@@ -50,19 +50,60 @@ Use `--request -` to read task JSON from standard input. Results go to stdout; d
50
50
  worldlabs tasks run atlasGenerate --request - --wait --json < request.json > result.json
51
51
  ```
52
52
 
53
- Use `--json` for machine-readable output. Task submission generates an idempotency key and prints it to stderr before sending the request. Save that value and pass it back with `--idempotency-key` to retry an uncertain submission. The bundled `@worldlabsai/client-next` SDK handles API retries and operation polling.
53
+ Use `--json` for machine-readable output. Task submission generates an idempotency key and prints it to stderr before sending the request. Save that value and pass it back with `--idempotency-key` to retry an uncertain submission. The bundled `@worldlabsai/client` SDK handles API retries and operation polling.
54
54
 
55
- Skill updates and removal refuse to change a directory with local edits or unrelated files. Move a customized `worldlabs` skill aside before reinstalling.
55
+ ## Install agent skills
56
56
 
57
- The installed skill includes task descriptions and an upload/submit/result example. It points to
58
- [How To Use Atlas](https://marble5.worldlabs.ai/docs/how-to-use-atlas) for maintained guidance on model inputs, cameras, and task combinations.
57
+ Install the World Labs skill from [worldlabsai/skills](https://github.com/worldlabsai/skills)
58
+ using [Skills CLI](https://skills.sh). Installation is global and requires
59
+ network access, npm, and Git. The installer detects available agents and handles
60
+ client selection:
61
+
62
+ ```sh
63
+ worldlabs skills install
64
+ ```
65
+
66
+ To choose an agent directly, pass `--client`:
67
+
68
+ ```sh
69
+ worldlabs skills install --client cursor
70
+ worldlabs skills install --client gemini-cli
71
+ ```
72
+
73
+ Pass `--yes` to accept installer prompts. Run the install command again to
74
+ refresh the World Labs skill. To remove it, select the agent you installed it
75
+ for:
76
+
77
+ ```sh
78
+ worldlabs skills uninstall --client cursor
79
+ ```
80
+
81
+ Start a new agent session after installation to load the skill. Older
82
+ CLI-managed installations are moved to a local backup, whose path is printed
83
+ in the terminal. If installation fails or is cancelled, the CLI waits for the
84
+ installer to stop, then restores the old skill unless
85
+ the installer has already written a replacement; in that case, it keeps both
86
+ copies and reports the backup path. If an older installation has local edits,
87
+ installation stops and leaves it in place.
88
+
89
+ If the CLI cannot confirm that the installer has stopped, it retains the backup
90
+ without restoring it. Stop any remaining installer processes before restoring
91
+ that backup manually.
92
+
93
+ The public skill points to [How To Use Atlas](https://docs.atlas-beta.worldlabs.ai/how-to-use-atlas) for maintained guidance on model inputs, cameras, and task combinations.
59
94
 
60
95
  ## Configuration
61
96
 
62
97
  The CLI uses `https://api.atlas-beta.worldlabs.ai` by default. Saved keys live in separate files per API origin under `~/.config/worldlabs/credentials/`. On macOS/Linux, new directories use mode `700` and new files use mode `600`. On Windows, files inherit the directory permissions. Keys are plaintext, so programs running as your user can read them. `auth status` shows the saved file location.
63
98
 
64
- Set `WORLDLABS_CONFIG_DIR` to use another configuration directory, such as a temporary directory for tests. Keep it outside repositories. `WLT_API_KEY` takes precedence over saved keys for automation. Generated Windows skill commands use PowerShell.
99
+ Set `WORLDLABS_CONFIG_DIR` to use another configuration directory, such as a temporary directory for tests. Keep it outside repositories. `WLT_API_KEY` takes precedence over saved keys for automation.
100
+
101
+ To save a key without the hidden prompt, for example from a secret manager or where no one can type into the terminal, pipe it to `--api-key-stdin`. The CLI checks it and saves it like `auth login`; the key stays out of the command line and shell history:
102
+
103
+ ```sh
104
+ worldlabs auth login --api-key-stdin < key.txt
105
+ ```
65
106
 
66
107
  For development or a nonproduction deployment, set `WORLDLABS_API_ORIGIN` and, when needed, `WORLDLABS_API_KEYS_URL`. The key page defaults to `https://atlas-beta.worldlabs.ai/api-keys`. Do not commit credentials, client IDs, or nonproduction hostnames.
67
108
 
68
- See the [World Labs CLI skill](skills/worldlabs/SKILL.md).
109
+ See the [World Labs skill](https://github.com/worldlabsai/skills).
package/dist/index.js CHANGED
@@ -4,6 +4,14 @@
4
4
  import { readFileSync } from "node:fs";
5
5
  import { parseArgs as parseArgs2 } from "node:util";
6
6
 
7
+ // ../client/dist/core/network-error.js
8
+ var NetworkError = class extends TypeError {
9
+ name = "NetworkError";
10
+ constructor(cause) {
11
+ super(cause.message, { cause });
12
+ }
13
+ };
14
+
7
15
  // ../client/dist/core/sleep.js
8
16
  var sleep = async (ms, signal) => {
9
17
  if (signal?.aborted) {
@@ -21,6 +29,11 @@ var sleep = async (ms, signal) => {
21
29
  };
22
30
 
23
31
  // ../client/dist/core/transport.js
32
+ var validationIssue = (issue) => {
33
+ const location = typeof issue === "object" && issue !== null ? Reflect.get(issue, "loc") : void 0;
34
+ const message = typeof issue === "object" && issue !== null ? Reflect.get(issue, "msg") : issue;
35
+ return Array.isArray(location) ? `${location.join(".")}: ${String(message)}` : String(message);
36
+ };
24
37
  var errorDetail = (body) => {
25
38
  if (typeof body !== "object" || body === null) {
26
39
  return void 0;
@@ -30,6 +43,9 @@ var errorDetail = (body) => {
30
43
  if (typeof value === "string" && value !== "") {
31
44
  return value;
32
45
  }
46
+ if (Array.isArray(value) && value.length > 0) {
47
+ return value.map((issue) => validationIssue(issue)).join("; ");
48
+ }
33
49
  }
34
50
  return void 0;
35
51
  };
@@ -56,35 +72,36 @@ var fillPath = (path, params) => path.replaceAll(/\{(?<name>\w+)\}/gu, (_, name)
56
72
  }
57
73
  return encodeURIComponent(value);
58
74
  });
59
- var compose = (middleware, fetch2) => {
75
+ var compose = (middleware, fetch2, options) => {
60
76
  let next = fetch2;
61
77
  for (const layer of middleware.toReversed()) {
62
78
  const inner = next;
63
- next = async (request) => await layer(request, inner);
79
+ next = async (request) => await layer(request, inner, options);
64
80
  }
65
81
  return next;
66
82
  };
67
83
  var parseBody = async (response) => {
68
- const text2 = await response.text();
69
- if (text2 === "") {
84
+ const text3 = await response.text();
85
+ if (text3 === "") {
70
86
  return void 0;
71
87
  }
72
88
  try {
73
- return JSON.parse(text2);
89
+ return JSON.parse(text3);
74
90
  } catch {
75
- return text2;
91
+ return text3;
76
92
  }
77
93
  };
78
94
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
79
- var retryDelay = (error, attempt) => {
80
- const backoff = 1e3 * 2 ** attempt * (0.5 + Math.random() / 2);
95
+ var MAX_RETRY_AFTER_MS = 1e4;
96
+ var retryAfterMs = (error) => {
81
97
  const value = error instanceof ApiError ? error.response.headers.get("retry-after")?.trim() : void 0;
82
98
  if (!value) {
83
- return backoff;
99
+ return void 0;
84
100
  }
85
101
  const delay = /^\d+$/u.test(value) ? Number(value) * 1e3 : Date.parse(value) - Date.now();
86
- return Math.max(backoff, Number.isFinite(delay) ? delay : 0);
102
+ return Number.isFinite(delay) ? Math.max(0, delay) : void 0;
87
103
  };
104
+ var retryDelay = (error, attempt) => Math.max(1e3 * 2 ** attempt * (0.5 + Math.random() / 2), retryAfterMs(error) ?? 0);
88
105
  var createTransport = (options) => {
89
106
  const fetch2 = options.fetch ?? (async (request) => await globalThis.fetch(request));
90
107
  const send = compose(options.middleware ?? [], fetch2);
@@ -93,7 +110,8 @@ var createTransport = (options) => {
93
110
  return {
94
111
  fetch: fetch2,
95
112
  send,
96
- request: async (path, { body, params, query, retry = false, ...init } = {}) => {
113
+ request: async (path, requestOptions = {}) => {
114
+ const { body, params, query, retry = false, ...init } = requestOptions;
97
115
  const url = new URL(baseUrl + fillPath(path, { ...boundParams, ...params }));
98
116
  for (const [key, value] of Object.entries(query ?? {})) {
99
117
  if (value !== void 0 && value !== null) {
@@ -110,14 +128,14 @@ var createTransport = (options) => {
110
128
  ...body === void 0 ? {} : { body: JSON.stringify(body) }
111
129
  });
112
130
  let networkError;
113
- const sendRequest = retry ? compose(options.middleware ?? [], async (attempt) => {
131
+ const sendRequest = compose(options.middleware ?? [], async (attempt) => {
114
132
  try {
115
133
  return await fetch2(attempt);
116
134
  } catch (error) {
117
135
  networkError = error;
118
136
  throw error;
119
137
  }
120
- }) : send;
138
+ }, requestOptions);
121
139
  for (let attempt = 0; ; attempt += 1) {
122
140
  request.signal.throwIfAborted();
123
141
  networkError = void 0;
@@ -139,8 +157,11 @@ var createTransport = (options) => {
139
157
  return parsed;
140
158
  } catch (error) {
141
159
  request.signal.throwIfAborted();
142
- const transient = error instanceof ApiError ? RETRYABLE_STATUS_CODES.has(error.response.status) : error instanceof TypeError && error === networkError;
160
+ const networkFailure = error instanceof TypeError && error === networkError;
161
+ const transient = error instanceof ApiError ? RETRYABLE_STATUS_CODES.has(error.response.status) && (retryAfterMs(error) ?? 0) <= MAX_RETRY_AFTER_MS : networkFailure;
143
162
  if (!retry || attempt >= 3 || !transient) {
163
+ if (networkFailure)
164
+ throw new NetworkError(error);
144
165
  throw error;
145
166
  }
146
167
  await sleep(retryDelay(error, attempt), request.signal);
@@ -281,6 +302,7 @@ var createOperations = (transport, operationPath2) => {
281
302
  *
282
303
  * An operation can finish with an API-reported task error; inspect `error` before
283
304
  * reading `response`. Aborting stops local waiting without canceling the task.
305
+ * With `retryNetworkErrors`, network failures resume polling until cancellation.
284
306
  * HTTP 502, 503, and 504 polling failures are retried up to three times in a row;
285
307
  * a successful poll resets the retry count.
286
308
  *
@@ -303,6 +325,9 @@ var createOperations = (transport, operationPath2) => {
303
325
  gatewayErrors = 0;
304
326
  return current;
305
327
  } catch (error) {
328
+ signal?.throwIfAborted();
329
+ if (options?.retryNetworkErrors && error instanceof NetworkError)
330
+ return operation;
306
331
  gatewayErrors += 1;
307
332
  if (!(error instanceof ApiError) || ![502, 503, 504].includes(error.response.status) || gatewayErrors > 3) {
308
333
  throw error;
@@ -315,7 +340,7 @@ var createOperations = (transport, operationPath2) => {
315
340
  };
316
341
 
317
342
  // ../client/dist/core/tasks.js
318
- var submitTask = async (transport, path, body, { idempotencyKey = crypto.randomUUID(), signal, wait } = {}) => {
343
+ var submitTask = async (transport, path, body, { idempotencyKey = crypto.randomUUID(), signal, wait, retry = true } = {}) => {
319
344
  if (idempotencyKey.trim() === "") {
320
345
  throw new Error("Idempotency key must not be empty");
321
346
  }
@@ -324,7 +349,7 @@ var submitTask = async (transport, path, body, { idempotencyKey = crypto.randomU
324
349
  headers: { "Idempotency-Key": idempotencyKey },
325
350
  method: "POST",
326
351
  query: { wait },
327
- retry: true,
352
+ retry,
328
353
  ...signal ? { signal } : {}
329
354
  });
330
355
  };
@@ -393,6 +418,18 @@ var createTasks = (transport) => ({
393
418
  * @beta Requires beta access for your account.
394
419
  */
395
420
  images2PosedRGBD: async (body, options) => await submitTask(transport, "/api/v2/tasks:images2PosedRGBD", body, options),
421
+ /**
422
+ * Submit the posedImages2Splats task
423
+ * Retries network failures and HTTP 429/502/503/504 up to three times with the same key and body.
424
+ *
425
+ * @param body - Task inputs and settings.
426
+ * @param options - Abort signal, idempotency key, and server-wait preference.
427
+ * @returns An operation to pass to `client.operations.wait` for the typed result.
428
+ * Check the completed operation's `error` before reading its `response`.
429
+ * @throws {ApiError} If the API rejects the submission or retries are exhausted.
430
+ * @beta Requires beta access for your account.
431
+ */
432
+ posedImages2Splats: async (body, options) => await submitTask(transport, "/api/v2/tasks:posedImages2Splats", body, options),
396
433
  /**
397
434
  * Submit the splats2Mesh task
398
435
  * Retries network failures and HTTP 429/502/503/504 up to three times with the same key and body.
@@ -429,6 +466,9 @@ var createClient = (options) => {
429
466
  };
430
467
  };
431
468
 
469
+ // src/auth.ts
470
+ import { text } from "node:stream/consumers";
471
+
432
472
  // src/config.ts
433
473
  var CliError = class extends Error {
434
474
  name = "CliError";
@@ -529,13 +569,20 @@ async function prompt(message, hidden = false) {
529
569
  terminal: true
530
570
  });
531
571
  const controller = new AbortController();
532
- reader.once("SIGINT", () => controller.abort());
572
+ let interrupted = false;
573
+ reader.once("SIGINT", () => {
574
+ interrupted = true;
575
+ controller.abort();
576
+ });
577
+ reader.once("close", () => controller.abort());
533
578
  process.stderr.write(message);
534
579
  try {
535
580
  const answer = await reader.question("", { signal: controller.signal });
536
581
  return answer.trim();
537
582
  } catch {
538
- throw new CliError("Cancelled.");
583
+ throw new CliError(
584
+ interrupted ? "Cancelled." : "Input closed before an answer. For automation, set WLT_API_KEY or pipe the key to worldlabs auth login --api-key-stdin."
585
+ );
539
586
  } finally {
540
587
  reader.close();
541
588
  if (hidden) process.stderr.write("\n");
@@ -573,12 +620,25 @@ async function authStatus() {
573
620
  ...process.env.WLT_API_KEY?.trim() ? {} : { credentialFile: credentialPath(configuration().apiOrigin) }
574
621
  };
575
622
  }
576
- async function login(noBrowser) {
623
+ async function pipedApiKey() {
624
+ if (process.stdin.isTTY) {
625
+ throw new CliError(
626
+ "--api-key-stdin reads a piped key, for example: worldlabs auth login --api-key-stdin < key.txt"
627
+ );
628
+ }
629
+ const input = await text(process.stdin);
630
+ const apiKey = input.trim();
631
+ if (!apiKey) throw new CliError("No API key on standard input.");
632
+ if (/\s/u.test(apiKey)) throw new CliError("Standard input must contain only the API key.");
633
+ return apiKey;
634
+ }
635
+ async function login(noBrowser, pipedKey) {
636
+ if (pipedKey) return await saveValidatedKey(pipedKey);
577
637
  if (process.env.WLT_API_KEY?.trim()) return await authStatus();
578
638
  const { apiOrigin, apiKeysUrl } = configuration();
579
639
  if (!process.stdin.isTTY || !process.stderr.isTTY) {
580
640
  throw new CliError(
581
- "Run worldlabs auth login in your own terminal to paste a key privately. Agents should never ask for keys in chat."
641
+ "Run worldlabs auth login in your own terminal to paste a key privately. Agents should never ask for keys in chat. To read a key from a file or secret manager instead, pipe it to worldlabs auth login --api-key-stdin."
582
642
  );
583
643
  }
584
644
  process.stderr.write(
@@ -591,6 +651,10 @@ The key will be saved in plaintext at ${credentialPath(apiOrigin)}.
591
651
  if (!noBrowser) await openBrowser(apiKeysUrl);
592
652
  const apiKey = await prompt("Paste API key (hidden): ", true);
593
653
  if (!apiKey) throw new CliError("No key entered.");
654
+ return await saveValidatedKey(apiKey);
655
+ }
656
+ async function saveValidatedKey(apiKey) {
657
+ const { apiOrigin } = configuration();
594
658
  await createClient({ apiKey, baseUrl: apiOrigin }).assets.list({
595
659
  pageSize: 1,
596
660
  signal: AbortSignal.timeout(3e4)
@@ -618,7 +682,7 @@ import { randomUUID as randomUUID2 } from "node:crypto";
618
682
  import { openAsBlob } from "node:fs";
619
683
  import { open as open2, readFile, rm as rm2 } from "node:fs/promises";
620
684
  import nodePath2 from "node:path";
621
- import { text } from "node:stream/consumers";
685
+ import { text as text2 } from "node:stream/consumers";
622
686
  import { pipeline } from "node:stream/promises";
623
687
  import { parseArgs } from "node:util";
624
688
  var mimeTypes = {
@@ -626,9 +690,11 @@ var mimeTypes = {
626
690
  ".jpeg": "image/jpeg",
627
691
  ".png": "image/png",
628
692
  ".webp": "image/webp",
693
+ ".exr": "image/x-exr",
629
694
  ".mp4": "video/mp4",
630
- ".ply": "application/octet-stream",
631
- ".spz": "application/octet-stream"
695
+ ".glb": "model/gltf-binary",
696
+ ".ply": "model/x-ply",
697
+ ".spz": "model/x-spz"
632
698
  };
633
699
  function timeout(value) {
634
700
  const seconds = value === void 0 ? 600 : Number(value);
@@ -665,7 +731,7 @@ async function runApiCommand(args2) {
665
731
  throw new CliError("Unknown task. Run worldlabs tasks list.");
666
732
  }
667
733
  const body = JSON.parse(
668
- values.request === "-" ? await text(process.stdin) : await readFile(values.request, "utf8")
734
+ values.request === "-" ? await text2(process.stdin) : await readFile(values.request, "utf8")
669
735
  );
670
736
  if (!body || typeof body !== "object" || Array.isArray(body)) {
671
737
  throw new CliError("Task request must be a JSON object.");
@@ -759,24 +825,30 @@ Reuse this key and request if submission is interrupted.
759
825
  throw new CliError("Unknown command or arguments. Run worldlabs --help.");
760
826
  }
761
827
 
828
+ // src/node-version.ts
829
+ var MINIMUM_NODE_MAJOR = 22;
830
+ function nodeVersionProblem(version2) {
831
+ if (Number(version2.split(".")[0]) >= MINIMUM_NODE_MAJOR) return void 0;
832
+ return `worldlabs requires Node.js ${MINIMUM_NODE_MAJOR} or newer, but this is Node.js ${version2}. Install the current Node.js LTS from https://nodejs.org and run the command again.`;
833
+ }
834
+
762
835
  // src/skills.ts
763
- import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
764
- import { cp, lstat as lstat2, mkdir as mkdir2, readFile as readFile2, readdir, rename as rename2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
836
+ import { execFile, spawn as spawn2 } from "node:child_process";
837
+ import { createHash as createHash2 } from "node:crypto";
838
+ import { lstat as lstat2, mkdtemp, readFile as readFile2, readdir, rename as rename2, rmdir } from "node:fs/promises";
765
839
  import { homedir as homedir2 } from "node:os";
766
840
  import nodePath3 from "node:path";
767
- import { fileURLToPath } from "node:url";
768
841
  var marker = ".worldlabs-install.json";
769
- var source = fileURLToPath(new URL("../skills/worldlabs/", import.meta.url));
770
- function skillTarget(client) {
771
- if (client === "codex") {
772
- return nodePath3.join(
773
- process.env.CODEX_HOME ?? nodePath3.join(homedir2(), ".codex"),
774
- "skills",
775
- "worldlabs"
842
+ var InstallerTerminationError = class extends CliError {
843
+ name = "InstallerTerminationError";
844
+ };
845
+ function skillClient(client) {
846
+ if (client !== void 0 && !/^[a-z0-9][a-z0-9-]*$/u.test(client)) {
847
+ throw new CliError(
848
+ "--client must be an agent name, such as codex, claude-code, cursor, or gemini-cli."
776
849
  );
777
850
  }
778
- if (client === "claude-code") return nodePath3.join(homedir2(), ".claude", "skills", "worldlabs");
779
- throw new CliError("Choose --client codex or --client claude-code.");
851
+ return client;
780
852
  }
781
853
  async function fingerprint(root) {
782
854
  const hash = createHash2("sha256");
@@ -797,93 +869,213 @@ async function fingerprint(root) {
797
869
  await visit("");
798
870
  return hash.digest("hex");
799
871
  }
800
- async function manageSkills(action, target) {
801
- const parent = nodePath3.dirname(target);
802
- const lock = `${target}.lock`;
803
- await mkdir2(parent, { recursive: true });
872
+ async function cleanupLegacySkill(client) {
873
+ const root = client === "codex" ? process.env.CODEX_HOME ?? nodePath3.join(homedir2(), ".codex") : nodePath3.join(homedir2(), ".claude");
874
+ const target = nodePath3.join(root, "skills", "worldlabs");
875
+ let stat;
804
876
  try {
805
- await mkdir2(lock);
806
- } catch {
877
+ stat = await lstat2(target);
878
+ } catch (error) {
879
+ if (error.code === "ENOENT") return;
880
+ throw error;
881
+ }
882
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return;
883
+ let expected;
884
+ try {
885
+ expected = JSON.parse(await readFile2(nodePath3.join(target, marker), "utf8"));
886
+ } catch (error) {
887
+ if (error.code === "ENOENT") return;
888
+ throw new CliError(
889
+ `Cannot verify the old skill at ${target}. Move it aside before continuing.`
890
+ );
891
+ }
892
+ if (await fingerprint(target) !== expected) {
807
893
  throw new CliError(
808
- `Skill installation is locked at ${lock}. Remove the lock only if no installer is running.`
894
+ `Old skill has local changes at ${target}. Move it aside before continuing.`
809
895
  );
810
896
  }
811
- const staging = nodePath3.join(parent, `.worldlabs-${randomUUID3()}`);
812
- const backup = `${staging}-backup`;
897
+ const backupContainer = await mkdtemp(
898
+ nodePath3.join(nodePath3.dirname(target), ".worldlabs-backup-")
899
+ );
900
+ const backup = nodePath3.join(backupContainer, "worldlabs");
813
901
  try {
814
- let exists = false;
815
- try {
816
- await lstat2(target);
817
- exists = true;
818
- } catch (error) {
819
- if (error.code !== "ENOENT") throw error;
820
- }
821
- if (exists) {
822
- const actual = await fingerprint(target);
823
- let expected;
902
+ await rename2(target, backup);
903
+ } catch (error) {
904
+ await rmdir(backupContainer).catch(() => null);
905
+ throw error;
906
+ }
907
+ process.stderr.write(`Moved the old CLI-managed skill to ${backup}
908
+ `);
909
+ return { target, backup };
910
+ }
911
+ function runSkillsInstaller(args2, signal) {
912
+ signal.throwIfAborted();
913
+ const env = { ...process.env, DISABLE_TELEMETRY: "1" };
914
+ delete env.WLT_API_KEY;
915
+ const windows = process.platform === "win32";
916
+ return new Promise((resolve, reject) => {
917
+ const child = spawn2(
918
+ windows ? "cmd.exe" : "npx",
919
+ windows ? ["/d", "/s", "/c", `npx.cmd ${args2.join(" ")}`] : args2,
920
+ { env, detached: !windows, stdio: ["inherit", "pipe", "pipe"] }
921
+ );
922
+ child.stdout.pipe(process.stderr, { end: false });
923
+ child.stderr.pipe(process.stderr, { end: false });
924
+ let forceTimer;
925
+ let stopTimer;
926
+ let settled = false;
927
+ const finish = (error) => {
928
+ if (settled) return;
929
+ settled = true;
930
+ clearTimeout(forceTimer);
931
+ clearTimeout(stopTimer);
932
+ child.stdout.unpipe(process.stderr);
933
+ child.stderr.unpipe(process.stderr);
934
+ signal.removeEventListener("abort", cancel);
935
+ if (error) reject(error);
936
+ else resolve();
937
+ };
938
+ const cannotStop = () => {
939
+ child.stdout.destroy();
940
+ child.stderr.destroy();
941
+ child.unref();
942
+ finish(
943
+ new InstallerTerminationError(
944
+ "Could not stop the skills installer. Legacy backups were retained; stop the installer before restoring them."
945
+ )
946
+ );
947
+ };
948
+ const killGroup = (terminationSignal) => {
949
+ if (!child.pid) return;
824
950
  try {
825
- expected = JSON.parse(await readFile2(nodePath3.join(target, marker), "utf8"));
826
- } catch {
827
- throw new CliError(`Refusing to replace an unowned skill: ${target}`);
951
+ process.kill(-child.pid, terminationSignal);
952
+ } catch (error) {
953
+ if (error.code !== "ESRCH") cannotStop();
828
954
  }
829
- if (actual !== expected) {
830
- throw new CliError(`Skill has local changes. Move it aside before continuing: ${target}`);
955
+ };
956
+ const cancel = () => {
957
+ if (!child.pid) return;
958
+ stopTimer = setTimeout(cannotStop, 5e3).unref();
959
+ if (windows) {
960
+ execFile(
961
+ "taskkill",
962
+ ["/PID", String(child.pid), "/T", "/F"],
963
+ { timeout: 4e3 },
964
+ (error) => {
965
+ if (error && !settled) cannotStop();
966
+ }
967
+ );
968
+ } else {
969
+ forceTimer = setTimeout(() => killGroup("SIGKILL"), 2e3).unref();
970
+ killGroup(signal.reason);
831
971
  }
832
- }
833
- if (action === "uninstall") {
834
- if (exists) await rm3(target, { recursive: true });
835
- return { path: target, removed: exists };
836
- }
837
- await cp(source, staging, { recursive: true });
838
- const entry = fileURLToPath(
839
- new URL(import.meta.url.endsWith(".ts") ? "index.ts" : "index.js", import.meta.url)
972
+ };
973
+ signal.addEventListener("abort", cancel, { once: true });
974
+ child.once(
975
+ "error",
976
+ () => finish(
977
+ new CliError("Could not start the skills installer. Check that npm and npx are available.")
978
+ )
840
979
  );
841
- const windows = process.platform === "win32";
842
- const shell = windows ? "powershell" : "sh";
843
- const quote = (value) => `'${value.replaceAll("'", windows ? "''" : String.raw`'\''`)}'`;
844
- const command = `${windows ? "& " : ""}${quote(process.execPath)} ${quote(entry)}`;
845
- for (const name of await readdir(staging, { recursive: true })) {
846
- const path = nodePath3.join(staging, name);
847
- const stat = await lstat2(path);
848
- if (stat.isFile()) {
849
- const content = await readFile2(path, "utf8");
850
- await writeFile2(
851
- path,
852
- content.replaceAll("{{CLI}}", () => command).replaceAll("{{SHELL}}", shell)
853
- );
980
+ child.once(
981
+ "close",
982
+ (code) => finish(
983
+ code === 0 && !signal.aborted ? void 0 : new CliError("Skills installer did not complete. See its output above.")
984
+ )
985
+ );
986
+ });
987
+ }
988
+ async function manageSkills(action, client, yes = false) {
989
+ skillClient(client);
990
+ const moved = [];
991
+ const controller = new AbortController();
992
+ const cancel = (signal) => controller.abort(signal);
993
+ process.on("SIGINT", cancel);
994
+ process.on("SIGTERM", cancel);
995
+ try {
996
+ for (const candidate of client ? [client] : ["codex", "claude-code"]) {
997
+ if (candidate === "codex" || candidate === "claude-code") {
998
+ const record = await cleanupLegacySkill(candidate);
999
+ if (record) moved.push(record);
854
1000
  }
1001
+ if (controller.signal.aborted) throw new CliError("Skills installation cancelled.");
855
1002
  }
856
- await writeFile2(nodePath3.join(staging, marker), JSON.stringify(await fingerprint(staging)));
857
- if (exists) await rename2(target, backup);
858
- try {
859
- await rename2(staging, target);
860
- } catch (error) {
861
- if (exists) await rename2(backup, target);
862
- throw error;
1003
+ const args2 = [
1004
+ "--yes",
1005
+ "skills@1.7.2",
1006
+ ...action === "install" ? ["add", "worldlabsai/skills", "--skill", "worldlabs"] : ["remove", "worldlabs"],
1007
+ ...client ? ["--agent", client] : [],
1008
+ "--global",
1009
+ ...yes ? ["--yes"] : []
1010
+ ];
1011
+ await runSkillsInstaller(args2, controller.signal);
1012
+ } catch (error) {
1013
+ if (error instanceof InstallerTerminationError) throw error;
1014
+ for (const { target, backup } of moved.toReversed()) {
1015
+ try {
1016
+ await lstat2(target);
1017
+ process.stderr.write(
1018
+ `Could not restore the old skill because a skill now exists at ${target}. Backup retained at ${backup}
1019
+ `
1020
+ );
1021
+ } catch (restoreCheckError) {
1022
+ if (restoreCheckError.code !== "ENOENT") {
1023
+ process.stderr.write(
1024
+ `Could not check the old skill location ${target}. Backup retained at ${backup}
1025
+ `
1026
+ );
1027
+ continue;
1028
+ }
1029
+ try {
1030
+ await rename2(backup, target);
1031
+ await rmdir(nodePath3.dirname(backup)).catch(() => null);
1032
+ } catch {
1033
+ process.stderr.write(
1034
+ `Could not restore the old skill at ${target}. Backup retained at ${backup}
1035
+ `
1036
+ );
1037
+ }
1038
+ }
863
1039
  }
864
- await rm3(backup, { recursive: true, force: true });
865
- return { path: target, next: "Start a new agent session to load the worldlabs skill." };
1040
+ throw error;
866
1041
  } finally {
867
- await rm3(staging, { recursive: true, force: true });
868
- await rm3(lock, { recursive: true, force: true });
1042
+ process.off("SIGINT", cancel);
1043
+ process.off("SIGTERM", cancel);
1044
+ if (controller.signal.aborted) {
1045
+ process.kill(process.pid, controller.signal.reason);
1046
+ }
869
1047
  }
1048
+ return {
1049
+ client,
1050
+ ...moved.length ? { legacyBackups: moved.map(({ backup }) => backup) } : {},
1051
+ next: "Start a new agent session after changing skills."
1052
+ };
870
1053
  }
871
1054
 
872
1055
  // src/setup.ts
873
- async function setup(client, noBrowser) {
874
- let target;
875
- if (client !== void 0) {
876
- target = skillTarget(client);
877
- } else if (process.stdin.isTTY && process.stderr.isTTY) {
878
- const choice = await prompt("Install agent skill: 1) Codex 2) Claude Code 3) Skip [3]: ");
879
- if (choice === "1" || choice === "2") {
880
- target = skillTarget(choice === "1" ? "codex" : "claude-code");
881
- } else if (choice && choice !== "3") {
882
- throw new CliError("Choose 1, 2, or 3.");
1056
+ async function setup(client, noBrowser, yes = false, keyFromStdin = false) {
1057
+ const target = skillClient(client);
1058
+ const pipedKey = keyFromStdin ? await pipedApiKey() : void 0;
1059
+ let installSkill = target !== void 0;
1060
+ if (!installSkill && process.stdin.isTTY && process.stderr.isTTY) {
1061
+ const answer = await prompt("Install the World Labs agent skill? [y/N]: ");
1062
+ const choice = answer.toLowerCase();
1063
+ if (choice && !["y", "yes", "n", "no"].includes(choice)) {
1064
+ throw new CliError("Choose yes or no.");
1065
+ }
1066
+ installSkill = choice === "y" || choice === "yes";
1067
+ }
1068
+ const auth = await login(noBrowser, pipedKey);
1069
+ let skill;
1070
+ if (installSkill) {
1071
+ try {
1072
+ skill = await manageSkills("install", target, yes);
1073
+ } catch (error) {
1074
+ throw new CliError(
1075
+ `API authentication succeeded, but skill installation failed. ${error instanceof Error ? error.message : "Retry worldlabs skills install."}`
1076
+ );
883
1077
  }
884
1078
  }
885
- const auth = await login(noBrowser);
886
- const skill = target ? await manageSkills("install", target) : void 0;
887
1079
  return {
888
1080
  ...auth,
889
1081
  ...skill ? { skill } : {},
@@ -898,9 +1090,9 @@ var { version } = JSON.parse(
898
1090
  var help = `worldlabs - World Labs Developer API CLI
899
1091
 
900
1092
  Usage: worldlabs [--json] <command>
901
- setup [--client codex|claude-code] [--no-browser]
902
- skills install|uninstall --client codex|claude-code
903
- auth login [--no-browser]
1093
+ setup [--client <agent>] [--no-browser] [--api-key-stdin] [--yes]
1094
+ skills install|uninstall [--client <agent>] [--yes]
1095
+ auth login [--no-browser] [--api-key-stdin]
904
1096
  auth status
905
1097
  auth logout
906
1098
  config show
@@ -913,30 +1105,53 @@ Usage: worldlabs [--json] <command>
913
1105
  --version
914
1106
  --help
915
1107
 
1108
+ Skills use Skills CLI client detection when --client is omitted (e.g. codex, claude-code, cursor, gemini-cli).
916
1109
  Use --request - to read task JSON from standard input.
917
1110
 
918
- Saved login uses a plaintext file in your user configuration directory. Automation can set WLT_API_KEY.
1111
+ Saved login uses a plaintext file in your user configuration directory. Automation can set WLT_API_KEY,
1112
+ or save a key without the prompt by piping it to --api-key-stdin.
919
1113
  `;
920
1114
  async function main(args2) {
1115
+ const unsupported = nodeVersionProblem(process.versions.node);
1116
+ if (unsupported) throw new CliError(unsupported);
921
1117
  if (args2.length === 0 || args2.includes("--help")) return help;
922
1118
  if (args2.length === 1 && args2[0] === "--version") return version;
923
1119
  const [command, action, ...rest] = args2;
924
1120
  if (command === "setup") {
925
1121
  const { values } = parseArgs2({
926
1122
  args: args2.slice(1),
927
- options: { client: { type: "string" }, "no-browser": { type: "boolean" } }
1123
+ options: {
1124
+ client: { type: "string" },
1125
+ "no-browser": { type: "boolean" },
1126
+ "api-key-stdin": { type: "boolean" },
1127
+ yes: { type: "boolean" }
1128
+ }
928
1129
  });
929
- return await setup(values.client, Boolean(values["no-browser"]));
1130
+ return await setup(
1131
+ values.client,
1132
+ Boolean(values["no-browser"]),
1133
+ Boolean(values.yes),
1134
+ Boolean(values["api-key-stdin"])
1135
+ );
930
1136
  }
931
1137
  if (command === "skills" && (action === "install" || action === "uninstall")) {
932
- const { values } = parseArgs2({ args: rest, options: { client: { type: "string" } } });
933
- return await manageSkills(action, skillTarget(values.client));
1138
+ const { values } = parseArgs2({
1139
+ args: rest,
1140
+ options: { client: { type: "string" }, yes: { type: "boolean" } }
1141
+ });
1142
+ return await manageSkills(action, skillClient(values.client), Boolean(values.yes));
934
1143
  }
935
1144
  if (command === "config" && action === "show" && !rest.length) return configuration();
936
1145
  if (command === "auth") {
937
1146
  if (action === "login") {
938
- const { values } = parseArgs2({ args: rest, options: { "no-browser": { type: "boolean" } } });
939
- return await login(Boolean(values["no-browser"]));
1147
+ const { values } = parseArgs2({
1148
+ args: rest,
1149
+ options: { "no-browser": { type: "boolean" }, "api-key-stdin": { type: "boolean" } }
1150
+ });
1151
+ return await login(
1152
+ Boolean(values["no-browser"]),
1153
+ values["api-key-stdin"] ? await pipedApiKey() : void 0
1154
+ );
940
1155
  }
941
1156
  if (action === "status" && !rest.length) return await authStatus();
942
1157
  if (action === "logout" && !rest.length) return await logout();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@worldlabsai/cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "World Labs command line interface",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -8,16 +8,15 @@
8
8
  },
9
9
  "files": [
10
10
  "dist/index.js",
11
- "skills",
12
11
  "LICENSE"
13
12
  ],
14
13
  "type": "module",
15
14
  "engines": {
16
- "node": ">=24"
15
+ "node": ">=22"
17
16
  },
18
17
  "publishConfig": {
19
- "access": "restricted",
18
+ "access": "public",
20
19
  "registry": "https://registry.npmjs.org/",
21
- "tag": "next"
20
+ "tag": "latest"
22
21
  }
23
22
  }
@@ -1,43 +0,0 @@
1
- ---
2
- name: worldlabs
3
- description: Generate and edit scene images, estimate cameras and depth, or extract meshes with the World Labs CLI.
4
- ---
5
-
6
- # World Labs
7
-
8
- Use `{{CLI}}` for the Developer API. Run commands in `{{SHELL}}`. Run `{{CLI}} --help` for command syntax.
9
-
10
- ## Choose a task
11
-
12
- | Task | Purpose |
13
- | --- | --- |
14
- | `atlasTextToImage` | Generate starting images from a description of an environment. |
15
- | `images2PosedRGBD` | Estimate cameras and depth from images of a scene. |
16
- | `atlasGenerate` | Generate new viewpoints using posed RGBD context. |
17
- | `atlasChisel` | Generate posed images from a prompt and optional depth layouts. |
18
- | `atlasMasked` | Fill masked regions across posed images. |
19
- | `splats2Mesh` | Extract a mesh from a Gaussian splat scene. |
20
-
21
- Read [How To Use Atlas](https://marble5.worldlabs.ai/docs/how-to-use-atlas) before choosing model inputs, calculating cameras, or chaining tasks. It explains scene data, model behavior, and the requirements that affect results. Follow its task or camera links when you need exact fields or geometry calculations. `{{CLI}} tasks list` reports SDK task names; account access can differ.
22
-
23
- ## Submit and inspect
24
-
25
- Upload a local image:
26
-
27
- ```{{SHELL}}
28
- {{CLI}} assets upload photo.jpg --json > asset.json
29
- ```
30
-
31
- Write `request.json` as `{"frames":[{"imageAsset":{"assetId":"<id>"}}]}`, replacing `<id>` with the uploaded asset's `id` from `asset.json`.
32
-
33
- ```{{SHELL}}
34
- {{CLI}} tasks run images2PosedRGBD --request request.json --wait --json > result.json
35
- ```
36
-
37
- Use `--request -` to read JSON from stdin. Results go to stdout; diagnostics and the submission's idempotency key go to stderr. Preserve the key and reuse it with `--idempotency-key <key>` and the same request if submission is interrupted.
38
-
39
- A timeout stops local waiting, not server work. Resume with `{{CLI}} operations wait <operation-id> --json`. Check a completed operation's `error` before reading `response`. Download returned assets with `{{CLI}} assets download <asset-id> --out result.png`. Inspect the outputs before using them in another task; keep each image, camera, depth, and confidence together.
40
-
41
- ## Authentication
42
-
43
- Check `{{CLI}} auth status`. If login is needed, have the user run `{{CLI}} auth login` in their own terminal and enter the key privately. Never request keys in chat or put them in command arguments. Automation can supply `WLT_API_KEY`. The key selects the API project and billing; a successful status check verifies asset access, not every task permission.