@prave/cli 1.6.0 → 1.6.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/advisor.js +86 -0
- package/dist/commands/mcp-server.js +51 -0
- package/dist/commands/usage.js +24 -3
- package/dist/index.js +7 -0
- package/dist/lib/hook.js +7 -2
- package/dist/lib/usage-scanner.js +31 -5
- package/package.json +2 -2
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { track } from '../lib/analytics.js';
|
|
4
|
+
import { api, ApiError } from '../lib/api.js';
|
|
5
|
+
import { CONFIG } from '../lib/config.js';
|
|
6
|
+
import { requireAuth } from '../lib/credentials.js';
|
|
7
|
+
import { log } from '../utils/logger.js';
|
|
8
|
+
const RULE = '─────────────────────────────────────────';
|
|
9
|
+
export async function advisorCommand(task, opts = {}) {
|
|
10
|
+
const mode = opts.auto ? 'auto' : 'manual';
|
|
11
|
+
track('cli_advisor', { auto_mode: opts.auto === true, task_length: task?.length ?? 0 });
|
|
12
|
+
if (mode === 'manual') {
|
|
13
|
+
if (!task || task.trim().length < 8) {
|
|
14
|
+
log.error('Pass a task description of at least 8 characters, or use --auto.');
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
await requireAuth('advisor');
|
|
20
|
+
const spinner = ora(mode === 'auto' ? 'Thinking…' : `Thinking about "${truncate(task, 60)}"…`).start();
|
|
21
|
+
try {
|
|
22
|
+
const { data } = await api.post('/api/v1/advisor', { mode, task: mode === 'manual' ? task : undefined }, true);
|
|
23
|
+
spinner.stop();
|
|
24
|
+
console.log(chalk.dim(RULE));
|
|
25
|
+
const hasReasoned = data.recommendations.length > 0;
|
|
26
|
+
console.log(chalk.bold(hasReasoned ? 'Recommended for you' : 'Starting points'));
|
|
27
|
+
console.log(chalk.dim(RULE));
|
|
28
|
+
console.log(wrap(data.prose, 78));
|
|
29
|
+
console.log();
|
|
30
|
+
if (hasReasoned) {
|
|
31
|
+
for (const rec of data.recommendations) {
|
|
32
|
+
printSkill(rec.slug, rec.reason);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (data.candidate_slugs.length > 0) {
|
|
36
|
+
for (const slug of data.candidate_slugs) {
|
|
37
|
+
printSkill(slug);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
log.dim('No specific Skills to recommend yet.');
|
|
42
|
+
}
|
|
43
|
+
console.log(chalk.dim(`Quota: ${data.quota.used} / ${data.quota.limit} today on the ${data.quota.plan} plan.`));
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
spinner.stop();
|
|
47
|
+
log.error(err instanceof ApiError ? err.message : err.message);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Print one Skill row — bullet · slug · optional Why · install command
|
|
53
|
+
* · clickable prave.app link. The link is what lets the user verify
|
|
54
|
+
* the Skill on the web before pulling it into their library.
|
|
55
|
+
*/
|
|
56
|
+
function printSkill(slug, reason) {
|
|
57
|
+
console.log(` ${chalk.cyan('▸')} ${chalk.bold(slug)}`);
|
|
58
|
+
if (reason) {
|
|
59
|
+
console.log(` ${chalk.dim('Why:')} ${wrap(reason, 70).split('\n').join('\n ')}`);
|
|
60
|
+
}
|
|
61
|
+
console.log(` ${chalk.dim(`prave install ${slug}`)}`);
|
|
62
|
+
console.log(` ${chalk.dim(`${CONFIG.webUrl}/skills/${slug}`)}`);
|
|
63
|
+
console.log();
|
|
64
|
+
}
|
|
65
|
+
function truncate(s, n) {
|
|
66
|
+
return s.length <= n ? s : `${s.slice(0, n - 1)}…`;
|
|
67
|
+
}
|
|
68
|
+
function wrap(text, width) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const para of text.split('\n')) {
|
|
71
|
+
const words = para.trim().split(/\s+/);
|
|
72
|
+
let line = '';
|
|
73
|
+
for (const w of words) {
|
|
74
|
+
if ((line + ' ' + w).trim().length > width) {
|
|
75
|
+
out.push(line.trim());
|
|
76
|
+
line = w;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
line = `${line} ${w}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (line.trim())
|
|
83
|
+
out.push(line.trim());
|
|
84
|
+
}
|
|
85
|
+
return out.join('\n');
|
|
86
|
+
}
|
|
@@ -4,6 +4,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
5
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
6
6
|
import { api, ApiError } from '../lib/api.js';
|
|
7
|
+
import { CONFIG } from '../lib/config.js';
|
|
7
8
|
import { loadCredentials } from '../lib/credentials.js';
|
|
8
9
|
const TOOLS = [
|
|
9
10
|
{
|
|
@@ -62,6 +63,23 @@ const TOOLS = [
|
|
|
62
63
|
required: ['slug'],
|
|
63
64
|
},
|
|
64
65
|
},
|
|
66
|
+
{
|
|
67
|
+
name: 'prave_advisor',
|
|
68
|
+
description: "Contextual Skill advisor. Given a user task (e.g. 'I'm building a Next.js app with Stripe and auth'), returns a prose recommendation explaining which Skills genuinely fit and why. Tiered freemium quota — Free 3/day, Pro 10/day, Max 30/day — enforced server-side. Use this when the user asks 'what Skills should I install for X?' or 'help me pick Skills for my project'.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
task: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: "The user's task / context. Min 8 chars. Omit to run in auto mode.",
|
|
75
|
+
},
|
|
76
|
+
auto: {
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
description: "When true, ignore `task` and synthesise context from the user's installed Skills + recent usage.",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
65
83
|
];
|
|
66
84
|
export async function mcpServerCommand() {
|
|
67
85
|
// Auth check up-front. If the user runs `prave mcp-server` without
|
|
@@ -96,6 +114,8 @@ export async function mcpServerCommand() {
|
|
|
96
114
|
return await handleAuditLibrary();
|
|
97
115
|
case 'prave_install_skill':
|
|
98
116
|
return await handleInstallSkill(args);
|
|
117
|
+
case 'prave_advisor':
|
|
118
|
+
return await handleAdvisor(args);
|
|
99
119
|
default:
|
|
100
120
|
return mcpError(`Unknown tool: ${name}`);
|
|
101
121
|
}
|
|
@@ -202,6 +222,37 @@ async function handleInstallSkill(args) {
|
|
|
202
222
|
});
|
|
203
223
|
});
|
|
204
224
|
}
|
|
225
|
+
async function handleAdvisor(args) {
|
|
226
|
+
const mode = args.auto ? 'auto' : 'manual';
|
|
227
|
+
if (mode === 'manual' && (!args.task || args.task.trim().length < 8)) {
|
|
228
|
+
return mcpError("Pass a `task` of at least 8 characters describing what the user is building, or set `auto: true` to use their installed Skills as context.");
|
|
229
|
+
}
|
|
230
|
+
const { data } = await api.post('/api/v1/advisor', { mode, task: mode === 'manual' ? args.task : undefined }, true);
|
|
231
|
+
const lines = [data.prose, ''];
|
|
232
|
+
const hasReasoned = data.recommendations.length > 0;
|
|
233
|
+
if (hasReasoned) {
|
|
234
|
+
for (const rec of data.recommendations) {
|
|
235
|
+
lines.push(`• ${rec.slug}`);
|
|
236
|
+
lines.push(` Why: ${rec.reason}`);
|
|
237
|
+
lines.push(` Install: prave install ${rec.slug}`);
|
|
238
|
+
lines.push(` View: ${CONFIG.webUrl}/skills/${rec.slug}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else if (data.candidate_slugs.length > 0) {
|
|
242
|
+
lines.push('Starting points (no reasoned picks — browse these on prave.app):');
|
|
243
|
+
for (const slug of data.candidate_slugs) {
|
|
244
|
+
lines.push(`• ${slug}`);
|
|
245
|
+
lines.push(` Install: prave install ${slug}`);
|
|
246
|
+
lines.push(` View: ${CONFIG.webUrl}/skills/${slug}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
lines.push('(No specific Skills recommended for this context.)');
|
|
251
|
+
}
|
|
252
|
+
lines.push('');
|
|
253
|
+
lines.push(`Advisor quota: ${data.quota.used} / ${data.quota.limit} today on the ${data.quota.plan} plan.`);
|
|
254
|
+
return mcpText(lines.join('\n'));
|
|
255
|
+
}
|
|
205
256
|
/* ─── helpers ──────────────────────────────────────────────────── */
|
|
206
257
|
function mcpText(text) {
|
|
207
258
|
return { content: [{ type: 'text', text }] };
|
package/dist/commands/usage.js
CHANGED
|
@@ -118,9 +118,30 @@ export async function usageScanCommand(opts) {
|
|
|
118
118
|
* - one POST attempt with a hard 4 s deadline (background-friendly)
|
|
119
119
|
* - on auth/network failure we log + bail; never throw
|
|
120
120
|
*/
|
|
121
|
+
/**
|
|
122
|
+
* Telemetry hook reporter. Reads the agent's hook payload from stdin,
|
|
123
|
+
* extracts the slug, and posts to /api/v1/intelligence/usage/by-slug.
|
|
124
|
+
*
|
|
125
|
+
* Forward-compat: `--agent` is parameterised so when Codex/Gemini/
|
|
126
|
+
* Cursor/Continue/Cline/Amp ship their own lifecycle-hook contracts,
|
|
127
|
+
* the install code only needs to write a different value into the
|
|
128
|
+
* generated hook command. The reporter itself stays the same.
|
|
129
|
+
*
|
|
130
|
+
* Today only `claude` produces hook payloads; the API endpoint accepts
|
|
131
|
+
* the full enum already (intelligence.routes.ts), so this CLI change
|
|
132
|
+
* unlocks the path with zero server work.
|
|
133
|
+
*/
|
|
134
|
+
const TRACKED_AGENTS = ['claude', 'codex', 'cursor', 'gemini', 'cline', 'amp'];
|
|
135
|
+
function normaliseAgent(input) {
|
|
136
|
+
const lower = (input ?? '').toLowerCase();
|
|
137
|
+
return TRACKED_AGENTS.includes(lower)
|
|
138
|
+
? lower
|
|
139
|
+
: 'claude';
|
|
140
|
+
}
|
|
121
141
|
export async function usageReportCommand(opts = {}) {
|
|
122
142
|
const debug = process.env.PRAVE_DEBUG === '1' || process.env.PRAVE_DEBUG === 'true';
|
|
123
143
|
const source = opts.source === 'prompt' ? 'prompt' : 'tool';
|
|
144
|
+
const agent_type = normaliseAgent(opts.agent);
|
|
124
145
|
await rotateHookLog();
|
|
125
146
|
const stdinPayload = await readStdin();
|
|
126
147
|
if (!stdinPayload) {
|
|
@@ -197,7 +218,7 @@ export async function usageReportCommand(opts = {}) {
|
|
|
197
218
|
if (!session) {
|
|
198
219
|
await bufferEvent({
|
|
199
220
|
slug,
|
|
200
|
-
agent_type
|
|
221
|
+
agent_type,
|
|
201
222
|
triggered_at,
|
|
202
223
|
meta: { ...meta, source },
|
|
203
224
|
});
|
|
@@ -209,7 +230,7 @@ export async function usageReportCommand(opts = {}) {
|
|
|
209
230
|
try {
|
|
210
231
|
const { data } = await api.post('/api/v1/intelligence/usage/by-slug', {
|
|
211
232
|
slug,
|
|
212
|
-
agent_type
|
|
233
|
+
agent_type,
|
|
213
234
|
triggered_at,
|
|
214
235
|
meta: { ...meta, source },
|
|
215
236
|
}, true);
|
|
@@ -223,7 +244,7 @@ export async function usageReportCommand(opts = {}) {
|
|
|
223
244
|
// the event isn't lost. The next successful command will replay.
|
|
224
245
|
await bufferEvent({
|
|
225
246
|
slug,
|
|
226
|
-
agent_type
|
|
247
|
+
agent_type,
|
|
227
248
|
triggered_at,
|
|
228
249
|
meta: { ...meta, source },
|
|
229
250
|
});
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { conflictsCommand } from './commands/conflicts.js';
|
|
7
7
|
import { diffCommand } from './commands/diff.js';
|
|
8
|
+
import { advisorCommand } from './commands/advisor.js';
|
|
8
9
|
import { docsCommand } from './commands/docs.js';
|
|
9
10
|
import { hooksAuditCommand, hooksInstallCommand, hooksListCommand, hooksRemoveCommand, hooksSyncCommand, hooksUpdateCommand, } from './commands/hooks.js';
|
|
10
11
|
import { importCommand } from './commands/import.js';
|
|
@@ -110,6 +111,11 @@ program
|
|
|
110
111
|
.command('whatdoes <skillname>')
|
|
111
112
|
.description("Inspect a skill's triggers, tokens, conflicts")
|
|
112
113
|
.action(whatdoesCommand);
|
|
114
|
+
program
|
|
115
|
+
.command('advisor [task...]')
|
|
116
|
+
.description('Prose recommendation from the contextual Skill advisor — pass a task description, or use --auto to base it on your installed Skills.')
|
|
117
|
+
.option('--auto', 'use your installed Skills + recent usage as context (no task argument)')
|
|
118
|
+
.action((taskWords, opts) => advisorCommand(taskWords.length ? taskWords.join(' ') : undefined, opts));
|
|
113
119
|
program
|
|
114
120
|
.command('overview')
|
|
115
121
|
.description('Summary of your skill set, conflicts, and token cost')
|
|
@@ -135,6 +141,7 @@ usage
|
|
|
135
141
|
.command('report')
|
|
136
142
|
.description('Internal: invoked by the agent when a Skill fires (reads payload from stdin)')
|
|
137
143
|
.option('--source <kind>', 'event channel that fired this report', 'tool')
|
|
144
|
+
.option('--agent <name>', 'which agent fired the event (claude | codex | cursor | gemini | cline | amp). Forward-compat: only `claude` is wired by `prave usage hook install` today, but the parameter lets future installers point at other runtimes.', 'claude')
|
|
138
145
|
.action((opts) => usageReportCommand(opts));
|
|
139
146
|
const hook = usage.command('hook').description('Enable / disable real-time Skill invocation tracking');
|
|
140
147
|
hook
|
package/dist/lib/hook.js
CHANGED
|
@@ -43,11 +43,16 @@ const REGISTRY_CHANNELS = [
|
|
|
43
43
|
'PreCompact',
|
|
44
44
|
'SessionStart',
|
|
45
45
|
];
|
|
46
|
-
|
|
46
|
+
// Stamped with `--agent claude` so the reporter attributes the
|
|
47
|
+
// invocation to the right runtime in our usage telemetry. Forward-
|
|
48
|
+
// compat: when Codex/Gemini/Cursor add equivalent lifecycle hooks,
|
|
49
|
+
// the new per-agent install branch will write `--agent codex` (etc.)
|
|
50
|
+
// — the reporter itself already accepts the full enum.
|
|
51
|
+
const HOOK_COMMAND = 'prave usage report --agent=claude';
|
|
47
52
|
// Companion command for the UserPromptSubmit channel so a typed-slash
|
|
48
53
|
// `/graphify` is captured even when the Skill tool path doesn't fire a
|
|
49
54
|
// matching PostToolUse with a populated `tool_input.skill` field.
|
|
50
|
-
const PROMPT_HOOK_COMMAND = 'prave usage report --source=prompt';
|
|
55
|
+
const PROMPT_HOOK_COMMAND = 'prave usage report --source=prompt --agent=claude';
|
|
51
56
|
/**
|
|
52
57
|
* Install on BOTH `PostToolUse` (matcher `Skill`) and `UserPromptSubmit`
|
|
53
58
|
* (catches slash commands like `/graphify` before tool dispatch). The two
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
const PROJECTS_DIR = join(homedir(), '.claude', 'projects');
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
5
|
+
* Per-agent transcript roots. Today only `claude` ships a structured
|
|
6
|
+
* JSONL transcript at a known path; the other agents either don't
|
|
7
|
+
* persist transcripts in a comparable shape (Codex, Gemini CLI), keep
|
|
8
|
+
* them in opaque formats (Cursor, Continue), or don't expose them on
|
|
9
|
+
* disk at all (Cline, Amp).
|
|
10
|
+
*
|
|
11
|
+
* Forward-compat: when an agent publishes a JSONL-style transcript
|
|
12
|
+
* convention with `tool_use`-shaped lines, drop an entry into the map
|
|
13
|
+
* and the scanner picks it up automatically. The `extractEventsFromLine`
|
|
14
|
+
* parser is JSON-shape-agnostic — it walks any object tree looking for
|
|
15
|
+
* a `name === 'Skill'` tool-use plus the canonical `<command-name>`
|
|
16
|
+
* marker. As long as the new agent's transcript surfaces those it's a
|
|
17
|
+
* zero-code-change integration.
|
|
18
|
+
*/
|
|
19
|
+
const AGENT_TRANSCRIPT_ROOTS = {
|
|
20
|
+
claude: join(homedir(), '.claude', 'projects'),
|
|
21
|
+
// codex: join(homedir(), '.codex', 'projects'), // TBD — Codex transcript path
|
|
22
|
+
// gemini: join(homedir(), '.gemini-cli', 'sessions'), // TBD — Gemini CLI session path
|
|
23
|
+
// cursor: … // currently no disk transcript
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Scan every .jsonl transcript newer than `since` for Skill invocations
|
|
27
|
+
* across every agent we know how to read.
|
|
7
28
|
*
|
|
8
29
|
* @param installedSlugs the slugs we recognise — anything outside this
|
|
9
30
|
* set is ignored to avoid noise from quoted text.
|
|
@@ -18,9 +39,15 @@ export async function scanTranscriptsForUsage(installedSlugs, since) {
|
|
|
18
39
|
const slugSet = new Set(installedSlugs);
|
|
19
40
|
const sinceMs = since.getTime();
|
|
20
41
|
const events = [];
|
|
21
|
-
const
|
|
42
|
+
for (const root of Object.values(AGENT_TRANSCRIPT_ROOTS)) {
|
|
43
|
+
await scanTranscriptRoot(root, slugSet, sinceMs, events);
|
|
44
|
+
}
|
|
45
|
+
return events;
|
|
46
|
+
}
|
|
47
|
+
async function scanTranscriptRoot(root, slugSet, sinceMs, events) {
|
|
48
|
+
const projectDirs = await safeReaddir(root);
|
|
22
49
|
for (const projectName of projectDirs) {
|
|
23
|
-
const projectDir = join(
|
|
50
|
+
const projectDir = join(root, projectName);
|
|
24
51
|
const projectStat = await safeStat(projectDir);
|
|
25
52
|
if (!projectStat?.isDirectory())
|
|
26
53
|
continue;
|
|
@@ -46,7 +73,6 @@ export async function scanTranscriptsForUsage(installedSlugs, since) {
|
|
|
46
73
|
}
|
|
47
74
|
}
|
|
48
75
|
}
|
|
49
|
-
return events;
|
|
50
76
|
}
|
|
51
77
|
const COMMAND_NAME_RE = /<command-name>\s*\/?([a-z0-9][a-z0-9_-]{0,80})\s*<\/command-name>/gi;
|
|
52
78
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prave/cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "Prave CLI — discover, install, version, test, and ship Claude Skills. The developer platform for the complete Skill lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"ora": "^8.0.1",
|
|
55
55
|
"tar": "^7.4.3",
|
|
56
56
|
"undici": "^6.18.0",
|
|
57
|
-
"@prave/shared": "1.5.
|
|
57
|
+
"@prave/shared": "1.5.2"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^20.12.7",
|