tokentrace 0.6.0 → 0.7.0
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/CHANGELOG.md +22 -1
- package/README.md +24 -2
- package/app/api/settings/route.ts +3 -2
- package/app/optimisation/page.tsx +55 -2
- package/app/page.tsx +96 -1
- package/app/projects/page.tsx +51 -0
- package/app/sessions/page.tsx +55 -0
- package/bin/tokentrace.js +11 -0
- package/components/settings-panel.tsx +73 -9
- package/dist/runtime/digest.mjs +2227 -0
- package/dist/runtime/doctor.mjs +3 -0
- package/dist/runtime/insights.mjs +572 -73
- package/dist/runtime/scan.mjs +23 -2
- package/docs/assets/doctor-0.6.0.png +0 -0
- package/docs/assets/overview-0.6.0.png +0 -0
- package/docs/assets/overview-0.7.0.png +0 -0
- package/docs/assets/projects-0.7.0.png +0 -0
- package/docs/assets/sessions-0.7.0.png +0 -0
- package/docs/assets/settings-guardrails-0.7.0.png +0 -0
- package/docs/assets/settings-package-trust-0.6.0.png +0 -0
- package/docs/assets/usage-intelligence-0.7.0.png +0 -0
- package/package.json +1 -1
- package/scripts/build-cli-runtime.mjs +1 -0
- package/scripts/digest.ts +27 -0
- package/scripts/insights.ts +2 -1
- package/scripts/smoke-cli.mjs +3 -0
- package/scripts/smoke-packed-install.mjs +1 -1
- package/src/db/settings.ts +31 -2
- package/src/lib/analytics.ts +29 -0
- package/src/lib/daily-digest.ts +101 -0
- package/src/lib/project-signals.ts +139 -0
- package/src/lib/recommendations.ts +36 -0
- package/src/lib/review-queue.ts +222 -0
- package/src/lib/session-comparison.ts +94 -0
- package/src/lib/usage-guardrails.ts +113 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to TokenTrace are documented here.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## [0.7.0] - 2026-05-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- 0.7.0 Usage Intelligence roadmap for local guardrails, savings review, session comparison, project intelligence, and daily digest work.
|
|
10
|
+
- Local monthly cost and token guardrails stored in Settings.
|
|
11
|
+
- Overview Monthly Guardrails panel showing month-to-date local usage against configured limits.
|
|
12
|
+
- Recommendation rules for monthly guardrails that are near or over limit.
|
|
13
|
+
- Evidence-backed Review Queue on Insights, ranked from guardrails, unknown cost repair, high-impact sessions, dominant projects, model review, and cache reuse.
|
|
14
|
+
- `tokentrace insights --json` now includes the Review Queue for local automation.
|
|
15
|
+
- `tokentrace digest` and `tokentrace digest --json` for current-month local usage, guardrails, top review item, unknown-cost count, top project, and latest scan status.
|
|
16
|
+
- Session Comparison Flags on Sessions to highlight token and cost outliers compared with matching project, tool, and primary-model peers.
|
|
17
|
+
- Project Signals on Projects for dominant usage, unknown cost, estimated-token confidence, and model concentration patterns.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Renamed the Insights page header to Usage Intelligence to match the 0.7.0 product theme.
|
|
22
|
+
- Refreshed README screenshots for Overview, Usage Intelligence, Sessions, Projects, and local guardrail Settings using public-safe synthetic local data.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Increased the forced packed-install smoke test timeout so native SQLite dependency installation is not killed on slower machines.
|
|
6
27
|
|
|
7
28
|
## [0.6.0] - 2026-05-10
|
|
8
29
|
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Local-first analytics for AI CLI usage. TokenTrace scans local CLI logs, normali
|
|
|
8
8
|
|
|
9
9
|
TokenTrace is designed for local development machines first, with macOS-oriented defaults. It does not require a cloud account and does not send telemetry or logs anywhere.
|
|
10
10
|
|
|
11
|
-

|
|
11
|
+

|
|
12
12
|
|
|
13
13
|
## Start In Seconds
|
|
14
14
|
|
|
@@ -37,6 +37,8 @@ tokentrace serve --port 3210 --no-open
|
|
|
37
37
|
tokentrace scan # Scan local AI CLI usage logs
|
|
38
38
|
tokentrace doctor --json
|
|
39
39
|
# Inspect scan health and repair recommendations
|
|
40
|
+
tokentrace digest --json
|
|
41
|
+
# Print current-month local usage digest
|
|
40
42
|
tokentrace insights --json
|
|
41
43
|
# Print local recommendations as JSON
|
|
42
44
|
tokentrace status --json
|
|
@@ -82,7 +84,7 @@ npm run db:migrate # Create/update local SQLite tables
|
|
|
82
84
|
npm run db:seed # Seed editable provider/model prices
|
|
83
85
|
npm run reset # Clear imported data and scan history
|
|
84
86
|
npm test # Run parser and cost tests
|
|
85
|
-
npm run verify # Run Vitest and
|
|
87
|
+
npm run verify # Run Vitest, TypeScript, and ESLint checks
|
|
86
88
|
npm run package:test # Verify, build, and dry-run the npm package
|
|
87
89
|
npm run package:inspect
|
|
88
90
|
# Check package transparency guardrails
|
|
@@ -129,6 +131,10 @@ Default discovery checks these locations when present:
|
|
|
129
131
|
|
|
130
132
|
Use **Settings** in the dashboard to add custom folders, toggle raw message storage, and trigger scans. Use **Doctor**, **Discovery**, **Parser Debug**, and **Raw Data** to inspect discovered files, parser decisions, warnings, failures, extracted metadata, and confidence levels.
|
|
131
133
|
|
|
134
|
+
Settings also supports optional local monthly usage guardrails. Set a cost
|
|
135
|
+
limit, token limit, or both, and Overview will show month-to-date progress from
|
|
136
|
+
imported local CLI usage.
|
|
137
|
+
|
|
132
138
|
## Ingestion Architecture
|
|
133
139
|
|
|
134
140
|
TokenTrace's primary ingestion architecture is direct local filesystem ingestion:
|
|
@@ -191,6 +197,22 @@ Codex CLI status-line integration is intentionally deferred until its status-lin
|
|
|
191
197
|
|
|
192
198
|
## Screenshots
|
|
193
199
|
|
|
200
|
+
Usage Intelligence views from `0.7.0`:
|
|
201
|
+
|
|
202
|
+

|
|
203
|
+
|
|
204
|
+

|
|
205
|
+
|
|
206
|
+

|
|
207
|
+
|
|
208
|
+

|
|
209
|
+
|
|
210
|
+

|
|
211
|
+
|
|
212
|
+
Stable Daily Tool views from `0.6.0`:
|
|
213
|
+
|
|
214
|
+

|
|
215
|
+
|
|
194
216
|
CLI startup and help:
|
|
195
217
|
|
|
196
218
|

|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { getDatabasePath } from "@/src/db/client";
|
|
3
|
-
import { getAppSettings, saveAppSettings } from "@/src/db/settings";
|
|
3
|
+
import { getAppSettings, normalizeUsageGuardrails, saveAppSettings } from "@/src/db/settings";
|
|
4
4
|
|
|
5
5
|
export const dynamic = "force-dynamic";
|
|
6
6
|
|
|
@@ -18,7 +18,8 @@ export async function PUT(request: Request) {
|
|
|
18
18
|
: [];
|
|
19
19
|
const saved = saveAppSettings({
|
|
20
20
|
customFolders,
|
|
21
|
-
storeRawMessageContent: Boolean(body.storeRawMessageContent)
|
|
21
|
+
storeRawMessageContent: Boolean(body.storeRawMessageContent),
|
|
22
|
+
usageGuardrails: normalizeUsageGuardrails(body.usageGuardrails)
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
return NextResponse.json(saved);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
1
2
|
import { AlertTriangle, CheckCircle2, Info, Lightbulb } from "lucide-react";
|
|
2
3
|
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
3
5
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
6
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
4
7
|
import { PageHeader } from "@/components/ui/typography";
|
|
5
8
|
import { getAnalyticsData } from "@/src/lib/analytics";
|
|
6
9
|
|
|
@@ -24,10 +27,60 @@ export default function OptimisationPage() {
|
|
|
24
27
|
return (
|
|
25
28
|
<div className="space-y-6">
|
|
26
29
|
<PageHeader
|
|
27
|
-
title="
|
|
28
|
-
description="Deterministic
|
|
30
|
+
title="Usage Intelligence"
|
|
31
|
+
description="Deterministic local review queue based on guardrails, repair work, and usage impact."
|
|
29
32
|
/>
|
|
30
33
|
|
|
34
|
+
<Card>
|
|
35
|
+
<CardHeader className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
36
|
+
<div>
|
|
37
|
+
<CardTitle>Review Queue</CardTitle>
|
|
38
|
+
<CardDescription>
|
|
39
|
+
Evidence-backed next actions ordered by guardrails, repair work, and usage impact.
|
|
40
|
+
</CardDescription>
|
|
41
|
+
</div>
|
|
42
|
+
<Button asChild variant="outline" size="sm">
|
|
43
|
+
<Link href="/sessions">Open sessions</Link>
|
|
44
|
+
</Button>
|
|
45
|
+
</CardHeader>
|
|
46
|
+
<CardContent className="table-scroll">
|
|
47
|
+
<Table>
|
|
48
|
+
<TableHeader>
|
|
49
|
+
<TableRow>
|
|
50
|
+
<TableHead>Priority</TableHead>
|
|
51
|
+
<TableHead>Category</TableHead>
|
|
52
|
+
<TableHead>Review item</TableHead>
|
|
53
|
+
<TableHead>Impact</TableHead>
|
|
54
|
+
<TableHead>Action</TableHead>
|
|
55
|
+
</TableRow>
|
|
56
|
+
</TableHeader>
|
|
57
|
+
<TableBody>
|
|
58
|
+
{data.reviewQueue.map((item) => (
|
|
59
|
+
<TableRow key={item.id}>
|
|
60
|
+
<TableCell>
|
|
61
|
+
<Badge variant={severityVariant[item.severity]}>{item.severity}</Badge>
|
|
62
|
+
</TableCell>
|
|
63
|
+
<TableCell className="capitalize">{item.category.replace("-", " ")}</TableCell>
|
|
64
|
+
<TableCell className="max-w-xl">
|
|
65
|
+
<div className="font-medium">{item.title}</div>
|
|
66
|
+
<div className="mt-1 text-xs leading-relaxed text-muted-foreground">{item.evidence}</div>
|
|
67
|
+
</TableCell>
|
|
68
|
+
<TableCell>
|
|
69
|
+
<div className="text-sm font-medium">{item.impactValue}</div>
|
|
70
|
+
<div className="text-xs text-muted-foreground">{item.impactLabel}</div>
|
|
71
|
+
</TableCell>
|
|
72
|
+
<TableCell>
|
|
73
|
+
<Link href={item.href} className="font-medium text-primary underline-offset-4 hover:underline">
|
|
74
|
+
{item.action}
|
|
75
|
+
</Link>
|
|
76
|
+
</TableCell>
|
|
77
|
+
</TableRow>
|
|
78
|
+
))}
|
|
79
|
+
</TableBody>
|
|
80
|
+
</Table>
|
|
81
|
+
</CardContent>
|
|
82
|
+
</Card>
|
|
83
|
+
|
|
31
84
|
<div className="grid gap-4">
|
|
32
85
|
{data.insights.map((insight) => {
|
|
33
86
|
const Icon = severityIcon[insight.severity] ?? CheckCircle2;
|
package/app/page.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
|
-
import { ArrowRight, Coins, Database, MessageSquare, Minus, Sparkles, TrendingDown, TrendingUp } from "lucide-react";
|
|
2
|
+
import { ArrowRight, Coins, Database, Gauge, MessageSquare, Minus, Sparkles, TrendingDown, TrendingUp } from "lucide-react";
|
|
3
3
|
import { RankBarChart } from "@/components/charts/rank-bar-chart";
|
|
4
4
|
import { TrendChart } from "@/components/charts/trend-chart";
|
|
5
5
|
import { PeriodFilter } from "@/components/period-filter";
|
|
@@ -16,6 +16,7 @@ import { buildFirstRunStatus, type FirstRunStatus } from "@/src/lib/first-run-st
|
|
|
16
16
|
import { resolveDateRange } from "@/src/lib/date-range";
|
|
17
17
|
import { formatCurrency, formatTokens, percent } from "@/src/lib/format";
|
|
18
18
|
import { cn } from "@/src/lib/utils";
|
|
19
|
+
import type { UsageGuardrailMetric } from "@/src/lib/usage-guardrails";
|
|
19
20
|
|
|
20
21
|
export const dynamic = "force-dynamic";
|
|
21
22
|
|
|
@@ -127,6 +128,98 @@ function DeltaMetric({
|
|
|
127
128
|
);
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
function guardrailBadgeVariant(status: UsageGuardrailMetric["status"]) {
|
|
132
|
+
if (status === "exceeded") return "destructive";
|
|
133
|
+
if (status === "warning") return "warning";
|
|
134
|
+
if (status === "ok") return "success";
|
|
135
|
+
return "secondary";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function guardrailLabel(status: UsageGuardrailMetric["status"]) {
|
|
139
|
+
if (status === "exceeded") return "exceeded";
|
|
140
|
+
if (status === "warning") return "watch";
|
|
141
|
+
if (status === "ok") return "within limit";
|
|
142
|
+
return "not set";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function GuardrailMetricPanel({
|
|
146
|
+
label,
|
|
147
|
+
metric,
|
|
148
|
+
formatValue
|
|
149
|
+
}: {
|
|
150
|
+
label: string;
|
|
151
|
+
metric: UsageGuardrailMetric;
|
|
152
|
+
formatValue: (value: number | null) => string;
|
|
153
|
+
}) {
|
|
154
|
+
const cappedPercent = Math.min(1, Math.max(0, metric.percent));
|
|
155
|
+
const barClass =
|
|
156
|
+
metric.status === "exceeded"
|
|
157
|
+
? "bg-destructive"
|
|
158
|
+
: metric.status === "warning"
|
|
159
|
+
? "bg-amber-500"
|
|
160
|
+
: "bg-primary";
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className="min-w-0 border-t p-3 first:border-t-0 md:border-l md:border-t-0 md:first:border-l-0">
|
|
164
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
165
|
+
<FieldLabel>{label}</FieldLabel>
|
|
166
|
+
<Badge variant={guardrailBadgeVariant(metric.status)}>{guardrailLabel(metric.status)}</Badge>
|
|
167
|
+
</div>
|
|
168
|
+
<DataValue className="mt-1" size="md">
|
|
169
|
+
{metric.configured ? `${formatValue(metric.used)} / ${formatValue(metric.limit)}` : formatValue(metric.used)}
|
|
170
|
+
</DataValue>
|
|
171
|
+
<div className="mt-3 h-2 overflow-hidden rounded-full bg-muted">
|
|
172
|
+
<div className={cn("h-full rounded-full", barClass)} style={{ width: `${cappedPercent * 100}%` }} />
|
|
173
|
+
</div>
|
|
174
|
+
<div className="mt-2 text-xs text-muted-foreground">
|
|
175
|
+
{metric.configured
|
|
176
|
+
? `${percent(metric.percent)} used, ${formatValue(metric.remaining)} remaining`
|
|
177
|
+
: "Set a local monthly limit in Settings."}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function UsageGuardrailsPanel({
|
|
184
|
+
progress
|
|
185
|
+
}: {
|
|
186
|
+
progress: {
|
|
187
|
+
monthLabel: string;
|
|
188
|
+
cost: UsageGuardrailMetric;
|
|
189
|
+
tokens: UsageGuardrailMetric;
|
|
190
|
+
};
|
|
191
|
+
}) {
|
|
192
|
+
const configured = progress.cost.configured || progress.tokens.configured;
|
|
193
|
+
return (
|
|
194
|
+
<Card>
|
|
195
|
+
<CardHeader className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
196
|
+
<div>
|
|
197
|
+
<CardTitle className="flex items-center gap-2">
|
|
198
|
+
<Gauge className="h-4 w-4 text-primary" />
|
|
199
|
+
Monthly Guardrails
|
|
200
|
+
</CardTitle>
|
|
201
|
+
<CardDescription>
|
|
202
|
+
{configured
|
|
203
|
+
? `Month-to-date checks for ${progress.monthLabel}, based only on imported local CLI usage.`
|
|
204
|
+
: "Optional local limits for monthly cost and tokens."}
|
|
205
|
+
</CardDescription>
|
|
206
|
+
</div>
|
|
207
|
+
<Button asChild variant="outline" size="sm">
|
|
208
|
+
<Link href="/settings">
|
|
209
|
+
Configure <ArrowRight className="h-4 w-4" />
|
|
210
|
+
</Link>
|
|
211
|
+
</Button>
|
|
212
|
+
</CardHeader>
|
|
213
|
+
<CardContent className="p-0">
|
|
214
|
+
<div className="grid border-t md:grid-cols-2">
|
|
215
|
+
<GuardrailMetricPanel label="Cost" metric={progress.cost} formatValue={formatCurrency} />
|
|
216
|
+
<GuardrailMetricPanel label="Tokens" metric={progress.tokens} formatValue={formatTokens} />
|
|
217
|
+
</div>
|
|
218
|
+
</CardContent>
|
|
219
|
+
</Card>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
130
223
|
function FirstRunPanel({ status }: { status: FirstRunStatus }) {
|
|
131
224
|
return (
|
|
132
225
|
<Card className={status.tone === "warning" ? "border-amber-300 bg-amber-50/50" : undefined}>
|
|
@@ -253,6 +346,8 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
|
|
|
253
346
|
</CardContent>
|
|
254
347
|
</Card>
|
|
255
348
|
|
|
349
|
+
<UsageGuardrailsPanel progress={data.usageGuardrails} />
|
|
350
|
+
|
|
256
351
|
<Card>
|
|
257
352
|
<CardHeader>
|
|
258
353
|
<h2 className="text-sm font-semibold leading-tight">Recommended Next Actions</h2>
|
package/app/projects/page.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
import { RankBarChart } from "@/components/charts/rank-bar-chart";
|
|
3
3
|
import { TrendChart } from "@/components/charts/trend-chart";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
5
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
5
6
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
6
7
|
import { DataValue, MonoText, PageHeader } from "@/components/ui/typography";
|
|
@@ -20,6 +21,56 @@ export default function ProjectAnalyticsPage() {
|
|
|
20
21
|
description="Group usage by local repository or inferred project path."
|
|
21
22
|
/>
|
|
22
23
|
|
|
24
|
+
{data.projectSignals.length ? (
|
|
25
|
+
<Card>
|
|
26
|
+
<CardHeader>
|
|
27
|
+
<CardTitle>Project Signals</CardTitle>
|
|
28
|
+
<CardDescription>
|
|
29
|
+
Local project-level patterns that deserve review before optimizing individual sessions.
|
|
30
|
+
</CardDescription>
|
|
31
|
+
</CardHeader>
|
|
32
|
+
<CardContent className="table-scroll">
|
|
33
|
+
<Table>
|
|
34
|
+
<TableHeader>
|
|
35
|
+
<TableRow>
|
|
36
|
+
<TableHead>Priority</TableHead>
|
|
37
|
+
<TableHead>Signal</TableHead>
|
|
38
|
+
<TableHead>Project</TableHead>
|
|
39
|
+
<TableHead>Metric</TableHead>
|
|
40
|
+
<TableHead>Action</TableHead>
|
|
41
|
+
</TableRow>
|
|
42
|
+
</TableHeader>
|
|
43
|
+
<TableBody>
|
|
44
|
+
{data.projectSignals.slice(0, 8).map((signal) => (
|
|
45
|
+
<TableRow key={signal.id}>
|
|
46
|
+
<TableCell>
|
|
47
|
+
<Badge variant={signal.severity === "high" ? "destructive" : signal.severity === "medium" ? "warning" : "secondary"}>
|
|
48
|
+
{signal.severity}
|
|
49
|
+
</Badge>
|
|
50
|
+
</TableCell>
|
|
51
|
+
<TableCell className="capitalize">{signal.signal}</TableCell>
|
|
52
|
+
<TableCell className="max-w-lg">
|
|
53
|
+
<div className="font-medium">{signal.project}</div>
|
|
54
|
+
<MonoText className="mt-1 block truncate text-muted-foreground">{signal.path}</MonoText>
|
|
55
|
+
<div className="mt-1 text-xs text-muted-foreground">{signal.evidence}</div>
|
|
56
|
+
</TableCell>
|
|
57
|
+
<TableCell>
|
|
58
|
+
<div className="font-medium">{signal.metricValue}</div>
|
|
59
|
+
<div className="text-xs text-muted-foreground">{signal.metricLabel}</div>
|
|
60
|
+
</TableCell>
|
|
61
|
+
<TableCell>
|
|
62
|
+
<Link href={signal.href} className="font-medium text-primary underline-offset-4 hover:underline">
|
|
63
|
+
{signal.action}
|
|
64
|
+
</Link>
|
|
65
|
+
</TableCell>
|
|
66
|
+
</TableRow>
|
|
67
|
+
))}
|
|
68
|
+
</TableBody>
|
|
69
|
+
</Table>
|
|
70
|
+
</CardContent>
|
|
71
|
+
</Card>
|
|
72
|
+
) : null}
|
|
73
|
+
|
|
23
74
|
<div className="grid gap-4 lg:grid-cols-[1fr_1.4fr]">
|
|
24
75
|
<Card>
|
|
25
76
|
<CardHeader>
|
package/app/sessions/page.tsx
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
1
2
|
import { SessionExplorer } from "@/components/session-explorer";
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
5
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
2
6
|
import { PageHeader } from "@/components/ui/typography";
|
|
3
7
|
import { getAnalyticsData } from "@/src/lib/analytics";
|
|
8
|
+
import { formatCurrency, formatTokens } from "@/src/lib/format";
|
|
4
9
|
|
|
5
10
|
export const dynamic = "force-dynamic";
|
|
6
11
|
|
|
@@ -24,6 +29,56 @@ export default async function SessionsPage({
|
|
|
24
29
|
title="Session Explorer"
|
|
25
30
|
description="Search and filter imported sessions by tool, model, project, cost, and estimation status."
|
|
26
31
|
/>
|
|
32
|
+
{data.sessionComparisons.length ? (
|
|
33
|
+
<Card>
|
|
34
|
+
<CardHeader>
|
|
35
|
+
<CardTitle>Session Comparison Flags</CardTitle>
|
|
36
|
+
<CardDescription>
|
|
37
|
+
Sessions that are unusual compared with the same project, tool, and primary model.
|
|
38
|
+
</CardDescription>
|
|
39
|
+
</CardHeader>
|
|
40
|
+
<CardContent className="table-scroll">
|
|
41
|
+
<Table>
|
|
42
|
+
<TableHeader>
|
|
43
|
+
<TableRow>
|
|
44
|
+
<TableHead>Priority</TableHead>
|
|
45
|
+
<TableHead>Flag</TableHead>
|
|
46
|
+
<TableHead>Session</TableHead>
|
|
47
|
+
<TableHead>Tokens</TableHead>
|
|
48
|
+
<TableHead>Cost</TableHead>
|
|
49
|
+
<TableHead>Peer median</TableHead>
|
|
50
|
+
<TableHead>Action</TableHead>
|
|
51
|
+
</TableRow>
|
|
52
|
+
</TableHeader>
|
|
53
|
+
<TableBody>
|
|
54
|
+
{data.sessionComparisons.slice(0, 6).map((row) => (
|
|
55
|
+
<TableRow key={row.sessionId}>
|
|
56
|
+
<TableCell>
|
|
57
|
+
<Badge variant={row.severity === "high" ? "destructive" : "warning"}>{row.severity}</Badge>
|
|
58
|
+
</TableCell>
|
|
59
|
+
<TableCell>{row.flag}</TableCell>
|
|
60
|
+
<TableCell className="max-w-lg">
|
|
61
|
+
<div className="font-medium">{row.title}</div>
|
|
62
|
+
<div className="mt-1 text-xs text-muted-foreground">{row.project} / {row.tool} / {row.models}</div>
|
|
63
|
+
</TableCell>
|
|
64
|
+
<TableCell>{formatTokens(row.totalTokens)}</TableCell>
|
|
65
|
+
<TableCell>{formatCurrency(row.cost)}</TableCell>
|
|
66
|
+
<TableCell>
|
|
67
|
+
<div>{formatTokens(row.peerMedianTokens)}</div>
|
|
68
|
+
<div className="text-xs text-muted-foreground">{row.peerSessions} peer sessions</div>
|
|
69
|
+
</TableCell>
|
|
70
|
+
<TableCell>
|
|
71
|
+
<Link href={row.href} className="font-medium text-primary underline-offset-4 hover:underline">
|
|
72
|
+
Compare evidence
|
|
73
|
+
</Link>
|
|
74
|
+
</TableCell>
|
|
75
|
+
</TableRow>
|
|
76
|
+
))}
|
|
77
|
+
</TableBody>
|
|
78
|
+
</Table>
|
|
79
|
+
</CardContent>
|
|
80
|
+
</Card>
|
|
81
|
+
) : null}
|
|
27
82
|
<SessionExplorer
|
|
28
83
|
sessions={data.sessions}
|
|
29
84
|
initialProject={params?.project}
|
package/bin/tokentrace.js
CHANGED
|
@@ -28,6 +28,8 @@ Usage:
|
|
|
28
28
|
tokentrace scan Scan local AI CLI usage logs
|
|
29
29
|
tokentrace doctor --json
|
|
30
30
|
Inspect scan health and repair recommendations
|
|
31
|
+
tokentrace digest --json
|
|
32
|
+
Print current-month local usage digest
|
|
31
33
|
tokentrace insights --json
|
|
32
34
|
Print local recommendations as JSON
|
|
33
35
|
tokentrace status --json
|
|
@@ -365,6 +367,11 @@ async function doctor(args) {
|
|
|
365
367
|
await runNodeScript("doctor", args);
|
|
366
368
|
}
|
|
367
369
|
|
|
370
|
+
async function digest(args) {
|
|
371
|
+
await initializeDatabase({ quiet: true, refreshPrices: false });
|
|
372
|
+
await runNodeScript("digest", args);
|
|
373
|
+
}
|
|
374
|
+
|
|
368
375
|
async function insights(args) {
|
|
369
376
|
await initializeDatabase({ quiet: true, refreshPrices: false });
|
|
370
377
|
await runNodeScript("insights", args);
|
|
@@ -527,6 +534,10 @@ async function main() {
|
|
|
527
534
|
await doctor(args);
|
|
528
535
|
return;
|
|
529
536
|
}
|
|
537
|
+
if (command === "digest") {
|
|
538
|
+
await digest(args);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
530
541
|
if (command === "insights") {
|
|
531
542
|
await insights(args);
|
|
532
543
|
return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, useTransition } from "react";
|
|
4
|
-
import { FolderPlus, Play, RotateCcw, ShieldCheck, Trash2 } from "lucide-react";
|
|
4
|
+
import { FolderPlus, Gauge, Play, RotateCcw, ShieldCheck, Trash2 } from "lucide-react";
|
|
5
5
|
import type { ScanHealth } from "@/src/lib/scan-health";
|
|
6
6
|
import { formatAppVersion } from "@/src/lib/app-version";
|
|
7
7
|
import { formatDate, percent } from "@/src/lib/format";
|
|
@@ -15,6 +15,10 @@ import { DataValue, FieldLabel, MonoText } from "@/components/ui/typography";
|
|
|
15
15
|
type SettingsPayload = {
|
|
16
16
|
customFolders: string[];
|
|
17
17
|
storeRawMessageContent: boolean;
|
|
18
|
+
usageGuardrails: {
|
|
19
|
+
monthlyCostLimitUsd: number | null;
|
|
20
|
+
monthlyTokenLimit: number | null;
|
|
21
|
+
};
|
|
18
22
|
databasePath: string;
|
|
19
23
|
appVersion: string;
|
|
20
24
|
};
|
|
@@ -38,6 +42,13 @@ function toneVariant(tone: ScanHealth["tone"]) {
|
|
|
38
42
|
return "secondary";
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
function parseLimitInput(value: string) {
|
|
46
|
+
const trimmed = value.trim();
|
|
47
|
+
if (!trimmed) return null;
|
|
48
|
+
const parsed = Number(trimmed);
|
|
49
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
export function SettingsPanel({
|
|
42
53
|
initialSettings,
|
|
43
54
|
initialScanHealth
|
|
@@ -47,6 +58,12 @@ export function SettingsPanel({
|
|
|
47
58
|
}) {
|
|
48
59
|
const [customFolders, setCustomFolders] = useState(initialSettings.customFolders);
|
|
49
60
|
const [storeRaw, setStoreRaw] = useState(initialSettings.storeRawMessageContent);
|
|
61
|
+
const [monthlyCostLimitUsd, setMonthlyCostLimitUsd] = useState(
|
|
62
|
+
initialSettings.usageGuardrails.monthlyCostLimitUsd?.toString() ?? ""
|
|
63
|
+
);
|
|
64
|
+
const [monthlyTokenLimit, setMonthlyTokenLimit] = useState(
|
|
65
|
+
initialSettings.usageGuardrails.monthlyTokenLimit?.toString() ?? ""
|
|
66
|
+
);
|
|
50
67
|
const [newFolder, setNewFolder] = useState("");
|
|
51
68
|
const [force, setForce] = useState(false);
|
|
52
69
|
const [message, setMessage] = useState("");
|
|
@@ -64,16 +81,24 @@ export function SettingsPanel({
|
|
|
64
81
|
setCustomFolders((current) => current.filter((item) => item !== folder));
|
|
65
82
|
}
|
|
66
83
|
|
|
84
|
+
function settingsPayload() {
|
|
85
|
+
return {
|
|
86
|
+
customFolders,
|
|
87
|
+
storeRawMessageContent: storeRaw,
|
|
88
|
+
usageGuardrails: {
|
|
89
|
+
monthlyCostLimitUsd: parseLimitInput(monthlyCostLimitUsd),
|
|
90
|
+
monthlyTokenLimit: parseLimitInput(monthlyTokenLimit)
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
67
95
|
function saveSettings() {
|
|
68
96
|
startTransition(async () => {
|
|
69
97
|
setMessage("");
|
|
70
98
|
const response = await fetch("/api/settings", {
|
|
71
99
|
method: "PUT",
|
|
72
100
|
headers: { "content-type": "application/json" },
|
|
73
|
-
body: JSON.stringify(
|
|
74
|
-
customFolders,
|
|
75
|
-
storeRawMessageContent: storeRaw
|
|
76
|
-
})
|
|
101
|
+
body: JSON.stringify(settingsPayload())
|
|
77
102
|
});
|
|
78
103
|
setMessage(response.ok ? "Settings saved." : "Settings save failed.");
|
|
79
104
|
});
|
|
@@ -86,10 +111,7 @@ export function SettingsPanel({
|
|
|
86
111
|
await fetch("/api/settings", {
|
|
87
112
|
method: "PUT",
|
|
88
113
|
headers: { "content-type": "application/json" },
|
|
89
|
-
body: JSON.stringify(
|
|
90
|
-
customFolders,
|
|
91
|
-
storeRawMessageContent: storeRaw
|
|
92
|
-
})
|
|
114
|
+
body: JSON.stringify(settingsPayload())
|
|
93
115
|
});
|
|
94
116
|
const response = await fetch("/api/scan", {
|
|
95
117
|
method: "POST",
|
|
@@ -189,6 +211,48 @@ export function SettingsPanel({
|
|
|
189
211
|
</CardContent>
|
|
190
212
|
</Card>
|
|
191
213
|
|
|
214
|
+
<Card>
|
|
215
|
+
<CardHeader>
|
|
216
|
+
<CardTitle className="flex items-center gap-2">
|
|
217
|
+
<Gauge className="h-4 w-4 text-primary" />
|
|
218
|
+
Local Usage Guardrails
|
|
219
|
+
</CardTitle>
|
|
220
|
+
<CardDescription>
|
|
221
|
+
Optional month-to-date limits for local cost and token awareness.
|
|
222
|
+
</CardDescription>
|
|
223
|
+
</CardHeader>
|
|
224
|
+
<CardContent className="grid gap-4 border-y p-4 md:grid-cols-2">
|
|
225
|
+
<div className="space-y-2">
|
|
226
|
+
<Label htmlFor="monthly-cost-limit">Monthly cost limit</Label>
|
|
227
|
+
<Input
|
|
228
|
+
id="monthly-cost-limit"
|
|
229
|
+
type="number"
|
|
230
|
+
min="0"
|
|
231
|
+
step="0.01"
|
|
232
|
+
inputMode="decimal"
|
|
233
|
+
value={monthlyCostLimitUsd}
|
|
234
|
+
onChange={(event) => setMonthlyCostLimitUsd(event.target.value)}
|
|
235
|
+
placeholder="250"
|
|
236
|
+
/>
|
|
237
|
+
<p className="text-xs text-muted-foreground">USD limit for imported CLI usage this calendar month.</p>
|
|
238
|
+
</div>
|
|
239
|
+
<div className="space-y-2">
|
|
240
|
+
<Label htmlFor="monthly-token-limit">Monthly token limit</Label>
|
|
241
|
+
<Input
|
|
242
|
+
id="monthly-token-limit"
|
|
243
|
+
type="number"
|
|
244
|
+
min="0"
|
|
245
|
+
step="1000"
|
|
246
|
+
inputMode="numeric"
|
|
247
|
+
value={monthlyTokenLimit}
|
|
248
|
+
onChange={(event) => setMonthlyTokenLimit(event.target.value)}
|
|
249
|
+
placeholder="10000000"
|
|
250
|
+
/>
|
|
251
|
+
<p className="text-xs text-muted-foreground">Leave either field blank to disable that guardrail.</p>
|
|
252
|
+
</div>
|
|
253
|
+
</CardContent>
|
|
254
|
+
</Card>
|
|
255
|
+
|
|
192
256
|
<Card>
|
|
193
257
|
<CardHeader>
|
|
194
258
|
<CardTitle className="flex flex-wrap items-center gap-2">
|