orizu 0.3.4 → 0.3.6

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
@@ -23,7 +23,10 @@ import { clearServerCredentials, getServerCredentials, saveServerCredentials } f
23
23
  import { parseDatasetFile } from './file-parser.js'
24
24
  import { streamJsonlRowChunks } from './jsonl-stream.js'
25
25
  import { parseDatasetReference } from './dataset-download.js'
26
+ import { extractErrorMessage } from './error-response.js'
26
27
  import { parseGlobalFlags } from './global-flags.js'
28
+ import { getCapabilities, renderHelpForArgs, renderRootHelp } from './help.js'
29
+ import { runLocalAppPreview } from './preview-runtime.js'
27
30
  import {
28
31
  assertSecureTokenTransport,
29
32
  authedFetch,
@@ -31,6 +34,14 @@ import {
31
34
  resolveLoginBaseUrl,
32
35
  setGlobalFlags,
33
36
  } from './http.js'
37
+ import {
38
+ getSkillInstallPath,
39
+ installSkillTarget,
40
+ isSkillInstallTarget,
41
+ SKILL_INSTALL_TARGETS,
42
+ SkillInstallTarget,
43
+ targetNeedsOverwrite,
44
+ } from './skill-installer.js'
34
45
  import { formatTaskCreateError } from './task-create-error.js'
35
46
  import { LoginResponse } from './types.js'
36
47
 
@@ -163,6 +174,12 @@ interface ScorerSummary {
163
174
  implementationKind: string
164
175
  }
165
176
 
177
+ interface ScoreSubmitInput {
178
+ resultsJsonl?: string
179
+ aggregate?: Record<string, unknown>
180
+ aggregateWarning?: string
181
+ }
182
+
166
183
  interface RunnerExecContext {
167
184
  prompt: {
168
185
  body: string | null
@@ -263,14 +280,19 @@ function printVersion() {
263
280
  }
264
281
 
265
282
  function printUsage() {
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>]`)
283
+ printLine(renderRootHelp())
284
+ }
285
+
286
+ function printPreviewUsage() {
287
+ printLine(`\nLocal app preview:\n\n orizu apps preview --file <path> --input-schema <json-path> --output-schema <json-path> --sample-row <json-path> [--screenshot <png-path>] [--headed] [--keep-open] [--component <name>]`)
267
288
  }
268
289
 
269
290
  function printOptimizationUsage() {
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]`)
291
+ 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] [--no-skip-perfect-parent-reflection]\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]`)
271
292
  }
272
293
 
273
294
  let cliArgs = process.argv.slice(2)
295
+ let cliJsonOutput = false
274
296
 
275
297
  function getArg(name: string): string | null {
276
298
  const index = cliArgs.indexOf(name)
@@ -281,6 +303,31 @@ function getArg(name: string): string | null {
281
303
  return cliArgs[index + 1]
282
304
  }
283
305
 
306
+ function getArgs(name: string): string[] {
307
+ const values: string[] = []
308
+ for (let index = 0; index < cliArgs.length; index += 1) {
309
+ if (cliArgs[index] !== name) {
310
+ continue
311
+ }
312
+
313
+ const value = cliArgs[index + 1]
314
+ if (!value || value.startsWith('-')) {
315
+ throw new Error(`Usage: ${name} <value>`)
316
+ }
317
+
318
+ values.push(value)
319
+ index += 1
320
+ }
321
+
322
+ return values
323
+ }
324
+
325
+ function rejectDashPrefixedOptionValue(name: string, value: string | null) {
326
+ if (value && value.startsWith('-')) {
327
+ throw new Error(`Invalid value for ${name}: option values cannot start with a dash`)
328
+ }
329
+ }
330
+
284
331
  export function normalizeSlugInput(slug: string): string {
285
332
  return slug.trim().toLowerCase()
286
333
  }
@@ -2001,18 +2048,23 @@ async function submitScoreResults() {
2001
2048
  const candidateId = getArg('--candidate')
2002
2049
 
2003
2050
  if (!resultsPath || !scorerVersionId || (!subjectPromptVersionId && (!optimizationRunId || !candidateId))) {
2004
- throw new Error('Usage: orizu scores submit <results.jsonl|results.json> --project <team/project> --scorer-version <id> (--subject-version <prompt-version-id> | --optimization-run <id> --candidate <id>) [--dataset-version <id> --split-set <id> --split <name>] [--json]')
2051
+ throw new Error('Usage: orizu scores submit <results.jsonl|results.json> --project <team/project> --scorer-version <id> (--subject-version <prompt-version-id> | --optimization-run <id> --candidate <id>) [--aggregate] [--dataset-version <id> --split-set <id> --split <name>] [--json]')
2005
2052
  }
2006
2053
 
2007
2054
  const resultBytes = readSourceBytes(resultsPath)
2008
2055
  const raw = resultsPath.endsWith('.gz')
2009
2056
  ? gunzipSync(resultBytes).toString('utf8')
2010
2057
  : resultBytes.toString('utf8')
2011
- const resultsJsonl = normalizeScoreResultsInput(resultsPath, raw)
2058
+ const scoreInput = normalizeScoreResultsInput(resultsPath, raw, hasArg('--aggregate'))
2059
+ if (scoreInput.aggregateWarning) {
2060
+ console.warn(scoreInput.aggregateWarning)
2061
+ }
2062
+ const aggregatePayload = scoreInput.aggregate || {}
2012
2063
  const response = await authedFetch(`/api/cli/scores/submit?project=${encodeURIComponent(project)}`, {
2013
2064
  method: 'POST',
2014
2065
  headers: { 'Content-Type': 'application/json' },
2015
2066
  body: JSON.stringify({
2067
+ ...aggregatePayload,
2016
2068
  scorerVersionId,
2017
2069
  subjectPromptVersionId: subjectPromptVersionId || undefined,
2018
2070
  datasetVersionId: getArg('--dataset-version') || undefined,
@@ -2020,7 +2072,7 @@ async function submitScoreResults() {
2020
2072
  splitName: getArg('--split') || undefined,
2021
2073
  optimizationRunId: optimizationRunId || undefined,
2022
2074
  candidateId: candidateId || undefined,
2023
- resultsJsonl,
2075
+ resultsJsonl: scoreInput.resultsJsonl,
2024
2076
  }),
2025
2077
  })
2026
2078
 
@@ -2046,25 +2098,161 @@ async function submitScoreResults() {
2046
2098
  )
2047
2099
  }
2048
2100
 
2049
- function normalizeScoreResultsInput(sourcePath: string, raw: string): string {
2101
+ function looksLikeAggregateScoreObject(value: Record<string, unknown>): boolean {
2102
+ return (
2103
+ value.scoreValue !== undefined ||
2104
+ value.score_value !== undefined ||
2105
+ value.diagnostics !== undefined ||
2106
+ value.feedbackSummary !== undefined ||
2107
+ value.feedback_summary !== undefined ||
2108
+ value.rowEvidence !== undefined ||
2109
+ value.row_evidence !== undefined ||
2110
+ value.dependencyScoreRunIds !== undefined ||
2111
+ value.dependency_score_run_ids !== undefined
2112
+ )
2113
+ }
2114
+
2115
+ function numberFromUnknown(value: unknown): number | null {
2116
+ if (typeof value === 'number' && Number.isFinite(value)) return value
2117
+ if (typeof value === 'string' && value.trim()) {
2118
+ const parsed = Number(value)
2119
+ return Number.isFinite(parsed) ? parsed : null
2120
+ }
2121
+ if (typeof value === 'boolean') return value ? 1 : 0
2122
+ return null
2123
+ }
2124
+
2125
+ function normalizeScoreResultsInput(sourcePath: string, raw: string, aggregateMode = false): ScoreSubmitInput {
2050
2126
  const logicalPath = sourcePath.endsWith('.gz') ? sourcePath.slice(0, -3) : sourcePath
2051
- if (!logicalPath.endsWith('.json')) return raw
2127
+ if (!logicalPath.endsWith('.json')) return { resultsJsonl: raw }
2052
2128
 
2053
2129
  try {
2054
2130
  const parsed = JSON.parse(raw) as unknown
2131
+ if (aggregateMode) {
2132
+ if (!isRecord(parsed)) {
2133
+ throw new Error('Aggregate score results JSON must be an object')
2134
+ }
2135
+ return { aggregate: parsed }
2136
+ }
2137
+ if (isRecord(parsed) && looksLikeAggregateScoreObject(parsed)) {
2138
+ return {
2139
+ aggregate: parsed,
2140
+ aggregateWarning: 'Warning: aggregate score JSON detected. Pass --aggregate to make this explicit.',
2141
+ }
2142
+ }
2055
2143
  const rows = Array.isArray(parsed) ? parsed : [parsed]
2056
2144
  if (rows.some(row => !isRecord(row))) {
2057
2145
  throw new Error('JSON score results must be an object or an array of objects')
2058
2146
  }
2059
- return rows.map(row => JSON.stringify(row)).join('\n')
2147
+ return { resultsJsonl: rows.map(row => JSON.stringify(row)).join('\n') }
2060
2148
  } catch (error) {
2061
2149
  const message = error instanceof Error ? error.message : String(error)
2062
2150
  throw new Error(`Invalid score results JSON '${sourcePath}': ${message}`)
2063
2151
  }
2064
2152
  }
2065
2153
 
2154
+ function readOptionalJsonlFile(pathArg: string | null): string | undefined {
2155
+ if (!pathArg) return undefined
2156
+ const bytes = readSourceBytes(pathArg)
2157
+ return pathArg.endsWith('.gz')
2158
+ ? gunzipSync(bytes).toString('utf8')
2159
+ : bytes.toString('utf8')
2160
+ }
2161
+
2162
+ function parseAliasAssignments(value: string | null): Record<string, string> {
2163
+ const result: Record<string, string> = {}
2164
+ if (!value) return result
2165
+ for (const item of parseCommaSeparated(value)) {
2166
+ const equalsIndex = item.indexOf('=')
2167
+ if (equalsIndex <= 0 || equalsIndex === item.length - 1) {
2168
+ throw new Error(`Expected alias assignment in form alias=value, got '${item}'`)
2169
+ }
2170
+ result[item.slice(0, equalsIndex)] = item.slice(equalsIndex + 1)
2171
+ }
2172
+ return result
2173
+ }
2174
+
2175
+ function readDependencyResultAssignments(value: string | null): Record<string, string> | undefined {
2176
+ const assignments = parseAliasAssignments(value)
2177
+ const result: Record<string, string> = {}
2178
+ for (const [alias, path] of Object.entries(assignments)) {
2179
+ result[alias] = readOptionalJsonlFile(path) || ''
2180
+ }
2181
+ return Object.keys(result).length > 0 ? result : undefined
2182
+ }
2183
+
2184
+ async function execScorer() {
2185
+ const project = getArg('--project') || await resolveProjectSlug(null)
2186
+ const scorerVersionId = getArg('--scorer-version')
2187
+ const subjectPromptVersionId = getArg('--subject-version') || getArg('--prompt-version')
2188
+ const optimizationRunId = getArg('--optimization-run')
2189
+ const candidateId = getArg('--candidate')
2190
+ const datasetVersionId = getArg('--dataset-version')
2191
+ const splitSetId = getArg('--split-set')
2192
+ const splitName = getArg('--split')
2193
+ const outPath = getArg('--out')
2194
+
2195
+ if (
2196
+ !scorerVersionId ||
2197
+ (!subjectPromptVersionId && (!optimizationRunId || !candidateId)) ||
2198
+ !datasetVersionId ||
2199
+ !splitSetId ||
2200
+ !splitName
2201
+ ) {
2202
+ throw new Error('Usage: orizu scorers exec --scorer-version <id> (--subject-version <prompt-version-id> | --optimization-run <id> --candidate <id>) --dataset-version <id> --split-set <id> --split <name> [--subject-results <jsonl>] [--dependency-score-run <alias=id>] [--dependency-results <alias=path>] [--no-submit] [--out <score.json>] [--project <team/project>] [--json]')
2203
+ }
2204
+
2205
+ const response = await authedFetch(`/api/cli/scorers/exec?project=${encodeURIComponent(project)}`, {
2206
+ method: 'POST',
2207
+ headers: { 'Content-Type': 'application/json' },
2208
+ body: JSON.stringify({
2209
+ scorerVersionId,
2210
+ subjectPromptVersionId: subjectPromptVersionId || undefined,
2211
+ optimizationRunId: optimizationRunId || undefined,
2212
+ candidateId: candidateId || undefined,
2213
+ datasetVersionId,
2214
+ splitSetId,
2215
+ splitName,
2216
+ submit: !hasArg('--no-submit'),
2217
+ reuseExisting: !hasArg('--no-reuse-existing'),
2218
+ subjectResultsJsonl: readOptionalJsonlFile(getArg('--subject-results') || getArg('--outputs')),
2219
+ dependencyScoreRunIds: parseAliasAssignments(getArg('--dependency-score-run') || getArg('--dependency-score-runs')),
2220
+ dependencyResultsJsonl: readDependencyResultAssignments(getArg('--dependency-results')),
2221
+ }),
2222
+ })
2223
+
2224
+ if (!response.ok) {
2225
+ throw new Error(`Failed to execute scorer: ${await response.text()}`)
2226
+ }
2227
+
2228
+ const data = await parseJsonResponse<Record<string, unknown>>(response, 'Scorer exec')
2229
+ if (outPath) {
2230
+ writeFileSync(expandHomePath(outPath), `${JSON.stringify(data.scoreResult || data, null, 2)}\n`)
2231
+ }
2232
+ if (hasJsonFlag()) {
2233
+ printJson(data)
2234
+ return
2235
+ }
2236
+
2237
+ const scoreRun = isRecord(data.scoreRun) ? data.scoreRun : null
2238
+ const scoreResult = isRecord(data.scoreResult) ? data.scoreResult : null
2239
+ const scoreValue = scoreRun
2240
+ ? scoreRun.scoreValue
2241
+ : scoreResult
2242
+ ? scoreResult.scoreValue
2243
+ : null
2244
+ if (scoreRun && typeof scoreRun.id === 'string') {
2245
+ printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} -> score ${sanitizeTerminalText(scoreRun.id)} (${formatPercent(numberFromUnknown(scoreValue))})`)
2246
+ } else {
2247
+ printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} (${formatPercent(numberFromUnknown(scoreValue))})`)
2248
+ }
2249
+ if (outPath) {
2250
+ printLine(`Wrote scorer result to ${sanitizeTerminalText(outPath)}`)
2251
+ }
2252
+ }
2253
+
2066
2254
  type OptimizationLifecycleAction = 'pause' | 'resume' | 'finish' | 'fail' | 'cancel'
2067
- const OPTIMIZATION_REPORT_MAX_BYTES = 2 * 1024 * 1024
2255
+ const MARKDOWN_REPORT_MAX_BYTES = 2 * 1024 * 1024
2068
2256
 
2069
2257
  function parseOptionalNumberFlag(name: string): number | undefined {
2070
2258
  const value = getArg(name)
@@ -2084,10 +2272,15 @@ function hasObjectKeys(value: Record<string, unknown>): boolean {
2084
2272
  return Object.keys(value).length > 0
2085
2273
  }
2086
2274
 
2087
- function readOptimizationReportInput(): { markdown: string; sourceName: string | null } | null {
2275
+ function readMarkdownReportInput(
2276
+ reportLabel: 'Optimization' | 'Task'
2277
+ ): { markdown: string; sourceName: string | null } | null {
2088
2278
  const report = getArg('--report')
2089
2279
  const reportFile = getArg('--report-file')
2090
2280
 
2281
+ rejectDashPrefixedOptionValue('--report', report)
2282
+ rejectDashPrefixedOptionValue('--report-file', reportFile)
2283
+
2091
2284
  if (report && reportFile) {
2092
2285
  throw new Error('Use either --report or --report-file, not both')
2093
2286
  }
@@ -2114,16 +2307,24 @@ function readOptimizationReportInput(): { markdown: string; sourceName: string |
2114
2307
  }
2115
2308
 
2116
2309
  if (!markdown.trim()) {
2117
- throw new Error('Optimization report markdown must not be blank')
2310
+ throw new Error(`${reportLabel} report markdown must not be blank`)
2118
2311
  }
2119
2312
 
2120
- if (Buffer.byteLength(markdown, 'utf8') > OPTIMIZATION_REPORT_MAX_BYTES) {
2121
- throw new Error(`Optimization report exceeds ${OPTIMIZATION_REPORT_MAX_BYTES} bytes`)
2313
+ if (Buffer.byteLength(markdown, 'utf8') > MARKDOWN_REPORT_MAX_BYTES) {
2314
+ throw new Error(`${reportLabel} report exceeds ${MARKDOWN_REPORT_MAX_BYTES} bytes`)
2122
2315
  }
2123
2316
 
2124
2317
  return { markdown, sourceName }
2125
2318
  }
2126
2319
 
2320
+ function readOptimizationReportInput(): { markdown: string; sourceName: string | null } | null {
2321
+ return readMarkdownReportInput('Optimization')
2322
+ }
2323
+
2324
+ function readTaskReportInput(): { markdown: string; sourceName: string | null } | null {
2325
+ return readMarkdownReportInput('Task')
2326
+ }
2327
+
2127
2328
  async function startOptimizationRun() {
2128
2329
  const project = getArg('--project') || await resolveProjectSlug(null)
2129
2330
  const optimizerVersionId = getArg('--optimizer-version')
@@ -2521,13 +2722,120 @@ function readSourceBytes(pathArg: string): Buffer {
2521
2722
  }
2522
2723
 
2523
2724
  function hasJsonFlag(): boolean {
2524
- return hasArg('--json')
2725
+ return cliJsonOutput || hasArg('--json')
2525
2726
  }
2526
2727
 
2527
2728
  function printJson(value: Record<string, unknown>) {
2528
2729
  printLine(JSON.stringify(value))
2529
2730
  }
2530
2731
 
2732
+ function printCapabilities() {
2733
+ const capabilities = getCapabilities(getCliVersion())
2734
+ if (hasJsonFlag()) {
2735
+ printLine(JSON.stringify(capabilities))
2736
+ return
2737
+ }
2738
+
2739
+ printLine('orizu capabilities')
2740
+ printLine('')
2741
+ printLine('Run `orizu capabilities --json` for a machine-readable command manifest.')
2742
+ printLine('')
2743
+ for (const command of capabilities.commands) {
2744
+ printLine(` ${command.name.padEnd(34)} ${command.help}`)
2745
+ }
2746
+ }
2747
+
2748
+ async function askYesNo(question: string, defaultYes: boolean): Promise<boolean> {
2749
+ const rl = createInterface({ input, output })
2750
+ try {
2751
+ const suffix = defaultYes ? 'Y/n' : 'y/N'
2752
+ const answer = (await rl.question(`${question} (${suffix}) `)).trim().toLowerCase()
2753
+ if (!answer) {
2754
+ return defaultYes
2755
+ }
2756
+ return answer === 'y' || answer === 'yes'
2757
+ } finally {
2758
+ rl.close()
2759
+ }
2760
+ }
2761
+
2762
+ async function promptSkillInstallTargets(): Promise<SkillInstallTarget[]> {
2763
+ if (!isInteractiveTerminal()) {
2764
+ throw new Error(
2765
+ `Usage: orizu install-skill --target <${SKILL_INSTALL_TARGETS.join('|')}> [--yes] [--dry-run]`
2766
+ )
2767
+ }
2768
+
2769
+ printLine('Where should the Orizu CLI skill be installed?')
2770
+ const targets: SkillInstallTarget[] = []
2771
+ for (const target of SKILL_INSTALL_TARGETS) {
2772
+ const defaultYes = target === 'agent-user'
2773
+ if (await askYesNo(` Install to ${target}?`, defaultYes)) {
2774
+ targets.push(target)
2775
+ }
2776
+ }
2777
+
2778
+ return targets
2779
+ }
2780
+
2781
+ function parseSkillInstallTargets(): SkillInstallTarget[] {
2782
+ const rawTargets = getArgs('--target')
2783
+ const targets: SkillInstallTarget[] = []
2784
+ for (const target of rawTargets) {
2785
+ if (!isSkillInstallTarget(target)) {
2786
+ throw new Error(
2787
+ `Unknown skill install target '${target}'. Available targets: ${SKILL_INSTALL_TARGETS.join(', ')}`
2788
+ )
2789
+ }
2790
+ if (!targets.includes(target)) {
2791
+ targets.push(target)
2792
+ }
2793
+ }
2794
+ return targets
2795
+ }
2796
+
2797
+ function formatSkillInstallAction(action: string): string {
2798
+ if (action === 'created') return 'Wrote'
2799
+ if (action === 'updated') return 'Updated'
2800
+ if (action === 'would-create') return 'Would write'
2801
+ return 'Would update'
2802
+ }
2803
+
2804
+ async function installSkillCommand() {
2805
+ const skipConfirm = hasArg('--yes')
2806
+ const dryRun = hasArg('--dry-run')
2807
+ let targets = parseSkillInstallTargets()
2808
+ if (targets.length === 0) {
2809
+ targets = await promptSkillInstallTargets()
2810
+ }
2811
+
2812
+ if (targets.length === 0) {
2813
+ printLine('No targets selected; nothing to do.')
2814
+ return
2815
+ }
2816
+
2817
+ for (const target of targets) {
2818
+ let overwrite = skipConfirm
2819
+ const installPath = getSkillInstallPath(target)
2820
+ if (!dryRun && !overwrite && targetNeedsOverwrite(target)) {
2821
+ if (!isInteractiveTerminal()) {
2822
+ throw new Error(`${installPath} already exists. Pass --yes to replace it.`)
2823
+ }
2824
+ overwrite = await askYesNo(`Replace ${installPath}?`, true)
2825
+ if (!overwrite) {
2826
+ printLine(`Skipped ${installPath}`)
2827
+ continue
2828
+ }
2829
+ }
2830
+
2831
+ const result = installSkillTarget(target, {
2832
+ overwrite,
2833
+ dryRun,
2834
+ })
2835
+ printLine(`${formatSkillInstallAction(result.action)} ${result.path}`)
2836
+ }
2837
+ }
2838
+
2531
2839
  function readJsonFile(pathArg: string): Record<string, unknown> {
2532
2840
  const raw = readSourceFile(pathArg)
2533
2841
  try {
@@ -2789,15 +3097,61 @@ async function createAppFromFile() {
2789
3097
  }
2790
3098
 
2791
3099
  const data = await parseJsonResponse<{
2792
- app: { id: string; name: string; versionNum: number; componentName?: string }
3100
+ app: { id: string; name: string; versionNum: number; componentName?: string; url?: string }
2793
3101
  warnings?: string[]
2794
3102
  }>(response, 'App create')
2795
3103
  printLine(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`)
3104
+ if (data.app.url) {
3105
+ printLine(`View app: ${formatTerminalLink(data.app.url)}`)
3106
+ }
2796
3107
  if (data.warnings?.length) {
2797
3108
  printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
2798
3109
  }
2799
3110
  }
2800
3111
 
3112
+ async function previewAppFromFile() {
3113
+ const filePath = getArg('--file')
3114
+ const inputSchemaPath = getArg('--input-schema')
3115
+ const outputSchemaPath = getArg('--output-schema')
3116
+ const sampleRowPath = getArg('--sample-row')
3117
+ const screenshotPath = getArg('--screenshot')
3118
+ const component = getArg('--component') || undefined
3119
+ const headed = hasArg('--headed')
3120
+ const keepOpen = hasArg('--keep-open')
3121
+
3122
+ if (!filePath || !inputSchemaPath || !outputSchemaPath || !sampleRowPath) {
3123
+ throw new Error('Usage: orizu apps preview --file <path> --input-schema <json-path> --output-schema <json-path> --sample-row <json-path> [--screenshot <png-path>] [--headed] [--keep-open] [--component <name>]')
3124
+ }
3125
+
3126
+ const expandedFilePath = realpathSync(expandHomePath(filePath))
3127
+ const inputJsonSchema = readJsonFile(inputSchemaPath)
3128
+ const outputJsonSchema = readJsonFile(outputSchemaPath)
3129
+ const sampleRow = readJsonFile(sampleRowPath)
3130
+ const expandedScreenshotPath = screenshotPath ? expandHomePath(screenshotPath) : undefined
3131
+
3132
+ const result = await runLocalAppPreview({
3133
+ filePath: expandedFilePath,
3134
+ inputSchema: inputJsonSchema,
3135
+ outputSchema: outputJsonSchema,
3136
+ sampleRow,
3137
+ screenshotPath: expandedScreenshotPath,
3138
+ componentName: component,
3139
+ headed,
3140
+ keepOpen,
3141
+ })
3142
+
3143
+ printLine(`Preview rendered: ${formatTerminalLink(result.url)}`)
3144
+ if (result.screenshotPath) {
3145
+ printLine(`Screenshot: ${sanitizeTerminalText(result.screenshotPath)}`)
3146
+ }
3147
+ if (headed || keepOpen) {
3148
+ printLine('Headed preview is running. Close Chromium or stop the command when you are done.')
3149
+ }
3150
+ if (result.warnings.length > 0) {
3151
+ printLine(`Warnings: ${sanitizeTerminalText(result.warnings.join('; '))}`)
3152
+ }
3153
+ }
3154
+
2801
3155
  async function updateAppFromFile() {
2802
3156
  const filePath = getArg('--file')
2803
3157
  const inputSchemaPath = getArg('--input-schema')
@@ -2905,6 +3259,7 @@ async function createTask() {
2905
3259
  const appId = getArg('--app')
2906
3260
  const title = getArg('--title')
2907
3261
  const assignees = parseCommaSeparated(getArg('--assignees'))
3262
+ const publish = hasArg('--publish')
2908
3263
  const versionArg = getArg('--version')
2909
3264
  const instructions = getArg('--instructions')
2910
3265
  const labelsPerItemArg = getArg('--labels-per-item')
@@ -2913,8 +3268,8 @@ async function createTask() {
2913
3268
  const versionNum =
2914
3269
  Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : null
2915
3270
 
2916
- if (!projectSlug || !datasetId || !appId || !title || assignees.length === 0) {
2917
- throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> --assignees <userIdOrEmail1,userIdOrEmail2> [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]')
3271
+ if (!projectSlug || !datasetId || !appId || !title || (publish && assignees.length === 0)) {
3272
+ throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]')
2918
3273
  }
2919
3274
 
2920
3275
  if (versionArg && versionNum === null) {
@@ -2931,6 +3286,7 @@ async function createTask() {
2931
3286
  versionNum,
2932
3287
  title,
2933
3288
  memberIds: assignees,
3289
+ publish,
2934
3290
  instructions,
2935
3291
  requiredAssignmentsPerRow: labelsPerItem,
2936
3292
  }),
@@ -2973,6 +3329,13 @@ async function createTask() {
2973
3329
  status: data.task.status,
2974
3330
  requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
2975
3331
  assignmentsCreated: data.assignmentsCreated,
3332
+ draft: data.task.status === 'draft',
3333
+ ...(data.task.status === 'draft'
3334
+ ? {
3335
+ message: 'Draft created. Test it manually before assigning.',
3336
+ publishCommand: `orizu tasks publish --task ${data.task.id} --assignees <userId1,userId2>`,
3337
+ }
3338
+ : {}),
2976
3339
  ...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
2977
3340
  ...(data.warning ? { warning: data.warning } : {}),
2978
3341
  }, null, 2))
@@ -2981,15 +3344,76 @@ async function createTask() {
2981
3344
 
2982
3345
  const baseUrl = getBaseUrl()
2983
3346
  const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`
3347
+ const isDraft = data.task.status === 'draft'
2984
3348
  printLine(
2985
- `Created task ${sanitizeTerminalText(data.task.title)} (${sanitizeTerminalText(data.task.id)}) [${sanitizeTerminalText(data.task.status)}]` +
3349
+ `Created ${isDraft ? 'draft ' : ''}task ${sanitizeTerminalText(data.task.title)} (${sanitizeTerminalText(data.task.id)}) [${sanitizeTerminalText(data.task.status)}]` +
2986
3350
  `\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
2987
3351
  `\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
2988
3352
  `\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
2989
3353
  `\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
2990
3354
  `\n Assignments: ${data.assignmentsCreated}` +
2991
3355
  (data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
2992
- `\n URL: ${sanitizeTerminalText(taskUrl)}`
3356
+ `\n URL: ${sanitizeTerminalText(taskUrl)}` +
3357
+ (isDraft
3358
+ ? `\n\nThis task is a draft. Test it manually before assigning.\nPublish after approval: orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignees <userId1,userId2>`
3359
+ : '')
3360
+ )
3361
+ }
3362
+
3363
+ async function publishTask() {
3364
+ const taskId = getArg('--task')
3365
+ const assignees = parseCommaSeparated(getArg('--assignees'))
3366
+
3367
+ if (!taskId || assignees.length === 0) {
3368
+ throw new Error('Usage: orizu tasks publish --task <taskId> --assignees <userId1,userId2> [--json]')
3369
+ }
3370
+
3371
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/publish`, {
3372
+ method: 'POST',
3373
+ headers: { 'Content-Type': 'application/json' },
3374
+ body: JSON.stringify({ memberIds: assignees }),
3375
+ })
3376
+
3377
+ if (!response.ok) {
3378
+ throw new Error(`Failed to publish task: ${await extractErrorMessage(response)}`)
3379
+ }
3380
+
3381
+ const data = await parseJsonResponse<{
3382
+ task: { id: string; status: string }
3383
+ assignmentsProcessed: number
3384
+ assignmentsCreated: number
3385
+ assignmentsRemoved?: number
3386
+ assignmentShortfall?: number
3387
+ warning?: string
3388
+ }>(
3389
+ response,
3390
+ 'Task publish'
3391
+ )
3392
+
3393
+ if (hasArg('--json')) {
3394
+ printLine(JSON.stringify({
3395
+ taskId: data.task.id,
3396
+ status: data.task.status,
3397
+ assignmentsCreated: data.assignmentsCreated,
3398
+ assignmentsProcessed: data.assignmentsProcessed,
3399
+ ...(data.assignmentsRemoved !== undefined
3400
+ ? { assignmentsRemoved: data.assignmentsRemoved }
3401
+ : {}),
3402
+ ...(data.assignmentShortfall !== undefined
3403
+ ? { assignmentShortfall: data.assignmentShortfall }
3404
+ : {}),
3405
+ ...(data.warning ? { warning: data.warning } : {}),
3406
+ }, null, 2))
3407
+ return
3408
+ }
3409
+
3410
+ printLine(
3411
+ `Published task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]` +
3412
+ `\n Assignments: ${data.assignmentsCreated}` +
3413
+ (data.assignmentsRemoved !== undefined
3414
+ ? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
3415
+ : '') +
3416
+ (data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '')
2993
3417
  )
2994
3418
  }
2995
3419
 
@@ -3082,6 +3506,58 @@ async function updateTaskStatus(targetStatus: 'paused' | 'active') {
3082
3506
  printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
3083
3507
  }
3084
3508
 
3509
+ async function setTaskReport() {
3510
+ const taskId = getArg('--task')
3511
+ rejectDashPrefixedOptionValue('--task', taskId)
3512
+ if (!taskId) {
3513
+ throw new Error(
3514
+ 'Usage: orizu tasks report set --task <taskId> (--report <markdown|@file> | --report-file <path>) [--json]'
3515
+ )
3516
+ }
3517
+
3518
+ const report = readTaskReportInput()
3519
+ if (!report) {
3520
+ throw new Error(
3521
+ 'Usage: orizu tasks report set --task <taskId> (--report <markdown|@file> | --report-file <path>) [--json]'
3522
+ )
3523
+ }
3524
+
3525
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/report`, {
3526
+ method: 'PATCH',
3527
+ headers: { 'Content-Type': 'application/json' },
3528
+ body: JSON.stringify({
3529
+ reportMarkdown: report.markdown,
3530
+ reportSourceName: report.sourceName,
3531
+ }),
3532
+ })
3533
+
3534
+ if (!response.ok) {
3535
+ throw new Error(`Failed to update task report: ${await response.text()}`)
3536
+ }
3537
+
3538
+ const data = await parseJsonResponse<{
3539
+ task: {
3540
+ id: string
3541
+ status: string
3542
+ report: {
3543
+ markdown: string
3544
+ sourceName: string | null
3545
+ createdAt: string | null
3546
+ }
3547
+ }
3548
+ }>(response, 'Task report update')
3549
+
3550
+ if (hasJsonFlag()) {
3551
+ printJson(data)
3552
+ return
3553
+ }
3554
+
3555
+ printLine(
3556
+ `Uploaded task report for ${sanitizeTerminalText(data.task.id)} ` +
3557
+ `[${sanitizeTerminalText(data.task.status)}]`
3558
+ )
3559
+ }
3560
+
3085
3561
  interface AppDetailPayload {
3086
3562
  id: string
3087
3563
  name: string
@@ -4236,11 +4712,45 @@ async function runnersExec() {
4236
4712
  }
4237
4713
  }
4238
4714
 
4715
+ function isHelpToken(value: string): boolean {
4716
+ return value === '--help' || value === '-h'
4717
+ }
4718
+
4719
+ function extractHelpTarget(args: string[]): string[] | null {
4720
+ if (args[0] === 'help') {
4721
+ return helpTargetFromArgs(args.slice(1))
4722
+ }
4723
+
4724
+ if (!args.some(isHelpToken)) {
4725
+ return null
4726
+ }
4727
+
4728
+ return helpTargetFromArgs(args)
4729
+ }
4730
+
4731
+ function helpTargetFromArgs(args: string[]): string[] {
4732
+ const targetArgs = args.filter(arg => !isHelpToken(arg) && arg !== '--json')
4733
+ const optionIndex = targetArgs.findIndex(arg => arg.startsWith('-'))
4734
+ return optionIndex === -1 ? targetArgs : targetArgs.slice(0, optionIndex)
4735
+ }
4736
+
4239
4737
  export async function main(rawArgs = process.argv.slice(2)) {
4240
4738
  const parsed = parseGlobalFlags(rawArgs)
4241
4739
  setGlobalFlags(parsed.flags)
4242
4740
  cliArgs = parsed.args
4243
4741
 
4742
+ cliJsonOutput = false
4743
+ if (cliArgs[0] === '--json') {
4744
+ cliJsonOutput = true
4745
+ cliArgs = cliArgs.slice(1)
4746
+ }
4747
+
4748
+ const helpTarget = extractHelpTarget(cliArgs)
4749
+ if (helpTarget !== null) {
4750
+ printLine(renderHelpForArgs(helpTarget))
4751
+ return
4752
+ }
4753
+
4244
4754
  const command = cliArgs[0]
4245
4755
  const subcommand = cliArgs[1]
4246
4756
 
@@ -4251,10 +4761,24 @@ export async function main(rawArgs = process.argv.slice(2)) {
4251
4761
 
4252
4762
  if (!command) {
4253
4763
  printUsage()
4254
- printOptimizationUsage()
4255
4764
  process.exit(1)
4256
4765
  }
4257
4766
 
4767
+ if (command === 'capabilities') {
4768
+ printCapabilities()
4769
+ return
4770
+ }
4771
+
4772
+ if (command === 'install-skill') {
4773
+ await installSkillCommand()
4774
+ return
4775
+ }
4776
+
4777
+ if (command === 'skills' && subcommand === 'install') {
4778
+ await installSkillCommand()
4779
+ return
4780
+ }
4781
+
4258
4782
  if (command === 'login') {
4259
4783
  await login()
4260
4784
  return
@@ -4388,6 +4912,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4388
4912
  return
4389
4913
  }
4390
4914
 
4915
+ if (command === 'scorers' && subcommand === 'exec') {
4916
+ await execScorer()
4917
+ return
4918
+ }
4919
+
4391
4920
  if (command === 'runners' && subcommand === 'push') {
4392
4921
  await pushRunnerArtifact('runner')
4393
4922
  return
@@ -4452,6 +4981,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4452
4981
  return
4453
4982
  }
4454
4983
 
4984
+ if (command === 'apps' && subcommand === 'preview') {
4985
+ await previewAppFromFile()
4986
+ return
4987
+ }
4988
+
4455
4989
  if (command === 'apps' && subcommand === 'update') {
4456
4990
  await updateAppFromFile()
4457
4991
  return
@@ -4482,6 +5016,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4482
5016
  return
4483
5017
  }
4484
5018
 
5019
+ if (command === 'tasks' && subcommand === 'publish') {
5020
+ await publishTask()
5021
+ return
5022
+ }
5023
+
4485
5024
  if (command === 'tasks' && subcommand === 'assign') {
4486
5025
  await assignTask()
4487
5026
  return
@@ -4492,6 +5031,15 @@ export async function main(rawArgs = process.argv.slice(2)) {
4492
5031
  return
4493
5032
  }
4494
5033
 
5034
+ if (
5035
+ command === 'tasks' &&
5036
+ subcommand === 'report' &&
5037
+ (cliArgs[2] === 'set' || cliArgs[2] === 'upload')
5038
+ ) {
5039
+ await setTaskReport()
5040
+ return
5041
+ }
5042
+
4495
5043
  if (command === 'tasks' && subcommand === 'pause') {
4496
5044
  await updateTaskStatus('paused')
4497
5045
  return
@@ -4569,7 +5117,6 @@ export async function main(rawArgs = process.argv.slice(2)) {
4569
5117
  }
4570
5118
 
4571
5119
  printUsage()
4572
- printOptimizationUsage()
4573
5120
  process.exit(1)
4574
5121
  }
4575
5122