@martini-film/cli 0.3.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 +165 -44
- package/dist/api.js +57 -17
- package/dist/api.js.map +1 -1
- package/dist/approve.js +2 -1
- package/dist/approve.js.map +1 -1
- package/dist/cli.js +429 -144
- package/dist/cli.js.map +1 -1
- package/dist/commands/assets.js +141 -25
- 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 +15 -2
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/files.js +100 -7
- 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 +37 -6
- 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 +82 -6
- package/dist/config.js.map +1 -1
- package/dist/generated/guide.json +104 -0
- package/dist/generated/manifest.json +10 -2
- package/dist/guideCache.js +204 -0
- package/dist/guideCache.js.map +1 -0
- package/dist/help.js +375 -32
- 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 +210 -28
- package/dist/output.js.map +1 -1
- package/dist/schema.js +99 -14
- package/dist/schema.js.map +1 -1
- package/dist/wait.js +31 -1
- package/dist/wait.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { resolve, join } from 'node:path';
|
|
3
|
-
import { CliApiError, createApiClient } from './api.js';
|
|
3
|
+
import { CliApiError, createApiClient, isMediaBlock, isTextBlock } from './api.js';
|
|
4
4
|
import { approvalSupport, runWithApproval } from './approve.js';
|
|
5
5
|
import { flagBoolean, flagString, parseArgs } from './args.js';
|
|
6
6
|
import { download, upload, wait } from './commands/assets.js';
|
|
7
|
+
import { call } from './commands/call.js';
|
|
7
8
|
import { assertKnownFlags, compoundBooleanFlags, CONTEXT_FLAG_ALIASES, needsLocalProjection, selectedFields, timeoutMs, wantsWait, } from './commands/context.js';
|
|
8
9
|
import { files } from './commands/files.js';
|
|
10
|
+
import { guide } from './commands/guide.js';
|
|
9
11
|
import { login, logout, whoami } from './commands/login.js';
|
|
10
|
-
import { init
|
|
12
|
+
import { init } from './commands/init.js';
|
|
13
|
+
import { models, probe, tools, version } from './commands/tools.js';
|
|
14
|
+
import { use } from './commands/use.js';
|
|
11
15
|
import { resolveConfig } from './config.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
16
|
+
import { GUIDE_HASH_HEADER, guideFetcher, guideHashNotice, loadBakedGuide, openGuide, } from './guideCache.js';
|
|
17
|
+
import { renderCompoundHelp, renderHelp, renderToolHelp, schemaKeyLines } from './help.js';
|
|
18
|
+
import { findTool, loadManifest, TOOL_NAME_PATTERN } from './manifest.js';
|
|
19
|
+
import { openManifest } from './manifestCache.js';
|
|
20
|
+
import { EXIT, MEDIA_EXTENSIONS, assertRawDestination, expandMapPaths, hasUndeclaredMapStep, isDeclaredPath, isEmptyProjection, isMediaOutPath, noFieldsMatchedMessage, noteMediaStubs, printBytes, printError, printResult, printWarning, resolveMapPaths, selectFields, unmatchedFields, warnUnmatchedFields, } from './output.js';
|
|
21
|
+
import { ArgumentError, argumentSchema, buildArguments, flagSpecs, GLOBAL_FLAG_NAMES, unknownToolFlags, } from './schema.js';
|
|
22
|
+
import { assetsOutcome, handleOutcome, notCompleted, producedAssetIds, waitForAssets, waitForDelivery, waitForJob, waitForRun, WaitTimeoutError, } from './wait.js';
|
|
17
23
|
export const COMPOUND_COMMANDS = [
|
|
18
24
|
login,
|
|
19
25
|
logout,
|
|
20
26
|
whoami,
|
|
27
|
+
use,
|
|
28
|
+
guide,
|
|
21
29
|
tools,
|
|
30
|
+
call,
|
|
22
31
|
models,
|
|
23
32
|
upload,
|
|
24
33
|
download,
|
|
@@ -29,7 +38,14 @@ export const COMPOUND_COMMANDS = [
|
|
|
29
38
|
version,
|
|
30
39
|
];
|
|
31
40
|
export { GLOBAL_FLAG_NAMES };
|
|
32
|
-
const TOOL_ONLY_GLOBAL_FLAGS = new Set([
|
|
41
|
+
const TOOL_ONLY_GLOBAL_FLAGS = new Set([
|
|
42
|
+
'approve',
|
|
43
|
+
'max-olives',
|
|
44
|
+
'out-dir',
|
|
45
|
+
'schema',
|
|
46
|
+
'all',
|
|
47
|
+
'keys',
|
|
48
|
+
]);
|
|
33
49
|
const GLOBAL_FLAGS_FOR_COMPOUNDS = new Set([
|
|
34
50
|
...[...GLOBAL_FLAG_NAMES].filter(flag => !TOOL_ONLY_GLOBAL_FLAGS.has(flag)),
|
|
35
51
|
...CONTEXT_FLAG_ALIASES,
|
|
@@ -42,20 +58,13 @@ const WAIT_BY_DEFAULT = new Set([
|
|
|
42
58
|
'upscale_assets',
|
|
43
59
|
'render_blender_take',
|
|
44
60
|
]);
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const cwd = options.cwd ?? process.cwd();
|
|
49
|
-
const output = {
|
|
50
|
-
...(options.stdout ? { stdout: options.stdout } : {}),
|
|
51
|
-
...(options.stderr ? { stderr: options.stderr } : {}),
|
|
52
|
-
...(options.isTTY !== undefined ? { isTTY: options.isTTY } : {}),
|
|
53
|
-
};
|
|
54
|
-
const manifest = options.manifest ?? loadManifest();
|
|
61
|
+
const ORDERED_RESULT_TOOLS = new Set(['inspect_video']);
|
|
62
|
+
const BOOLEAN_GLOBAL_FLAGS = new Set(['help', 'version', 'wait', 'quiet', 'raw', 'context', 'approve', 'all', 'keys']);
|
|
63
|
+
function parseCommandLine(argv, manifest) {
|
|
55
64
|
const toolFor = (command) => command && !COMPOUND_COMMANDS.some(candidate => candidate.name === command)
|
|
56
65
|
? findTool(command, manifest)
|
|
57
66
|
: undefined;
|
|
58
|
-
|
|
67
|
+
return parseArgs(argv, (flag, command) => {
|
|
59
68
|
if (BOOLEAN_GLOBAL_FLAGS.has(flag))
|
|
60
69
|
return true;
|
|
61
70
|
const compound = COMPOUND_COMMANDS.find(candidate => candidate.name === command);
|
|
@@ -69,81 +78,225 @@ export async function main(options) {
|
|
|
69
78
|
const tool = toolFor(command);
|
|
70
79
|
return tool ? flagSpecs(tool).some(spec => spec.flag === flag && spec.kind === 'array') : false;
|
|
71
80
|
});
|
|
81
|
+
}
|
|
82
|
+
export async function main(options) {
|
|
83
|
+
const env = options.env ?? process.env;
|
|
84
|
+
const cwd = options.cwd ?? process.cwd();
|
|
85
|
+
const output = {
|
|
86
|
+
...(options.stdout ? { stdout: options.stdout } : {}),
|
|
87
|
+
...(options.stderr ? { stderr: options.stderr } : {}),
|
|
88
|
+
...(options.isTTY !== undefined ? { isTTY: options.isTTY } : {}),
|
|
89
|
+
};
|
|
90
|
+
let session;
|
|
72
91
|
try {
|
|
73
|
-
|
|
74
|
-
parsed.command = 'version';
|
|
75
|
-
if (parsed.command === undefined ||
|
|
76
|
-
parsed.command === 'help' ||
|
|
77
|
-
(flagBoolean(parsed.flags, 'help') && !parsed.command)) {
|
|
78
|
-
printResult(renderHelp(manifest, COMPOUND_COMMANDS), output);
|
|
79
|
-
return EXIT.ok;
|
|
80
|
-
}
|
|
81
|
-
const compound = COMPOUND_COMMANDS.find(command => command.name === parsed.command);
|
|
82
|
-
const tool = compound ? undefined : findTool(parsed.command, manifest);
|
|
83
|
-
if (!compound && !tool) {
|
|
84
|
-
throw new ArgumentError(`Unknown command "${parsed.command}". Run martini --help for the list.`);
|
|
85
|
-
}
|
|
86
|
-
if (flagBoolean(parsed.flags, 'help')) {
|
|
87
|
-
printResult(tool ? renderToolHelp(tool) : renderCompoundHelp(compound), output);
|
|
88
|
-
return EXIT.ok;
|
|
89
|
-
}
|
|
90
|
-
if (compound)
|
|
91
|
-
assertKnownFlags(compound, parsed.flags, GLOBAL_FLAGS_FOR_COMPOUNDS);
|
|
92
|
-
else if (parsed.positionals.length > 0)
|
|
93
|
-
throw strayPositionalError(tool, parsed.positionals);
|
|
94
|
-
const config = resolveConfig(parsed.flags, { env, cwd });
|
|
95
|
-
for (const warning of config.warnings)
|
|
96
|
-
printWarning('CONFIG_WARNING', warning, output);
|
|
97
|
-
const clientFor = options.createClient ??
|
|
98
|
-
(resolved => createApiClient(resolved, { ...(options.fetch ? { fetch: options.fetch } : {}), env }));
|
|
99
|
-
const out = flagString(parsed.flags, 'out');
|
|
100
|
-
if (out)
|
|
101
|
-
output.out = out;
|
|
102
|
-
output.inRun = Boolean(config.runId);
|
|
103
|
-
output.quiet = flagBoolean(parsed.flags, 'quiet') ?? false;
|
|
104
|
-
const context = {
|
|
105
|
-
client: clientFor(config),
|
|
106
|
-
clientFor,
|
|
107
|
-
config,
|
|
108
|
-
manifest,
|
|
109
|
-
flags: parsed.flags,
|
|
110
|
-
positionals: parsed.positionals,
|
|
111
|
-
output,
|
|
112
|
-
env,
|
|
113
|
-
cwd,
|
|
114
|
-
now: options.now ?? Date.now,
|
|
115
|
-
sleep: options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
116
|
-
};
|
|
117
|
-
const select = selectedFields(context);
|
|
118
|
-
timeoutMs(context);
|
|
119
|
-
if (compound) {
|
|
120
|
-
const result = await compound.run(context);
|
|
121
|
-
const projected = select.length && result && typeof result === 'object' ? selectFields(result, select) : result;
|
|
122
|
-
warnNoFieldsMatched(projected, select, output);
|
|
123
|
-
printResult(projected, output);
|
|
124
|
-
return EXIT.ok;
|
|
125
|
-
}
|
|
126
|
-
const result = await runTool(tool, context, parsed.flags);
|
|
127
|
-
warnNoFieldsMatched(result, select, output, knownResultFields(tool));
|
|
128
|
-
printResult(result, output);
|
|
129
|
-
return EXIT.ok;
|
|
92
|
+
session = openSession(options, env, cwd, output);
|
|
130
93
|
}
|
|
131
94
|
catch (error) {
|
|
132
|
-
if (error instanceof CliApiError && error.result !== undefined)
|
|
133
|
-
printResult(error.result, output);
|
|
134
95
|
return printError(error, output);
|
|
135
96
|
}
|
|
97
|
+
const { config, clientFor, client, manifests } = session;
|
|
98
|
+
let refreshed;
|
|
99
|
+
for (;;) {
|
|
100
|
+
const manifest = manifests.state.manifest;
|
|
101
|
+
const parsed = parseCommandLine(options.argv, manifest);
|
|
102
|
+
if (parsed.command)
|
|
103
|
+
output.command = parsed.command;
|
|
104
|
+
try {
|
|
105
|
+
if (flagBoolean(parsed.flags, 'version') && parsed.command === undefined)
|
|
106
|
+
parsed.command = 'version';
|
|
107
|
+
if (parsed.command === undefined && parsed.flags.has('schema')) {
|
|
108
|
+
throw new ArgumentError("--schema describes one tool's arguments: martini <tool> --schema [flag].");
|
|
109
|
+
}
|
|
110
|
+
if (parsed.command === undefined ||
|
|
111
|
+
parsed.command === 'help' ||
|
|
112
|
+
(flagBoolean(parsed.flags, 'help') && !parsed.command)) {
|
|
113
|
+
printResult(renderHelp(manifest, COMPOUND_COMMANDS), output);
|
|
114
|
+
return EXIT.ok;
|
|
115
|
+
}
|
|
116
|
+
const compound = COMPOUND_COMMANDS.find(command => command.name === parsed.command);
|
|
117
|
+
const tool = compound ? undefined : findTool(parsed.command, manifest);
|
|
118
|
+
if (!compound && !tool) {
|
|
119
|
+
if (refreshed === undefined && TOOL_NAME_PATTERN.test(parsed.command)) {
|
|
120
|
+
refreshed = await manifests.refresh();
|
|
121
|
+
if (refreshed === 'updated')
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
throw unknownCommandError(parsed.command, refreshed);
|
|
125
|
+
}
|
|
126
|
+
if (flagBoolean(parsed.flags, 'help')) {
|
|
127
|
+
printResult(tool
|
|
128
|
+
? renderToolHelp(tool, { all: flagBoolean(parsed.flags, 'all') === true })
|
|
129
|
+
: renderCompoundHelp(compound), output);
|
|
130
|
+
return EXIT.ok;
|
|
131
|
+
}
|
|
132
|
+
if (parsed.flags.has('schema')) {
|
|
133
|
+
if (compound) {
|
|
134
|
+
throw new ArgumentError(`--schema describes a tool's arguments; ${compound.name} is a compound command (martini ${compound.name} --help lists its flags).`);
|
|
135
|
+
}
|
|
136
|
+
const target = schemaTarget(options.argv, parsed.flags);
|
|
137
|
+
const schema = argumentSchema(tool, target);
|
|
138
|
+
printResult(flagBoolean(parsed.flags, 'keys') === true
|
|
139
|
+
? `${schemaKeyLines(schema, target ? `--${target.replace(/^--/, '')}` : `${tool.name} arguments`).join('\n')}\n`
|
|
140
|
+
: `${JSON.stringify(schema, null, 2)}\n`, output);
|
|
141
|
+
return EXIT.ok;
|
|
142
|
+
}
|
|
143
|
+
if (parsed.flags.has('keys'))
|
|
144
|
+
throw new ArgumentError('--keys goes with --schema: martini <tool> --schema <flag> --keys.');
|
|
145
|
+
if (parsed.flags.has('all'))
|
|
146
|
+
throw new ArgumentError('--all goes with --help: martini <tool> --help --all.');
|
|
147
|
+
if (compound)
|
|
148
|
+
assertKnownFlags(compound, parsed.flags, GLOBAL_FLAGS_FOR_COMPOUNDS);
|
|
149
|
+
else {
|
|
150
|
+
if (refreshed === undefined && unknownToolFlags(tool, parsed.flags, nonArgumentFlags(tool)).length > 0) {
|
|
151
|
+
refreshed = await manifests.refresh();
|
|
152
|
+
if (refreshed === 'updated')
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (parsed.positionals.length > 0)
|
|
156
|
+
throw strayPositionalError(tool, parsed.positionals);
|
|
157
|
+
}
|
|
158
|
+
for (const warning of config.warnings)
|
|
159
|
+
printWarning('CONFIG_WARNING', warning, output);
|
|
160
|
+
const out = flagString(parsed.flags, 'out');
|
|
161
|
+
if (out)
|
|
162
|
+
output.out = out;
|
|
163
|
+
output.inRun = Boolean(config.runId);
|
|
164
|
+
output.quiet = flagBoolean(parsed.flags, 'quiet') ?? false;
|
|
165
|
+
return await runCommand(compound, tool, {
|
|
166
|
+
client,
|
|
167
|
+
clientFor,
|
|
168
|
+
config,
|
|
169
|
+
manifest,
|
|
170
|
+
manifests,
|
|
171
|
+
guide: lazyGuide(options, config, env, output),
|
|
172
|
+
flags: parsed.flags,
|
|
173
|
+
positionals: parsed.positionals,
|
|
174
|
+
output,
|
|
175
|
+
env,
|
|
176
|
+
cwd,
|
|
177
|
+
now: options.now ?? Date.now,
|
|
178
|
+
sleep: options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error instanceof CliApiError && error.result !== undefined)
|
|
183
|
+
printResult(error.result, output);
|
|
184
|
+
return printError(error, output);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
136
187
|
}
|
|
137
|
-
|
|
188
|
+
function openSession(options, env, cwd, output) {
|
|
189
|
+
const baked = options.manifest ?? loadManifest();
|
|
190
|
+
const preliminary = parseCommandLine(options.argv, baked);
|
|
191
|
+
const config = resolveConfig(preliminary.flags, { env, cwd });
|
|
192
|
+
const warnings = { ...output, quiet: flagBoolean(preliminary.flags, 'quiet') ?? false };
|
|
193
|
+
const noticeGuideHash = guideHashNotice({
|
|
194
|
+
baked: loadBakedGuide(),
|
|
195
|
+
apiUrl: config.apiUrl,
|
|
196
|
+
apiKey: config.apiKey,
|
|
197
|
+
env,
|
|
198
|
+
quiet: warnings.quiet === true,
|
|
199
|
+
warn: (code, message) => printWarning(code, message, warnings),
|
|
200
|
+
});
|
|
201
|
+
const clientFor = options.createClient ??
|
|
202
|
+
((resolved) => createApiClient(resolved, {
|
|
203
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
204
|
+
env,
|
|
205
|
+
onResponse: response => {
|
|
206
|
+
const hash = response.headers.get(GUIDE_HASH_HEADER);
|
|
207
|
+
if (hash)
|
|
208
|
+
noticeGuideHash(hash);
|
|
209
|
+
},
|
|
210
|
+
}));
|
|
211
|
+
const client = clientFor(config);
|
|
212
|
+
const manifests = openManifest({
|
|
213
|
+
baked,
|
|
214
|
+
apiUrl: config.apiUrl,
|
|
215
|
+
apiKey: config.apiKey,
|
|
216
|
+
env,
|
|
217
|
+
fetchManifest: ifNoneMatch => client.fetchManifest(ifNoneMatch),
|
|
218
|
+
warn: (code, message) => printWarning(code, message, warnings),
|
|
219
|
+
...(options.now ? { now: options.now } : {}),
|
|
220
|
+
});
|
|
221
|
+
return { config, clientFor, client, manifests };
|
|
222
|
+
}
|
|
223
|
+
function lazyGuide(options, config, env, output) {
|
|
224
|
+
let runtime;
|
|
225
|
+
return () => (runtime ??= openGuide({
|
|
226
|
+
baked: loadBakedGuide(),
|
|
227
|
+
apiUrl: config.apiUrl,
|
|
228
|
+
apiKey: config.apiKey,
|
|
229
|
+
env,
|
|
230
|
+
fetchGuide: guideFetcher({ fetch: options.fetch ?? fetch, apiUrl: config.apiUrl, apiKey: config.apiKey, env }),
|
|
231
|
+
warn: (code, message) => printWarning(code, message, output),
|
|
232
|
+
...(options.now ? { now: options.now } : {}),
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
function schemaTarget(argv, flags) {
|
|
236
|
+
const value = flagString(flags, 'schema');
|
|
237
|
+
if (value)
|
|
238
|
+
return value;
|
|
239
|
+
const index = argv.indexOf('--schema');
|
|
240
|
+
const next = index >= 0 ? argv[index + 1] : undefined;
|
|
241
|
+
if (!next?.startsWith('--') || next === '--')
|
|
242
|
+
return undefined;
|
|
243
|
+
const name = next.slice(2).split('=')[0];
|
|
244
|
+
return GLOBAL_FLAG_NAMES.has(name) ? undefined : name;
|
|
245
|
+
}
|
|
246
|
+
function unknownCommandError(name, refreshed) {
|
|
247
|
+
const message = `Unknown command "${name}". Run martini --help for the list.`;
|
|
248
|
+
if (!TOOL_NAME_PATTERN.test(name))
|
|
249
|
+
return new ArgumentError(message);
|
|
250
|
+
const checked = refreshed === 'updated' || refreshed === 'unchanged';
|
|
251
|
+
return new ArgumentError(message, {
|
|
252
|
+
hint: checked
|
|
253
|
+
? `The server's tool list for this key has no "${name}" either (martini tools compares it with this build's). To send the call by name anyway: martini call ${name} --args '{…}'.`
|
|
254
|
+
: `The server's tool list was not checked, so a tool added since this build may be missing: martini call ${name} --args '{…}' sends the call by name.`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function nonArgumentFlags(tool) {
|
|
138
258
|
const properties = tool.inputSchema.properties ?? {};
|
|
139
259
|
const ignore = new Set(GLOBAL_FLAG_NAMES);
|
|
140
260
|
for (const alias of CONTEXT_FLAG_ALIASES) {
|
|
141
261
|
if (!(alias.replace(/-id$/, 'Id') in properties))
|
|
142
262
|
ignore.add(alias);
|
|
143
263
|
}
|
|
264
|
+
return ignore;
|
|
265
|
+
}
|
|
266
|
+
async function runCommand(compound, tool, context) {
|
|
267
|
+
const { output } = context;
|
|
268
|
+
const select = selectedFields(context);
|
|
269
|
+
timeoutMs(context);
|
|
270
|
+
if (compound) {
|
|
271
|
+
const result = await compound.run(context);
|
|
272
|
+
printResult(result && typeof result === 'object'
|
|
273
|
+
? projectResult(result, select, { readOnly: compound.readOnly === true, known: [], command: compound.name }, output)
|
|
274
|
+
: result, output);
|
|
275
|
+
return EXIT.ok;
|
|
276
|
+
}
|
|
277
|
+
printResult(await runTool(tool, context, context.flags), output);
|
|
278
|
+
return EXIT.ok;
|
|
279
|
+
}
|
|
280
|
+
function projectResult(result, select, what, output) {
|
|
281
|
+
if (select.length === 0)
|
|
282
|
+
return result;
|
|
283
|
+
const projected = selectFields(result, select);
|
|
284
|
+
if (!isEmptyProjection(projected) || isEmptyProjection(result)) {
|
|
285
|
+
warnUnmatchedFields(result, select, output, what.schema);
|
|
286
|
+
return projected;
|
|
287
|
+
}
|
|
288
|
+
if (allDeclared(select, what.schema))
|
|
289
|
+
return projected;
|
|
290
|
+
const message = noFieldsMatchedMessage(what.typed ?? select, what.known);
|
|
291
|
+
if (what.readOnly)
|
|
292
|
+
throw new CliApiError(message, { code: 'NO_FIELDS_MATCHED', status: 0 });
|
|
293
|
+
printWarning('NO_FIELDS_MATCHED', `${message} ${what.command} did its work, so its whole result is printed instead; do not run it again for this.`, output);
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
async function runTool(tool, context, flags) {
|
|
144
297
|
const useContext = flagBoolean(flags, 'context') !== false;
|
|
145
298
|
const args = buildArguments(tool, flags, {
|
|
146
|
-
ignore,
|
|
299
|
+
ignore: nonArgumentFlags(tool),
|
|
147
300
|
context: useContext
|
|
148
301
|
? {
|
|
149
302
|
...(context.config.projectId ? { projectId: context.config.projectId } : {}),
|
|
@@ -152,91 +305,108 @@ async function runTool(tool, context, flags) {
|
|
|
152
305
|
}
|
|
153
306
|
: {},
|
|
154
307
|
});
|
|
308
|
+
const readOnly = tool.annotations.readOnlyHint === true;
|
|
309
|
+
const out = flagString(flags, 'out');
|
|
155
310
|
const outDir = flagString(flags, 'out-dir');
|
|
156
311
|
if (outDir) {
|
|
157
|
-
if (
|
|
312
|
+
if (out)
|
|
158
313
|
throw new ArgumentError('Use either --out or --out-dir.');
|
|
159
|
-
if (
|
|
314
|
+
if (!readOnly) {
|
|
160
315
|
throw new ArgumentError('--out-dir requires a read-only tool. Run this command without --out-dir.');
|
|
161
316
|
}
|
|
162
|
-
const result = await context.client.
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
return result;
|
|
166
|
-
const headers = [...(result.text ?? '').matchAll(/^Asset ([A-Za-z0-9_-]+) \((\d+)\/\d+\)/gm)];
|
|
317
|
+
const result = await context.client.callToolBlocks(tool.name, args, { media: 'inline' });
|
|
318
|
+
const files = mediaFiles(result.blocks, tool.name);
|
|
319
|
+
if (files.length === 0)
|
|
320
|
+
return readableBlocks(result.blocks);
|
|
167
321
|
const directory = resolve(context.cwd, outDir);
|
|
168
|
-
const files = media.map((block, index) => {
|
|
169
|
-
const header = headers[index];
|
|
170
|
-
const assetId = header?.[1] ?? (media.length === 1 ? result.assetId : undefined);
|
|
171
|
-
if (!assetId) {
|
|
172
|
-
throw new CliApiError(`Media block ${index}: missing Asset header or single-media result.assetId.`, {
|
|
173
|
-
code: 'INVALID_RESPONSE',
|
|
174
|
-
status: 0,
|
|
175
|
-
tool: tool.name,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
const extensions = {
|
|
179
|
-
'image/jpeg': 'jpg',
|
|
180
|
-
'image/png': 'png',
|
|
181
|
-
'image/webp': 'webp',
|
|
182
|
-
'audio/mpeg': 'mp3',
|
|
183
|
-
'audio/wav': 'wav',
|
|
184
|
-
};
|
|
185
|
-
if (!block.mimeType || !Object.hasOwn(extensions, block.mimeType)) {
|
|
186
|
-
throw new CliApiError(`Media block ${index}: missing or unknown mimeType (${block.mimeType ?? 'missing'}).`, {
|
|
187
|
-
code: 'INVALID_RESPONSE',
|
|
188
|
-
status: 0,
|
|
189
|
-
tool: tool.name,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
const ext = extensions[block.mimeType];
|
|
193
|
-
const name = `${header ? `${header[2]}-` : ''}${assetId.replace(/[^A-Za-z0-9_-]/g, '_')}.${ext}`;
|
|
194
|
-
return { path: join(directory, name), bytes: Buffer.from(block.data, 'base64') };
|
|
195
|
-
});
|
|
196
322
|
mkdirSync(directory, { recursive: true });
|
|
197
|
-
|
|
198
|
-
writeFileSync(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
323
|
+
for (const file of files)
|
|
324
|
+
writeFileSync(join(directory, file.name), file.bytes);
|
|
325
|
+
const receipt = {
|
|
326
|
+
dir: directory,
|
|
327
|
+
files: files.map(file => ({
|
|
328
|
+
path: join(directory, file.name),
|
|
329
|
+
mimeType: file.mimeType,
|
|
330
|
+
bytes: file.bytes.byteLength,
|
|
331
|
+
...(file.about !== undefined ? { about: file.about } : {}),
|
|
332
|
+
})),
|
|
333
|
+
...(unpairedTexts(result.blocks).length ? { notes: unpairedTexts(result.blocks) } : {}),
|
|
334
|
+
};
|
|
335
|
+
return projectResult(receipt, selectedFields(context), { readOnly: false, known: Object.keys(receipt), command: tool.name }, context.output);
|
|
206
336
|
}
|
|
207
|
-
|
|
337
|
+
const mediaOut = out !== undefined && isMediaOutPath(out);
|
|
338
|
+
const raw = flagBoolean(flags, 'raw') === true;
|
|
339
|
+
const mediaFromResult = mediaOut && !raw && !readOnly;
|
|
340
|
+
if (raw || (mediaOut && readOnly)) {
|
|
208
341
|
assertRawDestination(context.output);
|
|
209
|
-
let
|
|
342
|
+
let bytes;
|
|
210
343
|
try {
|
|
211
|
-
|
|
344
|
+
bytes = await context.client.callToolRaw(tool.name, args);
|
|
212
345
|
}
|
|
213
346
|
catch (error) {
|
|
214
347
|
if (error instanceof CliApiError && error.code === 'NOT_RAW_ELIGIBLE') {
|
|
215
|
-
throw new ArgumentError(
|
|
348
|
+
throw new ArgumentError(raw
|
|
349
|
+
? 'This result cannot use --raw: it does not hold exactly one media block. Use --out-dir <dir> to write several media files.'
|
|
350
|
+
: `--out ${out} names a media file, but ${tool.name} did not answer exactly one image or audio block. Use --out-dir <dir> for several media files, or name a .json file for the JSON result.`);
|
|
216
351
|
}
|
|
217
352
|
throw error;
|
|
218
353
|
}
|
|
219
|
-
printBytes(
|
|
354
|
+
printBytes(bytes.bytes, {
|
|
355
|
+
...context.output,
|
|
356
|
+
contentType: bytes.contentType,
|
|
357
|
+
meta: bytes.meta,
|
|
358
|
+
fields: selectedFields(context),
|
|
359
|
+
});
|
|
220
360
|
delete context.output.out;
|
|
221
361
|
context.output.quiet = true;
|
|
222
|
-
return
|
|
362
|
+
return undefined;
|
|
223
363
|
}
|
|
224
|
-
const
|
|
364
|
+
const requested = mediaFromResult ? [] : selectedFields(context);
|
|
365
|
+
const finish = (result) => (mediaFromResult ? saveMediaFromResult(result, tool, context, out) : result);
|
|
366
|
+
if (ORDERED_RESULT_TOOLS.has(tool.name)) {
|
|
367
|
+
approvalFlags(tool, flags);
|
|
368
|
+
const media = context.output.out ? 'inline' : 'stub';
|
|
369
|
+
const result = await context.client.callToolBlocks(tool.name, args, { media });
|
|
370
|
+
if (media === 'stub')
|
|
371
|
+
noteMediaStubs(result.blocks, context.output);
|
|
372
|
+
return projectResult(readableBlocks(result.blocks), requested, { readOnly: true, known: ['blocks'], command: tool.name }, context.output);
|
|
373
|
+
}
|
|
374
|
+
const select = expandMapPaths(requested, tool.outputSchema);
|
|
375
|
+
const known = Object.keys(tool.outputSchema?.properties ?? {});
|
|
225
376
|
const shouldWait = wantsWait(context, WAIT_BY_DEFAULT.has(tool.name));
|
|
226
377
|
const approve = approvalFlags(tool, flags);
|
|
227
|
-
const local = shouldWait ||
|
|
378
|
+
const local = shouldWait ||
|
|
379
|
+
approve !== null ||
|
|
380
|
+
needsLocalProjection(select) ||
|
|
381
|
+
hasUndeclaredMapStep(requested, tool.outputSchema) ||
|
|
382
|
+
!readOnly ||
|
|
383
|
+
tool.outputSchema === undefined;
|
|
384
|
+
const what = { readOnly, known, command: tool.name, typed: requested, schema: tool.outputSchema };
|
|
385
|
+
const selectIn = (value) => resolveMapPaths(value, requested, tool.outputSchema);
|
|
228
386
|
const result = approve
|
|
229
387
|
? await runWithApproval(tool, context.client, args, approve.support, approve.maxOlives === undefined ? {} : { maxOlives: approve.maxOlives })
|
|
230
388
|
: await context.client.callTool(tool.name, args, !local && select.length ? { select } : {});
|
|
231
|
-
if (!shouldWait)
|
|
232
|
-
|
|
389
|
+
if (!shouldWait) {
|
|
390
|
+
if (local)
|
|
391
|
+
return finish(projectResult(result, selectIn(result), what, context.output));
|
|
392
|
+
if (select.length && unmatchedFields(result, select).length === select.length) {
|
|
393
|
+
if (allDeclared(select, tool.outputSchema))
|
|
394
|
+
return result;
|
|
395
|
+
throw new CliApiError(noFieldsMatchedMessage(requested, known), { code: 'NO_FIELDS_MATCHED', status: 0 });
|
|
396
|
+
}
|
|
397
|
+
warnUnmatchedFields(result, select, context.output, tool.outputSchema);
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
233
400
|
const projectId = typeof args.projectId === 'string' ? args.projectId : context.config.projectId;
|
|
234
401
|
try {
|
|
235
|
-
|
|
402
|
+
const waited = await waitForToolResult(result, context, projectId);
|
|
403
|
+
return finish(projectResult(waited, selectIn(waited), what, context.output));
|
|
236
404
|
}
|
|
237
405
|
catch (error) {
|
|
238
|
-
if (error instanceof
|
|
239
|
-
|
|
406
|
+
if (error instanceof WaitTimeoutError && projectId)
|
|
407
|
+
error.project = projectId;
|
|
408
|
+
if (error instanceof CliApiError && error.code !== 'NO_FIELDS_MATCHED' && error.result === undefined) {
|
|
409
|
+
const kept = selectFields(result, selectIn(result));
|
|
240
410
|
const base = kept && typeof kept === 'object' && !Array.isArray(kept) ? kept : { result: kept };
|
|
241
411
|
error.result =
|
|
242
412
|
error instanceof WaitTimeoutError
|
|
@@ -246,6 +416,117 @@ async function runTool(tool, context, flags) {
|
|
|
246
416
|
throw error;
|
|
247
417
|
}
|
|
248
418
|
}
|
|
419
|
+
function saveMediaFromResult(result, tool, context, out) {
|
|
420
|
+
const record = result && typeof result === 'object' && !Array.isArray(result) ? result : {};
|
|
421
|
+
const media = (Array.isArray(record.media) ? record.media : []).filter((block) => block !== null && typeof block === 'object' && typeof block.data === 'string');
|
|
422
|
+
delete context.output.out;
|
|
423
|
+
if (media.length !== 1) {
|
|
424
|
+
printWarning('OUT_NOT_MEDIA', `${tool.name} answered ${media.length === 0 ? 'no image or audio block' : `${media.length} media blocks`}, so nothing was written to ${out}; its result is printed instead. ${tool.name} did its work: do not run it again for this. Name a .json file to save a result, or use --out-dir on a read-only tool for several media files.`, context.output);
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
const { media: _media, ...rest } = record;
|
|
428
|
+
printBytes(Buffer.from(media[0].data, 'base64'), {
|
|
429
|
+
...context.output,
|
|
430
|
+
out,
|
|
431
|
+
...(typeof media[0].mimeType === 'string' ? { contentType: media[0].mimeType } : {}),
|
|
432
|
+
meta: rest,
|
|
433
|
+
fields: selectedFields(context),
|
|
434
|
+
});
|
|
435
|
+
context.output.quiet = true;
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
function allDeclared(paths, schema) {
|
|
439
|
+
return schema !== undefined && paths.every(path => isDeclaredPath(path, schema));
|
|
440
|
+
}
|
|
441
|
+
function readableBlocks(blocks) {
|
|
442
|
+
return {
|
|
443
|
+
blocks: blocks.map(block => {
|
|
444
|
+
if (!isTextBlock(block))
|
|
445
|
+
return block;
|
|
446
|
+
const parsed = parseLabelledJson(block.text);
|
|
447
|
+
return parsed ? { type: 'text', ...(parsed.label ? { label: parsed.label } : {}), json: parsed.json } : block;
|
|
448
|
+
}),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function parseLabelledJson(text) {
|
|
452
|
+
const header = /^(Asset [A-Za-z0-9_-]+ \(\d+\/\d+\))\s*\n/.exec(text);
|
|
453
|
+
const body = header ? text.slice(header[0].length) : text;
|
|
454
|
+
if (!body.trimStart().startsWith('{'))
|
|
455
|
+
return null;
|
|
456
|
+
try {
|
|
457
|
+
const json = JSON.parse(body);
|
|
458
|
+
if (!json || typeof json !== 'object' || Array.isArray(json))
|
|
459
|
+
return null;
|
|
460
|
+
return { ...(header ? { label: header[1] } : {}), json: json };
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function unpairedTexts(blocks) {
|
|
467
|
+
const notes = [];
|
|
468
|
+
blocks.forEach((block, index) => {
|
|
469
|
+
if (!isTextBlock(block))
|
|
470
|
+
return;
|
|
471
|
+
const previous = blocks[index - 1];
|
|
472
|
+
const next = blocks[index + 1];
|
|
473
|
+
const paired = (next !== undefined && isMediaBlock(next)) ||
|
|
474
|
+
(previous !== undefined && isMediaBlock(previous) && (index < 2 || !isTextBlock(blocks[index - 2])));
|
|
475
|
+
if (!paired)
|
|
476
|
+
notes.push(block.text);
|
|
477
|
+
});
|
|
478
|
+
return notes;
|
|
479
|
+
}
|
|
480
|
+
function mediaFiles(blocks, toolName) {
|
|
481
|
+
const invalid = (message) => new CliApiError(message, { code: 'INVALID_RESPONSE', status: 0, tool: toolName });
|
|
482
|
+
const taken = new Set();
|
|
483
|
+
const files = [];
|
|
484
|
+
let text;
|
|
485
|
+
blocks.forEach((block, index) => {
|
|
486
|
+
if (isTextBlock(block)) {
|
|
487
|
+
text = block.text;
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (!isMediaBlock(block))
|
|
491
|
+
return;
|
|
492
|
+
const mimeType = typeof block.mimeType === 'string' ? block.mimeType : undefined;
|
|
493
|
+
if (!mimeType || !Object.hasOwn(MEDIA_EXTENSIONS, mimeType)) {
|
|
494
|
+
throw invalid(`Block ${index} (${block.type}): missing or unknown mimeType (${mimeType ?? 'missing'}).`);
|
|
495
|
+
}
|
|
496
|
+
if (!('data' in block) || typeof block.data !== 'string')
|
|
497
|
+
throw invalid(`Block ${index} (${block.type}): no data.`);
|
|
498
|
+
const before = stemFrom(text);
|
|
499
|
+
const describing = before ? text : textAfter(blocks, index);
|
|
500
|
+
const stem = before ?? stemFrom(describing) ?? `block-${index}`;
|
|
501
|
+
let name = `${stem}.${MEDIA_EXTENSIONS[mimeType]}`;
|
|
502
|
+
for (let copy = 2; taken.has(name); copy++)
|
|
503
|
+
name = `${stem}-${copy}.${MEDIA_EXTENSIONS[mimeType]}`;
|
|
504
|
+
taken.add(name);
|
|
505
|
+
const about = describing === undefined ? null : parseLabelledJson(describing);
|
|
506
|
+
files.push({ name, mimeType, bytes: Buffer.from(block.data, 'base64'), ...(about ? { about: about.json } : {}) });
|
|
507
|
+
});
|
|
508
|
+
return files;
|
|
509
|
+
}
|
|
510
|
+
function stemFrom(text) {
|
|
511
|
+
if (text === undefined)
|
|
512
|
+
return null;
|
|
513
|
+
const header = /^Asset ([A-Za-z0-9_-]+) \((\d+)\/\d+\)/.exec(text);
|
|
514
|
+
if (header)
|
|
515
|
+
return `${header[2]}-${header[1]}`;
|
|
516
|
+
try {
|
|
517
|
+
const parsed = JSON.parse(text);
|
|
518
|
+
const assetId = parsed && typeof parsed === 'object' ? parsed.assetId : undefined;
|
|
519
|
+
if (typeof assetId === 'string' && assetId)
|
|
520
|
+
return assetId.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
function textAfter(blocks, index) {
|
|
527
|
+
const next = blocks[index + 1];
|
|
528
|
+
return next && isTextBlock(next) ? next.text : undefined;
|
|
529
|
+
}
|
|
249
530
|
async function waitForToolResult(result, context, projectId) {
|
|
250
531
|
if (!result || typeof result !== 'object')
|
|
251
532
|
return result;
|
|
@@ -253,25 +534,32 @@ async function waitForToolResult(result, context, projectId) {
|
|
|
253
534
|
if (record.approvalNeeded === true || record.generated === false)
|
|
254
535
|
return result;
|
|
255
536
|
const waitOptions = { timeoutMs: timeoutMs(context), now: context.now, sleep: context.sleep };
|
|
537
|
+
const settled = (waited) => {
|
|
538
|
+
const failure = notCompleted(waited);
|
|
539
|
+
if (!failure)
|
|
540
|
+
return { ...record, waited };
|
|
541
|
+
failure.result = { ...record, waited };
|
|
542
|
+
throw failure;
|
|
543
|
+
};
|
|
256
544
|
if (typeof record.runId === 'string') {
|
|
257
|
-
return
|
|
545
|
+
return settled(handleOutcome('run', await waitForRun(context.client, record.runId, waitOptions)));
|
|
258
546
|
}
|
|
259
547
|
if (typeof record.renderId === 'string' || typeof record.exportId === 'string') {
|
|
260
548
|
const ids = {
|
|
261
549
|
...(typeof record.renderId === 'string' ? { renderId: record.renderId } : {}),
|
|
262
550
|
...(typeof record.exportId === 'string' ? { exportId: record.exportId } : {}),
|
|
263
551
|
};
|
|
264
|
-
return
|
|
552
|
+
return settled(handleOutcome('render', await waitForDelivery(context.client, ids, waitOptions)));
|
|
265
553
|
}
|
|
266
554
|
if (typeof record.jobId === 'string') {
|
|
267
|
-
return
|
|
555
|
+
return settled(handleOutcome('job', await waitForJob(context.client, record.jobId, waitOptions)));
|
|
268
556
|
}
|
|
269
557
|
if (!projectId)
|
|
270
558
|
return result;
|
|
271
559
|
const ids = producedAssetIds(result);
|
|
272
560
|
if (ids.length === 0)
|
|
273
561
|
return result;
|
|
274
|
-
return
|
|
562
|
+
return settled(assetsOutcome(await waitForAssets(context.client, projectId, ids, waitOptions)));
|
|
275
563
|
}
|
|
276
564
|
function strayPositionalError(tool, tokens) {
|
|
277
565
|
const list = flagSpecs(tool).find(spec => spec.kind === 'array');
|
|
@@ -295,7 +583,4 @@ function approvalFlags(tool, flags) {
|
|
|
295
583
|
throw new ArgumentError('--max-olives must be a number of olives.');
|
|
296
584
|
return { support, maxOlives };
|
|
297
585
|
}
|
|
298
|
-
function knownResultFields(tool) {
|
|
299
|
-
return Object.keys(tool.outputSchema?.properties ?? {});
|
|
300
|
-
}
|
|
301
586
|
//# sourceMappingURL=cli.js.map
|