@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.
@@ -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 }] };
@@ -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: 'claude',
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: 'claude',
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: 'claude',
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
- const HOOK_COMMAND = 'prave usage report';
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
- * Scan every .jsonl transcript newer than `since` for Skill invocations.
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 projectDirs = await safeReaddir(PROJECTS_DIR);
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(PROJECTS_DIR, projectName);
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.0",
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.0"
57
+ "@prave/shared": "1.5.2"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^20.12.7",