@postplus/cli 0.1.38 → 0.1.39

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.
@@ -17,10 +17,7 @@ const HOSTED_DOMAIN_CAPABILITIES = {
17
17
  media: new Set(['media-file', 'media-generation', 'video-analysis']),
18
18
  mobile: new Set(['mobile-automation']),
19
19
  publish: new Set(['social-publishing']),
20
- research: new Set([
21
- 'public-content-collection',
22
- 'public-content-discovery',
23
- ]),
20
+ research: new Set(['public-content-collection', 'public-content-discovery']),
24
21
  };
25
22
  export async function runHostedDomainCommand(domain, args) {
26
23
  const [subcommand, ...rest] = args;
@@ -69,6 +66,16 @@ async function runResearchCollect(args) {
69
66
  `postplus-cli:research:${collectionKey}:${randomUUID()}`;
70
67
  const quoteConfirmationToken = flags.values.get('quote-confirmation-token') ??
71
68
  normalizeString(envelope.quoteConfirmationToken);
69
+ // Optional per-request cost ceiling (USD) overriding the hosted default.
70
+ const maxChargeFlag = flags.values.get('max-charge-usd');
71
+ let maxTotalChargeUsd;
72
+ if (maxChargeFlag !== undefined) {
73
+ const parsed = Number(maxChargeFlag);
74
+ if (!Number.isFinite(parsed) || parsed <= 0) {
75
+ throw new Error('--max-charge-usd must be a positive number of USD.');
76
+ }
77
+ maxTotalChargeUsd = parsed;
78
+ }
72
79
  const payload = await postHostedJson({
73
80
  body: {
74
81
  collectionKey,
@@ -76,6 +83,7 @@ async function runResearchCollect(args) {
76
83
  operationId,
77
84
  quoteConfirmationToken: quoteConfirmationToken ?? undefined,
78
85
  skillName,
86
+ maxTotalChargeUsd,
79
87
  },
80
88
  pathName: '/api/postplus-cli/hosted/collection',
81
89
  skillName,
@@ -348,7 +356,7 @@ function printResearchHelp() {
348
356
 
349
357
  Usage:
350
358
  postplus research schema [--collection-key <key>] [--json]
351
- postplus research collect --skill <skill-id> --collection-key <key> --input <hosted-envelope.json> [--output <result.json>]
359
+ postplus research collect --skill <skill-id> --collection-key <key> --input <hosted-envelope.json> [--max-charge-usd <usd>] [--output <result.json>]
352
360
  postplus research collect --run-handle <runHandle> [--output <result.json>]
353
361
  postplus research capability --request <hosted-capability-request.json> [--output <result.json>]
354
362
  `);
@@ -1,60 +1,72 @@
1
1
  // Generated from the PostPlus Cloud hosted catalog release gate.
2
2
  // Keep keys in sync with apps/web hosted capability and collection catalogs.
3
+ //
4
+ // The result-count field in each hint is a fetch-volume example using the actor's
5
+ // REAL input field (it shapes how much the actor fetches, not the cost ceiling).
6
+ // Total spend is bounded server-side by a per-request USD budget — pay-per-event
7
+ // actors via Apify maxTotalChargeUsd, Bright Data Facebook via
8
+ // limit_multiple_results — so these values are starting points, not the cap.
9
+ // Field names matter: clockworks TikTok actors fetch per resultsPerPage /
10
+ // maxProfilesPerQuery / commentsPerPost (maxItems is a run option they ignore as
11
+ // input) and apidojo youtube-comments uses maxItems (not maxComments).
3
12
  export const RESEARCH_COLLECTION_HINTS = {
4
13
  'google-trends-fast': {
5
- queries: ['portable blender'],
14
+ enableTrendingSearches: false,
15
+ geo: 'US',
16
+ keyword: 'portable blender',
17
+ timeframe: 'today 12-m',
6
18
  },
7
19
  'instagram-comments': {
8
20
  directUrls: ['https://www.instagram.com/p/example/'],
9
- resultsLimit: 5,
21
+ resultsLimit: 20,
10
22
  },
11
23
  'instagram-email-search': {
12
24
  Country: 'www',
13
25
  Email_Type: '0',
14
26
  Keyword: 'skincare creator',
15
- Limit: '10',
27
+ Limit: '25',
16
28
  social_network: 'instagram.com/',
17
29
  },
18
30
  'instagram-hashtags': {
19
31
  hashtags: ['desksetup'],
20
- resultsLimit: 3,
32
+ resultsLimit: 10,
21
33
  },
22
34
  'instagram-posts': {
23
- resultsLimit: 3,
35
+ resultsLimit: 12,
24
36
  username: ['openai'],
25
37
  },
26
38
  'instagram-profiles': {
27
- resultsLimit: 3,
28
39
  usernames: ['instagram'],
29
40
  },
30
41
  'instagram-search': {
31
- searchLimit: 3,
42
+ searchLimit: 10,
32
43
  searchTerms: ['skincare routine'],
33
44
  searchType: 'user',
34
45
  },
35
46
  'tiktok-ads-top': {
36
47
  include_analytics: true,
37
- limit: 1,
48
+ limit: 20,
38
49
  },
39
50
  'tiktok-comments': {
51
+ commentsPerPost: 20,
40
52
  postURLs: ['https://www.tiktok.com/@example/video/1234567890'],
41
- resultsPerPage: 5,
42
53
  },
43
54
  'tiktok-profiles': {
55
+ resultsPerPage: 12,
44
56
  usernames: ['tiktok'],
45
57
  },
46
58
  'tiktok-related-videos': {
47
- maxItems: 3,
48
59
  postURLs: ['https://www.tiktok.com/@example/video/1234567890'],
60
+ resultsPerPage: 10,
49
61
  },
50
62
  'tiktok-users': {
51
- maxItems: 5,
63
+ maxProfilesPerQuery: 10,
52
64
  searchQueries: ['skincare creator'],
53
65
  },
54
66
  'tiktok-videos': {
55
- maxItems: 3,
56
67
  proxyCountryCode: 'US',
57
68
  queries: ['portable blender'],
69
+ resultsPerPage: 10,
58
70
  searchSection: '/video',
59
71
  },
60
72
  'youtube-channel-summary': {
@@ -64,7 +76,7 @@ export const RESEARCH_COLLECTION_HINTS = {
64
76
  maxVideosPerChannel: 0,
65
77
  },
66
78
  'youtube-comments': {
67
- maxComments: 10,
79
+ maxItems: 50,
68
80
  startUrls: ['https://www.youtube.com/watch?v=dQw4w9WgXcQ'],
69
81
  },
70
82
  'youtube-video-download': {
@@ -74,6 +86,7 @@ export const RESEARCH_COLLECTION_HINTS = {
74
86
  export const PUBLIC_CONTENT_SOURCE_HINTS = {
75
87
  'facebook-group-posts': [
76
88
  {
89
+ num_of_posts: 25,
77
90
  url: 'https://www.facebook.com/groups/example',
78
91
  },
79
92
  ],
@@ -84,6 +97,7 @@ export const PUBLIC_CONTENT_SOURCE_HINTS = {
84
97
  ],
85
98
  'facebook-profile-posts': [
86
99
  {
100
+ num_of_posts: 25,
87
101
  url: 'https://www.facebook.com/openai',
88
102
  },
89
103
  ],
@@ -1,9 +1,15 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
1
4
  import { writeCurrentCliVersionToLocalConfig } from './client-compatibility.js';
2
5
  import { runCommand, runInteractiveCommand } from './command-runner.js';
3
6
  import { clearManagedSkillBaseline, readManagedSkillBaseline, writeManagedSkillBaseline, } from './local-state.js';
4
7
  import { POSTPLUS_SKILLS_AGENT_TARGETS, formatPostPlusSkillsInstallCommand, resolvePostPlusSkillsSource, loadPublicSkillCatalog, } from './skill-catalog.js';
5
8
  import { clearUpdateCheckCache } from './update-check.js';
6
9
  const NPX_SKILLS = ['-y', 'skills'];
10
+ const SKILLS_INSTALLER_GLOBAL_LOCK_PATH = ['.agents', '.skill-lock.json'];
11
+ const SKILLS_INSTALLER_PROJECT_LOCK_PATH = 'skills-lock.json';
12
+ const SKILLS_INSTALLER_POSTPLUS_SOURCE = 'postplusai/postplus-skills';
7
13
  const DEFAULT_SKILL_MUTATION_OPTIONS = {
8
14
  scope: 'global',
9
15
  };
@@ -12,8 +18,10 @@ export async function runPostPlusSkillUpdate(dependencies = {
12
18
  }, options = DEFAULT_SKILL_MUTATION_OPTIONS) {
13
19
  const catalog = await loadPublicSkillCatalog();
14
20
  const skillNames = catalog.skills.map((skill) => skill.skillId);
21
+ const releasedSkills = new Set(skillNames);
15
22
  const baseline = await readManagedSkillBaseline();
16
- const retiredSkillNames = baseline.skillNames.filter((skillName) => !skillNames.includes(skillName));
23
+ const lockedSkillNames = await readPostPlusInstallerLockedSkillEntries(options.scope).then((entries) => entries.map((entry) => entry.name));
24
+ const retiredSkillNames = mergeSkillNames(baseline.skillNames, lockedSkillNames).filter((skillName) => !releasedSkills.has(skillName));
17
25
  if (skillNames.length === 0) {
18
26
  throw new Error('PostPlus public skill catalog has no released skills.');
19
27
  }
@@ -45,7 +53,8 @@ export async function runPostPlusSkillUninstall(dependencies = {
45
53
  const catalog = await loadPublicSkillCatalog();
46
54
  const skillNames = catalog.skills.map((skill) => skill.skillId);
47
55
  const baseline = await readManagedSkillBaseline();
48
- const allKnownSkillNames = mergeSkillNames(skillNames, baseline.skillNames);
56
+ const lockedSkillNames = await readPostPlusInstallerLockedSkillEntries(options.scope).then((entries) => entries.map((entry) => entry.name));
57
+ const allKnownSkillNames = mergeSkillNames(mergeSkillNames(skillNames, baseline.skillNames), lockedSkillNames);
49
58
  if (allKnownSkillNames.length === 0) {
50
59
  throw new Error('PostPlus public skill catalog has no released skills.');
51
60
  }
@@ -96,9 +105,20 @@ async function inspectPostPlusSkillInstall(dependencies, options = {}) {
96
105
  const requiredSkillNames = catalog.skills.map((skill) => skill.skillId);
97
106
  const requiredSkills = new Set(requiredSkillNames);
98
107
  const baseline = await readManagedSkillBaseline();
99
- const retiredManagedSkills = baseline.skillNames.filter((skillName) => !requiredSkills.has(skillName));
108
+ const baselineRetiredManagedSkills = baseline.skillNames.filter((skillName) => !requiredSkills.has(skillName));
100
109
  try {
101
110
  const installed = await listInstalledSkills(dependencies);
111
+ const baselineRetiredSkills = new Set(baselineRetiredManagedSkills);
112
+ const lockedSkills = new Set((await readPostPlusInstallerLockedSkillEntries()).map((entry) => `${entry.scope}:${entry.name}`));
113
+ const installedRetiredManagedSkills = [
114
+ ...new Set(installed
115
+ .filter((skill) => baselineRetiredSkills.has(skill.name) ||
116
+ lockedSkills.has(`${skill.scope}:${skill.name}`))
117
+ .map((skill) => skill.name)),
118
+ ]
119
+ .filter((skillName) => !requiredSkills.has(skillName))
120
+ .sort((a, b) => a.localeCompare(b));
121
+ const retiredManagedSkills = mergeSkillNames(baselineRetiredManagedSkills, installedRetiredManagedSkills);
102
122
  const postPlusInstalled = installed.filter((skill) => requiredSkills.has(skill.name));
103
123
  const installedNames = new Set(postPlusInstalled.map((skill) => skill.name));
104
124
  const missingSkills = [...requiredSkills].filter((skill) => !installedNames.has(skill));
@@ -110,7 +130,8 @@ async function inspectPostPlusSkillInstall(dependencies, options = {}) {
110
130
  baseline,
111
131
  releaseId: catalog.releaseId,
112
132
  skillNames: requiredSkillNames,
113
- })) {
133
+ }) &&
134
+ installedRetiredManagedSkills.length === 0) {
114
135
  await writeManagedSkillBaseline({
115
136
  releaseId: catalog.releaseId,
116
137
  skillNames: requiredSkillNames,
@@ -128,7 +149,8 @@ async function inspectPostPlusSkillInstall(dependencies, options = {}) {
128
149
  return {
129
150
  catalog,
130
151
  report: {
131
- ok: missingSkills.length === 0,
152
+ ok: missingSkills.length === 0 &&
153
+ installedRetiredManagedSkills.length === 0,
132
154
  error: null,
133
155
  installCommand: formatPostPlusSkillsInstallCommand(catalog.source),
134
156
  installedCount: installedNames.size,
@@ -157,7 +179,7 @@ async function inspectPostPlusSkillInstall(dependencies, options = {}) {
157
179
  managedSkillsReleaseId: baseline.releaseId,
158
180
  missingSkills: [...requiredSkills],
159
181
  requiredCount: requiredSkills.size,
160
- retiredManagedSkills,
182
+ retiredManagedSkills: baselineRetiredManagedSkills,
161
183
  scopes: [],
162
184
  source: catalog.source,
163
185
  updateCommand: formatPostPlusSkillUpdateCommand(),
@@ -212,6 +234,9 @@ export function formatSkillBaselineVerifyReport(report) {
212
234
  else {
213
235
  lines.push(' Verified baseline: unchanged');
214
236
  }
237
+ if (report.retiredManagedSkills.length > 0) {
238
+ lines.push(` Retired managed skills: ${formatSkillList(report.retiredManagedSkills, 8)}`, ` Cleanup (global): ${report.updateCommand}`, ` Cleanup (current directory): ${formatPostPlusSkillUpdateCommand('current-directory')}`);
239
+ }
215
240
  if (report.missingSkills.length > 0) {
216
241
  lines.push(` Missing: ${formatSkillList(report.missingSkills, 8)}`, ` Fix (global): ${report.installCommand}`, ` Fix (current directory): ${formatPostPlusSkillsInstallCommand(report.source, 'current-directory')}`);
217
242
  }
@@ -274,6 +299,106 @@ function haveSameSkillNames(left, right) {
274
299
  return (normalizedLeft.length === normalizedRight.length &&
275
300
  normalizedLeft.every((value, index) => value === normalizedRight[index]));
276
301
  }
302
+ async function readPostPlusInstallerLockedSkillEntries(scope) {
303
+ const lockPaths = scope === 'global'
304
+ ? [{ path: getSkillsInstallerGlobalLockPath(), scope: 'global' }]
305
+ : scope === 'current-directory'
306
+ ? [
307
+ {
308
+ path: getSkillsInstallerProjectLockPath(),
309
+ scope: 'project',
310
+ },
311
+ ]
312
+ : [
313
+ {
314
+ path: getSkillsInstallerProjectLockPath(),
315
+ scope: 'project',
316
+ },
317
+ {
318
+ path: getSkillsInstallerGlobalLockPath(),
319
+ scope: 'global',
320
+ },
321
+ ];
322
+ const entries = await Promise.all(lockPaths.map((lock) => readPostPlusInstallerLockedSkillNamesFromPath(lock.path).then((skillNames) => skillNames.map((name) => ({
323
+ name,
324
+ scope: lock.scope,
325
+ })))));
326
+ return entries
327
+ .flat()
328
+ .sort((left, right) => left.scope.localeCompare(right.scope) ||
329
+ left.name.localeCompare(right.name));
330
+ }
331
+ async function readPostPlusInstallerLockedSkillNamesFromPath(lockPath) {
332
+ try {
333
+ const raw = await readFile(lockPath, 'utf8');
334
+ const payload = JSON.parse(raw);
335
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
336
+ return [];
337
+ }
338
+ const record = payload;
339
+ if (typeof record.version !== 'number') {
340
+ return [];
341
+ }
342
+ if (!record.skills || typeof record.skills !== 'object') {
343
+ return [];
344
+ }
345
+ return Object.entries(record.skills)
346
+ .filter(([, entry]) => isPostPlusSkillsInstallerLockEntry(entry))
347
+ .map(([skillName]) => skillName.trim())
348
+ .filter(Boolean)
349
+ .sort((a, b) => a.localeCompare(b));
350
+ }
351
+ catch (error) {
352
+ const nodeError = error;
353
+ if (nodeError.code === 'ENOENT') {
354
+ return [];
355
+ }
356
+ throw error;
357
+ }
358
+ }
359
+ function isPostPlusSkillsInstallerLockEntry(entry) {
360
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
361
+ return false;
362
+ }
363
+ const record = entry;
364
+ const source = typeof record.source === 'string' ? record.source.trim() : '';
365
+ const sourceUrl = typeof record.sourceUrl === 'string' ? record.sourceUrl.trim() : '';
366
+ return (normalizeSkillsInstallerSource(source) ===
367
+ SKILLS_INSTALLER_POSTPLUS_SOURCE ||
368
+ normalizeSkillsInstallerSource(sourceUrl) ===
369
+ SKILLS_INSTALLER_POSTPLUS_SOURCE);
370
+ }
371
+ function normalizeSkillsInstallerSource(value) {
372
+ let normalized = value.trim().replace(/\\/g, '/');
373
+ if (normalized.length === 0) {
374
+ return '';
375
+ }
376
+ const sshMatch = normalized.match(/^git@[^:]+:(.+)$/);
377
+ if (sshMatch) {
378
+ normalized = sshMatch[1] ?? '';
379
+ }
380
+ else if (/^https?:\/\//i.test(normalized) || /^ssh:\/\//i.test(normalized)) {
381
+ try {
382
+ normalized = new URL(normalized).pathname.replace(/^\/+/, '');
383
+ }
384
+ catch {
385
+ return normalized.toLowerCase();
386
+ }
387
+ }
388
+ return normalized
389
+ .replace(/\.git$/i, '')
390
+ .replace(/\/+$/, '')
391
+ .toLowerCase();
392
+ }
393
+ function getSkillsInstallerGlobalLockPath() {
394
+ const xdgStateHome = process.env.XDG_STATE_HOME?.trim();
395
+ return xdgStateHome
396
+ ? join(xdgStateHome, 'skills', '.skill-lock.json')
397
+ : join(homedir(), ...SKILLS_INSTALLER_GLOBAL_LOCK_PATH);
398
+ }
399
+ function getSkillsInstallerProjectLockPath() {
400
+ return join(process.cwd(), SKILLS_INSTALLER_PROJECT_LOCK_PATH);
401
+ }
277
402
  async function listInstalledSkills(dependencies) {
278
403
  const project = await listInstalledSkillsForScope(dependencies, []);
279
404
  const global = await listInstalledSkillsForScope(dependencies, ['--global']);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postplus/cli",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
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.",