@postplus/cli 0.1.33 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,7 +33,10 @@ Requires Node.js and npm.
33
33
  ```bash
34
34
  npm install -g @postplus/cli@latest
35
35
  postplus auth login
36
- npx -y skills add PostPlusAI/postplus-skills --global --full-depth --skill '*' --agent claude-code codex cursor github-copilot windsurf trae trae-cn openclaw hermes-agent --yes
36
+ POSTPLUS_AGENT_TARGETS="claude-code codex cursor github-copilot windsurf trae trae-cn openclaw hermes-agent"
37
+ for agent in $POSTPLUS_AGENT_TARGETS; do
38
+ npx -y skills add PostPlusAI/postplus-skills --global --full-depth --skill '*' --agent "$agent" --yes
39
+ done
37
40
  postplus skills verify
38
41
  ```
39
42
 
@@ -41,7 +44,10 @@ If you explicitly do not want global skills, run the install from the target
41
44
  project directory and omit `--global`:
42
45
 
43
46
  ```bash
44
- npx -y skills add PostPlusAI/postplus-skills --full-depth --skill '*' --agent claude-code codex cursor github-copilot windsurf trae trae-cn openclaw hermes-agent --yes
47
+ POSTPLUS_AGENT_TARGETS="claude-code codex cursor github-copilot windsurf trae trae-cn openclaw hermes-agent"
48
+ for agent in $POSTPLUS_AGENT_TARGETS; do
49
+ npx -y skills add PostPlusAI/postplus-skills --full-depth --skill '*' --agent "$agent" --yes
50
+ done
45
51
  ```
46
52
 
47
53
  Useful checks:
@@ -51,6 +57,22 @@ postplus status
51
57
  npx -y skills add PostPlusAI/postplus-skills --global --list
52
58
  ```
53
59
 
60
+ Hosted request schema discovery:
61
+
62
+ ```bash
63
+ postplus research schema --collection-key <collection-key> --json
64
+ postplus media schema --endpoint <endpoint-key> --json
65
+ postplus publish schema --json
66
+ postplus mobile schema --json
67
+ ```
68
+
69
+ Use these schema commands before an agent writes a `--input` or `--request`
70
+ JSON file for a hosted PostPlus command. For media work, run
71
+ `postplus media schema --json` first to list `endpointKeys`, then rerun with
72
+ the selected `--endpoint`. For research work, run
73
+ `postplus research schema --json` first to list `collectionKeys`, then rerun
74
+ with the selected `--collection-key`.
75
+
54
76
  ## Local Studio
55
77
 
56
78
  For heavier skills that benefit from a visual workspace, use the CLI-managed
@@ -384,7 +406,7 @@ Start from the job, not the file name.
384
406
 
385
407
  1. Describe the outcome you want in natural language.
386
408
  2. If you are unsure which workflow fits, start with a router skill.
387
- 3. Use `skills/INDEX.md` as the detailed map of active skills, boundaries, and handoffs.
409
+ 3. Use `skills/catalog.json` for machine-readable released skill metadata.
388
410
  4. Read the target `SKILL.md` before execution.
389
411
  5. Follow shared rulebooks when the task crosses platforms, products, ads, media, or publishing.
390
412
  6. Chain only the minimum skills needed to produce the next useful artifact.
@@ -392,9 +414,9 @@ Start from the job, not the file name.
392
414
  Important files:
393
415
 
394
416
  - `README.md`: this first-time onboarding page
395
- - `skills/INDEX.md`: detailed agent-facing navigation map
396
417
  - `skills/README.md`: short runtime catalog notes
397
- - `skills/shared-*.md`: shared routing and judgment rules
418
+ - `skills/catalog.json`: released skill metadata for CLI and verification
419
+ - `skills/00-shared/postplus-shared/references/`: shared routing and judgment rules
398
420
  - each `SKILL.md`: the workflow contract for one specific capability
399
421
 
400
422
  ## First Requests To Try
@@ -0,0 +1,21 @@
1
+ export function formatAccountBindingLines(input) {
2
+ if (input.accountType === 'team') {
3
+ return [
4
+ `Workspace: ${formatAccountBindingName(input)}`,
5
+ ...(input.accountSlug ? [`Workspace slug: ${input.accountSlug}`] : []),
6
+ `Account ID: ${input.accountId ?? 'not bound'}`,
7
+ ];
8
+ }
9
+ return [
10
+ `Account: ${input.accountName ?? 'not bound'}`,
11
+ `Account ID: ${input.accountId ?? 'not bound'}`,
12
+ ];
13
+ }
14
+ export function formatAccountBindingName(input) {
15
+ if (!input.accountName) {
16
+ return input.accountId ? `Account ${input.accountId}` : 'not bound';
17
+ }
18
+ return input.accountType === 'team'
19
+ ? `${input.accountName} (team)`
20
+ : input.accountName;
21
+ }
@@ -1,3 +1,4 @@
1
+ import { formatAccountBindingLines } from './account-binding-display.js';
1
2
  import { refreshRemoteAuthSession } from './auth-session.js';
2
3
  import { clearAuthState, generateAuthStatusReport } from './auth.js';
3
4
  import { buildPostPlusClientCompatibilityHeaders, formatPostPlusCompatibilityError, } from './client-compatibility.js';
@@ -8,6 +9,9 @@ export async function refreshRemoteAuth() {
8
9
  const refreshed = await refreshRemoteAuthSession();
9
10
  return {
10
11
  accountId: refreshed.accountId,
12
+ accountName: refreshed.accountName,
13
+ accountSlug: refreshed.accountSlug,
14
+ accountType: refreshed.accountType,
11
15
  apiBaseUrl: refreshed.apiBaseUrl,
12
16
  ok: true,
13
17
  subscriptionStatus: refreshed.subscriptionStatus,
@@ -21,7 +25,7 @@ export function formatAuthRefreshReport(report) {
21
25
  '',
22
26
  `Remote auth: ${report.ok ? 'OK' : 'FAILED'}`,
23
27
  `PostPlus Cloud: ${report.apiBaseUrl}`,
24
- `Account: ${report.accountId}`,
28
+ ...formatAccountBindingLines(report),
25
29
  `User: ${report.userEmail ?? report.userId}`,
26
30
  `Subscription: ${readSubscriptionStatusField(report).label}`,
27
31
  ].join('\n');
@@ -34,6 +34,9 @@ export async function loginWithCloudHandoff() {
34
34
  });
35
35
  await setLocalSession({
36
36
  accountId: validated.accountId,
37
+ accountName: validated.accountName,
38
+ accountSlug: validated.accountSlug,
39
+ accountType: validated.accountType,
37
40
  apiBaseUrl: baseUrl,
38
41
  cliSessionToken: handoffPayload.cliSessionToken,
39
42
  sessionExpiresAt: validated.sessionExpiresAt ?? handoffPayload.sessionExpiresAt ?? null,
@@ -43,6 +46,9 @@ export async function loginWithCloudHandoff() {
43
46
  await writeCurrentCliVersionToLocalConfig();
44
47
  return {
45
48
  accountId: validated.accountId,
49
+ accountName: validated.accountName,
50
+ accountSlug: validated.accountSlug,
51
+ accountType: validated.accountType,
46
52
  apiBaseUrl: baseUrl,
47
53
  ok: true,
48
54
  userEmail: validated.userEmail,
@@ -135,6 +141,9 @@ export async function validateCliSession(input) {
135
141
  if (!response.ok) {
136
142
  throw new Error(formatCliSessionAuthError(payload));
137
143
  }
144
+ if (!isValidatedCliSessionPayload(payload)) {
145
+ throw new Error('PostPlus CLI auth validation returned incomplete data.');
146
+ }
138
147
  return payload;
139
148
  }
140
149
  export function formatCliSessionAuthError(payload) {
@@ -168,11 +177,25 @@ function isCliAuthLoginCompletedPayload(payload) {
168
177
  payload.status === 'completed' &&
169
178
  typeof payload.cliSessionToken === 'string' &&
170
179
  typeof payload.accountId === 'string' &&
180
+ typeof payload.accountName === 'string' &&
181
+ (payload.accountSlug === null || typeof payload.accountSlug === 'string') &&
182
+ (payload.accountType === 'personal' || payload.accountType === 'team') &&
171
183
  typeof payload.userId === 'string');
172
184
  }
173
185
  function isCliAuthLoginPendingPayload(payload) {
174
186
  return 'status' in payload && payload.status === 'pending';
175
187
  }
188
+ function isValidatedCliSessionPayload(payload) {
189
+ return (typeof payload === 'object' &&
190
+ payload !== null &&
191
+ typeof payload.accountId === 'string' &&
192
+ typeof payload.accountName === 'string' &&
193
+ (payload.accountSlug === null ||
194
+ typeof payload.accountSlug === 'string') &&
195
+ (payload.accountType === 'personal' ||
196
+ payload.accountType === 'team') &&
197
+ typeof payload.userId === 'string');
198
+ }
176
199
  function formatRemoteAuthLoginError(payload) {
177
200
  const compatibilityError = formatPostPlusCompatibilityError(payload);
178
201
  if (compatibilityError) {
@@ -66,6 +66,9 @@ export async function refreshRemoteAuthSession(input) {
66
66
  }
67
67
  await setLocalSession({
68
68
  accountId: payload.accountId,
69
+ accountName: payload.accountName,
70
+ accountSlug: payload.accountSlug,
71
+ accountType: payload.accountType,
69
72
  apiBaseUrl,
70
73
  cliSessionToken: payload.cliSessionToken,
71
74
  sessionExpiresAt: payload.sessionExpiresAt,
@@ -86,5 +89,10 @@ function isRemoteAuthRefreshSuccessPayload(payload) {
86
89
  payload.cliSessionToken.trim().length >
87
90
  0 &&
88
91
  typeof payload.accountId === 'string' &&
92
+ typeof payload.accountName === 'string' &&
93
+ (payload.accountSlug === null ||
94
+ typeof payload.accountSlug === 'string') &&
95
+ (payload.accountType === 'personal' ||
96
+ payload.accountType === 'team') &&
89
97
  typeof payload.userId === 'string');
90
98
  }
@@ -1,3 +1,4 @@
1
+ import { formatAccountBindingLines } from './account-binding-display.js';
1
2
  import { resolveFreshRemoteAuth } from './auth-session.js';
2
3
  import { buildPostPlusClientCompatibilityHeaders, formatPostPlusCompatibilityError, } from './client-compatibility.js';
3
4
  import { readSubscriptionStatusField } from './subscription-status.js';
@@ -22,10 +23,16 @@ export async function validateRemoteAuth() {
22
23
  }
23
24
  const hasSubscriptionStatus = Object.prototype.hasOwnProperty.call(payload, 'subscriptionStatus');
24
25
  const accountId = readRequiredString(payload, 'accountId');
26
+ const accountName = readRequiredString(payload, 'accountName');
27
+ const accountSlug = readNullableString(payload, 'accountSlug');
28
+ const accountType = readAccountType(payload);
25
29
  const userId = readRequiredString(payload, 'userId');
26
30
  const userEmail = readNullableString(payload, 'userEmail');
27
31
  return {
28
32
  accountId,
33
+ accountName,
34
+ accountSlug,
35
+ accountType,
29
36
  apiBaseUrl: auth.apiBaseUrl,
30
37
  ok: true,
31
38
  source: auth.source,
@@ -42,11 +49,18 @@ export function formatAuthValidateReport(report) {
42
49
  '',
43
50
  `Remote auth: ${report.ok ? 'OK' : 'FAILED'}`,
44
51
  `PostPlus Cloud: ${report.apiBaseUrl}`,
45
- `Account: ${report.accountId}`,
52
+ ...formatAccountBindingLines(report),
46
53
  `User: ${report.userEmail ?? report.userId}`,
47
54
  `Subscription: ${readSubscriptionStatusField(report).label}`,
48
55
  ].join('\n');
49
56
  }
57
+ function readAccountType(payload) {
58
+ const value = payload.accountType;
59
+ if (value !== 'personal' && value !== 'team') {
60
+ throw new Error('Invalid PostPlus auth response: accountType must be personal or team.');
61
+ }
62
+ return value;
63
+ }
50
64
  async function fetchWhoami(input) {
51
65
  const compatibilityHeaders = await buildPostPlusClientCompatibilityHeaders();
52
66
  return fetch(`${input.apiBaseUrl}/api/postplus-cli/auth/whoami`, {
package/build/auth.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { formatAccountBindingLines } from './account-binding-display.js';
1
2
  import { clearLocalAuthState, getPostPlusConfigPath, hasLocalConfigFile, maskSecret, readLocalConfig, resolveApiBaseUrlState, resolveLocalSessionState, setLocalApiBaseUrl, } from './local-state.js';
2
3
  export async function generateAuthStatusReport() {
3
4
  const [sessionState, apiBaseUrlState, configExists, config] = await Promise.all([
@@ -22,6 +23,15 @@ export async function generateAuthStatusReport() {
22
23
  path: getPostPlusConfigPath(),
23
24
  exists: configExists,
24
25
  accountId: config?.accountId?.trim() || null,
26
+ accountName: typeof config?.accountName === 'string'
27
+ ? config.accountName.trim() || null
28
+ : null,
29
+ accountSlug: typeof config?.accountSlug === 'string'
30
+ ? config.accountSlug.trim() || null
31
+ : null,
32
+ accountType: config?.accountType === 'personal' || config?.accountType === 'team'
33
+ ? config.accountType
34
+ : null,
25
35
  sessionExpiresAt: typeof config?.sessionExpiresAt === 'number'
26
36
  ? config.sessionExpiresAt
27
37
  : null,
@@ -47,7 +57,12 @@ export function formatAuthStatusReport(report) {
47
57
  lines.push(report.config.exists
48
58
  ? `[PASS] local config: ${report.config.path}`
49
59
  : `[PASS] local config path: ${report.config.path}`);
50
- lines.push(` Account: ${report.config.accountId ?? 'not bound'}`);
60
+ lines.push(...formatAccountBindingLines({
61
+ accountId: report.config.accountId,
62
+ accountName: report.config.accountName,
63
+ accountSlug: report.config.accountSlug,
64
+ accountType: report.config.accountType,
65
+ }).map((line) => ` ${line}`));
51
66
  lines.push(` User: ${report.config.userEmail ?? report.config.userId ?? 'not bound'}`);
52
67
  lines.push(` Expires: ${report.config.sessionExpiresAt
53
68
  ? new Date(report.config.sessionExpiresAt * 1000).toISOString()
@@ -1,6 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { readLocalConfig, updateLocalConfig } from './local-state.js';
3
- export const POSTPLUS_CLIENT_CONTRACT_VERSION = 1;
3
+ export const POSTPLUS_CLIENT_CONTRACT_VERSION = 2;
4
4
  export const POSTPLUS_CLIENT_RUNTIME = 'postplus-cli';
5
5
  export const POSTPLUS_CLIENT_COMPATIBILITY_HEADERS = {
6
6
  cliVersion: 'x-postplus-cli-version',
package/build/doctor.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { formatAccountBindingName } from './account-binding-display.js';
1
2
  import { resolveFreshRemoteAuth, } from './auth-session.js';
2
3
  import { buildPostPlusClientCompatibilityHeaders, formatPostPlusCompatibilityError, } from './client-compatibility.js';
3
4
  import { resolveHostedBaseUrl } from './hosted-release.js';
@@ -131,13 +132,21 @@ async function checkRemoteAuth(input) {
131
132
  return createFail('remote_auth', 'Remote auth', readErrorMessage(payload, 'PostPlus Cloud rejected the CLI session.'), 'Run `postplus auth login`.');
132
133
  }
133
134
  const accountId = typeof payload.accountId === 'string' ? payload.accountId : 'unknown';
135
+ const accountName = typeof payload.accountName === 'string' ? payload.accountName : null;
136
+ const accountType = payload.accountType === 'personal' || payload.accountType === 'team'
137
+ ? payload.accountType
138
+ : null;
134
139
  const user = typeof payload.userEmail === 'string'
135
140
  ? payload.userEmail
136
141
  : typeof payload.userId === 'string'
137
142
  ? payload.userId
138
143
  : 'unknown';
139
144
  const subscription = readSubscriptionStatusField(payload).label;
140
- return createPass('remote_auth', 'Remote auth', `Account ${accountId}; user ${user}; subscription ${subscription}`);
145
+ return createPass('remote_auth', 'Remote auth', `${formatAccountBindingName({
146
+ accountId,
147
+ accountName,
148
+ accountType,
149
+ })}; account ${accountId}; user ${user}; subscription ${subscription}`);
141
150
  }
142
151
  catch (error) {
143
152
  return createFail('remote_auth', 'Remote auth', error instanceof Error
@@ -216,9 +225,7 @@ function readCapabilityFailureLabel(value, skillScope) {
216
225
  .map(readReadinessCheckFailureLabel)
217
226
  .filter((check) => check !== null)
218
227
  : [];
219
- const labelWithFailures = failedChecks.length > 0
220
- ? `${label} (${failedChecks.join(', ')})`
221
- : label;
228
+ const labelWithFailures = failedChecks.length > 0 ? `${label} (${failedChecks.join(', ')})` : label;
222
229
  return skillScope
223
230
  ? `${labelWithFailures} for ${skillScope.skill.skillId}`
224
231
  : labelWithFailures;
@@ -0,0 +1,366 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { resolveFreshRemoteAuth } from './auth-session.js';
5
+ import { buildPostPlusClientCompatibilityHeaders, formatPostPlusCompatibilityError, } from './client-compatibility.js';
6
+ import { buildHostedRequestSchemaReport, buildMediaGenerationRequestDimensions, } from './hosted-request-schemas.js';
7
+ import { readLargeCreditQuoteConfirmationChallenge, } from './quote-confirmation.js';
8
+ class HostedQuoteConfirmationRequiredError extends Error {
9
+ challenge;
10
+ constructor(message, challenge) {
11
+ super(message);
12
+ this.challenge = challenge;
13
+ this.name = 'HostedQuoteConfirmationRequiredError';
14
+ }
15
+ }
16
+ const HOSTED_DOMAIN_CAPABILITIES = {
17
+ media: new Set(['media-file', 'media-generation', 'video-analysis']),
18
+ mobile: new Set(['mobile-automation']),
19
+ publish: new Set(['social-publishing']),
20
+ research: new Set([
21
+ 'public-content-collection',
22
+ 'public-content-discovery',
23
+ ]),
24
+ };
25
+ export async function runHostedDomainCommand(domain, args) {
26
+ const [subcommand, ...rest] = args;
27
+ if (domain === 'research') {
28
+ if (subcommand === 'schema') {
29
+ return runHostedSchema(domain, rest);
30
+ }
31
+ if (subcommand === 'collect') {
32
+ return runResearchCollect(rest);
33
+ }
34
+ if (subcommand === 'capability') {
35
+ return runHostedCapability(domain, rest);
36
+ }
37
+ printResearchHelp();
38
+ return subcommand === undefined || isHelp(subcommand) ? 0 : 1;
39
+ }
40
+ if (subcommand === 'schema') {
41
+ return runHostedSchema(domain, rest);
42
+ }
43
+ if (subcommand === 'capability') {
44
+ return runHostedCapability(domain, rest);
45
+ }
46
+ printCapabilityHelp(domain);
47
+ return subcommand === undefined || isHelp(subcommand) ? 0 : 1;
48
+ }
49
+ async function runResearchCollect(args) {
50
+ const flags = parseFlags(args, new Set(['json']));
51
+ const runHandle = flags.values.get('run-handle');
52
+ const outputPath = flags.values.get('output') ?? null;
53
+ if (runHandle) {
54
+ const payload = await postHostedJson({
55
+ body: { runHandle },
56
+ pathName: '/api/postplus-cli/hosted/collection',
57
+ skillName: null,
58
+ });
59
+ await writeResult(payload, outputPath, flags.booleans.has('json'));
60
+ return 0;
61
+ }
62
+ const skillName = requireFlag(flags, 'skill');
63
+ const collectionKey = requireFlag(flags, 'collection-key');
64
+ const inputPath = requireFlag(flags, 'input');
65
+ const envelope = readHostedEnvelope(await readJsonFile(inputPath), inputPath);
66
+ const operationId = flags.values.get('hosted-operation-id') ??
67
+ normalizeString(envelope.hostedOperationId) ??
68
+ normalizeString(envelope.operationId) ??
69
+ `postplus-cli:research:${collectionKey}:${randomUUID()}`;
70
+ const quoteConfirmationToken = flags.values.get('quote-confirmation-token') ??
71
+ normalizeString(envelope.quoteConfirmationToken);
72
+ const payload = await postHostedJson({
73
+ body: {
74
+ collectionKey,
75
+ input: envelope.input,
76
+ operationId,
77
+ quoteConfirmationToken: quoteConfirmationToken ?? undefined,
78
+ skillName,
79
+ },
80
+ pathName: '/api/postplus-cli/hosted/collection',
81
+ skillName,
82
+ }).catch((error) => buildHostedCommandError(error, {
83
+ inputPath,
84
+ outputPath,
85
+ }));
86
+ await writeResult(payload, outputPath, flags.booleans.has('json'));
87
+ return 0;
88
+ }
89
+ async function runHostedSchema(domain, args) {
90
+ const flags = parseFlags(args, new Set(['json']));
91
+ const allowedFlags = domain === 'media'
92
+ ? new Set(['endpoint'])
93
+ : domain === 'research'
94
+ ? new Set(['collection-key'])
95
+ : new Set();
96
+ for (const key of flags.values.keys()) {
97
+ if (!allowedFlags.has(key)) {
98
+ throw new Error(`Unknown option for ${domain} schema: --${key}.`);
99
+ }
100
+ }
101
+ writeJson(buildHostedRequestSchemaReport({
102
+ collectionKey: flags.values.get('collection-key') ?? null,
103
+ domain,
104
+ endpointKey: flags.values.get('endpoint') ?? null,
105
+ }));
106
+ return 0;
107
+ }
108
+ async function runHostedCapability(domain, args) {
109
+ const flags = parseFlags(args, new Set(['json']));
110
+ const requestPath = requireFlag(flags, 'request');
111
+ const outputPath = flags.values.get('output') ?? null;
112
+ const request = await readJsonFile(requestPath);
113
+ if (!request || typeof request !== 'object' || Array.isArray(request)) {
114
+ throw new Error(`Hosted ${domain} capability request must be a JSON object.`);
115
+ }
116
+ const record = request;
117
+ const capability = requireDomainCapability(record, domain);
118
+ const operation = requireRecordString(record, 'operation');
119
+ const operationId = flags.values.get('hosted-operation-id') ??
120
+ normalizeString(record.operationId) ??
121
+ `postplus-cli:${domain}:${capability}:${operation}:${randomUUID()}`;
122
+ const quoteConfirmationToken = flags.values.get('quote-confirmation-token') ??
123
+ normalizeString(record.quoteConfirmationToken);
124
+ const publicRecord = { ...record };
125
+ delete publicRecord.skillName;
126
+ const derivedFields = buildDerivedHostedCapabilityFields({
127
+ capability,
128
+ domain,
129
+ operation,
130
+ record,
131
+ });
132
+ const body = {
133
+ ...publicRecord,
134
+ ...derivedFields,
135
+ capability,
136
+ operation,
137
+ operationId,
138
+ quoteConfirmationToken: quoteConfirmationToken ?? undefined,
139
+ };
140
+ const skillName = flags.values.get('skill') ?? normalizeString(record.skillName);
141
+ const payload = await postHostedJson({
142
+ body,
143
+ pathName: '/api/postplus-cli/hosted/capability',
144
+ skillName,
145
+ }).catch((error) => buildHostedCommandError(error, {
146
+ inputPath: requestPath,
147
+ outputPath,
148
+ }));
149
+ await writeResult(payload, outputPath, flags.booleans.has('json'));
150
+ return 0;
151
+ }
152
+ function buildDerivedHostedCapabilityFields(input) {
153
+ if (input.domain !== 'media' ||
154
+ input.capability !== 'media-generation' ||
155
+ input.operation !== 'request') {
156
+ return {};
157
+ }
158
+ if (Object.hasOwn(input.record, 'requestDimensions')) {
159
+ throw new Error('Hosted media-generation request must not include requestDimensions. The CLI derives billing dimensions from endpointKey and input.');
160
+ }
161
+ const endpointKey = requireRecordString(input.record, 'endpointKey');
162
+ const mediaInput = requireRecordObject(input.record, 'input');
163
+ return {
164
+ requestDimensions: buildMediaGenerationRequestDimensions(endpointKey, mediaInput),
165
+ };
166
+ }
167
+ async function postHostedJson(input) {
168
+ let auth = await resolveFreshRemoteAuth();
169
+ let response = await postJson({
170
+ apiBaseUrl: auth.apiBaseUrl,
171
+ body: input.body,
172
+ cliSessionToken: auth.cliSessionToken,
173
+ pathName: input.pathName,
174
+ skillName: input.skillName,
175
+ });
176
+ if (response.status === 401) {
177
+ auth = await resolveFreshRemoteAuth({ forceRefresh: true });
178
+ response = await postJson({
179
+ apiBaseUrl: auth.apiBaseUrl,
180
+ body: input.body,
181
+ cliSessionToken: auth.cliSessionToken,
182
+ pathName: input.pathName,
183
+ skillName: input.skillName,
184
+ });
185
+ }
186
+ const payload = await readJsonResponse(response);
187
+ if (!response.ok) {
188
+ const challenge = readLargeCreditQuoteConfirmationChallenge(payload);
189
+ if (challenge) {
190
+ throw new HostedQuoteConfirmationRequiredError(readProductError(payload), challenge);
191
+ }
192
+ const compatibilityError = formatPostPlusCompatibilityError(payload);
193
+ if (compatibilityError) {
194
+ throw new Error(compatibilityError);
195
+ }
196
+ throw new Error(readProductError(payload));
197
+ }
198
+ return payload;
199
+ }
200
+ async function buildHostedCommandError(error, input) {
201
+ if (!(error instanceof HostedQuoteConfirmationRequiredError)) {
202
+ throw error;
203
+ }
204
+ const challengePath = path.resolve(input.outputPath
205
+ ? `${input.outputPath}.quote-confirmation.json`
206
+ : `${input.inputPath}.quote-confirmation.json`);
207
+ await mkdir(path.dirname(challengePath), { recursive: true });
208
+ await writeFile(challengePath, `${JSON.stringify(error.challenge, null, 2)}\n`, {
209
+ encoding: 'utf8',
210
+ mode: 0o600,
211
+ });
212
+ throw new Error([
213
+ error.message,
214
+ `Quote confirmation challenge: ${challengePath}`,
215
+ `Confirm: postplus quote confirm --json --challenge-file "${challengePath}"`,
216
+ 'Then rerun the hosted command with --quote-confirmation-token <token>.',
217
+ ].join('\n'));
218
+ }
219
+ async function postJson(input) {
220
+ const headers = await buildPostPlusClientCompatibilityHeaders({
221
+ skillName: input.skillName,
222
+ });
223
+ return fetch(`${input.apiBaseUrl}${input.pathName}`, {
224
+ body: JSON.stringify(input.body),
225
+ headers: {
226
+ accept: 'application/json',
227
+ authorization: `Bearer ${input.cliSessionToken}`,
228
+ ...headers,
229
+ 'content-type': 'application/json',
230
+ },
231
+ method: 'POST',
232
+ signal: AbortSignal.timeout(120000),
233
+ });
234
+ }
235
+ async function readJsonResponse(response) {
236
+ const text = await response.text();
237
+ if (!text.trim()) {
238
+ return null;
239
+ }
240
+ try {
241
+ return JSON.parse(text);
242
+ }
243
+ catch {
244
+ throw new Error('PostPlus Cloud returned invalid JSON.');
245
+ }
246
+ }
247
+ function readProductError(payload) {
248
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
249
+ const record = payload;
250
+ if (typeof record.error === 'string' && record.error.trim()) {
251
+ return record.error.trim();
252
+ }
253
+ if (typeof record.message === 'string' && record.message.trim()) {
254
+ return record.message.trim();
255
+ }
256
+ }
257
+ return 'PostPlus hosted capability request failed.';
258
+ }
259
+ async function readJsonFile(filePath) {
260
+ try {
261
+ return JSON.parse(await readFile(filePath, 'utf8'));
262
+ }
263
+ catch (error) {
264
+ throw new Error(error instanceof Error
265
+ ? `Failed to read JSON file ${filePath}: ${error.message}`
266
+ : `Failed to read JSON file ${filePath}.`);
267
+ }
268
+ }
269
+ function readHostedEnvelope(value, filePath) {
270
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
271
+ throw new Error(`${filePath} must be a schemaVersion 1 hosted envelope.`);
272
+ }
273
+ const envelope = value;
274
+ if (envelope.schemaVersion !== 1 || !Object.hasOwn(envelope, 'input')) {
275
+ throw new Error(`${filePath} must be a schemaVersion 1 hosted envelope.`);
276
+ }
277
+ return envelope;
278
+ }
279
+ async function writeResult(payload, outputPath, forceStdout) {
280
+ const text = `${JSON.stringify(payload, null, 2)}\n`;
281
+ if (!outputPath || forceStdout) {
282
+ process.stdout.write(text);
283
+ }
284
+ if (outputPath) {
285
+ await mkdir(path.dirname(path.resolve(outputPath)), { recursive: true });
286
+ await writeFile(outputPath, text);
287
+ }
288
+ }
289
+ function parseFlags(args, booleanFlags) {
290
+ const values = new Map();
291
+ const booleans = new Set();
292
+ for (let index = 0; index < args.length; index += 1) {
293
+ const arg = args[index];
294
+ if (!arg.startsWith('--')) {
295
+ throw new Error(`Unexpected positional argument: ${arg}`);
296
+ }
297
+ const key = arg.slice(2);
298
+ if (booleanFlags.has(key)) {
299
+ booleans.add(key);
300
+ continue;
301
+ }
302
+ const value = args[index + 1];
303
+ if (!value || value.startsWith('--')) {
304
+ throw new Error(`Missing value for --${key}.`);
305
+ }
306
+ values.set(key, value);
307
+ index += 1;
308
+ }
309
+ return { booleans, values };
310
+ }
311
+ function requireFlag(flags, key) {
312
+ const value = flags.values.get(key);
313
+ if (!value) {
314
+ throw new Error(`Missing required option --${key}.`);
315
+ }
316
+ return value;
317
+ }
318
+ function requireDomainCapability(record, domain) {
319
+ const capability = requireRecordString(record, 'capability');
320
+ const allowed = HOSTED_DOMAIN_CAPABILITIES[domain];
321
+ if (!allowed.has(capability)) {
322
+ throw new Error(`Hosted ${domain} capability request uses unsupported capability ${capability}.`);
323
+ }
324
+ return capability;
325
+ }
326
+ function requireRecordString(record, key) {
327
+ const value = normalizeString(record[key]);
328
+ if (!value) {
329
+ throw new Error(`Hosted capability request must include string ${key}.`);
330
+ }
331
+ return value;
332
+ }
333
+ function requireRecordObject(record, key) {
334
+ const value = record[key];
335
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
336
+ throw new Error(`Hosted capability request must include object ${key}.`);
337
+ }
338
+ return value;
339
+ }
340
+ function normalizeString(value) {
341
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
342
+ }
343
+ function isHelp(value) {
344
+ return value === 'help' || value === '--help' || value === '-h';
345
+ }
346
+ function printResearchHelp() {
347
+ process.stdout.write(`PostPlus CLI - research commands
348
+
349
+ Usage:
350
+ postplus research schema [--collection-key <key>] [--json]
351
+ postplus research collect --skill <skill-id> --collection-key <key> --input <hosted-envelope.json> [--output <result.json>]
352
+ postplus research collect --run-handle <runHandle> [--output <result.json>]
353
+ postplus research capability --request <hosted-capability-request.json> [--output <result.json>]
354
+ `);
355
+ }
356
+ function printCapabilityHelp(domain) {
357
+ process.stdout.write(`PostPlus CLI - ${domain} commands
358
+
359
+ Usage:
360
+ postplus ${domain} schema${domain === 'media' ? ' [--endpoint <endpoint-key>]' : ''} [--json]
361
+ postplus ${domain} capability --request <hosted-capability-request.json> [--output <result.json>]
362
+ `);
363
+ }
364
+ function writeJson(value) {
365
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
366
+ }