@worldlabsai/cli 0.1.0 → 0.2.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://marble5.worldlabs.ai/docs/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
  };
@@ -429,6 +454,9 @@ var createClient = (options) => {
429
454
  };
430
455
  };
431
456
 
457
+ // src/auth.ts
458
+ import { text } from "node:stream/consumers";
459
+
432
460
  // src/config.ts
433
461
  var CliError = class extends Error {
434
462
  name = "CliError";
@@ -529,13 +557,20 @@ async function prompt(message, hidden = false) {
529
557
  terminal: true
530
558
  });
531
559
  const controller = new AbortController();
532
- reader.once("SIGINT", () => controller.abort());
560
+ let interrupted = false;
561
+ reader.once("SIGINT", () => {
562
+ interrupted = true;
563
+ controller.abort();
564
+ });
565
+ reader.once("close", () => controller.abort());
533
566
  process.stderr.write(message);
534
567
  try {
535
568
  const answer = await reader.question("", { signal: controller.signal });
536
569
  return answer.trim();
537
570
  } catch {
538
- throw new CliError("Cancelled.");
571
+ throw new CliError(
572
+ interrupted ? "Cancelled." : "Input closed before an answer. For automation, set WLT_API_KEY or pipe the key to worldlabs auth login --api-key-stdin."
573
+ );
539
574
  } finally {
540
575
  reader.close();
541
576
  if (hidden) process.stderr.write("\n");
@@ -573,12 +608,25 @@ async function authStatus() {
573
608
  ...process.env.WLT_API_KEY?.trim() ? {} : { credentialFile: credentialPath(configuration().apiOrigin) }
574
609
  };
575
610
  }
576
- async function login(noBrowser) {
611
+ async function pipedApiKey() {
612
+ if (process.stdin.isTTY) {
613
+ throw new CliError(
614
+ "--api-key-stdin reads a piped key, for example: worldlabs auth login --api-key-stdin < key.txt"
615
+ );
616
+ }
617
+ const input = await text(process.stdin);
618
+ const apiKey = input.trim();
619
+ if (!apiKey) throw new CliError("No API key on standard input.");
620
+ if (/\s/u.test(apiKey)) throw new CliError("Standard input must contain only the API key.");
621
+ return apiKey;
622
+ }
623
+ async function login(noBrowser, pipedKey) {
624
+ if (pipedKey) return await saveValidatedKey(pipedKey);
577
625
  if (process.env.WLT_API_KEY?.trim()) return await authStatus();
578
626
  const { apiOrigin, apiKeysUrl } = configuration();
579
627
  if (!process.stdin.isTTY || !process.stderr.isTTY) {
580
628
  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."
629
+ "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
630
  );
583
631
  }
584
632
  process.stderr.write(
@@ -591,6 +639,10 @@ The key will be saved in plaintext at ${credentialPath(apiOrigin)}.
591
639
  if (!noBrowser) await openBrowser(apiKeysUrl);
592
640
  const apiKey = await prompt("Paste API key (hidden): ", true);
593
641
  if (!apiKey) throw new CliError("No key entered.");
642
+ return await saveValidatedKey(apiKey);
643
+ }
644
+ async function saveValidatedKey(apiKey) {
645
+ const { apiOrigin } = configuration();
594
646
  await createClient({ apiKey, baseUrl: apiOrigin }).assets.list({
595
647
  pageSize: 1,
596
648
  signal: AbortSignal.timeout(3e4)
@@ -618,7 +670,7 @@ import { randomUUID as randomUUID2 } from "node:crypto";
618
670
  import { openAsBlob } from "node:fs";
619
671
  import { open as open2, readFile, rm as rm2 } from "node:fs/promises";
620
672
  import nodePath2 from "node:path";
621
- import { text } from "node:stream/consumers";
673
+ import { text as text2 } from "node:stream/consumers";
622
674
  import { pipeline } from "node:stream/promises";
623
675
  import { parseArgs } from "node:util";
624
676
  var mimeTypes = {
@@ -626,9 +678,11 @@ var mimeTypes = {
626
678
  ".jpeg": "image/jpeg",
627
679
  ".png": "image/png",
628
680
  ".webp": "image/webp",
681
+ ".exr": "image/x-exr",
629
682
  ".mp4": "video/mp4",
630
- ".ply": "application/octet-stream",
631
- ".spz": "application/octet-stream"
683
+ ".glb": "model/gltf-binary",
684
+ ".ply": "model/x-ply",
685
+ ".spz": "model/x-spz"
632
686
  };
633
687
  function timeout(value) {
634
688
  const seconds = value === void 0 ? 600 : Number(value);
@@ -665,7 +719,7 @@ async function runApiCommand(args2) {
665
719
  throw new CliError("Unknown task. Run worldlabs tasks list.");
666
720
  }
667
721
  const body = JSON.parse(
668
- values.request === "-" ? await text(process.stdin) : await readFile(values.request, "utf8")
722
+ values.request === "-" ? await text2(process.stdin) : await readFile(values.request, "utf8")
669
723
  );
670
724
  if (!body || typeof body !== "object" || Array.isArray(body)) {
671
725
  throw new CliError("Task request must be a JSON object.");
@@ -759,24 +813,30 @@ Reuse this key and request if submission is interrupted.
759
813
  throw new CliError("Unknown command or arguments. Run worldlabs --help.");
760
814
  }
761
815
 
816
+ // src/node-version.ts
817
+ var MINIMUM_NODE_MAJOR = 22;
818
+ function nodeVersionProblem(version2) {
819
+ if (Number(version2.split(".")[0]) >= MINIMUM_NODE_MAJOR) return void 0;
820
+ 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.`;
821
+ }
822
+
762
823
  // 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";
824
+ import { execFile, spawn as spawn2 } from "node:child_process";
825
+ import { createHash as createHash2 } from "node:crypto";
826
+ import { lstat as lstat2, mkdtemp, readFile as readFile2, readdir, rename as rename2, rmdir } from "node:fs/promises";
765
827
  import { homedir as homedir2 } from "node:os";
766
828
  import nodePath3 from "node:path";
767
- import { fileURLToPath } from "node:url";
768
829
  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"
830
+ var InstallerTerminationError = class extends CliError {
831
+ name = "InstallerTerminationError";
832
+ };
833
+ function skillClient(client) {
834
+ if (client !== void 0 && !/^[a-z0-9][a-z0-9-]*$/u.test(client)) {
835
+ throw new CliError(
836
+ "--client must be an agent name, such as codex, claude-code, cursor, or gemini-cli."
776
837
  );
777
838
  }
778
- if (client === "claude-code") return nodePath3.join(homedir2(), ".claude", "skills", "worldlabs");
779
- throw new CliError("Choose --client codex or --client claude-code.");
839
+ return client;
780
840
  }
781
841
  async function fingerprint(root) {
782
842
  const hash = createHash2("sha256");
@@ -797,93 +857,213 @@ async function fingerprint(root) {
797
857
  await visit("");
798
858
  return hash.digest("hex");
799
859
  }
800
- async function manageSkills(action, target) {
801
- const parent = nodePath3.dirname(target);
802
- const lock = `${target}.lock`;
803
- await mkdir2(parent, { recursive: true });
860
+ async function cleanupLegacySkill(client) {
861
+ const root = client === "codex" ? process.env.CODEX_HOME ?? nodePath3.join(homedir2(), ".codex") : nodePath3.join(homedir2(), ".claude");
862
+ const target = nodePath3.join(root, "skills", "worldlabs");
863
+ let stat;
804
864
  try {
805
- await mkdir2(lock);
806
- } catch {
865
+ stat = await lstat2(target);
866
+ } catch (error) {
867
+ if (error.code === "ENOENT") return;
868
+ throw error;
869
+ }
870
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return;
871
+ let expected;
872
+ try {
873
+ expected = JSON.parse(await readFile2(nodePath3.join(target, marker), "utf8"));
874
+ } catch (error) {
875
+ if (error.code === "ENOENT") return;
807
876
  throw new CliError(
808
- `Skill installation is locked at ${lock}. Remove the lock only if no installer is running.`
877
+ `Cannot verify the old skill at ${target}. Move it aside before continuing.`
809
878
  );
810
879
  }
811
- const staging = nodePath3.join(parent, `.worldlabs-${randomUUID3()}`);
812
- const backup = `${staging}-backup`;
880
+ if (await fingerprint(target) !== expected) {
881
+ throw new CliError(
882
+ `Old skill has local changes at ${target}. Move it aside before continuing.`
883
+ );
884
+ }
885
+ const backupContainer = await mkdtemp(
886
+ nodePath3.join(nodePath3.dirname(target), ".worldlabs-backup-")
887
+ );
888
+ const backup = nodePath3.join(backupContainer, "worldlabs");
813
889
  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;
890
+ await rename2(target, backup);
891
+ } catch (error) {
892
+ await rmdir(backupContainer).catch(() => null);
893
+ throw error;
894
+ }
895
+ process.stderr.write(`Moved the old CLI-managed skill to ${backup}
896
+ `);
897
+ return { target, backup };
898
+ }
899
+ function runSkillsInstaller(args2, signal) {
900
+ signal.throwIfAborted();
901
+ const env = { ...process.env, DISABLE_TELEMETRY: "1" };
902
+ delete env.WLT_API_KEY;
903
+ const windows = process.platform === "win32";
904
+ return new Promise((resolve, reject) => {
905
+ const child = spawn2(
906
+ windows ? "cmd.exe" : "npx",
907
+ windows ? ["/d", "/s", "/c", `npx.cmd ${args2.join(" ")}`] : args2,
908
+ { env, detached: !windows, stdio: ["inherit", "pipe", "pipe"] }
909
+ );
910
+ child.stdout.pipe(process.stderr, { end: false });
911
+ child.stderr.pipe(process.stderr, { end: false });
912
+ let forceTimer;
913
+ let stopTimer;
914
+ let settled = false;
915
+ const finish = (error) => {
916
+ if (settled) return;
917
+ settled = true;
918
+ clearTimeout(forceTimer);
919
+ clearTimeout(stopTimer);
920
+ child.stdout.unpipe(process.stderr);
921
+ child.stderr.unpipe(process.stderr);
922
+ signal.removeEventListener("abort", cancel);
923
+ if (error) reject(error);
924
+ else resolve();
925
+ };
926
+ const cannotStop = () => {
927
+ child.stdout.destroy();
928
+ child.stderr.destroy();
929
+ child.unref();
930
+ finish(
931
+ new InstallerTerminationError(
932
+ "Could not stop the skills installer. Legacy backups were retained; stop the installer before restoring them."
933
+ )
934
+ );
935
+ };
936
+ const killGroup = (terminationSignal) => {
937
+ if (!child.pid) return;
824
938
  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}`);
939
+ process.kill(-child.pid, terminationSignal);
940
+ } catch (error) {
941
+ if (error.code !== "ESRCH") cannotStop();
828
942
  }
829
- if (actual !== expected) {
830
- throw new CliError(`Skill has local changes. Move it aside before continuing: ${target}`);
943
+ };
944
+ const cancel = () => {
945
+ if (!child.pid) return;
946
+ stopTimer = setTimeout(cannotStop, 5e3).unref();
947
+ if (windows) {
948
+ execFile(
949
+ "taskkill",
950
+ ["/PID", String(child.pid), "/T", "/F"],
951
+ { timeout: 4e3 },
952
+ (error) => {
953
+ if (error && !settled) cannotStop();
954
+ }
955
+ );
956
+ } else {
957
+ forceTimer = setTimeout(() => killGroup("SIGKILL"), 2e3).unref();
958
+ killGroup(signal.reason);
831
959
  }
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)
960
+ };
961
+ signal.addEventListener("abort", cancel, { once: true });
962
+ child.once(
963
+ "error",
964
+ () => finish(
965
+ new CliError("Could not start the skills installer. Check that npm and npx are available.")
966
+ )
840
967
  );
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
- );
968
+ child.once(
969
+ "close",
970
+ (code) => finish(
971
+ code === 0 && !signal.aborted ? void 0 : new CliError("Skills installer did not complete. See its output above.")
972
+ )
973
+ );
974
+ });
975
+ }
976
+ async function manageSkills(action, client, yes = false) {
977
+ skillClient(client);
978
+ const moved = [];
979
+ const controller = new AbortController();
980
+ const cancel = (signal) => controller.abort(signal);
981
+ process.on("SIGINT", cancel);
982
+ process.on("SIGTERM", cancel);
983
+ try {
984
+ for (const candidate of client ? [client] : ["codex", "claude-code"]) {
985
+ if (candidate === "codex" || candidate === "claude-code") {
986
+ const record = await cleanupLegacySkill(candidate);
987
+ if (record) moved.push(record);
854
988
  }
989
+ if (controller.signal.aborted) throw new CliError("Skills installation cancelled.");
855
990
  }
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;
991
+ const args2 = [
992
+ "--yes",
993
+ "skills@1.7.2",
994
+ ...action === "install" ? ["add", "worldlabsai/skills", "--skill", "worldlabs"] : ["remove", "worldlabs"],
995
+ ...client ? ["--agent", client] : [],
996
+ "--global",
997
+ ...yes ? ["--yes"] : []
998
+ ];
999
+ await runSkillsInstaller(args2, controller.signal);
1000
+ } catch (error) {
1001
+ if (error instanceof InstallerTerminationError) throw error;
1002
+ for (const { target, backup } of moved.toReversed()) {
1003
+ try {
1004
+ await lstat2(target);
1005
+ process.stderr.write(
1006
+ `Could not restore the old skill because a skill now exists at ${target}. Backup retained at ${backup}
1007
+ `
1008
+ );
1009
+ } catch (restoreCheckError) {
1010
+ if (restoreCheckError.code !== "ENOENT") {
1011
+ process.stderr.write(
1012
+ `Could not check the old skill location ${target}. Backup retained at ${backup}
1013
+ `
1014
+ );
1015
+ continue;
1016
+ }
1017
+ try {
1018
+ await rename2(backup, target);
1019
+ await rmdir(nodePath3.dirname(backup)).catch(() => null);
1020
+ } catch {
1021
+ process.stderr.write(
1022
+ `Could not restore the old skill at ${target}. Backup retained at ${backup}
1023
+ `
1024
+ );
1025
+ }
1026
+ }
863
1027
  }
864
- await rm3(backup, { recursive: true, force: true });
865
- return { path: target, next: "Start a new agent session to load the worldlabs skill." };
1028
+ throw error;
866
1029
  } finally {
867
- await rm3(staging, { recursive: true, force: true });
868
- await rm3(lock, { recursive: true, force: true });
1030
+ process.off("SIGINT", cancel);
1031
+ process.off("SIGTERM", cancel);
1032
+ if (controller.signal.aborted) {
1033
+ process.kill(process.pid, controller.signal.reason);
1034
+ }
869
1035
  }
1036
+ return {
1037
+ client,
1038
+ ...moved.length ? { legacyBackups: moved.map(({ backup }) => backup) } : {},
1039
+ next: "Start a new agent session after changing skills."
1040
+ };
870
1041
  }
871
1042
 
872
1043
  // 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.");
1044
+ async function setup(client, noBrowser, yes = false, keyFromStdin = false) {
1045
+ const target = skillClient(client);
1046
+ const pipedKey = keyFromStdin ? await pipedApiKey() : void 0;
1047
+ let installSkill = target !== void 0;
1048
+ if (!installSkill && process.stdin.isTTY && process.stderr.isTTY) {
1049
+ const answer = await prompt("Install the World Labs agent skill? [y/N]: ");
1050
+ const choice = answer.toLowerCase();
1051
+ if (choice && !["y", "yes", "n", "no"].includes(choice)) {
1052
+ throw new CliError("Choose yes or no.");
1053
+ }
1054
+ installSkill = choice === "y" || choice === "yes";
1055
+ }
1056
+ const auth = await login(noBrowser, pipedKey);
1057
+ let skill;
1058
+ if (installSkill) {
1059
+ try {
1060
+ skill = await manageSkills("install", target, yes);
1061
+ } catch (error) {
1062
+ throw new CliError(
1063
+ `API authentication succeeded, but skill installation failed. ${error instanceof Error ? error.message : "Retry worldlabs skills install."}`
1064
+ );
883
1065
  }
884
1066
  }
885
- const auth = await login(noBrowser);
886
- const skill = target ? await manageSkills("install", target) : void 0;
887
1067
  return {
888
1068
  ...auth,
889
1069
  ...skill ? { skill } : {},
@@ -898,9 +1078,9 @@ var { version } = JSON.parse(
898
1078
  var help = `worldlabs - World Labs Developer API CLI
899
1079
 
900
1080
  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]
1081
+ setup [--client <agent>] [--no-browser] [--api-key-stdin] [--yes]
1082
+ skills install|uninstall [--client <agent>] [--yes]
1083
+ auth login [--no-browser] [--api-key-stdin]
904
1084
  auth status
905
1085
  auth logout
906
1086
  config show
@@ -913,30 +1093,53 @@ Usage: worldlabs [--json] <command>
913
1093
  --version
914
1094
  --help
915
1095
 
1096
+ Skills use Skills CLI client detection when --client is omitted (e.g. codex, claude-code, cursor, gemini-cli).
916
1097
  Use --request - to read task JSON from standard input.
917
1098
 
918
- Saved login uses a plaintext file in your user configuration directory. Automation can set WLT_API_KEY.
1099
+ Saved login uses a plaintext file in your user configuration directory. Automation can set WLT_API_KEY,
1100
+ or save a key without the prompt by piping it to --api-key-stdin.
919
1101
  `;
920
1102
  async function main(args2) {
1103
+ const unsupported = nodeVersionProblem(process.versions.node);
1104
+ if (unsupported) throw new CliError(unsupported);
921
1105
  if (args2.length === 0 || args2.includes("--help")) return help;
922
1106
  if (args2.length === 1 && args2[0] === "--version") return version;
923
1107
  const [command, action, ...rest] = args2;
924
1108
  if (command === "setup") {
925
1109
  const { values } = parseArgs2({
926
1110
  args: args2.slice(1),
927
- options: { client: { type: "string" }, "no-browser": { type: "boolean" } }
1111
+ options: {
1112
+ client: { type: "string" },
1113
+ "no-browser": { type: "boolean" },
1114
+ "api-key-stdin": { type: "boolean" },
1115
+ yes: { type: "boolean" }
1116
+ }
928
1117
  });
929
- return await setup(values.client, Boolean(values["no-browser"]));
1118
+ return await setup(
1119
+ values.client,
1120
+ Boolean(values["no-browser"]),
1121
+ Boolean(values.yes),
1122
+ Boolean(values["api-key-stdin"])
1123
+ );
930
1124
  }
931
1125
  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));
1126
+ const { values } = parseArgs2({
1127
+ args: rest,
1128
+ options: { client: { type: "string" }, yes: { type: "boolean" } }
1129
+ });
1130
+ return await manageSkills(action, skillClient(values.client), Boolean(values.yes));
934
1131
  }
935
1132
  if (command === "config" && action === "show" && !rest.length) return configuration();
936
1133
  if (command === "auth") {
937
1134
  if (action === "login") {
938
- const { values } = parseArgs2({ args: rest, options: { "no-browser": { type: "boolean" } } });
939
- return await login(Boolean(values["no-browser"]));
1135
+ const { values } = parseArgs2({
1136
+ args: rest,
1137
+ options: { "no-browser": { type: "boolean" }, "api-key-stdin": { type: "boolean" } }
1138
+ });
1139
+ return await login(
1140
+ Boolean(values["no-browser"]),
1141
+ values["api-key-stdin"] ? await pipedApiKey() : void 0
1142
+ );
940
1143
  }
941
1144
  if (action === "status" && !rest.length) return await authStatus();
942
1145
  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.2.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.