@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.
- package/README.md +173 -56
- package/dist/api.js +69 -2
- package/dist/api.js.map +1 -1
- package/dist/approve.js +2 -1
- package/dist/approve.js.map +1 -1
- package/dist/args.js +31 -6
- package/dist/args.js.map +1 -1
- package/dist/cli.js +457 -110
- package/dist/cli.js.map +1 -1
- package/dist/commands/assets.js +297 -45
- package/dist/commands/assets.js.map +1 -1
- package/dist/commands/call.js +99 -0
- package/dist/commands/call.js.map +1 -0
- package/dist/commands/context.js +16 -3
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/files.js +208 -83
- package/dist/commands/files.js.map +1 -1
- package/dist/commands/guide.js +67 -0
- package/dist/commands/guide.js.map +1 -0
- package/dist/commands/init.js +196 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.js +69 -15
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/tools.js +63 -78
- package/dist/commands/tools.js.map +1 -1
- package/dist/commands/use.js +143 -0
- package/dist/commands/use.js.map +1 -0
- package/dist/config.js +87 -6
- package/dist/config.js.map +1 -1
- package/dist/generated/guide.json +104 -0
- package/dist/generated/manifest.json +3885 -241
- package/dist/guideCache.js +204 -0
- package/dist/guideCache.js.map +1 -0
- package/dist/help.js +391 -34
- package/dist/help.js.map +1 -1
- package/dist/manifest.js +34 -3
- package/dist/manifest.js.map +1 -1
- package/dist/manifestCache.js +196 -0
- package/dist/manifestCache.js.map +1 -0
- package/dist/output.js +251 -28
- package/dist/output.js.map +1 -1
- package/dist/schema.js +132 -15
- package/dist/schema.js.map +1 -1
- package/dist/wait.js +181 -8
- package/dist/wait.js.map +1 -1
- 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
|
-
[
|
|
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
|
|
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
|
-
'
|
|
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(
|
|
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'}[] (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
148
|
-
lines.push(
|
|
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 <
|
|
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
|
|
263
|
-
const
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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+/);
|