@postplus/cli 0.1.45 → 0.1.47

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.
@@ -3,11 +3,11 @@ import { createReadStream } from 'node:fs';
3
3
  import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { resolveFreshRemoteAuth } from './auth-session.js';
6
- import { sendAuthedCloudRequest } from './authed-cloud-request.js';
6
+ import { sendAuthedCloudRequest, } from './authed-cloud-request.js';
7
7
  import { formatPostPlusCompatibilityError } from './client-compatibility.js';
8
8
  import { assertModelledFieldValuesInRange } from './hosted-field-validation.js';
9
9
  import { buildVerbTargetIndex, } from './hosted-manifest-index.js';
10
- import { buildHostedRequestSchemaReport, buildMediaGenerationRequestDimensions, } from './hosted-request-schemas.js';
10
+ import { buildHostedRequestSchemaReport } from './hosted-request-schemas.js';
11
11
  import { readLargeCreditQuoteConfirmationChallenge, } from './quote-confirmation.js';
12
12
  // Manifest-driven verb grammar indexes (SSOT projected from apps/web +
13
13
  // public-skill-metadata via the generated manifest). The verb/flag grammar,
@@ -27,6 +27,20 @@ function buildPublishVerbIndex() {
27
27
  }
28
28
  return index;
29
29
  }
30
+ // Reads the request-json body for a surface: from the injected object (lib path)
31
+ // or by reading `--request <file>` (bin path). This is the SINGLE place the two
32
+ // paths diverge on input source; the resolved body then flows through the SAME
33
+ // validation + envelope build, so the URL/body/headers stay byte-identical.
34
+ async function resolveRequestBody(context, flags) {
35
+ if (context) {
36
+ if (context.requestJson === undefined) {
37
+ throw new Error('This hosted command requires a requestJson body.');
38
+ }
39
+ return { body: context.requestJson, errorInputLabel: 'requestJson' };
40
+ }
41
+ const requestPath = requireFlag(flags, 'request');
42
+ return { body: await readJsonFile(requestPath), errorInputLabel: requestPath };
43
+ }
30
44
  class HostedQuoteConfirmationRequiredError extends Error {
31
45
  challenge;
32
46
  constructor(message, challenge) {
@@ -43,41 +57,45 @@ class HostedProductRequestError extends Error {
43
57
  this.name = 'HostedProductRequestError';
44
58
  }
45
59
  }
46
- export async function runHostedDomainCommand(domain, args) {
60
+ export async function runHostedDomainCommand(domain, args,
61
+ // Present only on the in-process hosted-lib path; the bin path never passes it.
62
+ // See HostedRequestContext: it carries the injected auth/releaseId/requestJson
63
+ // and switches every leaf onto the no-disk, no-file, return-payload behavior.
64
+ context) {
47
65
  const [subcommand, ...rest] = args;
48
66
  if (domain === 'research') {
49
67
  if (subcommand === 'schema') {
50
- return runHostedSchema(domain, rest);
68
+ return runHostedSchema(domain, rest, context);
51
69
  }
52
70
  if (subcommand === 'collect') {
53
- return runResearchCollect(rest);
71
+ return runResearchCollect(rest, context);
54
72
  }
55
73
  if (subcommand === 'scrape') {
56
- return runResearchScrape(rest);
74
+ return runResearchScrape(rest, context);
57
75
  }
58
76
  printResearchHelp();
59
77
  return subcommand === undefined || isHelp(subcommand) ? 0 : 1;
60
78
  }
61
79
  if (subcommand === 'schema') {
62
- return runHostedSchema(domain, rest);
80
+ return runHostedSchema(domain, rest, context);
63
81
  }
64
82
  // Poll a pending async media-generation run by handle. This is a hand-coded
65
83
  // branch (not a manifest verb) because a status poll has no endpointKey/field
66
84
  // contract to project — exactly like the `research collect --run-handle`
67
85
  // polling branch. It must be checked before the manifest verb dispatch.
68
86
  if (domain === 'media' && subcommand === 'poll') {
69
- return runMediaPoll(rest);
87
+ return runMediaPoll(rest, context);
70
88
  }
71
89
  if (domain === 'media' &&
72
90
  subcommand &&
73
91
  MEDIA_VERB_ENDPOINTS.has(subcommand)) {
74
- return runMediaVerb(subcommand, rest);
92
+ return runMediaVerb(subcommand, rest, context);
75
93
  }
76
94
  // publish: the OPERATION is the subcommand (no separate target positional).
77
95
  if (domain === 'publish' &&
78
96
  subcommand &&
79
97
  PUBLISH_VERB_OPERATIONS.has(subcommand)) {
80
- return runPublishOperation(subcommand, rest);
98
+ return runPublishOperation(subcommand, rest, context);
81
99
  }
82
100
  printDomainVerbHelp(domain);
83
101
  return subcommand === undefined || isHelp(subcommand) ? 0 : 1;
@@ -87,7 +105,7 @@ export async function runHostedDomainCommand(domain, args) {
87
105
  // scalar intent/default fields to flags, a request-json surface reads the nested
88
106
  // envelope from `--request <file>`. Either way runner-managed fields (billing
89
107
  // dimensions, ids, tokens) are derived/minted by the runner, never agent-supplied.
90
- async function runMediaVerb(verb, args) {
108
+ async function runMediaVerb(verb, args, context) {
91
109
  const targets = MEDIA_VERB_ENDPOINTS.get(verb);
92
110
  if (!targets) {
93
111
  throw new Error(`Unknown media verb ${verb}.`);
@@ -112,6 +130,7 @@ async function runMediaVerb(verb, args) {
112
130
  modelKey: targetKey,
113
131
  resolved,
114
132
  verb,
133
+ context,
115
134
  });
116
135
  }
117
136
  if (resolved.surface === 'request-json') {
@@ -120,6 +139,7 @@ async function runMediaVerb(verb, args) {
120
139
  endpointKey: targetKey,
121
140
  resolved,
122
141
  verb,
142
+ context,
123
143
  });
124
144
  }
125
145
  return runMediaVerbFlags({
@@ -127,12 +147,13 @@ async function runMediaVerb(verb, args) {
127
147
  endpointKey: targetKey,
128
148
  resolved,
129
149
  verb,
150
+ context,
130
151
  });
131
152
  }
132
153
  // Flags surface (e.g. audio-transcription): scalar intent/default fields map to
133
154
  // flags; runner-managed fields have no flag so the agent cannot pass them.
134
155
  async function runMediaVerbFlags(args) {
135
- const { endpointKey, resolved, verb } = args;
156
+ const { endpointKey, resolved, verb, context } = args;
136
157
  const endpoint = requireResolvedEndpoint(resolved, verb, endpointKey);
137
158
  const fields = endpoint.fields;
138
159
  const flagToField = new Map();
@@ -193,6 +214,7 @@ async function runMediaVerbFlags(args) {
193
214
  outputPath,
194
215
  quoteConfirmationToken: flags.values.get('quote-confirmation-token'),
195
216
  skillName: flags.values.get('skill') ?? resolved.skill,
217
+ context,
196
218
  });
197
219
  }
198
220
  // Request-json surface (e.g. seedance-submitter): the nested envelope is supplied
@@ -200,7 +222,7 @@ async function runMediaVerbFlags(args) {
200
222
  // so the body carries only the media-generation input. Runner-managed fields have
201
223
  // no flag and must not appear in the body — the CLI fast-fails if they do.
202
224
  async function runMediaVerbRequestJson(args) {
203
- const { endpointKey, resolved, verb } = args;
225
+ const { endpointKey, resolved, verb, context } = args;
204
226
  const endpoint = requireResolvedEndpoint(resolved, verb, endpointKey);
205
227
  const flags = parseFlags(args.args, new Set(['json']));
206
228
  const allowedKeys = new Set([
@@ -216,9 +238,8 @@ async function runMediaVerbRequestJson(args) {
216
238
  throw new Error(`Unknown option for media ${verb}: --${key}.`);
217
239
  }
218
240
  }
219
- const requestPath = requireFlag(flags, 'request');
220
241
  const outputPath = flags.values.get('output') ?? null;
221
- const raw = await readJsonFile(requestPath);
242
+ const { body: raw, errorInputLabel } = await resolveRequestBody(context, flags);
222
243
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
223
244
  throw new Error(`media ${verb} ${endpointKey} --request must be a JSON object of media-generation input.`);
224
245
  }
@@ -239,7 +260,7 @@ async function runMediaVerbRequestJson(args) {
239
260
  return submitMediaGenerationRequest({
240
261
  capability: resolved.capability,
241
262
  endpointKey,
242
- errorInputLabel: requestPath,
263
+ errorInputLabel,
243
264
  input,
244
265
  json: flags.booleans.has('json'),
245
266
  operationId: flags.values.get('hosted-operation-id') ??
@@ -247,6 +268,7 @@ async function runMediaVerbRequestJson(args) {
247
268
  outputPath,
248
269
  quoteConfirmationToken: flags.values.get('quote-confirmation-token'),
249
270
  skillName: flags.values.get('skill') ?? resolved.skill,
271
+ context,
250
272
  });
251
273
  }
252
274
  function requireResolvedEndpoint(resolved, verb, endpointKey) {
@@ -264,7 +286,7 @@ function requireResolvedEndpoint(resolved, verb, endpointKey) {
264
286
  // `estimatedUsage.videoSeconds` so the Web boundary can route eligible short
265
287
  // videos through its preflight/routing path (omit it to use the default route).
266
288
  async function runVideoAnalysisVerb(args) {
267
- const { modelKey, resolved, verb } = args;
289
+ const { modelKey, resolved, verb, context } = args;
268
290
  const flags = parseFlags(args.args, new Set(['json']));
269
291
  const allowedKeys = new Set([
270
292
  'hosted-operation-id',
@@ -280,9 +302,8 @@ async function runVideoAnalysisVerb(args) {
280
302
  throw new Error(`Unknown option for media ${verb}: --${key}.`);
281
303
  }
282
304
  }
283
- const requestPath = requireFlag(flags, 'request');
284
305
  const outputPath = flags.values.get('output') ?? null;
285
- const raw = await readJsonFile(requestPath);
306
+ const { body: raw, errorInputLabel } = await resolveRequestBody(context, flags);
286
307
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
287
308
  throw new Error(`media ${verb} ${modelKey} --request must be a JSON object of Gemini request payload.`);
288
309
  }
@@ -311,16 +332,17 @@ async function runVideoAnalysisVerb(args) {
311
332
  `postplus-cli:media:video-analysis:analyze:${randomUUID()}`,
312
333
  quoteConfirmationToken: flags.values.get('quote-confirmation-token') ?? undefined,
313
334
  };
314
- return runHostedCommand({
335
+ return dispatchHostedCommand({
315
336
  request: () => postHostedJson({
316
337
  body,
317
338
  pathName: '/api/postplus-cli/hosted/capability',
318
339
  skillName: flags.values.get('skill') ?? resolved.skill,
340
+ context,
319
341
  }),
320
- errorInputLabel: requestPath,
342
+ errorInputLabel,
321
343
  json: flags.booleans.has('json'),
322
344
  outputPath,
323
- });
345
+ }, context);
324
346
  }
325
347
  // `media-file upload`: the generic local-file -> hosted media verb. Released
326
348
  // skills ship no scripts, so a skill that must place a local file behind hosted
@@ -346,15 +368,15 @@ function inferUploadMimeType(filePath) {
346
368
  return (MEDIA_FILE_MIME_BY_EXTENSION[path.extname(filePath).toLowerCase()] ??
347
369
  'application/octet-stream');
348
370
  }
349
- export async function runMediaFileCommand(args) {
371
+ export async function runMediaFileCommand(args, context) {
350
372
  const [subcommand, ...rest] = args;
351
373
  if (subcommand === 'upload') {
352
- return runMediaFileUpload(rest);
374
+ return runMediaFileUpload(rest, context);
353
375
  }
354
376
  printMediaFileHelp();
355
377
  return subcommand === undefined || isHelp(subcommand) ? 0 : 1;
356
378
  }
357
- async function runMediaFileUpload(args) {
379
+ async function runMediaFileUpload(args, context) {
358
380
  const flags = parseFlags(args, new Set(['json']));
359
381
  const allowedKeys = new Set([
360
382
  'hosted-operation-id',
@@ -391,12 +413,13 @@ async function runMediaFileUpload(args) {
391
413
  `postplus-cli:media-file:create-upload-url:${randomUUID()}`,
392
414
  quoteConfirmationToken: flags.values.get('quote-confirmation-token') ?? undefined,
393
415
  };
394
- return runHostedCommand({
416
+ return dispatchHostedCommand({
395
417
  request: async () => {
396
418
  const payload = await postHostedJson({
397
419
  body,
398
420
  pathName: '/api/postplus-cli/hosted/capability',
399
421
  skillName: flags.values.get('skill') ?? null,
422
+ context,
400
423
  });
401
424
  const output = readHostedUploadOutput(payload);
402
425
  const signedUpload = readSignedUpload(output);
@@ -418,12 +441,13 @@ async function runMediaFileUpload(args) {
418
441
  },
419
442
  pathName: '/api/postplus-cli/hosted/capability',
420
443
  skillName: flags.values.get('skill') ?? null,
444
+ context,
421
445
  });
422
446
  },
423
447
  errorInputLabel: inputFile,
424
448
  json: flags.booleans.has('json'),
425
449
  outputPath,
426
- });
450
+ }, context);
427
451
  }
428
452
  function readHostedUploadOutput(payload) {
429
453
  if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
@@ -492,6 +516,9 @@ Usage:
492
516
  // Shared submit path for both surfaces: wrap the media input, derive billing
493
517
  // dimensions from endpointKey + input, and POST to the Web boundary.
494
518
  function submitMediaGenerationRequest(params) {
519
+ // Billing dimensions are derived solely at the Web boundary from
520
+ // (endpointKey, input); the CLI sends only the payload. The Web request schema
521
+ // rejects any caller-supplied `requestDimensions` (single source of truth).
495
522
  const body = {
496
523
  capability: params.capability,
497
524
  endpointKey: params.endpointKey,
@@ -499,18 +526,18 @@ function submitMediaGenerationRequest(params) {
499
526
  operation: 'request',
500
527
  operationId: params.operationId,
501
528
  quoteConfirmationToken: params.quoteConfirmationToken ?? undefined,
502
- requestDimensions: buildMediaGenerationRequestDimensions(params.endpointKey, params.input),
503
529
  };
504
- return runHostedCommand({
530
+ return dispatchHostedCommand({
505
531
  request: () => postHostedJson({
506
532
  body,
507
533
  pathName: '/api/postplus-cli/hosted/capability',
508
534
  skillName: params.skillName,
535
+ context: params.context,
509
536
  }),
510
537
  errorInputLabel: params.errorInputLabel,
511
538
  json: params.json,
512
539
  outputPath: params.outputPath,
513
- });
540
+ }, params.context);
514
541
  }
515
542
  // Poll a pending media-generation run: `postplus media poll --handle <run-id>`.
516
543
  // A media `create`/`transcribe`/`analyze` submit returns an async run handle
@@ -522,11 +549,11 @@ function submitMediaGenerationRequest(params) {
522
549
  // The body carries only the status quadruple; submit-only fields (input,
523
550
  // requestDimensions, quoteConfirmationToken) are never sent. Mirrors the
524
551
  // `research collect --run-handle` polling branch.
525
- async function runMediaPoll(args) {
552
+ async function runMediaPoll(args, context) {
526
553
  const flags = parseFlags(args, new Set(['json']));
527
554
  const handle = requireFlag(flags, 'handle');
528
555
  const outputPath = flags.values.get('output') ?? null;
529
- return runHostedCommand({
556
+ return dispatchHostedCommand({
530
557
  request: () => postHostedJson({
531
558
  body: {
532
559
  capability: 'media-generation',
@@ -536,11 +563,12 @@ async function runMediaPoll(args) {
536
563
  },
537
564
  pathName: '/api/postplus-cli/hosted/capability',
538
565
  skillName: null,
566
+ context,
539
567
  }),
540
568
  errorInputLabel: 'media-poll-handle',
541
569
  json: flags.booleans.has('json'),
542
570
  outputPath,
543
- });
571
+ }, context);
544
572
  }
545
573
  function buildMediaVerbInput(input) {
546
574
  const record = {};
@@ -604,23 +632,24 @@ function buildMediaVerbInput(input) {
604
632
  // schemaVersion envelope), and posts to /hosted/collection. The resolved entry
605
633
  // gives the default skillName (overridable by `--skill`); the actorId stays
606
634
  // internal and is never placed on the public body.
607
- async function runResearchCollect(args) {
635
+ async function runResearchCollect(args, context) {
608
636
  const [first, ...rest] = args;
609
637
  // Polling path: `research collect --run-handle <h>`. No positional collectionKey.
610
638
  if (!first || first.startsWith('--')) {
611
639
  const flags = parseFlags(args, new Set(['json']));
612
640
  const runHandle = requireFlag(flags, 'run-handle');
613
641
  const outputPath = flags.values.get('output') ?? null;
614
- return runHostedCommand({
642
+ return dispatchHostedCommand({
615
643
  request: () => postHostedJson({
616
644
  body: { runHandle, runHandleType: 'hosted-collection' },
617
645
  pathName: '/api/postplus-cli/hosted/collection',
618
646
  skillName: null,
647
+ context,
619
648
  }),
620
649
  errorInputLabel: 'research-collect-run-handle',
621
650
  json: flags.booleans.has('json'),
622
651
  outputPath,
623
- });
652
+ }, context);
624
653
  }
625
654
  const verb = 'collect';
626
655
  const collectionKey = first;
@@ -650,9 +679,8 @@ async function runResearchCollect(args) {
650
679
  throw new Error(`Unknown option for research ${verb}: --${key}.`);
651
680
  }
652
681
  }
653
- const requestPath = requireFlag(flags, 'request');
654
682
  const outputPath = flags.values.get('output') ?? null;
655
- const raw = await readJsonFile(requestPath);
683
+ const { body: raw, errorInputLabel } = await resolveRequestBody(context, flags);
656
684
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
657
685
  throw new Error(`research ${verb} ${collectionKey} --request must be a JSON object of collection input.`);
658
686
  }
@@ -671,7 +699,7 @@ async function runResearchCollect(args) {
671
699
  }
672
700
  maxTotalChargeUsd = parsed;
673
701
  }
674
- return runHostedCommand({
702
+ return dispatchHostedCommand({
675
703
  request: () => postHostedJson({
676
704
  body: {
677
705
  collectionKey,
@@ -683,11 +711,12 @@ async function runResearchCollect(args) {
683
711
  },
684
712
  pathName: '/api/postplus-cli/hosted/collection',
685
713
  skillName,
714
+ context,
686
715
  }),
687
- errorInputLabel: requestPath,
716
+ errorInputLabel,
688
717
  json: flags.booleans.has('json'),
689
718
  outputPath,
690
- });
719
+ }, context);
691
720
  }
692
721
  // Manifest-driven public-content-collection verb (request-json surface). Resolves
693
722
  // the positional `<sourceKey>` against the research verb index for verb `scrape`,
@@ -696,7 +725,7 @@ async function runResearchCollect(args) {
696
725
  // capability `public-content-collection` / operation `scrape`. The resolved entry
697
726
  // gives the default skillName (overridable by `--skill`); the datasetId stays
698
727
  // internal and is never placed on the public body.
699
- async function runResearchScrape(args) {
728
+ async function runResearchScrape(args, context) {
700
729
  const [first, ...rest] = args;
701
730
  const verb = 'scrape';
702
731
  const targets = RESEARCH_VERB_TARGETS.get(verb);
@@ -704,16 +733,17 @@ async function runResearchScrape(args) {
704
733
  const flags = parseFlags(args, new Set(['json']));
705
734
  const runHandle = requireFlag(flags, 'run-handle');
706
735
  const outputPath = flags.values.get('output') ?? null;
707
- return runHostedCommand({
736
+ return dispatchHostedCommand({
708
737
  request: () => postHostedJson({
709
738
  body: { runHandle, runHandleType: 'public-content-collection' },
710
739
  pathName: '/api/postplus-cli/hosted/collection',
711
740
  skillName: null,
741
+ context,
712
742
  }),
713
743
  errorInputLabel: 'research-scrape-run-handle',
714
744
  json: flags.booleans.has('json'),
715
745
  outputPath,
716
- });
746
+ }, context);
717
747
  }
718
748
  const sourceKey = first;
719
749
  const resolved = targets?.get(sourceKey);
@@ -741,9 +771,8 @@ async function runResearchScrape(args) {
741
771
  throw new Error(`Unknown option for research ${verb}: --${key}.`);
742
772
  }
743
773
  }
744
- const requestPath = requireFlag(flags, 'request');
745
774
  const outputPath = flags.values.get('output') ?? null;
746
- const raw = await readJsonFile(requestPath);
775
+ const { body: raw, errorInputLabel } = await resolveRequestBody(context, flags);
747
776
  if (!Array.isArray(raw) || raw.length === 0) {
748
777
  throw new Error(`research ${verb} ${sourceKey} --request must be a non-empty JSON array of scrape input records.`);
749
778
  }
@@ -764,7 +793,7 @@ async function runResearchScrape(args) {
764
793
  }
765
794
  // The Web /hosted/capability scrape contract is a strict object: skillName is
766
795
  // carried as the compatibility header (postHostedJson), never on the public body.
767
- return runHostedCommand({
796
+ return dispatchHostedCommand({
768
797
  request: () => postHostedJson({
769
798
  body: {
770
799
  capability: 'public-content-collection',
@@ -777,11 +806,12 @@ async function runResearchScrape(args) {
777
806
  },
778
807
  pathName: '/api/postplus-cli/hosted/capability',
779
808
  skillName,
809
+ context,
780
810
  }),
781
- errorInputLabel: requestPath,
811
+ errorInputLabel,
782
812
  json: flags.booleans.has('json'),
783
813
  outputPath,
784
- });
814
+ }, context);
785
815
  }
786
816
  // Manifest-driven publish operation (request-json surface). The OPERATION is the
787
817
  // subcommand and the target: `postplus publish <operation> --request <file>`. The
@@ -790,7 +820,7 @@ async function runResearchScrape(args) {
790
820
  // Side-effecting operations surface the Web quote-confirmation challenge; the
791
821
  // shared runHostedCommand handles the challenge -> retry-with-token path. There is
792
822
  // no requestDimensions/approval/execute — those were private-runtime concepts.
793
- async function runPublishOperation(operation, args) {
823
+ async function runPublishOperation(operation, args, context) {
794
824
  const resolved = PUBLISH_VERB_OPERATIONS.get(operation);
795
825
  if (!resolved) {
796
826
  throw new Error(`Unknown publish operation ${operation}. Valid: ${[...PUBLISH_VERB_OPERATIONS.keys()].join(', ')}.`);
@@ -814,9 +844,8 @@ async function runPublishOperation(operation, args) {
814
844
  throw new Error(`Unknown option for publish ${operation}: --${key}.`);
815
845
  }
816
846
  }
817
- const requestPath = requireFlag(flags, 'request');
818
847
  const outputPath = flags.values.get('output') ?? null;
819
- const raw = await readJsonFile(requestPath);
848
+ const { body: raw, errorInputLabel } = await resolveRequestBody(context, flags);
820
849
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
821
850
  throw new Error(`publish ${operation} --request must be a JSON object of publishing input.`);
822
851
  }
@@ -825,7 +854,7 @@ async function runPublishOperation(operation, args) {
825
854
  const operationId = flags.values.get('hosted-operation-id') ??
826
855
  `postplus-cli:publish:social-publishing:request:${randomUUID()}`;
827
856
  const quoteConfirmationToken = flags.values.get('quote-confirmation-token');
828
- return runHostedCommand({
857
+ return dispatchHostedCommand({
829
858
  request: () => postHostedJson({
830
859
  body: {
831
860
  capability: 'social-publishing',
@@ -836,13 +865,14 @@ async function runPublishOperation(operation, args) {
836
865
  },
837
866
  pathName: '/api/postplus-cli/hosted/capability',
838
867
  skillName,
868
+ context,
839
869
  }),
840
- errorInputLabel: requestPath,
870
+ errorInputLabel,
841
871
  json: flags.booleans.has('json'),
842
872
  outputPath,
843
- });
873
+ }, context);
844
874
  }
845
- async function runHostedSchema(domain, args) {
875
+ async function runHostedSchema(domain, args, context) {
846
876
  const flags = parseFlags(args, new Set(['json']));
847
877
  const allowedFlags = domain === 'media'
848
878
  ? new Set(['endpoint'])
@@ -854,24 +884,40 @@ async function runHostedSchema(domain, args) {
854
884
  throw new Error(`Unknown option for ${domain} schema: --${key}.`);
855
885
  }
856
886
  }
857
- writeJson(buildHostedRequestSchemaReport({
887
+ const report = buildHostedRequestSchemaReport({
858
888
  collectionKey: flags.values.get('collection-key') ?? null,
859
889
  domain,
860
890
  endpointKey: flags.values.get('endpoint') ?? null,
861
- }));
891
+ });
892
+ // In-process / context path: RETURN the structured catalog so the model
893
+ // receives it as the call result. The bin path (no context) keeps writeJson +
894
+ // return 0 for human CLI stdout output. Mirrors the spend-verb dispatch.
895
+ if (context) {
896
+ return report;
897
+ }
898
+ writeJson(report);
862
899
  return 0;
863
900
  }
864
901
  async function postHostedJson(input) {
865
- const auth = await resolveFreshRemoteAuth();
866
- const response = await sendAuthedCloudRequest({
867
- auth,
868
- body: input.body,
869
- method: 'POST',
870
- pathName: input.pathName,
871
- retryOn401: () => resolveFreshRemoteAuth({ forceRefresh: true }),
872
- skillName: input.skillName,
873
- timeoutMs: 120000,
874
- });
902
+ const response = input.context
903
+ ? await sendAuthedCloudRequest({
904
+ auth: input.context.auth,
905
+ body: input.body,
906
+ method: 'POST',
907
+ pathName: input.pathName,
908
+ skillName: input.skillName,
909
+ skillsReleaseId: input.context.skillsReleaseId ?? null,
910
+ timeoutMs: 120000,
911
+ })
912
+ : await sendAuthedCloudRequest({
913
+ auth: await resolveFreshRemoteAuth(),
914
+ body: input.body,
915
+ method: 'POST',
916
+ pathName: input.pathName,
917
+ retryOn401: () => resolveFreshRemoteAuth({ forceRefresh: true }),
918
+ skillName: input.skillName,
919
+ timeoutMs: 120000,
920
+ });
875
921
  const payload = await readJsonResponse(response);
876
922
  if (!response.ok) {
877
923
  const productError = readHostedProductError(payload);
@@ -887,7 +933,7 @@ async function postHostedJson(input) {
887
933
  }
888
934
  return payload;
889
935
  }
890
- // Single exit path for every hosted command: success writes the result and
936
+ // Single exit path for the BIN hosted command: success writes the result and
891
937
  // returns 0; a quote challenge writes the challenge file and rethrows actionable
892
938
  // guidance; a structured product error writes the full error envelope to the
893
939
  // result JSON and surfaces code/layer/operationId on the terminal, exiting 1.
@@ -919,6 +965,20 @@ async function runHostedCommand(input) {
919
965
  await writeResult(payload, input.outputPath, input.json);
920
966
  return 0;
921
967
  }
968
+ // Single exit path for both BIN and LIB hosted commands. Each dispatch function
969
+ // builds the SAME `request` closure (resolve verb -> build envelope -> POST) and
970
+ // hands it here. The bin path (no `context`) keeps stdout/file/exit-code behavior
971
+ // via runHostedCommand. The lib path (with `context`) returns the parsed payload
972
+ // and rethrows the structured HostedProductRequestError / quote-confirmation error
973
+ // VERBATIM — no stdout, no file writes, no exit code — so the in-process caller
974
+ // surfaces the structured JSON and fails honestly. Because the closure is shared,
975
+ // the wire request (URL + body + headers) is byte-identical across both paths.
976
+ async function dispatchHostedCommand(input, context) {
977
+ if (!context) {
978
+ return runHostedCommand(input);
979
+ }
980
+ return input.request();
981
+ }
922
982
  async function writeQuoteConfirmationChallenge(error, input) {
923
983
  const challengePath = path.resolve(input.outputPath
924
984
  ? `${input.outputPath}.quote-confirmation.json`