trickle-cli 0.1.196 → 0.1.198

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.
@@ -262,6 +262,52 @@ function costReportCommand(opts) {
262
262
  console.log(` ${chalk_1.default.cyan(name.padEnd(30))} $${data.cost.toFixed(4).padEnd(10)} ${chalk_1.default.gray(pct + '%')} ${data.calls} calls ${formatTokens(data.tokens)} tokens`);
263
263
  }
264
264
  }
265
+ // Cache hit/miss analysis — detect from latency bimodality
266
+ if (calls.length >= 4) {
267
+ // Group by model, find bimodal latency distribution
268
+ const modelLatencies = {};
269
+ for (const c of calls) {
270
+ if (!c.durationMs || c.error)
271
+ continue;
272
+ const key = c.model || 'unknown';
273
+ if (!modelLatencies[key])
274
+ modelLatencies[key] = [];
275
+ modelLatencies[key].push(c.durationMs);
276
+ }
277
+ let cacheDetected = false;
278
+ const cacheAnalysis = [];
279
+ for (const [model, latencies] of Object.entries(modelLatencies)) {
280
+ if (latencies.length < 3)
281
+ continue;
282
+ latencies.sort((a, b) => a - b);
283
+ const median = latencies[Math.floor(latencies.length / 2)];
284
+ // Split into fast (< 30% of median) and slow (>= 30% of median)
285
+ const threshold = median * 0.3;
286
+ const fast = latencies.filter(l => l < threshold);
287
+ const slow = latencies.filter(l => l >= threshold);
288
+ if (fast.length >= 1 && slow.length >= 1 && fast.length / latencies.length >= 0.1) {
289
+ const fastAvg = fast.reduce((s, l) => s + l, 0) / fast.length;
290
+ const slowAvg = slow.reduce((s, l) => s + l, 0) / slow.length;
291
+ // Only report if there's a significant speed difference (5x+)
292
+ if (slowAvg / Math.max(1, fastAvg) >= 5) {
293
+ cacheDetected = true;
294
+ cacheAnalysis.push({
295
+ model, fastCalls: fast.length, slowCalls: slow.length,
296
+ fastAvg: Math.round(fastAvg), slowAvg: Math.round(slowAvg),
297
+ hitRate: Math.round((fast.length / latencies.length) * 100),
298
+ });
299
+ }
300
+ }
301
+ }
302
+ if (cacheDetected) {
303
+ console.log(chalk_1.default.gray('\n ' + '─'.repeat(60)));
304
+ console.log(chalk_1.default.bold(' Cache Analysis') + chalk_1.default.gray(' (detected from latency bimodality)'));
305
+ for (const ca of cacheAnalysis) {
306
+ const speedup = (ca.slowAvg / Math.max(1, ca.fastAvg)).toFixed(0);
307
+ console.log(` ${chalk_1.default.cyan(ca.model.padEnd(25))} hit rate: ${chalk_1.default.green(ca.hitRate + '%')} (${ca.fastCalls} fast, ${ca.slowCalls} slow) ${speedup}x speedup fast=${ca.fastAvg}ms slow=${ca.slowAvg}ms`);
308
+ }
309
+ }
310
+ }
265
311
  if (costlyCalls.length > 0) {
266
312
  console.log(chalk_1.default.gray('\n ' + '─'.repeat(60)));
267
313
  console.log(chalk_1.default.bold(' Most Expensive Calls'));
@@ -279,8 +279,7 @@ async function runCommand(command, opts) {
279
279
  if (!backendProc) {
280
280
  // Fall back to local/offline mode instead of exiting
281
281
  localMode = true;
282
- console.log(chalk_1.default.yellow(`\n Backend not availableusing local mode (offline)`));
283
- console.log(chalk_1.default.gray(" Observations will be saved to .trickle/observations.jsonl"));
282
+ // Silent for first-time users — local mode is the default experience
284
283
  }
285
284
  }
286
285
  // Detect language and inject instrumentation
@@ -400,13 +399,9 @@ async function executeSingleRun(instrumentedCommand, env, opts, singleFile, loca
400
399
  await autoCloudPush();
401
400
  // Generate post-run summary for AI agents
402
401
  (0, summary_1.writeRunSummary)({ exitCode, command: instrumentedCommand });
403
- // Next steps hint
404
402
  console.log("");
405
- console.log(chalk_1.default.bold(" Next steps:"));
406
- console.log(chalk_1.default.gray(" trickle summary ") + "full analysis (errors, queries, root causes)");
407
- console.log(chalk_1.default.gray(" trickle explain <file> ") + "understand a file (functions, call graph, data flow)");
408
- console.log(chalk_1.default.gray(" trickle flamegraph ") + "performance hotspots");
409
- console.log(chalk_1.default.gray(" trickle test ") + "run tests with observability");
403
+ console.log(chalk_1.default.gray(" trickle summary ") + "full analysis");
404
+ console.log(chalk_1.default.gray(" trickle why ") + "trace any error to root cause");
410
405
  console.log("");
411
406
  return exitCode;
412
407
  }
@@ -562,8 +557,19 @@ async function autoCloudPush() {
562
557
  try {
563
558
  const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
564
559
  if (config.url && config.token) {
565
- const { cloudPush } = await Promise.resolve().then(() => __importStar(require("./cloud")));
566
- await cloudPush();
560
+ // Suppress console output for auto-push to avoid noise
561
+ const origLog = console.log;
562
+ const origErr = console.error;
563
+ console.log = () => { };
564
+ console.error = () => { };
565
+ try {
566
+ const { cloudPush } = await Promise.resolve().then(() => __importStar(require("./cloud")));
567
+ await cloudPush();
568
+ }
569
+ finally {
570
+ console.log = origLog;
571
+ console.error = origErr;
572
+ }
567
573
  }
568
574
  }
569
575
  catch { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-cli",
3
- "version": "0.1.196",
3
+ "version": "0.1.198",
4
4
  "description": "CLI for trickle runtime type observability",
5
5
  "bin": {
6
6
  "trickle": "dist/index.js"
@@ -241,6 +241,54 @@ export function costReportCommand(opts: { json?: boolean; budget?: string }): vo
241
241
  }
242
242
  }
243
243
 
244
+ // Cache hit/miss analysis — detect from latency bimodality
245
+ if (calls.length >= 4) {
246
+ // Group by model, find bimodal latency distribution
247
+ const modelLatencies: Record<string, number[]> = {};
248
+ for (const c of calls) {
249
+ if (!c.durationMs || c.error) continue;
250
+ const key = c.model || 'unknown';
251
+ if (!modelLatencies[key]) modelLatencies[key] = [];
252
+ modelLatencies[key].push(c.durationMs);
253
+ }
254
+
255
+ let cacheDetected = false;
256
+ const cacheAnalysis: Array<{ model: string; fastCalls: number; slowCalls: number; fastAvg: number; slowAvg: number; hitRate: number }> = [];
257
+
258
+ for (const [model, latencies] of Object.entries(modelLatencies)) {
259
+ if (latencies.length < 3) continue;
260
+ latencies.sort((a, b) => a - b);
261
+ const median = latencies[Math.floor(latencies.length / 2)];
262
+ // Split into fast (< 30% of median) and slow (>= 30% of median)
263
+ const threshold = median * 0.3;
264
+ const fast = latencies.filter(l => l < threshold);
265
+ const slow = latencies.filter(l => l >= threshold);
266
+
267
+ if (fast.length >= 1 && slow.length >= 1 && fast.length / latencies.length >= 0.1) {
268
+ const fastAvg = fast.reduce((s, l) => s + l, 0) / fast.length;
269
+ const slowAvg = slow.reduce((s, l) => s + l, 0) / slow.length;
270
+ // Only report if there's a significant speed difference (5x+)
271
+ if (slowAvg / Math.max(1, fastAvg) >= 5) {
272
+ cacheDetected = true;
273
+ cacheAnalysis.push({
274
+ model, fastCalls: fast.length, slowCalls: slow.length,
275
+ fastAvg: Math.round(fastAvg), slowAvg: Math.round(slowAvg),
276
+ hitRate: Math.round((fast.length / latencies.length) * 100),
277
+ });
278
+ }
279
+ }
280
+ }
281
+
282
+ if (cacheDetected) {
283
+ console.log(chalk.gray('\n ' + '─'.repeat(60)));
284
+ console.log(chalk.bold(' Cache Analysis') + chalk.gray(' (detected from latency bimodality)'));
285
+ for (const ca of cacheAnalysis) {
286
+ const speedup = (ca.slowAvg / Math.max(1, ca.fastAvg)).toFixed(0);
287
+ console.log(` ${chalk.cyan(ca.model.padEnd(25))} hit rate: ${chalk.green(ca.hitRate + '%')} (${ca.fastCalls} fast, ${ca.slowCalls} slow) ${speedup}x speedup fast=${ca.fastAvg}ms slow=${ca.slowAvg}ms`);
288
+ }
289
+ }
290
+ }
291
+
244
292
  if (costlyCalls.length > 0) {
245
293
  console.log(chalk.gray('\n ' + '─'.repeat(60)));
246
294
  console.log(chalk.bold(' Most Expensive Calls'));
@@ -290,16 +290,7 @@ export async function runCommand(
290
290
  if (!backendProc) {
291
291
  // Fall back to local/offline mode instead of exiting
292
292
  localMode = true;
293
- console.log(
294
- chalk.yellow(
295
- `\n Backend not available — using local mode (offline)`,
296
- ),
297
- );
298
- console.log(
299
- chalk.gray(
300
- " Observations will be saved to .trickle/observations.jsonl",
301
- ),
302
- );
293
+ // Silent for first-time users — local mode is the default experience
303
294
  }
304
295
  }
305
296
 
@@ -453,13 +444,9 @@ async function executeSingleRun(
453
444
  // Generate post-run summary for AI agents
454
445
  writeRunSummary({ exitCode, command: instrumentedCommand });
455
446
 
456
- // Next steps hint
457
447
  console.log("");
458
- console.log(chalk.bold(" Next steps:"));
459
- console.log(chalk.gray(" trickle summary ") + "full analysis (errors, queries, root causes)");
460
- console.log(chalk.gray(" trickle explain <file> ") + "understand a file (functions, call graph, data flow)");
461
- console.log(chalk.gray(" trickle flamegraph ") + "performance hotspots");
462
- console.log(chalk.gray(" trickle test ") + "run tests with observability");
448
+ console.log(chalk.gray(" trickle summary ") + "full analysis");
449
+ console.log(chalk.gray(" trickle why ") + "trace any error to root cause");
463
450
  console.log("");
464
451
 
465
452
  return exitCode;
@@ -632,8 +619,18 @@ async function autoCloudPush(): Promise<void> {
632
619
  try {
633
620
  const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
634
621
  if (config.url && config.token) {
635
- const { cloudPush } = await import("./cloud");
636
- await cloudPush();
622
+ // Suppress console output for auto-push to avoid noise
623
+ const origLog = console.log;
624
+ const origErr = console.error;
625
+ console.log = () => {};
626
+ console.error = () => {};
627
+ try {
628
+ const { cloudPush } = await import("./cloud");
629
+ await cloudPush();
630
+ } finally {
631
+ console.log = origLog;
632
+ console.error = origErr;
633
+ }
637
634
  }
638
635
  } catch {}
639
636
  }