@postplus/cli 0.1.34 → 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,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
+ }