orizu 0.3.5 → 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,9 @@ 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'
27
29
  import { runLocalAppPreview } from './preview-runtime.js'
28
30
  import {
29
31
  assertSecureTokenTransport,
@@ -32,6 +34,14 @@ import {
32
34
  resolveLoginBaseUrl,
33
35
  setGlobalFlags,
34
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'
35
45
  import { formatTaskCreateError } from './task-create-error.js'
36
46
  import { LoginResponse } from './types.js'
37
47
 
@@ -270,8 +280,7 @@ function printVersion() {
270
280
  }
271
281
 
272
282
  function printUsage() {
273
- printLine(`orizu global options:\n\n --local Use http://localhost:3000\n --server <url> Use a specific server origin (for example: https://preview.example.com)\n --version, -v Print the orizu CLI version\n\norizu commands:\n\n orizu login [--no-prompt-if-logged-in]\n orizu logout\n orizu whoami\n orizu env [--project <team/project>] [--project-id <projectId>]\n orizu log <event_type> --run-id <id> --sequence <n> --payload @event.json\n orizu teams list\n orizu teams create [--name <name>]\n orizu teams members list [--team <teamSlug>]\n orizu teams members add --email <email> [--team <teamSlug>]\n orizu teams members remove --email <email> [--team <teamSlug>]\n orizu teams members role --team <teamSlug> --email <email> --role <admin|member>\n orizu projects list [--team <teamSlug>]\n orizu projects create --name <name> [--team <teamSlug>]\n orizu prompts list --project <team/project>\n orizu prompts comments <prompt-id-or-name> --project <team/project> [--label <label> | --version <id>] [--json]\n orizu prompts pull <prompt-id-or-name> --project <team/project> --out <dir> [--label <label> | --version <id>] [--json]\n orizu prompts push <dir> [--runner-version <id>] [--project <team/project>] [--parent <version-id>] [--json]\n orizu prompts labels set <prompt-name> <label> --version <version-id> [--project <team/project>] [--json]\n orizu prompts scorers set-headline <prompt-id> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]\n orizu prompts scorers add <prompt-id> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]\n orizu scorers list --project <team/project>\n orizu scorers register --project <team/project> --name <name> --manifest <manifest.json> [--prompt-version <id>] [--runner-version <id>] [--label <label>] [--json]\n orizu scorers detail <scorer-id-or-name> --project <team/project> [--json]\n orizu scorers labels set <scorer-name> <label> --version <scorer-version-id> [--project <team/project>] [--json]\n orizu scorers exec --scorer-version <id> (--subject-version <prompt-version-id> | --optimization-run <id> --candidate <id>) --dataset-version <id> --split-set <id> --split <name> [--subject-results <jsonl>] [--dependency-score-run <alias=id>] [--dependency-results <alias=path>] [--no-submit] [--out <score.json>] [--project <team/project>] [--json]\n orizu scores submit <results.jsonl|results.json> --scorer-version <id> (--subject-version <prompt-version-id> | --optimization-run <id> --candidate <id>) [--aggregate] [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]\n orizu judges list --project <team/project>\n orizu judges pull <judge-id-or-name> --project <team/project> --out <dir> [--label <label> | --version <id>] [--json]\n orizu judges push <dir> [--runner-version <id>] [--project <team/project>] [--parent <version-id>] [--json]\n orizu runners push <dir> [--project <team/project>] [--name <name>] [--label <label>] [--json]\n orizu runners exec (--prompt <prompt-version-id> | --prompt-version <id> --runner-version <id> | --scorer-version <id>) --dataset-version <id> --split-set <id-or-name> --split <name> [--runner-dir <dir>] --out <results.jsonl|results.jsonl.gz>\n orizu optimizers push <dir> [--project <team/project>] [--name <name>] [--label <label>] [--json]\n orizu runs submit <results.jsonl|results.jsonl.gz> --prompt-version <id> --runner-version <id> --dataset-version <id> --split-set <id> --split <name> [--project <team/project>]\n orizu apps list [--project <team/project>]\n orizu apps create --project <team/project> --name <name> --dataset <datasetId> --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps update [--app <appId>] [--project <team/project>] --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps link-dataset --dataset <datasetId> [--app <appId>] [--project <team/project>] [--version <n>]\n orizu apps detail --app <appId> [--project <team/project>] [--json]\n orizu apps export [--app <appId>] [--project <team/project>] [--version <n>] [--out <path>]\n orizu tasks list [--project <team/project>]\n orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> --assignees <userIdOrEmail1,userIdOrEmail2> [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]\n orizu tasks assign --task <taskId> --assignees <userId1,userId2>\n orizu tasks status --task <taskId> [--json]\n orizu tasks pause --task <taskId>\n orizu tasks unpause --task <taskId>\n orizu datasets upload --file <path> [--project <team/project>] [--name <name>] [--readme-file <README.md> | --readme-text <markdown>]\n orizu datasets push <path> [--project <team/project>] [--name <name>] [--readme-file <README.md> | --readme-text <markdown>] [--json]\n orizu datasets readme set <datasetId|dataset-name> [--project <team/project>] (--readme-file <README.md> | --readme-text <markdown>) [--json]\n orizu datasets versions create <datasetId|dataset-name> [--project <team/project>] [--label <label>] [--readme-file <README.md> | --readme-text <markdown>] [--json]\n orizu datasets splits create <datasetVersionId> [--from-file <split.json>] [--json]\n orizu datasets download [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--format <csv|json|jsonl>] [--out <path>]\n orizu datasets append [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>\n orizu datasets edit-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>\n orizu datasets delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --row-ids <id1,id2>\n orizu datasets delete [--dataset <datasetId|datasetUrl>] [--project <team/project>]\n orizu datasets lock [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--reason <text>]\n orizu datasets clone [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--name <name>]\n orizu tasks export [--task <taskId>] [--format <csv|json|jsonl>] [--out <path>]`)
274
- printPreviewUsage()
283
+ printLine(renderRootHelp())
275
284
  }
276
285
 
277
286
  function printPreviewUsage() {
@@ -279,10 +288,11 @@ function printPreviewUsage() {
279
288
  }
280
289
 
281
290
  function printOptimizationUsage() {
282
- printLine(`\nOptimization lifecycle commands:\n\n orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]\n orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--log-dir logs]\n orizu optimizations export <run-id> [--out <path>] [--json]\n orizu optimizations pause <run-id> [--reason <text>] [--json]\n orizu optimizations resume <run-id> [--json]\n orizu optimizations finish <run-id> [--best-score <n>] [--best-candidate <id>] [--result-prompt-version <id>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--json]`)
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]`)
283
292
  }
284
293
 
285
294
  let cliArgs = process.argv.slice(2)
295
+ let cliJsonOutput = false
286
296
 
287
297
  function getArg(name: string): string | null {
288
298
  const index = cliArgs.indexOf(name)
@@ -293,6 +303,31 @@ function getArg(name: string): string | null {
293
303
  return cliArgs[index + 1]
294
304
  }
295
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
+
296
331
  export function normalizeSlugInput(slug: string): string {
297
332
  return slug.trim().toLowerCase()
298
333
  }
@@ -2217,7 +2252,7 @@ async function execScorer() {
2217
2252
  }
2218
2253
 
2219
2254
  type OptimizationLifecycleAction = 'pause' | 'resume' | 'finish' | 'fail' | 'cancel'
2220
- const OPTIMIZATION_REPORT_MAX_BYTES = 2 * 1024 * 1024
2255
+ const MARKDOWN_REPORT_MAX_BYTES = 2 * 1024 * 1024
2221
2256
 
2222
2257
  function parseOptionalNumberFlag(name: string): number | undefined {
2223
2258
  const value = getArg(name)
@@ -2237,10 +2272,15 @@ function hasObjectKeys(value: Record<string, unknown>): boolean {
2237
2272
  return Object.keys(value).length > 0
2238
2273
  }
2239
2274
 
2240
- function readOptimizationReportInput(): { markdown: string; sourceName: string | null } | null {
2275
+ function readMarkdownReportInput(
2276
+ reportLabel: 'Optimization' | 'Task'
2277
+ ): { markdown: string; sourceName: string | null } | null {
2241
2278
  const report = getArg('--report')
2242
2279
  const reportFile = getArg('--report-file')
2243
2280
 
2281
+ rejectDashPrefixedOptionValue('--report', report)
2282
+ rejectDashPrefixedOptionValue('--report-file', reportFile)
2283
+
2244
2284
  if (report && reportFile) {
2245
2285
  throw new Error('Use either --report or --report-file, not both')
2246
2286
  }
@@ -2267,16 +2307,24 @@ function readOptimizationReportInput(): { markdown: string; sourceName: string |
2267
2307
  }
2268
2308
 
2269
2309
  if (!markdown.trim()) {
2270
- throw new Error('Optimization report markdown must not be blank')
2310
+ throw new Error(`${reportLabel} report markdown must not be blank`)
2271
2311
  }
2272
2312
 
2273
- if (Buffer.byteLength(markdown, 'utf8') > OPTIMIZATION_REPORT_MAX_BYTES) {
2274
- 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`)
2275
2315
  }
2276
2316
 
2277
2317
  return { markdown, sourceName }
2278
2318
  }
2279
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
+
2280
2328
  async function startOptimizationRun() {
2281
2329
  const project = getArg('--project') || await resolveProjectSlug(null)
2282
2330
  const optimizerVersionId = getArg('--optimizer-version')
@@ -2674,13 +2722,120 @@ function readSourceBytes(pathArg: string): Buffer {
2674
2722
  }
2675
2723
 
2676
2724
  function hasJsonFlag(): boolean {
2677
- return hasArg('--json')
2725
+ return cliJsonOutput || hasArg('--json')
2678
2726
  }
2679
2727
 
2680
2728
  function printJson(value: Record<string, unknown>) {
2681
2729
  printLine(JSON.stringify(value))
2682
2730
  }
2683
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
+
2684
2839
  function readJsonFile(pathArg: string): Record<string, unknown> {
2685
2840
  const raw = readSourceFile(pathArg)
2686
2841
  try {
@@ -3104,6 +3259,7 @@ async function createTask() {
3104
3259
  const appId = getArg('--app')
3105
3260
  const title = getArg('--title')
3106
3261
  const assignees = parseCommaSeparated(getArg('--assignees'))
3262
+ const publish = hasArg('--publish')
3107
3263
  const versionArg = getArg('--version')
3108
3264
  const instructions = getArg('--instructions')
3109
3265
  const labelsPerItemArg = getArg('--labels-per-item')
@@ -3112,8 +3268,8 @@ async function createTask() {
3112
3268
  const versionNum =
3113
3269
  Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : null
3114
3270
 
3115
- if (!projectSlug || !datasetId || !appId || !title || assignees.length === 0) {
3116
- 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]')
3117
3273
  }
3118
3274
 
3119
3275
  if (versionArg && versionNum === null) {
@@ -3130,6 +3286,7 @@ async function createTask() {
3130
3286
  versionNum,
3131
3287
  title,
3132
3288
  memberIds: assignees,
3289
+ publish,
3133
3290
  instructions,
3134
3291
  requiredAssignmentsPerRow: labelsPerItem,
3135
3292
  }),
@@ -3172,6 +3329,13 @@ async function createTask() {
3172
3329
  status: data.task.status,
3173
3330
  requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
3174
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
+ : {}),
3175
3339
  ...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
3176
3340
  ...(data.warning ? { warning: data.warning } : {}),
3177
3341
  }, null, 2))
@@ -3180,15 +3344,76 @@ async function createTask() {
3180
3344
 
3181
3345
  const baseUrl = getBaseUrl()
3182
3346
  const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`
3347
+ const isDraft = data.task.status === 'draft'
3183
3348
  printLine(
3184
- `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)}]` +
3185
3350
  `\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
3186
3351
  `\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
3187
3352
  `\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
3188
3353
  `\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
3189
3354
  `\n Assignments: ${data.assignmentsCreated}` +
3190
3355
  (data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
3191
- `\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)}` : '')
3192
3417
  )
3193
3418
  }
3194
3419
 
@@ -3281,6 +3506,58 @@ async function updateTaskStatus(targetStatus: 'paused' | 'active') {
3281
3506
  printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
3282
3507
  }
3283
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
+
3284
3561
  interface AppDetailPayload {
3285
3562
  id: string
3286
3563
  name: string
@@ -4435,11 +4712,45 @@ async function runnersExec() {
4435
4712
  }
4436
4713
  }
4437
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
+
4438
4737
  export async function main(rawArgs = process.argv.slice(2)) {
4439
4738
  const parsed = parseGlobalFlags(rawArgs)
4440
4739
  setGlobalFlags(parsed.flags)
4441
4740
  cliArgs = parsed.args
4442
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
+
4443
4754
  const command = cliArgs[0]
4444
4755
  const subcommand = cliArgs[1]
4445
4756
 
@@ -4450,10 +4761,24 @@ export async function main(rawArgs = process.argv.slice(2)) {
4450
4761
 
4451
4762
  if (!command) {
4452
4763
  printUsage()
4453
- printOptimizationUsage()
4454
4764
  process.exit(1)
4455
4765
  }
4456
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
+
4457
4782
  if (command === 'login') {
4458
4783
  await login()
4459
4784
  return
@@ -4691,6 +5016,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
4691
5016
  return
4692
5017
  }
4693
5018
 
5019
+ if (command === 'tasks' && subcommand === 'publish') {
5020
+ await publishTask()
5021
+ return
5022
+ }
5023
+
4694
5024
  if (command === 'tasks' && subcommand === 'assign') {
4695
5025
  await assignTask()
4696
5026
  return
@@ -4701,6 +5031,15 @@ export async function main(rawArgs = process.argv.slice(2)) {
4701
5031
  return
4702
5032
  }
4703
5033
 
5034
+ if (
5035
+ command === 'tasks' &&
5036
+ subcommand === 'report' &&
5037
+ (cliArgs[2] === 'set' || cliArgs[2] === 'upload')
5038
+ ) {
5039
+ await setTaskReport()
5040
+ return
5041
+ }
5042
+
4704
5043
  if (command === 'tasks' && subcommand === 'pause') {
4705
5044
  await updateTaskStatus('paused')
4706
5045
  return
@@ -4778,7 +5117,6 @@ export async function main(rawArgs = process.argv.slice(2)) {
4778
5117
  }
4779
5118
 
4780
5119
  printUsage()
4781
- printOptimizationUsage()
4782
5120
  process.exit(1)
4783
5121
  }
4784
5122