@labelbox/recursion-cli 0.0.45 → 0.0.47

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
@@ -1,44 +1,63 @@
1
1
  # @labelbox/recursion-cli
2
2
 
3
- `rl` — the Recursion CLI. It mirrors the TypeScript SDK
3
+ `recursion` — the Recursion CLI. It mirrors the TypeScript SDK
4
4
  (`@labelbox/recursion-sdk`) exactly: where the SDK is `rl.synthesizers.create(...)`, the
5
- CLI is `rl synthesizers create …`. Dots become spaces; you get `--help` at every
6
- level.
5
+ CLI is `recursion synthesizers create …`. Dots become spaces; you get `--help` at every
6
+ level. The existing `rl` executable remains a temporary compatibility alias and
7
+ runs the same command tree; new usage should prefer `recursion`.
7
8
 
8
9
  ```sh
9
- rl --help # list nouns (synthesizers, synthesizer-runs)
10
- rl synthesizers --help # list verbs (create, get, list, …)
11
- rl synthesizers create --help # list flags
10
+ recursion --help # list nouns (synthesizers, synthesizer-runs)
11
+ recursion synthesizers --help # list verbs (create, get, list, …)
12
+ recursion synthesizers create --help # list flags
12
13
  ```
13
14
 
14
15
  ## How it works (fully live, zero per-operation code)
15
16
 
16
17
  The CLI ships **no** baked API reference and has **no** `@labelbox/recursion-sdk` dependency.
17
- On each run it **fetches a manifest** from `GET /cli/manifest` on whatever server
18
- `--base-url` points at (production by default, or staging, or `localhost`), validates
19
- it (`src/manifest.ts`), and builds its entire command tree, `--help`, request/response
20
- shapes, and docs browse surfaces from it. Dispatch is generic (`src/dispatch.ts`):
18
+ On each run it **revalidates the manifest** from `GET /cli/manifest` when the server
19
+ selected by `--base-url` is reachable (production by default, or staging, or
20
+ `localhost`). If the server is unavailable, it falls back only to a locally cached
21
+ manifest that was previously validated. From that manifest it builds its entire
22
+ command tree, `--help`, request/response shapes, and docs browse surfaces
23
+ (`src/manifest.ts`). Dispatch is generic (`src/dispatch.ts`):
21
24
  each request is built straight from the manifest operation's HTTP method + path
22
25
  template + params + body — there is no hand-written command per operation and no
23
26
  baked client.
24
27
 
25
28
  The result: **adding or changing a backend endpoint needs zero CLI release** — the
26
29
  live CLI reflects it as soon as the backend deploys. The CLI is re-released only when
27
- its own engine code changes. The manifest is revalidated on every run via a
28
- conditional fetch (ETag / `If-None-Match`), cached per base-url under
29
- `~/.cache/rl-gym/`, so it can never serve stale data and never needs manual busting.
30
+ its own engine code changes. When the server is reachable, the manifest is
31
+ revalidated on every run via a conditional fetch (ETag / `If-None-Match`) and cached
32
+ per base-url under `~/.cache/recursion/`. When the server is unreachable, the CLI may
33
+ use the last locally validated copy so commands remain available offline. If only the
34
+ previous `~/.cache/rl-gym/` entry exists, the CLI validates and uses it immediately,
35
+ then adopts it into the Recursion cache non-destructively. The
36
+ old entry is retained unchanged for rollback; new server responses are written only
37
+ to the Recursion cache.
38
+
39
+ Legacy adoption uses atomic create-if-absent publication: a validated copy is
40
+ hard-linked from a unique same-directory temporary file, so it never replaces a
41
+ canonical entry created concurrently. A valid concurrent canonical entry wins; an
42
+ invalid one is left untouched while that invocation uses the validated legacy value
43
+ in memory. Validated fresh HTTP `200` responses use atomic same-directory rename and
44
+ therefore retain last-network-writer behavior. Replacement preserves an existing
45
+ file's POSIX mode bits, but deliberately publishes a new inode and does not preserve
46
+ its ACLs or extended attributes. Both paths require parent-directory write/search
47
+ permission; if unavailable, the CLI keeps using the validated in-memory result and
48
+ never falls back to a partial direct write.
30
49
 
31
50
  ### Docs browse surfaces
32
51
 
33
- Beyond the executable `rl <noun> <verb>` operations, the manifest carries the docs,
34
- exposed as one consistent positional shape — `rl <group> [<id>]` (bare lists, an id
52
+ Beyond the executable `recursion <noun> <verb>` operations, the manifest carries the docs,
53
+ exposed as one consistent positional shape — `recursion <group> [<id>]` (bare lists, an id
35
54
  shows that one):
36
55
 
37
56
  ```sh
38
- rl resources [<id>] # Reference — resource hubs: object shape, operations, recipes
39
- rl recipes [<id>] # How-to — multi-step user-goal guides (--format cli|ts|curl)
40
- rl explain [<concept>] # Explanation — concept pages
41
- rl tutorials [<id>] # Tutorials — getting-started docs
57
+ recursion resources [<id>] # Reference — resource hubs: object shape, operations, recipes
58
+ recursion recipes [<id>] # How-to — multi-step user-goal guides (--format cli|ts|curl)
59
+ recursion explain [<concept>] # Explanation — concept pages
60
+ recursion tutorials [<id>] # Tutorials — getting-started docs
42
61
  ```
43
62
 
44
63
  ## Install / run
@@ -48,9 +67,9 @@ setup beyond Node itself.
48
67
 
49
68
  ```sh
50
69
  npm uninstall -g @recursion/cli @labelbox/rl-cli # no-op unless a pre-rename package is installed
51
- npm install -g @labelbox/recursion-cli # they all own the global `rl` bin — EEXIST otherwise
70
+ npm install -g @labelbox/recursion-cli # old packages also own the `rl` alias — EEXIST otherwise
52
71
  # or, zero-install:
53
- npx @labelbox/recursion-cli --help
72
+ npx --package @labelbox/recursion-cli recursion --help
54
73
  ```
55
74
 
56
75
  The CLI is standalone, so this pulls in no `@labelbox/recursion-sdk`.
@@ -92,19 +111,21 @@ host with `--base-url <url>` (defaults to the public Recursion API gateway, see
92
111
  no-content operations).
93
112
 
94
113
  ```sh
95
- rl synthesizers get --synthesizer-job-id sj_01HX...
96
- rl synthesizers create --environment-id env_01HX... --from-json ./body.json
97
- rl synthesizer-runs trigger --problem-version-id pv_01HX... --from-json ./run.json
114
+ recursion synthesizers get --synthesizer-job-id sj_01HX...
115
+ recursion synthesizers create --environment-id env_01HX... --from-json ./body.json
116
+ recursion synthesizer-runs trigger --problem-version-id pv_01HX... --from-json ./run.json
98
117
  ```
99
118
 
100
119
  ## The command surface is the live server
101
120
 
102
121
  There is **nothing to regenerate or commit** for the CLI — the command surface is
103
- fetched fresh from the target server's `GET /cli/manifest` on every run, so a backend
104
- change flows through automatically after deploy with no CLI step. The manifest itself
105
- is assembled by `yarn generate cli:manifest` (included in `yarn generate prerequisites`) from the
106
- spec-derived reference files and embedded into the backend; to add or change a
107
- command, change the backend `@SdkRoute` not this package.
122
+ revalidated against the target server's `GET /cli/manifest` on every run when the
123
+ server is reachable, with a validated local-cache fallback when it is offline. A
124
+ backend change therefore flows through automatically after deploy with no CLI step.
125
+ The manifest itself is assembled by `yarn generate cli:manifest` (included in
126
+ `yarn generate prerequisites`) from the spec-derived reference files and embedded
127
+ into the backend; to add or change a command, change the backend `@SdkRoute` — not
128
+ this package.
108
129
 
109
130
  The *engine* (manifest fetch + cache + validation, generic dispatch, flag mapping,
110
131
  help formatting, the request-body / returns shape trees, and the docs browse
package/dist/embed.d.ts CHANGED
@@ -8,7 +8,7 @@ export interface ExecuteCliOptions {
8
8
  apiKey: string;
9
9
  /** The API the CLI dispatches against. Server configuration, not caller input. */
10
10
  baseUrl: string;
11
- /** Reported by `rl --version`. */
11
+ /** Reported by `recursion --version`. */
12
12
  version: string;
13
13
  /** The pre-assembled manifest — served from memory, never re-fetched over HTTP. */
14
14
  manifest: Manifest;
@@ -31,7 +31,7 @@ export interface CliResult {
31
31
  */
32
32
  export declare function assertAllowedArgv(args: readonly string[]): void;
33
33
  /**
34
- * Run one `rl` invocation in-process and capture its output.
34
+ * Run one `recursion` invocation in-process and capture its output.
35
35
  *
36
36
  * Resolves for both success and failure — a non-zero exit is a normal outcome the
37
37
  * model should see and correct from, not an exception. It rejects only on a
package/dist/embed.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { run } from './run.js';
2
- // Running `rl` inside a server process, on behalf of an agent, without a shell.
2
+ // Running `recursion` inside a server process, on behalf of an agent, without a shell.
3
3
  //
4
4
  // The MCP endpoint exposes one tool whose argument is an argv array. This module
5
5
  // is the whole surface it needs: `executeCli` builds the argv, runs the same
@@ -91,11 +91,11 @@ export function assertAllowedArgv(args) {
91
91
  const command = args.find((arg) => !arg.startsWith('-'));
92
92
  if (command !== undefined && BLOCKED_COMMANDS.has(command)) {
93
93
  throw new Error(`"${command}" is not available here: it acts on a local developer checkout, ` +
94
- 'which this server does not have. Run it from your own terminal with the rl CLI.');
94
+ 'which this server does not have. Run it from your own terminal with the recursion CLI.');
95
95
  }
96
96
  }
97
97
  /**
98
- * Run one `rl` invocation in-process and capture its output.
98
+ * Run one `recursion` invocation in-process and capture its output.
99
99
  *
100
100
  * Resolves for both success and failure — a non-zero exit is a normal outcome the
101
101
  * model should see and correct from, not an exception. It rejects only on a
@@ -125,7 +125,7 @@ export async function executeCli(options) {
125
125
  // argv can outrank it.
126
126
  argv: [
127
127
  'node',
128
- 'rl',
128
+ 'recursion',
129
129
  `--api-key=${options.apiKey}`,
130
130
  `--base-url=${options.baseUrl}`,
131
131
  ...options.args,
package/dist/git-host.js CHANGED
@@ -128,7 +128,7 @@ function stashClaim(repoDir, claim) {
128
128
  function readStashedClaim(repoDir) {
129
129
  const path = claimFilePath(repoDir);
130
130
  if (!existsSync(path)) {
131
- throw new Error(`no claim found at ${path} — run \`rl scaffold\` in this directory first, or pass --path to point at a scaffolded repo`);
131
+ throw new Error(`no claim found at ${path} — run \`recursion scaffold\` in this directory first, or pass --path to point at a scaffolded repo`);
132
132
  }
133
133
  const parsed = StashedClaimSchema.safeParse(JSON.parse(readFileSync(path, 'utf8')));
134
134
  if (!parsed.success) {
@@ -398,13 +398,22 @@ export type Manifest = z.infer<typeof ManifestSchema>;
398
398
  * and only a deliberate format bump trips this.
399
399
  */
400
400
  export declare function parseManifest(value: unknown, source: string): Manifest;
401
+ /**
402
+ * A filesystem-safe, collision-free slug for a base URL, so each server caches
403
+ * independently. The readable part is the sanitized URL (handy when eyeballing the
404
+ * cache dir); a short hash of the *full* URL is appended so two URLs that sanitize
405
+ * to the same string (e.g. `https://x.com:8080` vs `https://x-com-8080`) still get
406
+ * distinct cache files rather than silently sharing — and poisoning — one.
407
+ */
408
+ export declare function hostSlug(baseUrl: string): string;
401
409
  /**
402
410
  * Fetch the command manifest, revalidating the cache on every run:
403
411
  * - send `If-None-Match` with the cached ETag → `304` means the cache is provably
404
412
  * current (use it); `200` means the surface changed (validate + replace cache).
405
413
  * - a network error (incl. a timeout) falls back to the cached copy with a warning;
406
414
  * with no cache it errors clearly.
407
- * The cache therefore can never serve stale data and never needs manual busting — a
408
- * backend redeploy is picked up automatically on the next command.
415
+ * When the server is reachable, the cache is revalidated and a backend redeploy is
416
+ * picked up automatically. Offline, the last locally validated copy may be older
417
+ * than the server but keeps the CLI usable.
409
418
  */
410
419
  export declare function fetchManifest(baseUrl: string, apiKey: string): Promise<Manifest>;
package/dist/manifest.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createHash } from 'node:crypto';
2
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { closeSync, fchmodSync, linkSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeSync, } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import process from 'node:process';
@@ -9,8 +9,8 @@ import { z } from 'zod';
9
9
  // from `GET /cli/manifest` on whatever server `--base-url` points at, instead of
10
10
  // baking a compiled-in `@labelbox/recursion-sdk` reference. This module owns:
11
11
  // 1. the manifest's runtime Zod schema + TS types (the validation boundary), and
12
- // 2. `fetchManifest` — a conditional-fetch cache (ETag / If-None-Match) that can
13
- // never serve stale data and needs no manual busting.
12
+ // 2. `fetchManifest` — a conditional-fetch cache (ETag / If-None-Match) that is
13
+ // revalidated whenever the server is reachable and remains usable offline.
14
14
  // The schema lives here, not in sdk-ts: the CLI no longer depends on that package
15
15
  // (see the plan's standalone-CLI decision). The backend embeds the same manifest at
16
16
  // build time (`dx manifest:generate`); this is the consumer mirror.
@@ -159,7 +159,7 @@ export const ManifestOperationSchema = z.object({
159
159
  params: z.array(ManifestParamSchema),
160
160
  response: ShapeNodeSchema.optional(),
161
161
  });
162
- // ── Resources — the Reference browse surface (`rl resources [<id>]`) ──────────
162
+ // ── Resources — the Reference browse surface (`recursion resources [<id>]`) ──────────
163
163
  const ManifestResourceObjectSchema = z.object({
164
164
  name: z.string(),
165
165
  fields: z.array(ShapeNodeSchema),
@@ -175,14 +175,14 @@ export const ManifestResourceSchema = z.object({
175
175
  object: ManifestResourceObjectSchema.optional(),
176
176
  operationIds: z.array(z.string()),
177
177
  });
178
- // ── Recipes — the How-to browse surface (`rl recipes [<id>]`) ─────────────────
178
+ // ── Recipes — the How-to browse surface (`recursion recipes [<id>]`) ─────────────────
179
179
  const RecipeSnippetSchema = z.object({ setup: z.string(), main: z.string() });
180
180
  // Steps are read only to map a recipe to the resources it touches (via each SDK
181
181
  // step's operationId), so they're modeled loosely — just the fields the CLI uses.
182
182
  const ManifestRecipeStepSchema = z.object({ operationId: z.string().optional() });
183
183
  // A recipe's place in the relationship graph (mirrors `RecipeRelated` in
184
184
  // sdk-ts's recipes-schema). Modeled loosely here — the CLI only reads these to
185
- // render the "Related" / "Unblocks" block on `rl recipes <id>`; the generator
185
+ // render the "Related" / "Unblocks" block on `recursion recipes <id>`; the generator
186
186
  // is the source of truth that validates them. Without these fields the manifest
187
187
  // parse would silently strip `related`, so the CLI would never see the links.
188
188
  const ManifestLinkTargetSchema = z.object({
@@ -224,7 +224,7 @@ export const ManifestRecipeSchema = z.object({
224
224
  // The recipe's relationship links (optional — absent for an island recipe).
225
225
  related: ManifestRelatedSchema.optional(),
226
226
  });
227
- // ── Concepts — the Explanation browse surface (`rl explain [<concept>]`) ──────
227
+ // ── Concepts — the Explanation browse surface (`recursion explain [<concept>]`) ──────
228
228
  export const ManifestConceptSchema = z.object({
229
229
  id: z.string(),
230
230
  title: z.string(),
@@ -234,7 +234,7 @@ export const ManifestConceptSchema = z.object({
234
234
  // metadata — the frontend fetches the `.md` separately; the CLI bundles it).
235
235
  body: z.string(),
236
236
  });
237
- // ── Tutorials — the getting-started docs (`rl tutorials [<id>]`) ──────────────
237
+ // ── Tutorials — the getting-started docs (`recursion tutorials [<id>]`) ──────────────
238
238
  export const ManifestTutorialSchema = z.object({
239
239
  id: z.string(),
240
240
  title: z.string(),
@@ -279,7 +279,7 @@ export function parseManifest(value, source) {
279
279
  // means no pre-rename package (@labelbox/rl-cli, or the older
280
280
  // @recursion/cli) owns the `rl` bin. A pre-rename user sees the
281
281
  // pre-rename binary's own message instead.
282
- 'this `rl` CLI is out of date — upgrade it (`npm install -g @labelbox/recursion-cli`)'
282
+ 'this `recursion` CLI is out of date — upgrade it (`npm install -g @labelbox/recursion-cli`)'
283
283
  : 'the server is older than this CLI — point --base-url at an up-to-date server';
284
284
  throw new Error(`command-manifest format mismatch: this CLI speaks v${MANIFEST_FORMAT_VERSION}, ` +
285
285
  `${source} served v${formatVersion}. ${direction}.`);
@@ -300,12 +300,15 @@ const CacheSchema = z.object({ etag: z.string().optional(), manifest: z.unknown(
300
300
  * to the same string (e.g. `https://x.com:8080` vs `https://x-com-8080`) still get
301
301
  * distinct cache files rather than silently sharing — and poisoning — one.
302
302
  */
303
- function hostSlug(baseUrl) {
303
+ export function hostSlug(baseUrl) {
304
304
  const readable = baseUrl.replace(/[^a-zA-Z0-9]+/gu, '-').replace(/^-+|-+$/gu, '') || 'default';
305
305
  const hash = createHash('sha256').update(baseUrl, 'utf8').digest('hex').slice(0, 8);
306
306
  return `${readable}-${hash}`;
307
307
  }
308
- function cachePathFor(baseUrl) {
308
+ function canonicalCachePathFor(baseUrl) {
309
+ return join(homedir(), '.cache', 'recursion', `${hostSlug(baseUrl)}.json`);
310
+ }
311
+ function legacyCachePathFor(baseUrl) {
309
312
  return join(homedir(), '.cache', 'rl-gym', `${hostSlug(baseUrl)}.json`);
310
313
  }
311
314
  function readCache(path) {
@@ -333,23 +336,131 @@ function readCache(path) {
333
336
  // arrives). Treating it as cold lets the next fetch pull — and re-cache — a full
334
337
  // body, so the CLI self-heals without a manual cache delete.
335
338
  try {
336
- return { etag: parsed.data.etag, manifest: parseManifest(parsed.data.manifest, 'the cache') };
339
+ return {
340
+ etag: parsed.data.etag,
341
+ manifest: parseManifest(parsed.data.manifest, 'the cache'),
342
+ raw,
343
+ };
337
344
  }
338
345
  catch {
339
346
  return undefined;
340
347
  }
341
348
  }
342
- function writeCache(path, entry) {
349
+ const CACHE_TEMP_PREFIX = '.rlc-';
350
+ function uniqueCacheTempPath(directory) {
351
+ return join(directory, `${CACHE_TEMP_PREFIX}${process.pid.toString()}-${randomUUID()}.tmp`);
352
+ }
353
+ /**
354
+ * Create, populate, close, and publish one exclusively owned same-directory temp.
355
+ *
356
+ * Ownership begins immediately after `openSync` succeeds, so every later failure —
357
+ * including a partial write or close failure — triggers best-effort pathname cleanup.
358
+ * Descriptor ownership is cleared before the single close attempt: retrying a numeric
359
+ * descriptor could close an unrelated resource if the operating system reused it.
360
+ * Publication happens only after close succeeds. The caller reports whether it
361
+ * transferred the inode (rename) or retained it (hard-link adoption), which determines
362
+ * final cleanup ownership.
363
+ */
364
+ function publishFromOwnedTemp(path, raw, prepare, publish) {
365
+ let tempPath;
366
+ let descriptor;
367
+ let ownsTemp = false;
343
368
  try {
344
- mkdirSync(dirname(path), { recursive: true });
345
- writeFileSync(path, JSON.stringify(entry));
369
+ const directory = dirname(path);
370
+ mkdirSync(directory, { recursive: true });
371
+ tempPath = uniqueCacheTempPath(directory);
372
+ descriptor = openSync(tempPath, 'wx');
373
+ ownsTemp = true;
374
+ const bytes = Buffer.from(raw, 'utf8');
375
+ let offset = 0;
376
+ while (offset < bytes.byteLength) {
377
+ const written = writeSync(descriptor, bytes, offset, bytes.byteLength - offset);
378
+ if (written === 0)
379
+ throw new Error('writing the cache temp made no progress');
380
+ offset += written;
381
+ }
382
+ prepare(descriptor);
383
+ const descriptorToClose = descriptor;
384
+ descriptor = undefined;
385
+ closeSync(descriptorToClose);
386
+ if (publish(tempPath) === 'transferred')
387
+ ownsTemp = false;
346
388
  }
347
389
  catch {
348
- // A non-writable cache dir is a bandwidth optimization lost, not a failure
349
- // the command still ran off the fetched manifest. Stay silent.
390
+ // Cache persistence is a bandwidth optimization. The caller already has a
391
+ // validated in-memory manifest, so publication failures are non-fatal.
350
392
  }
393
+ finally {
394
+ if (descriptor !== undefined) {
395
+ const descriptorToClose = descriptor;
396
+ descriptor = undefined;
397
+ try {
398
+ closeSync(descriptorToClose);
399
+ }
400
+ catch {
401
+ // Never retry a numeric descriptor: it may already have been reused.
402
+ }
403
+ }
404
+ if (ownsTemp && tempPath !== undefined) {
405
+ try {
406
+ unlinkSync(tempPath);
407
+ }
408
+ catch {
409
+ // Pathname cleanup is best-effort; readers inspect only the final path.
410
+ }
411
+ }
412
+ }
413
+ }
414
+ /**
415
+ * Replace a cache with a validated HTTP 200 response via same-directory rename.
416
+ *
417
+ * Rename deliberately publishes a new inode: an existing destination's POSIX mode
418
+ * bits are copied to that inode, but its ACLs and xattrs are not. The operation
419
+ * requires write/search permission on the parent directory; it never falls back to
420
+ * a partial direct write when temporary-file creation or rename is unavailable.
421
+ */
422
+ function replaceCacheAtomically(path, raw) {
423
+ publishFromOwnedTemp(path, raw, (descriptor) => {
424
+ // Read the mode immediately before publication. Concurrent HTTP 200 writers all
425
+ // preserve the established mode while rename keeps last-network-writer-wins.
426
+ const destination = statSync(path, { throwIfNoEntry: false });
427
+ if (destination !== undefined)
428
+ fchmodSync(descriptor, destination.mode & 0o7777);
429
+ }, (tempPath) => {
430
+ renameSync(tempPath, path);
431
+ return 'transferred';
432
+ });
433
+ }
434
+ /**
435
+ * Publish validated legacy bytes only if the canonical destination is absent.
436
+ *
437
+ * The hard link is the no-replace commit point: unlike rename, it fails if any
438
+ * concurrent invocation has created the destination. Best-effort temporary-path
439
+ * cleanup is attempted afterward, while a winning canonical path is never overwritten.
440
+ */
441
+ function publishCacheIfAbsent(path, raw) {
442
+ publishFromOwnedTemp(path, raw, () => { }, (tempPath) => {
443
+ linkSync(tempPath, path);
444
+ return 'retained';
445
+ });
446
+ }
447
+ /** Prefer canonical cache; otherwise adopt a validated legacy file without mutating it. */
448
+ function selectCache(baseUrl) {
449
+ const canonicalPath = canonicalCachePathFor(baseUrl);
450
+ const canonical = readCache(canonicalPath);
451
+ if (canonical !== undefined)
452
+ return canonical;
453
+ const legacy = readCache(legacyCachePathFor(baseUrl));
454
+ if (legacy === undefined)
455
+ return undefined;
456
+ // Copy the validated bytes exactly. Hard-link publication cannot replace a
457
+ // canonical file that appears after the reads above. Re-read afterward: prefer a
458
+ // valid concurrent winner, but leave an invalid winner untouched and keep using
459
+ // the already-validated legacy value in memory.
460
+ publishCacheIfAbsent(canonicalPath, legacy.raw);
461
+ return readCache(canonicalPath) ?? legacy;
351
462
  }
352
- // Every `rl` invocation gates on this fetch, so it must be bounded: a server that
463
+ // Every `recursion` invocation gates on this fetch, so it must be bounded: a server that
353
464
  // accepts the connection but never responds (a hung gateway, a stalled captive
354
465
  // portal) would otherwise hang the CLI forever. A timeout makes `fetch` reject, so
355
466
  // the catch below degrades to the cached manifest (or the clear cold-cache error).
@@ -360,13 +471,14 @@ const MANIFEST_FETCH_TIMEOUT_MS = 30_000;
360
471
  * current (use it); `200` means the surface changed (validate + replace cache).
361
472
  * - a network error (incl. a timeout) falls back to the cached copy with a warning;
362
473
  * with no cache it errors clearly.
363
- * The cache therefore can never serve stale data and never needs manual busting — a
364
- * backend redeploy is picked up automatically on the next command.
474
+ * When the server is reachable, the cache is revalidated and a backend redeploy is
475
+ * picked up automatically. Offline, the last locally validated copy may be older
476
+ * than the server but keeps the CLI usable.
365
477
  */
366
478
  export async function fetchManifest(baseUrl, apiKey) {
367
479
  const url = supportUrl(baseUrl, '/cli/manifest');
368
- const cachePath = cachePathFor(baseUrl);
369
- const cached = readCache(cachePath);
480
+ const canonicalCachePath = canonicalCachePathFor(baseUrl);
481
+ const cached = selectCache(baseUrl);
370
482
  let res;
371
483
  try {
372
484
  res = await fetch(url, {
@@ -410,6 +522,6 @@ export async function fetchManifest(baseUrl, apiKey) {
410
522
  'points at a recursion API (not a login page or proxy).');
411
523
  }
412
524
  const manifest = parseManifest(json, url);
413
- writeCache(cachePath, { etag: res.headers.get('etag') ?? undefined, manifest: json });
525
+ replaceCacheAtomically(canonicalCachePath, JSON.stringify({ etag: res.headers.get('etag') ?? undefined, manifest: json }));
414
526
  return manifest;
415
527
  }
package/dist/program.d.ts CHANGED
@@ -20,7 +20,7 @@ export declare function parseBodyBase(opts: Record<string, unknown>): Record<str
20
20
  * Where CLI output goes, and the reason nothing here touches `process` directly.
21
21
  *
22
22
  * The terminal entrypoint (`bin.ts`) binds these to the real streams. The MCP
23
- * endpoint binds them to string buffers so one `rl` invocation can be executed
23
+ * endpoint binds them to string buffers so one `recursion` invocation can be executed
24
24
  * **in-process** on behalf of an agent and its output returned as a tool result.
25
25
  * A stray `process.stdout.write` would leak an agent's output into the server's
26
26
  * logs; a stray `process.exit` would take the whole API server down mid-request.
@@ -86,7 +86,7 @@ export declare function leafShapeHelp(entry: ManifestOperation): string;
86
86
  * and `--data` is the single documented way to pass a body.
87
87
  */
88
88
  export declare function addOptions(command: Command, entry: ManifestOperation, fileFlags?: boolean): void;
89
- /** Output formats `rl recipes <id>` can render, mapped to the composed snippet field. */
89
+ /** Output formats `recursion recipes <id>` can render, mapped to the composed snippet field. */
90
90
  declare const RECIPE_FORMATS: {
91
91
  readonly cli: "cli";
92
92
  readonly ts: "sdk";
@@ -94,7 +94,7 @@ declare const RECIPE_FORMATS: {
94
94
  readonly curl: "curl";
95
95
  };
96
96
  export type RecipeFormat = keyof typeof RECIPE_FORMATS;
97
- /** The full catalog, grouped by category, for `rl recipes`. */
97
+ /** The full catalog, grouped by category, for `recursion recipes`. */
98
98
  export declare function renderRecipeList(reference: Record<string, ManifestRecipe>): string;
99
99
  /**
100
100
  * The "Related" + "Unblocks" block for a recipe — its place in the graph. The
@@ -104,10 +104,10 @@ export declare function renderRecipeList(reference: Record<string, ManifestRecip
104
104
  * and where it can go next. Returns `''` when the recipe is an island.
105
105
  */
106
106
  export declare function renderRelatedBlock(entry: ManifestRecipe, allRecipes: Record<string, ManifestRecipe>): string;
107
- /** One recipe rendered for `rl recipes <id>`: goal + composed code + related links + a docs link. */
107
+ /** One recipe rendered for `recursion recipes <id>`: goal + composed code + related links + a docs link. */
108
108
  export declare function renderRecipeShow(entry: ManifestRecipe, format: RecipeFormat, allRecipes: Record<string, ManifestRecipe>): string;
109
109
  /** The program shell — name, description, version, and the global options. */
110
110
  export declare function buildBaseProgram(version: string, io: CliIo): Command;
111
- /** Build the full `rl` program from a fetched manifest. */
111
+ /** Build the full `recursion` program from a fetched manifest. */
112
112
  export declare function buildProgram(manifest: Manifest, ctx: ProgramContext): Command;
113
113
  export {};
package/dist/program.js CHANGED
@@ -336,7 +336,7 @@ export function addOptions(command, entry, fileFlags = true) {
336
336
  // resource type as their request body, so `agent_id` is both the path
337
337
  // parameter and a property of the body schema. Commander throws when the same
338
338
  // flag is added twice, and that throw happens while the whole program is
339
- // being built, so one such operation takes down `rl` entirely rather than
339
+ // being built, so one such operation takes down `recursion` entirely rather than
340
340
  // just itself. Path and query win: they address the resource and are what the
341
341
  // URL is built from. The shadowed body field is still settable through
342
342
  // `--from-json` / `--data`.
@@ -386,15 +386,15 @@ export function addOptions(command, entry, fileFlags = true) {
386
386
  command.option('--data <json>', 'Inline JSON request body');
387
387
  }
388
388
  }
389
- // ── docs browse surfaces — one consistent shape: `rl <group> [<id>]` ─────────
389
+ // ── docs browse surfaces — one consistent shape: `recursion <group> [<id>]` ─────────
390
390
  //
391
391
  // All four read-only browse groups (resources / recipes / explain / tutorials)
392
392
  // take one positional shape: bare lists, an id shows that one. No `show`/`list`
393
- // verbs (those are the action layer's, `rl <noun> <verb>`). All data is the live
393
+ // verbs (those are the action layer's, `recursion <noun> <verb>`). All data is the live
394
394
  // manifest; these commands are pure presentation (no further network).
395
395
  // The Labelbox app host (not the recursion API base) — recipes/docs render in-app.
396
396
  const RECIPE_DOCS_BASE_URL = 'https://app.labelbox.com';
397
- /** Output formats `rl recipes <id>` can render, mapped to the composed snippet field. */
397
+ /** Output formats `recursion recipes <id>` can render, mapped to the composed snippet field. */
398
398
  const RECIPE_FORMATS = { cli: 'cli', ts: 'sdk', py: 'python', curl: 'curl' };
399
399
  function isRecipeFormat(value) {
400
400
  return Object.hasOwn(RECIPE_FORMATS, value);
@@ -421,7 +421,7 @@ function groupByDomain(entries, domainOf, domains) {
421
421
  const ACTIONS_HELP_GROUP = 'Actions:';
422
422
  const DOCS_HELP_GROUP = 'Documentation:';
423
423
  const GIT_HOST_HELP_GROUP = 'Coding tasks:';
424
- /** Nested resource groups (`rl problems --help`, etc.) — allowed subcommands. */
424
+ /** Nested resource groups (`recursion problems --help`, etc.) — allowed subcommands. */
425
425
  const COMMANDS_HELP_GROUP = 'Commands';
426
426
  /** Nested resource groups — subcommands the caller can't run (separate help section). */
427
427
  const UNAVAILABLE_HELP_GROUP = 'Unavailable (missing permission)';
@@ -463,7 +463,7 @@ function resolveResourceDomain(resource, byId) {
463
463
  }
464
464
  return undefined;
465
465
  }
466
- /** The `rl <callPath>` invocation for an operation, with its required-flag hints. */
466
+ /** The `recursion <callPath>` invocation for an operation, with its required-flag hints. */
467
467
  function operationInvocation(op) {
468
468
  const flags = [];
469
469
  for (const param of op.params) {
@@ -475,12 +475,12 @@ function operationInvocation(op) {
475
475
  if (op.bodyKey)
476
476
  flags.push('--data <json>');
477
477
  // kebab each segment so the printed command matches the real one (the tree is
478
- // built from `kebab(segment)`) — `rl environments attach-external-id`, not the
478
+ // built from `kebab(segment)`) — `recursion environments attach-external-id`, not the
479
479
  // camelCase callPath.
480
480
  const command = op.callPath.map(kebab).join(' ');
481
- return `rl ${command}${flags.length ? ` ${flags.join(' ')}` : ''}`;
481
+ return `recursion ${command}${flags.length ? ` ${flags.join(' ')}` : ''}`;
482
482
  }
483
- /** `rl resources [<id>]` — the Reference hub (grouped browse, or one resource). */
483
+ /** `recursion resources [<id>]` — the Reference hub (grouped browse, or one resource). */
484
484
  function addResourcesCommand(program, manifest, io) {
485
485
  const byId = manifest.resources;
486
486
  program
@@ -493,11 +493,11 @@ function addResourcesCommand(program, manifest, io) {
493
493
  return;
494
494
  }
495
495
  // `Object.hasOwn`, not `byId[id]`: a Zod `z.record` is a plain object, so a bare
496
- // read walks the prototype chain — `rl resources constructor` would resolve to a
496
+ // read walks the prototype chain — `recursion resources constructor` would resolve to a
497
497
  // truthy inherited member, bypass the not-found guard, and crash in the renderer.
498
498
  const entry = Object.hasOwn(byId, id) ? byId[id] : undefined;
499
499
  if (!entry) {
500
- throw new Error(`unknown resource "${id}". Run \`rl resources\` to list resources.`);
500
+ throw new Error(`unknown resource "${id}". Run \`recursion resources\` to list resources.`);
501
501
  }
502
502
  io.stdout(renderResourceShow(entry, manifest));
503
503
  });
@@ -506,7 +506,7 @@ function renderResourceList(manifest) {
506
506
  const byId = manifest.resources;
507
507
  const all = Object.values(byId);
508
508
  const groups = groupByDomain(all, (r) => resolveResourceDomain(r, byId), manifest.domains);
509
- const lines = ['Resources — run `rl resources <id>` for the full overview.', ''];
509
+ const lines = ['Resources — run `recursion resources <id>` for the full overview.', ''];
510
510
  for (const group of groups) {
511
511
  lines.push(group.title);
512
512
  const sorted = group.entries.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id, 'en'));
@@ -549,7 +549,7 @@ function renderResourceShow(resource, manifest) {
549
549
  }
550
550
  return `${parts.join('\n')}\n`;
551
551
  }
552
- /** `rl recipes [<id>]` — How-to (list grouped by category, or one recipe). */
552
+ /** `recursion recipes [<id>]` — How-to (list grouped by category, or one recipe). */
553
553
  function addRecipesCommand(program, manifest, io) {
554
554
  program
555
555
  .command('recipes [id]')
@@ -564,7 +564,7 @@ function addRecipesCommand(program, manifest, io) {
564
564
  // Object.hasOwn guards the prototype chain (see the resources hub above).
565
565
  const entry = Object.hasOwn(manifest.recipes, id) ? manifest.recipes[id] : undefined;
566
566
  if (!entry) {
567
- throw new Error(`unknown recipe "${id}". Run \`rl recipes\` to see all recipes.`);
567
+ throw new Error(`unknown recipe "${id}". Run \`recursion recipes\` to see all recipes.`);
568
568
  }
569
569
  const format = opts.format ?? 'cli';
570
570
  // `Object.hasOwn`, not `format in RECIPE_FORMATS`: `in` walks the prototype
@@ -577,7 +577,7 @@ function addRecipesCommand(program, manifest, io) {
577
577
  io.stdout(renderRecipeShow(entry, format, manifest.recipes));
578
578
  });
579
579
  }
580
- /** The full catalog, grouped by category, for `rl recipes`. */
580
+ /** The full catalog, grouped by category, for `recursion recipes`. */
581
581
  export function renderRecipeList(reference) {
582
582
  const entries = Object.values(reference);
583
583
  if (entries.length === 0)
@@ -588,7 +588,7 @@ export function renderRecipeList(reference) {
588
588
  group.push(entry);
589
589
  byCategory.set(entry.category, group);
590
590
  }
591
- const lines = ['Recipes — run `rl recipes <id>` for the full walkthrough.', ''];
591
+ const lines = ['Recipes — run `recursion recipes <id>` for the full walkthrough.', ''];
592
592
  for (const category of [...byCategory.keys()].sort()) {
593
593
  lines.push(category);
594
594
  const group = byCategory.get(category) ?? [];
@@ -649,7 +649,7 @@ export function renderRelatedBlock(entry, allRecipes) {
649
649
  }
650
650
  return lines.length > 0 ? `Related\n${lines.join('\n')}\n` : '';
651
651
  }
652
- /** One recipe rendered for `rl recipes <id>`: goal + composed code + related links + a docs link. */
652
+ /** One recipe rendered for `recursion recipes <id>`: goal + composed code + related links + a docs link. */
653
653
  export function renderRecipeShow(entry, format, allRecipes) {
654
654
  // Optional on the wire: an older server predates the Python surface, so
655
655
  // `--format py` renders the note rather than crashing on a missing snippet.
@@ -672,7 +672,7 @@ export function renderRecipeShow(entry, format, allRecipes) {
672
672
  '',
673
673
  ].join('\n');
674
674
  }
675
- /** `rl explain [<concept>]` — Explanation (list grouped by domain, or one page). */
675
+ /** `recursion explain [<concept>]` — Explanation (list grouped by domain, or one page). */
676
676
  function addExplainCommand(program, manifest, io) {
677
677
  program
678
678
  .command('explain [concept]')
@@ -688,14 +688,17 @@ function addExplainCommand(program, manifest, io) {
688
688
  ? manifest.concepts[concept]
689
689
  : undefined;
690
690
  if (!entry) {
691
- throw new Error(`unknown concept "${concept}". Run \`rl explain\` to list concepts.`);
691
+ throw new Error(`unknown concept "${concept}". Run \`recursion explain\` to list concepts.`);
692
692
  }
693
693
  io.stdout(renderConceptShow(entry));
694
694
  });
695
695
  }
696
696
  function renderConceptList(manifest) {
697
697
  const groups = groupByDomain(Object.values(manifest.concepts), (c) => c.domain, manifest.domains);
698
- const lines = ['Explanations — run `rl explain <concept>` for the full page.', ''];
698
+ const lines = [
699
+ 'Explanations — run `recursion explain <concept>` for the full page.',
700
+ '',
701
+ ];
699
702
  for (const group of groups) {
700
703
  lines.push(group.title);
701
704
  for (const c of group.entries.sort((a, b) => a.id.localeCompare(b.id, 'en'))) {
@@ -713,7 +716,7 @@ function renderConceptShow(concept) {
713
716
  }
714
717
  return `${parts.join('\n')}\n`;
715
718
  }
716
- /** `rl tutorials [<id>]` — Tutorials (the existing getting-started docs). */
719
+ /** `recursion tutorials [<id>]` — Tutorials (the existing getting-started docs). */
717
720
  function addTutorialsCommand(program, manifest, io) {
718
721
  program
719
722
  .command('tutorials [id]')
@@ -721,7 +724,10 @@ function addTutorialsCommand(program, manifest, io) {
721
724
  .description('Browse getting-started tutorials (run without an argument to list all)')
722
725
  .action((id) => {
723
726
  if (id === undefined) {
724
- const lines = ['Tutorials — run `rl tutorials <id>` for the full text.', ''];
727
+ const lines = [
728
+ 'Tutorials — run `recursion tutorials <id>` for the full text.',
729
+ '',
730
+ ];
725
731
  for (const t of manifest.tutorials)
726
732
  lines.push(` ${t.id} — ${t.title}`);
727
733
  io.stdout(`${lines.join('\n')}\n`);
@@ -729,7 +735,7 @@ function addTutorialsCommand(program, manifest, io) {
729
735
  }
730
736
  const entry = manifest.tutorials.find((t) => t.id === id);
731
737
  if (!entry) {
732
- throw new Error(`unknown tutorial "${id}". Run \`rl tutorials\` to list tutorials.`);
738
+ throw new Error(`unknown tutorial "${id}". Run \`recursion tutorials\` to list tutorials.`);
733
739
  }
734
740
  if (entry.body === null) {
735
741
  io.stdout(`${entry.title}\n\nThis tutorial is a notebook — open it in the app: ` +
@@ -741,7 +747,7 @@ function addTutorialsCommand(program, manifest, io) {
741
747
  }
742
748
  /** The program shell — name, description, version, and the global options. */
743
749
  export function buildBaseProgram(version, io) {
744
- return (new Command('rl')
750
+ return (new Command('recursion')
745
751
  // Route commander's own output (help text, unknown-command errors, invalid
746
752
  // option values) through the caller's sinks, and turn its `process.exit`
747
753
  // calls into thrown `CommanderError`s.
@@ -753,13 +759,13 @@ export function buildBaseProgram(version, io) {
753
759
  // process streams and still calling `process.exit`.
754
760
  .configureOutput({ writeOut: io.stdout, writeErr: io.stderr })
755
761
  .exitOverride()
756
- .description('Command-line interface for the Recursion RL platform')
762
+ .description('Command-line interface for the Recursion Platform')
757
763
  .version(version)
758
764
  .option('--api-key <key>', 'API key (defaults to the LABELBOX_API_KEY env var)')
759
765
  .option('--base-url <url>', 'Recursion API base URL (defaults to the RECURSION_BASE_URL env var, then the production host)')
760
766
  .option('--quiet', 'Output only the resulting resource id (JSON otherwise)'));
761
767
  }
762
- /** Build the full `rl` program from a fetched manifest. */
768
+ /** Build the full `recursion` program from a fetched manifest. */
763
769
  export function buildProgram(manifest, ctx) {
764
770
  // `ctx` structurally satisfies CliIo, so the root program — and by inheritance
765
771
  // every command built below — writes to the caller's sinks and never exits.
@@ -859,7 +865,7 @@ export function buildProgram(manifest, ctx) {
859
865
  for (const [parent, gatedCount] of gatedCountByParent) {
860
866
  parent.addHelpText('before', permissionGroupBanner(gatedCount));
861
867
  }
862
- // The docs browse groups + the bespoke `rl skills` action group.
868
+ // The docs browse groups + the bespoke `recursion skills` action group.
863
869
  addResourcesCommand(program, manifest, ctx);
864
870
  addRecipesCommand(program, manifest, ctx);
865
871
  addExplainCommand(program, manifest, ctx);
@@ -873,7 +879,7 @@ export function buildProgram(manifest, ctx) {
873
879
  // Lazy implicit help skips _initCommandGroup unless created via helpCommand(true).
874
880
  program.helpCommand(true);
875
881
  program.addHelpText('after', `
876
- Run \`rl <command> --help\` for details and flags on any command.
877
- Browse the full reference with \`rl resources\`, or guided walkthroughs with \`rl recipes\`.`);
882
+ Run \`recursion <command> --help\` for details and flags on any command.
883
+ Browse the full reference with \`recursion resources\`, or guided walkthroughs with \`recursion recipes\`.`);
878
884
  return program;
879
885
  }
package/dist/resolve.d.ts CHANGED
@@ -4,7 +4,7 @@ export declare function flagValue(argv: string[], name: string): string | undefi
4
4
  export declare function resolveApiKey(argv: string[]): string | undefined;
5
5
  /**
6
6
  * The base URL from the environment: the current `RECURSION_BASE_URL`, else the
7
- * deprecated `RL_GYM_BASE_URL`. Shared by `resolveBaseUrl` here and the `rl skills`
7
+ * deprecated `RL_GYM_BASE_URL`. Shared by `resolveBaseUrl` here and the `recursion skills`
8
8
  * resolver so the precedence lives — and is tested — in one place.
9
9
  */
10
10
  export declare function envBaseUrl(): string | undefined;
package/dist/resolve.js CHANGED
@@ -13,7 +13,7 @@ export function flagValue(argv, name) {
13
13
  const index = argv.indexOf(`--${name}`);
14
14
  if (index >= 0) {
15
15
  // The space-form value is the next token — but only if it isn't itself an
16
- // option (`rl --api-key --quiet`): a dangling `--api-key` has no value, so we
16
+ // option (`recursion --api-key --quiet`): a dangling `--api-key` has no value, so we
17
17
  // return undefined and let the env fallback apply rather than silently
18
18
  // consuming `--quiet` as a bogus (non-empty) key. Values that legitimately
19
19
  // start with `-` use the `--name=value` form above.
@@ -30,7 +30,7 @@ export function resolveApiKey(argv) {
30
30
  }
31
31
  /**
32
32
  * The base URL from the environment: the current `RECURSION_BASE_URL`, else the
33
- * deprecated `RL_GYM_BASE_URL`. Shared by `resolveBaseUrl` here and the `rl skills`
33
+ * deprecated `RL_GYM_BASE_URL`. Shared by `resolveBaseUrl` here and the `recursion skills`
34
34
  * resolver so the precedence lives — and is tested — in one place.
35
35
  */
36
36
  export function envBaseUrl() {
package/dist/run.d.ts CHANGED
@@ -31,7 +31,7 @@ export interface RunDeps {
31
31
  *
32
32
  * **Never throws and never exits.** It returns the exit code the caller should
33
33
  * use, having already rendered any failure to `deps.stderr`. That contract is
34
- * what lets the MCP endpoint run one `rl` invocation in-process on behalf of an
34
+ * what lets the MCP endpoint run one `recursion` invocation in-process on behalf of an
35
35
  * agent: a thrown error would become a 500 instead of a tool result, and a
36
36
  * `process.exit` would take the API server down mid-request. `bin.ts` is the only
37
37
  * caller that turns the returned code back into a real exit.
package/dist/run.js CHANGED
@@ -12,7 +12,7 @@ const MISSING_KEY_MESSAGE = 'missing API key — set LABELBOX_API_KEY or pass --
12
12
  *
13
13
  * **Never throws and never exits.** It returns the exit code the caller should
14
14
  * use, having already rendered any failure to `deps.stderr`. That contract is
15
- * what lets the MCP endpoint run one `rl` invocation in-process on behalf of an
15
+ * what lets the MCP endpoint run one `recursion` invocation in-process on behalf of an
16
16
  * agent: a thrown error would become a 500 instead of a tool result, and a
17
17
  * `process.exit` would take the API server down mid-request. `bin.ts` is the only
18
18
  * caller that turns the returned code back into a real exit.
package/dist/skills.d.ts CHANGED
@@ -41,7 +41,7 @@ export declare function listSkills(opts: {
41
41
  apiKey?: string | undefined;
42
42
  baseUrl: string;
43
43
  }): Promise<SkillSummary[]>;
44
- /** Programmatic core of `rl skills check` — fetch latest, read installed, compare. */
44
+ /** Programmatic core of `recursion skills check` — fetch latest, read installed, compare. */
45
45
  export declare function checkSkill(name: string, opts: {
46
46
  apiKey: string;
47
47
  baseUrl: string;
@@ -53,7 +53,7 @@ export declare function checkSkill(name: string, opts: {
53
53
  /** Raised when `install` refuses to clobber a locally-modified file (no `--force`). */
54
54
  export declare class HandEditedError extends Error {
55
55
  }
56
- /** Programmatic core of `rl skills install` — fetch latest and write it (guarded). */
56
+ /** Programmatic core of `recursion skills install` — fetch latest and write it (guarded). */
57
57
  export declare function installSkill(name: string, opts: {
58
58
  apiKey: string;
59
59
  baseUrl: string;
package/dist/skills.js CHANGED
@@ -6,14 +6,14 @@ import process from 'node:process';
6
6
  import { z } from 'zod';
7
7
  import { DEFAULT_BASE_URL, supportUrl } from './manifest.js';
8
8
  import { envBaseUrl } from './resolve.js';
9
- // ── bespoke `rl skills` group (hand-written, not spec-derived) ────────────────
9
+ // ── bespoke `recursion skills` group (hand-written, not spec-derived) ────────────────
10
10
  //
11
11
  // Keeps a downloaded Claude skill current. The platform serves each skill from
12
12
  // `GET /skills/:skillName` with a content-hash version; this group fetches that,
13
13
  // compares it to the installed `~/.claude/skills/<name>/SKILL.md`, and refreshes
14
- // it. The skill's own doctor preflight runs `rl skills check` each session and
15
- // refreshes via `rl skills install` when stale. Deliberately NOT an `@SdkRoute`
16
- // endpoint — that would auto-generate a colliding `rl skills` command.
14
+ // it. The skill's own doctor preflight runs `recursion skills check` each session and
15
+ // refreshes via `recursion skills install` when stale. Deliberately NOT an `@SdkRoute`
16
+ // endpoint — that would auto-generate a colliding `recursion skills` command.
17
17
  /** The skill installed by default when no name is given. */
18
18
  const DEFAULT_SKILL = 'recursion';
19
19
  // ── hash contract — MUST stay byte-identical to apps/recursion/api/src/skills/skill-hash.ts ──
@@ -79,9 +79,9 @@ function resolveAuth(program) {
79
79
  }
80
80
  return { apiKey, baseUrl: resolveBaseUrl(program) };
81
81
  }
82
- // Each `rl skills` fetch is bounded for the same reason as the manifest fetch
82
+ // Each `recursion skills` fetch is bounded for the same reason as the manifest fetch
83
83
  // (see manifest.ts): a server that accepts the connection but never responds would
84
- // otherwise hang `rl skills check`/`install`/`list` indefinitely. The timeout makes
84
+ // otherwise hang `recursion skills check`/`install`/`list` indefinitely. The timeout makes
85
85
  // `fetch` reject so the caller surfaces a clear error rather than blocking forever.
86
86
  const SKILLS_FETCH_TIMEOUT_MS = 30_000;
87
87
  /**
@@ -143,7 +143,7 @@ function readInstalled(path) {
143
143
  return undefined;
144
144
  }
145
145
  }
146
- /** Programmatic core of `rl skills check` — fetch latest, read installed, compare. */
146
+ /** Programmatic core of `recursion skills check` — fetch latest, read installed, compare. */
147
147
  export async function checkSkill(name, opts) {
148
148
  const latest = await fetchSkill(name, opts.apiKey, opts.baseUrl);
149
149
  const installed = readInstalled(installedPath(name, opts.skillFile));
@@ -152,7 +152,7 @@ export async function checkSkill(name, opts) {
152
152
  /** Raised when `install` refuses to clobber a locally-modified file (no `--force`). */
153
153
  export class HandEditedError extends Error {
154
154
  }
155
- /** Programmatic core of `rl skills install` — fetch latest and write it (guarded). */
155
+ /** Programmatic core of `recursion skills install` — fetch latest and write it (guarded). */
156
156
  export async function installSkill(name, opts) {
157
157
  const latest = await fetchSkill(name, opts.apiKey, opts.baseUrl);
158
158
  const path = installedPath(name, opts.skillFile);
@@ -197,7 +197,7 @@ export function addSkillsCommands(program, docsHelpGroup) {
197
197
  return;
198
198
  }
199
199
  const lines = available.map((s) => s.description ? ` ${s.name} — ${s.description}` : ` ${s.name}`);
200
- process.stdout.write(`Installable skills (run \`rl skills install <name>\`):\n${lines.join('\n')}\n`);
200
+ process.stdout.write(`Installable skills (run \`recursion skills install <name>\`):\n${lines.join('\n')}\n`);
201
201
  }
202
202
  catch (err) {
203
203
  process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\n`);
@@ -218,7 +218,7 @@ export function addSkillsCommands(program, docsHelpGroup) {
218
218
  return;
219
219
  }
220
220
  const lead = status === 'missing' ? 'is not installed' : 'is out of date';
221
- process.stdout.write(`${skill} ${lead} — run \`rl skills install ${skill}\`.\n`);
221
+ process.stdout.write(`${skill} ${lead} — run \`recursion skills install ${skill}\`.\n`);
222
222
  process.exit(1);
223
223
  }
224
224
  catch (err) {
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@labelbox/recursion-cli",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Labelbox/recursion-platform.git"
8
8
  },
9
9
  "bin": {
10
+ "recursion": "./dist/bin.js",
10
11
  "rl": "./dist/bin.js"
11
12
  },
12
13
  "files": [