@martini-film/cli 0.2.0 → 0.4.0

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.
Files changed (46) hide show
  1. package/README.md +173 -56
  2. package/dist/api.js +69 -2
  3. package/dist/api.js.map +1 -1
  4. package/dist/approve.js +2 -1
  5. package/dist/approve.js.map +1 -1
  6. package/dist/args.js +31 -6
  7. package/dist/args.js.map +1 -1
  8. package/dist/cli.js +457 -110
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/assets.js +297 -45
  11. package/dist/commands/assets.js.map +1 -1
  12. package/dist/commands/call.js +99 -0
  13. package/dist/commands/call.js.map +1 -0
  14. package/dist/commands/context.js +16 -3
  15. package/dist/commands/context.js.map +1 -1
  16. package/dist/commands/files.js +208 -83
  17. package/dist/commands/files.js.map +1 -1
  18. package/dist/commands/guide.js +67 -0
  19. package/dist/commands/guide.js.map +1 -0
  20. package/dist/commands/init.js +196 -0
  21. package/dist/commands/init.js.map +1 -0
  22. package/dist/commands/login.js +69 -15
  23. package/dist/commands/login.js.map +1 -1
  24. package/dist/commands/tools.js +63 -78
  25. package/dist/commands/tools.js.map +1 -1
  26. package/dist/commands/use.js +143 -0
  27. package/dist/commands/use.js.map +1 -0
  28. package/dist/config.js +87 -6
  29. package/dist/config.js.map +1 -1
  30. package/dist/generated/guide.json +104 -0
  31. package/dist/generated/manifest.json +3885 -241
  32. package/dist/guideCache.js +204 -0
  33. package/dist/guideCache.js.map +1 -0
  34. package/dist/help.js +391 -34
  35. package/dist/help.js.map +1 -1
  36. package/dist/manifest.js +34 -3
  37. package/dist/manifest.js.map +1 -1
  38. package/dist/manifestCache.js +196 -0
  39. package/dist/manifestCache.js.map +1 -0
  40. package/dist/output.js +251 -28
  41. package/dist/output.js.map +1 -1
  42. package/dist/schema.js +132 -15
  43. package/dist/schema.js.map +1 -1
  44. package/dist/wait.js +181 -8
  45. package/dist/wait.js.map +1 -1
  46. package/package.json +2 -2
package/dist/help.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { CLI_VERSION } from './api.js';
2
2
  import { approvalSupport } from './approve.js';
3
3
  import { groupTools } from './manifest.js';
4
- import { flagSpecs, kebab } from './schema.js';
4
+ import { flagSpecs, kebab, propertySchema, withoutNull } from './schema.js';
5
5
  export const SUPERSEDED_BY_COMPOUND = {
6
6
  upload_assets_prepare: 'upload',
7
7
  upload_assets_complete: 'upload',
@@ -9,12 +9,31 @@ export const SUPERSEDED_BY_COMPOUND = {
9
9
  get_asset_download_url: 'download',
10
10
  };
11
11
  const CONTEXT_PROPERTIES = ['projectId', 'canvasId', 'runId'];
12
+ export const MEDIA_RESULT_TOOLS = new Set([
13
+ 'view_asset',
14
+ 'view_library_asset',
15
+ 'inspect_video',
16
+ 'render_timeline_frame',
17
+ 'inspect_timeline_artifact',
18
+ 'get_timeline_audio_report',
19
+ 'extract_video_frame',
20
+ ]);
21
+ const COMPACT_FLAGS_OVER = 12;
22
+ const RESULT_FIELD_DEFAULT_LINES = 30;
12
23
  export const GLOBAL_FLAGS = [
13
24
  [
14
25
  '--fields a,b.c',
15
- 'keep only these dotted paths of the result (on the server; in the CLI after a wait or when a segment is * — assets.*.status steps into an id-keyed map)',
26
+ 'keep only these dotted paths of the result (on the server; in the CLI after a wait or when a segment is * — assets.*.status steps into an id-keyed map); a tool argument named like a global flag is spelled --tool-<name> (--tool-fields)',
16
27
  ],
17
- ['--out <path>', 'write the result (or media bytes) to a file; print one summary line'],
28
+ [
29
+ '--out-dir <dir>',
30
+ 'write each media block to its own file, in result order, named by the asset its text block names',
31
+ ],
32
+ [
33
+ '--out <path>',
34
+ 'write the result to a file and print a one-line JSON receipt; a media extension (.jpg .png .webp .mp3 .wav) writes the media bytes of a one-media result (view_asset, render_timeline_frame)',
35
+ ],
36
+ ['--raw', 'with --out or a pipe: the media bytes of a one-media result, whatever the file is called'],
18
37
  [
19
38
  '--args <object|@file>',
20
39
  'pass the whole arguments object as JSON instead of flags (tools only; --json is an alias unless the tool has an argument named json)',
@@ -25,7 +44,7 @@ export const GLOBAL_FLAGS = [
25
44
  ],
26
45
  [
27
46
  '--timeout <seconds>',
28
- 'how long --wait waits before exiting 4 with the result and the pending ids (default MARTINI_WAIT_TIMEOUT_SECONDS, which a sandbox sets from its step limit, else 50)',
47
+ 'how long --wait waits before exiting 4 with the result and the pending ids (default MARTINI_WAIT_TIMEOUT_SECONDS when set — a Martini workflow run sets it to its step limit — else 50)',
29
48
  ],
30
49
  [
31
50
  '--approve --max-olives <n>',
@@ -33,19 +52,26 @@ export const GLOBAL_FLAGS = [
33
52
  ],
34
53
  [
35
54
  '--project / --canvas / --run <id>',
36
- 'context defaults for projectId / canvasId / runId (--project-id, --canvas-id, --run-id mean the same); --no-context sends only what you typed',
55
+ 'projectId / canvasId / runId for this command (--project-id, --canvas-id, --run-id mean the same); martini use <project> sets the first two once for the directory; --no-context sends only what you typed',
37
56
  ],
38
57
  [
39
58
  '--api-key / --api-url / --profile',
40
59
  'credential and endpoint overrides (else env, .martini/config.json, ~/.config/martini/config.json)',
41
60
  ],
42
61
  ['--quiet', 'print nothing on success'],
62
+ [
63
+ '--schema [<flag>] [--keys]',
64
+ "on a tool: print that argument's JSON Schema (no flag: the whole input schema) and exit; --keys prints one line per key instead (name, type, first sentence); needs no key, sends nothing",
65
+ ],
66
+ ['--help [--all]', 'on a tool: its flags, result fields, and examples; --all prints every description in full'],
43
67
  ];
68
+ export const EXIT_CODES_LINE = 'Exit codes: 0 done, all of it · 1 tool error · 2 usage · 3 auth · 4 not ready or timed out · 5 over budget (olives, or a quote over --max-olives)';
44
69
  export function renderHelp(manifest, compounds) {
45
70
  const lines = [];
46
71
  lines.push(`martini ${CLI_VERSION} — the Martini CLI for agents and scripts`);
47
72
  lines.push('');
48
73
  lines.push('Usage: martini <command> [flags] martini <command> --help');
74
+ lines.push('Start with martini guide: the operator rules, once per session (martini guide <topic> for one topic).');
49
75
  lines.push('');
50
76
  lines.push('Commands (what no single tool does):');
51
77
  for (const command of compounds)
@@ -74,14 +100,14 @@ export function renderHelp(manifest, compounds) {
74
100
  for (const [flag, description] of GLOBAL_FLAGS)
75
101
  lines.push(` ${flag.padEnd(36)} ${description}`);
76
102
  lines.push('');
77
- lines.push('Output: JSON on stdout (compact when piped). Errors: one JSON object on stderr.');
78
- lines.push('Exit codes: 0 ok · 1 tool error · 2 usage · 3 auth · 4 not ready or timed out · 5 insufficient olives');
103
+ lines.push('Output: JSON on stdout (compact when piped); guide, init without --write, files cat, and --help print text. Errors: one JSON object on stderr; a command that did part of its work prints that part on stdout as well.');
104
+ lines.push(EXIT_CODES_LINE);
79
105
  lines.push('Docs: https://www.martini.film/docs/cli');
80
106
  return lines.join('\n') + '\n';
81
107
  }
82
- function describeFlag(spec) {
108
+ function describeFlag(tool, spec) {
83
109
  const type = spec.kind === 'array'
84
- ? `${spec.items ?? 'string'}[] (repeat the flag, or a JSON list)`
110
+ ? `${spec.items ?? 'string'}[] (values after the flag, a repeated flag, or a JSON list)`
85
111
  : spec.kind === 'object' || spec.kind === 'json'
86
112
  ? spec.acceptsString
87
113
  ? 'JSON or text'
@@ -94,11 +120,96 @@ function describeFlag(spec) {
94
120
  const values = spec.enum ?? spec.itemEnum;
95
121
  if (values)
96
122
  parts.push(`${spec.itemEnum ? 'each ' : ''}one of: ${values.map(value => JSON.stringify(value)).join(', ')}`);
123
+ if (takesJson(spec))
124
+ parts.push(`(martini ${tool.name} --schema ${spec.flag})`);
97
125
  return parts.join(' ');
98
126
  }
99
- export function renderToolHelp(tool) {
127
+ export function firstSentence(text, max = 100) {
128
+ const flat = text.replace(/\s+/g, ' ').trim();
129
+ const end = flat.search(/(?<!\b(?:e\.g|i\.e|e|i|vs|etc))(?<!\d)[.!?](?=\s|$)/);
130
+ const sentence = end >= 0 ? flat.slice(0, end + 1) : flat;
131
+ return sentence.length > max ? `${sentence.slice(0, max - 1).trimEnd()}…` : sentence;
132
+ }
133
+ function flagType(spec) {
134
+ if (spec.kind === 'array')
135
+ return `${spec.items ?? 'string'}[]`;
136
+ if (spec.kind === 'object' || spec.kind === 'json')
137
+ return spec.acceptsString ? 'JSON or text' : 'JSON';
138
+ return spec.kind;
139
+ }
140
+ function briefFlag(spec) {
141
+ const values = spec.enum ?? spec.itemEnum;
142
+ const choice = values && values.length <= 6 ? ` one of: ${values.map(value => JSON.stringify(value)).join(', ')}` : '';
143
+ const about = spec.description ? ` ${firstSentence(spec.description)}` : '';
144
+ return `${`--${spec.flag}`.padEnd(30)} ${flagType(spec)}${choice}${about}`;
145
+ }
146
+ export function schemaKeyLines(raw, what) {
147
+ const { schema } = withoutNull(raw);
148
+ const type = typeOf(schema);
149
+ const variants = unionVariants(schema);
150
+ if (variants) {
151
+ return variants.flatMap((variant, index) => [
152
+ ...(index > 0 ? [''] : []),
153
+ ...schemaKeyLines(variant, `${what}, variant ${index + 1} of ${variants.length}`),
154
+ ]);
155
+ }
156
+ if (type === 'array' && schema.items)
157
+ return schemaKeyLines(schema.items, `${what} (each element)`);
158
+ const properties = Object.entries(schema.properties ?? {});
159
+ if (type !== 'object' || properties.length === 0) {
160
+ return [
161
+ `${what}: ${variantSummary(schema)}${schema.description ? ` — ${firstSentence(schema.description, 160)}` : ''}`,
162
+ ];
163
+ }
164
+ const required = new Set(schema.required ?? []);
165
+ const names = properties.map(([name]) => `${name}${required.has(name) ? '*' : ''}`);
166
+ const kinds = properties.map(([, value]) => keyType(value));
167
+ const nameWidth = Math.min(32, Math.max(...names.map(name => name.length)));
168
+ const kindWidth = Math.min(28, Math.max(...kinds.map(kind => kind.length)));
169
+ return [
170
+ `${what}: ${properties.length} key${properties.length === 1 ? '' : 's'} (* required)`,
171
+ ...properties.map(([, value], index) => {
172
+ const about = value.description ? firstSentence(value.description, 110) : '';
173
+ return ` ${names[index].padEnd(nameWidth)} ${kinds[index].padEnd(kindWidth)}${about ? ` ${about}` : ''}`.trimEnd();
174
+ }),
175
+ ];
176
+ }
177
+ function keyType(raw) {
178
+ const { schema, nullable } = withoutNull(raw);
179
+ const summary = (() => {
180
+ if ('const' in schema)
181
+ return JSON.stringify(schema.const);
182
+ if (schema.enum?.length) {
183
+ const listedValues = listed(schema.enum.map(value => JSON.stringify(value)));
184
+ return listedValues.length <= 48 ? listedValues : 'string (enum)';
185
+ }
186
+ const type = typeOf(schema);
187
+ if (type === 'array')
188
+ return `${(schema.items && typeOf(withoutNull(schema.items).schema)) ?? 'any'}[]`;
189
+ return type ?? (unionVariants(schema) ? 'union' : 'any');
190
+ })();
191
+ return nullable ? `${summary}?` : summary;
192
+ }
193
+ function takesJson(spec) {
194
+ return (spec.kind === 'object' ||
195
+ spec.kind === 'json' ||
196
+ (spec.kind === 'array' && (spec.items === 'object' || spec.items === 'json')));
197
+ }
198
+ function shadowNote(tool, spec, shadows) {
199
+ const scalar = (kind) => kind === 'number' || kind === 'integer' ? '1' : kind === 'boolean' ? 'true' : kind === 'string' ? '"…"' : '{…}';
200
+ const sample = spec.kind === 'array' ? `[${scalar(spec.items === 'json' ? 'string' : spec.items)}]` : scalar(spec.kind);
201
+ const row = GLOBAL_FLAGS.find(([label]) => label.split(/[\s/]+/).includes(`--${shadows}`));
202
+ const role = row ? ` (${row[1].split(/ \(|;/)[0]})` : '';
203
+ return [
204
+ ...wrap(`The tool's own "${spec.property}" argument. --${shadows} is the CLI's global flag on every command${role}, so give this one as --${spec.flag}, or inside --args:`, 96).split('\n'),
205
+ ` martini ${tool.name} --args '{"${spec.property}": ${sample}}'`,
206
+ ];
207
+ }
208
+ export function renderToolHelp(tool, options = {}) {
100
209
  const specs = flagSpecs(tool);
101
210
  const lines = [];
211
+ const compact = !options.all && specs.length > COMPACT_FLAGS_OVER;
212
+ const shortened = [];
102
213
  lines.push(`martini ${tool.name}${tool.title ? ` — ${tool.title}` : ''}`);
103
214
  lines.push('');
104
215
  lines.push(wrap(tool.description, 100));
@@ -117,10 +228,33 @@ export function renderToolHelp(tool) {
117
228
  lines.push('Flags:');
118
229
  if (specs.length === 0)
119
230
  lines.push(' (no arguments)');
231
+ const brief = compact ? specs.filter(spec => !spec.required) : [];
120
232
  for (const spec of specs) {
121
- lines.push(` ${describeFlag(spec)}`);
233
+ if (brief.includes(spec))
234
+ continue;
235
+ lines.push(` ${describeFlag(tool, spec)}`);
122
236
  if (spec.description)
123
237
  lines.push(` ${wrap(spec.description, 96).split('\n').join('\n ')}`);
238
+ for (const line of shapeLines(propertySchema(tool, spec.property))) {
239
+ lines.push(` ${wrap(line, 94).split('\n').join('\n ')}`);
240
+ }
241
+ if (spec.shadows)
242
+ for (const line of shadowNote(tool, spec, spec.shadows))
243
+ lines.push(` ${line}`);
244
+ }
245
+ if (brief.length > 0) {
246
+ lines.push('');
247
+ lines.push(`Optional flags (${brief.length}; one line each):`);
248
+ for (const spec of brief)
249
+ lines.push(` ${briefFlag(spec)}`);
250
+ shortened.push('every optional flag in full');
251
+ }
252
+ const rules = ruleLines(tool, specs);
253
+ if (rules.length > 0) {
254
+ lines.push('');
255
+ lines.push('Rules:');
256
+ for (const line of rules)
257
+ lines.push(` ${line}`);
124
258
  }
125
259
  const properties = tool.inputSchema.properties ?? {};
126
260
  const contextual = CONTEXT_PROPERTIES.filter(property => property in properties);
@@ -128,7 +262,7 @@ export function renderToolHelp(tool) {
128
262
  lines.push('');
129
263
  lines.push(wrap(`Context: ${contextual.map(property => `--${kebabOf(property)}`).join(' / ')} may be omitted; the value then comes from ${contextual
130
264
  .map(property => `--${property.replace(/Id$/, '')}`)
131
- .join(' / ')}, ${contextual.map(property => `MARTINI_${property.replace(/Id$/, '').toUpperCase()}_ID`).join(' / ')}, or .martini/config.json (a run sandbox has one).`, 100));
265
+ .join(' / ')}, ${contextual.map(property => `MARTINI_${property.replace(/Id$/, '').toUpperCase()}_ID`).join(' / ')}, or what martini use <project> set for this directory (a Martini workflow run has its own).`, 100));
132
266
  }
133
267
  const approval = approvalSupport(tool);
134
268
  if (approval) {
@@ -137,16 +271,43 @@ export function renderToolHelp(tool) {
137
271
  ? `Approval: without --${kebab(approval.argument)} the tool answers a quote (approvalNeeded: true, planHash, totalEstimatedOlives) and starts nothing. --approve --max-olives <n> takes the quote, refuses when the total is over the cap (PLAN_OVER_BUDGET, nothing started), and otherwise re-calls with the quote's planHash${approval.echoesBatchRequestId ? ', batchRequestId,' : ''} and the approval in one command; the result carries approvedPlan.`
138
272
  : `Approval: --${kebab(approval.argument)} records that the plan was approved; there is no server quote, so --approve attests and --max-olives cannot apply here (for a capped batch: create_nodes, then generate_nodes --approve --max-olives <n>).`, 100));
139
273
  }
274
+ if (MEDIA_RESULT_TOOLS.has(tool.name)) {
275
+ lines.push('');
276
+ lines.push('Output: the result carries media. Printed, each image or audio block is a stub (type and size). Save it:');
277
+ lines.push(` martini ${tool.name} … --out <file.jpg> one media block → that file; prints { path, bytes, contentType, result }`);
278
+ if (tool.annotations.readOnlyHint) {
279
+ lines.push(` martini ${tool.name} … --out-dir <dir> every block → a file each, named by its asset; prints { dir, files }`);
280
+ }
281
+ lines.push('Then open the file to look at it: nothing is shown until you do.');
282
+ }
140
283
  if (tool.outputSchema) {
284
+ const every = resultFieldLines(tool.outputSchema);
285
+ let depth = RESULT_FIELD_DEPTH;
286
+ while (!options.all &&
287
+ depth > 1 &&
288
+ resultFieldLines(tool.outputSchema, depth).length > RESULT_FIELD_DEFAULT_LINES) {
289
+ depth -= 1;
290
+ }
291
+ const shown = resultFieldLines(tool.outputSchema, depth);
292
+ const deeper = every.length - shown.length;
141
293
  lines.push('');
142
294
  lines.push('Result fields (use with --fields; the tool may return more):');
143
- for (const line of resultFieldLines(tool.outputSchema))
295
+ for (const line of shown)
144
296
  lines.push(` ${line}`);
297
+ if (deeper > 0) {
298
+ lines.push(` … ${deeper} more nested field${deeper === 1 ? '' : 's'}`);
299
+ shortened.push('every result field');
300
+ }
145
301
  }
146
302
  lines.push('');
147
- lines.push('Example:');
148
- lines.push(` ${exampleFor(tool, specs)}`);
303
+ const examples = examplesFor(tool, specs);
304
+ lines.push(examples.length > 1 ? 'Examples:' : 'Example:');
305
+ for (const example of examples)
306
+ lines.push(` ${example}`);
149
307
  lines.push('');
308
+ if (shortened.length > 0) {
309
+ lines.push(wrap(`Shortened: martini ${tool.name} --help --all prints ${shortened.join(' and ')}; martini ${tool.name} --schema <flag> --keys lists one JSON argument's keys.`, 100));
310
+ }
150
311
  const owner = 'json' in (tool.inputSchema.properties ?? {});
151
312
  lines.push(owner
152
313
  ? 'The whole arguments object can also be given as JSON: --args \'{"...": ...}\' or --args @args.json (this tool owns --json).'
@@ -154,7 +315,7 @@ export function renderToolHelp(tool) {
154
315
  return lines.join('\n') + '\n';
155
316
  }
156
317
  const RESULT_FIELD_DEPTH = 3;
157
- export function resultFieldLines(schema) {
318
+ export function resultFieldLines(schema, maxDepth = RESULT_FIELD_DEPTH) {
158
319
  const rows = [];
159
320
  const visit = (node, path, depth) => {
160
321
  const properties = node.properties ?? {};
@@ -163,7 +324,7 @@ export function resultFieldLines(schema) {
163
324
  const here = path ? `${path}.${name}` : name;
164
325
  const { kind, inner, mapped } = fieldKind(field);
165
326
  rows.push([mapped ? `${here}.*` : here, kind, describeField(field)]);
166
- if (inner && depth < RESULT_FIELD_DEPTH)
327
+ if (inner && depth < maxDepth)
167
328
  visit(inner, mapped ? `${here}.*` : here, depth + 1);
168
329
  }
169
330
  };
@@ -256,27 +417,223 @@ export function renderCompoundHelp(command) {
256
417
  lines.push(` ${example}`);
257
418
  lines.push('');
258
419
  }
259
- lines.push('Global flags apply too: --project / --canvas (or --project-id / --canvas-id), --fields, --out, --wait / --no-wait, --timeout, --quiet, --api-key, --profile.');
420
+ lines.push('Global flags apply too: --project / --canvas (or --project-id / --canvas-id; martini use sets both once), --fields, --out, --wait / --no-wait, --timeout, --quiet, --api-key, --profile.');
260
421
  return lines.join('\n') + '\n';
261
422
  }
262
- export function exampleFor(tool, specs = flagSpecs(tool)) {
263
- const required = specs.filter(spec => spec.required && spec.property !== 'projectId' && spec.property !== 'canvasId');
423
+ export function examplesFor(tool, specs = flagSpecs(tool)) {
424
+ const needed = new Set(specs.filter(spec => spec.required).map(spec => spec.property));
425
+ for (const [, variants] of rootRules(tool.inputSchema))
426
+ for (const key of variants[0].required ?? [])
427
+ needed.add(key);
264
428
  const parts = [`martini ${tool.name}`];
265
- for (const spec of required) {
266
- const sample = spec.enum?.[0] !== undefined
267
- ? JSON.stringify(spec.enum[0])
268
- : spec.kind === 'boolean'
269
- ? ''
270
- : spec.kind === 'number' || spec.kind === 'integer'
271
- ? '1'
272
- : spec.kind === 'array'
273
- ? '<id>'
274
- : spec.kind === 'object' || spec.kind === 'json'
275
- ? "'{...}'"
276
- : `<${spec.flag}>`;
277
- parts.push(sample ? `--${spec.flag} ${sample}` : `--${spec.flag}`);
429
+ for (const spec of specs) {
430
+ if (!needed.has(spec.property))
431
+ continue;
432
+ if (spec.property === 'projectId')
433
+ parts.push('--project <project-id>');
434
+ else if (spec.property === 'canvasId')
435
+ parts.push('--canvas <canvas-id>');
436
+ else
437
+ parts.push(exampleFlag(tool, spec));
438
+ }
439
+ const first = parts.join(' ');
440
+ const optional = specs.find(spec => !needed.has(spec.property) && takesJson(spec) && isObjectSample(sampleFor(tool, spec)));
441
+ return optional ? [first, `${first} ${exampleFlag(tool, optional)}`] : [first];
442
+ }
443
+ function exampleFlag(tool, spec) {
444
+ if (takesJson(spec))
445
+ return `--${spec.flag} ${shellWord(JSON.stringify(sampleFor(tool, spec)))}`;
446
+ const sample = spec.enum?.[0] !== undefined
447
+ ? JSON.stringify(spec.enum[0])
448
+ : spec.kind === 'boolean'
449
+ ? ''
450
+ : spec.kind === 'number' || spec.kind === 'integer'
451
+ ? '1'
452
+ : spec.kind === 'array'
453
+ ? spec.itemEnum?.[0] !== undefined
454
+ ? JSON.stringify(spec.itemEnum[0])
455
+ : spec.items === 'number' || spec.items === 'integer'
456
+ ? '1'
457
+ : spec.items === 'boolean'
458
+ ? 'true'
459
+ : '<id>'
460
+ : `<${spec.flag}>`;
461
+ return sample ? `--${spec.flag} ${sample}` : `--${spec.flag}`;
462
+ }
463
+ function sampleFor(tool, spec) {
464
+ return sampleValue(propertySchema(tool, spec.property), spec.property, 0);
465
+ }
466
+ function isObjectSample(value) {
467
+ return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length > 0;
468
+ }
469
+ function shellWord(text) {
470
+ return /^[\w.,:@/-]+$/.test(text) ? text : `'${text.replace(/'/g, `'\\''`)}'`;
471
+ }
472
+ const SAMPLE_DEPTH = 2;
473
+ function sampleValue(raw, key, depth) {
474
+ const { schema } = withoutNull(raw);
475
+ if ('const' in schema)
476
+ return schema.const;
477
+ if (schema.enum?.length)
478
+ return schema.enum[0];
479
+ const variants = unionVariants(schema);
480
+ if (variants) {
481
+ const object = variants.find(variant => typeOf(withoutNull(variant).schema) === 'object');
482
+ return sampleValue(object ?? variants[0], key, depth);
483
+ }
484
+ switch (typeOf(schema)) {
485
+ case 'string':
486
+ return `<${key}>`;
487
+ case 'number':
488
+ case 'integer':
489
+ return typeof schema.minimum === 'number' && schema.minimum > 1 ? schema.minimum : 1;
490
+ case 'boolean':
491
+ return true;
492
+ case 'array':
493
+ return schema.items ? [sampleValue(schema.items, key, depth)] : [];
494
+ case 'object':
495
+ return sampleObject(schema, depth, depth >= SAMPLE_DEPTH);
496
+ default:
497
+ return {};
278
498
  }
279
- return parts.join(' ');
499
+ }
500
+ function sampleObject(schema, depth, requiredOnly = false) {
501
+ const properties = schema.properties ?? {};
502
+ const names = Object.keys(properties);
503
+ if (names.length === 0) {
504
+ const values = schema.additionalProperties;
505
+ return !requiredOnly && values && typeof values === 'object'
506
+ ? { '<key>': sampleValue(values, 'value', depth + 1) }
507
+ : {};
508
+ }
509
+ const required = (schema.required ?? []).filter(name => name in properties);
510
+ const keys = required.length > 0 ? required : requiredOnly ? [] : names.slice(0, 1);
511
+ return Object.fromEntries(keys.map(name => [name, sampleValue(properties[name], name, depth + 1)]));
512
+ }
513
+ const SUMMARY_KEYS = 12;
514
+ const SUMMARY_OBJECT_KEYS = 64;
515
+ const NESTED_SUMMARY_WIDTH = 64;
516
+ const SUMMARY_WIDTH = 96;
517
+ export function shapeLines(raw) {
518
+ const { schema } = withoutNull(raw);
519
+ const variants = unionVariants(schema);
520
+ if (variants)
521
+ return unionLines('one of:', variants);
522
+ const type = typeOf(schema);
523
+ if (type === 'object') {
524
+ const summary = objectSummary(schema);
525
+ return summary ? [`shape: ${summary}`] : [];
526
+ }
527
+ if (type === 'array' && schema.items) {
528
+ const items = withoutNull(schema.items).schema;
529
+ const itemVariants = unionVariants(items);
530
+ if (itemVariants)
531
+ return unionLines('each one of:', itemVariants);
532
+ const summary = typeOf(items) === 'object' ? objectSummary(items) : undefined;
533
+ return summary ? [`each: ${summary}`] : [];
534
+ }
535
+ return [];
536
+ }
537
+ function unionLines(label, variants) {
538
+ const summaries = variants.map(variantSummary);
539
+ const line = `${label} ${summaries.join(' | ')}`;
540
+ return line.length <= SUMMARY_WIDTH ? [line] : [label, ...summaries.map(summary => ` ${summary}`)];
541
+ }
542
+ function unionVariants(schema) {
543
+ const variants = (schema.anyOf ?? schema.oneOf)?.filter(variant => variant.type !== 'null' && variant.const !== null);
544
+ return variants && variants.length > 1 ? variants : undefined;
545
+ }
546
+ function typeOf(schema) {
547
+ return Array.isArray(schema.type) ? schema.type.find(type => type !== 'null') : schema.type;
548
+ }
549
+ function objectSummary(schema, nested = false) {
550
+ const properties = Object.entries(schema.properties ?? {});
551
+ if (properties.length === 0) {
552
+ const values = schema.additionalProperties;
553
+ return values && typeof values === 'object' ? `{ <key>: ${variantSummary(values)} }` : undefined;
554
+ }
555
+ const required = new Set(schema.required ?? []);
556
+ const ordered = [
557
+ ...properties.filter(([name]) => required.has(name)),
558
+ ...properties.filter(([name]) => !required.has(name)),
559
+ ];
560
+ const shown = ordered.slice(0, Math.max(SUMMARY_OBJECT_KEYS, required.size));
561
+ const keys = shown.map(([name, value]) => `${name}${required.has(name) ? '*' : ''}${fixedValue(value) || (!nested && required.has(name) ? requiredShape(value) : '')}`);
562
+ const more = ordered.length - shown.length;
563
+ return `{ ${keys.join(', ')}${more > 0 ? `, … +${more} more` : ''} }`;
564
+ }
565
+ function requiredShape(raw) {
566
+ const { schema } = withoutNull(raw);
567
+ const list = typeOf(schema) === 'array' && schema.items ? withoutNull(schema.items).schema : undefined;
568
+ const object = list ?? schema;
569
+ if (typeOf(object) !== 'object' || unionVariants(object))
570
+ return '';
571
+ const summary = objectSummary(object, true);
572
+ if (!summary || summary.length > NESTED_SUMMARY_WIDTH)
573
+ return '';
574
+ return `: ${list ? `[${summary}]` : summary}`;
575
+ }
576
+ function fixedValue(raw) {
577
+ const { schema } = withoutNull(raw);
578
+ if ('const' in schema)
579
+ return `: ${JSON.stringify(schema.const)}`;
580
+ return schema.enum?.length === 1 ? `: ${JSON.stringify(schema.enum[0])}` : '';
581
+ }
582
+ function variantSummary(raw) {
583
+ const { schema } = withoutNull(raw);
584
+ if ('const' in schema)
585
+ return JSON.stringify(schema.const);
586
+ if (schema.enum?.length)
587
+ return listed(schema.enum.map(value => JSON.stringify(value)));
588
+ const variants = unionVariants(schema);
589
+ if (variants)
590
+ return `(${listed(variants.map(variantSummary))})`;
591
+ const type = typeOf(schema);
592
+ if (type === 'object')
593
+ return objectSummary(schema) ?? 'object';
594
+ if (type === 'array')
595
+ return `${(schema.items && typeOf(withoutNull(schema.items).schema)) ?? 'any'}[]`;
596
+ return type ?? 'any';
597
+ }
598
+ function listed(values) {
599
+ const shown = values.slice(0, SUMMARY_KEYS);
600
+ return `${shown.join(' | ')}${values.length > shown.length ? ` | … +${values.length - shown.length} more` : ''}`;
601
+ }
602
+ export function ruleLines(tool, specs = flagSpecs(tool)) {
603
+ const flagOf = (property) => `--${specs.find(spec => spec.property === property)?.flag ?? kebab(property)}`;
604
+ const lines = [];
605
+ for (const [label, variants] of rootRules(tool.inputSchema)) {
606
+ const requires = variants.map(variant => (variant.required ?? []).map(flagOf).join(' and '));
607
+ if (variants.every(variant => onlyRequires(variant) && (variant.required?.length ?? 0) > 0)) {
608
+ lines.push(`${label}: ${requires.join(' | ')}`);
609
+ continue;
610
+ }
611
+ lines.push(`${label} these:`);
612
+ variants.forEach((variant, index) => {
613
+ const adds = Object.keys(variant.properties ?? {}).map(flagOf);
614
+ const needs = requires[index] ? `requires ${requires[index]}` : 'requires nothing more';
615
+ lines.push(` - ${needs}${adds.length ? `; takes ${adds.join(', ')}` : ''}`);
616
+ });
617
+ }
618
+ return lines;
619
+ }
620
+ function rootRules(schema) {
621
+ const rules = [];
622
+ const collect = (node) => {
623
+ if (node.oneOf?.length)
624
+ rules.push(['Exactly one of', node.oneOf]);
625
+ if (node.anyOf?.length)
626
+ rules.push(['At least one of', node.anyOf]);
627
+ for (const member of node.allOf ?? [])
628
+ collect(member);
629
+ };
630
+ collect(schema);
631
+ return rules;
632
+ }
633
+ function onlyRequires(variant) {
634
+ return Object.entries(variant).every(([key, value]) => key === 'required' ||
635
+ key === 'description' ||
636
+ (key === 'not' && typeof value === 'object' && value !== null && onlyRequires(value)));
280
637
  }
281
638
  export function wrap(text, width) {
282
639
  const words = text.split(/\s+/);