orizu 0.3.2 → 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
@@ -101,6 +101,17 @@ orizu logout
101
101
  <td>Create a project</td>
102
102
  <td><code>orizu projects create --name "Support QA" --team ops-eval</code></td>
103
103
  </tr>
104
+ <tr>
105
+ <td colspan="2"><strong>Prompts</strong></td>
106
+ </tr>
107
+ <tr>
108
+ <td>List prompts</td>
109
+ <td><code>orizu prompts list --project ops-eval/support-qa</code></td>
110
+ </tr>
111
+ <tr>
112
+ <td>Show prompt comments</td>
113
+ <td><code>orizu prompts comments &lt;promptIdOrName&gt; --project ops-eval/support-qa</code></td>
114
+ </tr>
104
115
  <tr>
105
116
  <td colspan="2"><strong>Datasets</strong></td>
106
117
  </tr>
@@ -162,6 +173,10 @@ orizu logout
162
173
  <td>Link a dataset to an app</td>
163
174
  <td><code>orizu apps link-dataset --app &lt;appId&gt; --dataset &lt;datasetId&gt;</code></td>
164
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>
165
180
  <tr>
166
181
  <td colspan="2"><strong>Tasks</strong></td>
167
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 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]\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
  }
@@ -480,6 +495,63 @@ function printTaskStatusSummary(data) {
480
495
  });
481
496
  }
482
497
  }
498
+ function compactTerminalText(value, maxLength = 160) {
499
+ const compact = sanitizeTerminalText(value).replace(/\s+/g, ' ').trim();
500
+ if (compact.length <= maxLength) {
501
+ return compact;
502
+ }
503
+ return `${compact.slice(0, Math.max(0, maxLength - 1)).trim()}…`;
504
+ }
505
+ function printIndentedBody(value, indent) {
506
+ const lines = sanitizeTerminalText(value || '').split(/\r?\n/);
507
+ for (const line of lines) {
508
+ printLine(`${indent}${line}`);
509
+ }
510
+ }
511
+ function formatPromptCommentAnchor(anchor) {
512
+ if (!anchor) {
513
+ return null;
514
+ }
515
+ const lineLabel = anchor.startLine && anchor.endLine
516
+ ? anchor.startLine === anchor.endLine
517
+ ? `line ${anchor.startLine}`
518
+ : `lines ${anchor.startLine}-${anchor.endLine}`
519
+ : null;
520
+ const selectedText = anchor.text ? `"${compactTerminalText(anchor.text, 180)}"` : null;
521
+ if (lineLabel && selectedText) {
522
+ return `${lineLabel}: ${selectedText}`;
523
+ }
524
+ return lineLabel || selectedText;
525
+ }
526
+ function printPromptComments(data) {
527
+ const versionLabel = data.version.versionLabel || (data.version.versionNumber ? `v${data.version.versionNumber}` : data.version.id);
528
+ printLine(`Prompt: ${sanitizeTerminalText(data.prompt.name)} (${sanitizeTerminalText(data.prompt.id)})`);
529
+ printLine(`Version: ${sanitizeTerminalText(String(versionLabel))} (${sanitizeTerminalText(data.version.id)})`);
530
+ printLine(`Comments: ${data.summary.threadCount} thread${data.summary.threadCount === 1 ? '' : 's'} ` +
531
+ `(${data.summary.openThreadCount} open, ${data.summary.resolvedThreadCount} resolved, ${data.summary.replyCount} replies)`);
532
+ if (data.comments.length === 0) {
533
+ printLine('\nNo comments found for this prompt version.');
534
+ return;
535
+ }
536
+ for (const [index, thread] of data.comments.entries()) {
537
+ const authorName = thread.author?.name || 'Unknown';
538
+ const anchor = formatPromptCommentAnchor(thread.anchor);
539
+ printLine('');
540
+ printLine(`${index + 1}. [${thread.status}] ${sanitizeTerminalText(authorName)} · ${sanitizeTerminalText(thread.createdAt)}`);
541
+ if (anchor) {
542
+ printLine(` Selection: ${anchor}`);
543
+ }
544
+ printIndentedBody(thread.body, ' ');
545
+ if (thread.replies.length > 0) {
546
+ printLine(` Replies (${thread.replies.length})`);
547
+ for (const reply of thread.replies) {
548
+ const replyAuthor = reply.author?.name || 'Unknown';
549
+ printLine(` - ${sanitizeTerminalText(replyAuthor)} · ${sanitizeTerminalText(reply.createdAt)}`);
550
+ printIndentedBody(reply.body, ' ');
551
+ }
552
+ }
553
+ }
554
+ }
483
555
  const DEFAULT_AUTH_CALLBACK_PORT = 43123;
484
556
  function resolveAuthCallbackPort() {
485
557
  const envPort = process.env.ORIZU_AUTH_PORT;
@@ -788,6 +860,33 @@ async function listJudges() {
788
860
  const data = await parseJsonResponse(response, 'Judges list');
789
861
  printPromptSummaries(data.judges, 'No judges found.');
790
862
  }
863
+ async function listPromptComments() {
864
+ const promptRef = getPositionalArg(2);
865
+ const project = getArg('--project') || await resolveProjectSlug(null);
866
+ const label = getArg('--label');
867
+ const version = getArg('--version');
868
+ if (!promptRef) {
869
+ throw new Error('Usage: orizu prompts comments <prompt-id-or-name> --project <team/project> [--label <label> | --version <version-id>] [--json]');
870
+ }
871
+ if (label && version) {
872
+ throw new Error('Use either --label or --version, not both');
873
+ }
874
+ const params = new URLSearchParams({ project });
875
+ if (label)
876
+ params.set('label', label);
877
+ if (version)
878
+ params.set('version', version);
879
+ const response = await authedFetch(`/api/cli/prompts/${encodeURIComponent(promptRef)}/comments?${params.toString()}`);
880
+ if (!response.ok) {
881
+ throw new Error(`Failed to fetch prompt comments: ${await response.text()}`);
882
+ }
883
+ const data = await parseJsonResponse(response, 'Prompt comments');
884
+ if (hasJsonFlag()) {
885
+ printJson(data);
886
+ return;
887
+ }
888
+ printPromptComments(data);
889
+ }
791
890
  async function listScorers() {
792
891
  const project = getArg('--project') || await resolveProjectSlug(null);
793
892
  const response = await authedFetch(`/api/cli/scorers?project=${encodeURIComponent(project)}`);
@@ -1160,14 +1259,19 @@ async function createDatasetVersion() {
1160
1259
  const datasetId = getPositionalArg(3) || getArg('--dataset');
1161
1260
  const versionLabel = getArg('--label') || getArg('--version-label') || null;
1162
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]';
1163
1263
  if (!datasetId) {
1164
- throw new Error('Usage: orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--json]');
1264
+ throw new Error(usage);
1165
1265
  }
1266
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage);
1166
1267
  const resolvedDatasetId = await resolveDatasetIdForProject(datasetId, project);
1167
1268
  const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(resolvedDatasetId)}/versions`, {
1168
1269
  method: 'POST',
1169
1270
  headers: { 'Content-Type': 'application/json' },
1170
- body: JSON.stringify({ versionLabel }),
1271
+ body: JSON.stringify({
1272
+ versionLabel,
1273
+ ...(readmeMarkdown !== null ? { readmeMarkdown } : {}),
1274
+ }),
1171
1275
  });
1172
1276
  if (!response.ok) {
1173
1277
  throw new Error(`Failed to create dataset version: ${await response.text()}`);
@@ -1329,6 +1433,7 @@ function normalizeScoreResultsInput(sourcePath, raw) {
1329
1433
  throw new Error(`Invalid score results JSON '${sourcePath}': ${message}`);
1330
1434
  }
1331
1435
  }
1436
+ const OPTIMIZATION_REPORT_MAX_BYTES = 2 * 1024 * 1024;
1332
1437
  function parseOptionalNumberFlag(name) {
1333
1438
  const value = getArg(name);
1334
1439
  if (!value) {
@@ -1343,6 +1448,40 @@ function parseOptionalNumberFlag(name) {
1343
1448
  function hasObjectKeys(value) {
1344
1449
  return Object.keys(value).length > 0;
1345
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
+ }
1346
1485
  async function startOptimizationRun() {
1347
1486
  const project = getArg('--project') || await resolveProjectSlug(null);
1348
1487
  const optimizerVersionId = getArg('--optimizer-version');
@@ -1419,6 +1558,10 @@ async function updateOptimizationRunLifecycle(action) {
1419
1558
  };
1420
1559
  const metadata = readJsonObjectArg(getArg('--metadata'), 'optimization metadata');
1421
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
+ }
1422
1565
  if ((action === 'pause' || action === 'cancel') && reason) {
1423
1566
  metadata.reason = reason;
1424
1567
  }
@@ -1429,6 +1572,10 @@ async function updateOptimizationRunLifecycle(action) {
1429
1572
  if (hasObjectKeys(metadata)) {
1430
1573
  body.metadata = metadata;
1431
1574
  }
1575
+ if (report) {
1576
+ body.reportMarkdown = report.markdown;
1577
+ body.reportSourceName = report.sourceName;
1578
+ }
1432
1579
  if (action === 'finish') {
1433
1580
  const bestScore = parseOptionalNumberFlag('--best-score');
1434
1581
  if (bestScore !== undefined) {
@@ -1473,6 +1620,27 @@ async function updateOptimizationRunLifecycle(action) {
1473
1620
  printLine(`Cancelled optimization run ${id}`);
1474
1621
  }
1475
1622
  }
1623
+ async function exportOptimizationRun() {
1624
+ const runId = getPositionalArg(2) || getArg('--run-id');
1625
+ const outPathArg = getArg('--out');
1626
+ if (!runId) {
1627
+ throw new Error('Usage: orizu optimizations export <run-id> [--out <path>] [--json]');
1628
+ }
1629
+ const response = await authedFetch(`/api/cli/optimization-runs/${encodeURIComponent(runId)}/export`);
1630
+ if (!response.ok) {
1631
+ throw new Error(`Failed to export optimization run: ${await response.text()}`);
1632
+ }
1633
+ const data = await parseJsonResponse(response, 'Optimization export');
1634
+ if (hasJsonFlag() && !outPathArg) {
1635
+ printJson(data);
1636
+ return;
1637
+ }
1638
+ const filename = outPathArg
1639
+ ? expandHomePath(outPathArg)
1640
+ : `${runId}.optimization.json`;
1641
+ writeTextFileEnsuringDir(filename, `${JSON.stringify(data, null, 2)}\n`);
1642
+ printLine(`Saved optimization export to ${sanitizeTerminalText(filename)}`);
1643
+ }
1476
1644
  function removeFlagWithValue(args, flag) {
1477
1645
  const filtered = [];
1478
1646
  for (let index = 0; index < args.length; index += 1) {
@@ -2080,6 +2248,14 @@ async function updateTaskStatus(targetStatus) {
2080
2248
  const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused';
2081
2249
  printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`);
2082
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
+ }
2083
2259
  async function appDetail() {
2084
2260
  const appId = getArg('--app');
2085
2261
  const project = getArg('--project');
@@ -2118,6 +2294,40 @@ async function appDetail() {
2118
2294
  printLine(` Created: ${sanitizeTerminalText(detail.createdAt)}`);
2119
2295
  printLine(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`);
2120
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
+ }
2121
2331
  async function listTeamMembers() {
2122
2332
  const teamSlug = await resolveTeamSlug(getArg('--team'));
2123
2333
  printTeamMembers(await fetchTeamMembers(teamSlug));
@@ -2182,7 +2392,7 @@ async function changeTeamMemberRole() {
2182
2392
  }
2183
2393
  printLine(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`);
2184
2394
  }
2185
- async function createDatasetFromRows(project, name, sourceType, rows) {
2395
+ async function createDatasetFromRows(project, name, sourceType, rows, readmeMarkdown = null) {
2186
2396
  const response = await authedFetch('/api/cli/datasets/upload', {
2187
2397
  method: 'POST',
2188
2398
  headers: {
@@ -2193,6 +2403,7 @@ async function createDatasetFromRows(project, name, sourceType, rows) {
2193
2403
  name,
2194
2404
  rows,
2195
2405
  sourceType,
2406
+ ...(readmeMarkdown !== null ? { readmeMarkdown } : {}),
2196
2407
  }),
2197
2408
  });
2198
2409
  if (!response.ok) {
@@ -2201,7 +2412,7 @@ async function createDatasetFromRows(project, name, sourceType, rows) {
2201
2412
  }
2202
2413
  return parseJsonResponse(response, 'Dataset upload');
2203
2414
  }
2204
- async function uploadJsonlDatasetInChunks(file, project, datasetName) {
2415
+ async function uploadJsonlDatasetInChunks(file, project, datasetName, readmeMarkdown = null) {
2205
2416
  let dataset = null;
2206
2417
  let totalUploaded = 0;
2207
2418
  let chunkIndex = 0;
@@ -2229,7 +2440,7 @@ async function uploadJsonlDatasetInChunks(file, project, datasetName) {
2229
2440
  printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`);
2230
2441
  try {
2231
2442
  if (!dataset) {
2232
- const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk);
2443
+ const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk, readmeMarkdown);
2233
2444
  dataset = data.dataset;
2234
2445
  totalUploaded = data.dataset.rowCount;
2235
2446
  continue;
@@ -2264,18 +2475,20 @@ async function uploadDataset() {
2264
2475
  const projectArg = getArg('--project');
2265
2476
  const fileArg = getArg('--file');
2266
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>]';
2267
2479
  if (!fileArg) {
2268
- throw new Error('Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>]');
2480
+ throw new Error(usage);
2269
2481
  }
2482
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage);
2270
2483
  const file = expandHomePath(fileArg);
2271
2484
  const project = await resolveProjectSlug(projectArg);
2272
2485
  const datasetName = name || basename(file);
2273
2486
  if (extname(file).toLowerCase() === '.jsonl') {
2274
- await uploadJsonlDatasetInChunks(file, project, datasetName);
2487
+ await uploadJsonlDatasetInChunks(file, project, datasetName, readmeMarkdown);
2275
2488
  return;
2276
2489
  }
2277
2490
  const { rows, sourceType } = parseDatasetFile(file);
2278
- const data = await createDatasetFromRows(project, datasetName, sourceType, rows);
2491
+ const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown);
2279
2492
  printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`);
2280
2493
  if (data.dataset.url) {
2281
2494
  printLine(`View dataset: ${formatTerminalLink(data.dataset.url)}`);
@@ -2285,20 +2498,26 @@ async function pushDataset() {
2285
2498
  const projectArg = getArg('--project');
2286
2499
  const fileArg = getPositionalArg(2) || getArg('--file');
2287
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]';
2288
2502
  if (!fileArg) {
2289
- throw new Error('Usage: orizu datasets push <rows.csv|rows.json|rows.jsonl> [--project <team/project>] [--name <name>] [--json]');
2503
+ throw new Error(usage);
2290
2504
  }
2505
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage);
2291
2506
  const file = expandHomePath(fileArg);
2292
2507
  const project = await resolveProjectSlug(projectArg);
2293
2508
  const datasetName = name || basename(file);
2294
2509
  const { rows, sourceType } = parseDatasetFile(file);
2295
- const data = await createDatasetFromRows(project, datasetName, sourceType, rows);
2510
+ const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown);
2296
2511
  if (hasJsonFlag()) {
2297
- printJson({
2512
+ const payload = {
2298
2513
  dataset_id: data.dataset.id,
2299
2514
  name: data.dataset.name,
2300
2515
  row_count: data.dataset.rowCount,
2301
- });
2516
+ };
2517
+ if (data.readmeVersion?.id) {
2518
+ payload.readme_version_id = data.readmeVersion.id;
2519
+ }
2520
+ printJson(payload);
2302
2521
  return;
2303
2522
  }
2304
2523
  printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`);
@@ -2306,6 +2525,38 @@ async function pushDataset() {
2306
2525
  printLine(`View dataset: ${formatTerminalLink(data.dataset.url)}`);
2307
2526
  }
2308
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
+ }
2309
2560
  function getDatasetReferenceInput() {
2310
2561
  const fromFlag = getArg('--dataset');
2311
2562
  if (fromFlag) {
@@ -2937,6 +3188,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
2937
3188
  await listPrompts();
2938
3189
  return;
2939
3190
  }
3191
+ if (command === 'prompts' && subcommand === 'comments') {
3192
+ await listPromptComments();
3193
+ return;
3194
+ }
2940
3195
  if (command === 'prompts' && subcommand === 'pull') {
2941
3196
  await pullPromptArtifact('prompt');
2942
3197
  return;
@@ -3013,6 +3268,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
3013
3268
  await runGepaOptimization();
3014
3269
  return;
3015
3270
  }
3271
+ if (command === 'optimizations' && subcommand === 'export') {
3272
+ await exportOptimizationRun();
3273
+ return;
3274
+ }
3016
3275
  if (command === 'optimizations' &&
3017
3276
  (subcommand === 'pause' ||
3018
3277
  subcommand === 'resume' ||
@@ -3042,6 +3301,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
3042
3301
  await appDetail();
3043
3302
  return;
3044
3303
  }
3304
+ if (command === 'apps' && subcommand === 'export') {
3305
+ await exportAppSource();
3306
+ return;
3307
+ }
3045
3308
  if (command === 'tasks' && subcommand === 'list') {
3046
3309
  await listTasks();
3047
3310
  return;
@@ -3083,6 +3346,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
3083
3346
  return;
3084
3347
  }
3085
3348
  const datasetsAction = cliArgs[2];
3349
+ if (command === 'datasets' && subcommand === 'readme' && datasetsAction === 'set') {
3350
+ await setDatasetReadme();
3351
+ return;
3352
+ }
3086
3353
  if (command === 'datasets' && subcommand === 'versions' && datasetsAction === 'create') {
3087
3354
  await createDatasetVersion();
3088
3355
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {