@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.
- package/README.md +169 -44
- package/dist/api.js +71 -23
- package/dist/api.js.map +1 -1
- package/dist/approve.js +16 -1
- package/dist/approve.js.map +1 -1
- package/dist/cli.js +487 -142
- package/dist/cli.js.map +1 -1
- package/dist/commands/assets.js +165 -29
- package/dist/commands/assets.js.map +1 -1
- package/dist/commands/call.js +131 -0
- package/dist/commands/call.js.map +1 -0
- package/dist/commands/context.js +11 -2
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/files.js +117 -9
- package/dist/commands/files.js.map +1 -1
- package/dist/commands/guide.js +71 -0
- package/dist/commands/guide.js.map +1 -0
- package/dist/commands/init.js +213 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.js +37 -6
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/tools.js +88 -80
- package/dist/commands/tools.js.map +1 -1
- package/dist/commands/use.js +148 -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 +73 -24
- 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 +27 -0
- package/dist/manifest.js.map +1 -1
- package/dist/manifestCache.js +196 -0
- package/dist/manifestCache.js.map +1 -0
- package/dist/output.js +289 -30
- package/dist/output.js.map +1 -1
- package/dist/schema.js +281 -16
- package/dist/schema.js.map +1 -1
- package/dist/wait.js +33 -2
- 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 { 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
|
-
|
|
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
|
-
[
|
|
18
|
-
|
|
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
|
-
'
|
|
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(
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
162
|
-
lines.push(
|
|
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 <
|
|
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
|
|
277
|
-
const
|
|
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
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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+/);
|