trickle-cli 0.1.194 → 0.1.196
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/cleanup.d.ts +12 -0
- package/dist/commands/cleanup.js +173 -0
- package/dist/commands/cost-report.js +58 -1
- package/dist/index.js +12 -0
- package/package.json +1 -1
- package/src/commands/cleanup.ts +162 -0
- package/src/commands/cost-report.ts +56 -1
- package/src/index.ts +13 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trickle cleanup — Smart data management for .trickle/ files.
|
|
3
|
+
*
|
|
4
|
+
* Prunes old data, compacts JSONL files, and manages retention.
|
|
5
|
+
* Essential for heavy workloads where agents produce 10-100x more data.
|
|
6
|
+
*/
|
|
7
|
+
export declare function cleanupCommand(opts: {
|
|
8
|
+
retainDays?: string;
|
|
9
|
+
retainLines?: string;
|
|
10
|
+
dryRun?: boolean;
|
|
11
|
+
json?: boolean;
|
|
12
|
+
}): void;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* trickle cleanup — Smart data management for .trickle/ files.
|
|
4
|
+
*
|
|
5
|
+
* Prunes old data, compacts JSONL files, and manages retention.
|
|
6
|
+
* Essential for heavy workloads where agents produce 10-100x more data.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.cleanupCommand = cleanupCommand;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
49
|
+
function cleanupCommand(opts) {
|
|
50
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
51
|
+
if (!fs.existsSync(dir)) {
|
|
52
|
+
console.log(chalk_1.default.yellow(' No .trickle/ directory found.'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const retainDays = opts.retainDays ? parseInt(opts.retainDays, 10) : 7;
|
|
56
|
+
const retainLines = opts.retainLines ? parseInt(opts.retainLines, 10) : 0;
|
|
57
|
+
const cutoffMs = Date.now() - retainDays * 24 * 60 * 60 * 1000;
|
|
58
|
+
const dryRun = opts.dryRun || false;
|
|
59
|
+
const jsonlFiles = [
|
|
60
|
+
'observations.jsonl', 'variables.jsonl', 'calltrace.jsonl',
|
|
61
|
+
'queries.jsonl', 'errors.jsonl', 'llm.jsonl', 'agents.jsonl',
|
|
62
|
+
'mcp.jsonl', 'logs.jsonl', 'console.jsonl', 'traces.jsonl',
|
|
63
|
+
'alerts.jsonl', 'profile.jsonl',
|
|
64
|
+
];
|
|
65
|
+
let totalBefore = 0;
|
|
66
|
+
let totalAfter = 0;
|
|
67
|
+
let totalLinesRemoved = 0;
|
|
68
|
+
let filesProcessed = 0;
|
|
69
|
+
const details = [];
|
|
70
|
+
for (const file of jsonlFiles) {
|
|
71
|
+
const filePath = path.join(dir, file);
|
|
72
|
+
if (!fs.existsSync(filePath))
|
|
73
|
+
continue;
|
|
74
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
75
|
+
const beforeSize = Buffer.byteLength(content);
|
|
76
|
+
totalBefore += beforeSize;
|
|
77
|
+
const lines = content.split('\n').filter(Boolean);
|
|
78
|
+
let kept;
|
|
79
|
+
if (retainLines > 0) {
|
|
80
|
+
// Keep only the last N lines
|
|
81
|
+
kept = lines.slice(-retainLines);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Keep lines with timestamp newer than cutoff
|
|
85
|
+
kept = lines.filter(line => {
|
|
86
|
+
try {
|
|
87
|
+
const obj = JSON.parse(line);
|
|
88
|
+
const ts = obj.timestamp || 0;
|
|
89
|
+
// If timestamp is in seconds (< 2000000000), convert to ms
|
|
90
|
+
const tsMs = ts < 2_000_000_000 ? ts * 1000 : ts;
|
|
91
|
+
// Keep if no timestamp (can't determine age) or if newer than cutoff
|
|
92
|
+
return !ts || tsMs > cutoffMs;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return true; // Keep unparseable lines
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const removed = lines.length - kept.length;
|
|
100
|
+
totalLinesRemoved += removed;
|
|
101
|
+
const newContent = kept.length > 0 ? kept.join('\n') + '\n' : '';
|
|
102
|
+
const afterSize = Buffer.byteLength(newContent);
|
|
103
|
+
totalAfter += afterSize;
|
|
104
|
+
filesProcessed++;
|
|
105
|
+
details.push({ file, before: beforeSize, after: afterSize, removed });
|
|
106
|
+
if (!dryRun && removed > 0) {
|
|
107
|
+
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Also clean up snapshot directory if it exists and is old
|
|
111
|
+
const snapshotDir = path.join(dir, 'snapshot');
|
|
112
|
+
if (fs.existsSync(snapshotDir)) {
|
|
113
|
+
try {
|
|
114
|
+
const stat = fs.statSync(snapshotDir);
|
|
115
|
+
if (stat.mtimeMs < cutoffMs && !dryRun) {
|
|
116
|
+
fs.rmSync(snapshotDir, { recursive: true });
|
|
117
|
+
details.push({ file: 'snapshot/', before: 0, after: 0, removed: -1 });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch { }
|
|
121
|
+
}
|
|
122
|
+
// Clean up CSV export directory
|
|
123
|
+
const csvDir = path.join(dir, 'csv');
|
|
124
|
+
if (fs.existsSync(csvDir) && !dryRun) {
|
|
125
|
+
try {
|
|
126
|
+
const stat = fs.statSync(csvDir);
|
|
127
|
+
if (stat.mtimeMs < cutoffMs) {
|
|
128
|
+
fs.rmSync(csvDir, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch { }
|
|
132
|
+
}
|
|
133
|
+
const result = {
|
|
134
|
+
filesProcessed,
|
|
135
|
+
linesRemoved: totalLinesRemoved,
|
|
136
|
+
bytesBefore: totalBefore,
|
|
137
|
+
bytesAfter: totalAfter,
|
|
138
|
+
bytesSaved: totalBefore - totalAfter,
|
|
139
|
+
};
|
|
140
|
+
if (opts.json) {
|
|
141
|
+
console.log(JSON.stringify({ ...result, details, dryRun, retainDays }, null, 2));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log(chalk_1.default.bold(` trickle cleanup${dryRun ? ' (dry run)' : ''}`));
|
|
146
|
+
console.log(chalk_1.default.gray(' ' + '─'.repeat(50)));
|
|
147
|
+
console.log(` Retention: ${retainLines > 0 ? `last ${retainLines} lines per file` : `${retainDays} days`}`);
|
|
148
|
+
console.log(` Files scanned: ${filesProcessed}`);
|
|
149
|
+
if (totalLinesRemoved === 0) {
|
|
150
|
+
console.log(chalk_1.default.green(' No data to prune — all data is within retention window.'));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.log(` Lines removed: ${chalk_1.default.yellow(String(totalLinesRemoved))}`);
|
|
154
|
+
console.log(` Space: ${formatBytes(totalBefore)} → ${formatBytes(totalAfter)} (${chalk_1.default.green('saved ' + formatBytes(result.bytesSaved))})`);
|
|
155
|
+
if (dryRun) {
|
|
156
|
+
console.log(chalk_1.default.yellow('\n Dry run — no files modified. Remove --dry-run to apply.'));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
for (const d of details.filter(d => d.removed > 0)) {
|
|
160
|
+
console.log(chalk_1.default.gray(` ${d.file}: ${d.removed} lines removed`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
console.log(chalk_1.default.gray(' ' + '─'.repeat(50)));
|
|
165
|
+
console.log('');
|
|
166
|
+
}
|
|
167
|
+
function formatBytes(bytes) {
|
|
168
|
+
if (bytes >= 1_000_000)
|
|
169
|
+
return (bytes / 1_000_000).toFixed(1) + 'MB';
|
|
170
|
+
if (bytes >= 1_000)
|
|
171
|
+
return (bytes / 1_000).toFixed(1) + 'KB';
|
|
172
|
+
return bytes + 'B';
|
|
173
|
+
}
|
|
@@ -147,10 +147,46 @@ function costReportCommand(opts) {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
+
// Model tier analysis — classify models into frontier/standard/mini tiers
|
|
151
|
+
// Ordered longest-first to avoid substring matches (gpt-4o-mini before gpt-4o)
|
|
152
|
+
const TIER_RULES = [
|
|
153
|
+
['gpt-4o-mini', 'mini'], ['gpt-4-turbo', 'frontier'], ['gpt-4o', 'standard'], ['gpt-4', 'frontier'],
|
|
154
|
+
['gpt-3.5-turbo', 'mini'], ['o1-mini', 'standard'], ['o1-pro', 'frontier'], ['o1', 'frontier'],
|
|
155
|
+
['o3-mini', 'standard'], ['o3', 'frontier'], ['o4-mini', 'standard'],
|
|
156
|
+
['claude-opus', 'frontier'], ['claude-sonnet', 'standard'], ['claude-haiku', 'mini'],
|
|
157
|
+
['gemini-2.5-flash-lite', 'mini'], ['gemini-2.5-flash', 'standard'], ['gemini-2.5-pro', 'frontier'],
|
|
158
|
+
['gemini-2.0-flash', 'mini'], ['gemini-1.5-pro', 'frontier'], ['gemini-1.5-flash', 'mini'],
|
|
159
|
+
];
|
|
160
|
+
function classifyTier(model) {
|
|
161
|
+
for (const [pattern, tier] of TIER_RULES) {
|
|
162
|
+
if (model.includes(pattern))
|
|
163
|
+
return tier;
|
|
164
|
+
}
|
|
165
|
+
if (model.includes('mini') || model.includes('lite') || model.includes('haiku') || model.includes('flash'))
|
|
166
|
+
return 'mini';
|
|
167
|
+
if (model.includes('pro') || model.includes('opus') || model.includes('turbo'))
|
|
168
|
+
return 'frontier';
|
|
169
|
+
return 'standard';
|
|
170
|
+
}
|
|
171
|
+
const byTier = {};
|
|
172
|
+
for (const c of calls) {
|
|
173
|
+
const tier = classifyTier(c.model || '');
|
|
174
|
+
if (!byTier[tier])
|
|
175
|
+
byTier[tier] = { calls: 0, tokens: 0, cost: 0, avgLatency: 0, errors: 0 };
|
|
176
|
+
byTier[tier].calls++;
|
|
177
|
+
byTier[tier].tokens += c.totalTokens || 0;
|
|
178
|
+
byTier[tier].cost += c.estimatedCostUsd || 0;
|
|
179
|
+
byTier[tier].avgLatency += c.durationMs || 0;
|
|
180
|
+
if (c.error)
|
|
181
|
+
byTier[tier].errors++;
|
|
182
|
+
}
|
|
183
|
+
for (const t of Object.values(byTier)) {
|
|
184
|
+
t.avgLatency = t.calls > 0 ? t.avgLatency / t.calls : 0;
|
|
185
|
+
}
|
|
150
186
|
if (opts.json) {
|
|
151
187
|
console.log(JSON.stringify({
|
|
152
188
|
summary: { totalCost, totalTokens, totalInputTokens, totalOutputTokens, totalCalls: calls.length, totalDurationMs: totalDuration, errors: errorCount, monthlyProjection },
|
|
153
|
-
byProvider, byModel,
|
|
189
|
+
byProvider, byModel, byTier,
|
|
154
190
|
...(Object.keys(byAgent).length > 0 ? { byAgent } : {}),
|
|
155
191
|
}, null, 2));
|
|
156
192
|
return;
|
|
@@ -195,6 +231,27 @@ function costReportCommand(opts) {
|
|
|
195
231
|
}
|
|
196
232
|
// Top costly calls
|
|
197
233
|
const costlyCalls = calls.filter(c => c.estimatedCostUsd > 0).sort((a, b) => b.estimatedCostUsd - a.estimatedCostUsd).slice(0, 5);
|
|
234
|
+
// By tier
|
|
235
|
+
if (Object.keys(byTier).length > 1) {
|
|
236
|
+
console.log(chalk_1.default.gray('\n ' + '─'.repeat(60)));
|
|
237
|
+
console.log(chalk_1.default.bold(' Model Tier Analysis'));
|
|
238
|
+
const tierOrder = ['frontier', 'standard', 'mini'];
|
|
239
|
+
const tierLabels = { frontier: '🔴 Frontier', standard: '🟡 Standard', mini: '🟢 Mini' };
|
|
240
|
+
for (const tier of tierOrder) {
|
|
241
|
+
const data = byTier[tier];
|
|
242
|
+
if (!data)
|
|
243
|
+
continue;
|
|
244
|
+
const pct = totalCost > 0 ? ((data.cost / totalCost) * 100).toFixed(0) : '0';
|
|
245
|
+
const callPct = calls.length > 0 ? ((data.calls / calls.length) * 100).toFixed(0) : '0';
|
|
246
|
+
const errRate = data.calls > 0 ? ((data.errors / data.calls) * 100).toFixed(0) : '0';
|
|
247
|
+
console.log(` ${(tierLabels[tier] || tier).padEnd(16)} $${data.cost.toFixed(4).padEnd(10)} ${chalk_1.default.gray(pct + '% cost')} ${data.calls} calls (${callPct}%) avg ${data.avgLatency.toFixed(0)}ms ${data.errors > 0 ? chalk_1.default.red(errRate + '% err') : chalk_1.default.green('0% err')}`);
|
|
248
|
+
}
|
|
249
|
+
// Tier optimization suggestion
|
|
250
|
+
const frontierPct = byTier.frontier ? (byTier.frontier.calls / calls.length) * 100 : 0;
|
|
251
|
+
if (frontierPct > 50) {
|
|
252
|
+
console.log(chalk_1.default.yellow(` 💡 ${frontierPct.toFixed(0)}% of calls use frontier models. Consider routing simple tasks to mini tier for ~75% savings.`));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
198
255
|
// By agent (if agent data exists)
|
|
199
256
|
if (Object.keys(byAgent).length > 0) {
|
|
200
257
|
console.log(chalk_1.default.gray('\n ' + '─'.repeat(60)));
|
package/dist/index.js
CHANGED
|
@@ -920,6 +920,18 @@ program
|
|
|
920
920
|
const { whyCommand } = await Promise.resolve().then(() => __importStar(require("./commands/why")));
|
|
921
921
|
whyCommand(query, opts);
|
|
922
922
|
});
|
|
923
|
+
// trickle cleanup
|
|
924
|
+
program
|
|
925
|
+
.command("cleanup")
|
|
926
|
+
.description("Prune old .trickle/ data — manage retention for heavy workloads")
|
|
927
|
+
.option("--retain-days <days>", "Keep data from last N days (default: 7)")
|
|
928
|
+
.option("--retain-lines <lines>", "Keep only last N lines per file (overrides --retain-days)")
|
|
929
|
+
.option("--dry-run", "Show what would be removed without modifying files")
|
|
930
|
+
.option("--json", "Output structured JSON")
|
|
931
|
+
.action(async (opts) => {
|
|
932
|
+
const { cleanupCommand } = await Promise.resolve().then(() => __importStar(require("./commands/cleanup")));
|
|
933
|
+
cleanupCommand(opts);
|
|
934
|
+
});
|
|
923
935
|
// trickle eval
|
|
924
936
|
program
|
|
925
937
|
.command("eval")
|
package/package.json
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trickle cleanup — Smart data management for .trickle/ files.
|
|
3
|
+
*
|
|
4
|
+
* Prunes old data, compacts JSONL files, and manages retention.
|
|
5
|
+
* Essential for heavy workloads where agents produce 10-100x more data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
|
|
12
|
+
interface CleanupResult {
|
|
13
|
+
filesProcessed: number;
|
|
14
|
+
linesRemoved: number;
|
|
15
|
+
bytesBefore: number;
|
|
16
|
+
bytesAfter: number;
|
|
17
|
+
bytesSaved: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function cleanupCommand(opts: {
|
|
21
|
+
retainDays?: string;
|
|
22
|
+
retainLines?: string;
|
|
23
|
+
dryRun?: boolean;
|
|
24
|
+
json?: boolean;
|
|
25
|
+
}): void {
|
|
26
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(dir)) {
|
|
29
|
+
console.log(chalk.yellow(' No .trickle/ directory found.'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const retainDays = opts.retainDays ? parseInt(opts.retainDays, 10) : 7;
|
|
34
|
+
const retainLines = opts.retainLines ? parseInt(opts.retainLines, 10) : 0;
|
|
35
|
+
const cutoffMs = Date.now() - retainDays * 24 * 60 * 60 * 1000;
|
|
36
|
+
const dryRun = opts.dryRun || false;
|
|
37
|
+
|
|
38
|
+
const jsonlFiles = [
|
|
39
|
+
'observations.jsonl', 'variables.jsonl', 'calltrace.jsonl',
|
|
40
|
+
'queries.jsonl', 'errors.jsonl', 'llm.jsonl', 'agents.jsonl',
|
|
41
|
+
'mcp.jsonl', 'logs.jsonl', 'console.jsonl', 'traces.jsonl',
|
|
42
|
+
'alerts.jsonl', 'profile.jsonl',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
let totalBefore = 0;
|
|
46
|
+
let totalAfter = 0;
|
|
47
|
+
let totalLinesRemoved = 0;
|
|
48
|
+
let filesProcessed = 0;
|
|
49
|
+
|
|
50
|
+
const details: Array<{ file: string; before: number; after: number; removed: number }> = [];
|
|
51
|
+
|
|
52
|
+
for (const file of jsonlFiles) {
|
|
53
|
+
const filePath = path.join(dir, file);
|
|
54
|
+
if (!fs.existsSync(filePath)) continue;
|
|
55
|
+
|
|
56
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
57
|
+
const beforeSize = Buffer.byteLength(content);
|
|
58
|
+
totalBefore += beforeSize;
|
|
59
|
+
|
|
60
|
+
const lines = content.split('\n').filter(Boolean);
|
|
61
|
+
let kept: string[];
|
|
62
|
+
|
|
63
|
+
if (retainLines > 0) {
|
|
64
|
+
// Keep only the last N lines
|
|
65
|
+
kept = lines.slice(-retainLines);
|
|
66
|
+
} else {
|
|
67
|
+
// Keep lines with timestamp newer than cutoff
|
|
68
|
+
kept = lines.filter(line => {
|
|
69
|
+
try {
|
|
70
|
+
const obj = JSON.parse(line);
|
|
71
|
+
const ts = obj.timestamp || 0;
|
|
72
|
+
// If timestamp is in seconds (< 2000000000), convert to ms
|
|
73
|
+
const tsMs = ts < 2_000_000_000 ? ts * 1000 : ts;
|
|
74
|
+
// Keep if no timestamp (can't determine age) or if newer than cutoff
|
|
75
|
+
return !ts || tsMs > cutoffMs;
|
|
76
|
+
} catch {
|
|
77
|
+
return true; // Keep unparseable lines
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const removed = lines.length - kept.length;
|
|
83
|
+
totalLinesRemoved += removed;
|
|
84
|
+
|
|
85
|
+
const newContent = kept.length > 0 ? kept.join('\n') + '\n' : '';
|
|
86
|
+
const afterSize = Buffer.byteLength(newContent);
|
|
87
|
+
totalAfter += afterSize;
|
|
88
|
+
filesProcessed++;
|
|
89
|
+
|
|
90
|
+
details.push({ file, before: beforeSize, after: afterSize, removed });
|
|
91
|
+
|
|
92
|
+
if (!dryRun && removed > 0) {
|
|
93
|
+
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Also clean up snapshot directory if it exists and is old
|
|
98
|
+
const snapshotDir = path.join(dir, 'snapshot');
|
|
99
|
+
if (fs.existsSync(snapshotDir)) {
|
|
100
|
+
try {
|
|
101
|
+
const stat = fs.statSync(snapshotDir);
|
|
102
|
+
if (stat.mtimeMs < cutoffMs && !dryRun) {
|
|
103
|
+
fs.rmSync(snapshotDir, { recursive: true });
|
|
104
|
+
details.push({ file: 'snapshot/', before: 0, after: 0, removed: -1 });
|
|
105
|
+
}
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Clean up CSV export directory
|
|
110
|
+
const csvDir = path.join(dir, 'csv');
|
|
111
|
+
if (fs.existsSync(csvDir) && !dryRun) {
|
|
112
|
+
try {
|
|
113
|
+
const stat = fs.statSync(csvDir);
|
|
114
|
+
if (stat.mtimeMs < cutoffMs) {
|
|
115
|
+
fs.rmSync(csvDir, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result: CleanupResult = {
|
|
121
|
+
filesProcessed,
|
|
122
|
+
linesRemoved: totalLinesRemoved,
|
|
123
|
+
bytesBefore: totalBefore,
|
|
124
|
+
bytesAfter: totalAfter,
|
|
125
|
+
bytesSaved: totalBefore - totalAfter,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (opts.json) {
|
|
129
|
+
console.log(JSON.stringify({ ...result, details, dryRun, retainDays }, null, 2));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log(chalk.bold(` trickle cleanup${dryRun ? ' (dry run)' : ''}`));
|
|
135
|
+
console.log(chalk.gray(' ' + '─'.repeat(50)));
|
|
136
|
+
console.log(` Retention: ${retainLines > 0 ? `last ${retainLines} lines per file` : `${retainDays} days`}`);
|
|
137
|
+
console.log(` Files scanned: ${filesProcessed}`);
|
|
138
|
+
|
|
139
|
+
if (totalLinesRemoved === 0) {
|
|
140
|
+
console.log(chalk.green(' No data to prune — all data is within retention window.'));
|
|
141
|
+
} else {
|
|
142
|
+
console.log(` Lines removed: ${chalk.yellow(String(totalLinesRemoved))}`);
|
|
143
|
+
console.log(` Space: ${formatBytes(totalBefore)} → ${formatBytes(totalAfter)} (${chalk.green('saved ' + formatBytes(result.bytesSaved))})`);
|
|
144
|
+
|
|
145
|
+
if (dryRun) {
|
|
146
|
+
console.log(chalk.yellow('\n Dry run — no files modified. Remove --dry-run to apply.'));
|
|
147
|
+
} else {
|
|
148
|
+
for (const d of details.filter(d => d.removed > 0)) {
|
|
149
|
+
console.log(chalk.gray(` ${d.file}: ${d.removed} lines removed`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(chalk.gray(' ' + '─'.repeat(50)));
|
|
155
|
+
console.log('');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function formatBytes(bytes: number): string {
|
|
159
|
+
if (bytes >= 1_000_000) return (bytes / 1_000_000).toFixed(1) + 'MB';
|
|
160
|
+
if (bytes >= 1_000) return (bytes / 1_000).toFixed(1) + 'KB';
|
|
161
|
+
return bytes + 'B';
|
|
162
|
+
}
|
|
@@ -122,10 +122,44 @@ export function costReportCommand(opts: { json?: boolean; budget?: string }): vo
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// Model tier analysis — classify models into frontier/standard/mini tiers
|
|
126
|
+
// Ordered longest-first to avoid substring matches (gpt-4o-mini before gpt-4o)
|
|
127
|
+
const TIER_RULES: Array<[string, string]> = [
|
|
128
|
+
['gpt-4o-mini', 'mini'], ['gpt-4-turbo', 'frontier'], ['gpt-4o', 'standard'], ['gpt-4', 'frontier'],
|
|
129
|
+
['gpt-3.5-turbo', 'mini'], ['o1-mini', 'standard'], ['o1-pro', 'frontier'], ['o1', 'frontier'],
|
|
130
|
+
['o3-mini', 'standard'], ['o3', 'frontier'], ['o4-mini', 'standard'],
|
|
131
|
+
['claude-opus', 'frontier'], ['claude-sonnet', 'standard'], ['claude-haiku', 'mini'],
|
|
132
|
+
['gemini-2.5-flash-lite', 'mini'], ['gemini-2.5-flash', 'standard'], ['gemini-2.5-pro', 'frontier'],
|
|
133
|
+
['gemini-2.0-flash', 'mini'], ['gemini-1.5-pro', 'frontier'], ['gemini-1.5-flash', 'mini'],
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
function classifyTier(model: string): string {
|
|
137
|
+
for (const [pattern, tier] of TIER_RULES) {
|
|
138
|
+
if (model.includes(pattern)) return tier;
|
|
139
|
+
}
|
|
140
|
+
if (model.includes('mini') || model.includes('lite') || model.includes('haiku') || model.includes('flash')) return 'mini';
|
|
141
|
+
if (model.includes('pro') || model.includes('opus') || model.includes('turbo')) return 'frontier';
|
|
142
|
+
return 'standard';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const byTier: Record<string, { calls: number; tokens: number; cost: number; avgLatency: number; errors: number }> = {};
|
|
146
|
+
for (const c of calls) {
|
|
147
|
+
const tier = classifyTier(c.model || '');
|
|
148
|
+
if (!byTier[tier]) byTier[tier] = { calls: 0, tokens: 0, cost: 0, avgLatency: 0, errors: 0 };
|
|
149
|
+
byTier[tier].calls++;
|
|
150
|
+
byTier[tier].tokens += c.totalTokens || 0;
|
|
151
|
+
byTier[tier].cost += c.estimatedCostUsd || 0;
|
|
152
|
+
byTier[tier].avgLatency += c.durationMs || 0;
|
|
153
|
+
if (c.error) byTier[tier].errors++;
|
|
154
|
+
}
|
|
155
|
+
for (const t of Object.values(byTier)) {
|
|
156
|
+
t.avgLatency = t.calls > 0 ? t.avgLatency / t.calls : 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
125
159
|
if (opts.json) {
|
|
126
160
|
console.log(JSON.stringify({
|
|
127
161
|
summary: { totalCost, totalTokens, totalInputTokens, totalOutputTokens, totalCalls: calls.length, totalDurationMs: totalDuration, errors: errorCount, monthlyProjection },
|
|
128
|
-
byProvider, byModel,
|
|
162
|
+
byProvider, byModel, byTier,
|
|
129
163
|
...(Object.keys(byAgent).length > 0 ? { byAgent } : {}),
|
|
130
164
|
}, null, 2));
|
|
131
165
|
return;
|
|
@@ -175,6 +209,27 @@ export function costReportCommand(opts: { json?: boolean; budget?: string }): vo
|
|
|
175
209
|
|
|
176
210
|
// Top costly calls
|
|
177
211
|
const costlyCalls = calls.filter(c => c.estimatedCostUsd > 0).sort((a, b) => b.estimatedCostUsd - a.estimatedCostUsd).slice(0, 5);
|
|
212
|
+
// By tier
|
|
213
|
+
if (Object.keys(byTier).length > 1) {
|
|
214
|
+
console.log(chalk.gray('\n ' + '─'.repeat(60)));
|
|
215
|
+
console.log(chalk.bold(' Model Tier Analysis'));
|
|
216
|
+
const tierOrder = ['frontier', 'standard', 'mini'];
|
|
217
|
+
const tierLabels: Record<string, string> = { frontier: '🔴 Frontier', standard: '🟡 Standard', mini: '🟢 Mini' };
|
|
218
|
+
for (const tier of tierOrder) {
|
|
219
|
+
const data = byTier[tier];
|
|
220
|
+
if (!data) continue;
|
|
221
|
+
const pct = totalCost > 0 ? ((data.cost / totalCost) * 100).toFixed(0) : '0';
|
|
222
|
+
const callPct = calls.length > 0 ? ((data.calls / calls.length) * 100).toFixed(0) : '0';
|
|
223
|
+
const errRate = data.calls > 0 ? ((data.errors / data.calls) * 100).toFixed(0) : '0';
|
|
224
|
+
console.log(` ${(tierLabels[tier] || tier).padEnd(16)} $${data.cost.toFixed(4).padEnd(10)} ${chalk.gray(pct + '% cost')} ${data.calls} calls (${callPct}%) avg ${data.avgLatency.toFixed(0)}ms ${data.errors > 0 ? chalk.red(errRate + '% err') : chalk.green('0% err')}`);
|
|
225
|
+
}
|
|
226
|
+
// Tier optimization suggestion
|
|
227
|
+
const frontierPct = byTier.frontier ? (byTier.frontier.calls / calls.length) * 100 : 0;
|
|
228
|
+
if (frontierPct > 50) {
|
|
229
|
+
console.log(chalk.yellow(` 💡 ${frontierPct.toFixed(0)}% of calls use frontier models. Consider routing simple tasks to mini tier for ~75% savings.`));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
178
233
|
// By agent (if agent data exists)
|
|
179
234
|
if (Object.keys(byAgent).length > 0) {
|
|
180
235
|
console.log(chalk.gray('\n ' + '─'.repeat(60)));
|
package/src/index.ts
CHANGED
|
@@ -953,6 +953,19 @@ program
|
|
|
953
953
|
whyCommand(query, opts);
|
|
954
954
|
});
|
|
955
955
|
|
|
956
|
+
// trickle cleanup
|
|
957
|
+
program
|
|
958
|
+
.command("cleanup")
|
|
959
|
+
.description("Prune old .trickle/ data — manage retention for heavy workloads")
|
|
960
|
+
.option("--retain-days <days>", "Keep data from last N days (default: 7)")
|
|
961
|
+
.option("--retain-lines <lines>", "Keep only last N lines per file (overrides --retain-days)")
|
|
962
|
+
.option("--dry-run", "Show what would be removed without modifying files")
|
|
963
|
+
.option("--json", "Output structured JSON")
|
|
964
|
+
.action(async (opts) => {
|
|
965
|
+
const { cleanupCommand } = await import("./commands/cleanup");
|
|
966
|
+
cleanupCommand(opts);
|
|
967
|
+
});
|
|
968
|
+
|
|
956
969
|
// trickle eval
|
|
957
970
|
program
|
|
958
971
|
.command("eval")
|