@postplus/cli 0.1.42 → 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.'));
@@ -459,17 +456,6 @@ function readErrorMessage(payload, fallback) {
459
456
  ? payload.error
460
457
  : fallback;
461
458
  }
462
- async function requestWithAuth(input, path) {
463
- const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
464
- return fetch(`${input.apiBaseUrl}${path}`, {
465
- headers: {
466
- accept: 'application/json',
467
- ...compatibilityHeaders,
468
- authorization: `Bearer ${input.cliSessionToken}`,
469
- },
470
- signal: AbortSignal.timeout(15000),
471
- });
472
- }
473
459
  export function formatDoctorReport(report) {
474
460
  const lines = ['PostPlus CLI doctor', ''];
475
461
  for (const check of report.checks) {
@@ -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
  }
@@ -308,14 +310,12 @@ async function runVideoAnalysisVerb(args) {
308
310
  outputPath,
309
311
  });
310
312
  }
311
- // `media-file upload`: the generic local-file -> hosted storageReference verb.
312
- // Released skills ship no scripts, so any skill that must place a local file
313
- // behind a hosted reference before a hosted request (e.g. video-analysis before
314
- // `media analyze`) drives it through this verb. It is capability-generic: it has
315
- // no knowledge of any one skill's request payload, it only mints an opaque
316
- // storageReference the agent then embeds in its own hosted request. The runner
317
- // holds no provider keys — it asks the Web boundary for a signed upload target,
318
- // 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.
319
319
  const MEDIA_FILE_MIME_BY_EXTENSION = {
320
320
  '.gif': 'image/gif',
321
321
  '.jpeg': 'image/jpeg',
@@ -366,6 +366,7 @@ async function runMediaFileUpload(args) {
366
366
  }
367
367
  const mimeType = flags.values.get('mime') ?? inferUploadMimeType(absolutePath);
368
368
  const outputPath = flags.values.get('output') ?? null;
369
+ const hostedOperationId = flags.values.get('hosted-operation-id') ?? null;
369
370
  const body = {
370
371
  capability: 'media-file',
371
372
  operation: 'create-upload-url',
@@ -374,7 +375,7 @@ async function runMediaFileUpload(args) {
374
375
  name: path.basename(absolutePath),
375
376
  sizeBytes: fileStat.size,
376
377
  },
377
- operationId: flags.values.get('hosted-operation-id') ??
378
+ operationId: hostedOperationId ??
378
379
  `postplus-cli:media-file:create-upload-url:${randomUUID()}`,
379
380
  quoteConfirmationToken: flags.values.get('quote-confirmation-token') ?? undefined,
380
381
  };
@@ -387,8 +388,25 @@ async function runMediaFileUpload(args) {
387
388
  });
388
389
  const output = readHostedUploadOutput(payload);
389
390
  const signedUpload = readSignedUpload(output);
391
+ const storageReference = readStorageReferenceValue(output);
390
392
  await putHostedMediaBytes(signedUpload, absolutePath);
391
- 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
+ });
392
410
  },
393
411
  errorInputLabel: inputFile,
394
412
  json: flags.booleans.has('json'),
@@ -581,7 +599,7 @@ async function runResearchCollect(args) {
581
599
  const outputPath = flags.values.get('output') ?? null;
582
600
  return runHostedCommand({
583
601
  request: () => postHostedJson({
584
- body: { runHandle },
602
+ body: { runHandle, runHandleType: 'public-content-collection' },
585
603
  pathName: '/api/postplus-cli/hosted/collection',
586
604
  skillName: null,
587
605
  }),
@@ -819,24 +837,16 @@ async function runHostedSchema(domain, args) {
819
837
  return 0;
820
838
  }
821
839
  async function postHostedJson(input) {
822
- let auth = await resolveFreshRemoteAuth();
823
- let response = await postJson({
824
- apiBaseUrl: auth.apiBaseUrl,
840
+ const auth = await resolveFreshRemoteAuth();
841
+ const response = await sendAuthedCloudRequest({
842
+ auth,
825
843
  body: input.body,
826
- cliSessionToken: auth.cliSessionToken,
844
+ method: 'POST',
827
845
  pathName: input.pathName,
846
+ retryOn401: () => resolveFreshRemoteAuth({ forceRefresh: true }),
828
847
  skillName: input.skillName,
848
+ timeoutMs: 120000,
829
849
  });
830
- if (response.status === 401) {
831
- auth = await resolveFreshRemoteAuth({ forceRefresh: true });
832
- response = await postJson({
833
- apiBaseUrl: auth.apiBaseUrl,
834
- body: input.body,
835
- cliSessionToken: auth.cliSessionToken,
836
- pathName: input.pathName,
837
- skillName: input.skillName,
838
- });
839
- }
840
850
  const payload = await readJsonResponse(response);
841
851
  if (!response.ok) {
842
852
  const productError = readHostedProductError(payload);
@@ -895,22 +905,6 @@ async function writeQuoteConfirmationChallenge(error, input) {
895
905
  });
896
906
  return challengePath;
897
907
  }
898
- async function postJson(input) {
899
- const headers = await buildPostPlusClientCompatibilityHeaders({
900
- skillName: input.skillName,
901
- });
902
- return fetch(`${input.apiBaseUrl}${input.pathName}`, {
903
- body: JSON.stringify(input.body),
904
- headers: {
905
- accept: 'application/json',
906
- authorization: `Bearer ${input.cliSessionToken}`,
907
- ...headers,
908
- 'content-type': 'application/json',
909
- },
910
- method: 'POST',
911
- signal: AbortSignal.timeout(120000),
912
- });
913
- }
914
908
  async function readJsonResponse(response) {
915
909
  const text = await response.text();
916
910
  if (!text.trim()) {
@@ -1138,7 +1132,9 @@ function printOpaqueTargetHelp(domain, verb, targetKey, resolved) {
1138
1132
  : 'a provider-shaped JSON object of input';
1139
1133
  // publish's operation is both the verb and the target, so the header/usage show
1140
1134
  // it once; research shows `<verb> <target>`.
1141
- const header = domain === 'publish' ? `publish ${targetKey}` : `research ${verb} ${targetKey}`;
1135
+ const header = domain === 'publish'
1136
+ ? `publish ${targetKey}`
1137
+ : `research ${verb} ${targetKey}`;
1142
1138
  const usage = domain === 'publish'
1143
1139
  ? ` postplus publish ${targetKey} --request <input.json> [--json] [--output <result.json>]`
1144
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.42",
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",