@prave/cli 1.4.9 → 1.4.10

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.
@@ -5,6 +5,7 @@ import { track } from '../lib/analytics.js';
5
5
  import { api, ApiError } from '../lib/api.js';
6
6
  import { CONFIG } from '../lib/config.js';
7
7
  import { saveCredentials } from '../lib/credentials.js';
8
+ import { readLocalConfig, writeLocalConfigPatch } from '../lib/local-config.js';
8
9
  import { log } from '../utils/logger.js';
9
10
  /**
10
11
  * `prave login` — device-code flow against the Prave API / Supabase session.
@@ -51,12 +52,23 @@ export async function loginCommand() {
51
52
  catch (flushErr) {
52
53
  log.dim(`Telemetry sync skipped: ${flushErr.message}`);
53
54
  }
54
- // Onboarding: prefill from the SaaS profile, let the user toggle
55
- // with space/enter, persist back, and offer to install hooks.
56
- // Failures here are non-fatal login succeeded.
55
+ // First-login onboarding only. The agent picker + hook installer
56
+ // used to fire on EVERY login, which was annoying for users who
57
+ // re-login often (CI environments, multi-machine setups, after a
58
+ // token expiry). Now we gate on a persisted flag in
59
+ // ~/.prave/config.json — once they've completed the picker once,
60
+ // it never auto-fires again. Manual re-run available via
61
+ // `prave settings agent`.
57
62
  try {
58
- const { runAgentOnboarding } = await import('../lib/agent-onboarding.js');
59
- await runAgentOnboarding();
63
+ const cfg = await readLocalConfig();
64
+ if (cfg.agent_onboarding_done !== true) {
65
+ const { runAgentOnboarding } = await import('../lib/agent-onboarding.js');
66
+ await runAgentOnboarding();
67
+ await writeLocalConfigPatch({ agent_onboarding_done: true });
68
+ }
69
+ else {
70
+ log.dim('Welcome back — run `prave settings agent` to change your agent setup.');
71
+ }
60
72
  }
61
73
  catch (onboardErr) {
62
74
  log.dim(`Agent onboarding skipped: ${onboardErr.message}`);
@@ -1,10 +1,9 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
1
  import { createInterface } from 'node:readline/promises';
3
2
  import chalk from 'chalk';
4
3
  import { AGENT_REGISTRY } from '@prave/shared';
5
4
  import { api, ApiError } from '../lib/api.js';
6
5
  import { requireAuth } from '../lib/credentials.js';
7
- import { CONFIG } from '../lib/config.js';
6
+ import { writeLocalConfigPatch } from '../lib/local-config.js';
8
7
  import { log } from '../utils/logger.js';
9
8
  function detectOs() {
10
9
  if (process.platform === 'darwin')
@@ -13,20 +12,14 @@ function detectOs() {
13
12
  return 'windows';
14
13
  return 'linux';
15
14
  }
16
- async function loadLocalConfig() {
17
- try {
18
- const raw = await readFile(CONFIG.configPath, 'utf8');
19
- return JSON.parse(raw);
20
- }
21
- catch {
22
- return {};
23
- }
24
- }
25
15
  async function saveLocalConfig(settings) {
26
- await mkdir(CONFIG.praveDir, { recursive: true });
27
- const existing = await loadLocalConfig();
28
- const next = { ...existing, agentSettings: settings };
29
- await writeFile(CONFIG.configPath, JSON.stringify(next, null, 2), 'utf8');
16
+ // Mark onboarding done as a side effect — running `prave settings
17
+ // agent` manually counts as completing the picker, so the user won't
18
+ // get auto-prompted again on the next `prave login`.
19
+ await writeLocalConfigPatch({
20
+ agentSettings: settings,
21
+ agent_onboarding_done: true,
22
+ });
30
23
  }
31
24
  async function fetchSettings() {
32
25
  const { data } = await api.get('/api/v1/settings/agents', true);
@@ -432,8 +432,8 @@ export async function usageStatusCommand() {
432
432
  const checkmark = (ok) => (ok ? chalk.green('✓') : chalk.red('✗'));
433
433
  log.info(chalk.bold('Usage tracking status'));
434
434
  console.log();
435
- console.log(` ${checkmark(toolHookInstalled)} PostToolUse hook (Skill tool fires): ${toolHookInstalled ? chalk.green('installed') : chalk.yellow('missing — `prave usage hook install`')}`);
436
- console.log(` ${checkmark(promptHookInstalled)} UserPromptSubmit hook (slash commands like /graphify): ${promptHookInstalled ? chalk.green('installed') : chalk.yellow('missing — `prave usage hook install`')}`);
435
+ console.log(` ${checkmark(toolHookInstalled)} Skill invocation tracking: ${toolHookInstalled ? chalk.green('installed') : chalk.yellow('missing — `prave usage hook install`')}`);
436
+ console.log(` ${checkmark(promptHookInstalled)} Slash-command tracking (e.g. /graphify): ${promptHookInstalled ? chalk.green('installed') : chalk.yellow('missing — `prave usage hook install`')}`);
437
437
  console.log(` ${checkmark(apiReachable)} API reachable + auth valid: ${apiReachable ? chalk.green('yes') : chalk.red(apiMessage || 'no')}`);
438
438
  console.log(` ${checkmark(Boolean(lastScanAt))} Transcript scanner watermark: ${lastScanAt ?? chalk.dim('never run — `prave sync` includes it')}`);
439
439
  console.log(` ${checkmark(recent7 > 0)} Events in last 7 days: ${chalk.cyan(String(recent7))}`);
@@ -470,7 +470,7 @@ export async function usageStatusCommand() {
470
470
  console.log();
471
471
  log.warn('Telemetry may be incomplete. Suggested fixes:');
472
472
  if (!toolHookInstalled || !promptHookInstalled) {
473
- log.dim(' • Run `prave usage hook install` to wire BOTH PostToolUse and UserPromptSubmit.');
473
+ log.dim(' • Run `prave usage hook install` to enable both Skill-fire and slash-command tracking.');
474
474
  }
475
475
  if (!apiReachable) {
476
476
  log.dim(' • Run `prave login` — your access token may have expired (silently 401-ing).');
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ const program = new Command()
49
49
  .addHelpText('after', '\nRun `prave <cmd> --help` for command-specific options.');
50
50
  program
51
51
  .command('login')
52
- .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.')
52
+ .description('Sign this machine in (browser opens, no password typed). Credentials are stored securely on disk and refreshed automatically.')
53
53
  .action(loginCommand);
54
54
  program
55
55
  .command('logout')
@@ -130,17 +130,17 @@ const usage = program
130
130
  .description('Track which Skills you actually use (powers the optimiser)');
131
131
  usage
132
132
  .command('report')
133
- .description('Internal: invoked by the Claude Code PostToolUse / UserPromptSubmit hook (reads stdin)')
134
- .option('--source <kind>', 'hook channel that fired this report ("tool" or "prompt")', 'tool')
133
+ .description('Internal: invoked by the agent when a Skill fires (reads payload from stdin)')
134
+ .option('--source <kind>', 'event channel that fired this report', 'tool')
135
135
  .action((opts) => usageReportCommand(opts));
136
- const hook = usage.command('hook').description('Install/uninstall the Claude Code real-time usage hook');
136
+ const hook = usage.command('hook').description('Enable / disable real-time Skill invocation tracking');
137
137
  hook
138
138
  .command('install')
139
- .description('Add a PostToolUse hook to ~/.claude/settings.json')
139
+ .description('Enable real-time invocation tracking for your installed Skills')
140
140
  .action(usageHookInstallCommand);
141
141
  hook
142
142
  .command('uninstall')
143
- .description('Remove the Prave-managed hook from ~/.claude/settings.json')
143
+ .description('Disable real-time invocation tracking')
144
144
  .action(usageHookUninstallCommand);
145
145
  program
146
146
  .command('mcp-server')
@@ -188,7 +188,7 @@ program
188
188
  ' prave optimize --remove-unused # delete 30d-silent skills from disk',
189
189
  '',
190
190
  'Usage tracking (Pro+)',
191
- ' prave usage hook install # real-time PostToolUse hook',
191
+ ' prave usage hook install # enable real-time invocation tracking',
192
192
  ' prave usage hook uninstall',
193
193
  '',
194
194
  'Settings',
@@ -0,0 +1,44 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { CONFIG } from './config.js';
3
+ /**
4
+ * Shared read / write for `~/.prave/config.json`.
5
+ *
6
+ * The same file holds three classes of state, all merged into one
7
+ * JSON object so callers can patch one key without clobbering the
8
+ * others:
9
+ *
10
+ * • `agentSettings` — written by `prave settings`
11
+ * • Nudge bookkeeping (`first_run`, `nudge_count`) — written by
12
+ * the conversion-nudge module
13
+ * • Onboarding flags (`agent_onboarding_done`) — written by
14
+ * `prave login` so we only walk the user through the agent
15
+ * picker once
16
+ *
17
+ * Previously each consumer reimplemented the same readFile / merge /
18
+ * writeFile dance — three copies in three files. Centralising here
19
+ * means a missing key in one consumer no longer wipes the keys
20
+ * another consumer just wrote.
21
+ */
22
+ export async function readLocalConfig() {
23
+ try {
24
+ const raw = await readFile(CONFIG.configPath, 'utf8');
25
+ const parsed = JSON.parse(raw);
26
+ if (parsed && typeof parsed === 'object')
27
+ return parsed;
28
+ return {};
29
+ }
30
+ catch {
31
+ return {};
32
+ }
33
+ }
34
+ /**
35
+ * Merge-write: reads the current config, shallow-merges `patch`, and
36
+ * writes the result. Creates `~/.prave/` with restrictive mode (0700)
37
+ * if it doesn't exist yet.
38
+ */
39
+ export async function writeLocalConfigPatch(patch) {
40
+ const existing = await readLocalConfig();
41
+ const next = { ...existing, ...patch };
42
+ await mkdir(CONFIG.praveDir, { recursive: true, mode: 0o700 });
43
+ await writeFile(CONFIG.configPath, JSON.stringify(next, null, 2), 'utf8');
44
+ }
@@ -47,7 +47,7 @@ export const NUDGE_DEEP = {
47
47
  bullets: [
48
48
  'AI-generated descriptions for all your Skills',
49
49
  'Conflict detection across your library',
50
- '30-day usage history from PostToolUse hook',
50
+ '30-day Skill invocation history',
51
51
  'Cross-machine sync',
52
52
  ],
53
53
  cta: 'prave.app/signup · free forever · 10 seconds',
package/dist/lib/nudge.js CHANGED
@@ -1,8 +1,27 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
1
  import chalk from 'chalk';
3
- import { CONFIG } from './config.js';
4
2
  import { loadCredentials } from './credentials.js';
3
+ import { readLocalConfig, writeLocalConfigPatch } from './local-config.js';
5
4
  import { isAlwaysShow, NUDGE_FIRST_RUN, nudgeFor, } from './nudge-constants.js';
5
+ /**
6
+ * Conversion nudges for anonymous CLI users.
7
+ *
8
+ * 4,300 npm-installs, 1 signed-in account. The CLI works for anonymous
9
+ * users by design (lower friction = more installs) but the handoff to
10
+ * the dashboard was missing. These nudges fix that.
11
+ *
12
+ * Rules baked in here, not in the call sites:
13
+ * • Silent for signed-in users. Always.
14
+ * • Soft nudges throttled to ~1 in every 3 commands via a counter
15
+ * persisted to ~/.prave/config.json (`nudge_count`).
16
+ * • Strong nudges (overview/whatdoes/first-run) bypass the throttle —
17
+ * they're the highest-intent moments and worth a fuller pitch.
18
+ * • First-run banner shows once on the user's very first command;
19
+ * `first_run: false` then locks it down forever.
20
+ * • One nudge per process. Multiple commands chained in a wrapper
21
+ * script don't spam.
22
+ * • Skipped when stdout isn't a TTY (piped output, CI) and when
23
+ * PRAVE_QUIET=1 / PRAVE_TELEMETRY=0.
24
+ */
6
25
  let alreadyNudged = false;
7
26
  /**
8
27
  * Auth snapshot captured at command START, used at command END.
@@ -35,24 +54,6 @@ export async function isAuthenticated() {
35
54
  }
36
55
  return true;
37
56
  }
38
- async function readConfig() {
39
- try {
40
- const raw = await readFile(CONFIG.configPath, 'utf8');
41
- const parsed = JSON.parse(raw);
42
- if (parsed && typeof parsed === 'object')
43
- return parsed;
44
- return {};
45
- }
46
- catch {
47
- return {};
48
- }
49
- }
50
- async function writeConfigPatch(patch) {
51
- const existing = await readConfig();
52
- const next = { ...existing, ...patch };
53
- await mkdir(CONFIG.praveDir, { recursive: true, mode: 0o700 });
54
- await writeFile(CONFIG.configPath, JSON.stringify(next, null, 2), 'utf8');
55
- }
56
57
  /**
57
58
  * Counter-based throttle for soft nudges. Reads the current count,
58
59
  * increments, persists, and returns true when (count % 3 === 0) — so
@@ -60,10 +61,10 @@ async function writeConfigPatch(patch) {
60
61
  * we'd rather skip than be loud on the user's first real command.
61
62
  */
62
63
  export async function shouldShowNudge() {
63
- const cfg = await readConfig();
64
+ const cfg = await readLocalConfig();
64
65
  const current = typeof cfg.nudge_count === 'number' ? cfg.nudge_count : 0;
65
66
  const next = current + 1;
66
- await writeConfigPatch({ nudge_count: next });
67
+ await writeLocalConfigPatch({ nudge_count: next });
67
68
  return next % 3 === 0;
68
69
  }
69
70
  /* --------------------------------------------------------------------- */
@@ -141,7 +142,7 @@ export async function nudgeFirstRun() {
141
142
  const startedAnon = wasAnonymousAtStart ?? !(await isAuthenticated());
142
143
  if (!startedAnon)
143
144
  return false;
144
- const cfg = await readConfig();
145
+ const cfg = await readLocalConfig();
145
146
  // first_run defaults to "yes, show it" unless we've already flipped
146
147
  // the flag. A missing config file → first run.
147
148
  const alreadyShown = cfg.first_run === false;
@@ -149,7 +150,7 @@ export async function nudgeFirstRun() {
149
150
  return false;
150
151
  alreadyNudged = true;
151
152
  showNudge(NUDGE_FIRST_RUN);
152
- await writeConfigPatch({ first_run: false });
153
+ await writeLocalConfigPatch({ first_run: false });
153
154
  return true;
154
155
  }
155
156
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prave/cli",
3
- "version": "1.4.9",
3
+ "version": "1.4.10",
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.4.9"
57
+ "@prave/shared": "1.4.10"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^20.12.7",