@prave/cli 1.0.1 → 1.0.3
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/dist/commands/deploy.js +22 -1
- package/dist/commands/export.js +4 -4
- package/dist/commands/import.js +20 -7
- package/dist/commands/install.js +23 -0
- package/dist/commands/update.js +8 -0
- package/dist/commands/usage.js +143 -16
- package/dist/index.js +91 -17
- package/dist/lib/plan.js +11 -3
- package/package.json +2 -2
package/dist/commands/deploy.js
CHANGED
|
@@ -6,6 +6,7 @@ import ora from 'ora';
|
|
|
6
6
|
import { AGENT_REGISTRY } from '@prave/shared';
|
|
7
7
|
import { api, ApiError } from '../lib/api.js';
|
|
8
8
|
import { requireAuth } from '../lib/credentials.js';
|
|
9
|
+
import { fetchMyPlan, formatUpgradeHint } from '../lib/plan.js';
|
|
9
10
|
import { CONFIG } from '../lib/config.js';
|
|
10
11
|
import { log } from '../utils/logger.js';
|
|
11
12
|
function detectOsKey(detected) {
|
|
@@ -73,6 +74,17 @@ export async function deployCommand(skillName, opts = {}) {
|
|
|
73
74
|
if (!session)
|
|
74
75
|
return;
|
|
75
76
|
const start = Date.now();
|
|
77
|
+
// Plan-gate the multi-agent target list. Free can only deploy to
|
|
78
|
+
// Claude Code; Pro+ unlocks all 6. We compute the allowed set from
|
|
79
|
+
// PLAN_LIMITS so the Stripe-side and CLI-side stay in lockstep.
|
|
80
|
+
const me = await fetchMyPlan();
|
|
81
|
+
const allowedAgents = new Set(me.limits.multi_agent_targets);
|
|
82
|
+
if (allowedAgents.size === 0) {
|
|
83
|
+
log.warn(`Your ${me.limits.label} plan can't deploy to any agent.`);
|
|
84
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
76
88
|
let settings;
|
|
77
89
|
try {
|
|
78
90
|
const { data } = await api.get('/api/v1/settings/agents', true);
|
|
@@ -86,7 +98,16 @@ export async function deployCommand(skillName, opts = {}) {
|
|
|
86
98
|
const targetFilter = opts.agent && opts.agent.toLowerCase() !== 'all'
|
|
87
99
|
? opts.agent.toLowerCase()
|
|
88
100
|
: null;
|
|
89
|
-
const targets = settings.enabled_agents
|
|
101
|
+
const targets = settings.enabled_agents
|
|
102
|
+
.filter((a) => allowedAgents.has(a))
|
|
103
|
+
.filter((a) => targetFilter === null || a === targetFilter);
|
|
104
|
+
// If the user picked a target their plan can't reach, tell them why.
|
|
105
|
+
if (targetFilter && !allowedAgents.has(targetFilter)) {
|
|
106
|
+
log.warn(`Deploying to ${targetFilter} requires the Pro plan. Free can only deploy to Claude Code.`);
|
|
107
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
90
111
|
if (targets.length === 0) {
|
|
91
112
|
log.warn('No matching agents enabled. Run `prave settings` to configure.');
|
|
92
113
|
return;
|
package/dist/commands/export.js
CHANGED
|
@@ -10,12 +10,12 @@ import { log } from '../utils/logger.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export async function exportCommand(slug, opts = {}) {
|
|
12
12
|
const session = await loadCredentials();
|
|
13
|
-
// Plan gate:
|
|
14
|
-
//
|
|
13
|
+
// Plan gate: export is part of the Pro+ authoring toolkit. Free can browse
|
|
14
|
+
// and install, not extract for republishing.
|
|
15
15
|
if (session) {
|
|
16
16
|
const me = await fetchMyPlan();
|
|
17
|
-
if (me.limits.
|
|
18
|
-
log.warn('Export requires the
|
|
17
|
+
if (!me.limits.can_authoring_public && !me.limits.can_authoring_private) {
|
|
18
|
+
log.warn('Export requires the Pro plan or higher.');
|
|
19
19
|
log.dim(formatUpgradeHint('explorer'));
|
|
20
20
|
return;
|
|
21
21
|
}
|
package/dist/commands/import.js
CHANGED
|
@@ -79,19 +79,32 @@ export async function importCommand(opts) {
|
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
const visibility = opts.private ? 'private' : 'public';
|
|
82
|
-
// Plan gate:
|
|
83
|
-
//
|
|
82
|
+
// Plan gate: authoring (public + private) is Pro+. Free is read-only on
|
|
83
|
+
// the registry side — discover, install, bookmark, but no upload.
|
|
84
84
|
const me = await fetchMyPlan();
|
|
85
|
-
if (
|
|
86
|
-
log.warn('
|
|
85
|
+
if (!me.limits.can_authoring_public && !me.limits.can_authoring_private) {
|
|
86
|
+
log.warn('Uploading Skills requires the Pro plan or higher.');
|
|
87
87
|
log.dim(formatUpgradeHint('explorer'));
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
|
+
if (visibility === 'private' && !me.limits.can_authoring_private) {
|
|
91
|
+
log.warn('Private uploads require the Pro plan.');
|
|
92
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (visibility === 'public' && !me.limits.can_authoring_public) {
|
|
96
|
+
log.warn('Public uploads require the Pro plan.');
|
|
97
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Clamp the queue to the caller's authoring ceiling. `null` = unlimited
|
|
101
|
+
// (Pro + Max), so the cap-check is a no-op for paid tiers.
|
|
90
102
|
let queue = skills;
|
|
91
|
-
if (me.limits.
|
|
92
|
-
|
|
103
|
+
if (me.limits.authoring_max_skills !== null &&
|
|
104
|
+
queue.length > me.limits.authoring_max_skills) {
|
|
105
|
+
log.warn(`Your ${me.limits.label} plan caps authored Skills at ${me.limits.authoring_max_skills}. Trimming queue from ${queue.length} → ${me.limits.authoring_max_skills}.`);
|
|
93
106
|
log.dim(formatUpgradeHint('explorer'));
|
|
94
|
-
queue = queue.slice(0, me.limits.
|
|
107
|
+
queue = queue.slice(0, me.limits.authoring_max_skills);
|
|
95
108
|
}
|
|
96
109
|
const uploadSpinner = ora(`Uploading ${queue.length} skills as ${visibility}…`).start();
|
|
97
110
|
let ok = 0;
|
package/dist/commands/install.js
CHANGED
|
@@ -6,6 +6,7 @@ import ora from 'ora';
|
|
|
6
6
|
import { api, ApiError } from '../lib/api.js';
|
|
7
7
|
import { CONFIG } from '../lib/config.js';
|
|
8
8
|
import { loadCredentials, requireAuth } from '../lib/credentials.js';
|
|
9
|
+
import { fetchMyPlan, formatUpgradeHint } from '../lib/plan.js';
|
|
9
10
|
import { log } from '../utils/logger.js';
|
|
10
11
|
/**
|
|
11
12
|
* `prave install <slug>` — pulls SKILL.md to ~/.claude/skills/<slug>/.
|
|
@@ -25,6 +26,28 @@ export async function installCommand(slug, opts = {}) {
|
|
|
25
26
|
const session = await requireAuth('prave install');
|
|
26
27
|
if (!session)
|
|
27
28
|
return;
|
|
29
|
+
// Plan gate — Free is capped at 5 installs / month. We surface the
|
|
30
|
+
// remaining count proactively so the user knows where they stand
|
|
31
|
+
// before hitting the wall on `install N+1`.
|
|
32
|
+
const me = await fetchMyPlan();
|
|
33
|
+
if (me.limits.install_monthly_limit !== null) {
|
|
34
|
+
try {
|
|
35
|
+
const { data } = await api.get('/api/v1/me/usage-snapshot', true);
|
|
36
|
+
const remaining = data.installs.remaining;
|
|
37
|
+
if (remaining !== null && remaining <= 0) {
|
|
38
|
+
log.warn(`Your ${me.limits.label} plan caps installs at ${me.limits.install_monthly_limit}/month. You've used all of them.`);
|
|
39
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (remaining !== null && remaining <= 2) {
|
|
44
|
+
log.dim(`Heads up — ${remaining} install${remaining === 1 ? '' : 's'} left this month on the ${me.limits.label} plan.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* snapshot endpoint unavailable — server still gates */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
28
51
|
const spinner = ora(`Resolving ${slug}…`).start();
|
|
29
52
|
let installedSlugs = [];
|
|
30
53
|
try {
|
package/dist/commands/update.js
CHANGED
|
@@ -6,6 +6,7 @@ import ora from 'ora';
|
|
|
6
6
|
import { api } from '../lib/api.js';
|
|
7
7
|
import { CONFIG } from '../lib/config.js';
|
|
8
8
|
import { requireAuth } from '../lib/credentials.js';
|
|
9
|
+
import { fetchMyPlan, formatUpgradeHint } from '../lib/plan.js';
|
|
9
10
|
import { log } from '../utils/logger.js';
|
|
10
11
|
/**
|
|
11
12
|
* `prave update [<slug>]` — diff every CLI-installed Skill against its
|
|
@@ -24,6 +25,13 @@ export async function updateCommand(slug, opts = {}) {
|
|
|
24
25
|
const session = await requireAuth('prave update');
|
|
25
26
|
if (!session)
|
|
26
27
|
return;
|
|
28
|
+
const me = await fetchMyPlan();
|
|
29
|
+
if (!me.limits.can_cli_update) {
|
|
30
|
+
log.warn(`\`prave update\` requires the Pro plan or higher.`);
|
|
31
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
27
35
|
const spinner = ora(slug ? `Resolving ${slug}…` : 'Checking your installed skills…').start();
|
|
28
36
|
let installs;
|
|
29
37
|
try {
|
package/dist/commands/usage.js
CHANGED
|
@@ -104,43 +104,93 @@ export async function usageScanCommand(opts) {
|
|
|
104
104
|
/**
|
|
105
105
|
* `prave usage report` — invoked by the Claude Code `PostToolUse` hook.
|
|
106
106
|
* Reads the hook payload from stdin, extracts the Skill name, and fires
|
|
107
|
-
* a single-event POST
|
|
108
|
-
* to disturb the user's
|
|
107
|
+
* a single-event POST against the slug-keyed self-healing endpoint.
|
|
108
|
+
* Errors are silent (we never want a hook failure to disturb the user's
|
|
109
|
+
* workflow); set `PRAVE_DEBUG=1` to see what's happening in
|
|
110
|
+
* `~/.prave/usage.log`.
|
|
109
111
|
*/
|
|
110
112
|
export async function usageReportCommand() {
|
|
111
|
-
|
|
113
|
+
const debug = process.env.PRAVE_DEBUG === '1' || process.env.PRAVE_DEBUG === 'true';
|
|
112
114
|
const stdinPayload = await readStdin();
|
|
113
|
-
if (!stdinPayload)
|
|
115
|
+
if (!stdinPayload) {
|
|
116
|
+
if (debug)
|
|
117
|
+
await debugLog('no stdin payload');
|
|
114
118
|
return;
|
|
119
|
+
}
|
|
120
|
+
if (debug)
|
|
121
|
+
await debugLog(`stdin len=${stdinPayload.length}`);
|
|
122
|
+
// Claude Code's PostToolUse payload is documented as `{tool_name,
|
|
123
|
+
// tool_input, tool_response, ...}`, but we still defend against
|
|
124
|
+
// alternate shapes (capitalisation, future schema drift) so the hook
|
|
125
|
+
// doesn't silently break on a single field rename.
|
|
115
126
|
let parsed = {};
|
|
116
127
|
try {
|
|
117
128
|
parsed = JSON.parse(stdinPayload);
|
|
118
129
|
}
|
|
119
130
|
catch {
|
|
131
|
+
if (debug)
|
|
132
|
+
await debugLog('payload is not valid JSON');
|
|
120
133
|
return;
|
|
121
134
|
}
|
|
122
|
-
const rawSlug = parsed
|
|
123
|
-
if (
|
|
135
|
+
const rawSlug = extractSkillSlug(parsed);
|
|
136
|
+
if (!rawSlug) {
|
|
137
|
+
if (debug)
|
|
138
|
+
await debugLog(`no skill slug in payload: ${stdinPayload.slice(0, 200)}`);
|
|
124
139
|
return;
|
|
125
|
-
|
|
140
|
+
}
|
|
141
|
+
const slug = rawSlug.toLowerCase().split(':').pop()?.trim().replace(/[^a-z0-9_-]+/g, '-');
|
|
126
142
|
if (!slug)
|
|
127
143
|
return;
|
|
128
|
-
|
|
129
|
-
|
|
144
|
+
if (debug)
|
|
145
|
+
await debugLog(`slug=${slug}`);
|
|
130
146
|
const session = await requireAuthSilent();
|
|
131
|
-
if (!session)
|
|
147
|
+
if (!session) {
|
|
148
|
+
if (debug)
|
|
149
|
+
await debugLog('no auth — skipping');
|
|
132
150
|
return;
|
|
151
|
+
}
|
|
133
152
|
try {
|
|
134
|
-
const { data
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return;
|
|
138
|
-
await api.post('/api/v1/intelligence/usage', { skill_metadata_id: id, trigger_phrase: null }, true);
|
|
153
|
+
const { data } = await api.post('/api/v1/intelligence/usage/by-slug', { slug, agent_type: 'claude', triggered_at: new Date().toISOString() }, true);
|
|
154
|
+
if (debug)
|
|
155
|
+
await debugLog(`ok recorded=${data.recorded} stub=${data.created_stub}`);
|
|
139
156
|
}
|
|
140
|
-
catch {
|
|
157
|
+
catch (err) {
|
|
158
|
+
if (debug)
|
|
159
|
+
await debugLog(`error: ${err.message}`);
|
|
141
160
|
/* silent — never break the host shell */
|
|
142
161
|
}
|
|
143
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Walk the hook payload looking for the invoked Skill's slug. Tries the
|
|
165
|
+
* documented Claude Code path first, then several plausible aliases so
|
|
166
|
+
* we don't break if the schema gains a new wrapper or rename.
|
|
167
|
+
*/
|
|
168
|
+
function extractSkillSlug(payload) {
|
|
169
|
+
const candidates = [
|
|
170
|
+
payload.tool_input?.skill,
|
|
171
|
+
payload.toolInput?.skill,
|
|
172
|
+
payload.input?.skill,
|
|
173
|
+
payload.parameters?.skill,
|
|
174
|
+
payload.skill,
|
|
175
|
+
];
|
|
176
|
+
for (const c of candidates) {
|
|
177
|
+
if (typeof c === 'string' && c.trim())
|
|
178
|
+
return c;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
async function debugLog(line) {
|
|
183
|
+
try {
|
|
184
|
+
const { mkdir, appendFile } = await import('node:fs/promises');
|
|
185
|
+
const { join } = await import('node:path');
|
|
186
|
+
await mkdir(CONFIG.praveDir, { recursive: true });
|
|
187
|
+
const stamp = new Date().toISOString();
|
|
188
|
+
await appendFile(join(CONFIG.praveDir, 'usage.log'), `${stamp} ${line}\n`);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* swallow — debug is best-effort */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
144
194
|
export async function usageHookInstallCommand() {
|
|
145
195
|
const session = await requireAuth('prave usage hook install');
|
|
146
196
|
if (!session)
|
|
@@ -168,6 +218,83 @@ export async function usageHookUninstallCommand() {
|
|
|
168
218
|
const results = await uninstallHooksForAgents(agents);
|
|
169
219
|
printAgentResults(results, 'uninstall');
|
|
170
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* `prave usage status` — diagnostic. Reports hook health, recent event
|
|
223
|
+
* counts, and a tail of the debug log so users can troubleshoot why
|
|
224
|
+
* "unused for 30+ days" still shows up after running a Skill.
|
|
225
|
+
*/
|
|
226
|
+
export async function usageStatusCommand() {
|
|
227
|
+
const session = await requireAuth('prave usage status');
|
|
228
|
+
if (!session)
|
|
229
|
+
return;
|
|
230
|
+
const { existsSync } = await import('node:fs');
|
|
231
|
+
const { readFile } = await import('node:fs/promises');
|
|
232
|
+
const { join } = await import('node:path');
|
|
233
|
+
const { homedir } = await import('node:os');
|
|
234
|
+
// 1. Hook installed?
|
|
235
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
236
|
+
let hookInstalled = false;
|
|
237
|
+
try {
|
|
238
|
+
const raw = await readFile(settingsPath, 'utf8');
|
|
239
|
+
hookInstalled = raw.includes('__prave_managed') && raw.includes('Skill');
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
/* no settings.json yet */
|
|
243
|
+
}
|
|
244
|
+
// 2. Last 7 days of recorded events from the API.
|
|
245
|
+
let recent7 = 0;
|
|
246
|
+
let topSlugs = [];
|
|
247
|
+
try {
|
|
248
|
+
const { data } = await api.get('/api/v1/intelligence/usage/recent?days=7', true);
|
|
249
|
+
recent7 = data.events?.length ?? 0;
|
|
250
|
+
topSlugs = data.by_skill ?? [];
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
/* endpoint optional — older servers may not have it */
|
|
254
|
+
}
|
|
255
|
+
// 3. Cursor watermark — when did the scanner last run?
|
|
256
|
+
const cursorPath = join(CONFIG.praveDir, 'usage-cursor.json');
|
|
257
|
+
let lastScanAt = null;
|
|
258
|
+
try {
|
|
259
|
+
const raw = await readFile(cursorPath, 'utf8');
|
|
260
|
+
lastScanAt = JSON.parse(raw).lastScanAt ?? null;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
/* never scanned */
|
|
264
|
+
}
|
|
265
|
+
// 4. Debug log tail.
|
|
266
|
+
const logPath = join(CONFIG.praveDir, 'usage.log');
|
|
267
|
+
const debugAvailable = existsSync(logPath);
|
|
268
|
+
// Render.
|
|
269
|
+
const checkmark = (ok) => (ok ? chalk.green('✓') : chalk.dim('—'));
|
|
270
|
+
log.info(chalk.bold('Usage tracking status'));
|
|
271
|
+
console.log();
|
|
272
|
+
console.log(` ${checkmark(hookInstalled)} Real-time hook (Claude Code): ${hookInstalled ? chalk.green('installed') : chalk.yellow('not installed — `prave usage hook install`')}`);
|
|
273
|
+
console.log(` ${checkmark(Boolean(lastScanAt))} Transcript scanner watermark: ${lastScanAt ?? chalk.dim('never run — `prave sync` includes it')}`);
|
|
274
|
+
console.log(` ${checkmark(recent7 > 0)} Events in last 7 days: ${chalk.cyan(String(recent7))}`);
|
|
275
|
+
if (topSlugs.length) {
|
|
276
|
+
console.log();
|
|
277
|
+
console.log(chalk.dim(' Most active in last 7 days:'));
|
|
278
|
+
for (const s of topSlugs.slice(0, 5)) {
|
|
279
|
+
console.log(` ${chalk.cyan(s.count.toString().padStart(4))} ${s.name}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
console.log();
|
|
283
|
+
if (debugAvailable) {
|
|
284
|
+
log.dim(`Debug log: ${logPath}`);
|
|
285
|
+
log.dim('Set PRAVE_DEBUG=1 in your shell to enable verbose hook logging.');
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
log.dim('No debug log yet. Set PRAVE_DEBUG=1 to capture every hook fire to ~/.prave/usage.log');
|
|
289
|
+
}
|
|
290
|
+
if (recent7 === 0) {
|
|
291
|
+
console.log();
|
|
292
|
+
log.warn('No events recorded yet. Quick checks:');
|
|
293
|
+
log.dim(' 1. Is the hook installed? See checkmarks above.');
|
|
294
|
+
log.dim(' 2. Run a Skill in Claude Code, then `tail ~/.prave/usage.log` (with PRAVE_DEBUG=1)');
|
|
295
|
+
log.dim(' 3. Or run `prave usage scan --since 7d` to backfill from transcripts.');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
171
298
|
async function fetchEnabledAgents() {
|
|
172
299
|
try {
|
|
173
300
|
const { data } = await api.get('/api/v1/settings/agents', true);
|
package/dist/index.js
CHANGED
|
@@ -20,18 +20,42 @@ import { settingsCommand } from './commands/settings.js';
|
|
|
20
20
|
import { syncCommand } from './commands/sync.js';
|
|
21
21
|
import { uninstallCommand } from './commands/uninstall.js';
|
|
22
22
|
import { updateCommand } from './commands/update.js';
|
|
23
|
-
import { usageHookInstallCommand, usageHookUninstallCommand, usageReportCommand, usageScanCommand, } from './commands/usage.js';
|
|
23
|
+
import { usageHookInstallCommand, usageHookUninstallCommand, usageReportCommand, usageScanCommand, usageStatusCommand, } from './commands/usage.js';
|
|
24
24
|
import { whatdoesCommand } from './commands/whatdoes.js';
|
|
25
25
|
import { whoamiCommand } from './commands/whoami.js';
|
|
26
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
27
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
28
28
|
const program = new Command()
|
|
29
29
|
.name('prave')
|
|
30
|
-
.description(
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
.description([
|
|
31
|
+
'Prave — Developer platform for Claude Skills.',
|
|
32
|
+
'',
|
|
33
|
+
' Lifecycle: discover · install · author · sync · audit · ship.',
|
|
34
|
+
' Targets ~/.claude/skills/ by default; multi-agent via `prave deploy`.',
|
|
35
|
+
'',
|
|
36
|
+
' Quick start:',
|
|
37
|
+
' prave login # device-code auth in your browser',
|
|
38
|
+
' prave search <q> # discover community skills',
|
|
39
|
+
' prave install <slug> # pull into ~/.claude/skills/',
|
|
40
|
+
' prave usage hook install # real-time invocation tracking',
|
|
41
|
+
'',
|
|
42
|
+
' Docs: https://prave.app/docs',
|
|
43
|
+
' Issues: https://github.com/eppstudio/prave/issues',
|
|
44
|
+
].join('\n'))
|
|
45
|
+
.version(pkg.version)
|
|
46
|
+
.addHelpText('after', '\nRun `prave <cmd> --help` for command-specific options.');
|
|
47
|
+
program
|
|
48
|
+
.command('login')
|
|
49
|
+
.description('Authenticate this machine via device-code (browser opens, no password is ever typed). Tokens are saved chmod-600 to ~/.prave/credentials.json and refresh automatically.')
|
|
50
|
+
.action(loginCommand);
|
|
51
|
+
program
|
|
52
|
+
.command('logout')
|
|
53
|
+
.description('Remove stored credentials from ~/.prave/credentials.json. Re-run `prave login` to authenticate again.')
|
|
54
|
+
.action(logoutCommand);
|
|
55
|
+
program
|
|
56
|
+
.command('whoami')
|
|
57
|
+
.description('Print the signed-in user, plan, and credentials path. Detects stale credentials predating the refresh-token flow.')
|
|
58
|
+
.action(whoamiCommand);
|
|
35
59
|
program
|
|
36
60
|
.command('import')
|
|
37
61
|
.description('Scan ~/.claude/skills/ — optionally upload to Prave')
|
|
@@ -41,9 +65,9 @@ program
|
|
|
41
65
|
.action(importCommand);
|
|
42
66
|
program
|
|
43
67
|
.command('install <slug>')
|
|
44
|
-
.description('Install a Skill into ~/.claude/skills/
|
|
45
|
-
.option('--no-deps', 'skip transitive dependency resolution')
|
|
46
|
-
.option('--force', 'install even
|
|
68
|
+
.description('Install a Skill from the registry into ~/.claude/skills/<slug>/. Resolves and installs transitive dependencies by default. Free plan caps at 5 installs/month — Pro and Max are unlimited.')
|
|
69
|
+
.option('--no-deps', 'skip transitive dependency resolution (don\'t pull deps the Skill declares)')
|
|
70
|
+
.option('--force', 'install even when the SKILL.md content is empty (rare; use for stub Skills)')
|
|
47
71
|
.action(installCommand);
|
|
48
72
|
program
|
|
49
73
|
.command('uninstall <slug>')
|
|
@@ -61,11 +85,11 @@ program
|
|
|
61
85
|
.action((slug, opts) => updateCommand(slug, opts));
|
|
62
86
|
program
|
|
63
87
|
.command('list')
|
|
64
|
-
.description('List installed
|
|
65
|
-
.option('--remote', '
|
|
66
|
-
.option('--verbose', 'show enriched intelligence
|
|
67
|
-
.option('--conflicts', '
|
|
68
|
-
.option('--heavy', '
|
|
88
|
+
.description('List Skills installed locally (default) or stored on your Prave account (--remote).')
|
|
89
|
+
.option('--remote', 'fetch Skills you own + have installed from the Prave API instead of disk')
|
|
90
|
+
.option('--verbose', 'show enriched intelligence: estimated tokens, token tier, target agents')
|
|
91
|
+
.option('--conflicts', 'narrow to Skills flagged as conflicting with another by trigger overlap')
|
|
92
|
+
.option('--heavy', 'narrow to Skills above the heavy-tier threshold (>5k estimated tokens)')
|
|
69
93
|
.action(listCommand);
|
|
70
94
|
program.command('search <query>').description('Search public Skills').action(searchCommand);
|
|
71
95
|
program
|
|
@@ -110,6 +134,10 @@ usage
|
|
|
110
134
|
.option('--since <window>', 'override the watermark, e.g. "7d" or "12h"')
|
|
111
135
|
.option('--quiet', 'log a one-liner instead of a spinner')
|
|
112
136
|
.action(usageScanCommand);
|
|
137
|
+
usage
|
|
138
|
+
.command('status')
|
|
139
|
+
.description('Diagnose hook health, recent event counts, and most-used Skills')
|
|
140
|
+
.action(usageStatusCommand);
|
|
113
141
|
usage
|
|
114
142
|
.command('report')
|
|
115
143
|
.description('Internal: invoked by the Claude Code PostToolUse hook (reads stdin)')
|
|
@@ -132,10 +160,56 @@ program
|
|
|
132
160
|
.action(findCommand);
|
|
133
161
|
program
|
|
134
162
|
.command('deploy <skillname>')
|
|
135
|
-
.description('Deploy a
|
|
136
|
-
.option('--agent <agent>', '
|
|
137
|
-
.option('--dry-run', 'log
|
|
163
|
+
.description('Deploy a Skill to every configured agent (Claude Code, Codex, Cursor, Gemini, Cline, Amp). Free plan deploys to Claude Code only; Pro and Max deploy across all six. The skill must already exist locally — use `prave install` first or point to a SKILL.md folder.')
|
|
164
|
+
.option('--agent <agent>', 'restrict deploy to a single agent (claude-code | codex | cursor | gemini | cline | amp)')
|
|
165
|
+
.option('--dry-run', 'log every destination path but write nothing — preview before committing')
|
|
138
166
|
.action(deployCommand);
|
|
167
|
+
// ─── Help command — full quick-reference ───────────────────────────
|
|
168
|
+
program
|
|
169
|
+
.command('cheat')
|
|
170
|
+
.description('Print a one-page cheat sheet of every command.')
|
|
171
|
+
.action(() => {
|
|
172
|
+
console.log([
|
|
173
|
+
'Prave CLI cheat sheet',
|
|
174
|
+
'',
|
|
175
|
+
'Auth & account',
|
|
176
|
+
' prave login # device-code browser auth',
|
|
177
|
+
' prave logout # forget credentials',
|
|
178
|
+
' prave whoami # signed-in user + plan',
|
|
179
|
+
'',
|
|
180
|
+
'Discover & install',
|
|
181
|
+
' prave search <q> # public skill search',
|
|
182
|
+
' prave find <q> [--smart|--local] # smart cross-source search',
|
|
183
|
+
' prave install <slug> [--no-deps] # install into ~/.claude/skills/',
|
|
184
|
+
' prave uninstall <slug> # remove a local skill',
|
|
185
|
+
' prave list [--remote] [--verbose] # what is installed (or remote)',
|
|
186
|
+
' prave export <slug> -o file.md # dump SKILL.md without installing',
|
|
187
|
+
'',
|
|
188
|
+
'Authoring & sync',
|
|
189
|
+
' prave import [--upload --public] # publish ~/.claude/skills/ to Prave',
|
|
190
|
+
' prave deploy <skill> [--agent x] # mirror to other agents',
|
|
191
|
+
' prave sync # pull every installed update',
|
|
192
|
+
' prave update [slug] [--dry-run] # diff + pull outdated skills',
|
|
193
|
+
' prave diff <slug> # local vs registry diff',
|
|
194
|
+
'',
|
|
195
|
+
'Intelligence',
|
|
196
|
+
' prave overview [--json] # token cost summary',
|
|
197
|
+
' prave whatdoes <skill> # triggers, tokens, conflicts',
|
|
198
|
+
' prave conflicts # cross-skill collision check',
|
|
199
|
+
' prave optimize # heavy / underused / mergeable',
|
|
200
|
+
'',
|
|
201
|
+
'Usage tracking (Pro+)',
|
|
202
|
+
' prave usage hook install # real-time PostToolUse hook',
|
|
203
|
+
' prave usage hook uninstall',
|
|
204
|
+
' prave usage scan [--since 7d] # transcript scanner',
|
|
205
|
+
' prave usage status # hook health + recent counts',
|
|
206
|
+
'',
|
|
207
|
+
'Settings',
|
|
208
|
+
' prave settings # configure agents + paths',
|
|
209
|
+
'',
|
|
210
|
+
'Docs: https://prave.app/docs',
|
|
211
|
+
].join('\n'));
|
|
212
|
+
});
|
|
139
213
|
program.parseAsync().catch((err) => {
|
|
140
214
|
console.error(err.message);
|
|
141
215
|
process.exit(1);
|
package/dist/lib/plan.js
CHANGED
|
@@ -26,6 +26,14 @@ export const fetchMyPlan = async () => {
|
|
|
26
26
|
return fallback;
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Stable copy block surfaced by every plan-gated CLI command. Built from
|
|
31
|
+
* `PLAN_LIMITS` so prices/labels stay consistent with the SaaS pricing
|
|
32
|
+
* card automatically.
|
|
33
|
+
*/
|
|
34
|
+
export const formatUpgradeHint = (required) => {
|
|
35
|
+
const limits = PLAN_LIMITS[required];
|
|
36
|
+
if (required === 'free')
|
|
37
|
+
return 'You\'re already on the Free plan.';
|
|
38
|
+
return `Upgrade to ${limits.label} ($${limits.price_usd_monthly}/mo or $${limits.price_usd_yearly}/yr) at https://prave.app/dashboard/settings/billing`;
|
|
39
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prave/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Prave CLI — import, export, install, sync Claude Skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"open": "^10.1.0",
|
|
17
17
|
"ora": "^8.0.1",
|
|
18
18
|
"undici": "^6.18.0",
|
|
19
|
-
"@prave/shared": "1.0.
|
|
19
|
+
"@prave/shared": "1.0.3"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^20.12.7",
|