@postplus/cli 0.1.45 → 0.1.47

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,93 +1,13 @@
1
- // Schema-driven early field validation (issue #475). Mirrors the Web boundary's
2
- // assertModelledFieldValuesInRange: every enum / numeric-range field an endpoint
3
- // advertises in the generated execution manifest — the same EnvelopeFieldSpec the
4
- // Web validator reads is checked here BEFORE the request is posted, so the agent
5
- // gets an immediate field-level error (e.g. seedance resolution "999p") instead of
6
- // waiting for the round-trip. The Web boundary stays the AUTHORITATIVE gate; this is
7
- // only pre-submit feedback.
8
- //
9
- // Casing-faithfulness is the reason this cannot be a CLI-side island: the per-field
10
- // canonicalization rule is read from the manifest `canonicalize` hint, the SAME
11
- // single source the Web validator reads, so "720P" / "4K" pass and "english" /
12
- // "999p" fail exactly as they do on the boundary. The two canonicalize functions
13
- // below are stable 3-line algorithms; WHICH field uses WHICH is decided by the
14
- // schema hint, never re-guessed here.
15
- // k-tier normalization for image resolution ("4K" -> "4k"). Mirrors the Web
16
- // canonicalizeImageResolution exactly.
17
- function canonicalizeImageResolution(value) {
18
- const trimmed = value.trim();
19
- const tier = trimmed.match(/^(\d+(?:\.\d+)?)\s*k$/iu);
20
- return tier ? `${tier[1]}k` : trimmed;
21
- }
22
- function canonicalizeLowercaseToken(value) {
23
- return value.trim().toLowerCase();
24
- }
25
- function canonicalizeModelledFieldValue(field, value) {
26
- switch (field.canonicalize) {
27
- case 'image-resolution-tier':
28
- return canonicalizeImageResolution(value);
29
- case 'lowercase':
30
- return canonicalizeLowercaseToken(value);
31
- default:
32
- return value;
33
- }
34
- }
35
- function isIntegerInRange(min, max, value) {
36
- return Number.isInteger(value) && value >= min && value <= max;
37
- }
38
- function formatReceivedValue(raw) {
39
- return typeof raw === 'string' ? `"${raw}"` : String(raw);
40
- }
41
- function assertModelledNumberFieldValue(endpointKey, field, raw) {
42
- const enumValues = field.enumValues && field.enumValues.length > 0 ? field.enumValues : null;
43
- const constraint = enumValues
44
- ? `must be one of ${enumValues.join(', ')}`
45
- : `must be an integer from ${field.min} to ${field.max}`;
46
- if (typeof raw !== 'number' || !Number.isFinite(raw)) {
47
- throw new Error(`${endpointKey} ${field.name} ${constraint}; received ${formatReceivedValue(raw)}.`);
48
- }
49
- if (enumValues) {
50
- if (!enumValues.includes(String(raw))) {
51
- throw new Error(`${endpointKey} ${field.name} ${constraint}; received ${raw}.`);
52
- }
53
- return;
54
- }
55
- if (field.min === undefined || field.max === undefined) {
56
- return;
57
- }
58
- if (!isIntegerInRange(field.min, field.max, raw)) {
59
- throw new Error(`${endpointKey} ${field.name} ${constraint}; received ${raw}.`);
60
- }
61
- }
62
- // Validates every advertised enum / numeric-range field present in the input against
63
- // the manifest contract. Skips runner-managed fields (no caller input), fields with
64
- // neither an enum nor a range, and fields the input omits — exactly mirroring the Web
65
- // boundary so a value the CLI accepts the boundary also accepts, and vice versa.
1
+ // Schema-driven early field validation (issue #475). The canonicalize + enum/range
2
+ // algorithm is the SSOT `hosted-field-validation-core`, authored in apps/web and
3
+ // projected VERBATIM into ./generated by the hosted-execution-manifest codegen — the CLI
4
+ // submodule cannot import apps/web TS, and a hand-copy here would let the casing / range
5
+ // rules drift from the Web boundary. This module only injects the CLI's plain-Error
6
+ // factory and keeps the established (endpointKey, fields, input) signature its callers
7
+ // use. The Web boundary stays the AUTHORITATIVE gate; this is pre-submit feedback so the
8
+ // agent gets an immediate field-level error (e.g. seedance resolution "999p") instead of
9
+ // waiting for the round-trip.
10
+ import { assertModelledFieldValuesInRange as assertModelledFieldValuesInRangeCore } from './generated/hosted-field-validation-core.generated.js';
66
11
  export function assertModelledFieldValuesInRange(endpointKey, fields, input) {
67
- for (const field of fields) {
68
- if (field.class === 'runner-managed') {
69
- continue;
70
- }
71
- const enumValues = field.enumValues && field.enumValues.length > 0 ? field.enumValues : null;
72
- const hasRange = field.min !== undefined && field.max !== undefined;
73
- if (!enumValues && !hasRange) {
74
- continue;
75
- }
76
- if (!Object.hasOwn(input, field.name)) {
77
- continue;
78
- }
79
- if (field.type === 'number') {
80
- assertModelledNumberFieldValue(endpointKey, field, input[field.name]);
81
- continue;
82
- }
83
- const raw = input[field.name];
84
- if (typeof raw !== 'string' || !raw.trim()) {
85
- continue;
86
- }
87
- const value = raw.trim();
88
- if (enumValues &&
89
- !enumValues.includes(canonicalizeModelledFieldValue(field, value))) {
90
- throw new Error(`${endpointKey} ${field.name} must be one of ${enumValues.join(', ')}; received "${value}".`);
91
- }
92
- }
12
+ assertModelledFieldValuesInRangeCore(endpointKey, fields, input, (message) => new Error(message));
93
13
  }
@@ -0,0 +1,34 @@
1
+ import type { AuthedCloudRequestAuth } from './authed-cloud-request.js';
2
+ export type HostedLibDomain = 'media' | 'research' | 'publish' | 'media-file';
3
+ export type RunHostedRequestInput = {
4
+ /** Which hosted verb family `args` belongs to (the first CLI token). */
5
+ domain: HostedLibDomain;
6
+ /**
7
+ * The CLI tokens AFTER the domain, exactly as the bin would receive them, e.g.
8
+ * `['create', 'video-seedance-2-text']` or `['collect', 'tiktok-research']`.
9
+ * Flags surfaces still pass their `--flag value` tokens here; request-json
10
+ * surfaces pass the body via `requestJson` instead of a `--request <file>`.
11
+ */
12
+ args: string[];
13
+ /**
14
+ * The request-json envelope for request-json surfaces, injected in place of a
15
+ * `--request <file>` read. Omit it for flags surfaces (media create/transcribe
16
+ * flag-driven) and for surfaces that need no body (polling/run-handle).
17
+ */
18
+ requestJson?: Record<string, unknown> | unknown[];
19
+ /** The account's fresh session auth, supplied by the trusted host runtime. */
20
+ auth: AuthedCloudRequestAuth;
21
+ /**
22
+ * The skills release id stamped into `x-postplus-skills-release-id`. Provided
23
+ * verbatim by the host (it is NOT read from disk on this path).
24
+ */
25
+ skillsReleaseId?: string;
26
+ };
27
+ /**
28
+ * Runs a hosted media / research / publish / media-file request in-process and
29
+ * returns the parsed hosted payload. Throws the structured
30
+ * HostedProductRequestError / quote-confirmation error VERBATIM on failure — no
31
+ * stdout, no file writes, no exit code. The wire request is identical to the bin
32
+ * path for the same input (proven by the parity test).
33
+ */
34
+ export declare function runHostedRequest(input: RunHostedRequestInput): Promise<unknown>;
@@ -0,0 +1,45 @@
1
+ // In-process hosted-execution library entry. This is the trusted-runtime
2
+ // counterpart to the `postplus` bin: a host process (e.g. eve-agent) that already
3
+ // holds the account's fresh session auth can run the SAME hosted verb grammar
4
+ // in-process — resolve verb -> build the typed envelope -> POST -> return the
5
+ // parsed payload — WITHOUT spawning a CLI subprocess, reading disk config, or
6
+ // writing any temp/output file.
7
+ //
8
+ // Why this exists (anti-drift): the bin path and this lib path share ONE
9
+ // resolve+build+post core in hosted-domain-commands.ts. For the same
10
+ // domain+args+input+auth they produce a byte-identical hosted HTTP request (URL +
11
+ // JSON body + headers). The only divergence is the input source and the result
12
+ // sink, both injected via HostedRequestContext: auth + skillsReleaseId come in as
13
+ // parameters (no `resolveFreshRemoteAuth()` disk read, no 401-refresh-retry — the
14
+ // host supplies fresh session auth each turn), the request-json body comes from
15
+ // `requestJson` instead of a `--request <file>`, and the parsed payload is RETURNED
16
+ // (the structured HostedProductRequestError / quote-confirmation error are thrown
17
+ // verbatim) instead of being written to stdout/file with an exit code.
18
+ //
19
+ // Scope: only the hosted spend/write surfaces go through here —
20
+ // media / research / publish / media-file. Read-only diagnostics (status / doctor
21
+ // / skills / whoami / quote / list / --version / --help) are NOT hosted-domain
22
+ // commands and are out of scope for this entry.
23
+ import { runHostedDomainCommand, runMediaFileCommand, } from './hosted-domain-commands.js';
24
+ /**
25
+ * Runs a hosted media / research / publish / media-file request in-process and
26
+ * returns the parsed hosted payload. Throws the structured
27
+ * HostedProductRequestError / quote-confirmation error VERBATIM on failure — no
28
+ * stdout, no file writes, no exit code. The wire request is identical to the bin
29
+ * path for the same input (proven by the parity test).
30
+ */
31
+ export async function runHostedRequest(input) {
32
+ const context = {
33
+ auth: input.auth,
34
+ ...(input.skillsReleaseId !== undefined
35
+ ? { skillsReleaseId: input.skillsReleaseId }
36
+ : {}),
37
+ ...(input.requestJson !== undefined
38
+ ? { requestJson: input.requestJson }
39
+ : {}),
40
+ };
41
+ if (input.domain === 'media-file') {
42
+ return runMediaFileCommand(input.args, context);
43
+ }
44
+ return runHostedDomainCommand(input.domain, input.args, context);
45
+ }
@@ -0,0 +1,69 @@
1
+ export type HostedDomain = 'media' | 'publish' | 'research';
2
+ export type ManifestFieldClass = 'intent' | 'default' | 'runner-managed';
3
+ export type ManifestField = {
4
+ name: string;
5
+ class: ManifestFieldClass;
6
+ flag: string | null;
7
+ type: 'string' | 'number' | 'boolean' | 'media-url';
8
+ repeatable?: boolean;
9
+ enumValues?: readonly string[];
10
+ min?: number;
11
+ max?: number;
12
+ canonicalize?: 'lowercase' | 'image-resolution-tier';
13
+ default?: string | number | boolean;
14
+ required: boolean;
15
+ derivedFrom?: string;
16
+ };
17
+ export type ManifestEndpoint = {
18
+ endpointKey: string;
19
+ provider: string;
20
+ providerModelPath: string;
21
+ fields: readonly ManifestField[];
22
+ billingDimensions?: readonly string[];
23
+ };
24
+ export type ManifestModel = {
25
+ modelKey: string;
26
+ providerModelPath: string;
27
+ };
28
+ export type ManifestCollection = {
29
+ collectionKey: string;
30
+ actorId: string;
31
+ };
32
+ export type ManifestSource = {
33
+ sourceKey: string;
34
+ datasetId: string;
35
+ };
36
+ export type ManifestOperation = {
37
+ operation: string;
38
+ };
39
+ export type ManifestEntry = {
40
+ skill: string;
41
+ mode?: 'cli-runner';
42
+ surface: 'flags' | 'request-json';
43
+ verb: string;
44
+ domain: HostedDomain;
45
+ capability: string;
46
+ endpointKeys?: readonly string[];
47
+ modelKeys?: readonly string[];
48
+ collectionKeys?: readonly string[];
49
+ sourceKeys?: readonly string[];
50
+ endpoints?: readonly ManifestEndpoint[];
51
+ models?: readonly ManifestModel[];
52
+ collections?: readonly ManifestCollection[];
53
+ sources?: readonly ManifestSource[];
54
+ operations?: readonly ManifestOperation[];
55
+ };
56
+ export type ResolvedVerbTarget = {
57
+ skill: string;
58
+ capability: string;
59
+ surface: 'flags' | 'request-json';
60
+ endpoint?: ManifestEndpoint;
61
+ model?: ManifestModel;
62
+ collection?: ManifestCollection;
63
+ source?: ManifestSource;
64
+ operation?: string;
65
+ };
66
+ export declare function allManifestEntries(): ManifestEntry[];
67
+ export declare function buildVerbTargetIndex(domain: HostedDomain): Map<string, Map<string, ResolvedVerbTarget>>;
68
+ export declare function manifestTargetKeys(domain: HostedDomain, capability?: string): string[];
69
+ export declare function findMediaEndpoint(endpointKey: string): ManifestEndpoint | null;
@@ -1,4 +1,3 @@
1
- import { Buffer } from 'node:buffer';
2
1
  import { findMediaEndpoint, manifestTargetKeys, } from './hosted-manifest-index.js';
3
2
  const JSON_OBJECT_SCHEMA = {
4
3
  additionalProperties: true,
@@ -211,113 +210,6 @@ function requireMediaEndpoint(endpointKey) {
211
210
  }
212
211
  return endpoint;
213
212
  }
214
- const MANIFEST_FIELD_DEFAULTS = buildManifestFieldDefaults();
215
- const MANIFEST_DERIVED_DIMENSIONS = buildManifestDerivedDimensions();
216
- function buildManifestFieldDefaults() {
217
- const index = new Map();
218
- for (const key of manifestTargetKeys('media', 'media-generation')) {
219
- const endpoint = findMediaEndpoint(key);
220
- if (!endpoint) {
221
- continue;
222
- }
223
- const byField = new Map();
224
- for (const field of endpoint.fields) {
225
- if (field.default !== undefined) {
226
- byField.set(field.name, field.default);
227
- }
228
- }
229
- index.set(key, byField);
230
- }
231
- return index;
232
- }
233
- function manifestFieldDefault(endpointKey, fieldName) {
234
- return MANIFEST_FIELD_DEFAULTS.get(endpointKey)?.get(fieldName);
235
- }
236
- function buildManifestDerivedDimensions() {
237
- const index = new Map();
238
- for (const key of manifestTargetKeys('media', 'media-generation')) {
239
- const endpoint = findMediaEndpoint(key);
240
- if (!endpoint) {
241
- continue;
242
- }
243
- const derived = endpoint.fields.flatMap((field) => field.class === 'runner-managed' && field.derivedFrom
244
- ? [{ fieldName: field.name, sourceName: field.derivedFrom }]
245
- : []);
246
- if (derived.length > 0) {
247
- index.set(key, derived);
248
- }
249
- }
250
- return index;
251
- }
252
- export function buildMediaGenerationRequestDimensions(endpointKey, input) {
253
- const dimensions = {
254
- billableUnitCount: 1,
255
- operationKey: endpointKey,
256
- };
257
- for (const derived of MANIFEST_DERIVED_DIMENSIONS.get(endpointKey) ?? []) {
258
- const value = input[derived.sourceName] ??
259
- manifestFieldDefault(endpointKey, derived.sourceName);
260
- if (value !== undefined) {
261
- dimensions[derived.fieldName] = value;
262
- }
263
- }
264
- if (endpointKey.startsWith('image-')) {
265
- const resolution = typeof input.resolution === 'string' && input.resolution.trim()
266
- ? input.resolution.trim()
267
- : manifestFieldDefault(endpointKey, 'resolution');
268
- const quality = typeof input.quality === 'string' && input.quality.trim()
269
- ? input.quality.trim()
270
- : manifestFieldDefault(endpointKey, 'quality');
271
- if (typeof resolution === 'string') {
272
- dimensions.imageSize = resolution;
273
- }
274
- if (typeof quality === 'string') {
275
- dimensions.quality = quality;
276
- }
277
- }
278
- if (endpointKey.startsWith('video-')) {
279
- const manifestResolution = manifestFieldDefault(endpointKey, 'resolution');
280
- const duration = readPositiveNumber(input.duration) ??
281
- readPositiveNumber(manifestFieldDefault(endpointKey, 'duration')) ??
282
- 5;
283
- const resolution = typeof input.resolution === 'string' && input.resolution.trim()
284
- ? input.resolution.trim()
285
- : typeof manifestResolution === 'string'
286
- ? manifestResolution
287
- : '720p';
288
- dimensions.audioMode =
289
- endpointKey.startsWith('video-kling-v3-0-') && input.sound !== true
290
- ? 'off'
291
- : 'on';
292
- dimensions.duration = Math.ceil(duration);
293
- dimensions.requestBytes = Buffer.byteLength(JSON.stringify(input));
294
- dimensions.resolution = resolution;
295
- if (endpointKey.startsWith('video-seedance-2-')) {
296
- const referenceVideoCount = Array.isArray(input.reference_videos)
297
- ? input.reference_videos.length
298
- : 0;
299
- dimensions.referenceVideoCount = referenceVideoCount;
300
- dimensions.referenceVideoMode =
301
- referenceVideoCount > 0
302
- ? 'with_reference_videos'
303
- : 'without_reference_videos';
304
- }
305
- if (endpointKey === 'video-kling-v2-6-pro-motion-control') {
306
- dimensions.characterOrientation =
307
- typeof input.character_orientation === 'string'
308
- ? input.character_orientation
309
- : 'image';
310
- dimensions.motionControlMode = 'reference_motion_transfer';
311
- }
312
- }
313
- return dimensions;
314
- }
315
- function readPositiveNumber(value) {
316
- if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
317
- return null;
318
- }
319
- return value;
320
- }
321
213
  function buildPublishSchemaReport() {
322
214
  const operations = manifestTargetKeys('publish', 'social-publishing');
323
215
  return {
package/build/index.js CHANGED
@@ -412,17 +412,19 @@ async function main() {
412
412
  case 'doctor':
413
413
  process.exitCode = await runDoctor(parseDiagnosticOptions(rest));
414
414
  return;
415
+ // The bin path never passes the in-process context, so these always resolve
416
+ // to the numeric exit code (the `unknown` return is the hosted-lib path only).
415
417
  case 'research':
416
- process.exitCode = await runHostedDomainCommand('research', rest);
418
+ process.exitCode = (await runHostedDomainCommand('research', rest));
417
419
  return;
418
420
  case 'media':
419
- process.exitCode = await runHostedDomainCommand('media', rest);
421
+ process.exitCode = (await runHostedDomainCommand('media', rest));
420
422
  return;
421
423
  case 'media-file':
422
- process.exitCode = await runMediaFileCommand(rest);
424
+ process.exitCode = (await runMediaFileCommand(rest));
423
425
  return;
424
426
  case 'publish':
425
- process.exitCode = await runHostedDomainCommand('publish', rest);
427
+ process.exitCode = (await runHostedDomainCommand('publish', rest));
426
428
  return;
427
429
  case 'quote':
428
430
  process.exitCode = await runQuoteCommand(rest);
@@ -1,6 +1,6 @@
1
1
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
- import { readCurrentCliVersion } from './client-compatibility.js';
3
+ import { POSTPLUS_CLI_UPDATE_COMMAND, readCurrentCliVersion, } from './client-compatibility.js';
4
4
  import { runInteractiveCommand as runDefaultInteractiveCommand, } from './command-runner.js';
5
5
  import { getPostPlusConfigDir, readManagedSkillBaseline, } from './local-state.js';
6
6
  import { POSTPLUS_SKILLS_REPO, loadPublicSkillCatalog, } from './skill-catalog.js';
@@ -8,7 +8,6 @@ const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
8
8
  const UPDATE_CHECK_CACHE_FILE = 'update-check.json';
9
9
  const NPM_PACKAGE_NAME = '@postplus/cli';
10
10
  const NPM_LATEST_URL = `https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`;
11
- export const POSTPLUS_CLI_UPDATE_COMMAND = 'npm install -g @postplus/cli@latest';
12
11
  const POSTPLUS_CLI_UPDATE_ARGS = ['install', '-g', '@postplus/cli@latest'];
13
12
  export async function generateUpdateStatusReport(input = {}, dependencies = {
14
13
  fetchFn: fetch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postplus/cli",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
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.",
@@ -13,13 +13,19 @@
13
13
  "build/auth-validate.js",
14
14
  "build/auth.js",
15
15
  "build/authed-cloud-request.js",
16
+ "build/authed-cloud-request.d.ts",
16
17
  "build/client-compatibility.js",
17
18
  "build/command-runner.js",
18
19
  "build/doctor.js",
19
20
  "build/hosted-domain-commands.js",
21
+ "build/hosted-domain-commands.d.ts",
22
+ "build/hosted-lib.js",
23
+ "build/hosted-lib.d.ts",
20
24
  "build/generated/hosted-execution-manifest.generated.js",
25
+ "build/generated/hosted-field-validation-core.generated.js",
21
26
  "build/hosted-field-validation.js",
22
27
  "build/hosted-manifest-index.js",
28
+ "build/hosted-manifest-index.d.ts",
23
29
  "build/hosted-release.js",
24
30
  "build/hosted-request-schemas.js",
25
31
  "build/index.js",
@@ -47,6 +53,13 @@
47
53
  "bin": {
48
54
  "postplus": "build/index.js"
49
55
  },
56
+ "exports": {
57
+ "./hosted-lib": {
58
+ "types": "./build/hosted-lib.d.ts",
59
+ "default": "./build/hosted-lib.js"
60
+ },
61
+ "./package.json": "./package.json"
62
+ },
50
63
  "scripts": {
51
64
  "build": "node ./scripts/clean-build.mjs && tsc && node ./scripts/finalize-build.mjs",
52
65
  "clean": "rm -rf .turbo node_modules build",