orizu 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +1418 -13
  2. package/package.json +1 -1
  3. package/src/index.ts +1849 -22
package/src/index.ts CHANGED
@@ -1,10 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { createHash, randomBytes } from 'crypto'
3
- import { basename, extname } from 'path'
4
- import { pathToFileURL } from 'url'
2
+ import { createHash, randomBytes, randomUUID } from 'crypto'
3
+ import { basename, delimiter, dirname, extname, isAbsolute, join, normalize } from 'path'
4
+ import { fileURLToPath } from 'url'
5
5
  import { createServer } from 'http'
6
- import { readFileSync, statSync, writeFileSync } from 'fs'
7
- import { spawn } from 'child_process'
6
+ import { gunzipSync, gzipSync } from 'zlib'
7
+ import {
8
+ existsSync,
9
+ mkdirSync,
10
+ mkdtempSync,
11
+ readdirSync,
12
+ readFileSync,
13
+ realpathSync,
14
+ rmSync,
15
+ statSync,
16
+ writeFileSync,
17
+ } from 'fs'
18
+ import { spawn, spawnSync } from 'child_process'
19
+ import { tmpdir } from 'os'
8
20
  import { createInterface } from 'readline/promises'
9
21
  import { stdin as input, stdout as output } from 'process'
10
22
  import { clearServerCredentials, getServerCredentials, saveServerCredentials } from './credentials.js'
@@ -80,6 +92,41 @@ interface DatasetSelection {
80
92
  name?: string
81
93
  }
82
94
 
95
+ interface PromptSummary {
96
+ id: string
97
+ name: string
98
+ role: string
99
+ description?: string | null
100
+ }
101
+
102
+ interface ScorerSummary {
103
+ id: string
104
+ name: string
105
+ mode: string
106
+ metricLabel: string
107
+ implementationKind: string
108
+ }
109
+
110
+ interface RunnerExecContext {
111
+ prompt: {
112
+ body: string | null
113
+ bodyKind: string
114
+ providerSettings: Record<string, unknown>
115
+ promptId?: string
116
+ promptVersionId: string
117
+ runnerVersionId: string
118
+ }
119
+ scorer?: {
120
+ versionId: string
121
+ metricKey: string
122
+ higherIsBetter: boolean
123
+ } | null
124
+ rows: Array<{
125
+ id: string
126
+ row: Record<string, unknown>
127
+ }>
128
+ }
129
+
83
130
  type DatasetUploadSourceType = 'csv' | 'json' | 'jsonl'
84
131
 
85
132
  interface DatasetUploadResponse {
@@ -153,7 +200,11 @@ function printVersion() {
153
200
  }
154
201
 
155
202
  function printUsage() {
156
- printLine(`orizu global options:\n\n --local Use http://localhost:3000\n --server <url> Use a specific server origin (for example: https://preview.example.com)\n --version, -v Print the orizu CLI version\n\norizu commands:\n\n orizu login\n orizu logout\n orizu whoami\n orizu teams list\n orizu teams create [--name <name>]\n orizu teams members list [--team <teamSlug>]\n orizu teams members add --email <email> [--team <teamSlug>]\n orizu teams members remove --email <email> [--team <teamSlug>]\n orizu teams members role --team <teamSlug> --email <email> --role <admin|member>\n orizu projects list [--team <teamSlug>]\n orizu projects create --name <name> [--team <teamSlug>]\n orizu apps list [--project <team/project>]\n orizu apps create --project <team/project> --name <name> --dataset <datasetId> --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps update [--app <appId>] [--project <team/project>] --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps link-dataset --dataset <datasetId> [--app <appId>] [--project <team/project>] [--version <n>]\n orizu apps detail --app <appId> [--project <team/project>] [--json]\n orizu tasks list [--project <team/project>]\n orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> --assignees <userIdOrEmail1,userIdOrEmail2> [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]\n orizu tasks assign --task <taskId> --assignees <userId1,userId2>\n orizu tasks status --task <taskId> [--json]\n orizu tasks pause --task <taskId>\n orizu tasks unpause --task <taskId>\n orizu datasets upload --file <path> [--project <team/project>] [--name <name>]\n orizu datasets download [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--format <csv|json|jsonl>] [--out <path>]\n orizu datasets append [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>\n orizu datasets edit-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>\n orizu datasets delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --row-ids <id1,id2>\n orizu datasets delete [--dataset <datasetId|datasetUrl>] [--project <team/project>]\n orizu datasets lock [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--reason <text>]\n orizu datasets clone [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--name <name>]\n orizu tasks export [--task <taskId>] [--format <csv|json|jsonl>] [--out <path>]`)
203
+ printLine(`orizu global options:\n\n --local Use http://localhost:3000\n --server <url> Use a specific server origin (for example: https://preview.example.com)\n --version, -v Print the orizu CLI version\n\norizu commands:\n\n orizu login [--no-prompt-if-logged-in]\n orizu logout\n orizu whoami\n orizu env [--project <team/project>] [--project-id <projectId>]\n orizu log <event_type> --run-id <id> --sequence <n> --payload @event.json\n orizu teams list\n orizu teams create [--name <name>]\n orizu teams members list [--team <teamSlug>]\n orizu teams members add --email <email> [--team <teamSlug>]\n orizu teams members remove --email <email> [--team <teamSlug>]\n orizu teams members role --team <teamSlug> --email <email> --role <admin|member>\n orizu projects list [--team <teamSlug>]\n orizu projects create --name <name> [--team <teamSlug>]\n orizu prompts list --project <team/project>\n orizu prompts pull <prompt-id-or-name> --project <team/project> --out <dir> [--label <label> | --version <id>] [--json]\n orizu prompts push <dir> [--runner-version <id>] [--project <team/project>] [--parent <version-id>] [--json]\n orizu prompts labels set <prompt-name> <label> --version <version-id> [--project <team/project>] [--json]\n orizu prompts scorers set-headline <prompt-id> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]\n orizu prompts scorers add <prompt-id> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]\n orizu scorers list --project <team/project>\n orizu scorers register --project <team/project> --name <name> --manifest <manifest.json> [--prompt-version <id>] [--runner-version <id>] [--label <label>] [--json]\n orizu scorers detail <scorer-id-or-name> --project <team/project> [--json]\n orizu scorers labels set <scorer-name> <label> --version <scorer-version-id> [--project <team/project>] [--json]\n orizu scores submit <results.jsonl|results.json> --scorer-version <id> --subject-version <prompt-version-id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]\n orizu judges list --project <team/project>\n orizu judges pull <judge-id-or-name> --project <team/project> --out <dir> [--label <label> | --version <id>] [--json]\n orizu judges push <dir> [--runner-version <id>] [--project <team/project>] [--parent <version-id>] [--json]\n orizu runners push <dir> [--project <team/project>] [--name <name>] [--label <label>] [--json]\n orizu runners exec (--prompt <prompt-version-id> | --prompt-version <id> --runner-version <id> | --scorer-version <id>) --dataset-version <id> --split-set <id-or-name> --split <name> [--runner-dir <dir>] --out <results.jsonl|results.jsonl.gz>\n orizu optimizers push <dir> [--project <team/project>] [--name <name>] [--label <label>] [--json]\n orizu runs submit <results.jsonl|results.jsonl.gz> --prompt-version <id> --runner-version <id> --dataset-version <id> --split-set <id> --split <name> [--project <team/project>]\n orizu apps list [--project <team/project>]\n orizu apps create --project <team/project> --name <name> --dataset <datasetId> --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps update [--app <appId>] [--project <team/project>] --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps link-dataset --dataset <datasetId> [--app <appId>] [--project <team/project>] [--version <n>]\n orizu apps detail --app <appId> [--project <team/project>] [--json]\n orizu tasks list [--project <team/project>]\n orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> --assignees <userIdOrEmail1,userIdOrEmail2> [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]\n orizu tasks assign --task <taskId> --assignees <userId1,userId2>\n orizu tasks status --task <taskId> [--json]\n orizu tasks pause --task <taskId>\n orizu tasks unpause --task <taskId>\n orizu datasets upload --file <path> [--project <team/project>] [--name <name>]\n orizu datasets push <path> [--project <team/project>] [--name <name>] [--json]\n orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--json]\n orizu datasets splits create <datasetVersionId> [--from-file <split.json>] [--json]\n orizu datasets download [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--format <csv|json|jsonl>] [--out <path>]\n orizu datasets append [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>\n orizu datasets edit-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>\n orizu datasets delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --row-ids <id1,id2>\n orizu datasets delete [--dataset <datasetId|datasetUrl>] [--project <team/project>]\n orizu datasets lock [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--reason <text>]\n orizu datasets clone [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--name <name>]\n orizu tasks export [--task <taskId>] [--format <csv|json|jsonl>] [--out <path>]`)
204
+ }
205
+
206
+ function printOptimizationUsage() {
207
+ printLine(`\nOptimization lifecycle commands:\n\n orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]\n orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation]\n orizu optimizations pause <run-id> [--reason <text>] [--json]\n orizu optimizations resume <run-id> [--json]\n orizu optimizations finish <run-id> [--best-score <n>] [--best-candidate <id>] [--result-prompt-version <id>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--json]`)
157
208
  }
158
209
 
159
210
  let cliArgs = process.argv.slice(2)
@@ -288,6 +339,92 @@ export async function parseJsonResponse<T>(response: Response, context: string):
288
339
  )
289
340
  }
290
341
  }
342
+
343
+ function shellQuote(value: string): string {
344
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
345
+ }
346
+
347
+ function getStoredAuthTokenForBaseUrl(baseUrl: string): string {
348
+ const credentials = getServerCredentials(baseUrl)
349
+ if (!credentials) {
350
+ throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`)
351
+ }
352
+
353
+ return 'accessToken' in credentials ? credentials.accessToken : credentials.apiKey
354
+ }
355
+
356
+ function printPromptSummaries(items: PromptSummary[], emptyMessage: string) {
357
+ if (items.length === 0) {
358
+ printLine(emptyMessage)
359
+ return
360
+ }
361
+
362
+ const rows = items.map(item => ({
363
+ id: sanitizeTerminalText(item.id),
364
+ name: sanitizeTerminalText(item.name),
365
+ role: sanitizeTerminalText(item.role),
366
+ }))
367
+ const idWidth = Math.max('ID'.length, ...rows.map(row => row.id.length))
368
+ const nameWidth = Math.max('NAME'.length, ...rows.map(row => row.name.length))
369
+ const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length))
370
+
371
+ printLine(`${'ID'.padEnd(idWidth)} ${'NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`)
372
+ printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`)
373
+ rows.forEach(row => {
374
+ printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`)
375
+ })
376
+ }
377
+
378
+ function printScorerSummaries(items: ScorerSummary[]) {
379
+ if (items.length === 0) {
380
+ printLine('No scorers found.')
381
+ return
382
+ }
383
+
384
+ const rows = items.map(item => ({
385
+ id: sanitizeTerminalText(item.id),
386
+ name: sanitizeTerminalText(item.name),
387
+ mode: sanitizeTerminalText(item.mode),
388
+ metric: sanitizeTerminalText(item.metricLabel),
389
+ implementation: sanitizeTerminalText(item.implementationKind),
390
+ }))
391
+ const idWidth = Math.max('ID'.length, ...rows.map(row => row.id.length))
392
+ const nameWidth = Math.max('NAME'.length, ...rows.map(row => row.name.length))
393
+ const modeWidth = Math.max('MODE'.length, ...rows.map(row => row.mode.length))
394
+ const metricWidth = Math.max('METRIC'.length, ...rows.map(row => row.metric.length))
395
+
396
+ printLine(`${'ID'.padEnd(idWidth)} ${'NAME'.padEnd(nameWidth)} ${'MODE'.padEnd(modeWidth)} ${'METRIC'.padEnd(metricWidth)} IMPLEMENTATION`)
397
+ printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(modeWidth)} ${'-'.repeat(metricWidth)} ${'-'.repeat('IMPLEMENTATION'.length)}`)
398
+ rows.forEach(row => {
399
+ printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.mode.padEnd(modeWidth)} ${row.metric.padEnd(metricWidth)} ${row.implementation}`)
400
+ })
401
+ }
402
+
403
+ function readJsonObjectArg(valueArg: string | null, label: string): Record<string, unknown> {
404
+ if (!valueArg) {
405
+ return {}
406
+ }
407
+
408
+ const trimmed = valueArg.trim()
409
+ const raw = trimmed.startsWith('{')
410
+ ? trimmed
411
+ : readSourceFile(trimmed.startsWith('@') ? trimmed.slice(1) : trimmed)
412
+
413
+ try {
414
+ const parsed = JSON.parse(raw) as unknown
415
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
416
+ throw new Error('JSON root must be an object')
417
+ }
418
+ return parsed as Record<string, unknown>
419
+ } catch (error: any) {
420
+ throw new Error(`Invalid ${label} '${valueArg}': ${error?.message || String(error)}`)
421
+ }
422
+ }
423
+
424
+ function readJsonPayloadArg(pathArg: string | null): Record<string, unknown> {
425
+ return readJsonObjectArg(pathArg, 'event payload')
426
+ }
427
+
291
428
  async function promptSelect<T>(
292
429
  title: string,
293
430
  items: T[],
@@ -392,7 +529,8 @@ async function fetchTeamMembers(teamSlug: string): Promise<TeamMember[]> {
392
529
  return data.members
393
530
  }
394
531
 
395
- async function resolveProjectSlug(projectArg: string | null): Promise<string> {
532
+ async function resolveProjectSelection(projectArg: string | null): Promise<Project> {
533
+ projectArg = projectArg || process.env.ORIZU_PROJECT || null
396
534
  const teams = await fetchTeams()
397
535
 
398
536
  if (teams.length === 0) {
@@ -415,7 +553,7 @@ async function resolveProjectSlug(projectArg: string | null): Promise<string> {
415
553
  { forcePrompt: true }
416
554
  )
417
555
 
418
- return `${project.teamSlug}/${project.slug}`
556
+ return project
419
557
  }
420
558
 
421
559
  const segments = projectArg.split('/')
@@ -440,7 +578,7 @@ async function resolveProjectSlug(projectArg: string | null): Promise<string> {
440
578
  project => `${project.name} (${project.teamSlug}/${project.slug})`
441
579
  )
442
580
 
443
- return `${selectedProject.teamSlug}/${selectedProject.slug}`
581
+ return selectedProject
444
582
  }
445
583
 
446
584
  const projects = await fetchProjects(matchedTeam.slug)
@@ -454,10 +592,15 @@ async function resolveProjectSlug(projectArg: string | null): Promise<string> {
454
592
  project => `${project.name} (${project.teamSlug}/${project.slug})`
455
593
  )
456
594
 
457
- return `${selectedProject.teamSlug}/${selectedProject.slug}`
595
+ return selectedProject
458
596
  }
459
597
 
460
- return `${matchedTeam.slug}/${matchedProject.slug}`
598
+ return matchedProject
599
+ }
600
+
601
+ async function resolveProjectSlug(projectArg: string | null): Promise<string> {
602
+ const project = await resolveProjectSelection(projectArg)
603
+ return `${project.teamSlug}/${project.slug}`
461
604
  }
462
605
 
463
606
  async function selectTaskIdInteractively(): Promise<string> {
@@ -820,8 +963,14 @@ function renderCliAuthBrowserPage(status: 'success' | 'error'): string {
820
963
  }
821
964
 
822
965
  async function login() {
823
- const baseUrl = resolveLoginBaseUrl()
966
+ const baseUrl = hasArg('--no-prompt-if-logged-in') ? getBaseUrl() : resolveLoginBaseUrl()
824
967
  assertSecureTokenTransport(baseUrl)
968
+
969
+ if (hasArg('--no-prompt-if-logged-in') && getServerCredentials(baseUrl)) {
970
+ printLine(`Already logged in to ${sanitizeTerminalText(baseUrl)}.`)
971
+ return
972
+ }
973
+
825
974
  const codeVerifier = createCodeVerifier()
826
975
  const codeChallenge = createCodeChallenge(codeVerifier)
827
976
  const callbackPort = resolveAuthCallbackPort()
@@ -947,19 +1096,1033 @@ async function logout() {
947
1096
  : undefined,
948
1097
  })
949
1098
 
950
- if (!response.ok) {
951
- remoteLogoutError = sanitizeTerminalText(await response.text()).slice(0, 180)
1099
+ if (!response.ok) {
1100
+ remoteLogoutError = sanitizeTerminalText(await response.text()).slice(0, 180)
1101
+ }
1102
+ } catch (error) {
1103
+ const message = error instanceof Error ? error.message : String(error)
1104
+ remoteLogoutError = sanitizeTerminalText(message).slice(0, 180)
1105
+ }
1106
+
1107
+ clearServerCredentials(baseUrl)
1108
+ if (remoteLogoutError) {
1109
+ console.warn(`Warning: remote logout failed: ${remoteLogoutError}`)
1110
+ }
1111
+ printLine(`Logged out from ${sanitizeTerminalText(baseUrl)}.`)
1112
+ }
1113
+
1114
+ async function printEnv() {
1115
+ const baseUrl = getBaseUrl()
1116
+ const token = getStoredAuthTokenForBaseUrl(baseUrl)
1117
+ const projectArg = getArg('--project')
1118
+ const project = projectArg ? await resolveProjectSelection(projectArg) : null
1119
+ const projectId = project?.id || getArg('--project-id') || process.env.ORIZU_PROJECT_ID || ''
1120
+ const projectSlug = project ? `${project.teamSlug}/${project.slug}` : process.env.ORIZU_PROJECT || ''
1121
+
1122
+ printLine(`export ORIZU_API_URL=${shellQuote(baseUrl)}`)
1123
+ printLine(`export ORIZU_TOKEN=${shellQuote(token)}`)
1124
+ printLine(`export ORIZU_PROJECT_ID=${shellQuote(projectId)}`)
1125
+ if (projectSlug) {
1126
+ printLine(`export ORIZU_PROJECT=${shellQuote(projectSlug)}`)
1127
+ }
1128
+ }
1129
+
1130
+ async function logOptimizationEvent() {
1131
+ const eventType = cliArgs[1]
1132
+ const runId = getArg('--run-id')
1133
+ const sequenceArg = getArg('--sequence')
1134
+ const payload = readJsonPayloadArg(getArg('--payload'))
1135
+ const sequence = sequenceArg ? Number(sequenceArg) : Number.NaN
1136
+
1137
+ if (!eventType || eventType.startsWith('--') || !runId || !Number.isInteger(sequence) || sequence <= 0) {
1138
+ throw new Error('Usage: orizu log <event_type> --run-id <id> --sequence <n> --payload @event.json')
1139
+ }
1140
+
1141
+ const response = await authedFetch(`/api/cli/optimization-runs/${encodeURIComponent(runId)}/events`, {
1142
+ method: 'POST',
1143
+ headers: { 'Content-Type': 'application/json' },
1144
+ body: JSON.stringify({
1145
+ eventId: getArg('--event-id') || randomUUID(),
1146
+ sequence,
1147
+ eventType,
1148
+ eventLayer: getArg('--event-layer') || 'core',
1149
+ optimizerFamily: getArg('--optimizer-family'),
1150
+ iteration: getArg('--iteration') ? Number(getArg('--iteration')) : undefined,
1151
+ candidateId: getArg('--candidate-id'),
1152
+ parentCandidateId: getArg('--parent-candidate-id'),
1153
+ childCandidateId: getArg('--child-candidate-id'),
1154
+ payload,
1155
+ }),
1156
+ })
1157
+
1158
+ if (!response.ok) {
1159
+ throw new Error(`Failed to log event: ${await response.text()}`)
1160
+ }
1161
+
1162
+ const data = await parseJsonResponse<{ eventId: string }>(response, 'Optimization event log')
1163
+ printLine(`Logged event ${sanitizeTerminalText(data.eventId)}`)
1164
+ }
1165
+
1166
+ async function listPrompts() {
1167
+ const project = getArg('--project') || await resolveProjectSlug(null)
1168
+ const response = await authedFetch(`/api/cli/prompts?project=${encodeURIComponent(project)}`)
1169
+ if (!response.ok) {
1170
+ throw new Error(`Failed to fetch prompts: ${await response.text()}`)
1171
+ }
1172
+
1173
+ const data = await parseJsonResponse<{ prompts: PromptSummary[] }>(response, 'Prompts list')
1174
+ printPromptSummaries(data.prompts, 'No prompts found.')
1175
+ }
1176
+
1177
+ async function listJudges() {
1178
+ const project = getArg('--project') || await resolveProjectSlug(null)
1179
+ const response = await authedFetch(`/api/cli/judges?project=${encodeURIComponent(project)}`)
1180
+ if (!response.ok) {
1181
+ throw new Error(`Failed to fetch judges: ${await response.text()}`)
1182
+ }
1183
+
1184
+ const data = await parseJsonResponse<{ judges: PromptSummary[] }>(response, 'Judges list')
1185
+ printPromptSummaries(data.judges, 'No judges found.')
1186
+ }
1187
+
1188
+ async function listScorers() {
1189
+ const project = getArg('--project') || await resolveProjectSlug(null)
1190
+ const response = await authedFetch(`/api/cli/scorers?project=${encodeURIComponent(project)}`)
1191
+ if (!response.ok) {
1192
+ throw new Error(`Failed to fetch scorers: ${await response.text()}`)
1193
+ }
1194
+
1195
+ const data = await parseJsonResponse<{ scorers: ScorerSummary[] }>(response, 'Scorers list')
1196
+ printScorerSummaries(data.scorers)
1197
+ }
1198
+
1199
+ async function registerScorer() {
1200
+ const project = getArg('--project') || await resolveProjectSlug(null)
1201
+ const name = getArg('--name')
1202
+ const manifestPath = getArg('--manifest')
1203
+
1204
+ if (!name || !manifestPath) {
1205
+ throw new Error('Usage: orizu scorers register --project <team/project> --name <name> --manifest <manifest.json> [--prompt-version <id>] [--runner-version <id>] [--label <label>] [--json]')
1206
+ }
1207
+
1208
+ const manifest = readJsonFile(manifestPath)
1209
+ const response = await authedFetch(`/api/cli/scorers?project=${encodeURIComponent(project)}`, {
1210
+ method: 'POST',
1211
+ headers: { 'Content-Type': 'application/json' },
1212
+ body: JSON.stringify({
1213
+ name,
1214
+ manifest,
1215
+ promptVersionId: getArg('--prompt-version') || undefined,
1216
+ runnerVersionId: getArg('--runner-version') || undefined,
1217
+ label: getArg('--label') || undefined,
1218
+ }),
1219
+ })
1220
+
1221
+ if (!response.ok) {
1222
+ throw new Error(`Failed to register scorer: ${await response.text()}`)
1223
+ }
1224
+
1225
+ const data = await parseJsonResponse<Record<string, unknown>>(response, 'Scorer register')
1226
+ if (hasJsonFlag()) {
1227
+ printJson(data)
1228
+ return
1229
+ }
1230
+
1231
+ printLine(`Registered scorer ${sanitizeTerminalText(name)} (${sanitizeTerminalText(String(data.scorer_version_id || 'unknown version'))})`)
1232
+ }
1233
+
1234
+ async function showScorerDetail() {
1235
+ const scorerId = getPositionalArg(2)
1236
+ const project = getArg('--project') || await resolveProjectSlug(null)
1237
+ if (!scorerId) {
1238
+ throw new Error('Usage: orizu scorers detail <scorer-id-or-name> --project <team/project> [--json]')
1239
+ }
1240
+
1241
+ const response = await authedFetch(`/api/cli/scorers/${encodeURIComponent(scorerId)}?project=${encodeURIComponent(project)}`)
1242
+ if (!response.ok) {
1243
+ throw new Error(`Failed to fetch scorer: ${await response.text()}`)
1244
+ }
1245
+
1246
+ const data = await parseJsonResponse<Record<string, unknown>>(response, 'Scorer detail')
1247
+ if (hasJsonFlag()) {
1248
+ printJson(data)
1249
+ return
1250
+ }
1251
+
1252
+ const scorer = data.scorer as Record<string, unknown> | undefined
1253
+ printLine(`${sanitizeTerminalText(String(scorer?.name || scorerId))}`)
1254
+ printLine(`Metric: ${sanitizeTerminalText(String(scorer?.metricLabel || 'Score'))}`)
1255
+ }
1256
+
1257
+ async function setScorerLabel() {
1258
+ const scorerName = getPositionalArg(3)
1259
+ const label = getPositionalArg(4)
1260
+ const project = getArg('--project') || await resolveProjectSlug(null)
1261
+ const scorerVersionId = getArg('--version')
1262
+
1263
+ if (!scorerName || !label || !scorerVersionId) {
1264
+ throw new Error('Usage: orizu scorers labels set <scorer-name> <label> --version <scorer-version-id> [--project <team/project>] [--json]')
1265
+ }
1266
+
1267
+ const response = await authedFetch(`/api/cli/scorers/labels?project=${encodeURIComponent(project)}`, {
1268
+ method: 'POST',
1269
+ headers: { 'Content-Type': 'application/json' },
1270
+ body: JSON.stringify({ scorerName, label, scorerVersionId }),
1271
+ })
1272
+
1273
+ if (!response.ok) {
1274
+ throw new Error(`Failed to set scorer label: ${await response.text()}`)
1275
+ }
1276
+
1277
+ const data = await parseJsonResponse<Record<string, unknown>>(response, 'Scorer label set')
1278
+ if (hasJsonFlag()) {
1279
+ printJson(data)
1280
+ return
1281
+ }
1282
+
1283
+ printLine(`Moved ${sanitizeTerminalText(label)} to ${sanitizeTerminalText(scorerVersionId)}`)
1284
+ }
1285
+
1286
+ async function pushRunnerArtifact(kind: 'runner' | 'optimizer') {
1287
+ const artifactDir = getPositionalArg(2)
1288
+ const project = getArg('--project') || await resolveProjectSlug(null)
1289
+ const name = getArg('--name')
1290
+ const label = getArg('--label') || undefined
1291
+
1292
+ if (!artifactDir) {
1293
+ throw new Error(`Usage: orizu ${kind === 'runner' ? 'runners' : 'optimizers'} push <dir> --project <team/project> [--name <name>] [--label <label>] [--json]`)
1294
+ }
1295
+
1296
+ const manifest = readManifestFile(artifactDir)
1297
+ const artifactName = name || (typeof manifest.name === 'string' ? manifest.name : basename(expandHomePath(artifactDir)))
1298
+ const description = typeof manifest.description === 'string' ? manifest.description : undefined
1299
+ const { zipBase64, contentSha256 } = zipDirectoryToBase64(artifactDir)
1300
+ const endpoint = kind === 'runner' ? 'runners' : 'optimizers'
1301
+ const response = await authedFetch(`/api/cli/${endpoint}?project=${encodeURIComponent(project)}`, {
1302
+ method: 'POST',
1303
+ headers: { 'Content-Type': 'application/json' },
1304
+ body: JSON.stringify({
1305
+ name: artifactName,
1306
+ description,
1307
+ label,
1308
+ manifest,
1309
+ zipBase64,
1310
+ contentSha256,
1311
+ }),
1312
+ })
1313
+
1314
+ if (!response.ok) {
1315
+ throw new Error(`Failed to push ${kind}: ${await response.text()}`)
1316
+ }
1317
+
1318
+ const data = await parseJsonResponse<Record<string, unknown>>(response, `${kind} push`)
1319
+ if (hasJsonFlag()) {
1320
+ printJson(data)
1321
+ return
1322
+ }
1323
+
1324
+ const versionId = kind === 'runner' ? data.runner_version_id : data.optimizer_version_id
1325
+ printLine(`Pushed ${kind} ${sanitizeTerminalText(String(artifactName))} (${sanitizeTerminalText(String(versionId || 'unknown version'))})`)
1326
+ }
1327
+
1328
+ async function pushPromptArtifact(kind: 'prompt' | 'judge') {
1329
+ const promptDir = getPositionalArg(2)
1330
+ const project = getArg('--project') || await resolveProjectSlug(null)
1331
+ const runnerVersionArg = getArg('--runner-version')
1332
+ const parentVersionId = getArg('--parent') || undefined
1333
+
1334
+ if (!promptDir) {
1335
+ throw new Error(`Usage: orizu ${kind === 'judge' ? 'judges' : 'prompts'} push <dir> --project <team/project> [--runner-version <id>] [--parent <version-id>] [--json]`)
1336
+ }
1337
+
1338
+ const promptRoot = expandHomePath(promptDir)
1339
+ const manifest = readJsonFile(join(promptRoot, 'orizu.prompt.json'))
1340
+ const primaryText = readPromptPrimaryText(manifest, promptRoot)
1341
+ const sidecars = readPromptSidecars(manifest, promptRoot)
1342
+ const runnerFromManifest = stringFromRecord(manifest, 'runner_version_id') ||
1343
+ (isRecord(manifest.runner) ? stringFromRecord(manifest.runner, 'version_id') : undefined)
1344
+ const runnerVersionId = runnerVersionArg || runnerFromManifest
1345
+ if (!runnerVersionId) {
1346
+ throw new Error(`Usage: orizu ${kind === 'judge' ? 'judges' : 'prompts'} push <dir> --project <team/project> [--runner-version <id>] [--parent <version-id>] [--json]`)
1347
+ }
1348
+ const baseBundle = isRecord(manifest.bundle) ? manifest.bundle : {}
1349
+ const endpoint = kind === 'judge' ? 'judges' : 'prompts'
1350
+ const response = await authedFetch(`/api/cli/${endpoint}?project=${encodeURIComponent(project)}`, {
1351
+ method: 'POST',
1352
+ headers: { 'Content-Type': 'application/json' },
1353
+ body: JSON.stringify({
1354
+ name: manifest.name,
1355
+ role: kind === 'judge' && manifest.role === undefined ? 'judge_per_row' : manifest.role,
1356
+ description: manifest.description,
1357
+ body: primaryText.body,
1358
+ bodyKind: primaryText.bodyKind,
1359
+ providerSettings: manifest.provider_settings || {},
1360
+ bundle: {
1361
+ ...baseBundle,
1362
+ tags: manifest.tags || [],
1363
+ provenance: manifest.provenance || {},
1364
+ primaryText: {
1365
+ path: primaryText.path,
1366
+ kind: primaryText.bodyKind,
1367
+ },
1368
+ sidecars,
1369
+ },
1370
+ runnerVersionId,
1371
+ parentVersionId,
1372
+ versionLabel: manifest.version_label,
1373
+ createdBy: manifest.provenance || { kind: 'human-edit' },
1374
+ }),
1375
+ })
1376
+
1377
+ if (!response.ok) {
1378
+ throw new Error(`Failed to push ${kind}: ${await response.text()}`)
1379
+ }
1380
+
1381
+ const data = await parseJsonResponse<Record<string, unknown>>(response, `${kind} push`)
1382
+ if (hasJsonFlag()) {
1383
+ printJson(data)
1384
+ return
1385
+ }
1386
+
1387
+ printLine(`Pushed ${kind} ${sanitizeTerminalText(String(manifest.name || promptDir))} (${sanitizeTerminalText(String(data.prompt_version_id || 'unknown version'))})`)
1388
+ }
1389
+
1390
+ async function pullPromptArtifact(kind: 'prompt' | 'judge') {
1391
+ const promptRef = getPositionalArg(2)
1392
+ const project = getArg('--project') || await resolveProjectSlug(null)
1393
+ const outDir = getArg('--out')
1394
+ const label = getArg('--label')
1395
+ const version = getArg('--version')
1396
+
1397
+ if (!promptRef || !outDir) {
1398
+ throw new Error(`Usage: orizu ${kind === 'judge' ? 'judges' : 'prompts'} pull <prompt-id-or-name> --project <team/project> --out <dir> [--label <label> | --version <version-id>] [--json]`)
1399
+ }
1400
+ if (label && version) {
1401
+ throw new Error('Use either --label or --version, not both')
1402
+ }
1403
+
1404
+ const params = new URLSearchParams({ project })
1405
+ if (label) params.set('label', label)
1406
+ if (version) params.set('version', version)
1407
+
1408
+ const endpoint = kind === 'judge' ? 'judges' : 'prompts'
1409
+ const response = await authedFetch(`/api/cli/${endpoint}/${encodeURIComponent(promptRef)}?${params.toString()}`)
1410
+ if (!response.ok) {
1411
+ throw new Error(`Failed to pull ${kind}: ${await response.text()}`)
1412
+ }
1413
+
1414
+ const data = await parseJsonResponse<{
1415
+ prompt: {
1416
+ id: string
1417
+ name: string
1418
+ role: string
1419
+ description?: string | null
1420
+ }
1421
+ version: {
1422
+ id: string
1423
+ versionNumber?: number
1424
+ versionLabel?: string | null
1425
+ body?: string | null
1426
+ bodyKind?: string | null
1427
+ providerSettings?: Record<string, unknown>
1428
+ runnerVersionId?: string | null
1429
+ bundle?: Record<string, unknown>
1430
+ }
1431
+ labels?: Array<{ label: string; promptVersionId: string }>
1432
+ }>(response, `${kind} pull`)
1433
+
1434
+ const targetDir = expandHomePath(outDir)
1435
+ mkdirSync(targetDir, { recursive: true })
1436
+
1437
+ const bundle = isRecord(data.version.bundle) ? data.version.bundle : {}
1438
+ const bundlePrimaryText = isRecord(bundle.primaryText)
1439
+ ? bundle.primaryText
1440
+ : isRecord(bundle.primary_text)
1441
+ ? bundle.primary_text
1442
+ : null
1443
+ const primaryPath = safeRelativePath(
1444
+ stringFromRecord(bundlePrimaryText || {}, 'path') || 'prompt.md',
1445
+ 'prompt.md'
1446
+ )
1447
+ const bodyKind = stringFromRecord(bundlePrimaryText || {}, 'kind') || data.version.bodyKind || 'text'
1448
+
1449
+ writeTextFileEnsuringDir(join(targetDir, primaryPath), data.version.body || '')
1450
+
1451
+ const exportedSidecars: Array<Record<string, unknown>> = []
1452
+ const sidecars = Array.isArray(bundle.sidecars) ? bundle.sidecars : []
1453
+ for (const rawSidecar of sidecars) {
1454
+ if (!isRecord(rawSidecar)) continue
1455
+ const sidecarPathValue = stringFromRecord(rawSidecar, 'path')
1456
+ if (!sidecarPathValue) continue
1457
+ const sidecarPath = safeRelativePath(sidecarPathValue, sidecarPathValue)
1458
+ const content = typeof rawSidecar.content === 'string' ? rawSidecar.content : null
1459
+ const contentSha256 = stringFromRecord(rawSidecar, 'contentSha256') ||
1460
+ stringFromRecord(rawSidecar, 'content_sha256') ||
1461
+ (content !== null ? sha256Hex(content) : undefined)
1462
+ if (content !== null) {
1463
+ writeTextFileEnsuringDir(join(targetDir, sidecarPath), content)
1464
+ }
1465
+ const { content: _ignoredContent, contentSha256: _ignoredCamel, content_sha256: _ignoredSnake, ...metadata } = rawSidecar
1466
+ exportedSidecars.push({
1467
+ ...metadata,
1468
+ type: stringFromRecord(rawSidecar, 'type') || 'file',
1469
+ path: sidecarPath,
1470
+ ...(contentSha256 ? { content_sha256: contentSha256 } : {}),
1471
+ })
1472
+ }
1473
+
1474
+ const labels = (data.labels || [])
1475
+ .filter(item => item.promptVersionId === data.version.id)
1476
+ .map(item => item.label)
1477
+
1478
+ const manifest: Record<string, unknown> = {
1479
+ schema_version: 'orizu.prompt.v1',
1480
+ name: data.prompt.name,
1481
+ role: data.prompt.role,
1482
+ description: data.prompt.description || undefined,
1483
+ primary_text: {
1484
+ path: primaryPath,
1485
+ kind: bodyKind,
1486
+ },
1487
+ provider_settings: data.version.providerSettings || {},
1488
+ runner_version_id: data.version.runnerVersionId || undefined,
1489
+ version_id: data.version.id,
1490
+ version_number: data.version.versionNumber,
1491
+ version_label: data.version.versionLabel || undefined,
1492
+ labels,
1493
+ sidecars: exportedSidecars,
1494
+ }
1495
+
1496
+ writeTextFileEnsuringDir(
1497
+ join(targetDir, 'orizu.prompt.json'),
1498
+ `${JSON.stringify(manifest, null, 2)}\n`
1499
+ )
1500
+
1501
+ if (hasJsonFlag()) {
1502
+ printJson({
1503
+ prompt_id: data.prompt.id,
1504
+ prompt_version_id: data.version.id,
1505
+ path: targetDir,
1506
+ })
1507
+ return
1508
+ }
1509
+
1510
+ printLine(`Pulled ${kind} ${sanitizeTerminalText(data.prompt.name)} to ${sanitizeTerminalText(targetDir)}`)
1511
+ }
1512
+
1513
+ async function setPromptLabel() {
1514
+ const promptName = getPositionalArg(3)
1515
+ const label = getPositionalArg(4)
1516
+ const project = getArg('--project') || await resolveProjectSlug(null)
1517
+ const promptVersionId = getArg('--version')
1518
+
1519
+ if (!promptName || !label || !promptVersionId) {
1520
+ throw new Error('Usage: orizu prompts labels set <prompt-name> <label> --version <prompt-version-id> [--project <team/project>] [--json]')
1521
+ }
1522
+
1523
+ const response = await authedFetch(`/api/cli/prompts/labels?project=${encodeURIComponent(project)}`, {
1524
+ method: 'POST',
1525
+ headers: { 'Content-Type': 'application/json' },
1526
+ body: JSON.stringify({
1527
+ promptName,
1528
+ label,
1529
+ promptVersionId,
1530
+ }),
1531
+ })
1532
+
1533
+ if (!response.ok) {
1534
+ throw new Error(`Failed to set prompt label: ${await response.text()}`)
1535
+ }
1536
+
1537
+ const data = await parseJsonResponse<Record<string, unknown>>(response, 'Prompt label set')
1538
+ if (hasJsonFlag()) {
1539
+ printJson(data)
1540
+ return
1541
+ }
1542
+
1543
+ printLine(`Moved ${sanitizeTerminalText(label)} to ${sanitizeTerminalText(promptVersionId)}`)
1544
+ }
1545
+
1546
+ async function bindPromptScorer(role: 'headline' | 'tracked') {
1547
+ const promptId = getPositionalArg(3)
1548
+ const project = getArg('--project') || await resolveProjectSlug(null)
1549
+ const scorerVersionId = getArg('--scorer-version')
1550
+
1551
+ if (!promptId || !scorerVersionId) {
1552
+ throw new Error(`Usage: orizu prompts scorers ${role === 'headline' ? 'set-headline' : 'add'} <prompt-id> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]`)
1553
+ }
1554
+
1555
+ const response = await authedFetch(`/api/cli/prompts/scorers?project=${encodeURIComponent(project)}`, {
1556
+ method: 'POST',
1557
+ headers: { 'Content-Type': 'application/json' },
1558
+ body: JSON.stringify({
1559
+ promptId,
1560
+ scorerVersionId,
1561
+ role,
1562
+ datasetVersionId: getArg('--dataset-version') || undefined,
1563
+ splitSetId: getArg('--split-set') || undefined,
1564
+ splitName: getArg('--split') || undefined,
1565
+ }),
1566
+ })
1567
+
1568
+ if (!response.ok) {
1569
+ throw new Error(`Failed to bind prompt scorer: ${await response.text()}`)
1570
+ }
1571
+
1572
+ const data = await parseJsonResponse<Record<string, unknown>>(response, 'Prompt scorer bind')
1573
+ if (hasJsonFlag()) {
1574
+ printJson(data)
1575
+ return
1576
+ }
1577
+
1578
+ printLine(`${role === 'headline' ? 'Set headline' : 'Added'} scorer ${sanitizeTerminalText(scorerVersionId)} for ${sanitizeTerminalText(promptId)}`)
1579
+ }
1580
+
1581
+ function getPositionalArg(index: number): string | null {
1582
+ const value = cliArgs[index]
1583
+ return value && !value.startsWith('--') ? value : null
1584
+ }
1585
+
1586
+ function parseRatioFlag(name: string, fallback: number): number {
1587
+ const value = getArg(name)
1588
+ if (!value) {
1589
+ return fallback
1590
+ }
1591
+
1592
+ const parsed = Number(value)
1593
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
1594
+ throw new Error(`${name} must be a number between 0 and 1`)
1595
+ }
1596
+
1597
+ return parsed
1598
+ }
1599
+
1600
+ function parsePositiveIntegerFlag(name: string, fallback: number): number {
1601
+ const value = getArg(name)
1602
+ if (!value) {
1603
+ return fallback
1604
+ }
1605
+
1606
+ const parsed = Number(value)
1607
+ if (!Number.isInteger(parsed) || parsed <= 0) {
1608
+ throw new Error(`${name} must be a positive integer`)
1609
+ }
1610
+
1611
+ return parsed
1612
+ }
1613
+
1614
+ function formatPercent(value: number | null): string {
1615
+ if (value === null || !Number.isFinite(value)) {
1616
+ return 'unscored'
1617
+ }
1618
+
1619
+ return `${(value * 100).toFixed(1)}%`
1620
+ }
1621
+
1622
+ function looksLikeUuid(value: string): boolean {
1623
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
1624
+ }
1625
+
1626
+ async function resolveDatasetIdForProject(datasetRef: string, projectArg: string | null): Promise<string> {
1627
+ if (looksLikeUuid(datasetRef)) {
1628
+ return datasetRef
1629
+ }
1630
+
1631
+ projectArg = projectArg || process.env.ORIZU_PROJECT || null
1632
+ if (!projectArg) {
1633
+ return datasetRef
1634
+ }
1635
+
1636
+ const project = projectArg || await resolveProjectSlug(null)
1637
+ const datasets = await fetchDatasets(project)
1638
+ const matches = datasets.filter(dataset => dataset.name === datasetRef || dataset.id === datasetRef)
1639
+
1640
+ if (matches.length === 0) {
1641
+ throw new Error(`Dataset '${sanitizeTerminalText(datasetRef)}' not found in ${sanitizeTerminalText(project)}`)
1642
+ }
1643
+
1644
+ return matches[0].id
1645
+ }
1646
+
1647
+ async function createDatasetVersion() {
1648
+ const datasetId = getPositionalArg(3) || getArg('--dataset')
1649
+ const versionLabel = getArg('--label') || getArg('--version-label') || null
1650
+ const project = getArg('--project')
1651
+
1652
+ if (!datasetId) {
1653
+ throw new Error('Usage: orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--json]')
1654
+ }
1655
+
1656
+ const resolvedDatasetId = await resolveDatasetIdForProject(datasetId, project)
1657
+ const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(resolvedDatasetId)}/versions`, {
1658
+ method: 'POST',
1659
+ headers: { 'Content-Type': 'application/json' },
1660
+ body: JSON.stringify({ versionLabel }),
1661
+ })
1662
+
1663
+ if (!response.ok) {
1664
+ throw new Error(`Failed to create dataset version: ${await response.text()}`)
1665
+ }
1666
+
1667
+ const data = await parseJsonResponse<{
1668
+ datasetVersion: {
1669
+ id: string
1670
+ rowCount: number
1671
+ artifactFormat?: string
1672
+ artifactStoragePath?: string
1673
+ }
1674
+ }>(response, 'Dataset version create')
1675
+
1676
+ if (hasJsonFlag()) {
1677
+ printJson({
1678
+ dataset_version_id: data.datasetVersion.id,
1679
+ row_count: data.datasetVersion.rowCount,
1680
+ })
1681
+ return
1682
+ }
1683
+
1684
+ const details = [
1685
+ `${data.datasetVersion.rowCount} rows`,
1686
+ data.datasetVersion.artifactFormat,
1687
+ ].filter(Boolean).join(', ')
1688
+ printLine(
1689
+ `Created dataset version ${sanitizeTerminalText(data.datasetVersion.id)}` +
1690
+ (details ? ` (${sanitizeTerminalText(details)})` : '')
1691
+ )
1692
+ }
1693
+
1694
+ async function createDatasetSplitSet() {
1695
+ const datasetVersionId = getPositionalArg(3) || getArg('--dataset-version')
1696
+ const splitFile = getArg('--from-file')
1697
+ const splitSpec = splitFile ? readJsonFile(splitFile) : null
1698
+ const name = getArg('--name') || (typeof splitSpec?.name === 'string' ? splitSpec.name : 'default')
1699
+ const strategy = getArg('--strategy') || (typeof splitSpec?.strategy === 'string' ? splitSpec.strategy : 'random')
1700
+ const seed = splitSpec && (splitSpec.seed === null || typeof splitSpec.seed === 'number')
1701
+ ? splitSpec.seed
1702
+ : parsePositiveIntegerFlag('--seed', 1)
1703
+ const train = parseRatioFlag('--train', 0.7)
1704
+ const validation = parseRatioFlag('--validation', 0.2)
1705
+ const test = parseRatioFlag('--test', 0.1)
1706
+
1707
+ if (!datasetVersionId) {
1708
+ throw new Error(
1709
+ 'Usage: orizu datasets splits create <datasetVersionId> [--from-file <split.json>] [--name <name>] [--seed <n>] [--train <ratio>] [--validation <ratio>] [--test <ratio>] [--json]'
1710
+ )
1711
+ }
1712
+
1713
+ const partitions = splitSpec && Array.isArray(splitSpec.partitions) ? splitSpec.partitions : undefined
1714
+ const metadata = splitSpec && typeof splitSpec.metadata === 'object' && splitSpec.metadata !== null
1715
+ ? splitSpec.metadata
1716
+ : undefined
1717
+
1718
+ const response = await authedFetch(`/api/cli/dataset-versions/${encodeURIComponent(datasetVersionId)}/split-sets`, {
1719
+ method: 'POST',
1720
+ headers: { 'Content-Type': 'application/json' },
1721
+ body: JSON.stringify({
1722
+ name,
1723
+ strategy,
1724
+ seed,
1725
+ train,
1726
+ validation,
1727
+ test,
1728
+ partitions,
1729
+ metadata,
1730
+ }),
1731
+ })
1732
+
1733
+ if (!response.ok) {
1734
+ throw new Error(`Failed to create dataset split set: ${await response.text()}`)
1735
+ }
1736
+
1737
+ const data = await parseJsonResponse<{ splitSet: { id: string } }>(response, 'Dataset split set create')
1738
+ if (hasJsonFlag()) {
1739
+ printJson({ split_set_id: data.splitSet.id })
1740
+ return
1741
+ }
1742
+
1743
+ printLine(`Created split set ${sanitizeTerminalText(data.splitSet.id)}`)
1744
+ }
1745
+
1746
+ async function submitRunResults() {
1747
+ const resultsPath = getPositionalArg(2) || getArg('--file') || getArg('--results')
1748
+ const project = getArg('--project') || await resolveProjectSlug(null)
1749
+ const promptVersionId = getArg('--prompt-version')
1750
+ const runnerVersionId = getArg('--runner-version')
1751
+ const datasetVersionId = getArg('--dataset-version')
1752
+ const splitSetId = getArg('--split-set')
1753
+ const splitName = getArg('--split')
1754
+
1755
+ if (
1756
+ !resultsPath ||
1757
+ !promptVersionId ||
1758
+ !runnerVersionId ||
1759
+ !datasetVersionId ||
1760
+ !splitSetId ||
1761
+ !splitName
1762
+ ) {
1763
+ throw new Error(
1764
+ 'Usage: orizu runs submit <results.jsonl> --project <team/project> --prompt-version <id> --runner-version <id> --dataset-version <id> --split-set <id> --split <name> [--judge-version <id>] [--judge-runner-version <id>]'
1765
+ )
1766
+ }
1767
+
1768
+ const resultBytes = readSourceBytes(resultsPath)
1769
+ const resultsJsonl = resultsPath.endsWith('.gz')
1770
+ ? gunzipSync(resultBytes).toString('utf8')
1771
+ : resultBytes.toString('utf8')
1772
+ const response = await authedFetch(`/api/cli/runs/submit?project=${encodeURIComponent(project)}`, {
1773
+ method: 'POST',
1774
+ headers: { 'Content-Type': 'application/json' },
1775
+ body: JSON.stringify({
1776
+ promptVersionId,
1777
+ runnerVersionId,
1778
+ datasetVersionId,
1779
+ splitSetId,
1780
+ splitName,
1781
+ judgeVersionId: getArg('--judge-version') || undefined,
1782
+ judgeRunnerVersionId: getArg('--judge-runner-version') || undefined,
1783
+ resultsJsonl,
1784
+ }),
1785
+ })
1786
+
1787
+ if (!response.ok) {
1788
+ throw new Error(`Failed to submit run: ${await response.text()}`)
1789
+ }
1790
+
1791
+ const data = await parseJsonResponse<{
1792
+ run: {
1793
+ id: string
1794
+ aggregateScore: number | null
1795
+ perRowResultsStoragePath: string
1796
+ }
1797
+ }>(response, 'Run submit')
1798
+ printLine(
1799
+ `Submitted run ${sanitizeTerminalText(data.run.id)} ` +
1800
+ `(${formatPercent(data.run.aggregateScore)}) -> ` +
1801
+ sanitizeTerminalText(data.run.perRowResultsStoragePath)
1802
+ )
1803
+ }
1804
+
1805
+ async function submitScoreResults() {
1806
+ const resultsPath = getPositionalArg(2) || getArg('--file') || getArg('--results')
1807
+ const project = getArg('--project') || await resolveProjectSlug(null)
1808
+ const scorerVersionId = getArg('--scorer-version')
1809
+ const subjectPromptVersionId = getArg('--subject-version') || getArg('--prompt-version')
1810
+ const optimizationRunId = getArg('--optimization-run')
1811
+ const candidateId = getArg('--candidate')
1812
+
1813
+ if (!resultsPath || !scorerVersionId || (!subjectPromptVersionId && (!optimizationRunId || !candidateId))) {
1814
+ throw new Error('Usage: orizu scores submit <results.jsonl|results.json> --project <team/project> --scorer-version <id> (--subject-version <prompt-version-id> | --optimization-run <id> --candidate <id>) [--dataset-version <id> --split-set <id> --split <name>] [--json]')
1815
+ }
1816
+
1817
+ const resultBytes = readSourceBytes(resultsPath)
1818
+ const raw = resultsPath.endsWith('.gz')
1819
+ ? gunzipSync(resultBytes).toString('utf8')
1820
+ : resultBytes.toString('utf8')
1821
+ const resultsJsonl = normalizeScoreResultsInput(resultsPath, raw)
1822
+ const response = await authedFetch(`/api/cli/scores/submit?project=${encodeURIComponent(project)}`, {
1823
+ method: 'POST',
1824
+ headers: { 'Content-Type': 'application/json' },
1825
+ body: JSON.stringify({
1826
+ scorerVersionId,
1827
+ subjectPromptVersionId: subjectPromptVersionId || undefined,
1828
+ datasetVersionId: getArg('--dataset-version') || undefined,
1829
+ splitSetId: getArg('--split-set') || undefined,
1830
+ splitName: getArg('--split') || undefined,
1831
+ optimizationRunId: optimizationRunId || undefined,
1832
+ candidateId: candidateId || undefined,
1833
+ resultsJsonl,
1834
+ }),
1835
+ })
1836
+
1837
+ if (!response.ok) {
1838
+ throw new Error(`Failed to submit score: ${await response.text()}`)
1839
+ }
1840
+
1841
+ const data = await parseJsonResponse<{
1842
+ scoreRun: {
1843
+ id: string
1844
+ scoreValue: number | null
1845
+ rowResultsStoragePath?: string | null
1846
+ }
1847
+ }>(response, 'Score submit')
1848
+ if (hasJsonFlag()) {
1849
+ printJson(data)
1850
+ return
1851
+ }
1852
+
1853
+ printLine(
1854
+ `Submitted score ${sanitizeTerminalText(data.scoreRun.id)} ` +
1855
+ `(${formatPercent(data.scoreRun.scoreValue)})`
1856
+ )
1857
+ }
1858
+
1859
+ function normalizeScoreResultsInput(sourcePath: string, raw: string): string {
1860
+ const logicalPath = sourcePath.endsWith('.gz') ? sourcePath.slice(0, -3) : sourcePath
1861
+ if (!logicalPath.endsWith('.json')) return raw
1862
+
1863
+ try {
1864
+ const parsed = JSON.parse(raw) as unknown
1865
+ const rows = Array.isArray(parsed) ? parsed : [parsed]
1866
+ if (rows.some(row => !isRecord(row))) {
1867
+ throw new Error('JSON score results must be an object or an array of objects')
1868
+ }
1869
+ return rows.map(row => JSON.stringify(row)).join('\n')
1870
+ } catch (error) {
1871
+ const message = error instanceof Error ? error.message : String(error)
1872
+ throw new Error(`Invalid score results JSON '${sourcePath}': ${message}`)
1873
+ }
1874
+ }
1875
+
1876
+ type OptimizationLifecycleAction = 'pause' | 'resume' | 'finish' | 'fail' | 'cancel'
1877
+
1878
+ function parseOptionalNumberFlag(name: string): number | undefined {
1879
+ const value = getArg(name)
1880
+ if (!value) {
1881
+ return undefined
1882
+ }
1883
+
1884
+ const parsed = Number(value)
1885
+ if (!Number.isFinite(parsed)) {
1886
+ throw new Error(`${name} must be a number`)
1887
+ }
1888
+
1889
+ return parsed
1890
+ }
1891
+
1892
+ function hasObjectKeys(value: Record<string, unknown>): boolean {
1893
+ return Object.keys(value).length > 0
1894
+ }
1895
+
1896
+ async function startOptimizationRun() {
1897
+ const project = getArg('--project') || await resolveProjectSlug(null)
1898
+ const optimizerVersionId = getArg('--optimizer-version')
1899
+ const promptVersionIds = parseCommaSeparated(
1900
+ getArg('--prompt-version') || getArg('--prompt-versions')
1901
+ )
1902
+ const judgeVersionIds = parseCommaSeparated(
1903
+ getArg('--judge-version') || getArg('--judge-versions')
1904
+ )
1905
+ const selectionScorer = getArg('--selection-scorer') || getArg('--scorer-version')
1906
+ const reflectionScorer = getArg('--reflection-scorer')
1907
+ const paretoScorers = parseCommaSeparated(getArg('--pareto-scorer') || getArg('--pareto-scorers'))
1908
+ const bestScorers = parseCommaSeparated(getArg('--best-scorer') || getArg('--best-scorers'))
1909
+ const datasetVersionId = getArg('--dataset-version')
1910
+ const splitSetId = getArg('--split-set')
1911
+ const metadata = readJsonObjectArg(getArg('--metadata'), 'optimization metadata')
1912
+
1913
+ if (
1914
+ !optimizerVersionId ||
1915
+ promptVersionIds.length === 0 ||
1916
+ !selectionScorer ||
1917
+ !datasetVersionId ||
1918
+ !splitSetId
1919
+ ) {
1920
+ throw new Error(
1921
+ 'Usage: orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]'
1922
+ )
1923
+ }
1924
+
1925
+ const scorers = [
1926
+ { scorerVersionId: selectionScorer, role: 'selection' },
1927
+ ...(reflectionScorer ? [{ scorerVersionId: reflectionScorer, role: 'reflection' }] : []),
1928
+ ...paretoScorers.map(scorerVersionId => ({
1929
+ scorerVersionId,
1930
+ role: 'tracked',
1931
+ trackedScope: 'pareto_candidates',
1932
+ })),
1933
+ ...bestScorers.map(scorerVersionId => ({
1934
+ scorerVersionId,
1935
+ role: 'tracked',
1936
+ trackedScope: 'best_candidate',
1937
+ })),
1938
+ ]
1939
+
1940
+ const response = await authedFetch(`/api/cli/optimization-runs?project=${encodeURIComponent(project)}`, {
1941
+ method: 'POST',
1942
+ headers: { 'Content-Type': 'application/json' },
1943
+ body: JSON.stringify({
1944
+ optimizerVersionId,
1945
+ promptVersionIds,
1946
+ judgeVersionIds,
1947
+ scorers,
1948
+ datasetVersionId,
1949
+ splitSetId,
1950
+ trainSplitName: getArg('--train-split') || undefined,
1951
+ validationSplitName: getArg('--validation-split') || undefined,
1952
+ metadata,
1953
+ }),
1954
+ })
1955
+
1956
+ if (!response.ok) {
1957
+ throw new Error(`Failed to start optimization run: ${await response.text()}`)
1958
+ }
1959
+
1960
+ const data = await parseJsonResponse<{
1961
+ optimization_run_id: string
1962
+ optimizationRun: {
1963
+ id: string
1964
+ status: string
1965
+ startedAt?: string | null
1966
+ createdAt?: string | null
1967
+ }
1968
+ }>(response, 'Optimization run start')
1969
+
1970
+ if (hasJsonFlag()) {
1971
+ printJson(data)
1972
+ return
1973
+ }
1974
+
1975
+ printLine(
1976
+ `Started optimization run ${sanitizeTerminalText(data.optimization_run_id)} ` +
1977
+ `(${sanitizeTerminalText(data.optimizationRun.status)})`
1978
+ )
1979
+ }
1980
+
1981
+ async function updateOptimizationRunLifecycle(action: OptimizationLifecycleAction) {
1982
+ const runId = getPositionalArg(2) || getArg('--run-id')
1983
+ if (!runId) {
1984
+ throw new Error(`Usage: orizu optimizations ${action} <run-id>`)
1985
+ }
1986
+
1987
+ const statusByAction: Record<OptimizationLifecycleAction, string> = {
1988
+ pause: 'paused',
1989
+ resume: 'running',
1990
+ finish: 'succeeded',
1991
+ fail: 'failed',
1992
+ cancel: 'cancelled',
1993
+ }
1994
+
1995
+ const body: Record<string, unknown> = {
1996
+ status: statusByAction[action],
1997
+ }
1998
+ const metadata = readJsonObjectArg(getArg('--metadata'), 'optimization metadata')
1999
+ const reason = getArg('--reason')
2000
+
2001
+ if ((action === 'pause' || action === 'cancel') && reason) {
2002
+ metadata.reason = reason
2003
+ }
2004
+
2005
+ if (action === 'fail' && reason) {
2006
+ metadata.failure_reason = reason
2007
+ body.failureReason = reason
2008
+ }
2009
+
2010
+ if (hasObjectKeys(metadata)) {
2011
+ body.metadata = metadata
2012
+ }
2013
+
2014
+ if (action === 'finish') {
2015
+ const bestScore = parseOptionalNumberFlag('--best-score')
2016
+ if (bestScore !== undefined) {
2017
+ body.bestScore = bestScore
2018
+ }
2019
+
2020
+ const bestCandidateId = getArg('--best-candidate')
2021
+ if (bestCandidateId) {
2022
+ body.bestCandidateId = bestCandidateId
2023
+ }
2024
+
2025
+ const resultPromptVersionId = getArg('--result-prompt-version')
2026
+ if (resultPromptVersionId) {
2027
+ body.resultPromptVersionId = resultPromptVersionId
2028
+ }
2029
+ }
2030
+
2031
+ const response = await authedFetch(`/api/cli/optimization-runs/${encodeURIComponent(runId)}`, {
2032
+ method: 'PATCH',
2033
+ headers: { 'Content-Type': 'application/json' },
2034
+ body: JSON.stringify(body),
2035
+ })
2036
+
2037
+ if (!response.ok) {
2038
+ throw new Error(`Failed to update optimization run: ${await response.text()}`)
2039
+ }
2040
+
2041
+ const data = await parseJsonResponse<{
2042
+ optimizationRun: {
2043
+ id: string
2044
+ status: string
2045
+ startedAt?: string | null
2046
+ finishedAt?: string | null
2047
+ bestScore?: number | null
2048
+ bestCandidateId?: string | null
2049
+ resultPromptVersionId?: string | null
2050
+ metadata?: Record<string, unknown>
2051
+ }
2052
+ }>(response, 'Optimization run update')
2053
+
2054
+ if (hasJsonFlag()) {
2055
+ printJson(data)
2056
+ return
2057
+ }
2058
+
2059
+ const id = sanitizeTerminalText(data.optimizationRun.id)
2060
+ if (action === 'pause') {
2061
+ printLine(`Paused optimization run ${id}`)
2062
+ } else if (action === 'resume') {
2063
+ printLine(`Resumed optimization run ${id}`)
2064
+ } else if (action === 'finish') {
2065
+ printLine(`Finished optimization run ${id}`)
2066
+ } else if (action === 'fail') {
2067
+ printLine(`Marked optimization run ${id} failed`)
2068
+ } else {
2069
+ printLine(`Cancelled optimization run ${id}`)
2070
+ }
2071
+ }
2072
+
2073
+ function removeFlagWithValue(args: string[], flag: string): string[] {
2074
+ const filtered: string[] = []
2075
+ for (let index = 0; index < args.length; index += 1) {
2076
+ if (args[index] === flag) {
2077
+ index += 1
2078
+ continue
952
2079
  }
953
- } catch (error) {
954
- const message = error instanceof Error ? error.message : String(error)
955
- remoteLogoutError = sanitizeTerminalText(message).slice(0, 180)
2080
+ filtered.push(args[index])
956
2081
  }
2082
+ return filtered
2083
+ }
957
2084
 
958
- clearServerCredentials(baseUrl)
959
- if (remoteLogoutError) {
960
- console.warn(`Warning: remote logout failed: ${remoteLogoutError}`)
2085
+ function bundledOrizuGepaPythonPath(): string | null {
2086
+ const candidate = fileURLToPath(new URL('../../orizu-gepa-python/src', import.meta.url))
2087
+ return existsSync(candidate) ? candidate : null
2088
+ }
2089
+
2090
+ async function runGepaOptimization() {
2091
+ const project = getArg('--project') || await resolveProjectSlug(null)
2092
+ const baseUrl = getBaseUrl()
2093
+ const token = getStoredAuthTokenForBaseUrl(baseUrl)
2094
+ const python = getArg('--python') || process.env.PYTHON || 'python3'
2095
+ const bundledPythonPath = bundledOrizuGepaPythonPath()
2096
+ let forwardedArgs = removeFlagWithValue(cliArgs.slice(2), '--python')
2097
+
2098
+ if (!forwardedArgs.includes('--project')) {
2099
+ forwardedArgs = ['--project', project, ...forwardedArgs]
2100
+ }
2101
+
2102
+ const pythonPathEntries = [
2103
+ bundledPythonPath,
2104
+ process.env.PYTHONPATH,
2105
+ ].filter((entry): entry is string => Boolean(entry))
2106
+
2107
+ const result = spawnSync(python, ['-m', 'orizu_gepa.cli', ...forwardedArgs], {
2108
+ stdio: 'inherit',
2109
+ env: {
2110
+ ...process.env,
2111
+ ORIZU_API_URL: baseUrl,
2112
+ ORIZU_TOKEN: token,
2113
+ ORIZU_PROJECT: project,
2114
+ PYTHONPATH: pythonPathEntries.join(delimiter),
2115
+ PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || '1',
2116
+ },
2117
+ })
2118
+
2119
+ if (result.error) {
2120
+ throw result.error
2121
+ }
2122
+
2123
+ if (result.status !== 0) {
2124
+ throw new Error(`orizu-gepa failed with exit code ${result.status}`)
961
2125
  }
962
- printLine(`Logged out from ${sanitizeTerminalText(baseUrl)}.`)
963
2126
  }
964
2127
 
965
2128
  async function listTeams() {
@@ -1074,6 +2237,26 @@ function readSourceFile(pathArg: string): string {
1074
2237
  }
1075
2238
  }
1076
2239
 
2240
+ function readSourceBytes(pathArg: string): Buffer {
2241
+ const expandedPath = expandHomePath(pathArg)
2242
+ try {
2243
+ return readFileSync(expandedPath)
2244
+ } catch (error: any) {
2245
+ if (error?.code === 'ENOENT') {
2246
+ throw new Error(`File not found: ${expandedPath}`)
2247
+ }
2248
+ throw new Error(`Failed to read file '${expandedPath}': ${error?.message || String(error)}`)
2249
+ }
2250
+ }
2251
+
2252
+ function hasJsonFlag(): boolean {
2253
+ return hasArg('--json')
2254
+ }
2255
+
2256
+ function printJson(value: Record<string, unknown>) {
2257
+ printLine(JSON.stringify(value))
2258
+ }
2259
+
1077
2260
  function readJsonFile(pathArg: string): Record<string, unknown> {
1078
2261
  const raw = readSourceFile(pathArg)
1079
2262
  try {
@@ -1088,6 +2271,218 @@ function readJsonFile(pathArg: string): Record<string, unknown> {
1088
2271
  }
1089
2272
  }
1090
2273
 
2274
+ function isRecord(value: unknown): value is Record<string, unknown> {
2275
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
2276
+ }
2277
+
2278
+ function stringFromRecord(record: Record<string, unknown>, key: string): string | undefined {
2279
+ const value = record[key]
2280
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined
2281
+ }
2282
+
2283
+ function safeRelativePath(value: string, fallback: string): string {
2284
+ const raw = value && value.trim() ? value.trim() : fallback
2285
+ if (raw.includes('\0')) {
2286
+ throw new Error(`Unsafe relative path: ${raw}`)
2287
+ }
2288
+ const normalized = normalize(raw)
2289
+ if (isAbsolute(raw) || normalized === '..' || normalized.startsWith('../') || normalized.startsWith('..\\')) {
2290
+ throw new Error(`Unsafe relative path: ${raw}`)
2291
+ }
2292
+ return normalized === '.' ? fallback : normalized
2293
+ }
2294
+
2295
+ function sha256Hex(value: string | Buffer): string {
2296
+ return createHash('sha256').update(value).digest('hex')
2297
+ }
2298
+
2299
+ function readPromptPrimaryText(manifest: Record<string, unknown>, promptDir: string): {
2300
+ body: string
2301
+ bodyKind: string
2302
+ path: string
2303
+ } {
2304
+ const primaryText = isRecord(manifest.primary_text) ? manifest.primary_text : null
2305
+ const pathValue = primaryText
2306
+ ? stringFromRecord(primaryText, 'path')
2307
+ : stringFromRecord(manifest, 'body_file')
2308
+ const bodyPath = safeRelativePath(pathValue || 'prompt.md', 'prompt.md')
2309
+ const bodyKind = primaryText
2310
+ ? stringFromRecord(primaryText, 'kind') || stringFromRecord(manifest, 'body_kind') || 'text'
2311
+ : stringFromRecord(manifest, 'body_kind') || 'text'
2312
+
2313
+ return {
2314
+ body: readSourceFile(join(promptDir, bodyPath)),
2315
+ bodyKind,
2316
+ path: bodyPath,
2317
+ }
2318
+ }
2319
+
2320
+ function readPromptSidecars(manifest: Record<string, unknown>, promptDir: string) {
2321
+ const sidecars = Array.isArray(manifest.sidecars) ? manifest.sidecars : []
2322
+ return sidecars
2323
+ .filter(isRecord)
2324
+ .map(sidecar => {
2325
+ const pathValue = stringFromRecord(sidecar, 'path')
2326
+ if (!pathValue) {
2327
+ throw new Error('Prompt sidecar entries require path')
2328
+ }
2329
+ const relativePath = safeRelativePath(pathValue, pathValue)
2330
+ const content = readSourceFile(join(promptDir, relativePath))
2331
+ const { content: _ignoredContent, content_sha256: _ignoredSnake, contentSha256: _ignoredCamel, ...metadata } = sidecar
2332
+ return {
2333
+ ...metadata,
2334
+ type: stringFromRecord(sidecar, 'type') || 'file',
2335
+ path: relativePath,
2336
+ content,
2337
+ contentSha256: sha256Hex(content),
2338
+ }
2339
+ })
2340
+ }
2341
+
2342
+ function writeTextFileEnsuringDir(pathArg: string, content: string) {
2343
+ mkdirSync(dirname(pathArg), { recursive: true })
2344
+ writeFileSync(pathArg, content)
2345
+ }
2346
+
2347
+ function shouldExcludeArtifactPath(relativePath: string): boolean {
2348
+ const parts = relativePath.split('/')
2349
+ return parts.some(part =>
2350
+ part === '.git' ||
2351
+ part === '.DS_Store' ||
2352
+ part === '__pycache__' ||
2353
+ part === '.pytest_cache'
2354
+ )
2355
+ }
2356
+
2357
+ function collectArtifactFiles(sourceDir: string, relativeDir = ''): string[] {
2358
+ const absoluteDir = relativeDir ? join(sourceDir, relativeDir) : sourceDir
2359
+ return readdirSync(absoluteDir, { withFileTypes: true })
2360
+ .flatMap(entry => {
2361
+ const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name
2362
+ if (shouldExcludeArtifactPath(relativePath)) {
2363
+ return []
2364
+ }
2365
+ if (entry.isDirectory()) {
2366
+ return collectArtifactFiles(sourceDir, relativePath)
2367
+ }
2368
+ return entry.isFile() ? [relativePath] : []
2369
+ })
2370
+ .sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
2371
+ }
2372
+
2373
+ const ZIP_GENERAL_PURPOSE_UTF8 = 0x0800
2374
+ const ZIP_DOS_TIME_2000_01_01 = 0
2375
+ const ZIP_DOS_DATE_2000_01_01 = ((2000 - 1980) << 9) | (1 << 5) | 1
2376
+ const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
2377
+ let value = index
2378
+ for (let bit = 0; bit < 8; bit++) {
2379
+ value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1)
2380
+ }
2381
+ return value >>> 0
2382
+ })
2383
+
2384
+ function crc32(bytes: Buffer): number {
2385
+ let crc = 0xffffffff
2386
+ for (const byte of bytes) {
2387
+ crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8)
2388
+ }
2389
+ return (crc ^ 0xffffffff) >>> 0
2390
+ }
2391
+
2392
+ function createStoredZip(entries: Array<{ path: string; data: Buffer }>): Buffer {
2393
+ const localParts: Buffer[] = []
2394
+ const centralParts: Buffer[] = []
2395
+ let offset = 0
2396
+
2397
+ for (const entry of entries) {
2398
+ const name = Buffer.from(entry.path, 'utf8')
2399
+ const crc = crc32(entry.data)
2400
+ const localHeader = Buffer.alloc(30)
2401
+ localHeader.writeUInt32LE(0x04034b50, 0)
2402
+ localHeader.writeUInt16LE(10, 4)
2403
+ localHeader.writeUInt16LE(ZIP_GENERAL_PURPOSE_UTF8, 6)
2404
+ localHeader.writeUInt16LE(0, 8)
2405
+ localHeader.writeUInt16LE(ZIP_DOS_TIME_2000_01_01, 10)
2406
+ localHeader.writeUInt16LE(ZIP_DOS_DATE_2000_01_01, 12)
2407
+ localHeader.writeUInt32LE(crc, 14)
2408
+ localHeader.writeUInt32LE(entry.data.length, 18)
2409
+ localHeader.writeUInt32LE(entry.data.length, 22)
2410
+ localHeader.writeUInt16LE(name.length, 26)
2411
+ localHeader.writeUInt16LE(0, 28)
2412
+
2413
+ localParts.push(localHeader, name, entry.data)
2414
+
2415
+ const centralHeader = Buffer.alloc(46)
2416
+ centralHeader.writeUInt32LE(0x02014b50, 0)
2417
+ centralHeader.writeUInt16LE(20, 4)
2418
+ centralHeader.writeUInt16LE(10, 6)
2419
+ centralHeader.writeUInt16LE(ZIP_GENERAL_PURPOSE_UTF8, 8)
2420
+ centralHeader.writeUInt16LE(0, 10)
2421
+ centralHeader.writeUInt16LE(ZIP_DOS_TIME_2000_01_01, 12)
2422
+ centralHeader.writeUInt16LE(ZIP_DOS_DATE_2000_01_01, 14)
2423
+ centralHeader.writeUInt32LE(crc, 16)
2424
+ centralHeader.writeUInt32LE(entry.data.length, 20)
2425
+ centralHeader.writeUInt32LE(entry.data.length, 24)
2426
+ centralHeader.writeUInt16LE(name.length, 28)
2427
+ centralHeader.writeUInt16LE(0, 30)
2428
+ centralHeader.writeUInt16LE(0, 32)
2429
+ centralHeader.writeUInt16LE(0, 34)
2430
+ centralHeader.writeUInt16LE(0, 36)
2431
+ centralHeader.writeUInt32LE(0, 38)
2432
+ centralHeader.writeUInt32LE(offset, 42)
2433
+ centralParts.push(centralHeader, name)
2434
+
2435
+ offset += localHeader.length + name.length + entry.data.length
2436
+ }
2437
+
2438
+ const centralDirectoryOffset = offset
2439
+ const centralDirectory = Buffer.concat(centralParts)
2440
+ const endOfCentralDirectory = Buffer.alloc(22)
2441
+ endOfCentralDirectory.writeUInt32LE(0x06054b50, 0)
2442
+ endOfCentralDirectory.writeUInt16LE(0, 4)
2443
+ endOfCentralDirectory.writeUInt16LE(0, 6)
2444
+ endOfCentralDirectory.writeUInt16LE(entries.length, 8)
2445
+ endOfCentralDirectory.writeUInt16LE(entries.length, 10)
2446
+ endOfCentralDirectory.writeUInt32LE(centralDirectory.length, 12)
2447
+ endOfCentralDirectory.writeUInt32LE(centralDirectoryOffset, 16)
2448
+ endOfCentralDirectory.writeUInt16LE(0, 20)
2449
+
2450
+ return Buffer.concat([...localParts, centralDirectory, endOfCentralDirectory])
2451
+ }
2452
+
2453
+ function zipDirectoryToBase64(dirArg: string): { zipBase64: string; contentSha256: string } {
2454
+ const sourceDir = expandHomePath(dirArg)
2455
+ try {
2456
+ const stats = statSync(sourceDir)
2457
+ if (!stats.isDirectory()) {
2458
+ throw new Error(`${sourceDir} is not a directory`)
2459
+ }
2460
+ } catch (error: any) {
2461
+ if (error?.code === 'ENOENT') {
2462
+ throw new Error(`Directory not found: ${sourceDir}`)
2463
+ }
2464
+ throw error
2465
+ }
2466
+
2467
+ const files = collectArtifactFiles(sourceDir)
2468
+ if (files.length === 0) {
2469
+ throw new Error(`Directory contains no artifact files: ${sourceDir}`)
2470
+ }
2471
+
2472
+ const bytes = createStoredZip(files.map(relativePath => ({
2473
+ path: relativePath,
2474
+ data: readFileSync(join(sourceDir, relativePath)),
2475
+ })))
2476
+ return {
2477
+ zipBase64: bytes.toString('base64'),
2478
+ contentSha256: createHash('sha256').update(bytes).digest('hex'),
2479
+ }
2480
+ }
2481
+
2482
+ function readManifestFile(dirArg: string): Record<string, unknown> {
2483
+ return readJsonFile(join(expandHomePath(dirArg), 'manifest.json'))
2484
+ }
2485
+
1091
2486
  async function createAppFromFile() {
1092
2487
  const project = getArg('--project')
1093
2488
  const name = getArg('--name')
@@ -1695,6 +3090,36 @@ async function uploadDataset() {
1695
3090
  }
1696
3091
  }
1697
3092
 
3093
+ async function pushDataset() {
3094
+ const projectArg = getArg('--project')
3095
+ const fileArg = getPositionalArg(2) || getArg('--file')
3096
+ const name = getArg('--name')
3097
+
3098
+ if (!fileArg) {
3099
+ throw new Error('Usage: orizu datasets push <rows.csv|rows.json|rows.jsonl> [--project <team/project>] [--name <name>] [--json]')
3100
+ }
3101
+
3102
+ const file = expandHomePath(fileArg)
3103
+ const project = await resolveProjectSlug(projectArg)
3104
+ const datasetName = name || basename(file)
3105
+ const { rows, sourceType } = parseDatasetFile(file)
3106
+ const data = await createDatasetFromRows(project, datasetName, sourceType, rows)
3107
+
3108
+ if (hasJsonFlag()) {
3109
+ printJson({
3110
+ dataset_id: data.dataset.id,
3111
+ name: data.dataset.name,
3112
+ row_count: data.dataset.rowCount,
3113
+ })
3114
+ return
3115
+ }
3116
+
3117
+ printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`)
3118
+ if (data.dataset.url) {
3119
+ printLine(`View dataset: ${formatTerminalLink(data.dataset.url)}`)
3120
+ }
3121
+ }
3122
+
1698
3123
  function getDatasetReferenceInput(): string | null {
1699
3124
  const fromFlag = getArg('--dataset')
1700
3125
  if (fromFlag) {
@@ -2167,6 +3592,253 @@ async function downloadAnnotations() {
2167
3592
  printLine(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`)
2168
3593
  }
2169
3594
 
3595
+ function readRunnerManifest(runnerDir: string): { command: string[]; supports_body_kind?: string[] } {
3596
+ const manifestPath = join(runnerDir, 'manifest.json')
3597
+ const raw = readSourceFile(manifestPath)
3598
+ const manifest = JSON.parse(raw) as {
3599
+ command?: unknown
3600
+ supports_body_kind?: unknown
3601
+ supports_body_kinds?: unknown
3602
+ }
3603
+
3604
+ if (!Array.isArray(manifest.command) || !manifest.command.every(item => typeof item === 'string')) {
3605
+ throw new Error(`Runner manifest at ${manifestPath} must include command: string[]`)
3606
+ }
3607
+
3608
+ const supportedBodyKinds = manifest.supports_body_kinds ?? manifest.supports_body_kind
3609
+ if (
3610
+ supportedBodyKinds !== undefined &&
3611
+ (!Array.isArray(supportedBodyKinds) ||
3612
+ !supportedBodyKinds.every(item => typeof item === 'string'))
3613
+ ) {
3614
+ throw new Error(`Runner manifest at ${manifestPath} has invalid supports_body_kinds`)
3615
+ }
3616
+
3617
+ return {
3618
+ command: manifest.command,
3619
+ supports_body_kind: supportedBodyKinds as string[] | undefined,
3620
+ }
3621
+ }
3622
+
3623
+ const RUNNER_TIMEOUT_MS = 120_000
3624
+ const RUNNER_OUTPUT_MAX_BYTES = 2 * 1024 * 1024
3625
+ const RUNNER_ARTIFACT_MAX_BYTES = 25 * 1024 * 1024
3626
+ const RUNNER_ENV_ALLOWLIST = new Set([
3627
+ 'PATH',
3628
+ 'SystemRoot',
3629
+ 'WINDIR',
3630
+ 'HOME',
3631
+ 'TMPDIR',
3632
+ 'TEMP',
3633
+ 'TMP',
3634
+ 'LANG',
3635
+ 'LC_ALL',
3636
+ 'PYTHONPATH',
3637
+ 'NODE_PATH',
3638
+ 'ANTHROPIC_API_KEY',
3639
+ 'OPENAI_API_KEY',
3640
+ 'GEMINI_API_KEY',
3641
+ 'GOOGLE_API_KEY',
3642
+ ])
3643
+
3644
+ function runnerSubprocessEnv(inputPath: string, outputPath: string): NodeJS.ProcessEnv {
3645
+ const env = {} as NodeJS.ProcessEnv
3646
+ for (const key of RUNNER_ENV_ALLOWLIST) {
3647
+ const value = process.env[key]
3648
+ if (value !== undefined) {
3649
+ env[key] = value
3650
+ }
3651
+ }
3652
+ env.ORIZU_RUNNER_INPUT_PATH = inputPath
3653
+ env.ORIZU_RUNNER_OUTPUT_PATH = outputPath
3654
+ return env
3655
+ }
3656
+
3657
+ function boundedRunnerOutput(value: string | Buffer | null | undefined): string {
3658
+ if (!value) return ''
3659
+ const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value)
3660
+ if (buffer.byteLength <= RUNNER_OUTPUT_MAX_BYTES) {
3661
+ return buffer.toString('utf8')
3662
+ }
3663
+ return `${buffer.subarray(0, RUNNER_OUTPUT_MAX_BYTES).toString('utf8')}\n[truncated]`
3664
+ }
3665
+
3666
+ async function materializeRunnerVersion(runnerVersionId: string): Promise<{ runnerDir: string; cleanup: () => void }> {
3667
+ const response = await authedFetch(`/api/cli/runner-versions/${encodeURIComponent(runnerVersionId)}/download`)
3668
+ if (!response.ok) {
3669
+ throw new Error(`Failed to download runner version: ${await response.text()}`)
3670
+ }
3671
+
3672
+ const tempDir = mkdtempSync(join(tmpdir(), 'orizu-runner-version-'))
3673
+ const zipPath = join(tempDir, 'runner.zip')
3674
+ const runnerDir = join(tempDir, 'runner')
3675
+ const zipBytes = new Uint8Array(await response.arrayBuffer())
3676
+ if (zipBytes.byteLength > RUNNER_ARTIFACT_MAX_BYTES) {
3677
+ rmSync(tempDir, { recursive: true, force: true })
3678
+ throw new Error(`Runner artifact exceeds ${RUNNER_ARTIFACT_MAX_BYTES} bytes`)
3679
+ }
3680
+ writeFileSync(zipPath, zipBytes)
3681
+
3682
+ const result = spawnSync('unzip', ['-q', zipPath, '-d', runnerDir], {
3683
+ encoding: 'utf8',
3684
+ })
3685
+ if (result.error) {
3686
+ rmSync(tempDir, { recursive: true, force: true })
3687
+ throw result.error
3688
+ }
3689
+ if (result.status !== 0) {
3690
+ rmSync(tempDir, { recursive: true, force: true })
3691
+ throw new Error(`unzip failed: ${sanitizeTerminalText(result.stderr || result.stdout || '')}`)
3692
+ }
3693
+
3694
+ return {
3695
+ runnerDir,
3696
+ cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
3697
+ }
3698
+ }
3699
+
3700
+ async function runnersExec() {
3701
+ const prompt = getArg('--prompt')
3702
+ const promptVersion = getArg('--prompt-version')
3703
+ const runnerVersion = getArg('--runner-version')
3704
+ const scorerVersion = getArg('--scorer-version')
3705
+ const datasetVersion = getArg('--dataset-version')
3706
+ const splitSet = getArg('--split-set')
3707
+ const split = getArg('--split')
3708
+ const runnerDirArg = getArg('--runner-dir')
3709
+ const outArg = getArg('--out')
3710
+
3711
+ if (
3712
+ (!prompt && !promptVersion && !scorerVersion) ||
3713
+ (scorerVersion && (prompt || promptVersion)) ||
3714
+ (promptVersion && !runnerVersion) ||
3715
+ !datasetVersion ||
3716
+ !splitSet ||
3717
+ !split ||
3718
+ !outArg
3719
+ ) {
3720
+ throw new Error(
3721
+ 'Usage: orizu runners exec (--prompt <prompt> | --prompt-version <id> --runner-version <id> | --scorer-version <id>) --dataset-version <id> --split-set <id-or-name> --split <name> [--runner-dir <dir>] --out <results.jsonl|results.jsonl.gz>'
3722
+ )
3723
+ }
3724
+
3725
+ const query = new URLSearchParams()
3726
+ if (scorerVersion) {
3727
+ query.set('scorerVersion', scorerVersion)
3728
+ } else if (promptVersion) {
3729
+ query.set('promptVersion', promptVersion)
3730
+ } else if (prompt) {
3731
+ query.set('prompt', prompt)
3732
+ }
3733
+ if (runnerVersion) {
3734
+ query.set('runnerVersion', runnerVersion)
3735
+ }
3736
+ query.set('datasetVersion', datasetVersion)
3737
+ query.set('splitSet', splitSet)
3738
+ query.set('split', split)
3739
+ const contextResponse = await authedFetch(`/api/cli/runners/exec-context?${query.toString()}`)
3740
+ if (!contextResponse.ok) {
3741
+ throw new Error(`Failed to fetch runner execution context: ${await contextResponse.text()}`)
3742
+ }
3743
+
3744
+ const context = await parseJsonResponse<RunnerExecContext>(contextResponse, 'Runner exec context')
3745
+ const materializedRunner = runnerDirArg
3746
+ ? { runnerDir: expandHomePath(runnerDirArg), cleanup: () => {} }
3747
+ : await materializeRunnerVersion(runnerVersion || context.prompt.runnerVersionId)
3748
+ const runnerDir = materializedRunner.runnerDir
3749
+ const manifest = readRunnerManifest(runnerDir)
3750
+
3751
+ try {
3752
+ if (manifest.supports_body_kind && !manifest.supports_body_kind.includes(context.prompt.bodyKind)) {
3753
+ throw new Error(
3754
+ `Runner does not support prompt body kind '${context.prompt.bodyKind}'. Supported kinds: ${manifest.supports_body_kind.join(', ')}`
3755
+ )
3756
+ }
3757
+
3758
+ const resultLines: string[] = []
3759
+ for (const row of context.rows) {
3760
+ const tempDir = mkdtempSync(join(tmpdir(), 'orizu-runner-'))
3761
+ const inputPath = join(tempDir, 'input.json')
3762
+ const outputPath = join(tempDir, 'output.json')
3763
+
3764
+ try {
3765
+ const modelOutput = row.row.model_output ?? row.row.modelOutput ?? row.row.output ?? null
3766
+ writeFileSync(inputPath, JSON.stringify({
3767
+ row: row.row,
3768
+ prompt: {
3769
+ body: context.prompt.body,
3770
+ body_kind: context.prompt.bodyKind,
3771
+ provider_settings: context.prompt.providerSettings,
3772
+ },
3773
+ subject: scorerVersion
3774
+ ? {
3775
+ type: 'scorer_row',
3776
+ row_id: row.id,
3777
+ scorer_version_id: context.scorer?.versionId || scorerVersion,
3778
+ prompt_version_id: context.prompt.promptVersionId,
3779
+ }
3780
+ : {
3781
+ type: 'prompt_version',
3782
+ row_id: row.id,
3783
+ prompt_version_id: context.prompt.promptVersionId,
3784
+ },
3785
+ scorer: scorerVersion
3786
+ ? {
3787
+ version_id: context.scorer?.versionId || scorerVersion,
3788
+ metric_key: context.scorer?.metricKey || 'score',
3789
+ higher_is_better: context.scorer?.higherIsBetter ?? true,
3790
+ }
3791
+ : null,
3792
+ model_output: modelOutput,
3793
+ prompt_version_id: context.prompt.promptVersionId,
3794
+ runner_version_id: context.prompt.runnerVersionId,
3795
+ run_id: null,
3796
+ }))
3797
+
3798
+ const result = spawnSync(manifest.command[0], manifest.command.slice(1), {
3799
+ cwd: runnerDir,
3800
+ env: runnerSubprocessEnv(inputPath, outputPath),
3801
+ encoding: 'utf8',
3802
+ maxBuffer: RUNNER_OUTPUT_MAX_BYTES,
3803
+ timeout: RUNNER_TIMEOUT_MS,
3804
+ })
3805
+
3806
+ if (result.error) {
3807
+ throw result.error
3808
+ }
3809
+
3810
+ if (result.status !== 0) {
3811
+ throw new Error(
3812
+ `Runner failed for row ${row.id} with exit code ${result.status}: ${sanitizeTerminalText(boundedRunnerOutput(result.stderr || result.stdout))}`
3813
+ )
3814
+ }
3815
+
3816
+ const runnerOutput = JSON.parse(readFileSync(outputPath, 'utf8')) as Record<string, unknown>
3817
+ resultLines.push(JSON.stringify({
3818
+ row_id: row.id,
3819
+ prompt_version_id: context.prompt.promptVersionId,
3820
+ runner_version_id: context.prompt.runnerVersionId,
3821
+ ...(scorerVersion ? { scorer_version_id: context.scorer?.versionId || scorerVersion } : {}),
3822
+ ...runnerOutput,
3823
+ }))
3824
+ } finally {
3825
+ rmSync(tempDir, { recursive: true, force: true })
3826
+ }
3827
+ }
3828
+
3829
+ const outPath = expandHomePath(outArg)
3830
+ const resultJsonl = `${resultLines.join('\n')}${resultLines.length > 0 ? '\n' : ''}`
3831
+ if (outPath.endsWith('.gz')) {
3832
+ writeFileSync(outPath, gzipSync(Buffer.from(resultJsonl, 'utf8')))
3833
+ } else {
3834
+ writeFileSync(outPath, resultJsonl)
3835
+ }
3836
+ printLine(`Wrote ${context.rows.length} runner results to ${sanitizeTerminalText(outPath)}`)
3837
+ } finally {
3838
+ materializedRunner.cleanup()
3839
+ }
3840
+ }
3841
+
2170
3842
  export async function main(rawArgs = process.argv.slice(2)) {
2171
3843
  const parsed = parseGlobalFlags(rawArgs)
2172
3844
  setGlobalFlags(parsed.flags)
@@ -2182,6 +3854,7 @@ export async function main(rawArgs = process.argv.slice(2)) {
2182
3854
 
2183
3855
  if (!command) {
2184
3856
  printUsage()
3857
+ printOptimizationUsage()
2185
3858
  process.exit(1)
2186
3859
  }
2187
3860
 
@@ -2200,6 +3873,16 @@ export async function main(rawArgs = process.argv.slice(2)) {
2200
3873
  return
2201
3874
  }
2202
3875
 
3876
+ if (command === 'env') {
3877
+ await printEnv()
3878
+ return
3879
+ }
3880
+
3881
+ if (command === 'log') {
3882
+ await logOptimizationEvent()
3883
+ return
3884
+ }
3885
+
2203
3886
  if (command === 'teams' && subcommand === 'list') {
2204
3887
  await listTeams()
2205
3888
  return
@@ -2238,6 +3921,120 @@ export async function main(rawArgs = process.argv.slice(2)) {
2238
3921
  return
2239
3922
  }
2240
3923
 
3924
+ if (command === 'prompts' && subcommand === 'list') {
3925
+ await listPrompts()
3926
+ return
3927
+ }
3928
+
3929
+ if (command === 'prompts' && subcommand === 'pull') {
3930
+ await pullPromptArtifact('prompt')
3931
+ return
3932
+ }
3933
+
3934
+ if (command === 'prompts' && subcommand === 'push') {
3935
+ await pushPromptArtifact('prompt')
3936
+ return
3937
+ }
3938
+
3939
+ if (command === 'prompts' && subcommand === 'labels' && cliArgs[2] === 'set') {
3940
+ await setPromptLabel()
3941
+ return
3942
+ }
3943
+
3944
+ if (command === 'prompts' && subcommand === 'scorers' && cliArgs[2] === 'set-headline') {
3945
+ await bindPromptScorer('headline')
3946
+ return
3947
+ }
3948
+
3949
+ if (command === 'prompts' && subcommand === 'scorers' && cliArgs[2] === 'add') {
3950
+ await bindPromptScorer('tracked')
3951
+ return
3952
+ }
3953
+
3954
+ if (command === 'judges' && subcommand === 'list') {
3955
+ await listJudges()
3956
+ return
3957
+ }
3958
+
3959
+ if (command === 'judges' && subcommand === 'pull') {
3960
+ await pullPromptArtifact('judge')
3961
+ return
3962
+ }
3963
+
3964
+ if (command === 'judges' && subcommand === 'push') {
3965
+ await pushPromptArtifact('judge')
3966
+ return
3967
+ }
3968
+
3969
+ if (command === 'scorers' && subcommand === 'list') {
3970
+ await listScorers()
3971
+ return
3972
+ }
3973
+
3974
+ if (command === 'scorers' && subcommand === 'register') {
3975
+ await registerScorer()
3976
+ return
3977
+ }
3978
+
3979
+ if (command === 'scorers' && subcommand === 'detail') {
3980
+ await showScorerDetail()
3981
+ return
3982
+ }
3983
+
3984
+ if (command === 'scorers' && subcommand === 'labels' && cliArgs[2] === 'set') {
3985
+ await setScorerLabel()
3986
+ return
3987
+ }
3988
+
3989
+ if (command === 'runners' && subcommand === 'push') {
3990
+ await pushRunnerArtifact('runner')
3991
+ return
3992
+ }
3993
+
3994
+ if (command === 'runners' && subcommand === 'exec') {
3995
+ await runnersExec()
3996
+ return
3997
+ }
3998
+
3999
+ if (command === 'optimizers' && subcommand === 'push') {
4000
+ await pushRunnerArtifact('optimizer')
4001
+ return
4002
+ }
4003
+
4004
+ if (command === 'runs' && subcommand === 'submit') {
4005
+ await submitRunResults()
4006
+ return
4007
+ }
4008
+
4009
+ if (command === 'scores' && subcommand === 'submit') {
4010
+ await submitScoreResults()
4011
+ return
4012
+ }
4013
+
4014
+ if (command === 'optimizations' && subcommand === 'start') {
4015
+ await startOptimizationRun()
4016
+ return
4017
+ }
4018
+
4019
+ if (command === 'optimizations' && subcommand === 'run-gepa') {
4020
+ await runGepaOptimization()
4021
+ return
4022
+ }
4023
+
4024
+ if (
4025
+ command === 'optimizations' &&
4026
+ (
4027
+ subcommand === 'pause' ||
4028
+ subcommand === 'resume' ||
4029
+ subcommand === 'finish' ||
4030
+ subcommand === 'fail' ||
4031
+ subcommand === 'cancel'
4032
+ )
4033
+ ) {
4034
+ await updateOptimizationRunLifecycle(subcommand)
4035
+ return
4036
+ }
4037
+
2241
4038
  if (command === 'apps' && subcommand === 'list') {
2242
4039
  await listApps()
2243
4040
  return
@@ -2298,6 +4095,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
2298
4095
  return
2299
4096
  }
2300
4097
 
4098
+ if (command === 'datasets' && subcommand === 'push') {
4099
+ await pushDataset()
4100
+ return
4101
+ }
4102
+
2301
4103
  if (command === 'datasets' && subcommand === 'download') {
2302
4104
  await downloadDataset()
2303
4105
  return
@@ -2308,6 +4110,17 @@ export async function main(rawArgs = process.argv.slice(2)) {
2308
4110
  return
2309
4111
  }
2310
4112
 
4113
+ const datasetsAction = cliArgs[2]
4114
+ if (command === 'datasets' && subcommand === 'versions' && datasetsAction === 'create') {
4115
+ await createDatasetVersion()
4116
+ return
4117
+ }
4118
+
4119
+ if (command === 'datasets' && subcommand === 'splits' && datasetsAction === 'create') {
4120
+ await createDatasetSplitSet()
4121
+ return
4122
+ }
4123
+
2311
4124
  if (command === 'datasets' && subcommand === 'edit-rows') {
2312
4125
  await editDatasetRows()
2313
4126
  return
@@ -2339,10 +4152,24 @@ export async function main(rawArgs = process.argv.slice(2)) {
2339
4152
  }
2340
4153
 
2341
4154
  printUsage()
4155
+ printOptimizationUsage()
2342
4156
  process.exit(1)
2343
4157
  }
2344
4158
 
2345
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
4159
+ function isCliEntrypoint(): boolean {
4160
+ const entry = process.argv[1]
4161
+ if (!entry) {
4162
+ return false
4163
+ }
4164
+
4165
+ try {
4166
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url))
4167
+ } catch {
4168
+ return false
4169
+ }
4170
+ }
4171
+
4172
+ if (isCliEntrypoint()) {
2346
4173
  main().catch(error => {
2347
4174
  console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'))
2348
4175
  process.exit(1)