tokentrace 0.8.0 → 0.8.1

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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to TokenTrace are documented here.
4
4
 
5
+ ## [0.8.1] - 2026-05-13
6
+
7
+ ### Fixed
8
+
9
+ - Codex CLI imports now read exact `token_count` totals, including cached input and reasoning output tokens, so cleared Codex sessions are no longer undercounted.
10
+ - Claude Code and Codex session artifacts can exceed the generic file-size cap without being skipped during discovery.
11
+ - OpenAI-style, Claude-style, and generic usage parsers now normalize cached, cache-write, and reasoning token fields without double-counting them.
12
+ - Parser-version-aware rescans now reprocess stale imports, and source-file replacement is atomic so a failed replacement cannot delete prior trusted sessions.
13
+ - Unknown-cost cause summaries now assign each interaction to one primary cause instead of overlapping buckets.
14
+ - Pricing, scan, settings, repair, and pricing-refresh APIs now reject malformed JSON with clean 400 responses.
15
+ - Manual pricing saves now reject blank model/provider names and invalid numeric prices instead of silently storing unknown prices.
16
+ - CLI commands now reject unknown flags across scan, pricing refresh, status, doctor, digest, and insights commands.
17
+ - Local release checks now run ProjScan through `projscan@latest`, matching the GitHub security workflow guardrail.
18
+
5
19
  ## [0.8.0] - 2026-05-12
6
20
 
7
21
  ### Added
@@ -1,10 +1,15 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { readJsonObject } from "@/src/lib/api-json";
2
3
  import { refreshPricing } from "@/src/lib/pricing-refresh";
3
4
 
4
5
  export const dynamic = "force-dynamic";
5
6
 
6
7
  export async function POST(request: Request) {
7
- const body = await request.json().catch(() => ({}));
8
+ const parsed = await readJsonObject(request);
9
+ if (!parsed.ok) {
10
+ return NextResponse.json({ error: parsed.error }, { status: 400 });
11
+ }
12
+ const body = parsed.body;
8
13
  const result = await refreshPricing({
9
14
  source: body?.source === "bundled" ? "bundled" : "remote",
10
15
  force: Boolean(body?.force)
@@ -1,12 +1,21 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { readJsonObject } from "@/src/lib/api-json";
2
3
  import { getPricingRows, upsertPricing } from "@/src/lib/pricing";
3
4
 
4
5
  export const dynamic = "force-dynamic";
5
6
 
6
- function nullableNumber(value: unknown) {
7
- if (value === "" || value == null) return null;
7
+ function requiredText(value: unknown) {
8
+ return typeof value === "string" ? value.trim() : "";
9
+ }
10
+
11
+ function nullablePrice(value: unknown, field: string) {
12
+ if (value == null) return { ok: true as const, value: null };
13
+ if (typeof value === "string" && value.trim() === "") return { ok: true as const, value: null };
8
14
  const number = Number(value);
9
- return Number.isFinite(number) ? number : null;
15
+ if (!Number.isFinite(number) || number < 0) {
16
+ return { ok: false as const, error: `${field} must be a non-negative number or empty` };
17
+ }
18
+ return { ok: true as const, value: number };
10
19
  }
11
20
 
12
21
  export async function GET() {
@@ -14,20 +23,34 @@ export async function GET() {
14
23
  }
15
24
 
16
25
  export async function POST(request: Request) {
17
- const body = await request.json();
18
- if (!body.providerId || !body.model) {
26
+ const parsed = await readJsonObject(request);
27
+ if (!parsed.ok) {
28
+ return NextResponse.json({ error: parsed.error }, { status: 400 });
29
+ }
30
+ const body = parsed.body;
31
+ const providerId = requiredText(body.providerId);
32
+ const model = requiredText(body.model);
33
+ if (!providerId || !model) {
19
34
  return NextResponse.json({ error: "providerId and model are required" }, { status: 400 });
20
35
  }
36
+ const inputTokenPrice = nullablePrice(body.inputTokenPrice, "inputTokenPrice");
37
+ const outputTokenPrice = nullablePrice(body.outputTokenPrice, "outputTokenPrice");
38
+ const cachedInputTokenPrice = nullablePrice(body.cachedInputTokenPrice, "cachedInputTokenPrice");
39
+ const cacheWriteTokenPrice = nullablePrice(body.cacheWriteTokenPrice, "cacheWriteTokenPrice");
40
+ if (!inputTokenPrice.ok) return NextResponse.json({ error: inputTokenPrice.error }, { status: 400 });
41
+ if (!outputTokenPrice.ok) return NextResponse.json({ error: outputTokenPrice.error }, { status: 400 });
42
+ if (!cachedInputTokenPrice.ok) return NextResponse.json({ error: cachedInputTokenPrice.error }, { status: 400 });
43
+ if (!cacheWriteTokenPrice.ok) return NextResponse.json({ error: cacheWriteTokenPrice.error }, { status: 400 });
21
44
 
22
45
  const id = upsertPricing({
23
- providerId: String(body.providerId),
24
- providerName: body.providerName ? String(body.providerName) : undefined,
25
- model: String(body.model),
26
- inputTokenPrice: nullableNumber(body.inputTokenPrice),
27
- outputTokenPrice: nullableNumber(body.outputTokenPrice),
28
- cachedInputTokenPrice: nullableNumber(body.cachedInputTokenPrice),
29
- cacheWriteTokenPrice: nullableNumber(body.cacheWriteTokenPrice),
30
- currency: body.currency ? String(body.currency) : "USD"
46
+ providerId,
47
+ providerName: requiredText(body.providerName) || undefined,
48
+ model,
49
+ inputTokenPrice: inputTokenPrice.value,
50
+ outputTokenPrice: outputTokenPrice.value,
51
+ cachedInputTokenPrice: cachedInputTokenPrice.value,
52
+ cacheWriteTokenPrice: cacheWriteTokenPrice.value,
53
+ currency: requiredText(body.currency) || "USD"
31
54
  });
32
55
 
33
56
  return NextResponse.json({ id, costsRecalculated: true });
@@ -5,6 +5,7 @@ import {
5
5
  saveUnknownCostReview,
6
6
  type UnknownCostRepairStatus
7
7
  } from "@/src/lib/unknown-cost-repair";
8
+ import { readJsonObject } from "@/src/lib/api-json";
8
9
 
9
10
  export const dynamic = "force-dynamic";
10
11
 
@@ -34,7 +35,11 @@ export async function GET() {
34
35
  }
35
36
 
36
37
  export async function PUT(request: Request) {
37
- const body = await request.json();
38
+ const parsed = await readJsonObject(request);
39
+ if (!parsed.ok) {
40
+ return NextResponse.json({ error: parsed.error }, { status: 400 });
41
+ }
42
+ const body = parsed.body;
38
43
  const key = text(body.key, 1000);
39
44
  const status = reviewState(body.status ?? body.state);
40
45
 
@@ -1,10 +1,15 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { runScan } from "@/src/ingestion/scan";
3
+ import { readJsonObject } from "@/src/lib/api-json";
3
4
 
4
5
  export const dynamic = "force-dynamic";
5
6
 
6
7
  export async function POST(request: Request) {
7
- const body = await request.json().catch(() => ({}));
8
+ const parsed = await readJsonObject(request);
9
+ if (!parsed.ok) {
10
+ return NextResponse.json({ error: parsed.error }, { status: 400 });
11
+ }
12
+ const body = parsed.body;
8
13
  const result = await runScan({
9
14
  folders: Array.isArray(body.folders)
10
15
  ? body.folders.filter((folder: unknown): folder is string => typeof folder === "string")
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { getDatabasePath } from "@/src/db/client";
3
3
  import { getAppSettings, normalizeUsageGuardrails, saveAppSettings } from "@/src/db/settings";
4
+ import { readJsonObject } from "@/src/lib/api-json";
4
5
 
5
6
  export const dynamic = "force-dynamic";
6
7
 
@@ -12,7 +13,11 @@ export async function GET() {
12
13
  }
13
14
 
14
15
  export async function PUT(request: Request) {
15
- const body = await request.json();
16
+ const parsed = await readJsonObject(request);
17
+ if (!parsed.ok) {
18
+ return NextResponse.json({ error: parsed.error }, { status: 400 });
19
+ }
20
+ const body = parsed.body;
16
21
  const customFolders = Array.isArray(body.customFolders)
17
22
  ? body.customFolders.filter((folder: unknown): folder is string => typeof folder === "string")
18
23
  : [];