orizu 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -173,6 +173,10 @@ orizu logout
173
173
  <td>Link a dataset to an app</td>
174
174
  <td><code>orizu apps link-dataset --app &lt;appId&gt; --dataset &lt;datasetId&gt;</code></td>
175
175
  </tr>
176
+ <tr>
177
+ <td>Export app source</td>
178
+ <td><code>orizu apps export --app &lt;appId&gt; --project ops-eval/support-qa --out ./apps/SupportLabeler.tsx</code></td>
179
+ </tr>
176
180
  <tr>
177
181
  <td colspan="2"><strong>Tasks</strong></td>
178
182
  </tr>
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import { parseDatasetReference } from './dataset-download.js';
16
16
  import { parseGlobalFlags } from './global-flags.js';
17
17
  import { assertSecureTokenTransport, authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags, } from './http.js';
18
18
  import { formatTaskCreateError } from './task-create-error.js';
19
+ const MAX_README_LENGTH = 200_000;
19
20
  function getCliVersion() {
20
21
  const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
21
22
  if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
@@ -30,10 +31,10 @@ function printVersion() {
30
31
  printLine(`orizu ${getCliVersion()}`);
31
32
  }
32
33
  function printUsage() {
33
- 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>]`);
34
+ 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 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>]`);
34
35
  }
35
36
  function printOptimizationUsage() {
36
- 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]`);
37
+ 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]`);
37
38
  }
38
39
  let cliArgs = process.argv.slice(2);
39
40
  function getArg(name) {
@@ -59,6 +60,20 @@ export function expandHomePath(path) {
59
60
  }
60
61
  return path;
61
62
  }
63
+ function readReadmeMarkdownFromArgs(usage) {
64
+ const readmeFile = getArg('--readme-file');
65
+ const readmeText = getArg('--readme-text');
66
+ if (readmeFile !== null && readmeText !== null) {
67
+ throw new Error(`Use either --readme-file or --readme-text, not both.\n${usage}`);
68
+ }
69
+ const markdown = readmeFile !== null
70
+ ? readFileSync(expandHomePath(readmeFile), 'utf8')
71
+ : readmeText;
72
+ if (markdown !== null && markdown.length > MAX_README_LENGTH) {
73
+ throw new Error(`README markdown is too large. Maximum size is ${MAX_README_LENGTH} characters.`);
74
+ }
75
+ return markdown;
76
+ }
62
77
  function createCodeVerifier() {
63
78
  return randomBytes(32).toString('base64url');
64
79
  }
@@ -1244,14 +1259,19 @@ async function createDatasetVersion() {
1244
1259
  const datasetId = getPositionalArg(3) || getArg('--dataset');
1245
1260
  const versionLabel = getArg('--label') || getArg('--version-label') || null;
1246
1261
  const project = getArg('--project');
1262
+ const usage = 'Usage: orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--readme-file <README.md> | --readme-text <markdown>] [--json]';
1247
1263
  if (!datasetId) {
1248
- throw new Error('Usage: orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--json]');
1264
+ throw new Error(usage);
1249
1265
  }
1266
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage);
1250
1267
  const resolvedDatasetId = await resolveDatasetIdForProject(datasetId, project);
1251
1268
  const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(resolvedDatasetId)}/versions`, {
1252
1269
  method: 'POST',
1253
1270
  headers: { 'Content-Type': 'application/json' },
1254
- body: JSON.stringify({ versionLabel }),
1271
+ body: JSON.stringify({
1272
+ versionLabel,
1273
+ ...(readmeMarkdown !== null ? { readmeMarkdown } : {}),
1274
+ }),
1255
1275
  });
1256
1276
  if (!response.ok) {
1257
1277
  throw new Error(`Failed to create dataset version: ${await response.text()}`);
@@ -1413,6 +1433,7 @@ function normalizeScoreResultsInput(sourcePath, raw) {
1413
1433
  throw new Error(`Invalid score results JSON '${sourcePath}': ${message}`);
1414
1434
  }
1415
1435
  }
1436
+ const OPTIMIZATION_REPORT_MAX_BYTES = 2 * 1024 * 1024;
1416
1437
  function parseOptionalNumberFlag(name) {
1417
1438
  const value = getArg(name);
1418
1439
  if (!value) {
@@ -1427,6 +1448,40 @@ function parseOptionalNumberFlag(name) {
1427
1448
  function hasObjectKeys(value) {
1428
1449
  return Object.keys(value).length > 0;
1429
1450
  }
1451
+ function readOptimizationReportInput() {
1452
+ const report = getArg('--report');
1453
+ const reportFile = getArg('--report-file');
1454
+ if (report && reportFile) {
1455
+ throw new Error('Use either --report or --report-file, not both');
1456
+ }
1457
+ if (!report && !reportFile) {
1458
+ return null;
1459
+ }
1460
+ let markdown;
1461
+ let sourceName;
1462
+ if (reportFile) {
1463
+ const expandedPath = expandHomePath(reportFile);
1464
+ markdown = readSourceFile(reportFile);
1465
+ sourceName = basename(expandedPath);
1466
+ }
1467
+ else if (report?.startsWith('@')) {
1468
+ const path = report.slice(1);
1469
+ const expandedPath = expandHomePath(path);
1470
+ markdown = readSourceFile(path);
1471
+ sourceName = basename(expandedPath);
1472
+ }
1473
+ else {
1474
+ markdown = report || '';
1475
+ sourceName = 'inline';
1476
+ }
1477
+ if (!markdown.trim()) {
1478
+ throw new Error('Optimization report markdown must not be blank');
1479
+ }
1480
+ if (Buffer.byteLength(markdown, 'utf8') > OPTIMIZATION_REPORT_MAX_BYTES) {
1481
+ throw new Error(`Optimization report exceeds ${OPTIMIZATION_REPORT_MAX_BYTES} bytes`);
1482
+ }
1483
+ return { markdown, sourceName };
1484
+ }
1430
1485
  async function startOptimizationRun() {
1431
1486
  const project = getArg('--project') || await resolveProjectSlug(null);
1432
1487
  const optimizerVersionId = getArg('--optimizer-version');
@@ -1503,6 +1558,10 @@ async function updateOptimizationRunLifecycle(action) {
1503
1558
  };
1504
1559
  const metadata = readJsonObjectArg(getArg('--metadata'), 'optimization metadata');
1505
1560
  const reason = getArg('--reason');
1561
+ const report = readOptimizationReportInput();
1562
+ if (report && (action === 'pause' || action === 'resume')) {
1563
+ throw new Error(`orizu optimizations ${action} does not accept optimization reports`);
1564
+ }
1506
1565
  if ((action === 'pause' || action === 'cancel') && reason) {
1507
1566
  metadata.reason = reason;
1508
1567
  }
@@ -1513,6 +1572,10 @@ async function updateOptimizationRunLifecycle(action) {
1513
1572
  if (hasObjectKeys(metadata)) {
1514
1573
  body.metadata = metadata;
1515
1574
  }
1575
+ if (report) {
1576
+ body.reportMarkdown = report.markdown;
1577
+ body.reportSourceName = report.sourceName;
1578
+ }
1516
1579
  if (action === 'finish') {
1517
1580
  const bestScore = parseOptionalNumberFlag('--best-score');
1518
1581
  if (bestScore !== undefined) {
@@ -2185,6 +2248,14 @@ async function updateTaskStatus(targetStatus) {
2185
2248
  const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused';
2186
2249
  printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`);
2187
2250
  }
2251
+ function defaultAppExportFilename(appName, versionNum) {
2252
+ const safeName = appName
2253
+ .trim()
2254
+ .replace(/[^A-Za-z0-9._-]+/g, '_')
2255
+ .replace(/^_+|_+$/g, '')
2256
+ .slice(0, 100);
2257
+ return `${safeName || 'app'}.v${versionNum}.tsx`;
2258
+ }
2188
2259
  async function appDetail() {
2189
2260
  const appId = getArg('--app');
2190
2261
  const project = getArg('--project');
@@ -2223,6 +2294,40 @@ async function appDetail() {
2223
2294
  printLine(` Created: ${sanitizeTerminalText(detail.createdAt)}`);
2224
2295
  printLine(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`);
2225
2296
  }
2297
+ async function exportAppSource() {
2298
+ const project = getArg('--project');
2299
+ const versionArg = getArg('--version');
2300
+ const outPathArg = getArg('--out');
2301
+ let appId = getArg('--app');
2302
+ const versionNum = versionArg && /^[1-9]\d*$/.test(versionArg)
2303
+ ? Number.parseInt(versionArg, 10)
2304
+ : undefined;
2305
+ if (versionArg && versionNum === undefined) {
2306
+ throw new Error('--version must be a positive integer');
2307
+ }
2308
+ if (!appId) {
2309
+ const selected = await selectAppIdInteractively(project);
2310
+ appId = selected.appId;
2311
+ }
2312
+ const query = new URLSearchParams();
2313
+ if (project) {
2314
+ query.set('project', project);
2315
+ }
2316
+ if (versionNum) {
2317
+ query.set('version', String(versionNum));
2318
+ }
2319
+ const suffix = query.toString() ? `?${query.toString()}` : '';
2320
+ const response = await authedFetch(`/api/cli/apps/${encodeURIComponent(appId)}/export${suffix}`);
2321
+ if (!response.ok) {
2322
+ throw new Error(`Failed to export app (${response.status}): ${await response.text()}`);
2323
+ }
2324
+ const data = await parseJsonResponse(response, 'App export');
2325
+ const filename = outPathArg
2326
+ ? expandHomePath(outPathArg)
2327
+ : defaultAppExportFilename(data.app.name, data.version.versionNum);
2328
+ writeTextFileEnsuringDir(filename, data.version.code);
2329
+ printLine(`Saved app ${sanitizeTerminalText(data.app.name)} v${data.version.versionNum} source to ${sanitizeTerminalText(filename)}`);
2330
+ }
2226
2331
  async function listTeamMembers() {
2227
2332
  const teamSlug = await resolveTeamSlug(getArg('--team'));
2228
2333
  printTeamMembers(await fetchTeamMembers(teamSlug));
@@ -2287,7 +2392,7 @@ async function changeTeamMemberRole() {
2287
2392
  }
2288
2393
  printLine(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`);
2289
2394
  }
2290
- async function createDatasetFromRows(project, name, sourceType, rows) {
2395
+ async function createDatasetFromRows(project, name, sourceType, rows, readmeMarkdown = null) {
2291
2396
  const response = await authedFetch('/api/cli/datasets/upload', {
2292
2397
  method: 'POST',
2293
2398
  headers: {
@@ -2298,6 +2403,7 @@ async function createDatasetFromRows(project, name, sourceType, rows) {
2298
2403
  name,
2299
2404
  rows,
2300
2405
  sourceType,
2406
+ ...(readmeMarkdown !== null ? { readmeMarkdown } : {}),
2301
2407
  }),
2302
2408
  });
2303
2409
  if (!response.ok) {
@@ -2306,7 +2412,7 @@ async function createDatasetFromRows(project, name, sourceType, rows) {
2306
2412
  }
2307
2413
  return parseJsonResponse(response, 'Dataset upload');
2308
2414
  }
2309
- async function uploadJsonlDatasetInChunks(file, project, datasetName) {
2415
+ async function uploadJsonlDatasetInChunks(file, project, datasetName, readmeMarkdown = null) {
2310
2416
  let dataset = null;
2311
2417
  let totalUploaded = 0;
2312
2418
  let chunkIndex = 0;
@@ -2334,7 +2440,7 @@ async function uploadJsonlDatasetInChunks(file, project, datasetName) {
2334
2440
  printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`);
2335
2441
  try {
2336
2442
  if (!dataset) {
2337
- const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk);
2443
+ const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk, readmeMarkdown);
2338
2444
  dataset = data.dataset;
2339
2445
  totalUploaded = data.dataset.rowCount;
2340
2446
  continue;
@@ -2369,18 +2475,20 @@ async function uploadDataset() {
2369
2475
  const projectArg = getArg('--project');
2370
2476
  const fileArg = getArg('--file');
2371
2477
  const name = getArg('--name');
2478
+ const usage = 'Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>] [--readme-file <README.md> | --readme-text <markdown>]';
2372
2479
  if (!fileArg) {
2373
- throw new Error('Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>]');
2480
+ throw new Error(usage);
2374
2481
  }
2482
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage);
2375
2483
  const file = expandHomePath(fileArg);
2376
2484
  const project = await resolveProjectSlug(projectArg);
2377
2485
  const datasetName = name || basename(file);
2378
2486
  if (extname(file).toLowerCase() === '.jsonl') {
2379
- await uploadJsonlDatasetInChunks(file, project, datasetName);
2487
+ await uploadJsonlDatasetInChunks(file, project, datasetName, readmeMarkdown);
2380
2488
  return;
2381
2489
  }
2382
2490
  const { rows, sourceType } = parseDatasetFile(file);
2383
- const data = await createDatasetFromRows(project, datasetName, sourceType, rows);
2491
+ const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown);
2384
2492
  printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`);
2385
2493
  if (data.dataset.url) {
2386
2494
  printLine(`View dataset: ${formatTerminalLink(data.dataset.url)}`);
@@ -2390,20 +2498,26 @@ async function pushDataset() {
2390
2498
  const projectArg = getArg('--project');
2391
2499
  const fileArg = getPositionalArg(2) || getArg('--file');
2392
2500
  const name = getArg('--name');
2501
+ 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]';
2393
2502
  if (!fileArg) {
2394
- throw new Error('Usage: orizu datasets push <rows.csv|rows.json|rows.jsonl> [--project <team/project>] [--name <name>] [--json]');
2503
+ throw new Error(usage);
2395
2504
  }
2505
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage);
2396
2506
  const file = expandHomePath(fileArg);
2397
2507
  const project = await resolveProjectSlug(projectArg);
2398
2508
  const datasetName = name || basename(file);
2399
2509
  const { rows, sourceType } = parseDatasetFile(file);
2400
- const data = await createDatasetFromRows(project, datasetName, sourceType, rows);
2510
+ const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown);
2401
2511
  if (hasJsonFlag()) {
2402
- printJson({
2512
+ const payload = {
2403
2513
  dataset_id: data.dataset.id,
2404
2514
  name: data.dataset.name,
2405
2515
  row_count: data.dataset.rowCount,
2406
- });
2516
+ };
2517
+ if (data.readmeVersion?.id) {
2518
+ payload.readme_version_id = data.readmeVersion.id;
2519
+ }
2520
+ printJson(payload);
2407
2521
  return;
2408
2522
  }
2409
2523
  printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`);
@@ -2411,6 +2525,38 @@ async function pushDataset() {
2411
2525
  printLine(`View dataset: ${formatTerminalLink(data.dataset.url)}`);
2412
2526
  }
2413
2527
  }
2528
+ async function setDatasetReadme() {
2529
+ const datasetId = getPositionalArg(3) || getArg('--dataset');
2530
+ const project = getArg('--project');
2531
+ const usage = 'Usage: orizu datasets readme set <datasetId|dataset-name> [--project <team/project>] (--readme-file <README.md> | --readme-text <markdown>) [--json]';
2532
+ if (!datasetId) {
2533
+ throw new Error(usage);
2534
+ }
2535
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage);
2536
+ if (readmeMarkdown === null) {
2537
+ throw new Error(usage);
2538
+ }
2539
+ const resolvedDatasetId = await resolveDatasetIdForProject(datasetId, project);
2540
+ const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(resolvedDatasetId)}/readme`, {
2541
+ method: 'POST',
2542
+ headers: { 'Content-Type': 'application/json' },
2543
+ body: JSON.stringify({ markdown: readmeMarkdown }),
2544
+ });
2545
+ if (!response.ok) {
2546
+ throw new Error(`Failed to save dataset README: ${await response.text()}`);
2547
+ }
2548
+ const data = await parseJsonResponse(response, 'Dataset README save');
2549
+ if (hasJsonFlag()) {
2550
+ printJson({
2551
+ dataset_id: data.version.dataset_id,
2552
+ readme_version_id: data.version.id,
2553
+ readme_version_num: data.version.version_num,
2554
+ });
2555
+ return;
2556
+ }
2557
+ printLine(`Saved README v${data.version.version_num} for dataset ` +
2558
+ `${sanitizeTerminalText(data.version.dataset_id)} (${sanitizeTerminalText(data.version.id)}).`);
2559
+ }
2414
2560
  function getDatasetReferenceInput() {
2415
2561
  const fromFlag = getArg('--dataset');
2416
2562
  if (fromFlag) {
@@ -3155,6 +3301,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
3155
3301
  await appDetail();
3156
3302
  return;
3157
3303
  }
3304
+ if (command === 'apps' && subcommand === 'export') {
3305
+ await exportAppSource();
3306
+ return;
3307
+ }
3158
3308
  if (command === 'tasks' && subcommand === 'list') {
3159
3309
  await listTasks();
3160
3310
  return;
@@ -3196,6 +3346,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
3196
3346
  return;
3197
3347
  }
3198
3348
  const datasetsAction = cliArgs[2];
3349
+ if (command === 'datasets' && subcommand === 'readme' && datasetsAction === 'set') {
3350
+ await setDatasetReadme();
3351
+ return;
3352
+ }
3199
3353
  if (command === 'datasets' && subcommand === 'versions' && datasetsAction === 'create') {
3200
3354
  await createDatasetVersion();
3201
3355
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -185,6 +185,8 @@ interface RunnerExecContext {
185
185
 
186
186
  type DatasetUploadSourceType = 'csv' | 'json' | 'jsonl'
187
187
 
188
+ const MAX_README_LENGTH = 200_000
189
+
188
190
  interface DatasetUploadResponse {
189
191
  dataset: {
190
192
  id: string
@@ -193,6 +195,11 @@ interface DatasetUploadResponse {
193
195
  sourceType: string
194
196
  url?: string
195
197
  }
198
+ readmeVersion?: {
199
+ id: string
200
+ version_num: number
201
+ created_at: string
202
+ } | null
196
203
  }
197
204
 
198
205
  interface TeamMember {
@@ -256,11 +263,11 @@ function printVersion() {
256
263
  }
257
264
 
258
265
  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>]`)
266
+ 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 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>]`)
260
267
  }
261
268
 
262
269
  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]`)
270
+ 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
271
  }
265
272
 
266
273
  let cliArgs = process.argv.slice(2)
@@ -295,6 +302,25 @@ export function expandHomePath(path: string): string {
295
302
  return path
296
303
  }
297
304
 
305
+ function readReadmeMarkdownFromArgs(usage: string): string | null {
306
+ const readmeFile = getArg('--readme-file')
307
+ const readmeText = getArg('--readme-text')
308
+
309
+ if (readmeFile !== null && readmeText !== null) {
310
+ throw new Error(`Use either --readme-file or --readme-text, not both.\n${usage}`)
311
+ }
312
+
313
+ const markdown = readmeFile !== null
314
+ ? readFileSync(expandHomePath(readmeFile), 'utf8')
315
+ : readmeText
316
+
317
+ if (markdown !== null && markdown.length > MAX_README_LENGTH) {
318
+ throw new Error(`README markdown is too large. Maximum size is ${MAX_README_LENGTH} characters.`)
319
+ }
320
+
321
+ return markdown
322
+ }
323
+
298
324
  function createCodeVerifier(): string {
299
325
  return randomBytes(32).toString('base64url')
300
326
  }
@@ -1807,16 +1833,21 @@ async function createDatasetVersion() {
1807
1833
  const datasetId = getPositionalArg(3) || getArg('--dataset')
1808
1834
  const versionLabel = getArg('--label') || getArg('--version-label') || null
1809
1835
  const project = getArg('--project')
1836
+ const usage = 'Usage: orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--readme-file <README.md> | --readme-text <markdown>] [--json]'
1810
1837
 
1811
1838
  if (!datasetId) {
1812
- throw new Error('Usage: orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--json]')
1839
+ throw new Error(usage)
1813
1840
  }
1814
1841
 
1842
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
1815
1843
  const resolvedDatasetId = await resolveDatasetIdForProject(datasetId, project)
1816
1844
  const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(resolvedDatasetId)}/versions`, {
1817
1845
  method: 'POST',
1818
1846
  headers: { 'Content-Type': 'application/json' },
1819
- body: JSON.stringify({ versionLabel }),
1847
+ body: JSON.stringify({
1848
+ versionLabel,
1849
+ ...(readmeMarkdown !== null ? { readmeMarkdown } : {}),
1850
+ }),
1820
1851
  })
1821
1852
 
1822
1853
  if (!response.ok) {
@@ -2033,6 +2064,7 @@ function normalizeScoreResultsInput(sourcePath: string, raw: string): string {
2033
2064
  }
2034
2065
 
2035
2066
  type OptimizationLifecycleAction = 'pause' | 'resume' | 'finish' | 'fail' | 'cancel'
2067
+ const OPTIMIZATION_REPORT_MAX_BYTES = 2 * 1024 * 1024
2036
2068
 
2037
2069
  function parseOptionalNumberFlag(name: string): number | undefined {
2038
2070
  const value = getArg(name)
@@ -2052,6 +2084,46 @@ function hasObjectKeys(value: Record<string, unknown>): boolean {
2052
2084
  return Object.keys(value).length > 0
2053
2085
  }
2054
2086
 
2087
+ function readOptimizationReportInput(): { markdown: string; sourceName: string | null } | null {
2088
+ const report = getArg('--report')
2089
+ const reportFile = getArg('--report-file')
2090
+
2091
+ if (report && reportFile) {
2092
+ throw new Error('Use either --report or --report-file, not both')
2093
+ }
2094
+
2095
+ if (!report && !reportFile) {
2096
+ return null
2097
+ }
2098
+
2099
+ let markdown: string
2100
+ let sourceName: string | null
2101
+
2102
+ if (reportFile) {
2103
+ const expandedPath = expandHomePath(reportFile)
2104
+ markdown = readSourceFile(reportFile)
2105
+ sourceName = basename(expandedPath)
2106
+ } else if (report?.startsWith('@')) {
2107
+ const path = report.slice(1)
2108
+ const expandedPath = expandHomePath(path)
2109
+ markdown = readSourceFile(path)
2110
+ sourceName = basename(expandedPath)
2111
+ } else {
2112
+ markdown = report || ''
2113
+ sourceName = 'inline'
2114
+ }
2115
+
2116
+ if (!markdown.trim()) {
2117
+ throw new Error('Optimization report markdown must not be blank')
2118
+ }
2119
+
2120
+ if (Buffer.byteLength(markdown, 'utf8') > OPTIMIZATION_REPORT_MAX_BYTES) {
2121
+ throw new Error(`Optimization report exceeds ${OPTIMIZATION_REPORT_MAX_BYTES} bytes`)
2122
+ }
2123
+
2124
+ return { markdown, sourceName }
2125
+ }
2126
+
2055
2127
  async function startOptimizationRun() {
2056
2128
  const project = getArg('--project') || await resolveProjectSlug(null)
2057
2129
  const optimizerVersionId = getArg('--optimizer-version')
@@ -2156,6 +2228,11 @@ async function updateOptimizationRunLifecycle(action: OptimizationLifecycleActio
2156
2228
  }
2157
2229
  const metadata = readJsonObjectArg(getArg('--metadata'), 'optimization metadata')
2158
2230
  const reason = getArg('--reason')
2231
+ const report = readOptimizationReportInput()
2232
+
2233
+ if (report && (action === 'pause' || action === 'resume')) {
2234
+ throw new Error(`orizu optimizations ${action} does not accept optimization reports`)
2235
+ }
2159
2236
 
2160
2237
  if ((action === 'pause' || action === 'cancel') && reason) {
2161
2238
  metadata.reason = reason
@@ -2170,6 +2247,11 @@ async function updateOptimizationRunLifecycle(action: OptimizationLifecycleActio
2170
2247
  body.metadata = metadata
2171
2248
  }
2172
2249
 
2250
+ if (report) {
2251
+ body.reportMarkdown = report.markdown
2252
+ body.reportSourceName = report.sourceName
2253
+ }
2254
+
2173
2255
  if (action === 'finish') {
2174
2256
  const bestScore = parseOptionalNumberFlag('--best-score')
2175
2257
  if (bestScore !== undefined) {
@@ -3023,6 +3105,28 @@ interface AppDetailPayload {
3023
3105
  projectName: string
3024
3106
  }
3025
3107
 
3108
+ interface AppExportPayload {
3109
+ app: {
3110
+ id: string
3111
+ name: string
3112
+ }
3113
+ version: {
3114
+ id: string
3115
+ versionNum: number
3116
+ createdAt?: string
3117
+ code: string
3118
+ }
3119
+ }
3120
+
3121
+ function defaultAppExportFilename(appName: string, versionNum: number): string {
3122
+ const safeName = appName
3123
+ .trim()
3124
+ .replace(/[^A-Za-z0-9._-]+/g, '_')
3125
+ .replace(/^_+|_+$/g, '')
3126
+ .slice(0, 100)
3127
+ return `${safeName || 'app'}.v${versionNum}.tsx`
3128
+ }
3129
+
3026
3130
  async function appDetail() {
3027
3131
  const appId = getArg('--app')
3028
3132
  const project = getArg('--project')
@@ -3068,6 +3172,50 @@ async function appDetail() {
3068
3172
  printLine(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`)
3069
3173
  }
3070
3174
 
3175
+ async function exportAppSource() {
3176
+ const project = getArg('--project')
3177
+ const versionArg = getArg('--version')
3178
+ const outPathArg = getArg('--out')
3179
+ let appId = getArg('--app')
3180
+
3181
+ const versionNum = versionArg && /^[1-9]\d*$/.test(versionArg)
3182
+ ? Number.parseInt(versionArg, 10)
3183
+ : undefined
3184
+
3185
+ if (versionArg && versionNum === undefined) {
3186
+ throw new Error('--version must be a positive integer')
3187
+ }
3188
+
3189
+ if (!appId) {
3190
+ const selected = await selectAppIdInteractively(project)
3191
+ appId = selected.appId
3192
+ }
3193
+
3194
+ const query = new URLSearchParams()
3195
+ if (project) {
3196
+ query.set('project', project)
3197
+ }
3198
+ if (versionNum) {
3199
+ query.set('version', String(versionNum))
3200
+ }
3201
+
3202
+ const suffix = query.toString() ? `?${query.toString()}` : ''
3203
+ const response = await authedFetch(`/api/cli/apps/${encodeURIComponent(appId)}/export${suffix}`)
3204
+ if (!response.ok) {
3205
+ throw new Error(`Failed to export app (${response.status}): ${await response.text()}`)
3206
+ }
3207
+
3208
+ const data = await parseJsonResponse<AppExportPayload>(response, 'App export')
3209
+ const filename = outPathArg
3210
+ ? expandHomePath(outPathArg)
3211
+ : defaultAppExportFilename(data.app.name, data.version.versionNum)
3212
+
3213
+ writeTextFileEnsuringDir(filename, data.version.code)
3214
+ printLine(
3215
+ `Saved app ${sanitizeTerminalText(data.app.name)} v${data.version.versionNum} source to ${sanitizeTerminalText(filename)}`
3216
+ )
3217
+ }
3218
+
3071
3219
  async function listTeamMembers() {
3072
3220
  const teamSlug = await resolveTeamSlug(getArg('--team'))
3073
3221
 
@@ -3154,7 +3302,8 @@ async function createDatasetFromRows(
3154
3302
  project: string,
3155
3303
  name: string,
3156
3304
  sourceType: DatasetUploadSourceType,
3157
- rows: Array<Record<string, unknown>>
3305
+ rows: Array<Record<string, unknown>>,
3306
+ readmeMarkdown: string | null = null
3158
3307
  ): Promise<DatasetUploadResponse> {
3159
3308
  const response = await authedFetch('/api/cli/datasets/upload', {
3160
3309
  method: 'POST',
@@ -3166,6 +3315,7 @@ async function createDatasetFromRows(
3166
3315
  name,
3167
3316
  rows,
3168
3317
  sourceType,
3318
+ ...(readmeMarkdown !== null ? { readmeMarkdown } : {}),
3169
3319
  }),
3170
3320
  })
3171
3321
 
@@ -3180,7 +3330,8 @@ async function createDatasetFromRows(
3180
3330
  async function uploadJsonlDatasetInChunks(
3181
3331
  file: string,
3182
3332
  project: string,
3183
- datasetName: string
3333
+ datasetName: string,
3334
+ readmeMarkdown: string | null = null
3184
3335
  ) {
3185
3336
  let dataset: DatasetUploadResponse['dataset'] | null = null
3186
3337
  let totalUploaded = 0
@@ -3215,7 +3366,7 @@ async function uploadJsonlDatasetInChunks(
3215
3366
 
3216
3367
  try {
3217
3368
  if (!dataset) {
3218
- const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk)
3369
+ const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk, readmeMarkdown)
3219
3370
  dataset = data.dataset
3220
3371
  totalUploaded = data.dataset.rowCount
3221
3372
  continue
@@ -3256,22 +3407,24 @@ async function uploadDataset() {
3256
3407
  const projectArg = getArg('--project')
3257
3408
  const fileArg = getArg('--file')
3258
3409
  const name = getArg('--name')
3410
+ const usage = 'Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>] [--readme-file <README.md> | --readme-text <markdown>]'
3259
3411
 
3260
3412
  if (!fileArg) {
3261
- throw new Error('Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>]')
3413
+ throw new Error(usage)
3262
3414
  }
3263
3415
 
3416
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
3264
3417
  const file = expandHomePath(fileArg)
3265
3418
  const project = await resolveProjectSlug(projectArg)
3266
3419
  const datasetName = name || basename(file)
3267
3420
 
3268
3421
  if (extname(file).toLowerCase() === '.jsonl') {
3269
- await uploadJsonlDatasetInChunks(file, project, datasetName)
3422
+ await uploadJsonlDatasetInChunks(file, project, datasetName, readmeMarkdown)
3270
3423
  return
3271
3424
  }
3272
3425
 
3273
3426
  const { rows, sourceType } = parseDatasetFile(file)
3274
- const data = await createDatasetFromRows(project, datasetName, sourceType, rows)
3427
+ const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown)
3275
3428
 
3276
3429
  printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`)
3277
3430
  if (data.dataset.url) {
@@ -3283,23 +3436,29 @@ async function pushDataset() {
3283
3436
  const projectArg = getArg('--project')
3284
3437
  const fileArg = getPositionalArg(2) || getArg('--file')
3285
3438
  const name = getArg('--name')
3439
+ 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
3440
 
3287
3441
  if (!fileArg) {
3288
- throw new Error('Usage: orizu datasets push <rows.csv|rows.json|rows.jsonl> [--project <team/project>] [--name <name>] [--json]')
3442
+ throw new Error(usage)
3289
3443
  }
3290
3444
 
3445
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
3291
3446
  const file = expandHomePath(fileArg)
3292
3447
  const project = await resolveProjectSlug(projectArg)
3293
3448
  const datasetName = name || basename(file)
3294
3449
  const { rows, sourceType } = parseDatasetFile(file)
3295
- const data = await createDatasetFromRows(project, datasetName, sourceType, rows)
3450
+ const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown)
3296
3451
 
3297
3452
  if (hasJsonFlag()) {
3298
- printJson({
3453
+ const payload: Record<string, unknown> = {
3299
3454
  dataset_id: data.dataset.id,
3300
3455
  name: data.dataset.name,
3301
3456
  row_count: data.dataset.rowCount,
3302
- })
3457
+ }
3458
+ if (data.readmeVersion?.id) {
3459
+ payload.readme_version_id = data.readmeVersion.id
3460
+ }
3461
+ printJson(payload)
3303
3462
  return
3304
3463
  }
3305
3464
 
@@ -3309,6 +3468,55 @@ async function pushDataset() {
3309
3468
  }
3310
3469
  }
3311
3470
 
3471
+ async function setDatasetReadme() {
3472
+ const datasetId = getPositionalArg(3) || getArg('--dataset')
3473
+ const project = getArg('--project')
3474
+ const usage = 'Usage: orizu datasets readme set <datasetId|dataset-name> [--project <team/project>] (--readme-file <README.md> | --readme-text <markdown>) [--json]'
3475
+
3476
+ if (!datasetId) {
3477
+ throw new Error(usage)
3478
+ }
3479
+
3480
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
3481
+ if (readmeMarkdown === null) {
3482
+ throw new Error(usage)
3483
+ }
3484
+
3485
+ const resolvedDatasetId = await resolveDatasetIdForProject(datasetId, project)
3486
+ const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(resolvedDatasetId)}/readme`, {
3487
+ method: 'POST',
3488
+ headers: { 'Content-Type': 'application/json' },
3489
+ body: JSON.stringify({ markdown: readmeMarkdown }),
3490
+ })
3491
+
3492
+ if (!response.ok) {
3493
+ throw new Error(`Failed to save dataset README: ${await response.text()}`)
3494
+ }
3495
+
3496
+ const data = await parseJsonResponse<{
3497
+ version: {
3498
+ id: string
3499
+ dataset_id: string
3500
+ version_num: number
3501
+ created_at: string
3502
+ }
3503
+ }>(response, 'Dataset README save')
3504
+
3505
+ if (hasJsonFlag()) {
3506
+ printJson({
3507
+ dataset_id: data.version.dataset_id,
3508
+ readme_version_id: data.version.id,
3509
+ readme_version_num: data.version.version_num,
3510
+ })
3511
+ return
3512
+ }
3513
+
3514
+ printLine(
3515
+ `Saved README v${data.version.version_num} for dataset ` +
3516
+ `${sanitizeTerminalText(data.version.dataset_id)} (${sanitizeTerminalText(data.version.id)}).`
3517
+ )
3518
+ }
3519
+
3312
3520
  function getDatasetReferenceInput(): string | null {
3313
3521
  const fromFlag = getArg('--dataset')
3314
3522
  if (fromFlag) {
@@ -4259,6 +4467,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4259
4467
  return
4260
4468
  }
4261
4469
 
4470
+ if (command === 'apps' && subcommand === 'export') {
4471
+ await exportAppSource()
4472
+ return
4473
+ }
4474
+
4262
4475
  if (command === 'tasks' && subcommand === 'list') {
4263
4476
  await listTasks()
4264
4477
  return
@@ -4310,6 +4523,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4310
4523
  }
4311
4524
 
4312
4525
  const datasetsAction = cliArgs[2]
4526
+ if (command === 'datasets' && subcommand === 'readme' && datasetsAction === 'set') {
4527
+ await setDatasetReadme()
4528
+ return
4529
+ }
4530
+
4313
4531
  if (command === 'datasets' && subcommand === 'versions' && datasetsAction === 'create') {
4314
4532
  await createDatasetVersion()
4315
4533
  return