@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/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, explainInvalidArguments, 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,245 @@ 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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
92
|
+
session = openSession(options, env, cwd, output);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return printError(error, output);
|
|
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
|
+
const target = schemaTarget(options.argv, parsed.flags);
|
|
134
|
+
const borrowed = compound ? compoundSchema(compound.name, target, manifest) : undefined;
|
|
135
|
+
if (compound && !borrowed) {
|
|
136
|
+
throw new ArgumentError(`--schema describes a tool's arguments; ${compound.name} is a compound command (martini ${compound.name} --help lists its flags).`);
|
|
137
|
+
}
|
|
138
|
+
const schema = borrowed ?? argumentSchema(tool, target);
|
|
139
|
+
printResult(flagBoolean(parsed.flags, 'keys') === true
|
|
140
|
+
? `${schemaKeyLines(schema, target ? `--${target.replace(/^--/, '')}` : `${tool.name} arguments`).join('\n')}\n`
|
|
141
|
+
: `${JSON.stringify(schema, null, 2)}\n`, output);
|
|
142
|
+
return EXIT.ok;
|
|
143
|
+
}
|
|
144
|
+
if (parsed.flags.has('keys'))
|
|
145
|
+
throw new ArgumentError('--keys goes with --schema: martini <tool> --schema <flag> --keys.');
|
|
146
|
+
if (parsed.flags.has('all'))
|
|
147
|
+
throw new ArgumentError('--all goes with --help: martini <tool> --help --all.');
|
|
148
|
+
if (compound)
|
|
149
|
+
assertKnownFlags(compound, parsed.flags, GLOBAL_FLAGS_FOR_COMPOUNDS);
|
|
150
|
+
else {
|
|
151
|
+
if (refreshed === undefined && unknownToolFlags(tool, parsed.flags, nonArgumentFlags(tool)).length > 0) {
|
|
152
|
+
refreshed = await manifests.refresh();
|
|
153
|
+
if (refreshed === 'updated')
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (parsed.positionals.length > 0)
|
|
157
|
+
throw strayPositionalError(tool, parsed.positionals);
|
|
158
|
+
}
|
|
159
|
+
for (const warning of config.warnings)
|
|
160
|
+
printWarning('CONFIG_WARNING', warning, output);
|
|
161
|
+
const out = flagString(parsed.flags, 'out');
|
|
162
|
+
if (out)
|
|
163
|
+
output.out = resolve(cwd, out);
|
|
164
|
+
output.inRun = Boolean(config.runId);
|
|
165
|
+
output.quiet = flagBoolean(parsed.flags, 'quiet') ?? false;
|
|
166
|
+
return await runCommand(compound, tool, {
|
|
167
|
+
client,
|
|
168
|
+
clientFor,
|
|
169
|
+
config,
|
|
170
|
+
manifest,
|
|
171
|
+
manifests,
|
|
172
|
+
guide: lazyGuide(options, config, env, output),
|
|
173
|
+
flags: parsed.flags,
|
|
174
|
+
positionals: parsed.positionals,
|
|
175
|
+
output,
|
|
176
|
+
env,
|
|
177
|
+
cwd,
|
|
178
|
+
now: options.now ?? Date.now,
|
|
179
|
+
sleep: options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
|
|
180
|
+
});
|
|
85
181
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (error instanceof CliApiError && error.result !== undefined)
|
|
184
|
+
printResult(error.result, output);
|
|
185
|
+
return printError(error, output);
|
|
89
186
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
manifest,
|
|
109
|
-
flags: parsed.flags,
|
|
110
|
-
positionals: parsed.positionals,
|
|
111
|
-
output,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function openSession(options, env, cwd, output) {
|
|
190
|
+
const baked = options.manifest ?? loadManifest();
|
|
191
|
+
const preliminary = parseCommandLine(options.argv, baked);
|
|
192
|
+
const config = resolveConfig(preliminary.flags, { env, cwd });
|
|
193
|
+
const warnings = { ...output, quiet: flagBoolean(preliminary.flags, 'quiet') ?? false };
|
|
194
|
+
const noticeGuideHash = guideHashNotice({
|
|
195
|
+
baked: loadBakedGuide(),
|
|
196
|
+
apiUrl: config.apiUrl,
|
|
197
|
+
apiKey: config.apiKey,
|
|
198
|
+
env,
|
|
199
|
+
quiet: warnings.quiet === true,
|
|
200
|
+
warn: (code, message) => printWarning(code, message, warnings),
|
|
201
|
+
});
|
|
202
|
+
const clientFor = options.createClient ??
|
|
203
|
+
((resolved) => createApiClient(resolved, {
|
|
204
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
112
205
|
env,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
206
|
+
onResponse: response => {
|
|
207
|
+
const hash = response.headers.get(GUIDE_HASH_HEADER);
|
|
208
|
+
if (hash)
|
|
209
|
+
noticeGuideHash(hash);
|
|
210
|
+
},
|
|
211
|
+
}));
|
|
212
|
+
const client = clientFor(config);
|
|
213
|
+
const manifests = openManifest({
|
|
214
|
+
baked,
|
|
215
|
+
apiUrl: config.apiUrl,
|
|
216
|
+
apiKey: config.apiKey,
|
|
217
|
+
env,
|
|
218
|
+
fetchManifest: ifNoneMatch => client.fetchManifest(ifNoneMatch),
|
|
219
|
+
warn: (code, message) => printWarning(code, message, warnings),
|
|
220
|
+
...(options.now ? { now: options.now } : {}),
|
|
221
|
+
});
|
|
222
|
+
return { config, clientFor, client, manifests };
|
|
223
|
+
}
|
|
224
|
+
function lazyGuide(options, config, env, output) {
|
|
225
|
+
let runtime;
|
|
226
|
+
return () => (runtime ??= openGuide({
|
|
227
|
+
baked: loadBakedGuide(),
|
|
228
|
+
apiUrl: config.apiUrl,
|
|
229
|
+
apiKey: config.apiKey,
|
|
230
|
+
env,
|
|
231
|
+
fetchGuide: guideFetcher({ fetch: options.fetch ?? fetch, apiUrl: config.apiUrl, apiKey: config.apiKey, env }),
|
|
232
|
+
warn: (code, message) => printWarning(code, message, output),
|
|
233
|
+
...(options.now ? { now: options.now } : {}),
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
function schemaTarget(argv, flags) {
|
|
237
|
+
const value = flagString(flags, 'schema');
|
|
238
|
+
if (value)
|
|
239
|
+
return value;
|
|
240
|
+
const index = argv.indexOf('--schema');
|
|
241
|
+
const next = index >= 0 ? argv[index + 1] : undefined;
|
|
242
|
+
if (!next?.startsWith('--') || next === '--')
|
|
243
|
+
return undefined;
|
|
244
|
+
const name = next.slice(2).split('=')[0];
|
|
245
|
+
return GLOBAL_FLAG_NAMES.has(name) ? undefined : name;
|
|
246
|
+
}
|
|
247
|
+
const COMPOUND_SCHEMAS = {
|
|
248
|
+
upload: { lineage: { tool: 'upload_assets_prepare', path: 'files.lineage' } },
|
|
249
|
+
};
|
|
250
|
+
function compoundSchema(command, target, manifest) {
|
|
251
|
+
const flag = target?.replace(/^--/, '');
|
|
252
|
+
const source = flag ? COMPOUND_SCHEMAS[command]?.[flag] : undefined;
|
|
253
|
+
if (!source)
|
|
254
|
+
return undefined;
|
|
255
|
+
const tool = findTool(source.tool, manifest);
|
|
256
|
+
try {
|
|
257
|
+
if (tool)
|
|
258
|
+
return argumentSchema(tool, source.path);
|
|
130
259
|
}
|
|
131
260
|
catch (error) {
|
|
132
|
-
if (error instanceof
|
|
133
|
-
|
|
134
|
-
return printError(error, output);
|
|
261
|
+
if (!(error instanceof ArgumentError))
|
|
262
|
+
throw error;
|
|
135
263
|
}
|
|
264
|
+
throw new ArgumentError(`This server's ${source.tool} takes no ${flag} block, so martini ${command} sends none (its result says lineage: "none").`);
|
|
136
265
|
}
|
|
137
|
-
|
|
266
|
+
function unknownCommandError(name, refreshed) {
|
|
267
|
+
const message = `Unknown command "${name}". Run martini --help for the list.`;
|
|
268
|
+
if (!TOOL_NAME_PATTERN.test(name))
|
|
269
|
+
return new ArgumentError(message);
|
|
270
|
+
const checked = refreshed === 'updated' || refreshed === 'unchanged';
|
|
271
|
+
return new ArgumentError(message, {
|
|
272
|
+
hint: checked
|
|
273
|
+
? `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 '{…}' --context (--context adds this directory's projectId; call fills nothing else).`
|
|
274
|
+
: `The server's tool list was not checked, so a tool added since this build may be missing: martini call ${name} --args '{…}' --context sends the call by name (--context adds this directory's projectId; call fills nothing else).`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function nonArgumentFlags(tool) {
|
|
138
278
|
const properties = tool.inputSchema.properties ?? {};
|
|
139
279
|
const ignore = new Set(GLOBAL_FLAG_NAMES);
|
|
140
280
|
for (const alias of CONTEXT_FLAG_ALIASES) {
|
|
141
281
|
if (!(alias.replace(/-id$/, 'Id') in properties))
|
|
142
282
|
ignore.add(alias);
|
|
143
283
|
}
|
|
284
|
+
return ignore;
|
|
285
|
+
}
|
|
286
|
+
async function runCommand(compound, tool, context) {
|
|
287
|
+
const { output } = context;
|
|
288
|
+
const select = selectedFields(context);
|
|
289
|
+
timeoutMs(context);
|
|
290
|
+
if (compound) {
|
|
291
|
+
const result = await compound.run(context);
|
|
292
|
+
printResult(result && typeof result === 'object'
|
|
293
|
+
? projectResult(result, select, { readOnly: compound.readOnly === true, known: [], command: compound.name }, output)
|
|
294
|
+
: result, output);
|
|
295
|
+
return EXIT.ok;
|
|
296
|
+
}
|
|
297
|
+
printResult(await runTool(tool, context, context.flags), output);
|
|
298
|
+
return EXIT.ok;
|
|
299
|
+
}
|
|
300
|
+
function projectResult(result, select, what, output) {
|
|
301
|
+
if (select.length === 0)
|
|
302
|
+
return result;
|
|
303
|
+
const projected = selectFields(result, select);
|
|
304
|
+
if (!isEmptyProjection(projected) || isEmptyProjection(result)) {
|
|
305
|
+
warnUnmatchedFields(result, select, output, what.schema);
|
|
306
|
+
return projected;
|
|
307
|
+
}
|
|
308
|
+
if (allDeclared(select, what.schema))
|
|
309
|
+
return projected;
|
|
310
|
+
const message = noFieldsMatchedMessage(what.typed ?? select, what.known, result, what.schema);
|
|
311
|
+
if (what.readOnly)
|
|
312
|
+
throw new CliApiError(message, { code: 'NO_FIELDS_MATCHED', status: 0 });
|
|
313
|
+
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);
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
async function runTool(tool, context, flags) {
|
|
144
317
|
const useContext = flagBoolean(flags, 'context') !== false;
|
|
145
318
|
const args = buildArguments(tool, flags, {
|
|
146
|
-
ignore,
|
|
319
|
+
ignore: nonArgumentFlags(tool),
|
|
147
320
|
context: useContext
|
|
148
321
|
? {
|
|
149
322
|
...(context.config.projectId ? { projectId: context.config.projectId } : {}),
|
|
@@ -152,91 +325,134 @@ async function runTool(tool, context, flags) {
|
|
|
152
325
|
}
|
|
153
326
|
: {},
|
|
154
327
|
});
|
|
328
|
+
try {
|
|
329
|
+
return await runToolWith(tool, context, flags, args);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (error instanceof CliApiError && error.code === 'INVALID_ARGUMENTS') {
|
|
333
|
+
const message = explainInvalidArguments(error.message, args, tool.inputSchema);
|
|
334
|
+
if (message !== error.message) {
|
|
335
|
+
const explained = new CliApiError(message, {
|
|
336
|
+
code: error.code,
|
|
337
|
+
status: error.status,
|
|
338
|
+
tool: error.tool,
|
|
339
|
+
...(error.hint ? { hint: error.hint } : {}),
|
|
340
|
+
});
|
|
341
|
+
if (error.result !== undefined)
|
|
342
|
+
explained.result = error.result;
|
|
343
|
+
throw explained;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function runToolWith(tool, context, flags, args) {
|
|
350
|
+
const readOnly = tool.annotations.readOnlyHint === true;
|
|
351
|
+
const out = context.output.out ?? flagString(flags, 'out');
|
|
155
352
|
const outDir = flagString(flags, 'out-dir');
|
|
156
353
|
if (outDir) {
|
|
157
|
-
if (
|
|
354
|
+
if (out)
|
|
158
355
|
throw new ArgumentError('Use either --out or --out-dir.');
|
|
159
|
-
if (
|
|
356
|
+
if (!readOnly) {
|
|
160
357
|
throw new ArgumentError('--out-dir requires a read-only tool. Run this command without --out-dir.');
|
|
161
358
|
}
|
|
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)];
|
|
359
|
+
const result = await context.client.callToolBlocks(tool.name, args, { media: 'inline' });
|
|
360
|
+
const files = mediaFiles(result.blocks, tool.name);
|
|
361
|
+
if (files.length === 0)
|
|
362
|
+
return readableBlocks(result.blocks);
|
|
167
363
|
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
364
|
mkdirSync(directory, { recursive: true });
|
|
197
|
-
|
|
198
|
-
writeFileSync(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
365
|
+
for (const file of files)
|
|
366
|
+
writeFileSync(join(directory, file.name), file.bytes);
|
|
367
|
+
const receipt = {
|
|
368
|
+
dir: directory,
|
|
369
|
+
files: files.map(file => ({
|
|
370
|
+
path: join(directory, file.name),
|
|
371
|
+
mimeType: file.mimeType,
|
|
372
|
+
bytes: file.bytes.byteLength,
|
|
373
|
+
...(file.about !== undefined ? { about: file.about } : {}),
|
|
374
|
+
})),
|
|
375
|
+
...(unpairedTexts(result.blocks).length ? { notes: unpairedTexts(result.blocks) } : {}),
|
|
376
|
+
};
|
|
377
|
+
return projectResult(receipt, selectedFields(context), { readOnly: false, known: Object.keys(receipt), command: tool.name }, context.output);
|
|
206
378
|
}
|
|
207
|
-
|
|
379
|
+
const mediaOut = out !== undefined && isMediaOutPath(out);
|
|
380
|
+
const raw = flagBoolean(flags, 'raw') === true;
|
|
381
|
+
const mediaFromResult = mediaOut && !raw && !readOnly;
|
|
382
|
+
if (raw || (mediaOut && readOnly)) {
|
|
208
383
|
assertRawDestination(context.output);
|
|
209
|
-
let
|
|
384
|
+
let bytes;
|
|
210
385
|
try {
|
|
211
|
-
|
|
386
|
+
bytes = await context.client.callToolRaw(tool.name, args);
|
|
212
387
|
}
|
|
213
388
|
catch (error) {
|
|
214
389
|
if (error instanceof CliApiError && error.code === 'NOT_RAW_ELIGIBLE') {
|
|
215
|
-
throw new ArgumentError(
|
|
390
|
+
throw new ArgumentError(raw
|
|
391
|
+
? 'This result cannot use --raw: it does not hold exactly one media block. Use --out-dir <dir> to write several media files.'
|
|
392
|
+
: `--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
393
|
}
|
|
217
394
|
throw error;
|
|
218
395
|
}
|
|
219
|
-
printBytes(
|
|
396
|
+
printBytes(bytes.bytes, {
|
|
397
|
+
...context.output,
|
|
398
|
+
contentType: bytes.contentType,
|
|
399
|
+
meta: bytes.meta,
|
|
400
|
+
fields: selectedFields(context),
|
|
401
|
+
wrote: !readOnly,
|
|
402
|
+
});
|
|
220
403
|
delete context.output.out;
|
|
221
404
|
context.output.quiet = true;
|
|
222
|
-
return
|
|
405
|
+
return undefined;
|
|
223
406
|
}
|
|
224
|
-
const
|
|
407
|
+
const requested = mediaFromResult ? [] : selectedFields(context);
|
|
408
|
+
const finish = (result) => (mediaFromResult ? saveMediaFromResult(result, tool, context, out) : result);
|
|
409
|
+
if (ORDERED_RESULT_TOOLS.has(tool.name)) {
|
|
410
|
+
approvalFlags(tool, flags);
|
|
411
|
+
const media = context.output.out ? 'inline' : 'stub';
|
|
412
|
+
const result = await context.client.callToolBlocks(tool.name, args, { media });
|
|
413
|
+
if (media === 'stub')
|
|
414
|
+
noteMediaStubs(result.blocks, context.output);
|
|
415
|
+
return projectResult(readableBlocks(result.blocks), requested, { readOnly: true, known: ['blocks'], command: tool.name }, context.output);
|
|
416
|
+
}
|
|
417
|
+
const select = expandMapPaths(requested, tool.outputSchema);
|
|
418
|
+
const known = Object.keys(tool.outputSchema?.properties ?? {});
|
|
225
419
|
const shouldWait = wantsWait(context, WAIT_BY_DEFAULT.has(tool.name));
|
|
226
420
|
const approve = approvalFlags(tool, flags);
|
|
227
|
-
const local = shouldWait ||
|
|
421
|
+
const local = shouldWait ||
|
|
422
|
+
approve !== null ||
|
|
423
|
+
needsLocalProjection(select) ||
|
|
424
|
+
hasUndeclaredMapStep(requested, tool.outputSchema) ||
|
|
425
|
+
!readOnly ||
|
|
426
|
+
tool.outputSchema === undefined;
|
|
427
|
+
const what = { readOnly, known, command: tool.name, typed: requested, schema: tool.outputSchema };
|
|
428
|
+
const selectIn = (value) => resolveMapPaths(value, requested, tool.outputSchema);
|
|
228
429
|
const result = approve
|
|
229
430
|
? await runWithApproval(tool, context.client, args, approve.support, approve.maxOlives === undefined ? {} : { maxOlives: approve.maxOlives })
|
|
230
431
|
: await context.client.callTool(tool.name, args, !local && select.length ? { select } : {});
|
|
231
|
-
if (!shouldWait)
|
|
232
|
-
|
|
432
|
+
if (!shouldWait) {
|
|
433
|
+
if (local)
|
|
434
|
+
return finish(projectResult(result, selectIn(result), what, context.output));
|
|
435
|
+
if (select.length && unmatchedFields(result, select).length === select.length) {
|
|
436
|
+
if (allDeclared(select, tool.outputSchema))
|
|
437
|
+
return result;
|
|
438
|
+
throw new CliApiError(noFieldsMatchedMessage(requested, known, undefined, tool.outputSchema), {
|
|
439
|
+
code: 'NO_FIELDS_MATCHED',
|
|
440
|
+
status: 0,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
warnUnmatchedFields(result, select, context.output, tool.outputSchema);
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
233
446
|
const projectId = typeof args.projectId === 'string' ? args.projectId : context.config.projectId;
|
|
234
447
|
try {
|
|
235
|
-
|
|
448
|
+
const waited = await waitForToolResult(result, context, projectId);
|
|
449
|
+
return finish(projectResult(waited, selectIn(waited), what, context.output));
|
|
236
450
|
}
|
|
237
451
|
catch (error) {
|
|
238
|
-
if (error instanceof
|
|
239
|
-
|
|
452
|
+
if (error instanceof WaitTimeoutError && projectId)
|
|
453
|
+
error.project = projectId;
|
|
454
|
+
if (error instanceof CliApiError && error.code !== 'NO_FIELDS_MATCHED' && error.result === undefined) {
|
|
455
|
+
const kept = selectFields(result, selectIn(result));
|
|
240
456
|
const base = kept && typeof kept === 'object' && !Array.isArray(kept) ? kept : { result: kept };
|
|
241
457
|
error.result =
|
|
242
458
|
error instanceof WaitTimeoutError
|
|
@@ -246,6 +462,131 @@ async function runTool(tool, context, flags) {
|
|
|
246
462
|
throw error;
|
|
247
463
|
}
|
|
248
464
|
}
|
|
465
|
+
function saveMediaFromResult(result, tool, context, out) {
|
|
466
|
+
const record = result && typeof result === 'object' && !Array.isArray(result) ? result : {};
|
|
467
|
+
const media = (Array.isArray(record.media) ? record.media : []).filter((block) => block !== null && typeof block === 'object' && typeof block.data === 'string');
|
|
468
|
+
delete context.output.out;
|
|
469
|
+
if (media.length !== 1) {
|
|
470
|
+
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);
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
const { media: _media, ...rest } = record;
|
|
474
|
+
printBytes(Buffer.from(media[0].data, 'base64'), {
|
|
475
|
+
...context.output,
|
|
476
|
+
out,
|
|
477
|
+
...(typeof media[0].mimeType === 'string' ? { contentType: media[0].mimeType } : {}),
|
|
478
|
+
meta: rest,
|
|
479
|
+
fields: selectedFields(context),
|
|
480
|
+
wrote: true,
|
|
481
|
+
});
|
|
482
|
+
context.output.quiet = true;
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
function allDeclared(paths, schema) {
|
|
486
|
+
return schema !== undefined && paths.every(path => isDeclaredPath(path, schema));
|
|
487
|
+
}
|
|
488
|
+
function readableBlocks(blocks) {
|
|
489
|
+
return {
|
|
490
|
+
blocks: blocks.map(block => {
|
|
491
|
+
if (!isTextBlock(block))
|
|
492
|
+
return block;
|
|
493
|
+
const parsed = parseLabelledJson(block.text);
|
|
494
|
+
return parsed ? { type: 'text', ...(parsed.label ? { label: parsed.label } : {}), json: parsed.json } : block;
|
|
495
|
+
}),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function parseLabelledJson(text) {
|
|
499
|
+
const header = /^(Asset [A-Za-z0-9_-]+ \(\d+\/\d+\))\s*\n/.exec(text);
|
|
500
|
+
const body = header ? text.slice(header[0].length) : text;
|
|
501
|
+
if (!body.trimStart().startsWith('{'))
|
|
502
|
+
return null;
|
|
503
|
+
try {
|
|
504
|
+
const json = JSON.parse(body);
|
|
505
|
+
if (!json || typeof json !== 'object' || Array.isArray(json))
|
|
506
|
+
return null;
|
|
507
|
+
return { ...(header ? { label: header[1] } : {}), json: json };
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function unpairedTexts(blocks) {
|
|
514
|
+
const notes = [];
|
|
515
|
+
blocks.forEach((block, index) => {
|
|
516
|
+
if (!isTextBlock(block))
|
|
517
|
+
return;
|
|
518
|
+
const previous = blocks[index - 1];
|
|
519
|
+
const next = blocks[index + 1];
|
|
520
|
+
const paired = (next !== undefined && isMediaBlock(next)) ||
|
|
521
|
+
(previous !== undefined && isMediaBlock(previous) && (index < 2 || !isTextBlock(blocks[index - 2])));
|
|
522
|
+
if (!paired)
|
|
523
|
+
notes.push(block.text);
|
|
524
|
+
});
|
|
525
|
+
return notes;
|
|
526
|
+
}
|
|
527
|
+
function mediaFiles(blocks, toolName) {
|
|
528
|
+
const invalid = (message) => new CliApiError(message, { code: 'INVALID_RESPONSE', status: 0, tool: toolName });
|
|
529
|
+
const taken = new Set();
|
|
530
|
+
const files = [];
|
|
531
|
+
let text;
|
|
532
|
+
blocks.forEach((block, index) => {
|
|
533
|
+
if (isTextBlock(block)) {
|
|
534
|
+
text = block.text;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (!isMediaBlock(block))
|
|
538
|
+
return;
|
|
539
|
+
const mimeType = typeof block.mimeType === 'string' ? block.mimeType : undefined;
|
|
540
|
+
if (!mimeType || !Object.hasOwn(MEDIA_EXTENSIONS, mimeType)) {
|
|
541
|
+
throw invalid(`Block ${index} (${block.type}): missing or unknown mimeType (${mimeType ?? 'missing'}).`);
|
|
542
|
+
}
|
|
543
|
+
if (!('data' in block) || typeof block.data !== 'string')
|
|
544
|
+
throw invalid(`Block ${index} (${block.type}): no data.`);
|
|
545
|
+
const before = stemFrom(text);
|
|
546
|
+
const describing = before ? text : textAfter(blocks, index);
|
|
547
|
+
const stem = before ?? stemFrom(describing) ?? `block-${index}`;
|
|
548
|
+
let name = `${stem}.${MEDIA_EXTENSIONS[mimeType]}`;
|
|
549
|
+
for (let copy = 2; taken.has(name); copy++)
|
|
550
|
+
name = `${stem}-${copy}.${MEDIA_EXTENSIONS[mimeType]}`;
|
|
551
|
+
taken.add(name);
|
|
552
|
+
const about = describing === undefined ? null : parseLabelledJson(describing);
|
|
553
|
+
files.push({ name, mimeType, bytes: Buffer.from(block.data, 'base64'), ...(about ? { about: about.json } : {}) });
|
|
554
|
+
});
|
|
555
|
+
return files;
|
|
556
|
+
}
|
|
557
|
+
function stemFrom(text) {
|
|
558
|
+
if (text === undefined)
|
|
559
|
+
return null;
|
|
560
|
+
const header = /^Asset ([A-Za-z0-9_-]+) \((\d+)\/\d+\)/.exec(text);
|
|
561
|
+
if (header) {
|
|
562
|
+
const name = nameSlug(parseLabelledJson(text)?.json.displayName);
|
|
563
|
+
return name ? `${header[2]}-${name}-${header[1]}` : `${header[2]}-${header[1]}`;
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
const parsed = JSON.parse(text);
|
|
567
|
+
const assetId = parsed && typeof parsed === 'object' ? parsed.assetId : undefined;
|
|
568
|
+
if (typeof assetId === 'string' && assetId)
|
|
569
|
+
return assetId.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
function nameSlug(value) {
|
|
576
|
+
if (typeof value !== 'string')
|
|
577
|
+
return '';
|
|
578
|
+
return value
|
|
579
|
+
.normalize('NFKD')
|
|
580
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
581
|
+
.toLowerCase()
|
|
582
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
583
|
+
.slice(0, 40)
|
|
584
|
+
.replace(/^-+|-+$/g, '');
|
|
585
|
+
}
|
|
586
|
+
function textAfter(blocks, index) {
|
|
587
|
+
const next = blocks[index + 1];
|
|
588
|
+
return next && isTextBlock(next) ? next.text : undefined;
|
|
589
|
+
}
|
|
249
590
|
async function waitForToolResult(result, context, projectId) {
|
|
250
591
|
if (!result || typeof result !== 'object')
|
|
251
592
|
return result;
|
|
@@ -253,25 +594,32 @@ async function waitForToolResult(result, context, projectId) {
|
|
|
253
594
|
if (record.approvalNeeded === true || record.generated === false)
|
|
254
595
|
return result;
|
|
255
596
|
const waitOptions = { timeoutMs: timeoutMs(context), now: context.now, sleep: context.sleep };
|
|
597
|
+
const settled = (waited) => {
|
|
598
|
+
const failure = notCompleted(waited);
|
|
599
|
+
if (!failure)
|
|
600
|
+
return { ...record, waited };
|
|
601
|
+
failure.result = { ...record, waited };
|
|
602
|
+
throw failure;
|
|
603
|
+
};
|
|
256
604
|
if (typeof record.runId === 'string') {
|
|
257
|
-
return
|
|
605
|
+
return settled(handleOutcome('run', await waitForRun(context.client, record.runId, waitOptions)));
|
|
258
606
|
}
|
|
259
607
|
if (typeof record.renderId === 'string' || typeof record.exportId === 'string') {
|
|
260
608
|
const ids = {
|
|
261
609
|
...(typeof record.renderId === 'string' ? { renderId: record.renderId } : {}),
|
|
262
610
|
...(typeof record.exportId === 'string' ? { exportId: record.exportId } : {}),
|
|
263
611
|
};
|
|
264
|
-
return
|
|
612
|
+
return settled(handleOutcome('render', await waitForDelivery(context.client, ids, waitOptions)));
|
|
265
613
|
}
|
|
266
614
|
if (typeof record.jobId === 'string') {
|
|
267
|
-
return
|
|
615
|
+
return settled(handleOutcome('job', await waitForJob(context.client, record.jobId, waitOptions)));
|
|
268
616
|
}
|
|
269
617
|
if (!projectId)
|
|
270
618
|
return result;
|
|
271
619
|
const ids = producedAssetIds(result);
|
|
272
620
|
if (ids.length === 0)
|
|
273
621
|
return result;
|
|
274
|
-
return
|
|
622
|
+
return settled(assetsOutcome(await waitForAssets(context.client, projectId, ids, waitOptions)));
|
|
275
623
|
}
|
|
276
624
|
function strayPositionalError(tool, tokens) {
|
|
277
625
|
const list = flagSpecs(tool).find(spec => spec.kind === 'array');
|
|
@@ -295,7 +643,4 @@ function approvalFlags(tool, flags) {
|
|
|
295
643
|
throw new ArgumentError('--max-olives must be a number of olives.');
|
|
296
644
|
return { support, maxOlives };
|
|
297
645
|
}
|
|
298
|
-
function knownResultFields(tool) {
|
|
299
|
-
return Object.keys(tool.outputSchema?.properties ?? {});
|
|
300
|
-
}
|
|
301
646
|
//# sourceMappingURL=cli.js.map
|