@prave/cli 1.6.1 → 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.
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { track } from '../lib/analytics.js';
4
4
  import { api, ApiError } from '../lib/api.js';
5
+ import { CONFIG } from '../lib/config.js';
5
6
  import { requireAuth } from '../lib/credentials.js';
6
7
  import { log } from '../utils/logger.js';
7
8
  const RULE = '─────────────────────────────────────────';
@@ -21,21 +22,24 @@ export async function advisorCommand(task, opts = {}) {
21
22
  const { data } = await api.post('/api/v1/advisor', { mode, task: mode === 'manual' ? task : undefined }, true);
22
23
  spinner.stop();
23
24
  console.log(chalk.dim(RULE));
24
- console.log(chalk.bold('Recommended for you'));
25
+ const hasReasoned = data.recommendations.length > 0;
26
+ console.log(chalk.bold(hasReasoned ? 'Recommended for you' : 'Starting points'));
25
27
  console.log(chalk.dim(RULE));
26
28
  console.log(wrap(data.prose, 78));
27
29
  console.log();
28
- if (data.recommendations.length === 0) {
29
- log.dim('No specific Skills to recommend yet.');
30
- }
31
- else {
30
+ if (hasReasoned) {
32
31
  for (const rec of data.recommendations) {
33
- console.log(` ${chalk.cyan('▸')} ${chalk.bold(rec.slug)}`);
34
- console.log(` ${chalk.dim('Why:')} ${wrap(rec.reason, 70).split('\n').join('\n ')}`);
35
- console.log(` ${chalk.dim(`prave install ${rec.slug}`)}`);
36
- console.log();
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);
37
38
  }
38
39
  }
40
+ else {
41
+ log.dim('No specific Skills to recommend yet.');
42
+ }
39
43
  console.log(chalk.dim(`Quota: ${data.quota.used} / ${data.quota.limit} today on the ${data.quota.plan} plan.`));
40
44
  }
41
45
  catch (err) {
@@ -44,6 +48,20 @@ export async function advisorCommand(task, opts = {}) {
44
48
  process.exitCode = 1;
45
49
  }
46
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
+ }
47
65
  function truncate(s, n) {
48
66
  return s.length <= n ? s : `${s.slice(0, n - 1)}…`;
49
67
  }
@@ -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
  {
@@ -228,16 +229,26 @@ async function handleAdvisor(args) {
228
229
  }
229
230
  const { data } = await api.post('/api/v1/advisor', { mode, task: mode === 'manual' ? args.task : undefined }, true);
230
231
  const lines = [data.prose, ''];
231
- if (data.recommendations.length === 0) {
232
- lines.push('(No specific Skills recommended for this context.)');
233
- }
234
- else {
232
+ const hasReasoned = data.recommendations.length > 0;
233
+ if (hasReasoned) {
235
234
  for (const rec of data.recommendations) {
236
235
  lines.push(`• ${rec.slug}`);
237
236
  lines.push(` Why: ${rec.reason}`);
238
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}`);
239
247
  }
240
248
  }
249
+ else {
250
+ lines.push('(No specific Skills recommended for this context.)');
251
+ }
241
252
  lines.push('');
242
253
  lines.push(`Advisor quota: ${data.quota.used} / ${data.quota.limit} today on the ${data.quota.plan} plan.`);
243
254
  return mcpText(lines.join('\n'));
@@ -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
@@ -141,6 +141,7 @@ usage
141
141
  .command('report')
142
142
  .description('Internal: invoked by the agent when a Skill fires (reads payload from stdin)')
143
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')
144
145
  .action((opts) => usageReportCommand(opts));
145
146
  const hook = usage.command('hook').description('Enable / disable real-time Skill invocation tracking');
146
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.1",
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.1"
57
+ "@prave/shared": "1.5.2"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^20.12.7",