orizu 0.3.5 → 0.3.7

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/dist/index.js CHANGED
@@ -13,9 +13,12 @@ import { clearServerCredentials, getServerCredentials, saveServerCredentials } f
13
13
  import { parseDatasetFile } from './file-parser.js';
14
14
  import { streamJsonlRowChunks } from './jsonl-stream.js';
15
15
  import { parseDatasetReference } from './dataset-download.js';
16
+ import { extractErrorMessage } from './error-response.js';
16
17
  import { parseGlobalFlags } from './global-flags.js';
18
+ import { getCapabilities, renderHelpForArgs, renderRootHelp } from './help.js';
17
19
  import { runLocalAppPreview } from './preview-runtime.js';
18
20
  import { assertSecureTokenTransport, authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags, } from './http.js';
21
+ import { getSkillInstallPath, installSkillTarget, isSkillInstallTarget, SKILL_INSTALL_TARGETS, targetNeedsOverwrite, } from './skill-installer.js';
19
22
  import { formatTaskCreateError } from './task-create-error.js';
20
23
  const MAX_README_LENGTH = 200_000;
21
24
  function getCliVersion() {
@@ -32,16 +35,16 @@ function printVersion() {
32
35
  printLine(`orizu ${getCliVersion()}`);
33
36
  }
34
37
  function printUsage() {
35
- 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>]`);
36
- printPreviewUsage();
38
+ printLine(renderRootHelp());
37
39
  }
38
40
  function printPreviewUsage() {
39
41
  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>]`);
40
42
  }
41
43
  function printOptimizationUsage() {
42
- 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]`);
44
+ 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] [--num-threads auto|N] [--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]`);
43
45
  }
44
46
  let cliArgs = process.argv.slice(2);
47
+ let cliJsonOutput = false;
45
48
  function getArg(name) {
46
49
  const index = cliArgs.indexOf(name);
47
50
  if (index === -1 || index + 1 >= cliArgs.length) {
@@ -49,6 +52,26 @@ function getArg(name) {
49
52
  }
50
53
  return cliArgs[index + 1];
51
54
  }
55
+ function getArgs(name) {
56
+ const values = [];
57
+ for (let index = 0; index < cliArgs.length; index += 1) {
58
+ if (cliArgs[index] !== name) {
59
+ continue;
60
+ }
61
+ const value = cliArgs[index + 1];
62
+ if (!value || value.startsWith('-')) {
63
+ throw new Error(`Usage: ${name} <value>`);
64
+ }
65
+ values.push(value);
66
+ index += 1;
67
+ }
68
+ return values;
69
+ }
70
+ function rejectDashPrefixedOptionValue(name, value) {
71
+ if (value && value.startsWith('-')) {
72
+ throw new Error(`Invalid value for ${name}: option values cannot start with a dash`);
73
+ }
74
+ }
52
75
  export function normalizeSlugInput(slug) {
53
76
  return slug.trim().toLowerCase();
54
77
  }
@@ -1569,7 +1592,7 @@ async function execScorer() {
1569
1592
  printLine(`Wrote scorer result to ${sanitizeTerminalText(outPath)}`);
1570
1593
  }
1571
1594
  }
1572
- const OPTIMIZATION_REPORT_MAX_BYTES = 2 * 1024 * 1024;
1595
+ const MARKDOWN_REPORT_MAX_BYTES = 2 * 1024 * 1024;
1573
1596
  function parseOptionalNumberFlag(name) {
1574
1597
  const value = getArg(name);
1575
1598
  if (!value) {
@@ -1584,9 +1607,11 @@ function parseOptionalNumberFlag(name) {
1584
1607
  function hasObjectKeys(value) {
1585
1608
  return Object.keys(value).length > 0;
1586
1609
  }
1587
- function readOptimizationReportInput() {
1610
+ function readMarkdownReportInput(reportLabel) {
1588
1611
  const report = getArg('--report');
1589
1612
  const reportFile = getArg('--report-file');
1613
+ rejectDashPrefixedOptionValue('--report', report);
1614
+ rejectDashPrefixedOptionValue('--report-file', reportFile);
1590
1615
  if (report && reportFile) {
1591
1616
  throw new Error('Use either --report or --report-file, not both');
1592
1617
  }
@@ -1611,13 +1636,19 @@ function readOptimizationReportInput() {
1611
1636
  sourceName = 'inline';
1612
1637
  }
1613
1638
  if (!markdown.trim()) {
1614
- throw new Error('Optimization report markdown must not be blank');
1639
+ throw new Error(`${reportLabel} report markdown must not be blank`);
1615
1640
  }
1616
- if (Buffer.byteLength(markdown, 'utf8') > OPTIMIZATION_REPORT_MAX_BYTES) {
1617
- throw new Error(`Optimization report exceeds ${OPTIMIZATION_REPORT_MAX_BYTES} bytes`);
1641
+ if (Buffer.byteLength(markdown, 'utf8') > MARKDOWN_REPORT_MAX_BYTES) {
1642
+ throw new Error(`${reportLabel} report exceeds ${MARKDOWN_REPORT_MAX_BYTES} bytes`);
1618
1643
  }
1619
1644
  return { markdown, sourceName };
1620
1645
  }
1646
+ function readOptimizationReportInput() {
1647
+ return readMarkdownReportInput('Optimization');
1648
+ }
1649
+ function readTaskReportInput() {
1650
+ return readMarkdownReportInput('Task');
1651
+ }
1621
1652
  async function startOptimizationRun() {
1622
1653
  const project = getArg('--project') || await resolveProjectSlug(null);
1623
1654
  const optimizerVersionId = getArg('--optimizer-version');
@@ -1922,11 +1953,106 @@ function readSourceBytes(pathArg) {
1922
1953
  }
1923
1954
  }
1924
1955
  function hasJsonFlag() {
1925
- return hasArg('--json');
1956
+ return cliJsonOutput || hasArg('--json');
1926
1957
  }
1927
1958
  function printJson(value) {
1928
1959
  printLine(JSON.stringify(value));
1929
1960
  }
1961
+ function printCapabilities() {
1962
+ const capabilities = getCapabilities(getCliVersion());
1963
+ if (hasJsonFlag()) {
1964
+ printLine(JSON.stringify(capabilities));
1965
+ return;
1966
+ }
1967
+ printLine('orizu capabilities');
1968
+ printLine('');
1969
+ printLine('Run `orizu capabilities --json` for a machine-readable command manifest.');
1970
+ printLine('');
1971
+ for (const command of capabilities.commands) {
1972
+ printLine(` ${command.name.padEnd(34)} ${command.help}`);
1973
+ }
1974
+ }
1975
+ async function askYesNo(question, defaultYes) {
1976
+ const rl = createInterface({ input, output });
1977
+ try {
1978
+ const suffix = defaultYes ? 'Y/n' : 'y/N';
1979
+ const answer = (await rl.question(`${question} (${suffix}) `)).trim().toLowerCase();
1980
+ if (!answer) {
1981
+ return defaultYes;
1982
+ }
1983
+ return answer === 'y' || answer === 'yes';
1984
+ }
1985
+ finally {
1986
+ rl.close();
1987
+ }
1988
+ }
1989
+ async function promptSkillInstallTargets() {
1990
+ if (!isInteractiveTerminal()) {
1991
+ throw new Error(`Usage: orizu install-skill --target <${SKILL_INSTALL_TARGETS.join('|')}> [--yes] [--dry-run]`);
1992
+ }
1993
+ printLine('Where should the Orizu CLI skill be installed?');
1994
+ const targets = [];
1995
+ for (const target of SKILL_INSTALL_TARGETS) {
1996
+ const defaultYes = target === 'agent-user';
1997
+ if (await askYesNo(` Install to ${target}?`, defaultYes)) {
1998
+ targets.push(target);
1999
+ }
2000
+ }
2001
+ return targets;
2002
+ }
2003
+ function parseSkillInstallTargets() {
2004
+ const rawTargets = getArgs('--target');
2005
+ const targets = [];
2006
+ for (const target of rawTargets) {
2007
+ if (!isSkillInstallTarget(target)) {
2008
+ throw new Error(`Unknown skill install target '${target}'. Available targets: ${SKILL_INSTALL_TARGETS.join(', ')}`);
2009
+ }
2010
+ if (!targets.includes(target)) {
2011
+ targets.push(target);
2012
+ }
2013
+ }
2014
+ return targets;
2015
+ }
2016
+ function formatSkillInstallAction(action) {
2017
+ if (action === 'created')
2018
+ return 'Wrote';
2019
+ if (action === 'updated')
2020
+ return 'Updated';
2021
+ if (action === 'would-create')
2022
+ return 'Would write';
2023
+ return 'Would update';
2024
+ }
2025
+ async function installSkillCommand() {
2026
+ const skipConfirm = hasArg('--yes');
2027
+ const dryRun = hasArg('--dry-run');
2028
+ let targets = parseSkillInstallTargets();
2029
+ if (targets.length === 0) {
2030
+ targets = await promptSkillInstallTargets();
2031
+ }
2032
+ if (targets.length === 0) {
2033
+ printLine('No targets selected; nothing to do.');
2034
+ return;
2035
+ }
2036
+ for (const target of targets) {
2037
+ let overwrite = skipConfirm;
2038
+ const installPath = getSkillInstallPath(target);
2039
+ if (!dryRun && !overwrite && targetNeedsOverwrite(target)) {
2040
+ if (!isInteractiveTerminal()) {
2041
+ throw new Error(`${installPath} already exists. Pass --yes to replace it.`);
2042
+ }
2043
+ overwrite = await askYesNo(`Replace ${installPath}?`, true);
2044
+ if (!overwrite) {
2045
+ printLine(`Skipped ${installPath}`);
2046
+ continue;
2047
+ }
2048
+ }
2049
+ const result = installSkillTarget(target, {
2050
+ overwrite,
2051
+ dryRun,
2052
+ });
2053
+ printLine(`${formatSkillInstallAction(result.action)} ${result.path}`);
2054
+ }
2055
+ }
1930
2056
  function readJsonFile(pathArg) {
1931
2057
  const raw = readSourceFile(pathArg);
1932
2058
  try {
@@ -2283,14 +2409,15 @@ async function createTask() {
2283
2409
  const appId = getArg('--app');
2284
2410
  const title = getArg('--title');
2285
2411
  const assignees = parseCommaSeparated(getArg('--assignees'));
2412
+ const publish = hasArg('--publish');
2286
2413
  const versionArg = getArg('--version');
2287
2414
  const instructions = getArg('--instructions');
2288
2415
  const labelsPerItemArg = getArg('--labels-per-item');
2289
2416
  const labelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1;
2290
2417
  const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN;
2291
2418
  const versionNum = Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : null;
2292
- if (!projectSlug || !datasetId || !appId || !title || assignees.length === 0) {
2293
- 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]');
2419
+ if (!projectSlug || !datasetId || !appId || !title || (publish && assignees.length === 0)) {
2420
+ 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]');
2294
2421
  }
2295
2422
  if (versionArg && versionNum === null) {
2296
2423
  throw new Error('--version must be a positive integer');
@@ -2305,6 +2432,7 @@ async function createTask() {
2305
2432
  versionNum,
2306
2433
  title,
2307
2434
  memberIds: assignees,
2435
+ publish,
2308
2436
  instructions,
2309
2437
  requiredAssignmentsPerRow: labelsPerItem,
2310
2438
  }),
@@ -2332,6 +2460,13 @@ async function createTask() {
2332
2460
  status: data.task.status,
2333
2461
  requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
2334
2462
  assignmentsCreated: data.assignmentsCreated,
2463
+ draft: data.task.status === 'draft',
2464
+ ...(data.task.status === 'draft'
2465
+ ? {
2466
+ message: 'Draft created. Test it manually before assigning.',
2467
+ publishCommand: `orizu tasks publish --task ${data.task.id} --assignees <userId1,userId2>`,
2468
+ }
2469
+ : {}),
2335
2470
  ...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
2336
2471
  ...(data.warning ? { warning: data.warning } : {}),
2337
2472
  }, null, 2));
@@ -2339,14 +2474,56 @@ async function createTask() {
2339
2474
  }
2340
2475
  const baseUrl = getBaseUrl();
2341
2476
  const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`;
2342
- printLine(`Created task ${sanitizeTerminalText(data.task.title)} (${sanitizeTerminalText(data.task.id)}) [${sanitizeTerminalText(data.task.status)}]` +
2477
+ const isDraft = data.task.status === 'draft';
2478
+ printLine(`Created ${isDraft ? 'draft ' : ''}task ${sanitizeTerminalText(data.task.title)} (${sanitizeTerminalText(data.task.id)}) [${sanitizeTerminalText(data.task.status)}]` +
2343
2479
  `\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
2344
2480
  `\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
2345
2481
  `\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
2346
2482
  `\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
2347
2483
  `\n Assignments: ${data.assignmentsCreated}` +
2348
2484
  (data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
2349
- `\n URL: ${sanitizeTerminalText(taskUrl)}`);
2485
+ `\n URL: ${sanitizeTerminalText(taskUrl)}` +
2486
+ (isDraft
2487
+ ? `\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>`
2488
+ : ''));
2489
+ }
2490
+ async function publishTask() {
2491
+ const taskId = getArg('--task');
2492
+ const assignees = parseCommaSeparated(getArg('--assignees'));
2493
+ if (!taskId || assignees.length === 0) {
2494
+ throw new Error('Usage: orizu tasks publish --task <taskId> --assignees <userId1,userId2> [--json]');
2495
+ }
2496
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/publish`, {
2497
+ method: 'POST',
2498
+ headers: { 'Content-Type': 'application/json' },
2499
+ body: JSON.stringify({ memberIds: assignees }),
2500
+ });
2501
+ if (!response.ok) {
2502
+ throw new Error(`Failed to publish task: ${await extractErrorMessage(response)}`);
2503
+ }
2504
+ const data = await parseJsonResponse(response, 'Task publish');
2505
+ if (hasArg('--json')) {
2506
+ printLine(JSON.stringify({
2507
+ taskId: data.task.id,
2508
+ status: data.task.status,
2509
+ assignmentsCreated: data.assignmentsCreated,
2510
+ assignmentsProcessed: data.assignmentsProcessed,
2511
+ ...(data.assignmentsRemoved !== undefined
2512
+ ? { assignmentsRemoved: data.assignmentsRemoved }
2513
+ : {}),
2514
+ ...(data.assignmentShortfall !== undefined
2515
+ ? { assignmentShortfall: data.assignmentShortfall }
2516
+ : {}),
2517
+ ...(data.warning ? { warning: data.warning } : {}),
2518
+ }, null, 2));
2519
+ return;
2520
+ }
2521
+ printLine(`Published task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]` +
2522
+ `\n Assignments: ${data.assignmentsCreated}` +
2523
+ (data.assignmentsRemoved !== undefined
2524
+ ? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
2525
+ : '') +
2526
+ (data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : ''));
2350
2527
  }
2351
2528
  async function assignTask() {
2352
2529
  const taskId = getArg('--task');
@@ -2425,6 +2602,35 @@ async function updateTaskStatus(targetStatus) {
2425
2602
  const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused';
2426
2603
  printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`);
2427
2604
  }
2605
+ async function setTaskReport() {
2606
+ const taskId = getArg('--task');
2607
+ rejectDashPrefixedOptionValue('--task', taskId);
2608
+ if (!taskId) {
2609
+ throw new Error('Usage: orizu tasks report set --task <taskId> (--report <markdown|@file> | --report-file <path>) [--json]');
2610
+ }
2611
+ const report = readTaskReportInput();
2612
+ if (!report) {
2613
+ throw new Error('Usage: orizu tasks report set --task <taskId> (--report <markdown|@file> | --report-file <path>) [--json]');
2614
+ }
2615
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/report`, {
2616
+ method: 'PATCH',
2617
+ headers: { 'Content-Type': 'application/json' },
2618
+ body: JSON.stringify({
2619
+ reportMarkdown: report.markdown,
2620
+ reportSourceName: report.sourceName,
2621
+ }),
2622
+ });
2623
+ if (!response.ok) {
2624
+ throw new Error(`Failed to update task report: ${await response.text()}`);
2625
+ }
2626
+ const data = await parseJsonResponse(response, 'Task report update');
2627
+ if (hasJsonFlag()) {
2628
+ printJson(data);
2629
+ return;
2630
+ }
2631
+ printLine(`Uploaded task report for ${sanitizeTerminalText(data.task.id)} ` +
2632
+ `[${sanitizeTerminalText(data.task.status)}]`);
2633
+ }
2428
2634
  function defaultAppExportFilename(appName, versionNum) {
2429
2635
  const safeName = appName
2430
2636
  .trim()
@@ -3293,10 +3499,37 @@ async function runnersExec() {
3293
3499
  materializedRunner.cleanup();
3294
3500
  }
3295
3501
  }
3502
+ function isHelpToken(value) {
3503
+ return value === '--help' || value === '-h';
3504
+ }
3505
+ function extractHelpTarget(args) {
3506
+ if (args[0] === 'help') {
3507
+ return helpTargetFromArgs(args.slice(1));
3508
+ }
3509
+ if (!args.some(isHelpToken)) {
3510
+ return null;
3511
+ }
3512
+ return helpTargetFromArgs(args);
3513
+ }
3514
+ function helpTargetFromArgs(args) {
3515
+ const targetArgs = args.filter(arg => !isHelpToken(arg) && arg !== '--json');
3516
+ const optionIndex = targetArgs.findIndex(arg => arg.startsWith('-'));
3517
+ return optionIndex === -1 ? targetArgs : targetArgs.slice(0, optionIndex);
3518
+ }
3296
3519
  export async function main(rawArgs = process.argv.slice(2)) {
3297
3520
  const parsed = parseGlobalFlags(rawArgs);
3298
3521
  setGlobalFlags(parsed.flags);
3299
3522
  cliArgs = parsed.args;
3523
+ cliJsonOutput = false;
3524
+ if (cliArgs[0] === '--json') {
3525
+ cliJsonOutput = true;
3526
+ cliArgs = cliArgs.slice(1);
3527
+ }
3528
+ const helpTarget = extractHelpTarget(cliArgs);
3529
+ if (helpTarget !== null) {
3530
+ printLine(renderHelpForArgs(helpTarget));
3531
+ return;
3532
+ }
3300
3533
  const command = cliArgs[0];
3301
3534
  const subcommand = cliArgs[1];
3302
3535
  if (command === '--version' || command === '-v') {
@@ -3305,9 +3538,20 @@ export async function main(rawArgs = process.argv.slice(2)) {
3305
3538
  }
3306
3539
  if (!command) {
3307
3540
  printUsage();
3308
- printOptimizationUsage();
3309
3541
  process.exit(1);
3310
3542
  }
3543
+ if (command === 'capabilities') {
3544
+ printCapabilities();
3545
+ return;
3546
+ }
3547
+ if (command === 'install-skill') {
3548
+ await installSkillCommand();
3549
+ return;
3550
+ }
3551
+ if (command === 'skills' && subcommand === 'install') {
3552
+ await installSkillCommand();
3553
+ return;
3554
+ }
3311
3555
  if (command === 'login') {
3312
3556
  await login();
3313
3557
  return;
@@ -3498,6 +3742,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
3498
3742
  await createTask();
3499
3743
  return;
3500
3744
  }
3745
+ if (command === 'tasks' && subcommand === 'publish') {
3746
+ await publishTask();
3747
+ return;
3748
+ }
3501
3749
  if (command === 'tasks' && subcommand === 'assign') {
3502
3750
  await assignTask();
3503
3751
  return;
@@ -3506,6 +3754,12 @@ export async function main(rawArgs = process.argv.slice(2)) {
3506
3754
  await taskStatus();
3507
3755
  return;
3508
3756
  }
3757
+ if (command === 'tasks' &&
3758
+ subcommand === 'report' &&
3759
+ (cliArgs[2] === 'set' || cliArgs[2] === 'upload')) {
3760
+ await setTaskReport();
3761
+ return;
3762
+ }
3509
3763
  if (command === 'tasks' && subcommand === 'pause') {
3510
3764
  await updateTaskStatus('paused');
3511
3765
  return;
@@ -3568,7 +3822,6 @@ export async function main(rawArgs = process.argv.slice(2)) {
3568
3822
  return;
3569
3823
  }
3570
3824
  printUsage();
3571
- printOptimizationUsage();
3572
3825
  process.exit(1);
3573
3826
  }
3574
3827
  function isCliEntrypoint() {
@@ -0,0 +1,180 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from 'fs';
2
+ import { dirname, isAbsolute, relative, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { homedir } from 'os';
5
+ export const SKILL_NAME = 'orizu-cli';
6
+ export const AGENTS_START_MARKER = '<!-- orizu-cli:start -->';
7
+ export const AGENTS_END_MARKER = '<!-- orizu-cli:end -->';
8
+ export const SKILL_INSTALL_TARGETS = [
9
+ 'agent-user',
10
+ 'codex-project',
11
+ 'claude-user',
12
+ 'claude-project',
13
+ 'agents-md',
14
+ ];
15
+ function getCliRoot() {
16
+ return resolve(dirname(fileURLToPath(import.meta.url)), '..');
17
+ }
18
+ function isSkillDir(path) {
19
+ return existsSync(resolve(path, 'SKILL.md'));
20
+ }
21
+ export function isSafeRelativeSkillPath(rel) {
22
+ return rel !== '..' && !rel.startsWith('../') && !isAbsolute(rel);
23
+ }
24
+ export function formatMissingSkillSourceError(paths) {
25
+ return `Unable to find bundled ${SKILL_NAME} skill. Reinstall the orizu CLI or run from the Orizu repository.\n`
26
+ + `Searched:\n- ${paths.join('\n- ')}`;
27
+ }
28
+ export function resolveSkillSourceDir() {
29
+ const override = process.env.ORIZU_SKILL_SOURCE_DIR;
30
+ if (override) {
31
+ const resolvedOverride = resolve(override);
32
+ if (!isSkillDir(resolvedOverride)) {
33
+ throw new Error(`ORIZU_SKILL_SOURCE_DIR is not an Orizu skill directory: ${resolvedOverride}`);
34
+ }
35
+ return resolvedOverride;
36
+ }
37
+ const cliRoot = getCliRoot();
38
+ const packagedSkill = resolve(cliRoot, 'vendor', 'skills', SKILL_NAME);
39
+ if (isSkillDir(packagedSkill)) {
40
+ return packagedSkill;
41
+ }
42
+ const repoSkill = resolve(cliRoot, '..', '..', 'skills', SKILL_NAME);
43
+ if (isSkillDir(repoSkill)) {
44
+ return repoSkill;
45
+ }
46
+ const searchedPaths = [
47
+ `packaged skill: ${packagedSkill}`,
48
+ `repository fallback: ${repoSkill}`,
49
+ ];
50
+ throw new Error(formatMissingSkillSourceError(searchedPaths));
51
+ }
52
+ function resolveHome(options) {
53
+ return resolve(options?.homeDir || process.env.HOME || homedir());
54
+ }
55
+ function resolveCwd(options) {
56
+ return resolve(options?.cwd || process.cwd());
57
+ }
58
+ export function getSkillInstallPath(target, options) {
59
+ const home = resolveHome(options);
60
+ const cwd = resolveCwd(options);
61
+ if (target === 'agent-user') {
62
+ return resolve(home, '.agents', 'skills', SKILL_NAME);
63
+ }
64
+ if (target === 'codex-project') {
65
+ return resolve(cwd, '.codex', 'skills', SKILL_NAME);
66
+ }
67
+ if (target === 'claude-user') {
68
+ return resolve(home, '.claude', 'skills', SKILL_NAME);
69
+ }
70
+ if (target === 'claude-project') {
71
+ return resolve(cwd, '.claude', 'skills', SKILL_NAME);
72
+ }
73
+ return resolve(cwd, 'AGENTS.md');
74
+ }
75
+ function shouldCopySkillPath(sourceRoot, sourcePath) {
76
+ const rel = relative(sourceRoot, sourcePath).replace(/\\/g, '/');
77
+ if (!rel) {
78
+ return true;
79
+ }
80
+ if (!isSafeRelativeSkillPath(rel)) {
81
+ return false;
82
+ }
83
+ const parts = rel.split('/');
84
+ return !parts.some(part => part === '.DS_Store' ||
85
+ part === '__pycache__' ||
86
+ part === '.pytest_cache') && !rel.endsWith('.pyc') && !rel.endsWith('.pyo');
87
+ }
88
+ function copySkillTree(sourceDir, targetDir) {
89
+ rmSync(targetDir, { recursive: true, force: true });
90
+ mkdirSync(dirname(targetDir), { recursive: true });
91
+ cpSync(sourceDir, targetDir, {
92
+ recursive: true,
93
+ filter: sourcePath => shouldCopySkillPath(sourceDir, sourcePath),
94
+ });
95
+ }
96
+ function stripFrontmatter(markdown) {
97
+ const match = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/);
98
+ if (!match) {
99
+ return markdown;
100
+ }
101
+ return markdown.slice(match[0].length).replace(/^[\r\n]+/, '');
102
+ }
103
+ function buildAgentsSection(sourceDir) {
104
+ const skillMarkdown = readFileSync(resolve(sourceDir, 'SKILL.md'), 'utf8');
105
+ const body = stripFrontmatter(skillMarkdown).trimEnd();
106
+ return `${AGENTS_START_MARKER}\n${body}\n${AGENTS_END_MARKER}\n`;
107
+ }
108
+ function installAgentsMd(sourceDir, targetPath, options) {
109
+ const section = buildAgentsSection(sourceDir);
110
+ const exists = existsSync(targetPath);
111
+ const existing = exists ? readFileSync(targetPath, 'utf8') : '';
112
+ const hasManagedSection = existing.includes(AGENTS_START_MARKER)
113
+ && existing.includes(AGENTS_END_MARKER);
114
+ const action = exists ? 'updated' : 'created';
115
+ if (options?.dryRun) {
116
+ return {
117
+ target: 'agents-md',
118
+ path: targetPath,
119
+ action: action === 'updated' ? 'would-update' : 'would-create',
120
+ };
121
+ }
122
+ if (hasManagedSection && !options?.overwrite) {
123
+ throw new Error(`${targetPath} already contains an Orizu CLI section. Pass --yes to replace it.`);
124
+ }
125
+ mkdirSync(dirname(targetPath), { recursive: true });
126
+ if (!exists) {
127
+ writeFileSync(targetPath, section, 'utf8');
128
+ return { target: 'agents-md', path: targetPath, action: 'created' };
129
+ }
130
+ if (hasManagedSection) {
131
+ const pattern = new RegExp(`${escapeRegExp(AGENTS_START_MARKER)}[\\s\\S]*?${escapeRegExp(AGENTS_END_MARKER)}\\n?`);
132
+ writeFileSync(targetPath, existing.replace(pattern, section), 'utf8');
133
+ return { target: 'agents-md', path: targetPath, action: 'updated' };
134
+ }
135
+ const existingPrefix = existing.trim().length === 0 ? '' : existing;
136
+ const separator = existingPrefix.endsWith('\n') ? '\n' : '\n\n';
137
+ writeFileSync(targetPath, `${existingPrefix}${existingPrefix ? separator : ''}${section}`, 'utf8');
138
+ return { target: 'agents-md', path: targetPath, action: 'updated' };
139
+ }
140
+ function escapeRegExp(value) {
141
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
142
+ }
143
+ export function targetNeedsOverwrite(target, options) {
144
+ const targetPath = getSkillInstallPath(target, options);
145
+ if (!existsSync(targetPath)) {
146
+ return false;
147
+ }
148
+ if (target !== 'agents-md') {
149
+ return true;
150
+ }
151
+ const existing = readFileSync(targetPath, 'utf8');
152
+ return existing.includes(AGENTS_START_MARKER) && existing.includes(AGENTS_END_MARKER);
153
+ }
154
+ export function installSkillTarget(target, options) {
155
+ const sourceDir = resolveSkillSourceDir();
156
+ const targetPath = getSkillInstallPath(target, options);
157
+ const exists = existsSync(targetPath);
158
+ if (target === 'agents-md') {
159
+ return installAgentsMd(sourceDir, targetPath, options);
160
+ }
161
+ if (options?.dryRun) {
162
+ return {
163
+ target,
164
+ path: targetPath,
165
+ action: exists ? 'would-update' : 'would-create',
166
+ };
167
+ }
168
+ if (exists && !options?.overwrite) {
169
+ throw new Error(`${targetPath} already exists. Pass --yes to replace it.`);
170
+ }
171
+ copySkillTree(sourceDir, targetPath);
172
+ return {
173
+ target,
174
+ path: targetPath,
175
+ action: exists ? 'updated' : 'created',
176
+ };
177
+ }
178
+ export function isSkillInstallTarget(value) {
179
+ return SKILL_INSTALL_TARGETS.includes(value);
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,8 +16,8 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "build": "tsc -p tsconfig.json",
19
- "prepack": "node scripts/vendor-gepa-python.mjs",
20
- "postpack": "node scripts/vendor-gepa-python.mjs --clean"
19
+ "prepack": "node scripts/vendor-gepa-python.mjs && node scripts/vendor-orizu-skill.mjs",
20
+ "postpack": "node scripts/vendor-gepa-python.mjs --clean && node scripts/vendor-orizu-skill.mjs --clean"
21
21
  },
22
22
  "dependencies": {
23
23
  "@playwright/test": "^1.60.0",