@prave/cli 1.0.6 → 1.0.8

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.
@@ -76,6 +76,22 @@ export async function findCommand(query, opts = {}) {
76
76
  }
77
77
  catch (err) {
78
78
  spinner.stop();
79
+ if (err instanceof ApiError && err.status === 402) {
80
+ // Server returned the semantic-search upsell. Render it as a
81
+ // structured upgrade hint instead of a raw error so the message
82
+ // reads like guidance, not a crash.
83
+ log.error(err.message);
84
+ console.log();
85
+ console.log(chalk.dim(' Pro · $12/mo includes:'));
86
+ console.log(chalk.dim(' · `prave search "<natural language>"` ranked by intent'));
87
+ console.log(chalk.dim(' · Skill Intelligence audit + 30-day trigger telemetry'));
88
+ console.log(chalk.dim(' · Cross-machine sync · Tester · Authoring'));
89
+ console.log();
90
+ console.log(` ${chalk.bold('→ Upgrade:')} ${chalk.cyan('https://prave.app/#pricing')}`);
91
+ console.log(chalk.dim(' Or browse the registry without semantic search:'), chalk.cyan('https://prave.app/discover'));
92
+ process.exitCode = 1;
93
+ return;
94
+ }
79
95
  log.error(err instanceof ApiError ? err.message : err.message);
80
96
  process.exitCode = 1;
81
97
  }
@@ -31,6 +31,10 @@ export async function loginCommand() {
31
31
  access_token: data.access_token,
32
32
  refresh_token: data.refresh_token,
33
33
  user_id: data.user_id,
34
+ // Server hands us the JWT's `exp` claim so the CLI can refresh
35
+ // proactively. Stored as unix-seconds; null when the server
36
+ // couldn't decode it (very old creds, mock mode).
37
+ expires_at: data.expires_at ?? undefined,
34
38
  });
35
39
  spinner.succeed('Logged in.');
36
40
  // Onboarding: prefill from the SaaS profile, let the user toggle
@@ -149,10 +149,43 @@ export async function usageReportCommand() {
149
149
  await debugLog('no auth — skipping');
150
150
  return;
151
151
  }
152
+ // Build the telemetry blob from whatever Claude Code shipped in the
153
+ // payload. Everything is best-effort — missing fields just don't
154
+ // appear in `meta`. The server schema (usageMetaSchema) validates
155
+ // primitives only, so we coerce here.
156
+ const meta = {
157
+ payload_bytes: stdinPayload.length,
158
+ };
159
+ const tr = parsed.tool_response;
160
+ if (tr && typeof tr === 'object' && tr !== null) {
161
+ const obj = tr;
162
+ if (typeof obj.duration_ms === 'number')
163
+ meta.duration_ms = obj.duration_ms;
164
+ if (typeof obj.duration === 'number')
165
+ meta.duration_ms = obj.duration;
166
+ if (typeof obj.output === 'string')
167
+ meta.output_chars = obj.output.length;
168
+ }
169
+ if (typeof parsed.tool_name === 'string')
170
+ meta.tool_name = parsed.tool_name;
171
+ if (typeof parsed.session_id === 'string')
172
+ meta.agent_session = parsed.session_id;
173
+ const ti = parsed.tool_input;
174
+ if (ti && typeof ti === 'object' && ti !== null) {
175
+ const obj = ti;
176
+ if (typeof obj.prompt === 'string')
177
+ meta.prompt_chars = obj.prompt.length;
178
+ }
152
179
  try {
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}`);
180
+ const { data } = await api.post('/api/v1/intelligence/usage/by-slug', {
181
+ slug,
182
+ agent_type: 'claude',
183
+ triggered_at: new Date().toISOString(),
184
+ meta,
185
+ }, true);
186
+ if (debug) {
187
+ await debugLog(`ok recorded=${data.recorded} stub=${data.created_stub} meta=${JSON.stringify(meta)}`);
188
+ }
156
189
  }
157
190
  catch (err) {
158
191
  if (debug)
package/dist/lib/api.js CHANGED
@@ -39,6 +39,8 @@ async function refreshTokens() {
39
39
  access_token: payload.data.access_token,
40
40
  refresh_token: payload.data.refresh_token ?? creds.refresh_token,
41
41
  user_id: payload.data.user_id,
42
+ // Persist the new expiry so the next call's proactive check works.
43
+ expires_at: payload.data.expires_at ?? undefined,
42
44
  };
43
45
  await saveCredentials(next);
44
46
  return next;
@@ -54,12 +56,31 @@ async function refreshTokens() {
54
56
  refreshing = null;
55
57
  }
56
58
  }
59
+ /**
60
+ * Pre-flight check: if the access token is expired or within 60s of
61
+ * expiring, refresh BEFORE the call. The post-call 401 retry below
62
+ * still catches the race where the token slips between this check and
63
+ * the server-side verify, but doing it proactively avoids the wasted
64
+ * round-trip on every long-idle session.
65
+ */
66
+ async function ensureFreshAccessToken(creds) {
67
+ const exp = creds.expires_at;
68
+ if (!exp)
69
+ return creds; // legacy creds without expiry → 401 path takes over
70
+ const now = Math.floor(Date.now() / 1000);
71
+ const SKEW = 60;
72
+ if (exp > now + SKEW)
73
+ return creds;
74
+ const refreshed = await refreshTokens();
75
+ return refreshed ?? creds;
76
+ }
57
77
  async function call(method, path, body, withAuth = false, attempt = 0) {
58
78
  const headers = { 'Content-Type': 'application/json' };
59
79
  if (withAuth) {
60
- const creds = await loadCredentials();
80
+ let creds = await loadCredentials();
61
81
  if (!creds)
62
82
  throw new ApiError('Not logged in. Run `prave login`.', 401);
83
+ creds = await ensureFreshAccessToken(creds);
63
84
  headers.Authorization = `Bearer ${creds.access_token}`;
64
85
  }
65
86
  const { statusCode, body: resBody } = await request(`${CONFIG.apiUrl}${path}`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prave/cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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.6"
19
+ "@prave/shared": "1.0.8"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^20.12.7",