@postplus/cli 0.1.27 → 0.1.29

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
@@ -24,7 +24,7 @@ PostPlus has three public surfaces that work together:
24
24
 
25
25
  - `https://postplus.io/`: the hosted product surface for account access, subscription state, and cloud-backed capabilities.
26
26
  - `https://github.com/PostPlusAI/postplus-skills`: the public skill repository that installs local marketing workflows into agent tools.
27
- - `https://github.com/PostPlusAI/postplus-cli`: the local command-line tool that signs you in, checks local readiness, and connects released skills to PostPlus account state.
27
+ - `https://github.com/PostPlusAI/postplus-cli`: the local command-line tool that signs you in, checks local readiness, confirms high-credit hosted requests, and connects released skills to PostPlus account state.
28
28
 
29
29
  ## Install
30
30
 
@@ -34,6 +34,7 @@ Requires Node.js and npm.
34
34
  npm install -g @postplus/cli@latest
35
35
  postplus auth login
36
36
  npx -y skills add PostPlusAI/postplus-skills --global --full-depth --skill '*' --agent claude-code codex cursor github-copilot windsurf trae trae-cn --yes
37
+ postplus skills verify
37
38
  ```
38
39
 
39
40
  Useful checks:
package/build/doctor.js CHANGED
@@ -2,6 +2,7 @@ import { resolveFreshRemoteAuth, } from './auth-session.js';
2
2
  import { buildPostPlusClientCompatibilityHeaders, formatPostPlusCompatibilityError, } from './client-compatibility.js';
3
3
  import { resolveHostedBaseUrl } from './hosted-release.js';
4
4
  import { formatLocalDependencyReport, generateLocalDependencyReport, } from './local-dependencies.js';
5
+ import { loadPublicSkillCatalog, } from './skill-catalog.js';
5
6
  import { readSubscriptionStatusField } from './subscription-status.js';
6
7
  function createPass(id, label, detail, severity = 'required') {
7
8
  return {
@@ -23,12 +24,16 @@ function createFail(id, label, detail, fix, input = {}) {
23
24
  metadata: input.metadata,
24
25
  };
25
26
  }
26
- export async function generateDoctorReport() {
27
+ export async function generateDoctorReport(options = {}) {
27
28
  const hostedBaseUrl = await resolveHostedBaseUrl();
28
29
  const checks = [
29
30
  createPass('hosted_base_url', 'PostPlus Cloud', `Using ${hostedBaseUrl ?? 'https://postplus.io'}`),
30
31
  ];
31
- checks.push(await checkLocalDependencies());
32
+ const skillScope = await resolveSkillScope(options.skillId);
33
+ if (skillScope) {
34
+ checks.push(createPass('skill_catalog', 'Skill selection', `Using ${skillScope.skill.skillId} from catalog ${skillScope.catalog.releaseId}`));
35
+ }
36
+ checks.push(await checkLocalDependencies(skillScope));
32
37
  if (!hostedBaseUrl) {
33
38
  checks.push(createFail('remote_auth', 'Remote auth', 'PostPlus Cloud base URL could not be resolved.', 'Configure POSTPLUS_API_BASE_URL or run `postplus auth login`.'));
34
39
  return buildDoctorReport(checks);
@@ -46,17 +51,40 @@ export async function generateDoctorReport() {
46
51
  const authCheck = await checkRemoteAuth(auth);
47
52
  checks.push(authCheck);
48
53
  if (authCheck.status === 'pass') {
49
- checks.push(await checkHostedCapabilities(auth));
54
+ checks.push(await checkHostedCapabilities(auth, skillScope));
55
+ }
56
+ return buildDoctorReport(checks, options.skillId);
57
+ }
58
+ async function resolveSkillScope(skillId) {
59
+ if (!skillId) {
60
+ return null;
61
+ }
62
+ const catalog = await loadPublicSkillCatalog();
63
+ const skill = catalog.skills.find((entry) => entry.skillId === skillId);
64
+ if (!skill) {
65
+ throw new Error(`Unknown PostPlus skill: ${skillId}. Run \`postplus list\` to see released skill ids.`);
50
66
  }
51
- return buildDoctorReport(checks);
67
+ return { catalog, skill };
52
68
  }
53
- async function checkLocalDependencies() {
69
+ async function checkLocalDependencies(skillScope) {
54
70
  try {
55
- const report = await generateLocalDependencyReport();
71
+ const report = await generateLocalDependencyReport(skillScope
72
+ ? {
73
+ loadCatalog: async () => ({
74
+ ...skillScope.catalog,
75
+ skills: [skillScope.skill],
76
+ }),
77
+ }
78
+ : {});
56
79
  const detail = formatLocalDependencyReport(report);
57
80
  if (!report.ok) {
58
- return createFail('local_dependencies', 'Task-specific local media dependencies', detail, 'Run the affected PostPlus skill in a local agent. The installed postplus-shared rules tell the agent how to bootstrap approved missing media dependencies.', {
59
- severity: 'task_specific',
81
+ const skillId = skillScope?.skill.skillId;
82
+ return createFail('local_dependencies', skillId
83
+ ? `Local dependencies for ${skillId}`
84
+ : 'Task-specific local media dependencies', detail, skillId
85
+ ? 'Run the selected PostPlus skill in a local agent. The installed postplus-shared rules tell the agent how to bootstrap approved missing dependencies.'
86
+ : 'Run the affected PostPlus skill in a local agent. The installed postplus-shared rules tell the agent how to bootstrap approved missing media dependencies.', {
87
+ severity: skillId ? 'required' : 'task_specific',
60
88
  metadata: {
61
89
  bootstrapRule: 'postplus-shared',
62
90
  missingDependencies: report.checks
@@ -69,7 +97,9 @@ async function checkLocalDependencies() {
69
97
  },
70
98
  });
71
99
  }
72
- return createPass('local_dependencies', 'Local dependencies', detail);
100
+ return createPass('local_dependencies', skillScope
101
+ ? `Local dependencies for ${skillScope.skill.skillId}`
102
+ : 'Local dependencies', detail);
73
103
  }
74
104
  catch (error) {
75
105
  return createFail('local_dependencies', 'Local dependencies', error instanceof Error
@@ -77,13 +107,14 @@ async function checkLocalDependencies() {
77
107
  : 'Failed to check local dependencies.');
78
108
  }
79
109
  }
80
- function buildDoctorReport(checks) {
110
+ function buildDoctorReport(checks, skillId) {
81
111
  const requiredOk = checks.every((check) => check.severity !== 'required' || check.status === 'pass');
82
112
  return {
83
113
  schemaVersion: 1,
84
114
  ok: checks.every((check) => check.status === 'pass'),
85
115
  requiredOk,
86
116
  checks,
117
+ ...(skillId ? { skillId } : {}),
87
118
  };
88
119
  }
89
120
  async function checkRemoteAuth(input) {
@@ -114,7 +145,7 @@ async function checkRemoteAuth(input) {
114
145
  : 'Failed to validate PostPlus Cloud auth.', 'Run `postplus auth validate` after confirming network access.');
115
146
  }
116
147
  }
117
- async function checkHostedCapabilities(input) {
148
+ async function checkHostedCapabilities(input, skillScope) {
118
149
  try {
119
150
  let response = await requestWithAuth(input, '/api/postplus-cli/hosted/readiness');
120
151
  if (response.status === 401) {
@@ -127,17 +158,28 @@ async function checkHostedCapabilities(input) {
127
158
  if (!response.ok) {
128
159
  return createFail('hosted_capabilities', 'Hosted capabilities', readErrorMessage(payload, 'PostPlus Cloud hosted readiness check failed.'));
129
160
  }
130
- const capabilities = Array.isArray(payload.capabilities)
131
- ? payload.capabilities
132
- : [];
133
- const failedLabels = capabilities
134
- .map(readCapabilityFailureLabel)
161
+ const capabilities = readHostedCapabilityEntries(payload.capabilities);
162
+ const relevantCapabilities = skillScope
163
+ ? filterCapabilitiesForSkill(capabilities, skillScope.skill.requirements)
164
+ : capabilities;
165
+ const failedLabels = relevantCapabilities
166
+ .map((value) => readCapabilityFailureLabel(value, skillScope))
135
167
  .filter((value) => value !== null);
136
- if (payload.ok !== true || failedLabels.length > 0) {
137
- return createFail('hosted_capabilities', 'Hosted capabilities', `Not ready: ${failedLabels.join(', ') || 'unknown capability failure'}`, 'Check PostPlus Cloud provider configuration and subscription state.');
168
+ if (skillScope && hasHostedRequirements(skillScope.skill.requirements)) {
169
+ const missingRequirements = collectMissingHostedRequirementLabels(relevantCapabilities, skillScope.skill.requirements);
170
+ failedLabels.push(...missingRequirements);
171
+ }
172
+ if (failedLabels.length > 0 ||
173
+ (!skillScope && payload.ok !== true && capabilities.length === 0)) {
174
+ const skillId = skillScope?.skill.skillId;
175
+ return createFail('hosted_capabilities', skillId ? `Hosted capabilities for ${skillId}` : 'Hosted capabilities', `Not ready: ${failedLabels.join(', ') || 'unknown capability failure'}`, 'Check PostPlus Cloud provider configuration and subscription state.', {
176
+ severity: skillId ? 'required' : 'task_specific',
177
+ });
138
178
  }
139
179
  const subscription = readSubscriptionStatusField(payload).label;
140
- return createPass('hosted_capabilities', 'Hosted capabilities', `Ready (${capabilities.length} capability checks passed; subscription ${subscription})`);
180
+ return createPass('hosted_capabilities', skillScope
181
+ ? `Hosted capabilities for ${skillScope.skill.skillId}`
182
+ : 'Hosted capabilities', `Ready (${relevantCapabilities.length} capability checks passed; subscription ${subscription})`);
141
183
  }
142
184
  catch (error) {
143
185
  return createFail('hosted_capabilities', 'Hosted capabilities', error instanceof Error
@@ -145,7 +187,13 @@ async function checkHostedCapabilities(input) {
145
187
  : 'Failed to check hosted capability readiness.');
146
188
  }
147
189
  }
148
- function readCapabilityFailureLabel(value) {
190
+ function readHostedCapabilityEntries(value) {
191
+ if (!Array.isArray(value)) {
192
+ return [];
193
+ }
194
+ return value.filter((entry) => !!entry && typeof entry === 'object' && !Array.isArray(entry));
195
+ }
196
+ function readCapabilityFailureLabel(value, skillScope) {
149
197
  if (!value || typeof value !== 'object') {
150
198
  return 'invalid capability response';
151
199
  }
@@ -163,9 +211,12 @@ function readCapabilityFailureLabel(value) {
163
211
  .map(readReadinessCheckFailureLabel)
164
212
  .filter((check) => check !== null)
165
213
  : [];
166
- return failedChecks.length > 0
214
+ const labelWithFailures = failedChecks.length > 0
167
215
  ? `${label} (${failedChecks.join(', ')})`
168
216
  : label;
217
+ return skillScope
218
+ ? `${labelWithFailures} for ${skillScope.skill.skillId}`
219
+ : labelWithFailures;
169
220
  }
170
221
  function readReadinessCheckFailureLabel(value) {
171
222
  if (!value || typeof value !== 'object') {
@@ -181,6 +232,128 @@ function readReadinessCheckFailureLabel(value) {
181
232
  ? record.id
182
233
  : 'unknown check';
183
234
  }
235
+ function filterCapabilitiesForSkill(capabilities, requirements) {
236
+ if (!hasHostedRequirements(requirements)) {
237
+ return [];
238
+ }
239
+ return capabilities.filter((capability) => capabilityMatchesRequirements(capability, requirements));
240
+ }
241
+ function capabilityMatchesRequirements(capability, requirements) {
242
+ const identifiers = collectCapabilityIdentifiers(capability);
243
+ const hostedCapabilities = new Set(requirements.hostedCapabilities);
244
+ const requirementKeys = collectHostedRequirementKeys(requirements);
245
+ return identifiers.some((identifier) => {
246
+ if (identifier === 'media-file:upload' &&
247
+ hostedCapabilities.has('media-file') &&
248
+ !requiresHostedMediaFileUpload(requirements)) {
249
+ return false;
250
+ }
251
+ const [prefix, suffix] = splitCapabilityIdentifier(identifier);
252
+ if (prefix === 'media-file' &&
253
+ suffix &&
254
+ suffix !== 'upload' &&
255
+ hostedCapabilities.has('media-file')) {
256
+ return true;
257
+ }
258
+ if (hostedCapabilities.has(identifier)) {
259
+ return true;
260
+ }
261
+ if (prefix &&
262
+ suffix &&
263
+ hostedCapabilities.has(prefix) &&
264
+ requirementKeys.has(suffix)) {
265
+ return true;
266
+ }
267
+ return requirementKeys.has(identifier) || requirementKeys.has(suffix);
268
+ });
269
+ }
270
+ function requiresHostedMediaFileUpload(requirements) {
271
+ return (requirements.hostedCapabilities.includes('media-generation') ||
272
+ requirements.endpointKeys.length > 0);
273
+ }
274
+ function collectCapabilityIdentifiers(capability) {
275
+ const identifiers = new Set();
276
+ for (const key of [
277
+ 'id',
278
+ 'key',
279
+ 'capability',
280
+ 'capabilityKey',
281
+ 'collectionKey',
282
+ 'endpointKey',
283
+ 'modelKey',
284
+ 'sourceKey',
285
+ 'accountConnection',
286
+ ]) {
287
+ const value = capability[key];
288
+ if (typeof value === 'string' && value.trim()) {
289
+ identifiers.add(value.trim());
290
+ }
291
+ }
292
+ if (Array.isArray(capability.checks)) {
293
+ for (const check of capability.checks) {
294
+ if (!check || typeof check !== 'object' || Array.isArray(check)) {
295
+ continue;
296
+ }
297
+ for (const identifier of collectCapabilityIdentifiers(check)) {
298
+ identifiers.add(identifier);
299
+ }
300
+ }
301
+ }
302
+ return [...identifiers];
303
+ }
304
+ function collectHostedRequirementKeys(requirements) {
305
+ return new Set([
306
+ ...requirements.accountConnections,
307
+ ...requirements.collectionKeys,
308
+ ...requirements.endpointKeys,
309
+ ...requirements.modelKeys,
310
+ ...requirements.sourceKeys,
311
+ ]);
312
+ }
313
+ function hasHostedRequirements(requirements) {
314
+ return (requirements.accountConnections.length > 0 ||
315
+ requirements.collectionKeys.length > 0 ||
316
+ requirements.endpointKeys.length > 0 ||
317
+ requirements.hostedCapabilities.length > 0 ||
318
+ requirements.modelKeys.length > 0 ||
319
+ requirements.sourceKeys.length > 0);
320
+ }
321
+ function collectMissingHostedRequirementLabels(capabilities, requirements) {
322
+ const availableIdentifiers = new Set(capabilities.flatMap(collectCapabilityIdentifiers));
323
+ const missing = [];
324
+ for (const capability of requirements.hostedCapabilities) {
325
+ if (![...availableIdentifiers].some((identifier) => identifierMatchesCapability(identifier, capability))) {
326
+ missing.push(capability);
327
+ }
328
+ }
329
+ for (const key of collectHostedRequirementKeys(requirements)) {
330
+ if (![...availableIdentifiers].some((identifier) => identifierMatchesKey(identifier, key))) {
331
+ missing.push(key);
332
+ }
333
+ }
334
+ return missing.map((value) => `${value} readiness check missing`);
335
+ }
336
+ function identifierMatchesKey(identifier, key) {
337
+ if (identifier === key) {
338
+ return true;
339
+ }
340
+ const [, suffix] = splitCapabilityIdentifier(identifier);
341
+ return suffix === key;
342
+ }
343
+ function identifierMatchesCapability(identifier, capability) {
344
+ if (identifier === capability) {
345
+ return true;
346
+ }
347
+ const [prefix] = splitCapabilityIdentifier(identifier);
348
+ return prefix === capability;
349
+ }
350
+ function splitCapabilityIdentifier(identifier) {
351
+ const index = identifier.indexOf(':');
352
+ if (index === -1) {
353
+ return [null, identifier];
354
+ }
355
+ return [identifier.slice(0, index), identifier.slice(index + 1)];
356
+ }
184
357
  function readErrorMessage(payload, fallback) {
185
358
  const compatibilityError = formatPostPlusCompatibilityError(payload);
186
359
  if (compatibilityError) {
package/build/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
2
3
  import { formatAuthRefreshReport, refreshRemoteAuth, revokeRemoteAuthAndReport, } from './auth-lifecycle.js';
3
4
  import { loginWithCloudHandoff } from './auth-login.js';
4
5
  import { formatAuthValidateReport, validateRemoteAuth, } from './auth-validate.js';
@@ -6,8 +7,9 @@ import { clearAuthState, formatAuthStatusReport, generateAuthStatusReport, } fro
6
7
  import { readCurrentCliVersion } from './client-compatibility.js';
7
8
  import { formatDoctorReport, generateDoctorReport } from './doctor.js';
8
9
  import { assertConfigFilePermissions } from './local-state.js';
10
+ import { readLargeCreditQuoteConfirmationChallenge, resolveLargeCreditQuoteConfirmation, } from './quote-confirmation.js';
9
11
  import { POSTPLUS_SKILLS_INSTALL_COMMAND, loadPublicSkillCatalog, } from './skill-catalog.js';
10
- import { runPostPlusSkillUninstall, runPostPlusSkillUpdate, } from './skill-management.js';
12
+ import { formatSkillBaselineVerifyReport, runPostPlusSkillUninstall, runPostPlusSkillUpdate, runPostPlusSkillVerify, } from './skill-management.js';
11
13
  import { formatStatusReport, generateStatusReport } from './status.js';
12
14
  import { refreshUpdateCheckCache, runCliSelfUpdateIfOutdated, } from './update-check.js';
13
15
  function printAuthHelp() {
@@ -37,27 +39,32 @@ Usage:
37
39
  postplus auth status [--json]
38
40
  postplus auth validate [--json]
39
41
  postplus auth logout [--json]
40
- postplus doctor [--json]
42
+ postplus doctor [--skill <skill-id>] [--json]
43
+ postplus quote confirm --json --challenge-file <path>
44
+ postplus skills verify [--json]
41
45
  postplus update
42
46
  postplus uninstall
43
47
  postplus list [--json]
44
- postplus status [--json]
48
+ postplus status [--skill <skill-id>] [--json]
45
49
  postplus version
46
50
  postplus help
47
51
 
48
52
  Skills:
49
53
  ${POSTPLUS_SKILLS_INSTALL_COMMAND}
54
+
55
+ After first install, run:
56
+ postplus skills verify
50
57
  `);
51
58
  }
52
- async function runDoctor(json) {
53
- const report = await generateDoctorReport();
54
- if (json) {
59
+ async function runDoctor(options) {
60
+ const report = await generateDoctorReport({ skillId: options.skillId });
61
+ if (options.json) {
55
62
  writeJson(report);
56
63
  }
57
64
  else {
58
65
  process.stdout.write(`${formatDoctorReport(report)}\n`);
59
66
  }
60
- return report.ok ? 0 : 1;
67
+ return report.requiredOk ? 0 : 1;
61
68
  }
62
69
  async function runAuthStatus(json) {
63
70
  const report = await generateAuthStatusReport();
@@ -69,9 +76,9 @@ async function runAuthStatus(json) {
69
76
  }
70
77
  return report.ok ? 0 : 1;
71
78
  }
72
- async function runStatus(json) {
73
- const report = await generateStatusReport();
74
- if (json) {
79
+ async function runStatus(options) {
80
+ const report = await generateStatusReport({ skillId: options.skillId });
81
+ if (options.json) {
75
82
  writeJson(report);
76
83
  }
77
84
  else {
@@ -116,9 +123,116 @@ async function runSkillUpdateCommand() {
116
123
  async function runSkillUninstallCommand() {
117
124
  return runPostPlusSkillUninstall();
118
125
  }
126
+ async function runSkillsCommand(rest) {
127
+ const [subcommand] = rest;
128
+ switch (subcommand) {
129
+ case 'verify': {
130
+ const options = rest.slice(1);
131
+ const unknownOption = options.find((option) => option !== '--json');
132
+ if (unknownOption) {
133
+ process.stderr.write(`Unknown option for skills verify: ${unknownOption}\n`);
134
+ return 1;
135
+ }
136
+ const report = await runPostPlusSkillVerify();
137
+ if (options.includes('--json')) {
138
+ writeJson(report);
139
+ }
140
+ else {
141
+ process.stdout.write(`${formatSkillBaselineVerifyReport(report)}\n`);
142
+ }
143
+ return report.ok ? 0 : 1;
144
+ }
145
+ case 'help':
146
+ case '--help':
147
+ case '-h':
148
+ case undefined:
149
+ process.stdout.write(`PostPlus CLI — skills commands
150
+
151
+ Usage:
152
+ postplus skills verify [--json] Verify installed public skills and record the managed baseline
153
+
154
+ Options:
155
+ --json Output results as JSON
156
+ `);
157
+ return 0;
158
+ default:
159
+ process.stderr.write(`Unknown skills command: ${subcommand}\n`);
160
+ return 1;
161
+ }
162
+ }
163
+ async function runQuoteCommand(rest) {
164
+ const [subcommand, ...options] = rest;
165
+ if (subcommand !== 'confirm') {
166
+ process.stderr.write(`Unknown quote command: ${subcommand ?? ''}\n`);
167
+ return 1;
168
+ }
169
+ const parsed = parseQuoteConfirmOptions(options);
170
+ if (!parsed.json) {
171
+ process.stderr.write('quote confirm requires --json.\n');
172
+ return 1;
173
+ }
174
+ if (!parsed.challengeFile) {
175
+ process.stderr.write('quote confirm requires --challenge-file.\n');
176
+ return 1;
177
+ }
178
+ const challenge = readLargeCreditQuoteConfirmationChallenge(JSON.parse(await readFile(parsed.challengeFile, 'utf8')));
179
+ if (!challenge) {
180
+ process.stderr.write('Invalid large credit quote confirmation challenge.\n');
181
+ return 1;
182
+ }
183
+ writeJson(await resolveLargeCreditQuoteConfirmation(challenge));
184
+ return 0;
185
+ }
186
+ function parseQuoteConfirmOptions(args) {
187
+ const options = {
188
+ challengeFile: null,
189
+ json: false,
190
+ };
191
+ for (let index = 0; index < args.length; index += 1) {
192
+ const arg = args[index];
193
+ if (arg === '--json') {
194
+ options.json = true;
195
+ continue;
196
+ }
197
+ if (arg === '--challenge-file') {
198
+ const challengeFile = args[index + 1];
199
+ if (!challengeFile || challengeFile.startsWith('--')) {
200
+ throw new Error('Missing value for --challenge-file.');
201
+ }
202
+ options.challengeFile = challengeFile;
203
+ index += 1;
204
+ continue;
205
+ }
206
+ throw new Error(`Unknown option for quote confirm: ${arg}`);
207
+ }
208
+ return options;
209
+ }
119
210
  function writeJson(value) {
120
211
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
121
212
  }
213
+ function parseDiagnosticOptions(args) {
214
+ const options = {
215
+ json: false,
216
+ };
217
+ for (let index = 0; index < args.length; index += 1) {
218
+ const arg = args[index];
219
+ if (arg === '--json') {
220
+ options.json = true;
221
+ continue;
222
+ }
223
+ if (arg === '--skill') {
224
+ const skillId = args[index + 1];
225
+ if (!skillId || skillId.startsWith('--')) {
226
+ throw new Error('Missing value for --skill.');
227
+ }
228
+ options.skillId = skillId;
229
+ index += 1;
230
+ continue;
231
+ }
232
+ throw new Error(`Unknown option for diagnostics command: ${arg}`);
233
+ }
234
+ return options;
235
+ }
122
236
  async function runAuthLogout(json) {
123
237
  const report = await clearAuthState();
124
238
  if (json) {
@@ -192,6 +306,9 @@ async function main() {
192
306
  if (helpTopic === 'auth') {
193
307
  printAuthHelp();
194
308
  }
309
+ else if (helpTopic === 'skills') {
310
+ await runSkillsCommand(['help']);
311
+ }
195
312
  else {
196
313
  printHelp();
197
314
  }
@@ -199,7 +316,13 @@ async function main() {
199
316
  return;
200
317
  }
201
318
  case 'doctor':
202
- process.exitCode = await runDoctor(json);
319
+ process.exitCode = await runDoctor(parseDiagnosticOptions(rest));
320
+ return;
321
+ case 'quote':
322
+ process.exitCode = await runQuoteCommand(rest);
323
+ return;
324
+ case 'skills':
325
+ process.exitCode = await runSkillsCommand(rest);
203
326
  return;
204
327
  case 'install':
205
328
  process.stderr.write(`PostPlus CLI does not install skills directly. Run \`${POSTPLUS_SKILLS_INSTALL_COMMAND}\`.\n`);
@@ -215,7 +338,7 @@ async function main() {
215
338
  process.exitCode = await runList(json);
216
339
  return;
217
340
  case 'status':
218
- process.exitCode = await runStatus(json);
341
+ process.exitCode = await runStatus(parseDiagnosticOptions(rest));
219
342
  return;
220
343
  case 'auth': {
221
344
  const [subcommand, ...authRest] = rest;
@@ -0,0 +1,163 @@
1
+ import readline from 'node:readline/promises';
2
+ import { readLocalConfig, updateLocalConfig } from './local-state.js';
3
+ const PRODUCT_ERROR_CODE = 'postplus_cli_quote_confirmation_required';
4
+ export function readLargeCreditQuoteConfirmationChallenge(value) {
5
+ if (!value || typeof value !== 'object') {
6
+ return null;
7
+ }
8
+ const record = value;
9
+ const challenge = record.productErrorCode === PRODUCT_ERROR_CODE
10
+ ? record.quoteConfirmation
11
+ : record.quoteConfirmation && typeof record.quoteConfirmation === 'object'
12
+ ? record.quoteConfirmation
13
+ : value;
14
+ if (!challenge || typeof challenge !== 'object') {
15
+ return null;
16
+ }
17
+ const parsed = challenge;
18
+ if (typeof parsed.accountId !== 'string' ||
19
+ typeof parsed.action !== 'string' ||
20
+ typeof parsed.estimatedMillicredits !== 'number' ||
21
+ typeof parsed.featureLabel !== 'string' ||
22
+ typeof parsed.operationId !== 'string' ||
23
+ typeof parsed.requiredTierMillicredits !== 'number' ||
24
+ typeof parsed.reservedMillicredits !== 'number' ||
25
+ typeof parsed.serviceLabel !== 'string' ||
26
+ typeof parsed.token !== 'string') {
27
+ return null;
28
+ }
29
+ return {
30
+ accountId: parsed.accountId,
31
+ action: parsed.action,
32
+ billingUnit: typeof parsed.billingUnit === 'string' ? parsed.billingUnit : undefined,
33
+ drivers: parseDrivers(parsed.drivers),
34
+ estimatedCredits: typeof parsed.estimatedCredits === 'number'
35
+ ? parsed.estimatedCredits
36
+ : undefined,
37
+ estimatedMillicredits: parsed.estimatedMillicredits,
38
+ estimatedOnly: parsed.estimatedOnly === true,
39
+ featureLabel: parsed.featureLabel,
40
+ operationId: parsed.operationId,
41
+ requiredTierCredits: typeof parsed.requiredTierCredits === 'number'
42
+ ? parsed.requiredTierCredits
43
+ : undefined,
44
+ requiredTierMillicredits: parsed.requiredTierMillicredits,
45
+ reservedCredits: typeof parsed.reservedCredits === 'number'
46
+ ? parsed.reservedCredits
47
+ : undefined,
48
+ reservedMillicredits: parsed.reservedMillicredits,
49
+ serviceLabel: parsed.serviceLabel,
50
+ token: parsed.token,
51
+ };
52
+ }
53
+ export async function resolveLargeCreditQuoteConfirmation(challenge, dependencies = {
54
+ confirm: confirmLargeCreditQuote,
55
+ }) {
56
+ const acknowledgedTierMillicredits = await readAcknowledgedTierMillicredits(challenge);
57
+ if (acknowledgedTierMillicredits < challenge.requiredTierMillicredits) {
58
+ await dependencies.confirm(challenge);
59
+ await writeAcknowledgedTierMillicredits(challenge);
60
+ }
61
+ return {
62
+ schemaVersion: 1,
63
+ token: challenge.token,
64
+ };
65
+ }
66
+ export async function confirmLargeCreditQuote(challenge) {
67
+ const terminal = readline.createInterface({
68
+ input: process.stdin,
69
+ output: process.stderr,
70
+ });
71
+ try {
72
+ const answer = await terminal.question(buildLargeCreditConfirmationPrompt(challenge));
73
+ if (answer.trim() !== 'CONFIRM') {
74
+ throw new Error('Large credit charge was not confirmed.');
75
+ }
76
+ }
77
+ finally {
78
+ terminal.close();
79
+ }
80
+ }
81
+ export function buildLargeCreditConfirmationPrompt(challenge) {
82
+ const lines = [
83
+ '',
84
+ 'PostPlus large credit warning',
85
+ `This request crosses the ${formatCredits(challenge.requiredTierMillicredits)}-credit warning tier.`,
86
+ `Estimated charge: ${formatCredits(challenge.estimatedMillicredits)} credits${challenge.estimatedOnly ? ' (estimate)' : ''}.`,
87
+ `Reserved before execution: ${formatCredits(challenge.reservedMillicredits)} credits.`,
88
+ `Capability: ${formatText(challenge.featureLabel)} / ${formatText(challenge.action)}.`,
89
+ `Service: ${formatText(challenge.serviceLabel)}.`,
90
+ ];
91
+ const drivers = Array.isArray(challenge.drivers)
92
+ ? challenge.drivers.filter((driver) => {
93
+ return (driver &&
94
+ typeof driver === 'object' &&
95
+ typeof driver.label === 'string' &&
96
+ driver.value !== undefined &&
97
+ driver.value !== null);
98
+ })
99
+ : [];
100
+ if (drivers.length > 0) {
101
+ lines.push('High-credit drivers:');
102
+ for (const driver of drivers.slice(0, 8)) {
103
+ lines.push(`- ${driver.label}: ${String(driver.value)}`);
104
+ }
105
+ }
106
+ lines.push('PostPlus will warn again only when a future request crosses a higher tier.', 'Type CONFIRM to continue: ');
107
+ return lines.join('\n');
108
+ }
109
+ async function readAcknowledgedTierMillicredits(challenge) {
110
+ const config = await readLocalConfig();
111
+ const tier = config?.largeCreditConfirmation?.acknowledgedTierMillicreditsByAccountId?.[challenge.accountId];
112
+ return typeof tier === 'number' && Number.isSafeInteger(tier) && tier > 0
113
+ ? tier
114
+ : 0;
115
+ }
116
+ async function writeAcknowledgedTierMillicredits(challenge) {
117
+ await updateLocalConfig((current) => {
118
+ const config = current ?? {};
119
+ const largeCreditConfirmation = config.largeCreditConfirmation ?? {};
120
+ const currentTiers = largeCreditConfirmation.acknowledgedTierMillicreditsByAccountId ?? {};
121
+ const previousTier = currentTiers[challenge.accountId];
122
+ return {
123
+ ...config,
124
+ largeCreditConfirmation: {
125
+ ...largeCreditConfirmation,
126
+ acknowledgedTierMillicreditsByAccountId: {
127
+ ...currentTiers,
128
+ [challenge.accountId]: Math.max(typeof previousTier === 'number' &&
129
+ Number.isSafeInteger(previousTier)
130
+ ? previousTier
131
+ : 0, challenge.requiredTierMillicredits),
132
+ },
133
+ },
134
+ };
135
+ });
136
+ }
137
+ function parseDrivers(value) {
138
+ if (!Array.isArray(value)) {
139
+ return [];
140
+ }
141
+ return value
142
+ .filter((driver) => {
143
+ return Boolean(driver) && typeof driver === 'object';
144
+ })
145
+ .filter((driver) => typeof driver.label === 'string')
146
+ .map((driver) => ({
147
+ key: typeof driver.key === 'string' ? driver.key : undefined,
148
+ label: driver.label,
149
+ value: driver.value,
150
+ }));
151
+ }
152
+ function formatText(value) {
153
+ return value.trim() ? value : 'unknown';
154
+ }
155
+ function formatCredits(millicredits) {
156
+ const credits = millicredits / 1_000;
157
+ if (!Number.isFinite(credits)) {
158
+ return 'unknown';
159
+ }
160
+ return Number.isInteger(credits)
161
+ ? String(credits)
162
+ : credits.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
163
+ }
@@ -15,6 +15,15 @@ export const POSTPLUS_SKILLS_INSTALL_COMMAND = formatPostPlusSkillsInstallComman
15
15
  export const POSTPLUS_SKILLS_LIST_COMMAND = formatPostPlusSkillsListCommand();
16
16
  const POSTPLUS_SKILLS_INDEX_URL = 'https://raw.githubusercontent.com/PostPlusAI/postplus-skills/main/skills/INDEX.md';
17
17
  const POSTPLUS_SKILLS_CATALOG_URL = 'https://raw.githubusercontent.com/PostPlusAI/postplus-skills/main/skills/catalog.json';
18
+ export const PUBLIC_SKILL_REQUIREMENT_KEYS = [
19
+ 'accountConnections',
20
+ 'collectionKeys',
21
+ 'endpointKeys',
22
+ 'hostedCapabilities',
23
+ 'localDependencies',
24
+ 'modelKeys',
25
+ 'sourceKeys',
26
+ ];
18
27
  export async function loadPublicSkillCatalog(fetchFn = fetch, env = process.env) {
19
28
  const catalogUrl = resolvePostPlusSkillsCatalogUrl(env);
20
29
  const skillsSource = resolvePostPlusSkillsSource(env);
@@ -95,17 +104,7 @@ function parsePublicSkillCatalog(payload) {
95
104
  const path = typeof skill.path === 'string' && skill.path.trim()
96
105
  ? skill.path.trim()
97
106
  : null;
98
- const requirements = skill.requirements &&
99
- typeof skill.requirements === 'object' &&
100
- !Array.isArray(skill.requirements)
101
- ? skill.requirements
102
- : {};
103
- const localDependencies = Array.isArray(requirements.localDependencies)
104
- ? requirements.localDependencies
105
- .filter((value) => typeof value === 'string')
106
- .map((value) => value.trim())
107
- .filter(Boolean)
108
- : [];
107
+ const requirements = parsePublicSkillRequirements(skill.requirements);
109
108
  const status = typeof skill.status === 'string' ? skill.status.trim() : '';
110
109
  if (!skillId ||
111
110
  !path ||
@@ -113,9 +112,10 @@ function parsePublicSkillCatalog(payload) {
113
112
  throw new Error('PostPlus public skill catalog has an invalid skill.');
114
113
  }
115
114
  return {
116
- localDependencies,
115
+ localDependencies: requirements.localDependencies,
117
116
  skillId,
118
117
  path,
118
+ requirements,
119
119
  };
120
120
  });
121
121
  if (skills.length === 0) {
@@ -127,3 +127,40 @@ function parsePublicSkillCatalog(payload) {
127
127
  source,
128
128
  };
129
129
  }
130
+ function parsePublicSkillRequirements(value) {
131
+ if (value === undefined) {
132
+ return createEmptyRequirements();
133
+ }
134
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
135
+ throw new Error('PostPlus public skill catalog has invalid skill requirements.');
136
+ }
137
+ const record = value;
138
+ const requirements = createEmptyRequirements();
139
+ for (const key of PUBLIC_SKILL_REQUIREMENT_KEYS) {
140
+ const raw = record[key];
141
+ if (raw === undefined) {
142
+ continue;
143
+ }
144
+ if (!Array.isArray(raw)) {
145
+ throw new Error(`PostPlus public skill catalog has invalid ${key} requirements.`);
146
+ }
147
+ requirements[key] = raw.map((item) => {
148
+ if (typeof item !== 'string' || !item.trim()) {
149
+ throw new Error(`PostPlus public skill catalog has invalid ${key} requirements.`);
150
+ }
151
+ return item.trim();
152
+ });
153
+ }
154
+ return requirements;
155
+ }
156
+ function createEmptyRequirements() {
157
+ return {
158
+ accountConnections: [],
159
+ collectionKeys: [],
160
+ endpointKeys: [],
161
+ hostedCapabilities: [],
162
+ localDependencies: [],
163
+ modelKeys: [],
164
+ sourceKeys: [],
165
+ };
166
+ }
@@ -49,8 +49,38 @@ export async function runPostPlusSkillUninstall(dependencies = {
49
49
  export async function generateSkillInstallStatusReport(dependencies = {
50
50
  runCommand,
51
51
  }) {
52
+ return (await inspectPostPlusSkillInstall(dependencies)).report;
53
+ }
54
+ export async function runPostPlusSkillVerify(dependencies = {
55
+ runCommand,
56
+ }) {
57
+ const inspection = await inspectPostPlusSkillInstall(dependencies);
58
+ const previousManagedSkillsReleaseId = inspection.report.managedSkillsReleaseId;
59
+ if (!inspection.report.ok) {
60
+ return {
61
+ ...inspection.report,
62
+ baselineUpdated: false,
63
+ previousManagedSkillsReleaseId,
64
+ verifiedSkillsReleaseId: null,
65
+ };
66
+ }
67
+ await writeManagedSkillBaseline({
68
+ releaseId: inspection.catalog.releaseId,
69
+ skillNames: inspection.requiredSkillNames,
70
+ });
71
+ await writeCurrentCliVersionToLocalConfig();
72
+ return {
73
+ ...inspection.report,
74
+ baselineUpdated: true,
75
+ managedSkillsReleaseId: inspection.catalog.releaseId,
76
+ previousManagedSkillsReleaseId,
77
+ verifiedSkillsReleaseId: inspection.catalog.releaseId,
78
+ };
79
+ }
80
+ async function inspectPostPlusSkillInstall(dependencies) {
52
81
  const catalog = await loadPublicSkillCatalog();
53
- const requiredSkills = new Set(catalog.skills.map((skill) => skill.skillId));
82
+ const requiredSkillNames = catalog.skills.map((skill) => skill.skillId);
83
+ const requiredSkills = new Set(requiredSkillNames);
54
84
  const baseline = await readManagedSkillBaseline();
55
85
  const retiredManagedSkills = baseline.skillNames.filter((skillName) => !requiredSkills.has(skillName));
56
86
  try {
@@ -64,36 +94,44 @@ export async function generateSkillInstallStatusReport(dependencies = {
64
94
  .filter((scope) => scope.trim().length > 0)),
65
95
  ].sort();
66
96
  return {
67
- ok: missingSkills.length === 0,
68
- error: null,
69
- installCommand: formatPostPlusSkillsInstallCommand(catalog.source),
70
- installedCount: installedNames.size,
71
- managedSkillsReleaseId: baseline.releaseId,
72
- missingSkills,
73
- requiredCount: requiredSkills.size,
74
- retiredManagedSkills,
75
- scopes,
76
- source: catalog.source,
77
- updateCommand: formatPostPlusSkillUpdateCommand(),
78
- uninstallCommand: formatPostPlusSkillUninstallCommand(),
97
+ catalog,
98
+ report: {
99
+ ok: missingSkills.length === 0,
100
+ error: null,
101
+ installCommand: formatPostPlusSkillsInstallCommand(catalog.source),
102
+ installedCount: installedNames.size,
103
+ managedSkillsReleaseId: baseline.releaseId,
104
+ missingSkills,
105
+ requiredCount: requiredSkills.size,
106
+ retiredManagedSkills,
107
+ scopes,
108
+ source: catalog.source,
109
+ updateCommand: formatPostPlusSkillUpdateCommand(),
110
+ uninstallCommand: formatPostPlusSkillUninstallCommand(),
111
+ },
112
+ requiredSkillNames,
79
113
  };
80
114
  }
81
115
  catch (error) {
82
116
  return {
83
- ok: false,
84
- error: error instanceof Error
85
- ? error.message
86
- : 'Failed to inspect installed PostPlus skills.',
87
- installCommand: formatPostPlusSkillsInstallCommand(catalog.source),
88
- installedCount: 0,
89
- managedSkillsReleaseId: baseline.releaseId,
90
- missingSkills: [...requiredSkills],
91
- requiredCount: requiredSkills.size,
92
- retiredManagedSkills,
93
- scopes: [],
94
- source: catalog.source,
95
- updateCommand: formatPostPlusSkillUpdateCommand(),
96
- uninstallCommand: formatPostPlusSkillUninstallCommand(),
117
+ catalog,
118
+ report: {
119
+ ok: false,
120
+ error: error instanceof Error
121
+ ? error.message
122
+ : 'Failed to inspect installed PostPlus skills.',
123
+ installCommand: formatPostPlusSkillsInstallCommand(catalog.source),
124
+ installedCount: 0,
125
+ managedSkillsReleaseId: baseline.releaseId,
126
+ missingSkills: [...requiredSkills],
127
+ requiredCount: requiredSkills.size,
128
+ retiredManagedSkills,
129
+ scopes: [],
130
+ source: catalog.source,
131
+ updateCommand: formatPostPlusSkillUpdateCommand(),
132
+ uninstallCommand: formatPostPlusSkillUninstallCommand(),
133
+ },
134
+ requiredSkillNames,
97
135
  };
98
136
  }
99
137
  }
@@ -122,6 +160,31 @@ export function formatSkillInstallStatusReport(report) {
122
160
  }
123
161
  return lines.join('\n');
124
162
  }
163
+ export function formatSkillBaselineVerifyReport(report) {
164
+ const lines = ['PostPlus skills verify', ''];
165
+ if (report.error) {
166
+ lines.push(`[FAIL] Skill installer: ${report.error}`);
167
+ }
168
+ else if (report.ok) {
169
+ lines.push(`[PASS] Installed released skills: ${report.installedCount}/${report.requiredCount}`);
170
+ }
171
+ else {
172
+ lines.push(`[FAIL] Installed released skills: ${report.installedCount}/${report.requiredCount}`);
173
+ }
174
+ lines.push(` Source: ${report.source}`);
175
+ lines.push(` Previous managed baseline: ${report.previousManagedSkillsReleaseId ?? 'none'}`);
176
+ if (report.baselineUpdated && report.verifiedSkillsReleaseId) {
177
+ lines.push(` Verified baseline: ${report.verifiedSkillsReleaseId}`);
178
+ lines.push(' Next: postplus status');
179
+ }
180
+ else {
181
+ lines.push(' Verified baseline: unchanged');
182
+ }
183
+ if (report.missingSkills.length > 0) {
184
+ lines.push(` Missing: ${formatSkillList(report.missingSkills, 8)}`, ` Fix: ${report.installCommand}`);
185
+ }
186
+ return lines.join('\n');
187
+ }
125
188
  export function buildPostPlusSkillUpdateArgs(skillNames) {
126
189
  if (skillNames.length === 0) {
127
190
  throw new Error('PostPlus public skill catalog has no released skills.');
package/build/status.js CHANGED
@@ -3,17 +3,17 @@ import { writeCurrentCliVersionToLocalConfig } from './client-compatibility.js';
3
3
  import { formatDoctorReport, generateDoctorReport, } from './doctor.js';
4
4
  import { formatSkillInstallStatusReport, generateSkillInstallStatusReport, } from './skill-management.js';
5
5
  import { formatUpdateStatusReport, generateUpdateStatusReport, } from './update-check.js';
6
- export async function generateStatusReport() {
7
- return generateStatusReportWithDependencies();
6
+ export async function generateStatusReport(options = {}) {
7
+ return generateStatusReportWithDependencies({}, options);
8
8
  }
9
- export async function generateStatusReportWithDependencies(dependencies = {}) {
9
+ export async function generateStatusReportWithDependencies(dependencies = {}, options = {}) {
10
10
  await writeCurrentCliVersionToLocalConfig();
11
11
  const generateAuthStatus = dependencies.generateAuthStatus ?? generateAuthStatusReport;
12
12
  const generateDoctor = dependencies.generateDoctor ?? generateDoctorReport;
13
13
  const generateSkillStatus = dependencies.generateSkillStatus ?? generateSkillInstallStatusReport;
14
14
  const generateUpdateStatus = dependencies.generateUpdateStatus ?? generateUpdateStatusReport;
15
15
  const [doctor, auth, skills, updates] = await Promise.all([
16
- generateDoctor(),
16
+ generateDoctor({ skillId: options.skillId }),
17
17
  generateAuthStatus(),
18
18
  generateSkillStatus(),
19
19
  generateUpdateStatus(),
@@ -25,6 +25,7 @@ export async function generateStatusReportWithDependencies(dependencies = {}) {
25
25
  auth,
26
26
  skills,
27
27
  updates,
28
+ ...(options.skillId ? { skillId: options.skillId } : {}),
28
29
  };
29
30
  }
30
31
  export function formatStatusReport(report) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postplus/cli",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
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.",
@@ -18,6 +18,7 @@
18
18
  "build/index.js",
19
19
  "build/local-dependencies.js",
20
20
  "build/local-state.js",
21
+ "build/quote-confirmation.js",
21
22
  "build/skill-catalog.js",
22
23
  "build/skill-management.js",
23
24
  "build/status.js",