@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 +27 -5
- package/build/account-binding-display.js +21 -0
- package/build/auth-lifecycle.js +5 -1
- package/build/auth-login.js +23 -0
- package/build/auth-session.js +8 -0
- package/build/auth-validate.js +15 -1
- package/build/auth.js +16 -1
- package/build/client-compatibility.js +1 -1
- package/build/doctor.js +11 -4
- package/build/hosted-domain-commands.js +366 -0
- package/build/hosted-request-schemas.js +507 -0
- package/build/hosted-schema-catalog.js +307 -0
- package/build/index.js +39 -3
- package/build/local-state.js +6 -0
- package/build/skill-catalog.js +1 -3
- package/build/skill-management.js +57 -20
- package/build/status.js +6 -3
- package/package.json +6 -2
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
|
-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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
|
+
}
|
package/build/auth-lifecycle.js
CHANGED
|
@@ -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
|
-
|
|
28
|
+
...formatAccountBindingLines(report),
|
|
25
29
|
`User: ${report.userEmail ?? report.userId}`,
|
|
26
30
|
`Subscription: ${readSubscriptionStatusField(report).label}`,
|
|
27
31
|
].join('\n');
|
package/build/auth-login.js
CHANGED
|
@@ -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) {
|
package/build/auth-session.js
CHANGED
|
@@ -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
|
}
|
package/build/auth-validate.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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 =
|
|
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',
|
|
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
|
+
}
|