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.
- package/dist/commands/cost-report.js +46 -0
- package/dist/commands/run.js +16 -10
- package/package.json +1 -1
- package/src/commands/cost-report.ts +48 -0
- package/src/commands/run.ts +15 -18
|
@@ -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'));
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
406
|
-
console.log(chalk_1.default.gray("
|
|
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
|
-
|
|
566
|
-
|
|
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
|
@@ -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'));
|
package/src/commands/run.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
459
|
-
console.log(chalk.gray("
|
|
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
|
-
|
|
636
|
-
|
|
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
|
}
|