orizu 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ import { parseDatasetFile } from './file-parser.js'
24
24
  import { streamJsonlRowChunks } from './jsonl-stream.js'
25
25
  import { parseDatasetReference } from './dataset-download.js'
26
26
  import { parseGlobalFlags } from './global-flags.js'
27
+ import { runLocalAppPreview } from './preview-runtime.js'
27
28
  import {
28
29
  assertSecureTokenTransport,
29
30
  authedFetch,
@@ -163,6 +164,12 @@ interface ScorerSummary {
163
164
  implementationKind: string
164
165
  }
165
166
 
167
+ interface ScoreSubmitInput {
168
+ resultsJsonl?: string
169
+ aggregate?: Record<string, unknown>
170
+ aggregateWarning?: string
171
+ }
172
+
166
173
  interface RunnerExecContext {
167
174
  prompt: {
168
175
  body: string | null
@@ -185,6 +192,8 @@ interface RunnerExecContext {
185
192
 
186
193
  type DatasetUploadSourceType = 'csv' | 'json' | 'jsonl'
187
194
 
195
+ const MAX_README_LENGTH = 200_000
196
+
188
197
  interface DatasetUploadResponse {
189
198
  dataset: {
190
199
  id: string
@@ -193,6 +202,11 @@ interface DatasetUploadResponse {
193
202
  sourceType: string
194
203
  url?: string
195
204
  }
205
+ readmeVersion?: {
206
+ id: string
207
+ version_num: number
208
+ created_at: string
209
+ } | null
196
210
  }
197
211
 
198
212
  interface TeamMember {
@@ -256,11 +270,16 @@ function printVersion() {
256
270
  }
257
271
 
258
272
  function printUsage() {
259
- 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 comments <prompt-id-or-name> --project <team/project> [--label <label> | --version <id>] [--json]\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>]`)
273
+ 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 comments <prompt-id-or-name> --project <team/project> [--label <label> | --version <id>] [--json]\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 scorers exec --scorer-version <id> (--subject-version <prompt-version-id> | --optimization-run <id> --candidate <id>) --dataset-version <id> --split-set <id> --split <name> [--subject-results <jsonl>] [--dependency-score-run <alias=id>] [--dependency-results <alias=path>] [--no-submit] [--out <score.json>] [--project <team/project>] [--json]\n orizu scores submit <results.jsonl|results.json> --scorer-version <id> (--subject-version <prompt-version-id> | --optimization-run <id> --candidate <id>) [--aggregate] [--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 apps export [--app <appId>] [--project <team/project>] [--version <n>] [--out <path>]\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>] [--readme-file <README.md> | --readme-text <markdown>]\n orizu datasets push <path> [--project <team/project>] [--name <name>] [--readme-file <README.md> | --readme-text <markdown>] [--json]\n orizu datasets readme set <datasetId|dataset-name> [--project <team/project>] (--readme-file <README.md> | --readme-text <markdown>) [--json]\n orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--readme-file <README.md> | --readme-text <markdown>] [--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>]`)
274
+ printPreviewUsage()
275
+ }
276
+
277
+ function printPreviewUsage() {
278
+ printLine(`\nLocal app preview:\n\n orizu apps preview --file <path> --input-schema <json-path> --output-schema <json-path> --sample-row <json-path> [--screenshot <png-path>] [--headed] [--keep-open] [--component <name>]`)
260
279
  }
261
280
 
262
281
  function printOptimizationUsage() {
263
- 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] [--log-dir logs]\n orizu optimizations export <run-id> [--out <path>] [--json]\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]`)
282
+ 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] [--log-dir logs]\n orizu optimizations export <run-id> [--out <path>] [--json]\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>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--json]`)
264
283
  }
265
284
 
266
285
  let cliArgs = process.argv.slice(2)
@@ -295,6 +314,25 @@ export function expandHomePath(path: string): string {
295
314
  return path
296
315
  }
297
316
 
317
+ function readReadmeMarkdownFromArgs(usage: string): string | null {
318
+ const readmeFile = getArg('--readme-file')
319
+ const readmeText = getArg('--readme-text')
320
+
321
+ if (readmeFile !== null && readmeText !== null) {
322
+ throw new Error(`Use either --readme-file or --readme-text, not both.\n${usage}`)
323
+ }
324
+
325
+ const markdown = readmeFile !== null
326
+ ? readFileSync(expandHomePath(readmeFile), 'utf8')
327
+ : readmeText
328
+
329
+ if (markdown !== null && markdown.length > MAX_README_LENGTH) {
330
+ throw new Error(`README markdown is too large. Maximum size is ${MAX_README_LENGTH} characters.`)
331
+ }
332
+
333
+ return markdown
334
+ }
335
+
298
336
  function createCodeVerifier(): string {
299
337
  return randomBytes(32).toString('base64url')
300
338
  }
@@ -1807,16 +1845,21 @@ async function createDatasetVersion() {
1807
1845
  const datasetId = getPositionalArg(3) || getArg('--dataset')
1808
1846
  const versionLabel = getArg('--label') || getArg('--version-label') || null
1809
1847
  const project = getArg('--project')
1848
+ const usage = 'Usage: orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--readme-file <README.md> | --readme-text <markdown>] [--json]'
1810
1849
 
1811
1850
  if (!datasetId) {
1812
- throw new Error('Usage: orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--json]')
1851
+ throw new Error(usage)
1813
1852
  }
1814
1853
 
1854
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
1815
1855
  const resolvedDatasetId = await resolveDatasetIdForProject(datasetId, project)
1816
1856
  const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(resolvedDatasetId)}/versions`, {
1817
1857
  method: 'POST',
1818
1858
  headers: { 'Content-Type': 'application/json' },
1819
- body: JSON.stringify({ versionLabel }),
1859
+ body: JSON.stringify({
1860
+ versionLabel,
1861
+ ...(readmeMarkdown !== null ? { readmeMarkdown } : {}),
1862
+ }),
1820
1863
  })
1821
1864
 
1822
1865
  if (!response.ok) {
@@ -1970,18 +2013,23 @@ async function submitScoreResults() {
1970
2013
  const candidateId = getArg('--candidate')
1971
2014
 
1972
2015
  if (!resultsPath || !scorerVersionId || (!subjectPromptVersionId && (!optimizationRunId || !candidateId))) {
1973
- 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]')
2016
+ 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>) [--aggregate] [--dataset-version <id> --split-set <id> --split <name>] [--json]')
1974
2017
  }
1975
2018
 
1976
2019
  const resultBytes = readSourceBytes(resultsPath)
1977
2020
  const raw = resultsPath.endsWith('.gz')
1978
2021
  ? gunzipSync(resultBytes).toString('utf8')
1979
2022
  : resultBytes.toString('utf8')
1980
- const resultsJsonl = normalizeScoreResultsInput(resultsPath, raw)
2023
+ const scoreInput = normalizeScoreResultsInput(resultsPath, raw, hasArg('--aggregate'))
2024
+ if (scoreInput.aggregateWarning) {
2025
+ console.warn(scoreInput.aggregateWarning)
2026
+ }
2027
+ const aggregatePayload = scoreInput.aggregate || {}
1981
2028
  const response = await authedFetch(`/api/cli/scores/submit?project=${encodeURIComponent(project)}`, {
1982
2029
  method: 'POST',
1983
2030
  headers: { 'Content-Type': 'application/json' },
1984
2031
  body: JSON.stringify({
2032
+ ...aggregatePayload,
1985
2033
  scorerVersionId,
1986
2034
  subjectPromptVersionId: subjectPromptVersionId || undefined,
1987
2035
  datasetVersionId: getArg('--dataset-version') || undefined,
@@ -1989,7 +2037,7 @@ async function submitScoreResults() {
1989
2037
  splitName: getArg('--split') || undefined,
1990
2038
  optimizationRunId: optimizationRunId || undefined,
1991
2039
  candidateId: candidateId || undefined,
1992
- resultsJsonl,
2040
+ resultsJsonl: scoreInput.resultsJsonl,
1993
2041
  }),
1994
2042
  })
1995
2043
 
@@ -2015,24 +2063,161 @@ async function submitScoreResults() {
2015
2063
  )
2016
2064
  }
2017
2065
 
2018
- function normalizeScoreResultsInput(sourcePath: string, raw: string): string {
2066
+ function looksLikeAggregateScoreObject(value: Record<string, unknown>): boolean {
2067
+ return (
2068
+ value.scoreValue !== undefined ||
2069
+ value.score_value !== undefined ||
2070
+ value.diagnostics !== undefined ||
2071
+ value.feedbackSummary !== undefined ||
2072
+ value.feedback_summary !== undefined ||
2073
+ value.rowEvidence !== undefined ||
2074
+ value.row_evidence !== undefined ||
2075
+ value.dependencyScoreRunIds !== undefined ||
2076
+ value.dependency_score_run_ids !== undefined
2077
+ )
2078
+ }
2079
+
2080
+ function numberFromUnknown(value: unknown): number | null {
2081
+ if (typeof value === 'number' && Number.isFinite(value)) return value
2082
+ if (typeof value === 'string' && value.trim()) {
2083
+ const parsed = Number(value)
2084
+ return Number.isFinite(parsed) ? parsed : null
2085
+ }
2086
+ if (typeof value === 'boolean') return value ? 1 : 0
2087
+ return null
2088
+ }
2089
+
2090
+ function normalizeScoreResultsInput(sourcePath: string, raw: string, aggregateMode = false): ScoreSubmitInput {
2019
2091
  const logicalPath = sourcePath.endsWith('.gz') ? sourcePath.slice(0, -3) : sourcePath
2020
- if (!logicalPath.endsWith('.json')) return raw
2092
+ if (!logicalPath.endsWith('.json')) return { resultsJsonl: raw }
2021
2093
 
2022
2094
  try {
2023
2095
  const parsed = JSON.parse(raw) as unknown
2096
+ if (aggregateMode) {
2097
+ if (!isRecord(parsed)) {
2098
+ throw new Error('Aggregate score results JSON must be an object')
2099
+ }
2100
+ return { aggregate: parsed }
2101
+ }
2102
+ if (isRecord(parsed) && looksLikeAggregateScoreObject(parsed)) {
2103
+ return {
2104
+ aggregate: parsed,
2105
+ aggregateWarning: 'Warning: aggregate score JSON detected. Pass --aggregate to make this explicit.',
2106
+ }
2107
+ }
2024
2108
  const rows = Array.isArray(parsed) ? parsed : [parsed]
2025
2109
  if (rows.some(row => !isRecord(row))) {
2026
2110
  throw new Error('JSON score results must be an object or an array of objects')
2027
2111
  }
2028
- return rows.map(row => JSON.stringify(row)).join('\n')
2112
+ return { resultsJsonl: rows.map(row => JSON.stringify(row)).join('\n') }
2029
2113
  } catch (error) {
2030
2114
  const message = error instanceof Error ? error.message : String(error)
2031
2115
  throw new Error(`Invalid score results JSON '${sourcePath}': ${message}`)
2032
2116
  }
2033
2117
  }
2034
2118
 
2119
+ function readOptionalJsonlFile(pathArg: string | null): string | undefined {
2120
+ if (!pathArg) return undefined
2121
+ const bytes = readSourceBytes(pathArg)
2122
+ return pathArg.endsWith('.gz')
2123
+ ? gunzipSync(bytes).toString('utf8')
2124
+ : bytes.toString('utf8')
2125
+ }
2126
+
2127
+ function parseAliasAssignments(value: string | null): Record<string, string> {
2128
+ const result: Record<string, string> = {}
2129
+ if (!value) return result
2130
+ for (const item of parseCommaSeparated(value)) {
2131
+ const equalsIndex = item.indexOf('=')
2132
+ if (equalsIndex <= 0 || equalsIndex === item.length - 1) {
2133
+ throw new Error(`Expected alias assignment in form alias=value, got '${item}'`)
2134
+ }
2135
+ result[item.slice(0, equalsIndex)] = item.slice(equalsIndex + 1)
2136
+ }
2137
+ return result
2138
+ }
2139
+
2140
+ function readDependencyResultAssignments(value: string | null): Record<string, string> | undefined {
2141
+ const assignments = parseAliasAssignments(value)
2142
+ const result: Record<string, string> = {}
2143
+ for (const [alias, path] of Object.entries(assignments)) {
2144
+ result[alias] = readOptionalJsonlFile(path) || ''
2145
+ }
2146
+ return Object.keys(result).length > 0 ? result : undefined
2147
+ }
2148
+
2149
+ async function execScorer() {
2150
+ const project = getArg('--project') || await resolveProjectSlug(null)
2151
+ const scorerVersionId = getArg('--scorer-version')
2152
+ const subjectPromptVersionId = getArg('--subject-version') || getArg('--prompt-version')
2153
+ const optimizationRunId = getArg('--optimization-run')
2154
+ const candidateId = getArg('--candidate')
2155
+ const datasetVersionId = getArg('--dataset-version')
2156
+ const splitSetId = getArg('--split-set')
2157
+ const splitName = getArg('--split')
2158
+ const outPath = getArg('--out')
2159
+
2160
+ if (
2161
+ !scorerVersionId ||
2162
+ (!subjectPromptVersionId && (!optimizationRunId || !candidateId)) ||
2163
+ !datasetVersionId ||
2164
+ !splitSetId ||
2165
+ !splitName
2166
+ ) {
2167
+ throw new Error('Usage: orizu scorers exec --scorer-version <id> (--subject-version <prompt-version-id> | --optimization-run <id> --candidate <id>) --dataset-version <id> --split-set <id> --split <name> [--subject-results <jsonl>] [--dependency-score-run <alias=id>] [--dependency-results <alias=path>] [--no-submit] [--out <score.json>] [--project <team/project>] [--json]')
2168
+ }
2169
+
2170
+ const response = await authedFetch(`/api/cli/scorers/exec?project=${encodeURIComponent(project)}`, {
2171
+ method: 'POST',
2172
+ headers: { 'Content-Type': 'application/json' },
2173
+ body: JSON.stringify({
2174
+ scorerVersionId,
2175
+ subjectPromptVersionId: subjectPromptVersionId || undefined,
2176
+ optimizationRunId: optimizationRunId || undefined,
2177
+ candidateId: candidateId || undefined,
2178
+ datasetVersionId,
2179
+ splitSetId,
2180
+ splitName,
2181
+ submit: !hasArg('--no-submit'),
2182
+ reuseExisting: !hasArg('--no-reuse-existing'),
2183
+ subjectResultsJsonl: readOptionalJsonlFile(getArg('--subject-results') || getArg('--outputs')),
2184
+ dependencyScoreRunIds: parseAliasAssignments(getArg('--dependency-score-run') || getArg('--dependency-score-runs')),
2185
+ dependencyResultsJsonl: readDependencyResultAssignments(getArg('--dependency-results')),
2186
+ }),
2187
+ })
2188
+
2189
+ if (!response.ok) {
2190
+ throw new Error(`Failed to execute scorer: ${await response.text()}`)
2191
+ }
2192
+
2193
+ const data = await parseJsonResponse<Record<string, unknown>>(response, 'Scorer exec')
2194
+ if (outPath) {
2195
+ writeFileSync(expandHomePath(outPath), `${JSON.stringify(data.scoreResult || data, null, 2)}\n`)
2196
+ }
2197
+ if (hasJsonFlag()) {
2198
+ printJson(data)
2199
+ return
2200
+ }
2201
+
2202
+ const scoreRun = isRecord(data.scoreRun) ? data.scoreRun : null
2203
+ const scoreResult = isRecord(data.scoreResult) ? data.scoreResult : null
2204
+ const scoreValue = scoreRun
2205
+ ? scoreRun.scoreValue
2206
+ : scoreResult
2207
+ ? scoreResult.scoreValue
2208
+ : null
2209
+ if (scoreRun && typeof scoreRun.id === 'string') {
2210
+ printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} -> score ${sanitizeTerminalText(scoreRun.id)} (${formatPercent(numberFromUnknown(scoreValue))})`)
2211
+ } else {
2212
+ printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} (${formatPercent(numberFromUnknown(scoreValue))})`)
2213
+ }
2214
+ if (outPath) {
2215
+ printLine(`Wrote scorer result to ${sanitizeTerminalText(outPath)}`)
2216
+ }
2217
+ }
2218
+
2035
2219
  type OptimizationLifecycleAction = 'pause' | 'resume' | 'finish' | 'fail' | 'cancel'
2220
+ const OPTIMIZATION_REPORT_MAX_BYTES = 2 * 1024 * 1024
2036
2221
 
2037
2222
  function parseOptionalNumberFlag(name: string): number | undefined {
2038
2223
  const value = getArg(name)
@@ -2052,6 +2237,46 @@ function hasObjectKeys(value: Record<string, unknown>): boolean {
2052
2237
  return Object.keys(value).length > 0
2053
2238
  }
2054
2239
 
2240
+ function readOptimizationReportInput(): { markdown: string; sourceName: string | null } | null {
2241
+ const report = getArg('--report')
2242
+ const reportFile = getArg('--report-file')
2243
+
2244
+ if (report && reportFile) {
2245
+ throw new Error('Use either --report or --report-file, not both')
2246
+ }
2247
+
2248
+ if (!report && !reportFile) {
2249
+ return null
2250
+ }
2251
+
2252
+ let markdown: string
2253
+ let sourceName: string | null
2254
+
2255
+ if (reportFile) {
2256
+ const expandedPath = expandHomePath(reportFile)
2257
+ markdown = readSourceFile(reportFile)
2258
+ sourceName = basename(expandedPath)
2259
+ } else if (report?.startsWith('@')) {
2260
+ const path = report.slice(1)
2261
+ const expandedPath = expandHomePath(path)
2262
+ markdown = readSourceFile(path)
2263
+ sourceName = basename(expandedPath)
2264
+ } else {
2265
+ markdown = report || ''
2266
+ sourceName = 'inline'
2267
+ }
2268
+
2269
+ if (!markdown.trim()) {
2270
+ throw new Error('Optimization report markdown must not be blank')
2271
+ }
2272
+
2273
+ if (Buffer.byteLength(markdown, 'utf8') > OPTIMIZATION_REPORT_MAX_BYTES) {
2274
+ throw new Error(`Optimization report exceeds ${OPTIMIZATION_REPORT_MAX_BYTES} bytes`)
2275
+ }
2276
+
2277
+ return { markdown, sourceName }
2278
+ }
2279
+
2055
2280
  async function startOptimizationRun() {
2056
2281
  const project = getArg('--project') || await resolveProjectSlug(null)
2057
2282
  const optimizerVersionId = getArg('--optimizer-version')
@@ -2156,6 +2381,11 @@ async function updateOptimizationRunLifecycle(action: OptimizationLifecycleActio
2156
2381
  }
2157
2382
  const metadata = readJsonObjectArg(getArg('--metadata'), 'optimization metadata')
2158
2383
  const reason = getArg('--reason')
2384
+ const report = readOptimizationReportInput()
2385
+
2386
+ if (report && (action === 'pause' || action === 'resume')) {
2387
+ throw new Error(`orizu optimizations ${action} does not accept optimization reports`)
2388
+ }
2159
2389
 
2160
2390
  if ((action === 'pause' || action === 'cancel') && reason) {
2161
2391
  metadata.reason = reason
@@ -2170,6 +2400,11 @@ async function updateOptimizationRunLifecycle(action: OptimizationLifecycleActio
2170
2400
  body.metadata = metadata
2171
2401
  }
2172
2402
 
2403
+ if (report) {
2404
+ body.reportMarkdown = report.markdown
2405
+ body.reportSourceName = report.sourceName
2406
+ }
2407
+
2173
2408
  if (action === 'finish') {
2174
2409
  const bestScore = parseOptionalNumberFlag('--best-score')
2175
2410
  if (bestScore !== undefined) {
@@ -2707,15 +2942,61 @@ async function createAppFromFile() {
2707
2942
  }
2708
2943
 
2709
2944
  const data = await parseJsonResponse<{
2710
- app: { id: string; name: string; versionNum: number; componentName?: string }
2945
+ app: { id: string; name: string; versionNum: number; componentName?: string; url?: string }
2711
2946
  warnings?: string[]
2712
2947
  }>(response, 'App create')
2713
2948
  printLine(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`)
2949
+ if (data.app.url) {
2950
+ printLine(`View app: ${formatTerminalLink(data.app.url)}`)
2951
+ }
2714
2952
  if (data.warnings?.length) {
2715
2953
  printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
2716
2954
  }
2717
2955
  }
2718
2956
 
2957
+ async function previewAppFromFile() {
2958
+ const filePath = getArg('--file')
2959
+ const inputSchemaPath = getArg('--input-schema')
2960
+ const outputSchemaPath = getArg('--output-schema')
2961
+ const sampleRowPath = getArg('--sample-row')
2962
+ const screenshotPath = getArg('--screenshot')
2963
+ const component = getArg('--component') || undefined
2964
+ const headed = hasArg('--headed')
2965
+ const keepOpen = hasArg('--keep-open')
2966
+
2967
+ if (!filePath || !inputSchemaPath || !outputSchemaPath || !sampleRowPath) {
2968
+ throw new Error('Usage: orizu apps preview --file <path> --input-schema <json-path> --output-schema <json-path> --sample-row <json-path> [--screenshot <png-path>] [--headed] [--keep-open] [--component <name>]')
2969
+ }
2970
+
2971
+ const expandedFilePath = realpathSync(expandHomePath(filePath))
2972
+ const inputJsonSchema = readJsonFile(inputSchemaPath)
2973
+ const outputJsonSchema = readJsonFile(outputSchemaPath)
2974
+ const sampleRow = readJsonFile(sampleRowPath)
2975
+ const expandedScreenshotPath = screenshotPath ? expandHomePath(screenshotPath) : undefined
2976
+
2977
+ const result = await runLocalAppPreview({
2978
+ filePath: expandedFilePath,
2979
+ inputSchema: inputJsonSchema,
2980
+ outputSchema: outputJsonSchema,
2981
+ sampleRow,
2982
+ screenshotPath: expandedScreenshotPath,
2983
+ componentName: component,
2984
+ headed,
2985
+ keepOpen,
2986
+ })
2987
+
2988
+ printLine(`Preview rendered: ${formatTerminalLink(result.url)}`)
2989
+ if (result.screenshotPath) {
2990
+ printLine(`Screenshot: ${sanitizeTerminalText(result.screenshotPath)}`)
2991
+ }
2992
+ if (headed || keepOpen) {
2993
+ printLine('Headed preview is running. Close Chromium or stop the command when you are done.')
2994
+ }
2995
+ if (result.warnings.length > 0) {
2996
+ printLine(`Warnings: ${sanitizeTerminalText(result.warnings.join('; '))}`)
2997
+ }
2998
+ }
2999
+
2719
3000
  async function updateAppFromFile() {
2720
3001
  const filePath = getArg('--file')
2721
3002
  const inputSchemaPath = getArg('--input-schema')
@@ -3023,6 +3304,28 @@ interface AppDetailPayload {
3023
3304
  projectName: string
3024
3305
  }
3025
3306
 
3307
+ interface AppExportPayload {
3308
+ app: {
3309
+ id: string
3310
+ name: string
3311
+ }
3312
+ version: {
3313
+ id: string
3314
+ versionNum: number
3315
+ createdAt?: string
3316
+ code: string
3317
+ }
3318
+ }
3319
+
3320
+ function defaultAppExportFilename(appName: string, versionNum: number): string {
3321
+ const safeName = appName
3322
+ .trim()
3323
+ .replace(/[^A-Za-z0-9._-]+/g, '_')
3324
+ .replace(/^_+|_+$/g, '')
3325
+ .slice(0, 100)
3326
+ return `${safeName || 'app'}.v${versionNum}.tsx`
3327
+ }
3328
+
3026
3329
  async function appDetail() {
3027
3330
  const appId = getArg('--app')
3028
3331
  const project = getArg('--project')
@@ -3068,6 +3371,50 @@ async function appDetail() {
3068
3371
  printLine(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`)
3069
3372
  }
3070
3373
 
3374
+ async function exportAppSource() {
3375
+ const project = getArg('--project')
3376
+ const versionArg = getArg('--version')
3377
+ const outPathArg = getArg('--out')
3378
+ let appId = getArg('--app')
3379
+
3380
+ const versionNum = versionArg && /^[1-9]\d*$/.test(versionArg)
3381
+ ? Number.parseInt(versionArg, 10)
3382
+ : undefined
3383
+
3384
+ if (versionArg && versionNum === undefined) {
3385
+ throw new Error('--version must be a positive integer')
3386
+ }
3387
+
3388
+ if (!appId) {
3389
+ const selected = await selectAppIdInteractively(project)
3390
+ appId = selected.appId
3391
+ }
3392
+
3393
+ const query = new URLSearchParams()
3394
+ if (project) {
3395
+ query.set('project', project)
3396
+ }
3397
+ if (versionNum) {
3398
+ query.set('version', String(versionNum))
3399
+ }
3400
+
3401
+ const suffix = query.toString() ? `?${query.toString()}` : ''
3402
+ const response = await authedFetch(`/api/cli/apps/${encodeURIComponent(appId)}/export${suffix}`)
3403
+ if (!response.ok) {
3404
+ throw new Error(`Failed to export app (${response.status}): ${await response.text()}`)
3405
+ }
3406
+
3407
+ const data = await parseJsonResponse<AppExportPayload>(response, 'App export')
3408
+ const filename = outPathArg
3409
+ ? expandHomePath(outPathArg)
3410
+ : defaultAppExportFilename(data.app.name, data.version.versionNum)
3411
+
3412
+ writeTextFileEnsuringDir(filename, data.version.code)
3413
+ printLine(
3414
+ `Saved app ${sanitizeTerminalText(data.app.name)} v${data.version.versionNum} source to ${sanitizeTerminalText(filename)}`
3415
+ )
3416
+ }
3417
+
3071
3418
  async function listTeamMembers() {
3072
3419
  const teamSlug = await resolveTeamSlug(getArg('--team'))
3073
3420
 
@@ -3154,7 +3501,8 @@ async function createDatasetFromRows(
3154
3501
  project: string,
3155
3502
  name: string,
3156
3503
  sourceType: DatasetUploadSourceType,
3157
- rows: Array<Record<string, unknown>>
3504
+ rows: Array<Record<string, unknown>>,
3505
+ readmeMarkdown: string | null = null
3158
3506
  ): Promise<DatasetUploadResponse> {
3159
3507
  const response = await authedFetch('/api/cli/datasets/upload', {
3160
3508
  method: 'POST',
@@ -3166,6 +3514,7 @@ async function createDatasetFromRows(
3166
3514
  name,
3167
3515
  rows,
3168
3516
  sourceType,
3517
+ ...(readmeMarkdown !== null ? { readmeMarkdown } : {}),
3169
3518
  }),
3170
3519
  })
3171
3520
 
@@ -3180,7 +3529,8 @@ async function createDatasetFromRows(
3180
3529
  async function uploadJsonlDatasetInChunks(
3181
3530
  file: string,
3182
3531
  project: string,
3183
- datasetName: string
3532
+ datasetName: string,
3533
+ readmeMarkdown: string | null = null
3184
3534
  ) {
3185
3535
  let dataset: DatasetUploadResponse['dataset'] | null = null
3186
3536
  let totalUploaded = 0
@@ -3215,7 +3565,7 @@ async function uploadJsonlDatasetInChunks(
3215
3565
 
3216
3566
  try {
3217
3567
  if (!dataset) {
3218
- const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk)
3568
+ const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk, readmeMarkdown)
3219
3569
  dataset = data.dataset
3220
3570
  totalUploaded = data.dataset.rowCount
3221
3571
  continue
@@ -3256,22 +3606,24 @@ async function uploadDataset() {
3256
3606
  const projectArg = getArg('--project')
3257
3607
  const fileArg = getArg('--file')
3258
3608
  const name = getArg('--name')
3609
+ const usage = 'Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>] [--readme-file <README.md> | --readme-text <markdown>]'
3259
3610
 
3260
3611
  if (!fileArg) {
3261
- throw new Error('Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>]')
3612
+ throw new Error(usage)
3262
3613
  }
3263
3614
 
3615
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
3264
3616
  const file = expandHomePath(fileArg)
3265
3617
  const project = await resolveProjectSlug(projectArg)
3266
3618
  const datasetName = name || basename(file)
3267
3619
 
3268
3620
  if (extname(file).toLowerCase() === '.jsonl') {
3269
- await uploadJsonlDatasetInChunks(file, project, datasetName)
3621
+ await uploadJsonlDatasetInChunks(file, project, datasetName, readmeMarkdown)
3270
3622
  return
3271
3623
  }
3272
3624
 
3273
3625
  const { rows, sourceType } = parseDatasetFile(file)
3274
- const data = await createDatasetFromRows(project, datasetName, sourceType, rows)
3626
+ const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown)
3275
3627
 
3276
3628
  printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`)
3277
3629
  if (data.dataset.url) {
@@ -3283,23 +3635,29 @@ async function pushDataset() {
3283
3635
  const projectArg = getArg('--project')
3284
3636
  const fileArg = getPositionalArg(2) || getArg('--file')
3285
3637
  const name = getArg('--name')
3638
+ const usage = 'Usage: orizu datasets push <rows.csv|rows.json|rows.jsonl> [--project <team/project>] [--name <name>] [--readme-file <README.md> | --readme-text <markdown>] [--json]'
3286
3639
 
3287
3640
  if (!fileArg) {
3288
- throw new Error('Usage: orizu datasets push <rows.csv|rows.json|rows.jsonl> [--project <team/project>] [--name <name>] [--json]')
3641
+ throw new Error(usage)
3289
3642
  }
3290
3643
 
3644
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
3291
3645
  const file = expandHomePath(fileArg)
3292
3646
  const project = await resolveProjectSlug(projectArg)
3293
3647
  const datasetName = name || basename(file)
3294
3648
  const { rows, sourceType } = parseDatasetFile(file)
3295
- const data = await createDatasetFromRows(project, datasetName, sourceType, rows)
3649
+ const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown)
3296
3650
 
3297
3651
  if (hasJsonFlag()) {
3298
- printJson({
3652
+ const payload: Record<string, unknown> = {
3299
3653
  dataset_id: data.dataset.id,
3300
3654
  name: data.dataset.name,
3301
3655
  row_count: data.dataset.rowCount,
3302
- })
3656
+ }
3657
+ if (data.readmeVersion?.id) {
3658
+ payload.readme_version_id = data.readmeVersion.id
3659
+ }
3660
+ printJson(payload)
3303
3661
  return
3304
3662
  }
3305
3663
 
@@ -3309,6 +3667,55 @@ async function pushDataset() {
3309
3667
  }
3310
3668
  }
3311
3669
 
3670
+ async function setDatasetReadme() {
3671
+ const datasetId = getPositionalArg(3) || getArg('--dataset')
3672
+ const project = getArg('--project')
3673
+ const usage = 'Usage: orizu datasets readme set <datasetId|dataset-name> [--project <team/project>] (--readme-file <README.md> | --readme-text <markdown>) [--json]'
3674
+
3675
+ if (!datasetId) {
3676
+ throw new Error(usage)
3677
+ }
3678
+
3679
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
3680
+ if (readmeMarkdown === null) {
3681
+ throw new Error(usage)
3682
+ }
3683
+
3684
+ const resolvedDatasetId = await resolveDatasetIdForProject(datasetId, project)
3685
+ const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(resolvedDatasetId)}/readme`, {
3686
+ method: 'POST',
3687
+ headers: { 'Content-Type': 'application/json' },
3688
+ body: JSON.stringify({ markdown: readmeMarkdown }),
3689
+ })
3690
+
3691
+ if (!response.ok) {
3692
+ throw new Error(`Failed to save dataset README: ${await response.text()}`)
3693
+ }
3694
+
3695
+ const data = await parseJsonResponse<{
3696
+ version: {
3697
+ id: string
3698
+ dataset_id: string
3699
+ version_num: number
3700
+ created_at: string
3701
+ }
3702
+ }>(response, 'Dataset README save')
3703
+
3704
+ if (hasJsonFlag()) {
3705
+ printJson({
3706
+ dataset_id: data.version.dataset_id,
3707
+ readme_version_id: data.version.id,
3708
+ readme_version_num: data.version.version_num,
3709
+ })
3710
+ return
3711
+ }
3712
+
3713
+ printLine(
3714
+ `Saved README v${data.version.version_num} for dataset ` +
3715
+ `${sanitizeTerminalText(data.version.dataset_id)} (${sanitizeTerminalText(data.version.id)}).`
3716
+ )
3717
+ }
3718
+
3312
3719
  function getDatasetReferenceInput(): string | null {
3313
3720
  const fromFlag = getArg('--dataset')
3314
3721
  if (fromFlag) {
@@ -4180,6 +4587,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4180
4587
  return
4181
4588
  }
4182
4589
 
4590
+ if (command === 'scorers' && subcommand === 'exec') {
4591
+ await execScorer()
4592
+ return
4593
+ }
4594
+
4183
4595
  if (command === 'runners' && subcommand === 'push') {
4184
4596
  await pushRunnerArtifact('runner')
4185
4597
  return
@@ -4244,6 +4656,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4244
4656
  return
4245
4657
  }
4246
4658
 
4659
+ if (command === 'apps' && subcommand === 'preview') {
4660
+ await previewAppFromFile()
4661
+ return
4662
+ }
4663
+
4247
4664
  if (command === 'apps' && subcommand === 'update') {
4248
4665
  await updateAppFromFile()
4249
4666
  return
@@ -4259,6 +4676,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4259
4676
  return
4260
4677
  }
4261
4678
 
4679
+ if (command === 'apps' && subcommand === 'export') {
4680
+ await exportAppSource()
4681
+ return
4682
+ }
4683
+
4262
4684
  if (command === 'tasks' && subcommand === 'list') {
4263
4685
  await listTasks()
4264
4686
  return
@@ -4310,6 +4732,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4310
4732
  }
4311
4733
 
4312
4734
  const datasetsAction = cliArgs[2]
4735
+ if (command === 'datasets' && subcommand === 'readme' && datasetsAction === 'set') {
4736
+ await setDatasetReadme()
4737
+ return
4738
+ }
4739
+
4313
4740
  if (command === 'datasets' && subcommand === 'versions' && datasetsAction === 'create') {
4314
4741
  await createDatasetVersion()
4315
4742
  return