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