@labelbox/recursion-cli 0.0.0 → 0.0.41

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 +1,113 @@
1
- Placeholder. Do not install. Real package ships from Recursion CI.
1
+ # @labelbox/recursion-cli
2
+
3
+ `rl` — the rl-gym command-line interface. It mirrors the TypeScript SDK
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.
7
+
8
+ ```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
12
+ ```
13
+
14
+ ## How it works (fully live, zero per-operation code)
15
+
16
+ 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`):
21
+ each request is built straight from the manifest operation's HTTP method + path
22
+ template + params + body — there is no hand-written command per operation and no
23
+ baked client.
24
+
25
+ The result: **adding or changing a backend endpoint needs zero CLI release** — the
26
+ 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
+
31
+ ### Docs browse surfaces
32
+
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
35
+ shows that one):
36
+
37
+ ```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
42
+ ```
43
+
44
+ ## Install / run
45
+
46
+ Published to **public npm** as `@labelbox/recursion-cli` — no token, no repo access, no
47
+ setup beyond Node itself.
48
+
49
+ ```sh
50
+ 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
52
+ # or, zero-install:
53
+ npx @labelbox/recursion-cli --help
54
+ ```
55
+
56
+ The CLI is standalone, so this pulls in no `@labelbox/recursion-sdk`.
57
+ **Full setup steps and install-failure triage live in
58
+ `apps/recursion/web/public/docs/cli-getting-started.md`** — that page owns them; don't
59
+ restate them here.
60
+
61
+ The CLI is released independently of the SDK (it no longer depends on it): a
62
+ `packages/cli/**` change cuts a CLI release via the **Release / CLI** GitHub
63
+ Action — see `yarn dx help release`.
64
+
65
+ From a monorepo checkout, run the bin directly without installing:
66
+
67
+ ```sh
68
+ yarn workspace @labelbox/recursion-cli build
69
+ node ./packages/cli/dist/bin.js --help
70
+ ```
71
+
72
+ ## Auth
73
+
74
+ Set `LABELBOX_API_KEY` in the environment, or pass `--api-key <key>`. Override the
75
+ host with `--base-url <url>` (defaults to the public Recursion API gateway, see
76
+ `DEFAULT_BASE_URL` in `src/manifest.ts`).
77
+
78
+ ## Flags
79
+
80
+ - **Path / query params** are individual flags: `--environment-id`, `--limit`, …
81
+ Required path and query params must be passed as flags. A query param whose type
82
+ is an array or object (e.g. `--models`, `--enrichment-filters`) takes a JSON value
83
+ (`--models '["a","b"]'`); its help text is tagged `[pass as JSON]`.
84
+ - **Request body**: scalar top-level fields are individual flags (`--name`,
85
+ `--system-prompt`, …); pass the full body — including complex fields like
86
+ `contextInputs` / `targetFields` — with `--from-json <file>` or `--data <json>`
87
+ (mutually exclusive — pass only one). Scalar flags override values from
88
+ `--from-json`/`--data`. A required scalar body field can be supplied by either
89
+ its flag or the JSON body.
90
+ - **Output**: prints the result as JSON by default; `--quiet` prints only the
91
+ resulting resource's `id` (a blank line for results without one, e.g. list or
92
+ no-content operations).
93
+
94
+ ```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
98
+ ```
99
+
100
+ ## The command surface is the live server
101
+
102
+ 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 dx manifest:generate` (wired into `dx codegen`) 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.
108
+
109
+ The *engine* (manifest fetch + cache + validation, generic dispatch, flag mapping,
110
+ help formatting, the request-body / returns shape trees, and the docs browse
111
+ renderers) is verified by `src/*.test.ts`, which build the program from a fixture
112
+ manifest in-memory and assert the realized flags + `--help` + browse output — no
113
+ committed golden snapshot to maintain.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import process from 'node:process';
3
+ import { fetchManifest } from './manifest.js';
4
+ import { fetchPermissions } from './permissions.js';
5
+ import { run } from './run.js';
6
+ import { readCliVersion } from './version.js';
7
+ // The orchestration lives in run.ts (testable); this entrypoint only binds the real
8
+ // dependencies and turns the returned code into a process exit. Runs on import —
9
+ // kept logic-free so it never needs its own test.
10
+ //
11
+ // `run()` never throws and never exits: it renders every failure (including a
12
+ // missing key, a manifest fetch/validation error, and synchronous throws from
13
+ // buildProgram() such as a reserved-flag collision) to the `stderr` sink as
14
+ // `error: <message>`, then returns the exit code. The `.catch()` below is a
15
+ // backstop for a defect in that contract, not a routine path.
16
+ run({
17
+ argv: process.argv,
18
+ // Pass the reader, not its result — run() invokes it so a throw (corrupt install)
19
+ // is rendered as `error: <message>` rather than a raw stack trace.
20
+ version: readCliVersion,
21
+ fetchManifest,
22
+ fetchPermissions,
23
+ // The terminal entrypoint: `scaffold`, `submit`, and `skills` act on the
24
+ // developer's own checkout, so this is the one caller that registers them.
25
+ localCheckout: true,
26
+ stdout: (text) => process.stdout.write(text),
27
+ stderr: (text) => process.stderr.write(text),
28
+ })
29
+ .then((code) => {
30
+ // Only exit non-zero. Calling process.exit(0) here could truncate a large
31
+ // pending stdout write; letting node drain and exit naturally cannot.
32
+ if (code !== 0)
33
+ process.exit(code);
34
+ })
35
+ .catch((err) => {
36
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\n`);
37
+ process.exit(1);
38
+ });
@@ -0,0 +1,15 @@
1
+ import type { ManifestOperation } from './manifest.js';
2
+ export interface DispatchContext {
3
+ baseUrl: string;
4
+ apiKey: string;
5
+ }
6
+ /** Build the full request URL: base + path (params substituted) + query string. */
7
+ export declare function buildUrl(op: ManifestOperation, params: Record<string, unknown>, baseUrl: string): string;
8
+ /**
9
+ * Build and send the request for one operation, returning the parsed success body
10
+ * (or `undefined` for a 204 / empty response). On a non-2xx response the parsed
11
+ * error body is thrown (so `formatError` prints the server's JSON detail);
12
+ * `fetch` rejecting for an unreachable server propagates and is mapped to the
13
+ * standard "could not be reached" message by `formatError`.
14
+ */
15
+ export declare function dispatchOperation(op: ManifestOperation, params: Record<string, unknown>, ctx: DispatchContext): Promise<unknown>;
@@ -0,0 +1,122 @@
1
+ // Bound every request so a server that accepts the connection but never responds
2
+ // can't hang the CLI indefinitely. Generous — recursion operations are quick REST
3
+ // calls (long work is async + polled), so this only trips on a genuine hang.
4
+ const REQUEST_TIMEOUT_MS = 120_000;
5
+ /** Serialize one query param value the way the generated client did. */
6
+ function serializeQueryParam(name, value) {
7
+ if (value === undefined || value === null)
8
+ return [];
9
+ if (Array.isArray(value)) {
10
+ // form + explode: repeat the key per element. A non-scalar element (object/array)
11
+ // is JSON-encoded so it survives rather than becoming `[object Object]` —
12
+ // consistent with the object branch below. No shipped query param is `object[]`
13
+ // today; this keeps the two branches from diverging.
14
+ return value
15
+ .filter((v) => v !== undefined && v !== null)
16
+ .map((v) => {
17
+ const encoded = typeof v === 'object' ? JSON.stringify(v) : String(v);
18
+ return `${name}=${encodeURIComponent(encoded)}`;
19
+ });
20
+ }
21
+ if (typeof value === 'object') {
22
+ // Object-valued query params (the `deepObject` `enrichmentFilters`, a
23
+ // record(string, string[])) go on the wire as a single JSON-encoded string. The
24
+ // backend does NOT bracket-parse (`name[key]=v` lands as a stray key and the
25
+ // param reads as absent — verified against the live API); its schema preprocess
26
+ // JSON.parses the value, and the frontend serializes it identically
27
+ // (apps/recursion/web/src/api/problems.ts). JSON also preserves the nested arrays a
28
+ // bracket + `String()` encoding silently corrupted.
29
+ return [`${name}=${encodeURIComponent(JSON.stringify(value))}`];
30
+ }
31
+ return [`${name}=${encodeURIComponent(String(value))}`];
32
+ }
33
+ /** Build the full request URL: base + path (params substituted) + query string. */
34
+ export function buildUrl(op, params, baseUrl) {
35
+ const path = op.path.replace(/\{([^}]+)\}/gu, (_match, name) => {
36
+ const value = params[name];
37
+ if (value === undefined || value === null) {
38
+ throw new Error(`missing required path parameter "${name}"`);
39
+ }
40
+ return encodeURIComponent(String(value));
41
+ });
42
+ const search = [];
43
+ for (const param of op.params) {
44
+ if (param.in === 'query')
45
+ search.push(...serializeQueryParam(param.name, params[param.name]));
46
+ }
47
+ const base = baseUrl.replace(/\/$/u, '');
48
+ const withPath = `${base}${path.startsWith('/') ? path : `/${path}`}`;
49
+ return search.length > 0 ? `${withPath}?${search.join('&')}` : withPath;
50
+ }
51
+ /** Parse a response body, dispatching on content type (JSON vs. raw text). */
52
+ async function parseBody(res) {
53
+ if (res.status === 204)
54
+ return undefined;
55
+ const text = await res.text();
56
+ if (text === '')
57
+ return undefined;
58
+ const contentType = res.headers.get('content-type') ?? '';
59
+ if (contentType.includes('application/json') || contentType.includes('+json')) {
60
+ try {
61
+ return JSON.parse(text);
62
+ }
63
+ catch {
64
+ return text;
65
+ }
66
+ }
67
+ return text;
68
+ }
69
+ /**
70
+ * Build and send the request for one operation, returning the parsed success body
71
+ * (or `undefined` for a 204 / empty response). On a non-2xx response the parsed
72
+ * error body is thrown (so `formatError` prints the server's JSON detail);
73
+ * `fetch` rejecting for an unreachable server propagates and is mapped to the
74
+ * standard "could not be reached" message by `formatError`.
75
+ */
76
+ export async function dispatchOperation(op, params, ctx) {
77
+ const url = buildUrl(op, params, ctx.baseUrl);
78
+ const headers = {
79
+ // biome-ignore lint/style/useNamingConvention: HTTP header names are not camelCase.
80
+ Authorization: `Bearer ${ctx.apiKey}`,
81
+ // biome-ignore lint/style/useNamingConvention: HTTP header names are not camelCase.
82
+ Accept: 'application/json',
83
+ };
84
+ let body;
85
+ if (op.bodyKey && params[op.bodyKey] !== undefined) {
86
+ body = JSON.stringify(params[op.bodyKey]);
87
+ headers['Content-Type'] = 'application/json';
88
+ }
89
+ let res;
90
+ try {
91
+ res = await fetch(url, {
92
+ method: op.httpMethod.toUpperCase(),
93
+ headers,
94
+ // Spread rather than `body` — `RequestInit.body` is not optional-undefined,
95
+ // so passing `undefined` explicitly is a type error under
96
+ // exactOptionalPropertyTypes (and a bodyless GET is the common case).
97
+ ...(body === undefined ? {} : { body }),
98
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
99
+ });
100
+ }
101
+ catch (err) {
102
+ // AbortSignal.timeout rejects with a TimeoutError — turn the hang into a clear,
103
+ // actionable message; other fetch failures (refused/reset) propagate unchanged.
104
+ if (err instanceof Error && err.name === 'TimeoutError') {
105
+ throw new Error(`request to ${url} timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);
106
+ }
107
+ throw err;
108
+ }
109
+ const parsed = await parseBody(res);
110
+ if (!res.ok) {
111
+ // Prefer the server's structured error body (formatError renders it as JSON);
112
+ // fall back to a status-coded Error when there's no usable body. `!== null` (not
113
+ // `!== undefined`) because `typeof null === 'object'` — a literal `null` body
114
+ // would otherwise be thrown and surface as an unactionable `error: null`.
115
+ if (parsed !== null && typeof parsed === 'object')
116
+ throw parsed;
117
+ throw new Error(typeof parsed === 'string' && parsed !== ''
118
+ ? `request failed (HTTP ${res.status}): ${parsed}`
119
+ : `request failed — HTTP ${res.status}`);
120
+ }
121
+ return parsed;
122
+ }
@@ -0,0 +1,41 @@
1
+ import type { Manifest } from './manifest.js';
2
+ import type { GrantedPermissions } from './permissions.js';
3
+ export type { Manifest, GrantedPermissions };
4
+ export interface ExecuteCliOptions {
5
+ /** CLI arguments as the caller would type them, e.g. `['environments', 'list']`. */
6
+ args: readonly string[];
7
+ /** The caller's own API key, from the request. Never the server's environment. */
8
+ apiKey: string;
9
+ /** The API the CLI dispatches against. Server configuration, not caller input. */
10
+ baseUrl: string;
11
+ /** Reported by `rl --version`. */
12
+ version: string;
13
+ /** The pre-assembled manifest — served from memory, never re-fetched over HTTP. */
14
+ manifest: Manifest;
15
+ /** The caller's granted permissions, so `--help` and gating match who is asking. */
16
+ granted: GrantedPermissions;
17
+ }
18
+ export interface CliResult {
19
+ stdout: string;
20
+ stderr: string;
21
+ exitCode: number;
22
+ /** True when the invocation failed — becomes the tool result's `isError`. */
23
+ failed: boolean;
24
+ }
25
+ /**
26
+ * Reject argv that would exfiltrate the caller's key, reach the filesystem, or
27
+ * spawn a process. Messages are written for the model, naming the alternative so
28
+ * it can correct itself rather than retrying the same call.
29
+ *
30
+ * @throws Error when the invocation is not permitted here.
31
+ */
32
+ export declare function assertAllowedArgv(args: readonly string[]): void;
33
+ /**
34
+ * Run one `rl` invocation in-process and capture its output.
35
+ *
36
+ * Resolves for both success and failure — a non-zero exit is a normal outcome the
37
+ * model should see and correct from, not an exception. It rejects only on a
38
+ * refused argv (see `assertAllowedArgv`), or if `run()` breaks its no-throw
39
+ * contract.
40
+ */
41
+ export declare function executeCli(options: ExecuteCliOptions): Promise<CliResult>;
package/dist/embed.js ADDED
@@ -0,0 +1,153 @@
1
+ import { run } from './run.js';
2
+ // Running `rl` inside a server process, on behalf of an agent, without a shell.
3
+ //
4
+ // The MCP endpoint exposes one tool whose argument is an argv array. This module
5
+ // is the whole surface it needs: `executeCli` builds the argv, runs the same
6
+ // `run()` the terminal entrypoint uses, and returns the captured output instead of
7
+ // writing to the process streams.
8
+ //
9
+ // Three things make this safe rather than merely convenient:
10
+ //
11
+ // 1. **No process is spawned.** There is no shell, so there is no quoting,
12
+ // escaping, or metacharacter surface — argv is passed as an array and reaches
13
+ // commander verbatim. An agent cannot reach the container's shell through it.
14
+ // 2. **Credentials and target are server-controlled**, never caller-supplied.
15
+ // See RESERVED_FLAGS — this is the difference between running the CLI for the
16
+ // caller and letting the caller point the CLI wherever it likes.
17
+ // 3. **Laptop-shaped commands are refused.** BLOCKED_COMMANDS covers the CLI's
18
+ // only routes to the host filesystem and to `spawnSync`.
19
+ //
20
+ // Everything else — the command tree, `--help`, permission gating, dispatch — is
21
+ // the real CLI, unchanged.
22
+ /**
23
+ * Command groups that never make sense server-side, and that are the CLI's only
24
+ * routes to the host filesystem or a child process.
25
+ *
26
+ * - `scaffold` / `submit` (`git-host.ts`) run `spawnSync('git', …)` and write an
27
+ * executable credential helper, handing it the parent's full environment.
28
+ * - `skills` (`skills.ts`) writes into `~/.claude/skills/` — on a server that is
29
+ * the container's home directory, not the caller's machine.
30
+ *
31
+ * **This set is not what makes them unreachable.** `run({ localCheckout: false })`
32
+ * below never registers them, so the capability is absent from the command tree
33
+ * rather than filtered out of argv. This check exists only so a model that asks for
34
+ * one gets an explanation instead of commander's bare `unknown command`.
35
+ */
36
+ const BLOCKED_COMMANDS = new Set(['scaffold', 'submit', 'skills']);
37
+ /**
38
+ * Flags the caller may not set, each because the server must decide it.
39
+ *
40
+ * `--base-url` is the security-critical one. `dispatchOperation` sends
41
+ * `Authorization: Bearer <apiKey>` to whatever base URL resolves, so a caller-set
42
+ * `--base-url` would send the *caller's own API key* to a host of the caller's
43
+ * choosing. Refusing it is what keeps this from being a credential-exfiltration
44
+ * primitive wrapped in a tool call.
45
+ *
46
+ * `--api-key` is refused for the same reason in reverse: the identity a request
47
+ * runs under is established by the request, not by an argument a model can write.
48
+ *
49
+ * Rejecting is not merely belt-and-braces over appending our own values.
50
+ * `resolveApiKey`/`resolveBaseUrl` read argv via `flagValue`, which prefers the
51
+ * `--name=value` form found *anywhere* in argv over the space form — so a
52
+ * caller-supplied `--base-url=…` would beat a server value appended afterwards.
53
+ * The only correct handling is to refuse the argv outright.
54
+ */
55
+ const RESERVED_FLAGS = ['--api-key', '--base-url'];
56
+ /**
57
+ * `--from-json <path>` reads an arbitrary file and folds it into the request body
58
+ * (`program.ts` → `parseBodyBase`). In a server process that is a file-read
59
+ * primitive handed to a model: the contents reach the API, and a parse failure
60
+ * echoes the path back in the error. `--data` accepts the same JSON inline and is
61
+ * always available, so nothing is lost by refusing the file form.
62
+ */
63
+ const BLOCKED_FLAG = '--from-json';
64
+ /** Does argv set this flag, in either the space form or the `=` form? */
65
+ function hasFlag(args, flag) {
66
+ return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
67
+ }
68
+ /**
69
+ * Reject argv that would exfiltrate the caller's key, reach the filesystem, or
70
+ * spawn a process. Messages are written for the model, naming the alternative so
71
+ * it can correct itself rather than retrying the same call.
72
+ *
73
+ * @throws Error when the invocation is not permitted here.
74
+ */
75
+ export function assertAllowedArgv(args) {
76
+ for (const flag of RESERVED_FLAGS) {
77
+ if (hasFlag(args, flag)) {
78
+ throw new Error(`${flag} cannot be set here — it is determined by your authenticated session. ` +
79
+ 'Remove it and retry.');
80
+ }
81
+ }
82
+ if (hasFlag(args, BLOCKED_FLAG)) {
83
+ throw new Error(`${BLOCKED_FLAG} is not available here because it reads a file from the server. ` +
84
+ 'Pass the same JSON inline with --data instead.');
85
+ }
86
+ // Only the command position, which is the first non-flag token — where commander
87
+ // resolves it from. Scanning every token instead would permanently refuse valid
88
+ // data that happens to spell one of these names (`--name submit`), and because it
89
+ // is the caller's own data the model could not retry its way out of it. The
90
+ // security property does not rest on this check anyway; see BLOCKED_COMMANDS.
91
+ const command = args.find((arg) => !arg.startsWith('-'));
92
+ if (command !== undefined && BLOCKED_COMMANDS.has(command)) {
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.');
95
+ }
96
+ }
97
+ /**
98
+ * Run one `rl` invocation in-process and capture its output.
99
+ *
100
+ * Resolves for both success and failure — a non-zero exit is a normal outcome the
101
+ * model should see and correct from, not an exception. It rejects only on a
102
+ * refused argv (see `assertAllowedArgv`), or if `run()` breaks its no-throw
103
+ * contract.
104
+ */
105
+ export async function executeCli(options) {
106
+ if (options.apiKey === '') {
107
+ throw new Error('cannot run the CLI without an API key for the calling user');
108
+ }
109
+ assertAllowedArgv(options.args);
110
+ const stdout = [];
111
+ const stderr = [];
112
+ const exitCode = await run({
113
+ // Server-controlled flags go first, before anything the caller sent — global
114
+ // flags precede the command, which is the form `resolveApiKey`/`resolveBaseUrl`
115
+ // scan for. `assertAllowedArgv` has already guaranteed the caller set neither.
116
+ // `run()` slices off the leading two entries as it would a real `process.argv`,
117
+ // so the placeholders have to be present.
118
+ //
119
+ // The `=` form, not the space form: `flagValue` refuses to read a space-form
120
+ // value that itself starts with `-`, so a key or URL beginning with a dash would
121
+ // read as unset and `resolveApiKey` would fall through to
122
+ // `process.env.LABELBOX_API_KEY` — turning a key in the container into the acting
123
+ // identity, the exact substitution this path exists to prevent. The `=` form
124
+ // binds any value, and is also the form `flagValue` prefers, so nothing later in
125
+ // argv can outrank it.
126
+ argv: [
127
+ 'node',
128
+ 'rl',
129
+ `--api-key=${options.apiKey}`,
130
+ `--base-url=${options.baseUrl}`,
131
+ ...options.args,
132
+ ],
133
+ version: () => options.version,
134
+ // Both fetches are satisfied from what the caller already resolved: the
135
+ // manifest is embedded in the server, and the permissions came in on the
136
+ // request. No HTTP, no ETag cache directory, and no way for a slow or
137
+ // unreachable network to stall a tool call.
138
+ fetchManifest: () => Promise.resolve(options.manifest),
139
+ fetchPermissions: () => Promise.resolve(options.granted),
140
+ // No developer checkout here, so `scaffold`, `submit`, and `skills` are never
141
+ // registered — the CLI's only `spawnSync`, `~/.claude/` writes, and
142
+ // `process.exit` calls are absent from this program, not merely refused.
143
+ localCheckout: false,
144
+ stdout: (text) => stdout.push(text),
145
+ stderr: (text) => stderr.push(text),
146
+ });
147
+ return {
148
+ stdout: stdout.join(''),
149
+ stderr: stderr.join(''),
150
+ exitCode,
151
+ failed: exitCode !== 0,
152
+ };
153
+ }
@@ -0,0 +1,16 @@
1
+ import type { Command } from 'commander';
2
+ /** `POST /v1/organizations/:organizationId/git-repo-claims` — mints a per-Aligner
3
+ * repo + push token for `problemId`. The claiming Aligner is the
4
+ * authenticated caller (the API key), never a client-supplied field. */
5
+ export declare function claimGitRepo(args: {
6
+ apiKey: string;
7
+ baseUrl: string;
8
+ organizationId: string;
9
+ problemId: string;
10
+ }): Promise<{
11
+ cloneUrl: string;
12
+ defaultBranch: string;
13
+ pushToken: string;
14
+ }>;
15
+ /** Register the `scaffold` / `submit` commands on `program`. */
16
+ export declare function addGitHostCommands(program: Command, helpGroup: string): void;