@postplus/cli 0.1.41 → 0.1.43

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.
@@ -1,7 +1,8 @@
1
1
  import { formatAccountBindingLines } from './account-binding-display.js';
2
2
  import { refreshRemoteAuthSession } from './auth-session.js';
3
3
  import { clearAuthState, generateAuthStatusReport } from './auth.js';
4
- import { buildPostPlusClientCompatibilityHeaders, formatPostPlusCompatibilityError, } from './client-compatibility.js';
4
+ import { sendAuthedCloudRequest } from './authed-cloud-request.js';
5
+ import { formatPostPlusCompatibilityError } from './client-compatibility.js';
5
6
  import { requireHostedBaseUrl } from './hosted-release.js';
6
7
  import { resolveCliSessionTokenState } from './local-state.js';
7
8
  import { readSubscriptionStatusField } from './subscription-status.js';
@@ -38,17 +39,11 @@ export async function revokeRemoteAuth() {
38
39
  if (!cliSessionTokenState.present || !cliSessionTokenState.value) {
39
40
  throw new Error('Run `postplus auth login` before revoking PostPlus auth.');
40
41
  }
41
- const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
42
- const response = await fetch(`${apiBaseUrl}/api/postplus-cli/auth/revoke`, {
42
+ const response = await sendAuthedCloudRequest({
43
+ auth: { apiBaseUrl, cliSessionToken: cliSessionTokenState.value },
44
+ body: {},
43
45
  method: 'POST',
44
- headers: {
45
- accept: 'application/json',
46
- ...compatibilityHeaders,
47
- authorization: `Bearer ${cliSessionTokenState.value}`,
48
- 'content-type': 'application/json',
49
- },
50
- body: JSON.stringify({}),
51
- signal: AbortSignal.timeout(15000),
46
+ pathName: '/api/postplus-cli/auth/revoke',
52
47
  });
53
48
  const payload = (await response.json());
54
49
  if (!response.ok) {
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import { sendAuthedCloudRequest } from './authed-cloud-request.js';
2
3
  import { buildPostPlusClientCompatibilityHeaders, formatPostPlusCompatibilityError, writeCurrentCliVersionToLocalConfig, } from './client-compatibility.js';
3
4
  import { requireHostedBaseUrl } from './hosted-release.js';
4
5
  import { setLocalSession } from './local-state.js';
@@ -127,15 +128,9 @@ export async function pollCloudAuthLogin(input) {
127
128
  throw new Error('PostPlus CLI sign-in poll returned incomplete data.');
128
129
  }
129
130
  export async function validateCliSession(input) {
130
- const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
131
- const response = await fetch(`${input.apiBaseUrl}/api/postplus-cli/auth/whoami`, {
132
- method: 'GET',
133
- headers: {
134
- accept: 'application/json',
135
- ...compatibilityHeaders,
136
- authorization: `Bearer ${input.cliSessionToken}`,
137
- },
138
- signal: AbortSignal.timeout(15000),
131
+ const response = await sendAuthedCloudRequest({
132
+ auth: input,
133
+ pathName: '/api/postplus-cli/auth/whoami',
139
134
  });
140
135
  const payload = (await response.json());
141
136
  if (!response.ok) {
@@ -1,4 +1,5 @@
1
- import { buildPostPlusClientCompatibilityHeaders, formatPostPlusCompatibilityError, writeCurrentCliVersionToLocalConfig, } from './client-compatibility.js';
1
+ import { sendAuthedCloudRequest } from './authed-cloud-request.js';
2
+ import { formatPostPlusCompatibilityError, writeCurrentCliVersionToLocalConfig, } from './client-compatibility.js';
2
3
  import { requireHostedBaseUrl } from './hosted-release.js';
3
4
  import { resolveCliSessionTokenState, setLocalSession } from './local-state.js';
4
5
  export async function resolveFreshRemoteAuth(options = {}) {
@@ -39,17 +40,11 @@ export async function refreshRemoteAuthSession(input) {
39
40
  if (!cliSessionToken) {
40
41
  throw new Error('Run `postplus auth login` before refreshing PostPlus auth.');
41
42
  }
42
- const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
43
- const response = await fetch(`${apiBaseUrl}/api/postplus-cli/auth/refresh`, {
43
+ const response = await sendAuthedCloudRequest({
44
+ auth: { apiBaseUrl, cliSessionToken },
45
+ body: {},
44
46
  method: 'POST',
45
- headers: {
46
- accept: 'application/json',
47
- ...compatibilityHeaders,
48
- authorization: `Bearer ${cliSessionToken}`,
49
- 'content-type': 'application/json',
50
- },
51
- body: JSON.stringify({}),
52
- signal: AbortSignal.timeout(15000),
47
+ pathName: '/api/postplus-cli/auth/refresh',
53
48
  });
54
49
  const payload = (await response.json());
55
50
  if (!response.ok) {
@@ -1,16 +1,15 @@
1
1
  import { formatAccountBindingLines } from './account-binding-display.js';
2
2
  import { resolveFreshRemoteAuth } from './auth-session.js';
3
- import { buildPostPlusClientCompatibilityHeaders, formatPostPlusCompatibilityError, } from './client-compatibility.js';
3
+ import { sendAuthedCloudRequest } from './authed-cloud-request.js';
4
+ import { formatPostPlusCompatibilityError } from './client-compatibility.js';
4
5
  import { readSubscriptionStatusField } from './subscription-status.js';
5
6
  export async function validateRemoteAuth() {
6
- let auth = await resolveFreshRemoteAuth();
7
- let response = await fetchWhoami(auth);
8
- if (response.status === 401) {
9
- auth = await resolveFreshRemoteAuth({
10
- forceRefresh: true,
11
- });
12
- response = await fetchWhoami(auth);
13
- }
7
+ const auth = await resolveFreshRemoteAuth();
8
+ const response = await sendAuthedCloudRequest({
9
+ auth,
10
+ pathName: '/api/postplus-cli/auth/whoami',
11
+ retryOn401: () => resolveFreshRemoteAuth({ forceRefresh: true }),
12
+ });
14
13
  const payload = (await response.json());
15
14
  if (!response.ok) {
16
15
  const compatibilityError = formatPostPlusCompatibilityError(payload);
@@ -61,18 +60,6 @@ function readAccountType(payload) {
61
60
  }
62
61
  return value;
63
62
  }
64
- async function fetchWhoami(input) {
65
- const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
66
- return fetch(`${input.apiBaseUrl}/api/postplus-cli/auth/whoami`, {
67
- method: 'GET',
68
- headers: {
69
- accept: 'application/json',
70
- ...compatibilityHeaders,
71
- authorization: `Bearer ${input.cliSessionToken}`,
72
- },
73
- signal: AbortSignal.timeout(15000),
74
- });
75
- }
76
63
  function readRequiredString(payload, fieldName) {
77
64
  const value = payload[fieldName];
78
65
  if (typeof value !== 'string' || !value.trim()) {
@@ -0,0 +1,42 @@
1
+ import { buildPostPlusClientCompatibilityHeaders } from './client-compatibility.js';
2
+ const DEFAULT_AUTHED_REQUEST_TIMEOUT_MS = 15_000;
3
+ /**
4
+ * Single transport envelope for every authenticated PostPlus Cloud request:
5
+ * canonical header set (`accept` + compatibility headers + `Bearer` token +
6
+ * optional `content-type`), `AbortSignal.timeout`, and an optional once-only
7
+ * 401-refresh-retry. It returns the raw `Response` so each caller keeps its own
8
+ * `!ok` interpretation — this is a narrow transport primitive, not a request
9
+ * framework.
10
+ */
11
+ export async function sendAuthedCloudRequest(input) {
12
+ let response = await issueAuthedCloudRequest(input.auth, input);
13
+ if (response.status === 401 && input.retryOn401) {
14
+ const refreshedAuth = await input.retryOn401();
15
+ response = await issueAuthedCloudRequest(refreshedAuth, input);
16
+ }
17
+ return response;
18
+ }
19
+ async function issueAuthedCloudRequest(auth, input) {
20
+ const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders({
21
+ skillName: input.skillName ?? null,
22
+ });
23
+ const hasBody = input.body !== undefined;
24
+ const headers = {
25
+ accept: 'application/json',
26
+ ...compatibilityHeaders,
27
+ authorization: `Bearer ${auth.cliSessionToken}`,
28
+ };
29
+ if (hasBody) {
30
+ headers['content-type'] = 'application/json';
31
+ }
32
+ const requestUrl = new URL(input.pathName, normalizeBaseUrl(auth.apiBaseUrl));
33
+ return fetch(requestUrl, {
34
+ method: input.method ?? 'GET',
35
+ headers,
36
+ ...(hasBody ? { body: JSON.stringify(input.body) } : {}),
37
+ signal: AbortSignal.timeout(input.timeoutMs ?? DEFAULT_AUTHED_REQUEST_TIMEOUT_MS),
38
+ });
39
+ }
40
+ function normalizeBaseUrl(apiBaseUrl) {
41
+ return apiBaseUrl.endsWith('/') ? apiBaseUrl : `${apiBaseUrl}/`;
42
+ }
package/build/doctor.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { formatAccountBindingName } from './account-binding-display.js';
2
2
  import { resolveFreshRemoteAuth, } from './auth-session.js';
3
- import { buildPostPlusClientCompatibilityHeaders, formatPostPlusCompatibilityError, } from './client-compatibility.js';
3
+ import { sendAuthedCloudRequest } from './authed-cloud-request.js';
4
+ import { formatPostPlusCompatibilityError } from './client-compatibility.js';
4
5
  import { resolveHostedBaseUrl } from './hosted-release.js';
5
6
  import { formatLocalDependencyReport, generateLocalDependencyReport, } from './local-dependencies.js';
6
7
  import { loadPublicSkillCatalog, } from './skill-catalog.js';
@@ -136,13 +137,11 @@ function buildDoctorReport(checks, skillId) {
136
137
  }
137
138
  async function checkRemoteAuth(input) {
138
139
  try {
139
- let response = await requestWithAuth(input, '/api/postplus-cli/auth/whoami');
140
- if (response.status === 401) {
141
- const refreshedAuth = await resolveFreshRemoteAuth({
142
- forceRefresh: true,
143
- });
144
- response = await requestWithAuth(refreshedAuth, '/api/postplus-cli/auth/whoami');
145
- }
140
+ const response = await sendAuthedCloudRequest({
141
+ auth: input,
142
+ pathName: '/api/postplus-cli/auth/whoami',
143
+ retryOn401: () => resolveFreshRemoteAuth({ forceRefresh: true }),
144
+ });
146
145
  const payload = (await response.json());
147
146
  if (!response.ok) {
148
147
  return createFail('remote_auth', 'Remote auth', readErrorMessage(payload, 'PostPlus Cloud rejected the CLI session.'), 'Run `postplus auth login`.');
@@ -172,13 +171,11 @@ async function checkRemoteAuth(input) {
172
171
  }
173
172
  async function checkHostedCapabilities(input, skillScope) {
174
173
  try {
175
- let response = await requestWithAuth(input, '/api/postplus-cli/hosted/readiness');
176
- if (response.status === 401) {
177
- const refreshedAuth = await resolveFreshRemoteAuth({
178
- forceRefresh: true,
179
- });
180
- response = await requestWithAuth(refreshedAuth, '/api/postplus-cli/hosted/readiness');
181
- }
174
+ const response = await sendAuthedCloudRequest({
175
+ auth: input,
176
+ pathName: '/api/postplus-cli/hosted/readiness',
177
+ retryOn401: () => resolveFreshRemoteAuth({ forceRefresh: true }),
178
+ });
182
179
  const payload = (await response.json());
183
180
  if (!response.ok) {
184
181
  return createFail('hosted_capabilities', 'Hosted capabilities', readErrorMessage(payload, 'PostPlus Cloud hosted readiness check failed.'));
@@ -389,8 +386,15 @@ function collectHostedRequirementKeys(requirements) {
389
386
  ...requirements.sourceKeys,
390
387
  ]);
391
388
  }
389
+ // Capability families whose sub-keys are intentionally NOT expressible as catalog
390
+ // requirement keys: a skill requires the bare family capability, and any released
391
+ // sub-key readiness row satisfies it. social-publishing operations and
392
+ // public-content-discovery tools (e.g. web-search) both have no requirement-key
393
+ // binding, so requiring the family must match the whole family. Without this,
394
+ // `public-content-discovery:web-search` readiness is filtered out for skills that
395
+ // require `public-content-discovery`, producing a false "readiness check missing".
392
396
  function isWholeFamilyHostedCapability(prefix) {
393
- return prefix === 'social-publishing';
397
+ return prefix === 'public-content-discovery' || prefix === 'social-publishing';
394
398
  }
395
399
  function requiresSocialPublishingPlan(requirements) {
396
400
  return requirements.hostedCapabilities.includes('social-publishing');
@@ -452,17 +456,6 @@ function readErrorMessage(payload, fallback) {
452
456
  ? payload.error
453
457
  : fallback;
454
458
  }
455
- async function requestWithAuth(input, path) {
456
- const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
457
- return fetch(`${input.apiBaseUrl}${path}`, {
458
- headers: {
459
- accept: 'application/json',
460
- ...compatibilityHeaders,
461
- authorization: `Bearer ${input.cliSessionToken}`,
462
- },
463
- signal: AbortSignal.timeout(15000),
464
- });
465
- }
466
459
  export function formatDoctorReport(report) {
467
460
  const lines = ['PostPlus CLI doctor', ''];
468
461
  for (const check of report.checks) {
@@ -441,6 +441,10 @@ export const HOSTED_EXECUTION_MANIFESTS = {
441
441
  "class": "default",
442
442
  "flag": "--output-format",
443
443
  "type": "string",
444
+ "enumValues": [
445
+ "png",
446
+ "jpeg"
447
+ ],
444
448
  "default": "png",
445
449
  "required": false
446
450
  },
@@ -518,6 +522,10 @@ export const HOSTED_EXECUTION_MANIFESTS = {
518
522
  "class": "default",
519
523
  "flag": "--output-format",
520
524
  "type": "string",
525
+ "enumValues": [
526
+ "png",
527
+ "jpeg"
528
+ ],
521
529
  "default": "png",
522
530
  "required": false
523
531
  },
@@ -576,6 +584,10 @@ export const HOSTED_EXECUTION_MANIFESTS = {
576
584
  "class": "default",
577
585
  "flag": "--output-format",
578
586
  "type": "string",
587
+ "enumValues": [
588
+ "png",
589
+ "jpeg"
590
+ ],
579
591
  "default": "png",
580
592
  "required": false
581
593
  },
@@ -634,6 +646,10 @@ export const HOSTED_EXECUTION_MANIFESTS = {
634
646
  "class": "default",
635
647
  "flag": "--output-format",
636
648
  "type": "string",
649
+ "enumValues": [
650
+ "png",
651
+ "jpeg"
652
+ ],
637
653
  "default": "png",
638
654
  "required": false
639
655
  },
@@ -692,6 +708,10 @@ export const HOSTED_EXECUTION_MANIFESTS = {
692
708
  "class": "default",
693
709
  "flag": "--output-format",
694
710
  "type": "string",
711
+ "enumValues": [
712
+ "png",
713
+ "jpeg"
714
+ ],
695
715
  "default": "png",
696
716
  "required": false
697
717
  },
@@ -758,6 +778,10 @@ export const HOSTED_EXECUTION_MANIFESTS = {
758
778
  "class": "default",
759
779
  "flag": "--output-format",
760
780
  "type": "string",
781
+ "enumValues": [
782
+ "png",
783
+ "jpeg"
784
+ ],
761
785
  "default": "png",
762
786
  "required": false
763
787
  },
@@ -824,6 +848,10 @@ export const HOSTED_EXECUTION_MANIFESTS = {
824
848
  "class": "default",
825
849
  "flag": "--output-format",
826
850
  "type": "string",
851
+ "enumValues": [
852
+ "png",
853
+ "jpeg"
854
+ ],
827
855
  "default": "png",
828
856
  "required": false
829
857
  },
@@ -890,6 +918,10 @@ export const HOSTED_EXECUTION_MANIFESTS = {
890
918
  "class": "default",
891
919
  "flag": "--output-format",
892
920
  "type": "string",
921
+ "enumValues": [
922
+ "png",
923
+ "jpeg"
924
+ ],
893
925
  "default": "png",
894
926
  "required": false
895
927
  },
@@ -1308,7 +1340,6 @@ export const HOSTED_EXECUTION_MANIFESTS = {
1308
1340
  "flag": null,
1309
1341
  "type": "string",
1310
1342
  "enumValues": [
1311
- "480p",
1312
1343
  "720p",
1313
1344
  "1080p"
1314
1345
  ],
@@ -1467,7 +1498,6 @@ export const HOSTED_EXECUTION_MANIFESTS = {
1467
1498
  "flag": null,
1468
1499
  "type": "string",
1469
1500
  "enumValues": [
1470
- "480p",
1471
1501
  "720p",
1472
1502
  "1080p"
1473
1503
  ],
@@ -2388,6 +2418,19 @@ export const HOSTED_EXECUTION_MANIFESTS = {
2388
2418
  "class": "default",
2389
2419
  "flag": null,
2390
2420
  "type": "string",
2421
+ "enumValues": [
2422
+ "auto",
2423
+ "Chinese",
2424
+ "English",
2425
+ "German",
2426
+ "Italian",
2427
+ "Portuguese",
2428
+ "Spanish",
2429
+ "Japanese",
2430
+ "Korean",
2431
+ "French",
2432
+ "Russian"
2433
+ ],
2391
2434
  "default": "auto",
2392
2435
  "required": false
2393
2436
  },
@@ -2448,6 +2491,19 @@ export const HOSTED_EXECUTION_MANIFESTS = {
2448
2491
  "class": "default",
2449
2492
  "flag": null,
2450
2493
  "type": "string",
2494
+ "enumValues": [
2495
+ "auto",
2496
+ "Chinese",
2497
+ "English",
2498
+ "German",
2499
+ "Italian",
2500
+ "Portuguese",
2501
+ "Spanish",
2502
+ "Japanese",
2503
+ "Korean",
2504
+ "French",
2505
+ "Russian"
2506
+ ],
2451
2507
  "default": "auto",
2452
2508
  "required": false
2453
2509
  },
@@ -3,9 +3,10 @@ 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 { buildPostPlusClientCompatibilityHeaders, formatPostPlusCompatibilityError, } from './client-compatibility.js';
7
- import { buildHostedRequestSchemaReport, buildMediaGenerationRequestDimensions, } from './hosted-request-schemas.js';
6
+ import { sendAuthedCloudRequest } from './authed-cloud-request.js';
7
+ import { formatPostPlusCompatibilityError } from './client-compatibility.js';
8
8
  import { buildVerbTargetIndex, } from './hosted-manifest-index.js';
9
+ import { buildHostedRequestSchemaReport, buildMediaGenerationRequestDimensions, } from './hosted-request-schemas.js';
9
10
  import { readLargeCreditQuoteConfirmationChallenge, } from './quote-confirmation.js';
10
11
  // Manifest-driven verb grammar indexes (SSOT projected from apps/web +
11
12
  // public-skill-metadata via the generated manifest). The verb/flag grammar,
@@ -66,7 +67,9 @@ export async function runHostedDomainCommand(domain, args) {
66
67
  if (domain === 'media' && subcommand === 'poll') {
67
68
  return runMediaPoll(rest);
68
69
  }
69
- if (domain === 'media' && subcommand && MEDIA_VERB_ENDPOINTS.has(subcommand)) {
70
+ if (domain === 'media' &&
71
+ subcommand &&
72
+ MEDIA_VERB_ENDPOINTS.has(subcommand)) {
70
73
  return runMediaVerb(subcommand, rest);
71
74
  }
72
75
  // publish: the OPERATION is the subcommand (no separate target positional).
@@ -217,8 +220,7 @@ async function runMediaVerbRequestJson(args) {
217
220
  // Runner-managed fields are minted/derived by the CLI; reject them in the body so
218
221
  // the agent cannot smuggle in ids, tokens, or billing dimensions.
219
222
  for (const field of endpoint.fields) {
220
- if (field.class === 'runner-managed' &&
221
- Object.hasOwn(input, field.name)) {
223
+ if (field.class === 'runner-managed' && Object.hasOwn(input, field.name)) {
222
224
  throw new Error(`media ${verb} ${endpointKey} input must not include runner-managed field "${field.name}"; the CLI mints or derives it.`);
223
225
  }
224
226
  }
@@ -244,8 +246,11 @@ function requireResolvedEndpoint(resolved, verb, endpointKey) {
244
246
  // video-analysis verb (request-json surface). The agent authors an opaque Gemini
245
247
  // request object (contents + generationConfig) in `--request <file>`; capability,
246
248
  // operation, and modelKey come from the verb + positional, so the body posts
247
- // EXACTLY the locked Web contract. There is no field classification and no
248
- // estimatedUsage — the payload is forwarded verbatim as the Gemini request.
249
+ // EXACTLY the locked Web contract. There is no field classification; the payload
250
+ // is forwarded verbatim as the Gemini request. The optional `--video-seconds`
251
+ // flag is the one runner-supplied hint: when provided it is sent as
252
+ // `estimatedUsage.videoSeconds` so the Web boundary can route eligible short
253
+ // videos through its preflight/routing path (omit it to use the default route).
249
254
  async function runVideoAnalysisVerb(args) {
250
255
  const { modelKey, resolved, verb } = args;
251
256
  const flags = parseFlags(args.args, new Set(['json']));
@@ -256,6 +261,7 @@ async function runVideoAnalysisVerb(args) {
256
261
  'quote-confirmation-token',
257
262
  'request',
258
263
  'skill',
264
+ 'video-seconds',
259
265
  ]);
260
266
  for (const key of [...flags.values.keys(), ...flags.booleans]) {
261
267
  if (!allowedKeys.has(key)) {
@@ -269,11 +275,26 @@ async function runVideoAnalysisVerb(args) {
269
275
  throw new Error(`media ${verb} ${modelKey} --request must be a JSON object of Gemini request payload.`);
270
276
  }
271
277
  const payload = raw;
278
+ // Optional runner-supplied hint: the source video duration. When provided it is
279
+ // forwarded as estimatedUsage.videoSeconds so the Web boundary's video-analysis
280
+ // routing/preflight can consider eligible short videos; omitting it leaves the
281
+ // request on the default route. The CLI does not probe the media itself (no
282
+ // ffprobe in the open-source runner) — it only passes a value the caller knows.
283
+ const videoSecondsFlag = flags.values.get('video-seconds') ?? null;
284
+ let estimatedUsage;
285
+ if (videoSecondsFlag !== null) {
286
+ const parsed = Number(videoSecondsFlag);
287
+ if (!Number.isFinite(parsed) || parsed <= 0) {
288
+ throw new Error(`media ${verb} --video-seconds must be a positive number of seconds.`);
289
+ }
290
+ estimatedUsage = { videoSeconds: parsed };
291
+ }
272
292
  const body = {
273
293
  capability: 'video-analysis',
274
294
  operation: 'analyze',
275
295
  modelKey,
276
296
  payload,
297
+ ...(estimatedUsage ? { estimatedUsage } : {}),
277
298
  operationId: flags.values.get('hosted-operation-id') ??
278
299
  `postplus-cli:media:video-analysis:analyze:${randomUUID()}`,
279
300
  quoteConfirmationToken: flags.values.get('quote-confirmation-token') ?? undefined,
@@ -289,14 +310,12 @@ async function runVideoAnalysisVerb(args) {
289
310
  outputPath,
290
311
  });
291
312
  }
292
- // `media-file upload`: the generic local-file -> hosted storageReference verb.
293
- // Released skills ship no scripts, so any skill that must place a local file
294
- // behind a hosted reference before a hosted request (e.g. video-analysis before
295
- // `media analyze`) drives it through this verb. It is capability-generic: it has
296
- // no knowledge of any one skill's request payload, it only mints an opaque
297
- // storageReference the agent then embeds in its own hosted request. The runner
298
- // holds no provider keys — it asks the Web boundary for a signed upload target,
299
- // PUTs the bytes, and returns the storageReference the Web boundary issued.
313
+ // `media-file upload`: the generic local-file -> hosted media verb. Released
314
+ // skills ship no scripts, so a skill that must place a local file behind hosted
315
+ // media first drives it through this verb. It is capability-generic: it knows no
316
+ // skill request payload. The runner asks the Web boundary for a signed upload
317
+ // target, PUTs bytes outside the JSON envelope, then asks the hosted provider
318
+ // upload operation for the reusable provider-facing result.
300
319
  const MEDIA_FILE_MIME_BY_EXTENSION = {
301
320
  '.gif': 'image/gif',
302
321
  '.jpeg': 'image/jpeg',
@@ -347,6 +366,7 @@ async function runMediaFileUpload(args) {
347
366
  }
348
367
  const mimeType = flags.values.get('mime') ?? inferUploadMimeType(absolutePath);
349
368
  const outputPath = flags.values.get('output') ?? null;
369
+ const hostedOperationId = flags.values.get('hosted-operation-id') ?? null;
350
370
  const body = {
351
371
  capability: 'media-file',
352
372
  operation: 'create-upload-url',
@@ -355,7 +375,7 @@ async function runMediaFileUpload(args) {
355
375
  name: path.basename(absolutePath),
356
376
  sizeBytes: fileStat.size,
357
377
  },
358
- operationId: flags.values.get('hosted-operation-id') ??
378
+ operationId: hostedOperationId ??
359
379
  `postplus-cli:media-file:create-upload-url:${randomUUID()}`,
360
380
  quoteConfirmationToken: flags.values.get('quote-confirmation-token') ?? undefined,
361
381
  };
@@ -368,8 +388,25 @@ async function runMediaFileUpload(args) {
368
388
  });
369
389
  const output = readHostedUploadOutput(payload);
370
390
  const signedUpload = readSignedUpload(output);
391
+ const storageReference = readStorageReferenceValue(output);
371
392
  await putHostedMediaBytes(signedUpload, absolutePath);
372
- return { storageReference: readStorageReferenceValue(output) };
393
+ return await postHostedJson({
394
+ body: {
395
+ capability: 'media-file',
396
+ operation: 'upload',
397
+ file: {
398
+ mimeType,
399
+ name: path.basename(absolutePath),
400
+ storageReference,
401
+ },
402
+ operationId: hostedOperationId
403
+ ? `${hostedOperationId}:upload`
404
+ : `postplus-cli:media-file:upload:${randomUUID()}`,
405
+ quoteConfirmationToken: flags.values.get('quote-confirmation-token') ?? undefined,
406
+ },
407
+ pathName: '/api/postplus-cli/hosted/capability',
408
+ skillName: flags.values.get('skill') ?? null,
409
+ });
373
410
  },
374
411
  errorInputLabel: inputFile,
375
412
  json: flags.booleans.has('json'),
@@ -562,7 +599,7 @@ async function runResearchCollect(args) {
562
599
  const outputPath = flags.values.get('output') ?? null;
563
600
  return runHostedCommand({
564
601
  request: () => postHostedJson({
565
- body: { runHandle },
602
+ body: { runHandle, runHandleType: 'public-content-collection' },
566
603
  pathName: '/api/postplus-cli/hosted/collection',
567
604
  skillName: null,
568
605
  }),
@@ -800,24 +837,16 @@ async function runHostedSchema(domain, args) {
800
837
  return 0;
801
838
  }
802
839
  async function postHostedJson(input) {
803
- let auth = await resolveFreshRemoteAuth();
804
- let response = await postJson({
805
- apiBaseUrl: auth.apiBaseUrl,
840
+ const auth = await resolveFreshRemoteAuth();
841
+ const response = await sendAuthedCloudRequest({
842
+ auth,
806
843
  body: input.body,
807
- cliSessionToken: auth.cliSessionToken,
844
+ method: 'POST',
808
845
  pathName: input.pathName,
846
+ retryOn401: () => resolveFreshRemoteAuth({ forceRefresh: true }),
809
847
  skillName: input.skillName,
848
+ timeoutMs: 120000,
810
849
  });
811
- if (response.status === 401) {
812
- auth = await resolveFreshRemoteAuth({ forceRefresh: true });
813
- response = await postJson({
814
- apiBaseUrl: auth.apiBaseUrl,
815
- body: input.body,
816
- cliSessionToken: auth.cliSessionToken,
817
- pathName: input.pathName,
818
- skillName: input.skillName,
819
- });
820
- }
821
850
  const payload = await readJsonResponse(response);
822
851
  if (!response.ok) {
823
852
  const productError = readHostedProductError(payload);
@@ -876,22 +905,6 @@ async function writeQuoteConfirmationChallenge(error, input) {
876
905
  });
877
906
  return challengePath;
878
907
  }
879
- async function postJson(input) {
880
- const headers = await buildPostPlusClientCompatibilityHeaders({
881
- skillName: input.skillName,
882
- });
883
- return fetch(`${input.apiBaseUrl}${input.pathName}`, {
884
- body: JSON.stringify(input.body),
885
- headers: {
886
- accept: 'application/json',
887
- authorization: `Bearer ${input.cliSessionToken}`,
888
- ...headers,
889
- 'content-type': 'application/json',
890
- },
891
- method: 'POST',
892
- signal: AbortSignal.timeout(120000),
893
- });
894
- }
895
908
  async function readJsonResponse(response) {
896
909
  const text = await response.text();
897
910
  if (!text.trim()) {
@@ -1028,10 +1041,13 @@ function printMediaEndpointHelp(domain, verb, targetKey, resolved) {
1028
1041
 
1029
1042
  Surface: request-json (opaque Gemini request payload)
1030
1043
  Usage:
1031
- postplus ${domain} ${verb} ${targetKey} --request <input.json> [--json] [--output <result.json>]
1044
+ postplus ${domain} ${verb} ${targetKey} --request <input.json> [--video-seconds <n>] [--json] [--output <result.json>]
1032
1045
 
1033
1046
  --request <file> A JSON object authored verbatim as the Gemini request
1034
1047
  (contents + optional generationConfig) under "payload".
1048
+ --video-seconds <n> Optional source video duration in seconds. Supplying it
1049
+ lets the hosted boundary route eligible short videos through
1050
+ its preflight path; omit it to use the default route.
1035
1051
  Runner-managed (minted by the CLI; never in the body): operationId, quoteConfirmationToken
1036
1052
  `);
1037
1053
  return;
@@ -1116,7 +1132,9 @@ function printOpaqueTargetHelp(domain, verb, targetKey, resolved) {
1116
1132
  : 'a provider-shaped JSON object of input';
1117
1133
  // publish's operation is both the verb and the target, so the header/usage show
1118
1134
  // it once; research shows `<verb> <target>`.
1119
- const header = domain === 'publish' ? `publish ${targetKey}` : `research ${verb} ${targetKey}`;
1135
+ const header = domain === 'publish'
1136
+ ? `publish ${targetKey}`
1137
+ : `research ${verb} ${targetKey}`;
1120
1138
  const usage = domain === 'publish'
1121
1139
  ? ` postplus publish ${targetKey} --request <input.json> [--json] [--output <result.json>]`
1122
1140
  : ` postplus research ${verb} ${targetKey} --request <input.json> [--skill <skill-id>] [--max-charge-usd <usd>] [--json] [--output <result.json>]`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postplus/cli",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017",
5
5
  "type": "module",
6
6
  "description": "PostPlus CLI for PostPlus Cloud auth, status, and diagnostics.",
@@ -12,6 +12,7 @@
12
12
  "build/auth-session.js",
13
13
  "build/auth-validate.js",
14
14
  "build/auth.js",
15
+ "build/authed-cloud-request.js",
15
16
  "build/client-compatibility.js",
16
17
  "build/command-runner.js",
17
18
  "build/doctor.js",