orizu 0.3.7 → 0.4.1

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/csv.ts CHANGED
@@ -1,4 +1,124 @@
1
- export function parseCSV(csvText: string): string[][] {
1
+ /**
2
+ * CSV parsing utilities with RFC 4180 compliance.
3
+ *
4
+ * This is the single canonical implementation shared by the web app and the
5
+ * published CLI: lib/csv-utils.ts re-exports from here. It lives inside
6
+ * packages/cli because the CLI is published standalone to npm and cannot
7
+ * import files outside its package root, while the app can import anything.
8
+ *
9
+ * Resource limits are enforced while parsing so a hostile CSV cannot exhaust
10
+ * memory. The numeric defaults are inlined here (this package cannot import
11
+ * @/lib/resource-limits) and mirror RESOURCE_LIMITS.CSV_* in that module —
12
+ * keep the two in sync.
13
+ */
14
+
15
+ export class CSVResourceLimitError extends Error {
16
+ constructor(message: string) {
17
+ super(message)
18
+ this.name = 'CSVResourceLimitError'
19
+ }
20
+ }
21
+
22
+ export interface CSVParseOptions {
23
+ maxBytes?: number
24
+ maxRows?: number
25
+ maxFields?: number
26
+ maxFieldBytes?: number
27
+ }
28
+
29
+ // Mirrors RESOURCE_LIMITS.CSV_* in lib/resource-limits.ts.
30
+ const DEFAULT_CSV_MAX_BYTES = 10 * 1024 * 1024
31
+ const DEFAULT_CSV_MAX_ROWS = 5_000
32
+ const DEFAULT_CSV_MAX_FIELDS = 200
33
+ const DEFAULT_CSV_MAX_FIELD_BYTES = 128 * 1024
34
+
35
+ function getUtf8ByteLength(value: string): number {
36
+ let bytes = 0
37
+
38
+ for (let index = 0; index < value.length; index += 1) {
39
+ const codePoint = value.charCodeAt(index)
40
+
41
+ if (codePoint <= 0x7f) {
42
+ bytes += 1
43
+ } else if (codePoint <= 0x7ff) {
44
+ bytes += 2
45
+ } else if (
46
+ codePoint >= 0xd800 &&
47
+ codePoint <= 0xdbff &&
48
+ index + 1 < value.length
49
+ ) {
50
+ const next = value.charCodeAt(index + 1)
51
+ if (next >= 0xdc00 && next <= 0xdfff) {
52
+ bytes += 4
53
+ index += 1
54
+ } else {
55
+ bytes += 3
56
+ }
57
+ } else {
58
+ bytes += 3
59
+ }
60
+ }
61
+
62
+ return bytes
63
+ }
64
+
65
+ function enforceCsvTextLimit(csvText: string, options: CSVParseOptions): void {
66
+ const maxBytes = options.maxBytes ?? DEFAULT_CSV_MAX_BYTES
67
+ if (getUtf8ByteLength(csvText) > maxBytes) {
68
+ throw new CSVResourceLimitError(`CSV data is too large (max ${maxBytes} bytes)`)
69
+ }
70
+ }
71
+
72
+ function enforceRowLimit(rowCount: number, options: CSVParseOptions): void {
73
+ const maxRows = options.maxRows ?? DEFAULT_CSV_MAX_ROWS
74
+ if (rowCount > maxRows) {
75
+ throw new CSVResourceLimitError(`CSV has too many rows (max ${maxRows})`)
76
+ }
77
+ }
78
+
79
+ function enforceFieldLimits(row: string[], options: CSVParseOptions): void {
80
+ const maxFields = options.maxFields ?? DEFAULT_CSV_MAX_FIELDS
81
+ if (row.length > maxFields) {
82
+ throw new CSVResourceLimitError(`CSV row has too many fields (max ${maxFields})`)
83
+ }
84
+
85
+ const maxFieldBytes = options.maxFieldBytes ?? DEFAULT_CSV_MAX_FIELD_BYTES
86
+ for (const field of row) {
87
+ if (getUtf8ByteLength(field) > maxFieldBytes) {
88
+ throw new CSVResourceLimitError(`CSV field is too large (max ${maxFieldBytes} bytes)`)
89
+ }
90
+ }
91
+ }
92
+
93
+ function addCsvRow(result: string[][], row: string[], options: CSVParseOptions): void {
94
+ // Only keep rows with at least one non-whitespace field
95
+ if (!row.some(field => field.trim())) {
96
+ return
97
+ }
98
+
99
+ enforceFieldLimits(row, options)
100
+ result.push(row)
101
+ enforceRowLimit(result.length, options)
102
+ }
103
+
104
+ /**
105
+ * Parses CSV text into a 2D array of strings, handling quoted fields, escaped
106
+ * quotes, multiline fields, and different line endings (CRLF, LF, CR).
107
+ * Completely empty rows are skipped.
108
+ *
109
+ * Resource limits (size, rows, fields, field size) are enforced during
110
+ * parsing; pass `options` to override the defaults. A violation throws
111
+ * {@link CSVResourceLimitError}.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const csv = 'name,age,city\nJohn,25,"New York"'
116
+ * parseCSV(csv) // [['name', 'age', 'city'], ['John', '25', 'New York']]
117
+ * ```
118
+ */
119
+ export function parseCSV(csvText: string, options: CSVParseOptions = {}): string[][] {
120
+ enforceCsvTextLimit(csvText, options)
121
+
2
122
  const result: string[][] = []
3
123
  let currentRow: string[] = []
4
124
  let currentField = ''
@@ -11,6 +131,7 @@ export function parseCSV(csvText: string): string[][] {
11
131
 
12
132
  if (char === '"') {
13
133
  if (inQuotes && nextChar === '"') {
134
+ // Escaped quote - convert "" to "
14
135
  currentField += '"'
15
136
  i += 2
16
137
  continue
@@ -32,9 +153,7 @@ export function parseCSV(csvText: string): string[][] {
32
153
  if (char === '\n' || char === '\r') {
33
154
  if (currentField || currentRow.length > 0) {
34
155
  currentRow.push(currentField)
35
- if (currentRow.some(field => field.trim())) {
36
- result.push(currentRow)
37
- }
156
+ addCsvRow(result, currentRow, options)
38
157
  currentRow = []
39
158
  currentField = ''
40
159
  }
@@ -53,18 +172,29 @@ export function parseCSV(csvText: string): string[][] {
53
172
  i++
54
173
  }
55
174
 
175
+ // CSV might not end with a newline
56
176
  if (currentField || currentRow.length > 0) {
57
177
  currentRow.push(currentField)
58
- if (currentRow.some(field => field.trim())) {
59
- result.push(currentRow)
60
- }
178
+ addCsvRow(result, currentRow, options)
61
179
  }
62
180
 
63
181
  return result
64
182
  }
65
183
 
66
- export function parseCSVToObjects(csvText: string): Record<string, unknown>[] {
67
- const rows = parseCSV(csvText)
184
+ /**
185
+ * Parses CSV data into an array of objects using the first row as headers.
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * const csv = 'name,age\nJohn,25'
190
+ * parseCSVToObjects(csv) // [{ name: 'John', age: '25' }]
191
+ * ```
192
+ */
193
+ export function parseCSVToObjects(
194
+ csvText: string,
195
+ options: CSVParseOptions = {}
196
+ ): Record<string, string>[] {
197
+ const rows = parseCSV(csvText, options)
68
198
 
69
199
  if (rows.length < 2) {
70
200
  return []
@@ -74,7 +204,7 @@ export function parseCSVToObjects(csvText: string): Record<string, unknown>[] {
74
204
  const dataRows = rows.slice(1)
75
205
 
76
206
  return dataRows.map(row => {
77
- const obj: Record<string, unknown> = {}
207
+ const obj: Record<string, string> = {}
78
208
  headers.forEach((header, index) => {
79
209
  obj[header] = row[index]?.trim() || ''
80
210
  })
package/src/help.ts CHANGED
@@ -22,6 +22,7 @@ interface CliGroupDoc {
22
22
  }
23
23
 
24
24
  const GLOBAL_OPTIONS = [
25
+ ['--json', 'Emit machine-readable JSON instead of human text. Available on every command, as a prefix (orizu --json <command>) or trailing flag.'],
25
26
  ['--local', 'Use http://localhost:3000.'],
26
27
  ['--server <url>', 'Use a specific server origin, for example https://preview.example.com.'],
27
28
  ['--version, -v', 'Print the orizu CLI version.'],
@@ -76,26 +77,111 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
76
77
  summary: 'Submit an optimization event payload.',
77
78
  group: 'Optimizations',
78
79
  },
80
+ {
81
+ path: ['setup'],
82
+ usage: 'orizu setup [--agent <claude|codex>]... [--scope global|project] [--mode auto|link|copy] [--workspace|--no-workspace] [--no-install] [--no-handoff] [--launch <claude|codex>] [--skip-login] [--yes] [--dry-run] [--no-input] [--json]',
83
+ summary: 'Guided onboarding: sign in, set up your coding agents, create a local workspace, and hand off to your agent.',
84
+ group: 'Agent setup',
85
+ options: [
86
+ { name: '--agent <agent>', help: 'Coding agent to set up. Repeat for multiple agents.', repeatable: true, choices: ['claude', 'codex'] },
87
+ { name: '--scope <scope>', help: 'global (default) installs for you across all projects; project installs into the current repo.', choices: ['global', 'project'] },
88
+ { name: '--mode <mode>', help: 'Skill install sync mode (auto, link, copy).', choices: ['auto', 'link', 'copy'] },
89
+ { name: '--workspace', help: 'Create the gitignored .orizu/ workspace without prompting.' },
90
+ { name: '--no-workspace', help: 'Skip workspace creation.' },
91
+ { name: '--no-install', help: 'Skip the agent integration phase.' },
92
+ { name: '--no-handoff', help: 'Skip printing the coding-agent setup prompt.' },
93
+ { name: '--launch <agent>', help: 'Launch a detected coding agent with the setup prompt (interactive terminals only).', choices: ['claude', 'codex'] },
94
+ { name: '--skip-login', help: 'Skip the authentication phase.' },
95
+ { name: '--yes', help: 'Apply changes without per-step confirmation prompts.' },
96
+ { name: '--dry-run', help: 'Preview every planned change without writing.' },
97
+ { name: '--no-input', help: 'Never prompt; behave as in a non-interactive terminal.' },
98
+ { name: '--json', help: 'Emit the setup summary as JSON.' },
99
+ ],
100
+ examples: [
101
+ 'orizu setup',
102
+ 'orizu setup --agent claude --agent codex --yes',
103
+ 'orizu setup --skip-login --no-workspace --dry-run',
104
+ ],
105
+ },
106
+ {
107
+ path: ['setup', 'prompt'],
108
+ usage: 'orizu setup prompt',
109
+ summary: 'Print the coding-agent setup prompt for repo-specific Orizu adoption.',
110
+ group: 'Agent setup',
111
+ examples: ['orizu setup prompt', 'claude "$(orizu setup prompt)"'],
112
+ },
79
113
  {
80
114
  path: ['install-skill'],
81
- usage: 'orizu install-skill [--target <target>]... [--yes] [--dry-run]',
82
- summary: 'Install the bundled Orizu CLI skill locally for coding agents.',
115
+ usage: 'orizu install-skill [--agent <claude|codex>]... [--scope global|project] [--mode auto|link|copy] [--target <target>]... [--yes] [--dry-run]',
116
+ summary: 'Set up the Orizu skill for your coding agents (Claude Code, Codex/Open Agent Skills, or project instructions).',
83
117
  group: 'Agent setup',
84
118
  aliases: [['skills', 'install']],
85
119
  options: [
120
+ {
121
+ name: '--agent <agent>',
122
+ help: 'Coding agent to set up. Repeat for multiple agents.',
123
+ repeatable: true,
124
+ choices: ['claude', 'codex'],
125
+ },
126
+ {
127
+ name: '--scope <scope>',
128
+ help: 'Where the install lives: global installs for you across all projects (default); project installs into the current repo.',
129
+ choices: ['global', 'project'],
130
+ },
131
+ {
132
+ name: '--mode <mode>',
133
+ help: 'How installs stay in sync with the CLI: auto (default) symlinks when the CLI install path is stable and copies otherwise; link forces a symlink; copy forces a full copy with sync metadata. Project-scope installs always copy.',
134
+ choices: ['auto', 'link', 'copy'],
135
+ },
86
136
  {
87
137
  name: '--target <target>',
88
- help: 'Install destination. Repeat for multiple targets.',
138
+ help: 'Advanced: explicit install destination ID. Repeat for multiple targets.',
89
139
  repeatable: true,
90
- choices: ['agent-user', 'codex-project', 'claude-user', 'claude-project', 'agents-md'],
140
+ choices: ['agent-user', 'agents-project', 'codex-project', 'claude-user', 'claude-project', 'agents-md'],
91
141
  },
92
142
  { name: '--yes', help: 'Replace an existing managed skill or AGENTS.md section without prompting.' },
93
143
  { name: '--dry-run', help: 'Show planned writes without changing files.' },
94
144
  ],
95
145
  examples: [
96
- 'orizu install-skill --target agent-user --yes',
97
- 'orizu install-skill --target codex-project --target agents-md',
98
- 'orizu skills install --target claude-user --dry-run',
146
+ 'orizu install-skill --agent claude --agent codex --yes',
147
+ 'orizu install-skill --agent claude --scope project --yes',
148
+ 'orizu install-skill --target agents-md --dry-run',
149
+ ],
150
+ },
151
+ {
152
+ path: ['skills', 'status'],
153
+ usage: 'orizu skills status [--json]',
154
+ summary: 'Report installed Orizu skill targets and whether each is in sync with the CLI.',
155
+ group: 'Agent setup',
156
+ options: [
157
+ { name: '--json', help: 'Emit machine-readable status for every known install target.' },
158
+ ],
159
+ examples: ['orizu skills status', 'orizu skills status --json'],
160
+ },
161
+ {
162
+ path: ['skills', 'update'],
163
+ usage: 'orizu skills update [--dry-run] [--json]',
164
+ summary: 'Refresh stale copied skill installs and repair broken symlinks.',
165
+ group: 'Agent setup',
166
+ options: [
167
+ { name: '--dry-run', help: 'Show which installs would be refreshed without changing files.' },
168
+ { name: '--json', help: 'Emit machine-readable update results.' },
169
+ ],
170
+ examples: ['orizu skills update', 'orizu skills update --dry-run'],
171
+ },
172
+ {
173
+ path: ['skills', 'path'],
174
+ usage: 'orizu skills path [--skill-md] [--json]',
175
+ summary: 'Print where the bundled Orizu skill lives so agents can read it without installing.',
176
+ group: 'Agent setup',
177
+ options: [
178
+ { name: '--skill-md', help: 'Print the full path to SKILL.md instead of the skill root directory.' },
179
+ { name: '--json', help: 'Emit machine-readable metadata: name, root, skillMd, source, cliVersion, skillHash.' },
180
+ ],
181
+ examples: [
182
+ 'orizu skills path',
183
+ 'orizu skills path --skill-md',
184
+ 'orizu skills path --json',
99
185
  ],
100
186
  },
101
187
  {
@@ -158,10 +244,22 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
158
244
  },
159
245
  {
160
246
  path: ['prompts', 'list'],
161
- usage: 'orizu prompts list --project <team/project>',
247
+ usage: 'orizu prompts list --project <team/project> [--status active|archived|all]',
162
248
  summary: 'List prompt artifacts in a project.',
163
249
  group: 'Prompts and judges',
164
250
  },
251
+ {
252
+ path: ['prompts', 'archive'],
253
+ usage: 'orizu prompts archive <prompt-id-or-name> --project <team/project> [--json]',
254
+ summary: 'Mark a prompt artifact as archived.',
255
+ group: 'Prompts and judges',
256
+ },
257
+ {
258
+ path: ['prompts', 'restore'],
259
+ usage: 'orizu prompts restore <prompt-id-or-name> --project <team/project> [--json]',
260
+ summary: 'Restore an archived prompt artifact to active status.',
261
+ group: 'Prompts and judges',
262
+ },
165
263
  {
166
264
  path: ['prompts', 'comments'],
167
265
  usage: 'orizu prompts comments <prompt-id-or-name> --project <team/project> [--label <label> | --version <id>] [--json]',
@@ -200,7 +298,7 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
200
298
  },
201
299
  {
202
300
  path: ['judges', 'list'],
203
- usage: 'orizu judges list --project <team/project>',
301
+ usage: 'orizu judges list --project <team/project> [--status active|archived|all]',
204
302
  summary: 'List judge prompt artifacts in a project.',
205
303
  group: 'Prompts and judges',
206
304
  },
@@ -284,7 +382,7 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
284
382
  },
285
383
  {
286
384
  path: ['optimizations', 'run-gepa'],
287
- usage: '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]',
385
+ usage: '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] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n>] [--num-threads auto|N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs]',
288
386
  summary: 'Run the bundled GEPA-style optimizer locally and stream events.',
289
387
  group: 'Optimizations',
290
388
  },
@@ -377,20 +475,20 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
377
475
  },
378
476
  {
379
477
  path: ['tasks', 'create'],
380
- 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]',
381
- summary: 'Create a draft review task by default, or publish immediately with --publish and assignees.',
478
+ usage: 'orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]',
479
+ summary: 'Create a draft review task by default, with auto distribution or an explicit row-assignment JSONL file.',
382
480
  group: 'Tasks',
383
481
  },
384
482
  {
385
483
  path: ['tasks', 'publish'],
386
- usage: 'orizu tasks publish --task <taskId> --assignees <userId1,userId2> [--json]',
484
+ usage: 'orizu tasks publish --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--json]',
387
485
  summary: 'Publish an approved draft task and assign reviewers.',
388
486
  group: 'Tasks',
389
487
  },
390
488
  {
391
489
  path: ['tasks', 'assign'],
392
- usage: 'orizu tasks assign --task <taskId> --assignees <userId1,userId2>',
393
- summary: 'Assign a task to canonical user IDs.',
490
+ usage: 'orizu tasks assign --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--replace-existing] [--json]',
491
+ summary: 'Assign a task with auto distribution or an explicit row-assignment JSONL file.',
394
492
  group: 'Tasks',
395
493
  },
396
494
  {
@@ -658,6 +756,7 @@ export function getCapabilities(version: string) {
658
756
  return {
659
757
  name: 'orizu',
660
758
  version,
759
+ globalOptions: GLOBAL_OPTIONS.map(([name, help]) => ({ name, help })),
661
760
  commands: COMMAND_DOCS.map(doc => ({
662
761
  name: commandKey(doc.path),
663
762
  usage: doc.usage,