@martini-film/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +173 -56
  2. package/dist/api.js +69 -2
  3. package/dist/api.js.map +1 -1
  4. package/dist/approve.js +2 -1
  5. package/dist/approve.js.map +1 -1
  6. package/dist/args.js +31 -6
  7. package/dist/args.js.map +1 -1
  8. package/dist/cli.js +457 -110
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/assets.js +297 -45
  11. package/dist/commands/assets.js.map +1 -1
  12. package/dist/commands/call.js +99 -0
  13. package/dist/commands/call.js.map +1 -0
  14. package/dist/commands/context.js +16 -3
  15. package/dist/commands/context.js.map +1 -1
  16. package/dist/commands/files.js +208 -83
  17. package/dist/commands/files.js.map +1 -1
  18. package/dist/commands/guide.js +67 -0
  19. package/dist/commands/guide.js.map +1 -0
  20. package/dist/commands/init.js +196 -0
  21. package/dist/commands/init.js.map +1 -0
  22. package/dist/commands/login.js +69 -15
  23. package/dist/commands/login.js.map +1 -1
  24. package/dist/commands/tools.js +63 -78
  25. package/dist/commands/tools.js.map +1 -1
  26. package/dist/commands/use.js +143 -0
  27. package/dist/commands/use.js.map +1 -0
  28. package/dist/config.js +87 -6
  29. package/dist/config.js.map +1 -1
  30. package/dist/generated/guide.json +104 -0
  31. package/dist/generated/manifest.json +3885 -241
  32. package/dist/guideCache.js +204 -0
  33. package/dist/guideCache.js.map +1 -0
  34. package/dist/help.js +391 -34
  35. package/dist/help.js.map +1 -1
  36. package/dist/manifest.js +34 -3
  37. package/dist/manifest.js.map +1 -1
  38. package/dist/manifestCache.js +196 -0
  39. package/dist/manifestCache.js.map +1 -0
  40. package/dist/output.js +251 -28
  41. package/dist/output.js.map +1 -1
  42. package/dist/schema.js +132 -15
  43. package/dist/schema.js.map +1 -1
  44. package/dist/wait.js +181 -8
  45. package/dist/wait.js.map +1 -1
  46. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1,22 +1,33 @@
1
- import { CliApiError, createApiClient } from './api.js';
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { resolve, join } from 'node:path';
3
+ import { CliApiError, createApiClient, isMediaBlock, isTextBlock } from './api.js';
2
4
  import { approvalSupport, runWithApproval } from './approve.js';
3
5
  import { flagBoolean, flagString, parseArgs } from './args.js';
4
6
  import { download, upload, wait } from './commands/assets.js';
7
+ import { call } from './commands/call.js';
5
8
  import { assertKnownFlags, compoundBooleanFlags, CONTEXT_FLAG_ALIASES, needsLocalProjection, selectedFields, timeoutMs, wantsWait, } from './commands/context.js';
6
9
  import { files } from './commands/files.js';
10
+ import { guide } from './commands/guide.js';
7
11
  import { login, logout, whoami } from './commands/login.js';
8
- import { init, models, probe, tools, version } from './commands/tools.js';
12
+ import { init } from './commands/init.js';
13
+ import { models, probe, tools, version } from './commands/tools.js';
14
+ import { use } from './commands/use.js';
9
15
  import { resolveConfig } from './config.js';
10
- import { renderCompoundHelp, renderHelp, renderToolHelp } from './help.js';
11
- import { findTool, loadManifest } from './manifest.js';
12
- import { EXIT, printBytes, printError, printResult, selectFields, warnNoFieldsMatched, } from './output.js';
13
- import { ArgumentError, buildArguments, flagSpecs } from './schema.js';
14
- import { assetsOutcome, handleOutcome, producedAssetIds, waitForAssets, waitForDelivery, waitForJob, waitForRun, WaitTimeoutError, } from './wait.js';
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';
15
23
  export const COMPOUND_COMMANDS = [
16
24
  login,
17
25
  logout,
18
26
  whoami,
27
+ use,
28
+ guide,
19
29
  tools,
30
+ call,
20
31
  models,
21
32
  upload,
22
33
  download,
@@ -26,26 +37,15 @@ export const COMPOUND_COMMANDS = [
26
37
  init,
27
38
  version,
28
39
  ];
29
- export const GLOBAL_FLAG_NAMES = new Set([
30
- 'help',
31
- 'version',
32
- 'fields',
33
- 'out',
34
- 'wait',
35
- 'timeout',
36
- 'quiet',
37
- 'raw',
38
- 'project',
39
- 'canvas',
40
- 'run',
41
- 'api-key',
42
- 'api-url',
43
- 'profile',
44
- 'context',
40
+ export { GLOBAL_FLAG_NAMES };
41
+ const TOOL_ONLY_GLOBAL_FLAGS = new Set([
45
42
  'approve',
46
43
  'max-olives',
44
+ 'out-dir',
45
+ 'schema',
46
+ 'all',
47
+ 'keys',
47
48
  ]);
48
- const TOOL_ONLY_GLOBAL_FLAGS = new Set(['approve', 'max-olives']);
49
49
  const GLOBAL_FLAGS_FOR_COMPOUNDS = new Set([
50
50
  ...[...GLOBAL_FLAG_NAMES].filter(flag => !TOOL_ONLY_GLOBAL_FLAGS.has(flag)),
51
51
  ...CONTEXT_FLAG_ALIASES,
@@ -58,7 +58,27 @@ const WAIT_BY_DEFAULT = new Set([
58
58
  'upscale_assets',
59
59
  'render_blender_take',
60
60
  ]);
61
- const BOOLEAN_GLOBAL_FLAGS = new Set(['help', 'version', 'wait', 'quiet', 'raw', 'context', 'approve']);
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) {
64
+ const toolFor = (command) => command && !COMPOUND_COMMANDS.some(candidate => candidate.name === command)
65
+ ? findTool(command, manifest)
66
+ : undefined;
67
+ return parseArgs(argv, (flag, command) => {
68
+ if (BOOLEAN_GLOBAL_FLAGS.has(flag))
69
+ return true;
70
+ const compound = COMPOUND_COMMANDS.find(candidate => candidate.name === command);
71
+ if (compound)
72
+ return compoundBooleanFlags(compound).includes(flag);
73
+ const tool = toolFor(command);
74
+ return tool ? flagSpecs(tool).some(spec => spec.flag === flag && spec.kind === 'boolean') : false;
75
+ }, (flag, command) => {
76
+ if (GLOBAL_FLAG_NAMES.has(flag))
77
+ return false;
78
+ const tool = toolFor(command);
79
+ return tool ? flagSpecs(tool).some(spec => spec.flag === flag && spec.kind === 'array') : false;
80
+ });
81
+ }
62
82
  export async function main(options) {
63
83
  const env = options.env ?? process.env;
64
84
  const cwd = options.cwd ?? process.cwd();
@@ -67,87 +87,216 @@ export async function main(options) {
67
87
  ...(options.stderr ? { stderr: options.stderr } : {}),
68
88
  ...(options.isTTY !== undefined ? { isTTY: options.isTTY } : {}),
69
89
  };
70
- const manifest = options.manifest ?? loadManifest();
71
- const parsed = parseArgs(options.argv, (flag, command) => {
72
- if (BOOLEAN_GLOBAL_FLAGS.has(flag))
73
- return true;
74
- const compound = COMPOUND_COMMANDS.find(candidate => candidate.name === command);
75
- if (compound)
76
- return compoundBooleanFlags(compound).includes(flag);
77
- const tool = command ? findTool(command, manifest) : undefined;
78
- return tool ? flagSpecs(tool).some(spec => spec.flag === flag && spec.kind === 'boolean') : false;
79
- });
90
+ let session;
80
91
  try {
81
- if (flagBoolean(parsed.flags, 'version') && parsed.command === undefined)
82
- parsed.command = 'version';
83
- if (parsed.command === undefined ||
84
- parsed.command === 'help' ||
85
- (flagBoolean(parsed.flags, 'help') && !parsed.command)) {
86
- printResult(renderHelp(manifest, COMPOUND_COMMANDS), output);
87
- return EXIT.ok;
88
- }
89
- const compound = COMPOUND_COMMANDS.find(command => command.name === parsed.command);
90
- const tool = compound ? undefined : findTool(parsed.command, manifest);
91
- if (!compound && !tool) {
92
- throw new ArgumentError(`Unknown command "${parsed.command}". Run martini --help for the list.`);
93
- }
94
- if (flagBoolean(parsed.flags, 'help')) {
95
- printResult(tool ? renderToolHelp(tool) : renderCompoundHelp(compound), output);
96
- return EXIT.ok;
97
- }
98
- if (compound)
99
- assertKnownFlags(compound, parsed.flags, GLOBAL_FLAGS_FOR_COMPOUNDS);
100
- const config = resolveConfig(parsed.flags, { env, cwd });
101
- const clientFor = options.createClient ??
102
- (resolved => createApiClient(resolved, { ...(options.fetch ? { fetch: options.fetch } : {}), env }));
103
- const out = flagString(parsed.flags, 'out');
104
- if (out)
105
- output.out = out;
106
- output.inRun = Boolean(config.runId);
107
- output.quiet = flagBoolean(parsed.flags, 'quiet') ?? false;
108
- const context = {
109
- client: clientFor(config),
110
- clientFor,
111
- config,
112
- manifest,
113
- flags: parsed.flags,
114
- positionals: parsed.positionals,
115
- output,
116
- env,
117
- cwd,
118
- now: options.now ?? Date.now,
119
- sleep: options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))),
120
- };
121
- const select = selectedFields(context);
122
- timeoutMs(context);
123
- if (compound) {
124
- const result = await compound.run(context);
125
- const projected = select.length && result && typeof result === 'object' ? selectFields(result, select) : result;
126
- warnNoFieldsMatched(projected, select, output);
127
- printResult(projected, output);
128
- return EXIT.ok;
129
- }
130
- const result = await runTool(tool, context, parsed.flags);
131
- warnNoFieldsMatched(result, select, output, knownResultFields(tool));
132
- printResult(result, output);
133
- return EXIT.ok;
92
+ session = openSession(options, env, cwd, output);
134
93
  }
135
94
  catch (error) {
136
- if (error instanceof CliApiError && error.result !== undefined)
137
- printResult(error.result, output);
138
95
  return printError(error, output);
139
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
+ }
140
187
  }
141
- async function runTool(tool, context, flags) {
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) {
142
258
  const properties = tool.inputSchema.properties ?? {};
143
259
  const ignore = new Set(GLOBAL_FLAG_NAMES);
144
260
  for (const alias of CONTEXT_FLAG_ALIASES) {
145
261
  if (!(alias.replace(/-id$/, 'Id') in properties))
146
262
  ignore.add(alias);
147
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) {
148
297
  const useContext = flagBoolean(flags, 'context') !== false;
149
298
  const args = buildArguments(tool, flags, {
150
- ignore,
299
+ ignore: nonArgumentFlags(tool),
151
300
  context: useContext
152
301
  ? {
153
302
  ...(context.config.projectId ? { projectId: context.config.projectId } : {}),
@@ -156,29 +305,108 @@ async function runTool(tool, context, flags) {
156
305
  }
157
306
  : {},
158
307
  });
159
- if (flagBoolean(flags, 'raw')) {
160
- const raw = await context.client.callToolRaw(tool.name, args);
161
- printBytes(raw.bytes, { ...context.output, contentType: raw.contentType });
308
+ const readOnly = tool.annotations.readOnlyHint === true;
309
+ const out = flagString(flags, 'out');
310
+ const outDir = flagString(flags, 'out-dir');
311
+ if (outDir) {
312
+ if (out)
313
+ throw new ArgumentError('Use either --out or --out-dir.');
314
+ if (!readOnly) {
315
+ throw new ArgumentError('--out-dir requires a read-only tool. Run this command without --out-dir.');
316
+ }
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);
321
+ const directory = resolve(context.cwd, outDir);
322
+ mkdirSync(directory, { recursive: true });
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);
336
+ }
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)) {
341
+ assertRawDestination(context.output);
342
+ let bytes;
343
+ try {
344
+ bytes = await context.client.callToolRaw(tool.name, args);
345
+ }
346
+ catch (error) {
347
+ if (error instanceof CliApiError && error.code === 'NOT_RAW_ELIGIBLE') {
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.`);
351
+ }
352
+ throw error;
353
+ }
354
+ printBytes(bytes.bytes, {
355
+ ...context.output,
356
+ contentType: bytes.contentType,
357
+ meta: bytes.meta,
358
+ fields: selectedFields(context),
359
+ });
162
360
  delete context.output.out;
163
361
  context.output.quiet = true;
164
- return raw.meta;
362
+ return undefined;
165
363
  }
166
- const select = selectedFields(context);
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 ?? {});
167
376
  const shouldWait = wantsWait(context, WAIT_BY_DEFAULT.has(tool.name));
168
377
  const approve = approvalFlags(tool, flags);
169
- const local = shouldWait || approve !== null || needsLocalProjection(select);
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);
170
386
  const result = approve
171
387
  ? await runWithApproval(tool, context.client, args, approve.support, approve.maxOlives === undefined ? {} : { maxOlives: approve.maxOlives })
172
388
  : await context.client.callTool(tool.name, args, !local && select.length ? { select } : {});
173
- if (!shouldWait)
174
- return local ? selectFields(result, select) : result;
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
+ }
175
400
  const projectId = typeof args.projectId === 'string' ? args.projectId : context.config.projectId;
176
401
  try {
177
- return selectFields(await waitForToolResult(result, context, projectId), select);
402
+ const waited = await waitForToolResult(result, context, projectId);
403
+ return finish(projectResult(waited, selectIn(waited), what, context.output));
178
404
  }
179
405
  catch (error) {
180
- if (error instanceof CliApiError) {
181
- const kept = selectFields(result, select);
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));
182
410
  const base = kept && typeof kept === 'object' && !Array.isArray(kept) ? kept : { result: kept };
183
411
  error.result =
184
412
  error instanceof WaitTimeoutError
@@ -188,6 +416,117 @@ async function runTool(tool, context, flags) {
188
416
  throw error;
189
417
  }
190
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
+ }
191
530
  async function waitForToolResult(result, context, projectId) {
192
531
  if (!result || typeof result !== 'object')
193
532
  return result;
@@ -195,25 +534,36 @@ async function waitForToolResult(result, context, projectId) {
195
534
  if (record.approvalNeeded === true || record.generated === false)
196
535
  return result;
197
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
+ };
198
544
  if (typeof record.runId === 'string') {
199
- return { ...record, waited: handleOutcome('run', await waitForRun(context.client, record.runId, waitOptions)) };
545
+ return settled(handleOutcome('run', await waitForRun(context.client, record.runId, waitOptions)));
200
546
  }
201
547
  if (typeof record.renderId === 'string' || typeof record.exportId === 'string') {
202
548
  const ids = {
203
549
  ...(typeof record.renderId === 'string' ? { renderId: record.renderId } : {}),
204
550
  ...(typeof record.exportId === 'string' ? { exportId: record.exportId } : {}),
205
551
  };
206
- return { ...record, waited: handleOutcome('render', await waitForDelivery(context.client, ids, waitOptions)) };
552
+ return settled(handleOutcome('render', await waitForDelivery(context.client, ids, waitOptions)));
207
553
  }
208
554
  if (typeof record.jobId === 'string') {
209
- return { ...record, waited: handleOutcome('job', await waitForJob(context.client, record.jobId, waitOptions)) };
555
+ return settled(handleOutcome('job', await waitForJob(context.client, record.jobId, waitOptions)));
210
556
  }
211
557
  if (!projectId)
212
558
  return result;
213
559
  const ids = producedAssetIds(result);
214
560
  if (ids.length === 0)
215
561
  return result;
216
- return { ...record, waited: assetsOutcome(await waitForAssets(context.client, projectId, ids, waitOptions)) };
562
+ return settled(assetsOutcome(await waitForAssets(context.client, projectId, ids, waitOptions)));
563
+ }
564
+ function strayPositionalError(tool, tokens) {
565
+ const list = flagSpecs(tool).find(spec => spec.kind === 'array');
566
+ return new ArgumentError(`Unexpected argument${tokens.length > 1 ? 's' : ''} ${tokens.map(token => JSON.stringify(token)).join(' ')} for ${tool.name}: a tool takes only --flags. Quote a value that contains spaces${list ? `; an array flag takes its values right after it (--${list.flag} a b)` : ''}. Run martini ${tool.name} --help.`);
217
567
  }
218
568
  function approvalFlags(tool, flags) {
219
569
  const approve = flagBoolean(flags, 'approve') === true;
@@ -233,7 +583,4 @@ function approvalFlags(tool, flags) {
233
583
  throw new ArgumentError('--max-olives must be a number of olives.');
234
584
  return { support, maxOlives };
235
585
  }
236
- function knownResultFields(tool) {
237
- return Object.keys(tool.outputSchema?.properties ?? {});
238
- }
239
586
  //# sourceMappingURL=cli.js.map