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/src/index.ts CHANGED
@@ -99,6 +99,62 @@ interface PromptSummary {
99
99
  description?: string | null
100
100
  }
101
101
 
102
+ interface PromptCommentAuthor {
103
+ name: string
104
+ initials?: string
105
+ color?: string
106
+ }
107
+
108
+ interface PromptCommentAnchor {
109
+ text: string | null
110
+ startLine: number | null
111
+ endLine: number | null
112
+ }
113
+
114
+ interface PromptCommentReply {
115
+ id: string
116
+ body: string
117
+ author: PromptCommentAuthor
118
+ createdAt: string
119
+ updatedAt: string
120
+ }
121
+
122
+ interface PromptCommentThread {
123
+ id: string
124
+ status: 'open' | 'resolved'
125
+ body: string
126
+ anchor: PromptCommentAnchor | null
127
+ author: PromptCommentAuthor
128
+ createdAt: string
129
+ updatedAt: string
130
+ resolvedAt: string | null
131
+ resolvedByUserId: string | null
132
+ replyCount: number
133
+ replies: PromptCommentReply[]
134
+ }
135
+
136
+ interface PromptCommentsPayload {
137
+ prompt: {
138
+ id: string
139
+ name: string
140
+ role: string
141
+ description?: string | null
142
+ }
143
+ version: {
144
+ id: string
145
+ versionNumber?: number
146
+ versionLabel?: string | null
147
+ status?: string | null
148
+ }
149
+ summary: {
150
+ threadCount: number
151
+ openThreadCount: number
152
+ resolvedThreadCount: number
153
+ replyCount: number
154
+ }
155
+ comments: PromptCommentThread[]
156
+ }
157
+
102
158
  interface ScorerSummary {
103
159
  id: string
104
160
  name: string
@@ -129,6 +185,8 @@ interface RunnerExecContext {
129
185
 
130
186
  type DatasetUploadSourceType = 'csv' | 'json' | 'jsonl'
131
187
 
188
+ const MAX_README_LENGTH = 200_000
189
+
132
190
  interface DatasetUploadResponse {
133
191
  dataset: {
134
192
  id: string
@@ -137,6 +195,11 @@ interface DatasetUploadResponse {
137
195
  sourceType: string
138
196
  url?: string
139
197
  }
198
+ readmeVersion?: {
199
+ id: string
200
+ version_num: number
201
+ created_at: string
202
+ } | null
140
203
  }
141
204
 
142
205
  interface TeamMember {
@@ -200,11 +263,11 @@ function printVersion() {
200
263
  }
201
264
 
202
265
  function printUsage() {
203
- printLine(`orizu global options:\n\n --local Use http://localhost:3000\n --server <url> Use a specific server origin (for example: https://preview.example.com)\n --version, -v Print the orizu CLI version\n\norizu commands:\n\n orizu login [--no-prompt-if-logged-in]\n orizu logout\n orizu whoami\n orizu env [--project <team/project>] [--project-id <projectId>]\n orizu log <event_type> --run-id <id> --sequence <n> --payload @event.json\n orizu teams list\n orizu teams create [--name <name>]\n orizu teams members list [--team <teamSlug>]\n orizu teams members add --email <email> [--team <teamSlug>]\n orizu teams members remove --email <email> [--team <teamSlug>]\n orizu teams members role --team <teamSlug> --email <email> --role <admin|member>\n orizu projects list [--team <teamSlug>]\n orizu projects create --name <name> [--team <teamSlug>]\n orizu prompts list --project <team/project>\n orizu prompts pull <prompt-id-or-name> --project <team/project> --out <dir> [--label <label> | --version <id>] [--json]\n orizu prompts push <dir> [--runner-version <id>] [--project <team/project>] [--parent <version-id>] [--json]\n orizu prompts labels set <prompt-name> <label> --version <version-id> [--project <team/project>] [--json]\n orizu prompts scorers set-headline <prompt-id> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]\n orizu prompts scorers add <prompt-id> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]\n orizu scorers list --project <team/project>\n orizu scorers register --project <team/project> --name <name> --manifest <manifest.json> [--prompt-version <id>] [--runner-version <id>] [--label <label>] [--json]\n orizu scorers detail <scorer-id-or-name> --project <team/project> [--json]\n orizu scorers labels set <scorer-name> <label> --version <scorer-version-id> [--project <team/project>] [--json]\n orizu scores submit <results.jsonl|results.json> --scorer-version <id> --subject-version <prompt-version-id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]\n orizu judges list --project <team/project>\n orizu judges pull <judge-id-or-name> --project <team/project> --out <dir> [--label <label> | --version <id>] [--json]\n orizu judges push <dir> [--runner-version <id>] [--project <team/project>] [--parent <version-id>] [--json]\n orizu runners push <dir> [--project <team/project>] [--name <name>] [--label <label>] [--json]\n orizu runners exec (--prompt <prompt-version-id> | --prompt-version <id> --runner-version <id> | --scorer-version <id>) --dataset-version <id> --split-set <id-or-name> --split <name> [--runner-dir <dir>] --out <results.jsonl|results.jsonl.gz>\n orizu optimizers push <dir> [--project <team/project>] [--name <name>] [--label <label>] [--json]\n orizu runs submit <results.jsonl|results.jsonl.gz> --prompt-version <id> --runner-version <id> --dataset-version <id> --split-set <id> --split <name> [--project <team/project>]\n orizu apps list [--project <team/project>]\n orizu apps create --project <team/project> --name <name> --dataset <datasetId> --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps update [--app <appId>] [--project <team/project>] --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps link-dataset --dataset <datasetId> [--app <appId>] [--project <team/project>] [--version <n>]\n orizu apps detail --app <appId> [--project <team/project>] [--json]\n orizu tasks list [--project <team/project>]\n orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> --assignees <userIdOrEmail1,userIdOrEmail2> [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]\n orizu tasks assign --task <taskId> --assignees <userId1,userId2>\n orizu tasks status --task <taskId> [--json]\n orizu tasks pause --task <taskId>\n orizu tasks unpause --task <taskId>\n orizu datasets upload --file <path> [--project <team/project>] [--name <name>]\n orizu datasets push <path> [--project <team/project>] [--name <name>] [--json]\n orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--json]\n orizu datasets splits create <datasetVersionId> [--from-file <split.json>] [--json]\n orizu datasets download [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--format <csv|json|jsonl>] [--out <path>]\n orizu datasets append [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>\n orizu datasets edit-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>\n orizu datasets delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --row-ids <id1,id2>\n orizu datasets delete [--dataset <datasetId|datasetUrl>] [--project <team/project>]\n orizu datasets lock [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--reason <text>]\n orizu datasets clone [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--name <name>]\n orizu tasks export [--task <taskId>] [--format <csv|json|jsonl>] [--out <path>]`)
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>]`)
204
267
  }
205
268
 
206
269
  function printOptimizationUsage() {
207
- printLine(`\nOptimization lifecycle commands:\n\n orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]\n orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation]\n orizu optimizations pause <run-id> [--reason <text>] [--json]\n orizu optimizations resume <run-id> [--json]\n orizu optimizations finish <run-id> [--best-score <n>] [--best-candidate <id>] [--result-prompt-version <id>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--json]`)
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]`)
208
271
  }
209
272
 
210
273
  let cliArgs = process.argv.slice(2)
@@ -239,6 +302,25 @@ export function expandHomePath(path: string): string {
239
302
  return path
240
303
  }
241
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
+
242
324
  function createCodeVerifier(): string {
243
325
  return randomBytes(32).toString('base64url')
244
326
  }
@@ -835,6 +917,78 @@ function printTaskStatusSummary(data: TaskStatusPayload) {
835
917
  }
836
918
  }
837
919
 
920
+ function compactTerminalText(value: string, maxLength = 160): string {
921
+ const compact = sanitizeTerminalText(value).replace(/\s+/g, ' ').trim()
922
+ if (compact.length <= maxLength) {
923
+ return compact
924
+ }
925
+
926
+ return `${compact.slice(0, Math.max(0, maxLength - 1)).trim()}…`
927
+ }
928
+
929
+ function printIndentedBody(value: string, indent: string) {
930
+ const lines = sanitizeTerminalText(value || '').split(/\r?\n/)
931
+ for (const line of lines) {
932
+ printLine(`${indent}${line}`)
933
+ }
934
+ }
935
+
936
+ function formatPromptCommentAnchor(anchor: PromptCommentAnchor | null): string | null {
937
+ if (!anchor) {
938
+ return null
939
+ }
940
+
941
+ const lineLabel = anchor.startLine && anchor.endLine
942
+ ? anchor.startLine === anchor.endLine
943
+ ? `line ${anchor.startLine}`
944
+ : `lines ${anchor.startLine}-${anchor.endLine}`
945
+ : null
946
+ const selectedText = anchor.text ? `"${compactTerminalText(anchor.text, 180)}"` : null
947
+
948
+ if (lineLabel && selectedText) {
949
+ return `${lineLabel}: ${selectedText}`
950
+ }
951
+ return lineLabel || selectedText
952
+ }
953
+
954
+ function printPromptComments(data: PromptCommentsPayload) {
955
+ const versionLabel = data.version.versionLabel || (
956
+ data.version.versionNumber ? `v${data.version.versionNumber}` : data.version.id
957
+ )
958
+
959
+ printLine(`Prompt: ${sanitizeTerminalText(data.prompt.name)} (${sanitizeTerminalText(data.prompt.id)})`)
960
+ printLine(`Version: ${sanitizeTerminalText(String(versionLabel))} (${sanitizeTerminalText(data.version.id)})`)
961
+ printLine(
962
+ `Comments: ${data.summary.threadCount} thread${data.summary.threadCount === 1 ? '' : 's'} ` +
963
+ `(${data.summary.openThreadCount} open, ${data.summary.resolvedThreadCount} resolved, ${data.summary.replyCount} replies)`
964
+ )
965
+
966
+ if (data.comments.length === 0) {
967
+ printLine('\nNo comments found for this prompt version.')
968
+ return
969
+ }
970
+
971
+ for (const [index, thread] of data.comments.entries()) {
972
+ const authorName = thread.author?.name || 'Unknown'
973
+ const anchor = formatPromptCommentAnchor(thread.anchor)
974
+ printLine('')
975
+ printLine(`${index + 1}. [${thread.status}] ${sanitizeTerminalText(authorName)} · ${sanitizeTerminalText(thread.createdAt)}`)
976
+ if (anchor) {
977
+ printLine(` Selection: ${anchor}`)
978
+ }
979
+ printIndentedBody(thread.body, ' ')
980
+
981
+ if (thread.replies.length > 0) {
982
+ printLine(` Replies (${thread.replies.length})`)
983
+ for (const reply of thread.replies) {
984
+ const replyAuthor = reply.author?.name || 'Unknown'
985
+ printLine(` - ${sanitizeTerminalText(replyAuthor)} · ${sanitizeTerminalText(reply.createdAt)}`)
986
+ printIndentedBody(reply.body, ' ')
987
+ }
988
+ }
989
+ }
990
+ }
991
+
838
992
  const DEFAULT_AUTH_CALLBACK_PORT = 43123
839
993
 
840
994
  function resolveAuthCallbackPort(): number {
@@ -1185,6 +1339,37 @@ async function listJudges() {
1185
1339
  printPromptSummaries(data.judges, 'No judges found.')
1186
1340
  }
1187
1341
 
1342
+ async function listPromptComments() {
1343
+ const promptRef = getPositionalArg(2)
1344
+ const project = getArg('--project') || await resolveProjectSlug(null)
1345
+ const label = getArg('--label')
1346
+ const version = getArg('--version')
1347
+
1348
+ if (!promptRef) {
1349
+ throw new Error('Usage: orizu prompts comments <prompt-id-or-name> --project <team/project> [--label <label> | --version <version-id>] [--json]')
1350
+ }
1351
+ if (label && version) {
1352
+ throw new Error('Use either --label or --version, not both')
1353
+ }
1354
+
1355
+ const params = new URLSearchParams({ project })
1356
+ if (label) params.set('label', label)
1357
+ if (version) params.set('version', version)
1358
+
1359
+ const response = await authedFetch(`/api/cli/prompts/${encodeURIComponent(promptRef)}/comments?${params.toString()}`)
1360
+ if (!response.ok) {
1361
+ throw new Error(`Failed to fetch prompt comments: ${await response.text()}`)
1362
+ }
1363
+
1364
+ const data = await parseJsonResponse<PromptCommentsPayload>(response, 'Prompt comments')
1365
+ if (hasJsonFlag()) {
1366
+ printJson(data as unknown as Record<string, unknown>)
1367
+ return
1368
+ }
1369
+
1370
+ printPromptComments(data)
1371
+ }
1372
+
1188
1373
  async function listScorers() {
1189
1374
  const project = getArg('--project') || await resolveProjectSlug(null)
1190
1375
  const response = await authedFetch(`/api/cli/scorers?project=${encodeURIComponent(project)}`)
@@ -1648,16 +1833,21 @@ async function createDatasetVersion() {
1648
1833
  const datasetId = getPositionalArg(3) || getArg('--dataset')
1649
1834
  const versionLabel = getArg('--label') || getArg('--version-label') || null
1650
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]'
1651
1837
 
1652
1838
  if (!datasetId) {
1653
- throw new Error('Usage: orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--json]')
1839
+ throw new Error(usage)
1654
1840
  }
1655
1841
 
1842
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
1656
1843
  const resolvedDatasetId = await resolveDatasetIdForProject(datasetId, project)
1657
1844
  const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(resolvedDatasetId)}/versions`, {
1658
1845
  method: 'POST',
1659
1846
  headers: { 'Content-Type': 'application/json' },
1660
- body: JSON.stringify({ versionLabel }),
1847
+ body: JSON.stringify({
1848
+ versionLabel,
1849
+ ...(readmeMarkdown !== null ? { readmeMarkdown } : {}),
1850
+ }),
1661
1851
  })
1662
1852
 
1663
1853
  if (!response.ok) {
@@ -1874,6 +2064,7 @@ function normalizeScoreResultsInput(sourcePath: string, raw: string): string {
1874
2064
  }
1875
2065
 
1876
2066
  type OptimizationLifecycleAction = 'pause' | 'resume' | 'finish' | 'fail' | 'cancel'
2067
+ const OPTIMIZATION_REPORT_MAX_BYTES = 2 * 1024 * 1024
1877
2068
 
1878
2069
  function parseOptionalNumberFlag(name: string): number | undefined {
1879
2070
  const value = getArg(name)
@@ -1893,6 +2084,46 @@ function hasObjectKeys(value: Record<string, unknown>): boolean {
1893
2084
  return Object.keys(value).length > 0
1894
2085
  }
1895
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
+
1896
2127
  async function startOptimizationRun() {
1897
2128
  const project = getArg('--project') || await resolveProjectSlug(null)
1898
2129
  const optimizerVersionId = getArg('--optimizer-version')
@@ -1997,6 +2228,11 @@ async function updateOptimizationRunLifecycle(action: OptimizationLifecycleActio
1997
2228
  }
1998
2229
  const metadata = readJsonObjectArg(getArg('--metadata'), 'optimization metadata')
1999
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
+ }
2000
2236
 
2001
2237
  if ((action === 'pause' || action === 'cancel') && reason) {
2002
2238
  metadata.reason = reason
@@ -2011,6 +2247,11 @@ async function updateOptimizationRunLifecycle(action: OptimizationLifecycleActio
2011
2247
  body.metadata = metadata
2012
2248
  }
2013
2249
 
2250
+ if (report) {
2251
+ body.reportMarkdown = report.markdown
2252
+ body.reportSourceName = report.sourceName
2253
+ }
2254
+
2014
2255
  if (action === 'finish') {
2015
2256
  const bestScore = parseOptionalNumberFlag('--best-score')
2016
2257
  if (bestScore !== undefined) {
@@ -2070,6 +2311,32 @@ async function updateOptimizationRunLifecycle(action: OptimizationLifecycleActio
2070
2311
  }
2071
2312
  }
2072
2313
 
2314
+ async function exportOptimizationRun() {
2315
+ const runId = getPositionalArg(2) || getArg('--run-id')
2316
+ const outPathArg = getArg('--out')
2317
+
2318
+ if (!runId) {
2319
+ throw new Error('Usage: orizu optimizations export <run-id> [--out <path>] [--json]')
2320
+ }
2321
+
2322
+ const response = await authedFetch(`/api/cli/optimization-runs/${encodeURIComponent(runId)}/export`)
2323
+ if (!response.ok) {
2324
+ throw new Error(`Failed to export optimization run: ${await response.text()}`)
2325
+ }
2326
+
2327
+ const data = await parseJsonResponse<Record<string, unknown>>(response, 'Optimization export')
2328
+ if (hasJsonFlag() && !outPathArg) {
2329
+ printJson(data)
2330
+ return
2331
+ }
2332
+
2333
+ const filename = outPathArg
2334
+ ? expandHomePath(outPathArg)
2335
+ : `${runId}.optimization.json`
2336
+ writeTextFileEnsuringDir(filename, `${JSON.stringify(data, null, 2)}\n`)
2337
+ printLine(`Saved optimization export to ${sanitizeTerminalText(filename)}`)
2338
+ }
2339
+
2073
2340
  function removeFlagWithValue(args: string[], flag: string): string[] {
2074
2341
  const filtered: string[] = []
2075
2342
  for (let index = 0; index < args.length; index += 1) {
@@ -2838,6 +3105,28 @@ interface AppDetailPayload {
2838
3105
  projectName: string
2839
3106
  }
2840
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
+
2841
3130
  async function appDetail() {
2842
3131
  const appId = getArg('--app')
2843
3132
  const project = getArg('--project')
@@ -2883,6 +3172,50 @@ async function appDetail() {
2883
3172
  printLine(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`)
2884
3173
  }
2885
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
+
2886
3219
  async function listTeamMembers() {
2887
3220
  const teamSlug = await resolveTeamSlug(getArg('--team'))
2888
3221
 
@@ -2969,7 +3302,8 @@ async function createDatasetFromRows(
2969
3302
  project: string,
2970
3303
  name: string,
2971
3304
  sourceType: DatasetUploadSourceType,
2972
- rows: Array<Record<string, unknown>>
3305
+ rows: Array<Record<string, unknown>>,
3306
+ readmeMarkdown: string | null = null
2973
3307
  ): Promise<DatasetUploadResponse> {
2974
3308
  const response = await authedFetch('/api/cli/datasets/upload', {
2975
3309
  method: 'POST',
@@ -2981,6 +3315,7 @@ async function createDatasetFromRows(
2981
3315
  name,
2982
3316
  rows,
2983
3317
  sourceType,
3318
+ ...(readmeMarkdown !== null ? { readmeMarkdown } : {}),
2984
3319
  }),
2985
3320
  })
2986
3321
 
@@ -2995,7 +3330,8 @@ async function createDatasetFromRows(
2995
3330
  async function uploadJsonlDatasetInChunks(
2996
3331
  file: string,
2997
3332
  project: string,
2998
- datasetName: string
3333
+ datasetName: string,
3334
+ readmeMarkdown: string | null = null
2999
3335
  ) {
3000
3336
  let dataset: DatasetUploadResponse['dataset'] | null = null
3001
3337
  let totalUploaded = 0
@@ -3030,7 +3366,7 @@ async function uploadJsonlDatasetInChunks(
3030
3366
 
3031
3367
  try {
3032
3368
  if (!dataset) {
3033
- const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk)
3369
+ const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk, readmeMarkdown)
3034
3370
  dataset = data.dataset
3035
3371
  totalUploaded = data.dataset.rowCount
3036
3372
  continue
@@ -3071,22 +3407,24 @@ async function uploadDataset() {
3071
3407
  const projectArg = getArg('--project')
3072
3408
  const fileArg = getArg('--file')
3073
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>]'
3074
3411
 
3075
3412
  if (!fileArg) {
3076
- throw new Error('Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>]')
3413
+ throw new Error(usage)
3077
3414
  }
3078
3415
 
3416
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
3079
3417
  const file = expandHomePath(fileArg)
3080
3418
  const project = await resolveProjectSlug(projectArg)
3081
3419
  const datasetName = name || basename(file)
3082
3420
 
3083
3421
  if (extname(file).toLowerCase() === '.jsonl') {
3084
- await uploadJsonlDatasetInChunks(file, project, datasetName)
3422
+ await uploadJsonlDatasetInChunks(file, project, datasetName, readmeMarkdown)
3085
3423
  return
3086
3424
  }
3087
3425
 
3088
3426
  const { rows, sourceType } = parseDatasetFile(file)
3089
- const data = await createDatasetFromRows(project, datasetName, sourceType, rows)
3427
+ const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown)
3090
3428
 
3091
3429
  printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`)
3092
3430
  if (data.dataset.url) {
@@ -3098,23 +3436,29 @@ async function pushDataset() {
3098
3436
  const projectArg = getArg('--project')
3099
3437
  const fileArg = getPositionalArg(2) || getArg('--file')
3100
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]'
3101
3440
 
3102
3441
  if (!fileArg) {
3103
- throw new Error('Usage: orizu datasets push <rows.csv|rows.json|rows.jsonl> [--project <team/project>] [--name <name>] [--json]')
3442
+ throw new Error(usage)
3104
3443
  }
3105
3444
 
3445
+ const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
3106
3446
  const file = expandHomePath(fileArg)
3107
3447
  const project = await resolveProjectSlug(projectArg)
3108
3448
  const datasetName = name || basename(file)
3109
3449
  const { rows, sourceType } = parseDatasetFile(file)
3110
- const data = await createDatasetFromRows(project, datasetName, sourceType, rows)
3450
+ const data = await createDatasetFromRows(project, datasetName, sourceType, rows, readmeMarkdown)
3111
3451
 
3112
3452
  if (hasJsonFlag()) {
3113
- printJson({
3453
+ const payload: Record<string, unknown> = {
3114
3454
  dataset_id: data.dataset.id,
3115
3455
  name: data.dataset.name,
3116
3456
  row_count: data.dataset.rowCount,
3117
- })
3457
+ }
3458
+ if (data.readmeVersion?.id) {
3459
+ payload.readme_version_id = data.readmeVersion.id
3460
+ }
3461
+ printJson(payload)
3118
3462
  return
3119
3463
  }
3120
3464
 
@@ -3124,6 +3468,55 @@ async function pushDataset() {
3124
3468
  }
3125
3469
  }
3126
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
+
3127
3520
  function getDatasetReferenceInput(): string | null {
3128
3521
  const fromFlag = getArg('--dataset')
3129
3522
  if (fromFlag) {
@@ -3930,6 +4323,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
3930
4323
  return
3931
4324
  }
3932
4325
 
4326
+ if (command === 'prompts' && subcommand === 'comments') {
4327
+ await listPromptComments()
4328
+ return
4329
+ }
4330
+
3933
4331
  if (command === 'prompts' && subcommand === 'pull') {
3934
4332
  await pullPromptArtifact('prompt')
3935
4333
  return
@@ -4025,6 +4423,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4025
4423
  return
4026
4424
  }
4027
4425
 
4426
+ if (command === 'optimizations' && subcommand === 'export') {
4427
+ await exportOptimizationRun()
4428
+ return
4429
+ }
4430
+
4028
4431
  if (
4029
4432
  command === 'optimizations' &&
4030
4433
  (
@@ -4064,6 +4467,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4064
4467
  return
4065
4468
  }
4066
4469
 
4470
+ if (command === 'apps' && subcommand === 'export') {
4471
+ await exportAppSource()
4472
+ return
4473
+ }
4474
+
4067
4475
  if (command === 'tasks' && subcommand === 'list') {
4068
4476
  await listTasks()
4069
4477
  return
@@ -4115,6 +4523,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4115
4523
  }
4116
4524
 
4117
4525
  const datasetsAction = cliArgs[2]
4526
+ if (command === 'datasets' && subcommand === 'readme' && datasetsAction === 'set') {
4527
+ await setDatasetReadme()
4528
+ return
4529
+ }
4530
+
4118
4531
  if (command === 'datasets' && subcommand === 'versions' && datasetsAction === 'create') {
4119
4532
  await createDatasetVersion()
4120
4533
  return