@labelbox/recursion-cli 0.0.0 → 0.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,190 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { basename, join } from 'node:path';
5
+ import process from 'node:process';
6
+ import { z } from 'zod';
7
+ import { DEFAULT_BASE_URL, supportUrl } from './manifest.js';
8
+ import { envBaseUrl } from './resolve.js';
9
+ const GitRepoClaimSchema = z.object({
10
+ cloneUrl: z.string(),
11
+ defaultBranch: z.string(),
12
+ pushToken: z.string(),
13
+ });
14
+ /** Filename (under `.git/`) the claim's identity is stashed in after
15
+ * `scaffold`, so `submit` can re-claim before every push. `submit` MUST
16
+ * re-claim rather than reuse a token stashed at scaffold time: the backend
17
+ * mints one push token per Aligner under a fixed name and deletes any
18
+ * prior token of that name on every claim (`GitHostClient.mintPushToken`),
19
+ * since the Forgejo username derives from the Aligner alone, not the
20
+ * problem. Without re-claiming, scaffolding a second problem would
21
+ * invalidate the first problem's stashed token and its `submit` would
22
+ * 401/403. */
23
+ const CLAIM_FILE = 'rl-forgejo-claim.json';
24
+ const StashedClaimSchema = z.object({
25
+ organizationId: z.string(),
26
+ problemId: z.string(),
27
+ });
28
+ function resolveBaseUrl(program) {
29
+ const { baseUrl } = program.opts();
30
+ return baseUrl ?? envBaseUrl() ?? DEFAULT_BASE_URL;
31
+ }
32
+ function resolveAuth(program) {
33
+ const { apiKey } = program.opts();
34
+ const { LABELBOX_API_KEY: envApiKey } = process.env;
35
+ const resolved = apiKey ?? envApiKey;
36
+ if (!resolved) {
37
+ throw new Error('missing API key — set LABELBOX_API_KEY or pass --api-key');
38
+ }
39
+ return { apiKey: resolved, baseUrl: resolveBaseUrl(program) };
40
+ }
41
+ /** `POST /v1/organizations/:organizationId/git-repo-claims` — mints a per-Aligner
42
+ * repo + push token for `problemId`. The claiming Aligner is the
43
+ * authenticated caller (the API key), never a client-supplied field. */
44
+ export async function claimGitRepo(args) {
45
+ const url = supportUrl(args.baseUrl, `/organizations/${encodeURIComponent(args.organizationId)}/git-repo-claims`);
46
+ const res = await fetch(url, {
47
+ method: 'POST',
48
+ headers: {
49
+ // biome-ignore lint/style/useNamingConvention: HTTP header names are not camelCase.
50
+ Authorization: `Bearer ${args.apiKey}`,
51
+ 'Content-Type': 'application/json',
52
+ },
53
+ body: JSON.stringify({ problemId: args.problemId }),
54
+ });
55
+ if (!res.ok) {
56
+ const text = await res.text().catch(() => '');
57
+ throw new Error(`could not claim a git repo for problem ${args.problemId} — HTTP ${res.status} ${text.slice(0, 200)}`);
58
+ }
59
+ const parsed = GitRepoClaimSchema.safeParse(await res.json());
60
+ if (!parsed.success) {
61
+ throw new Error(`unexpected response shape from ${url}`);
62
+ }
63
+ return parsed.data;
64
+ }
65
+ /** Writes a `GIT_ASKPASS` helper that answers "Username" prompts with a
66
+ * placeholder and "Password" prompts with `token`, via an env var — never
67
+ * the URL or argv, so the token never lands in shell history or `ps`
68
+ * listings. Returns the env overrides to pass to the `git` subprocess and
69
+ * the script's containing directory, for the caller to clean up.
70
+ *
71
+ * `GIT_ASKPASS` must be a single executable file, not a "command args"
72
+ * string — verified live: git silently never invokes a two-word value like
73
+ * `"<node> <script>"` (no error, no fallback prompt — it just doesn't run),
74
+ * so the script needs a `#!/usr/bin/env node` shebang and the exec bit.
75
+ *
76
+ * The script lives in its own `mkdtempSync` directory (not a fixed,
77
+ * guessable path directly under `tmpdir()`) and is written with the `wx`
78
+ * flag, which refuses to follow or overwrite an existing path — closing
79
+ * the local-attacker-pre-plants-a-symlink hardening gap a fixed name would
80
+ * leave open, even though the script itself carries no secret (the token
81
+ * rides in `RL_FORGEJO_TOKEN`, not the script body). */
82
+ function gitCredentialEnv(token) {
83
+ const dir = mkdtempSync(join(tmpdir(), 'rl-forgejo-'));
84
+ const scriptPath = join(dir, 'askpass.cjs');
85
+ writeFileSync(scriptPath, '#!/usr/bin/env node\n' +
86
+ "const p = process.argv[2] || '';\n" +
87
+ "process.stdout.write(/username/i.test(p) ? 'x-access-token' : (process.env.RL_FORGEJO_TOKEN || ''));\n", { flag: 'wx' });
88
+ chmodSync(scriptPath, 0o700);
89
+ return {
90
+ env: {
91
+ ...process.env,
92
+ // biome-ignore lint/style/useNamingConvention: environment variable names, not ours to rename
93
+ GIT_ASKPASS: scriptPath,
94
+ // biome-ignore lint/style/useNamingConvention: environment variable names, not ours to rename
95
+ RL_FORGEJO_TOKEN: token,
96
+ },
97
+ cleanupDir: dir,
98
+ };
99
+ }
100
+ function runGit(args, cwd, token) {
101
+ const { env, cleanupDir } = gitCredentialEnv(token);
102
+ try {
103
+ // `-c credential.helper=` (empty) disables any configured credential
104
+ // helper (e.g. macOS's osxkeychain, on by default with git-for-mac) for
105
+ // just this invocation — found live: without it, a helper transparently
106
+ // caches/replays a *previous* claim's credential for the same host,
107
+ // silently shadowing GIT_ASKPASS and authenticating as the wrong claim
108
+ // (or failing on an expired one) instead of using the fresh token below.
109
+ const result = spawnSync('git', ['-c', 'credential.helper=', ...args], {
110
+ cwd,
111
+ env,
112
+ stdio: 'inherit',
113
+ });
114
+ if (result.status !== 0) {
115
+ throw new Error(`git ${args.join(' ')} failed (exit ${result.status ?? 'unknown'})`);
116
+ }
117
+ }
118
+ finally {
119
+ rmSync(cleanupDir, { recursive: true, force: true });
120
+ }
121
+ }
122
+ function claimFilePath(repoDir) {
123
+ return join(repoDir, '.git', CLAIM_FILE);
124
+ }
125
+ function stashClaim(repoDir, claim) {
126
+ writeFileSync(claimFilePath(repoDir), JSON.stringify(claim), { mode: 0o600 });
127
+ }
128
+ function readStashedClaim(repoDir) {
129
+ const path = claimFilePath(repoDir);
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`);
132
+ }
133
+ const parsed = StashedClaimSchema.safeParse(JSON.parse(readFileSync(path, 'utf8')));
134
+ if (!parsed.success) {
135
+ throw new Error(`stashed claim at ${path} has an unexpected shape`);
136
+ }
137
+ return parsed.data;
138
+ }
139
+ /** Register the `scaffold` / `submit` commands on `program`. */
140
+ export function addGitHostCommands(program, helpGroup) {
141
+ program
142
+ .command('scaffold <problemId>')
143
+ .helpGroup(helpGroup)
144
+ .description('Claim a per-Aligner git repo for a coding-task problem and clone it locally')
145
+ .requiredOption('--organization-id <id>', 'Organization the problem belongs to')
146
+ .option('--out <dir>', 'Directory to clone into (default: derived from the repo name)')
147
+ .action(async (problemId, opts) => {
148
+ try {
149
+ const { apiKey, baseUrl } = resolveAuth(program);
150
+ const claim = await claimGitRepo({
151
+ apiKey,
152
+ baseUrl,
153
+ organizationId: opts.organizationId,
154
+ problemId,
155
+ });
156
+ const dir = opts.out ?? basename(claim.cloneUrl).replace(/\.git$/u, '');
157
+ mkdirSync(dir, { recursive: true });
158
+ runGit(['clone', claim.cloneUrl, dir], process.cwd(), claim.pushToken);
159
+ stashClaim(dir, { organizationId: opts.organizationId, problemId });
160
+ process.stdout.write(`Cloned into ${dir} (branch ${claim.defaultBranch}). Ready to work.\n`);
161
+ }
162
+ catch (err) {
163
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\n`);
164
+ process.exit(1);
165
+ }
166
+ });
167
+ program
168
+ .command('submit')
169
+ .helpGroup(helpGroup)
170
+ .description('Push local changes in a scaffolded repo back to its claimed remote')
171
+ .option('--path <dir>', 'Path to the scaffolded repo (default: current directory)')
172
+ .action(async (opts) => {
173
+ const dir = opts.path ?? process.cwd();
174
+ try {
175
+ // Re-claims rather than reusing a token stashed at `scaffold` time —
176
+ // see `CLAIM_FILE`'s doc comment: the backend's push token is a
177
+ // single fixed-name credential per Aligner, so scaffolding any other
178
+ // problem since would have invalidated a stashed one.
179
+ const stashed = readStashedClaim(dir);
180
+ const { apiKey, baseUrl } = resolveAuth(program);
181
+ const claim = await claimGitRepo({ apiKey, baseUrl, ...stashed });
182
+ runGit(['push'], dir, claim.pushToken);
183
+ process.stdout.write('Pushed.\n');
184
+ }
185
+ catch (err) {
186
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\n`);
187
+ process.exit(1);
188
+ }
189
+ });
190
+ }
@@ -0,0 +1,410 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The DEFAULT production base URL: the public API gateway origin. No path
4
+ * suffix — the API version lives in each operation's path (`/v1/...`), so the
5
+ * CLI joins this origin with the manifest's paths verbatim.
6
+ *
7
+ * Keep in sync with `DEFAULT_BASE_URL` in `packages/sdk-ts/src/index.ts`,
8
+ * `PUBLIC_API_BASE_URL` in `packages/sdk-ts/openapi-ts.config.ts`, and
9
+ * `CURL_BASE_URL` in `tools/dx/src/commands/sdk.ts`.
10
+ */
11
+ export declare const DEFAULT_BASE_URL = "https://api.recursion.labelbox.com";
12
+ /**
13
+ * Join a hand-written support-endpoint path onto the base URL, under the API
14
+ * version segment. `path` is version-relative and must start with `/`.
15
+ */
16
+ export declare function supportUrl(baseUrl: string, path: string): string;
17
+ /**
18
+ * The manifest format the engine understands. Bumped only on a *breaking* schema
19
+ * change (a renamed/removed required field, a changed enum) — additive fields are
20
+ * backward-compatible because the schema strips unknown keys rather than rejecting
21
+ * them, so a newer server never breaks an older CLI by adding data. A mismatch is
22
+ * surfaced with an actionable upgrade message at the validation boundary.
23
+ */
24
+ export declare const MANIFEST_FORMAT_VERSION = 1;
25
+ export interface ShapeNode {
26
+ name?: string | undefined;
27
+ type: string;
28
+ required?: boolean | undefined;
29
+ description?: string | undefined;
30
+ enum?: string[] | undefined;
31
+ itemEnum?: string[] | undefined;
32
+ default?: string | number | boolean | undefined;
33
+ format?: string | undefined;
34
+ example?: string | number | boolean | undefined;
35
+ nullable?: boolean | undefined;
36
+ minimum?: number | undefined;
37
+ maximum?: number | undefined;
38
+ exclusiveMinimum?: number | undefined;
39
+ exclusiveMaximum?: number | undefined;
40
+ minLength?: number | undefined;
41
+ maxLength?: number | undefined;
42
+ minItems?: number | undefined;
43
+ maxItems?: number | undefined;
44
+ pattern?: string | undefined;
45
+ fields?: ShapeNode[] | undefined;
46
+ items?: ShapeNode | undefined;
47
+ variants?: ShapeNode[] | undefined;
48
+ }
49
+ export declare const ShapeNodeSchema: z.ZodType<ShapeNode>;
50
+ /** The metadata / constraint keys a `ShapeNode` carries beyond its structure. */
51
+ export type ShapeMetaKey = Exclude<keyof ShapeNode, 'name' | 'type' | 'required' | 'description' | 'fields' | 'items' | 'variants'>;
52
+ /** A node's present metadata as ordered `[key, value]` pairs (drives the CLI tags). */
53
+ export declare function shapeMetaEntries(node: ShapeNode): Array<readonly [ShapeMetaKey, string | number | boolean | readonly string[]]>;
54
+ /** A request param: a named shape node plus its HTTP location (for generic dispatch). */
55
+ export declare const ManifestParamSchema: z.ZodObject<{
56
+ type: z.ZodString;
57
+ description: z.ZodOptional<z.ZodString>;
58
+ enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
59
+ itemEnum: z.ZodOptional<z.ZodArray<z.ZodString>>;
60
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
61
+ format: z.ZodOptional<z.ZodString>;
62
+ example: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
63
+ nullable: z.ZodOptional<z.ZodBoolean>;
64
+ minimum: z.ZodOptional<z.ZodNumber>;
65
+ maximum: z.ZodOptional<z.ZodNumber>;
66
+ exclusiveMinimum: z.ZodOptional<z.ZodNumber>;
67
+ exclusiveMaximum: z.ZodOptional<z.ZodNumber>;
68
+ minLength: z.ZodOptional<z.ZodNumber>;
69
+ maxLength: z.ZodOptional<z.ZodNumber>;
70
+ minItems: z.ZodOptional<z.ZodNumber>;
71
+ maxItems: z.ZodOptional<z.ZodNumber>;
72
+ pattern: z.ZodOptional<z.ZodString>;
73
+ fields: z.ZodOptional<z.ZodArray<z.ZodLazy<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>>>;
74
+ items: z.ZodOptional<z.ZodLazy<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>>;
75
+ variants: z.ZodOptional<z.ZodArray<z.ZodLazy<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>>>;
76
+ name: z.ZodString;
77
+ required: z.ZodBoolean;
78
+ in: z.ZodEnum<{
79
+ path: "path";
80
+ query: "query";
81
+ body: "body";
82
+ }>;
83
+ }, z.core.$strip>;
84
+ export type ManifestParam = z.infer<typeof ManifestParamSchema>;
85
+ /**
86
+ * One operation, carrying everything the CLI needs to build its command and
87
+ * dispatch generically: the `callPath` (command tree), the `httpMethod` + `path`
88
+ * template + `params[].in` + `bodyKey` (dispatch), and the recursive request/response
89
+ * shapes (`--help`). Mirrors the relevant subset of sdk-ts's `SdkReferenceEntry`.
90
+ */
91
+ export declare const ManifestOperationSchema: z.ZodObject<{
92
+ operationId: z.ZodString;
93
+ callPath: z.ZodArray<z.ZodString>;
94
+ summary: z.ZodString;
95
+ description: z.ZodOptional<z.ZodString>;
96
+ httpMethod: z.ZodEnum<{
97
+ get: "get";
98
+ post: "post";
99
+ put: "put";
100
+ patch: "patch";
101
+ delete: "delete";
102
+ }>;
103
+ path: z.ZodString;
104
+ bodyKey: z.ZodOptional<z.ZodString>;
105
+ requiredPermissions: z.ZodOptional<z.ZodArray<z.ZodString>>;
106
+ params: z.ZodArray<z.ZodObject<{
107
+ type: z.ZodString;
108
+ description: z.ZodOptional<z.ZodString>;
109
+ enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
110
+ itemEnum: z.ZodOptional<z.ZodArray<z.ZodString>>;
111
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
112
+ format: z.ZodOptional<z.ZodString>;
113
+ example: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
114
+ nullable: z.ZodOptional<z.ZodBoolean>;
115
+ minimum: z.ZodOptional<z.ZodNumber>;
116
+ maximum: z.ZodOptional<z.ZodNumber>;
117
+ exclusiveMinimum: z.ZodOptional<z.ZodNumber>;
118
+ exclusiveMaximum: z.ZodOptional<z.ZodNumber>;
119
+ minLength: z.ZodOptional<z.ZodNumber>;
120
+ maxLength: z.ZodOptional<z.ZodNumber>;
121
+ minItems: z.ZodOptional<z.ZodNumber>;
122
+ maxItems: z.ZodOptional<z.ZodNumber>;
123
+ pattern: z.ZodOptional<z.ZodString>;
124
+ fields: z.ZodOptional<z.ZodArray<z.ZodLazy<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>>>;
125
+ items: z.ZodOptional<z.ZodLazy<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>>;
126
+ variants: z.ZodOptional<z.ZodArray<z.ZodLazy<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>>>;
127
+ name: z.ZodString;
128
+ required: z.ZodBoolean;
129
+ in: z.ZodEnum<{
130
+ path: "path";
131
+ query: "query";
132
+ body: "body";
133
+ }>;
134
+ }, z.core.$strip>>;
135
+ response: z.ZodOptional<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>;
136
+ }, z.core.$strip>;
137
+ export type ManifestOperation = z.infer<typeof ManifestOperationSchema>;
138
+ export declare const ManifestResourceSchema: z.ZodObject<{
139
+ id: z.ZodString;
140
+ title: z.ZodString;
141
+ parent: z.ZodOptional<z.ZodString>;
142
+ order: z.ZodNumber;
143
+ domain: z.ZodOptional<z.ZodString>;
144
+ summary: z.ZodOptional<z.ZodString>;
145
+ description: z.ZodOptional<z.ZodString>;
146
+ object: z.ZodOptional<z.ZodObject<{
147
+ name: z.ZodString;
148
+ fields: z.ZodArray<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>;
149
+ }, z.core.$strip>>;
150
+ operationIds: z.ZodArray<z.ZodString>;
151
+ }, z.core.$strip>;
152
+ export type ManifestResource = z.infer<typeof ManifestResourceSchema>;
153
+ export declare const ManifestRelatedSchema: z.ZodObject<{
154
+ requires: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
155
+ type: z.ZodLiteral<"recipe">;
156
+ id: z.ZodString;
157
+ }, z.core.$strip>, z.ZodObject<{
158
+ type: z.ZodLiteral<"state">;
159
+ explanation: z.ZodString;
160
+ predicate: z.ZodOptional<z.ZodString>;
161
+ via: z.ZodOptional<z.ZodObject<{
162
+ type: z.ZodEnum<{
163
+ recipe: "recipe";
164
+ concept: "concept";
165
+ tutorial: "tutorial";
166
+ resource: "resource";
167
+ }>;
168
+ id: z.ZodString;
169
+ }, z.core.$strip>>;
170
+ }, z.core.$strip>]>>>;
171
+ variationOf: z.ZodOptional<z.ZodString>;
172
+ learnMore: z.ZodOptional<z.ZodArray<z.ZodObject<{
173
+ type: z.ZodEnum<{
174
+ recipe: "recipe";
175
+ concept: "concept";
176
+ tutorial: "tutorial";
177
+ resource: "resource";
178
+ }>;
179
+ id: z.ZodString;
180
+ }, z.core.$strip>>>;
181
+ }, z.core.$strip>;
182
+ export type ManifestRelated = z.infer<typeof ManifestRelatedSchema>;
183
+ export declare const ManifestRecipeSchema: z.ZodObject<{
184
+ id: z.ZodString;
185
+ title: z.ZodString;
186
+ goal: z.ZodString;
187
+ category: z.ZodString;
188
+ steps: z.ZodArray<z.ZodObject<{
189
+ operationId: z.ZodOptional<z.ZodString>;
190
+ }, z.core.$strip>>;
191
+ sdk: z.ZodObject<{
192
+ setup: z.ZodString;
193
+ main: z.ZodString;
194
+ }, z.core.$strip>;
195
+ python: z.ZodOptional<z.ZodObject<{
196
+ setup: z.ZodString;
197
+ main: z.ZodString;
198
+ }, z.core.$strip>>;
199
+ cli: z.ZodObject<{
200
+ setup: z.ZodString;
201
+ main: z.ZodString;
202
+ }, z.core.$strip>;
203
+ curl: z.ZodObject<{
204
+ setup: z.ZodString;
205
+ main: z.ZodString;
206
+ }, z.core.$strip>;
207
+ related: z.ZodOptional<z.ZodObject<{
208
+ requires: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
209
+ type: z.ZodLiteral<"recipe">;
210
+ id: z.ZodString;
211
+ }, z.core.$strip>, z.ZodObject<{
212
+ type: z.ZodLiteral<"state">;
213
+ explanation: z.ZodString;
214
+ predicate: z.ZodOptional<z.ZodString>;
215
+ via: z.ZodOptional<z.ZodObject<{
216
+ type: z.ZodEnum<{
217
+ recipe: "recipe";
218
+ concept: "concept";
219
+ tutorial: "tutorial";
220
+ resource: "resource";
221
+ }>;
222
+ id: z.ZodString;
223
+ }, z.core.$strip>>;
224
+ }, z.core.$strip>]>>>;
225
+ variationOf: z.ZodOptional<z.ZodString>;
226
+ learnMore: z.ZodOptional<z.ZodArray<z.ZodObject<{
227
+ type: z.ZodEnum<{
228
+ recipe: "recipe";
229
+ concept: "concept";
230
+ tutorial: "tutorial";
231
+ resource: "resource";
232
+ }>;
233
+ id: z.ZodString;
234
+ }, z.core.$strip>>>;
235
+ }, z.core.$strip>>;
236
+ }, z.core.$strip>;
237
+ export type ManifestRecipe = z.infer<typeof ManifestRecipeSchema>;
238
+ export declare const ManifestConceptSchema: z.ZodObject<{
239
+ id: z.ZodString;
240
+ title: z.ZodString;
241
+ domain: z.ZodString;
242
+ related: z.ZodArray<z.ZodString>;
243
+ body: z.ZodString;
244
+ }, z.core.$strip>;
245
+ export type ManifestConcept = z.infer<typeof ManifestConceptSchema>;
246
+ export declare const ManifestTutorialSchema: z.ZodObject<{
247
+ id: z.ZodString;
248
+ title: z.ZodString;
249
+ body: z.ZodNullable<z.ZodString>;
250
+ }, z.core.$strip>;
251
+ export type ManifestTutorial = z.infer<typeof ManifestTutorialSchema>;
252
+ export declare const ManifestDomainSchema: z.ZodObject<{
253
+ id: z.ZodString;
254
+ title: z.ZodString;
255
+ order: z.ZodNumber;
256
+ }, z.core.$strip>;
257
+ export type ManifestDomain = z.infer<typeof ManifestDomainSchema>;
258
+ export declare const ManifestSchema: z.ZodObject<{
259
+ formatVersion: z.ZodNumber;
260
+ hash: z.ZodString;
261
+ operations: z.ZodRecord<z.ZodString, z.ZodObject<{
262
+ operationId: z.ZodString;
263
+ callPath: z.ZodArray<z.ZodString>;
264
+ summary: z.ZodString;
265
+ description: z.ZodOptional<z.ZodString>;
266
+ httpMethod: z.ZodEnum<{
267
+ get: "get";
268
+ post: "post";
269
+ put: "put";
270
+ patch: "patch";
271
+ delete: "delete";
272
+ }>;
273
+ path: z.ZodString;
274
+ bodyKey: z.ZodOptional<z.ZodString>;
275
+ requiredPermissions: z.ZodOptional<z.ZodArray<z.ZodString>>;
276
+ params: z.ZodArray<z.ZodObject<{
277
+ type: z.ZodString;
278
+ description: z.ZodOptional<z.ZodString>;
279
+ enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
280
+ itemEnum: z.ZodOptional<z.ZodArray<z.ZodString>>;
281
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
282
+ format: z.ZodOptional<z.ZodString>;
283
+ example: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
284
+ nullable: z.ZodOptional<z.ZodBoolean>;
285
+ minimum: z.ZodOptional<z.ZodNumber>;
286
+ maximum: z.ZodOptional<z.ZodNumber>;
287
+ exclusiveMinimum: z.ZodOptional<z.ZodNumber>;
288
+ exclusiveMaximum: z.ZodOptional<z.ZodNumber>;
289
+ minLength: z.ZodOptional<z.ZodNumber>;
290
+ maxLength: z.ZodOptional<z.ZodNumber>;
291
+ minItems: z.ZodOptional<z.ZodNumber>;
292
+ maxItems: z.ZodOptional<z.ZodNumber>;
293
+ pattern: z.ZodOptional<z.ZodString>;
294
+ fields: z.ZodOptional<z.ZodArray<z.ZodLazy<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>>>;
295
+ items: z.ZodOptional<z.ZodLazy<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>>;
296
+ variants: z.ZodOptional<z.ZodArray<z.ZodLazy<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>>>;
297
+ name: z.ZodString;
298
+ required: z.ZodBoolean;
299
+ in: z.ZodEnum<{
300
+ path: "path";
301
+ query: "query";
302
+ body: "body";
303
+ }>;
304
+ }, z.core.$strip>>;
305
+ response: z.ZodOptional<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>;
306
+ }, z.core.$strip>>;
307
+ resources: z.ZodRecord<z.ZodString, z.ZodObject<{
308
+ id: z.ZodString;
309
+ title: z.ZodString;
310
+ parent: z.ZodOptional<z.ZodString>;
311
+ order: z.ZodNumber;
312
+ domain: z.ZodOptional<z.ZodString>;
313
+ summary: z.ZodOptional<z.ZodString>;
314
+ description: z.ZodOptional<z.ZodString>;
315
+ object: z.ZodOptional<z.ZodObject<{
316
+ name: z.ZodString;
317
+ fields: z.ZodArray<z.ZodType<ShapeNode, unknown, z.core.$ZodTypeInternals<ShapeNode, unknown>>>;
318
+ }, z.core.$strip>>;
319
+ operationIds: z.ZodArray<z.ZodString>;
320
+ }, z.core.$strip>>;
321
+ recipes: z.ZodRecord<z.ZodString, z.ZodObject<{
322
+ id: z.ZodString;
323
+ title: z.ZodString;
324
+ goal: z.ZodString;
325
+ category: z.ZodString;
326
+ steps: z.ZodArray<z.ZodObject<{
327
+ operationId: z.ZodOptional<z.ZodString>;
328
+ }, z.core.$strip>>;
329
+ sdk: z.ZodObject<{
330
+ setup: z.ZodString;
331
+ main: z.ZodString;
332
+ }, z.core.$strip>;
333
+ python: z.ZodOptional<z.ZodObject<{
334
+ setup: z.ZodString;
335
+ main: z.ZodString;
336
+ }, z.core.$strip>>;
337
+ cli: z.ZodObject<{
338
+ setup: z.ZodString;
339
+ main: z.ZodString;
340
+ }, z.core.$strip>;
341
+ curl: z.ZodObject<{
342
+ setup: z.ZodString;
343
+ main: z.ZodString;
344
+ }, z.core.$strip>;
345
+ related: z.ZodOptional<z.ZodObject<{
346
+ requires: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
347
+ type: z.ZodLiteral<"recipe">;
348
+ id: z.ZodString;
349
+ }, z.core.$strip>, z.ZodObject<{
350
+ type: z.ZodLiteral<"state">;
351
+ explanation: z.ZodString;
352
+ predicate: z.ZodOptional<z.ZodString>;
353
+ via: z.ZodOptional<z.ZodObject<{
354
+ type: z.ZodEnum<{
355
+ recipe: "recipe";
356
+ concept: "concept";
357
+ tutorial: "tutorial";
358
+ resource: "resource";
359
+ }>;
360
+ id: z.ZodString;
361
+ }, z.core.$strip>>;
362
+ }, z.core.$strip>]>>>;
363
+ variationOf: z.ZodOptional<z.ZodString>;
364
+ learnMore: z.ZodOptional<z.ZodArray<z.ZodObject<{
365
+ type: z.ZodEnum<{
366
+ recipe: "recipe";
367
+ concept: "concept";
368
+ tutorial: "tutorial";
369
+ resource: "resource";
370
+ }>;
371
+ id: z.ZodString;
372
+ }, z.core.$strip>>>;
373
+ }, z.core.$strip>>;
374
+ }, z.core.$strip>>;
375
+ concepts: z.ZodRecord<z.ZodString, z.ZodObject<{
376
+ id: z.ZodString;
377
+ title: z.ZodString;
378
+ domain: z.ZodString;
379
+ related: z.ZodArray<z.ZodString>;
380
+ body: z.ZodString;
381
+ }, z.core.$strip>>;
382
+ tutorials: z.ZodArray<z.ZodObject<{
383
+ id: z.ZodString;
384
+ title: z.ZodString;
385
+ body: z.ZodNullable<z.ZodString>;
386
+ }, z.core.$strip>>;
387
+ domains: z.ZodArray<z.ZodObject<{
388
+ id: z.ZodString;
389
+ title: z.ZodString;
390
+ order: z.ZodNumber;
391
+ }, z.core.$strip>>;
392
+ }, z.core.$strip>;
393
+ export type Manifest = z.infer<typeof ManifestSchema>;
394
+ /**
395
+ * Validate a fetched/cached manifest at the I/O boundary and check engine↔manifest
396
+ * format compatibility. A version mismatch is the one case worth an actionable
397
+ * message: the schema itself stays lenient (so additive server changes are safe),
398
+ * and only a deliberate format bump trips this.
399
+ */
400
+ export declare function parseManifest(value: unknown, source: string): Manifest;
401
+ /**
402
+ * Fetch the command manifest, revalidating the cache on every run:
403
+ * - send `If-None-Match` with the cached ETag → `304` means the cache is provably
404
+ * current (use it); `200` means the surface changed (validate + replace cache).
405
+ * - a network error (incl. a timeout) falls back to the cached copy with a warning;
406
+ * 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.
409
+ */
410
+ export declare function fetchManifest(baseUrl: string, apiKey: string): Promise<Manifest>;