@prave/cli 1.0.0 → 1.0.2
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/commands/whoami.js +11 -0
- package/dist/index.js +5 -1
- package/dist/lib/plan.js +11 -3
- package/dist/lib/prompt.js +92 -35
- 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/commands/whoami.js
CHANGED
|
@@ -17,6 +17,17 @@ export async function whoamiCommand() {
|
|
|
17
17
|
process.exitCode = 1;
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
+
// Detect creds left over from older CLI versions that predate the
|
|
21
|
+
// refresh-token flow. Without a refresh_token we can't auto-rotate the
|
|
22
|
+
// expired access_token, so the user gets stuck in a "session expired"
|
|
23
|
+
// loop. Direct them to `prave login` immediately rather than letting
|
|
24
|
+
// the API call fail confusingly.
|
|
25
|
+
if (!creds.refresh_token) {
|
|
26
|
+
log.warn('Stored credentials predate the refresh-token flow.');
|
|
27
|
+
log.dim('Run `prave login` once to upgrade — refreshes will be automatic afterwards.');
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
20
31
|
try {
|
|
21
32
|
const { data } = await api.get('/api/v1/me', true);
|
|
22
33
|
const handle = data.username || data.display_name || data.email || creds.email || 'unknown';
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ 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));
|
|
@@ -110,6 +110,10 @@ usage
|
|
|
110
110
|
.option('--since <window>', 'override the watermark, e.g. "7d" or "12h"')
|
|
111
111
|
.option('--quiet', 'log a one-liner instead of a spinner')
|
|
112
112
|
.action(usageScanCommand);
|
|
113
|
+
usage
|
|
114
|
+
.command('status')
|
|
115
|
+
.description('Diagnose hook health, recent event counts, and most-used Skills')
|
|
116
|
+
.action(usageStatusCommand);
|
|
113
117
|
usage
|
|
114
118
|
.command('report')
|
|
115
119
|
.description('Internal: invoked by the Claude Code PostToolUse hook (reads stdin)')
|
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/dist/lib/prompt.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { stdin, stdout } from 'node:process';
|
|
2
|
-
import readline from 'node:readline';
|
|
3
2
|
import chalk from 'chalk';
|
|
4
3
|
export async function checkboxPrompt(question, items, opts = {}) {
|
|
5
4
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
@@ -8,15 +7,17 @@ export async function checkboxPrompt(question, items, opts = {}) {
|
|
|
8
7
|
const selected = new Set(opts.initial ?? []);
|
|
9
8
|
let cursor = 0;
|
|
10
9
|
const minSelected = opts.minSelected ?? 0;
|
|
10
|
+
let dataHandler = null;
|
|
11
11
|
const cleanup = () => {
|
|
12
12
|
if (typeof stdin.setRawMode === 'function')
|
|
13
13
|
stdin.setRawMode(false);
|
|
14
|
-
|
|
14
|
+
if (dataHandler)
|
|
15
|
+
stdin.off('data', dataHandler);
|
|
15
16
|
stdin.pause();
|
|
16
17
|
stdout.write(showCursor);
|
|
18
|
+
stdout.write('\n');
|
|
17
19
|
};
|
|
18
20
|
return new Promise((resolve) => {
|
|
19
|
-
readline.emitKeypressEvents(stdin);
|
|
20
21
|
if (typeof stdin.setRawMode === 'function')
|
|
21
22
|
stdin.setRawMode(true);
|
|
22
23
|
stdin.resume();
|
|
@@ -45,53 +46,109 @@ export async function checkboxPrompt(question, items, opts = {}) {
|
|
|
45
46
|
? `${selected.size} selected · enter to confirm`
|
|
46
47
|
: `at least ${minSelected} required · ${selected.size} selected`)}\n`);
|
|
47
48
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Raw-byte parser. We listen to `data` directly instead of relying on
|
|
51
|
+
* `readline.emitKeypressEvents`, which is unreliable on macOS Terminal +
|
|
52
|
+
* iTerm under raw mode (the keypress event simply never fires for some
|
|
53
|
+
* users, leaving them stuck on whatever items the prompt prefilled).
|
|
54
|
+
*
|
|
55
|
+
* Sequences we care about:
|
|
56
|
+
* ESC [ A/B/C/D → arrow keys
|
|
57
|
+
* 0x0D / 0x0A → return
|
|
58
|
+
* 0x20 → space
|
|
59
|
+
* 0x03 → ctrl-c
|
|
60
|
+
* 0x1B (alone) → escape (heuristic: solo 0x1B with no follow-up)
|
|
61
|
+
* "k" / "j" / "a" → vim-style move + toggle-all
|
|
62
|
+
* "q" → cancel
|
|
63
|
+
*/
|
|
64
|
+
const handle = (data) => {
|
|
65
|
+
// An entire keystroke can arrive as one chunk on most terminals, but
|
|
66
|
+
// we guard against multi-byte chunks by walking the buffer.
|
|
67
|
+
let i = 0;
|
|
68
|
+
while (i < data.length) {
|
|
69
|
+
const b = data[i];
|
|
70
|
+
// CSI escape sequences: ESC [ X
|
|
71
|
+
if (b === 0x1b && data[i + 1] === 0x5b && data.length > i + 2) {
|
|
72
|
+
const arrow = data[i + 2];
|
|
73
|
+
if (arrow === 0x41)
|
|
74
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
75
|
+
else if (arrow === 0x42)
|
|
76
|
+
cursor = (cursor + 1) % items.length;
|
|
77
|
+
else if (arrow === 0x43) {
|
|
78
|
+
// right arrow → toggle (some users default to this)
|
|
79
|
+
const v = items[cursor].value;
|
|
80
|
+
if (selected.has(v))
|
|
81
|
+
selected.delete(v);
|
|
82
|
+
else
|
|
83
|
+
selected.add(v);
|
|
84
|
+
}
|
|
85
|
+
i += 3;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Bare ESC = cancel.
|
|
89
|
+
if (b === 0x1b && (data.length === i + 1 || data[i + 1] === undefined)) {
|
|
90
|
+
cleanup();
|
|
91
|
+
resolve(null);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Ctrl-C.
|
|
95
|
+
if (b === 0x03) {
|
|
96
|
+
cleanup();
|
|
97
|
+
resolve(null);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Enter — CR or LF.
|
|
101
|
+
if (b === 0x0d || b === 0x0a) {
|
|
102
|
+
if (selected.size < minSelected) {
|
|
103
|
+
i += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
cleanup();
|
|
107
|
+
resolve([...selected]);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Space — toggle current.
|
|
111
|
+
if (b === 0x20) {
|
|
66
112
|
const v = items[cursor].value;
|
|
67
113
|
if (selected.has(v))
|
|
68
114
|
selected.delete(v);
|
|
69
115
|
else
|
|
70
116
|
selected.add(v);
|
|
71
|
-
|
|
117
|
+
i += 1;
|
|
118
|
+
continue;
|
|
72
119
|
}
|
|
73
|
-
|
|
120
|
+
// Letters: a (toggle all), j (down), k (up), q (cancel).
|
|
121
|
+
if (b === 0x61) {
|
|
74
122
|
if (selected.size === items.length)
|
|
75
123
|
selected.clear();
|
|
76
124
|
else
|
|
77
|
-
for (const
|
|
78
|
-
selected.add(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
125
|
+
for (const it of items)
|
|
126
|
+
selected.add(it.value);
|
|
127
|
+
i += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (b === 0x6a) {
|
|
131
|
+
cursor = (cursor + 1) % items.length;
|
|
132
|
+
i += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (b === 0x6b) {
|
|
136
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
137
|
+
i += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (b === 0x71) {
|
|
88
141
|
cleanup();
|
|
89
142
|
resolve(null);
|
|
90
143
|
return;
|
|
144
|
+
}
|
|
145
|
+
// Unknown byte — advance and ignore.
|
|
146
|
+
i += 1;
|
|
91
147
|
}
|
|
92
148
|
render();
|
|
93
149
|
};
|
|
94
|
-
|
|
150
|
+
dataHandler = handle;
|
|
151
|
+
stdin.on('data', handle);
|
|
95
152
|
render();
|
|
96
153
|
});
|
|
97
154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prave/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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.2"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^20.12.7",
|