Skip to content

feat: improve supply chain security and Socket.dev package score - #16

Merged
nazozokc merged 3 commits into
mainfrom
AI-agent
Jun 19, 2026
Merged

feat: improve supply chain security and Socket.dev package score#16
nazozokc merged 3 commits into
mainfrom
AI-agent

Conversation

@nazozokc

@nazozokc nazozokc commented Jun 19, 2026

Copy link
Copy Markdown
Owner

概要

npm パッケージ subtrack のサプライチェーンセキュリティを強化し、Socket.dev スコアを 75 → 100 に改善する。

変更内容

Package metadata(Socket.dev Quality スコア向上)

  • description, keywords, homepage, bugs を package.json に追加
  • engines.node >=22 で Node.js バージョンを明示
  • sideEffects: false でバンドラ最適化を許可

Supply chain security(サプライチェーン攻撃対策)

  • .npmrc: engine-strict=true, strict-peer-dependencies=true
  • pnpm-workspace.yaml: onlyBuiltDependencies で esbuild のみ許可(postinstall スクリプト制限)
  • Renovate: osvVulnerabilityAlerts 有効化、npm:unpublishSafe 追加
  • リリースパイプライン: publish 直前に pnpm audit を実施

Socket.dev スコア改善の根拠

カテゴリ 改善施策 影響
Quality description, keywords, homepage, bugs, engines, sideEffects 50 → 100 見込み
Supply Chain Risk onlyBuiltDependencies, osvVulnerabilityAlerts, audit gate アップ
License MIT(既存) 100 維持

確認事項

  • pnpm test 全 175 tests pass
  • pnpm build 成功
  • pnpm install --frozen-lockfile 正常

Summary by CodeRabbit

Release Notes

  • New Features

    • Added LLM API usage tracking with new usage command group for adding, listing, deleting, and refreshing usage entries.
    • Payment display now supports --api flag to show API usage statistics and costs.
    • Integrated model pricing lookup for automatic LLM cost calculation.
  • Improvements

    • Enhanced package management with stricter peer dependency checks.
    • Added dependency vulnerability auditing to release workflow.
    • Updated Node.js minimum requirement to v22.

nazozokc added 2 commits June 19, 2026 20:31
- Add .npmrc with engine-strict and strict-peer-dependencies
- Add package metadata (description, keywords, homepage, bugs)
- Set engines.node >=22 and sideEffects: false
- Enable OSV vulnerability alerts in Renovate with npm:unpublishSafe
- Add pnpm audit gate to release workflow
- Replace allowBuilds with onlyBuiltDependencies in workspace config
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@nazozokc, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 22 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3880baae-45f1-49ef-9c65-bd1c04261b19

📥 Commits

Reviewing files that changed from the base of the PR and between b120a98 and f07682c.

📒 Files selected for processing (1)
  • pnpm-workspace.yaml
📝 Walkthrough

Walkthrough

The PR introduces LLM API usage tracking (pricing lookup, llm_usage DB table, and usage add|list|delete|refresh CLI commands) while decomposing display.ts into fx.ts, export.ts, payment.ts, and import-csv.ts. Domain types are centralized into a new types.ts. Package metadata, engine constraints, and CI security hardening are also added.

Changes

Source code refactoring and LLM usage feature

Layer / File(s) Summary
Domain types consolidation
subtrack/src/types.ts
Defines Currency, Cycle, OCCURRENCES_PER_YEAR, periodFactor, SharedArgs, AddSharedArgs, LlmUsageEntry, AddLlmUsageArgs, GetLlmUsageOptions, UsageAddFlags, and AddFlags, extracted from db.ts and commands.ts.
display.ts decomposed into fx, export, and import-csv modules
subtrack/src/fx.ts, subtrack/src/export.ts, subtrack/src/import-csv.ts, subtrack/src/display.ts
fx.ts adds FxRates, fetchFxRates, convertPrice. export.ts adds exportCsv (with BOM + escapeCsv), exportJson, exportMd. import-csv.ts adds parseCsvLine and handleImport. display.ts removes all those exports and re-imports from fx.ts/types.ts.
Pricing engine with cache and model matching
subtrack/src/pricing.ts
Defines ModelPricingEntry/PricingCache types; implements ensurePricingCache (single-flight, disk-backed, GitHub-sourced), matchModel (ordered heuristics), calculateCostCents, getModelPricingDirect, and refreshPricingCache.
llm_usage DB schema and CRUD
subtrack/src/db.ts
Imports all types from types.ts, adds llm_usage table to getDb(), and exports addLlmUsage, getLlmUsage (filtered, paginated), deleteLlmUsage, getLlmUsageTotal, and getLlmUsageTotalByProvider.
payment.ts: showPayment with API cost integration and summary
subtrack/src/payment.ts
Adds a cycle date-range helper, showPayment (FX conversion, optional API usage total/breakdown, per-currency grouping), SummaryData type, calcSummary, and showSummary.
Usage CLI handlers and prompt validators
subtrack/src/usage.ts, subtrack/src/prompts.ts
prompts.ts adds LLM_PROVIDER_CHOICES, validateTokens, validateDate, validateModelName. usage.ts adds resolveUsageAddOptions (interactive pricing lookup with manual fallback), handleUsageAdd, handleUsageList, handleUsageDelete, handleUsageRefresh.
CLI wiring: usage command group, payment --api flag, commands.ts cleanup
subtrack/src/index.ts, subtrack/src/commands.ts
index.ts registers `usage add
Test suite updates
subtrack/src/db.test.ts, subtrack/src/display.test.ts, subtrack/src/commands.test.ts, subtrack/src/pricing.test.ts
Adds llm_usage schema and CRUD tests, moves periodFactor import to types.ts, re-points export/payment/summary tests to new modules, adds showPayment --api tests, mocks pricing for usage command tests, and adds full matchModel/calculateCostCents/ensurePricingCache coverage.

Package hardening and CI security

Layer / File(s) Summary
Package metadata, engine constraints, and CI security hardening
package.json, subtrack/package.json, .npmrc, pnpm-workspace.yaml, .github/renovate.json, .github/workflows/release.yml
Adds node >=22 engine constraints and package metadata. .npmrc enables engine-strict and strict-peer-dependencies. pnpm-workspace.yaml switches to onlyBuiltDependencies. Renovate adds npm:unpublishSafe and OSV vulnerability automerge. Release workflow adds pnpm audit --audit-level=high before publish.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant index.ts
  participant usage.ts
  participant pricing.ts
  participant db.ts

  rect rgba(70, 130, 180, 0.5)
    Note over User,db.ts: usage add
    User->>index.ts: subtrack usage add [flags]
    index.ts->>usage.ts: handleUsageAdd(flags)
    usage.ts->>pricing.ts: ensurePricingCache()
    pricing.ts-->>usage.ts: PricingCache | null
    usage.ts->>pricing.ts: matchModel(cache, provider, model)
    pricing.ts-->>usage.ts: ModelPricingEntry | null
    usage.ts->>pricing.ts: calculateCostCents(pricing, inputTokens, outputTokens)
    pricing.ts-->>usage.ts: cost in cents
    usage.ts->>db.ts: addLlmUsage(result)
    db.ts-->>usage.ts: void
    usage.ts-->>User: success log
  end

  rect rgba(60, 179, 113, 0.5)
    Note over User,db.ts: payment --api
    User->>index.ts: subtrack payment --api
    index.ts->>usage.ts: handlePayment(period, {api: true})
    usage.ts->>db.ts: getLlmUsageTotal(from, to)
    db.ts-->>usage.ts: total cost
    usage.ts->>db.ts: getLlmUsageTotalByProvider(from, to)
    db.ts-->>usage.ts: [{provider, total}]
    usage.ts-->>User: payment table + API usage breakdown
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • nazozokc/subtrack#12: Introduced handleExport, exportCsv, and exportMd in display.ts — this PR refactors those exact functions out into the new export.ts module.
  • nazozokc/subtrack#13: Added parseCsvLine and handleImport into commands.ts — this PR removes them from commands.ts and relocates them to the new import-csv.ts module.
  • nazozokc/subtrack#15: Touched index.ts CLI argument parsing for tags and payment commands — this PR also modifies the same command definitions, adding the --api flag and fixing positional tag parsing.

Poem

🐇 Hop hop, the modules are tidy now,
display.ts split with a joyful bow!
A pricing cache lives on the disk,
LLM tokens tracked at any brisk.
Types in one place, the bunny does cheer —
Cleaner than clover this time of year! 🌸

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly reflects the main objectives of this PR, which focuses on improving supply chain security and Socket.dev package score through metadata, dependency, and security configuration updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch AI-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (5)
subtrack/src/pricing.ts (1)

60-69: 💤 Low value

Consider adding a timeout to the fetch request.

The fetch call to GitHub has no timeout, which could cause the CLI to hang indefinitely if the network is slow or unresponsive. For a CLI tool this is minor since users can Ctrl+C, but adding AbortSignal.timeout() would improve UX.

💡 Optional: Add fetch timeout
     // Fetch from GitHub
     try {
-      const res = await fetch(GITHUB_JSON_URL)
+      const res = await fetch(GITHUB_JSON_URL, { signal: AbortSignal.timeout(15_000) })
       if (!res.ok) throw new Error(`GitHub responded with ${res.status}`)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/pricing.ts` around lines 60 - 69, The fetch request to
GITHUB_JSON_URL lacks a timeout configuration, which could cause the CLI to hang
indefinitely on slow or unresponsive networks. Add an AbortSignal with a timeout
to the fetch call by passing a signal option to the fetch function with
AbortSignal.timeout() specifying an appropriate timeout duration in milliseconds
(typically 5000-10000ms for network requests). This will automatically abort the
request if it exceeds the specified time limit.
subtrack/src/export.ts (1)

15-39: ⚡ Quick win

Document exported serializer APIs with JSDoc.

exportCsv, exportJson, and exportMd are public module APIs but currently undocumented.

As per coding guidelines, "Document public APIs with JSDoc comments in JavaScript/TypeScript".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/export.ts` around lines 15 - 39, Add JSDoc comments to the three
public export functions: exportCsv, exportJson, and exportMd. Each function
needs a JSDoc block that describes what the function does, documents the subs
parameter (SharedArgs array), and specifies the return type (string). The JSDoc
comments should clearly explain the purpose of each export function and what
format it produces.

Source: Coding guidelines

subtrack/src/types.ts (2)

49-55: ⚡ Quick win

Unify GetLlmUsageOptions to one exported type.

GetLlmUsageOptions is defined here and also in subtrack/src/db.ts (context snippet). Keeping both definitions risks silent contract drift between callers and DB filtering logic.

♻️ Suggested consolidation
-// subtrack/src/db.ts
-export type GetLlmUsageOptions = {
-  provider?: string
-  from?: string
-  to?: string
-  limit?: number
-  offset?: number
-}
+// subtrack/src/db.ts
+import type { GetLlmUsageOptions } from "./types.ts"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/types.ts` around lines 49 - 55, The GetLlmUsageOptions type is
currently defined in both subtrack/src/types.ts and subtrack/src/db.ts, creating
a risk of divergence between the two definitions. Remove the duplicate
GetLlmUsageOptions type definition from subtrack/src/db.ts and instead import it
from subtrack/src/types.ts at the top of the db.ts file. This ensures there is a
single source of truth for the type contract used by the DB filtering logic.

25-35: ⚡ Quick win

Add JSDoc to exported public API types in this module.

Most exported public type aliases here are undocumented, which weakens API discoverability after the type consolidation.

As per coding guidelines, "Document public APIs with JSDoc comments in JavaScript/TypeScript".

Also applies to: 36-55, 57-72

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/types.ts` around lines 25 - 35, Add JSDoc documentation comments
to all exported public type aliases in the module, including SharedArgs,
AddSharedArgs, and all other exported types mentioned in the comment (those at
lines 36-55 and 57-72). For each type, provide a clear description of what the
type represents and its purpose in the API. Place the JSDoc comment block
immediately above each type export to improve API discoverability and developer
understanding.

Source: Coding guidelines

subtrack/src/import-csv.ts (1)

8-43: ⚡ Quick win

Add JSDoc for exported import APIs.

parseCsvLine and handleImport are exported public APIs and should be documented.

As per coding guidelines, "Document public APIs with JSDoc comments in JavaScript/TypeScript".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/import-csv.ts` around lines 8 - 43, The exported functions
parseCsvLine and handleImport lack JSDoc documentation required for public APIs.
Add JSDoc comments above each function that describe their purpose, document all
parameters with their types and descriptions, and document the return type. For
parseCsvLine, document that it takes a CSV line string and returns an array of
parsed field strings. For handleImport, document the file parameter and options
parameter (including the dryRun option), and specify its return type as a
Promise.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 52-53: The audit step runs at the workspace root checking all
packages, but the publish is scoped only to ./subtrack, causing unrelated
vulnerabilities to block the release. Scope the pnpm audit command to only the
./subtrack package by adding the appropriate filter flag to the run command that
executes "pnpm audit --audit-level=high" so it matches the scope of the publish
step and only validates dependencies relevant to the package being released.

In `@subtrack/src/db.test.ts`:
- Around line 727-737: The test for getLlmUsage only validates descending date
ordering but does not test the tie-breaking behavior when multiple entries share
the same date. To address this, add additional test entries with identical dates
to the test function getLlmUsage returns entries ordered by date desc and add
assertions that verify these same-date entries are ordered by id in descending
order. This ensures the id DESC tie-break logic in the getLlmUsage query is
properly tested.

In `@subtrack/src/display.test.ts`:
- Around line 625-633: Replace the dynamic date generation using new
Date().toISOString().split("T")[0] with a fixed hardcoded date string (for
example, "2024-01-15") in the test "shows API usage in USD without --currency"
where addLlmUsage is called with the date parameter. This ensures deterministic
test behavior independent of timezone or when the test runs. Apply the same fix
to all other occurrences of this pattern in the test file, particularly in the
test cases referenced at lines 645-653.
- Around line 5-6: The import statement for spreadSubscription from the local
module does not include the .ts file extension, which is inconsistent with the
coding guidelines that require .ts extensions for local imports. Update the
import statement on line 5 to add the .ts extension to the ./display module
path, making it consistent with the pattern used in the types.ts import on line
6.

In `@subtrack/src/export.ts`:
- Around line 8-13: The escapeCsv function does not protect against formula
injection attacks where user input starting with =, +, -, or @ characters can
execute formulas when the CSV is opened in spreadsheet applications. Modify the
escapeCsv function to check if the value starts with any of these dangerous
characters and, if detected, prepend a single quote to neutralize the formula
execution. Apply this check before or after the existing quote/comma/newline
escaping logic to ensure all potentially malicious inputs are properly
neutralized.

In `@subtrack/src/fx.ts`:
- Around line 6-11: The fetchFxRates function has a fetch call that lacks a
timeout mechanism, allowing it to hang indefinitely if the upstream API stalls.
Implement a timeout for the fetch request in fetchFxRates using AbortController
by creating an abort controller, setting a timeout that triggers the abort (with
an appropriate duration like 5-10 seconds), passing the abort signal to the
fetch call, and handling the resulting AbortError that occurs if the timeout
expires before the request completes.

In `@subtrack/src/import-csv.ts`:
- Around line 54-57: The issue is that the code splits the content by newline in
the lines variable before parsing it as CSV, which breaks valid quoted multiline
CSV fields. Instead of performing the split("\n").map((l) =>
l.trim()).filter(Boolean) operation on the clean content, use a proper CSV
parsing library that correctly handles quoted fields containing newlines. Pass
the clean variable to a CSV parser that respects quote escaping rules, so fields
like "Line1\nLine2" remain intact as single fields during import and maintain
round-trip consistency with the exportCsv function.
- Around line 78-85: The condition checking the number of fields in the CSV
import function is too permissive. Currently it only rejects rows with fewer
than 5 fields using `fields.length < 5`, but this allows rows with extra columns
to pass through and get silently truncated during destructuring. Change the
condition to use `fields.length !== 5` instead to ensure rows are rejected
unless they have exactly 5 fields, which will prevent incorrect values from
being imported without explicit error handling.

In `@subtrack/src/payment.ts`:
- Around line 237-246: The monthlyByTag aggregation mixes currency values
without conversion, causing incorrect totals when a single tag contains
subscriptions in different currencies. Refactor the monthlyByTag data structure
to track totals separately by currency for each tag, changing from a flat
monthly amount to a nested structure that groups monthly totals and counts by
currency. Then update the display loop that iterates over sorted entries to
handle the per-currency breakdown, displaying each currency separately under its
tag with the appropriate currency symbol instead of assuming all values are in
USD.

In `@subtrack/src/pricing.test.ts`:
- Around line 126-138: In the test "ensurePricingCache returns null when fetch
fails and no cache", add an actual invocation of the ensurePricingCache function
from the imported pricing module and include assertions to verify it returns
null when fetch fails and no cache exists. Currently the test mocks
globalThis.fetch but never calls the ensurePricingCache function or asserts its
return value, rendering the test unable to validate the function's actual
behavior.

In `@subtrack/src/pricing.ts`:
- Around line 144-154: The calculateCostCents function is applying both
outputCost and reasoningCost to the same outputTokens value, causing
double-charging for output tokens. To fix this, either add a separate
reasoningTokens parameter to the function signature and use it for calculating
reasoningCost instead of outputTokens, or modify the reasoningCost calculation
to only apply when output_cost_per_reasoning_token is defined and make it
mutually exclusive with the standard output_cost_per_token calculation.
Additionally, ensure the corresponding test is updated to validate the corrected
behavior instead of the current double-charging scenario.

In `@subtrack/src/usage.ts`:
- Around line 88-97: The validation function validateDate returns true for empty
strings because !v.trim() evaluates to true when the trimmed value is empty, but
then the empty string gets assigned to the date variable instead of defaulting
to today. After the validateDate check in the conditional block where flags.date
is validated, add an additional check to ensure the trimmed date value is not
empty; if it is empty after trimming, set date to today instead of storing the
empty string. Alternatively, modify the conditional logic to check that
flags.date is not only defined but also has a non-empty trimmed value before
proceeding with validation.

---

Nitpick comments:
In `@subtrack/src/export.ts`:
- Around line 15-39: Add JSDoc comments to the three public export functions:
exportCsv, exportJson, and exportMd. Each function needs a JSDoc block that
describes what the function does, documents the subs parameter (SharedArgs
array), and specifies the return type (string). The JSDoc comments should
clearly explain the purpose of each export function and what format it produces.

In `@subtrack/src/import-csv.ts`:
- Around line 8-43: The exported functions parseCsvLine and handleImport lack
JSDoc documentation required for public APIs. Add JSDoc comments above each
function that describe their purpose, document all parameters with their types
and descriptions, and document the return type. For parseCsvLine, document that
it takes a CSV line string and returns an array of parsed field strings. For
handleImport, document the file parameter and options parameter (including the
dryRun option), and specify its return type as a Promise.

In `@subtrack/src/pricing.ts`:
- Around line 60-69: The fetch request to GITHUB_JSON_URL lacks a timeout
configuration, which could cause the CLI to hang indefinitely on slow or
unresponsive networks. Add an AbortSignal with a timeout to the fetch call by
passing a signal option to the fetch function with AbortSignal.timeout()
specifying an appropriate timeout duration in milliseconds (typically
5000-10000ms for network requests). This will automatically abort the request if
it exceeds the specified time limit.

In `@subtrack/src/types.ts`:
- Around line 49-55: The GetLlmUsageOptions type is currently defined in both
subtrack/src/types.ts and subtrack/src/db.ts, creating a risk of divergence
between the two definitions. Remove the duplicate GetLlmUsageOptions type
definition from subtrack/src/db.ts and instead import it from
subtrack/src/types.ts at the top of the db.ts file. This ensures there is a
single source of truth for the type contract used by the DB filtering logic.
- Around line 25-35: Add JSDoc documentation comments to all exported public
type aliases in the module, including SharedArgs, AddSharedArgs, and all other
exported types mentioned in the comment (those at lines 36-55 and 57-72). For
each type, provide a clear description of what the type represents and its
purpose in the API. Place the JSDoc comment block immediately above each type
export to improve API discoverability and developer understanding.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6f6a5fd9-993e-441c-846c-5d3667ab36ce

📥 Commits

Reviewing files that changed from the base of the PR and between e06501d and b120a98.

📒 Files selected for processing (22)
  • .github/renovate.json
  • .github/workflows/release.yml
  • .npmrc
  • package.json
  • pnpm-workspace.yaml
  • subtrack/package.json
  • subtrack/src/commands.test.ts
  • subtrack/src/commands.ts
  • subtrack/src/db.test.ts
  • subtrack/src/db.ts
  • subtrack/src/display.test.ts
  • subtrack/src/display.ts
  • subtrack/src/export.ts
  • subtrack/src/fx.ts
  • subtrack/src/import-csv.ts
  • subtrack/src/index.ts
  • subtrack/src/payment.ts
  • subtrack/src/pricing.test.ts
  • subtrack/src/pricing.ts
  • subtrack/src/prompts.ts
  • subtrack/src/types.ts
  • subtrack/src/usage.ts

Comment on lines +52 to +53
- name: Audit dependencies
run: pnpm audit --audit-level=high

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Scope the audit step to the package being published (Line 52).

The audit currently runs at workspace root, while publish is scoped to ./subtrack. This can block a valid subtrack release due to vulnerabilities in unrelated workspaces.

Suggested fix
       - name: Audit dependencies
-        run: pnpm audit --audit-level=high
+        working-directory: ./subtrack
+        run: pnpm audit --audit-level=high
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Audit dependencies
run: pnpm audit --audit-level=high
- name: Audit dependencies
working-directory: ./subtrack
run: pnpm audit --audit-level=high
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 52 - 53, The audit step runs at
the workspace root checking all packages, but the publish is scoped only to
./subtrack, causing unrelated vulnerabilities to block the release. Scope the
pnpm audit command to only the ./subtrack package by adding the appropriate
filter flag to the run command that executes "pnpm audit --audit-level=high" so
it matches the scope of the publish step and only validates dependencies
relevant to the package being released.

Comment thread subtrack/src/db.test.ts
Comment on lines +727 to +737
test("getLlmUsage returns entries ordered by date desc", async () => {
const db = await import("./db.ts")
db.addLlmUsage({ provider: "openai", model: "a", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-01", description: null })
db.addLlmUsage({ provider: "openai", model: "b", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-15", description: null })
db.addLlmUsage({ provider: "openai", model: "c", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-10", description: null })

const entries = db.getLlmUsage()
expect(entries[0].model).toBe("b") // latest first
expect(entries[1].model).toBe("c")
expect(entries[2].model).toBe("a")
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a same-date tie-break assertion for getLlmUsage ordering.

This test checks date-desc ordering, but not the equal-date id DESC tie-break used by the query. That leaves a deterministic ordering path untested.

As per coding guidelines: "Write unit tests for all functions and critical code paths."

💡 Suggested test addition
 test("getLlmUsage returns entries ordered by date desc", async () => {
   const db = await import("./db.ts")
   db.addLlmUsage({ provider: "openai", model: "a", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-01", description: null })
   db.addLlmUsage({ provider: "openai", model: "b", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-15", description: null })
   db.addLlmUsage({ provider: "openai", model: "c", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-10", description: null })

   const entries = db.getLlmUsage()
   expect(entries[0].model).toBe("b") // latest first
   expect(entries[1].model).toBe("c")
   expect(entries[2].model).toBe("a")
 })
+
+test("getLlmUsage uses id desc as tie-breaker for same date", async () => {
+  const db = await import("./db.ts")
+  db.addLlmUsage({ provider: "openai", model: "first", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-15", description: null })
+  db.addLlmUsage({ provider: "openai", model: "second", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-15", description: null })
+
+  const entries = db.getLlmUsage()
+  expect(entries[0].model).toBe("second")
+  expect(entries[1].model).toBe("first")
+})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("getLlmUsage returns entries ordered by date desc", async () => {
const db = await import("./db.ts")
db.addLlmUsage({ provider: "openai", model: "a", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-01", description: null })
db.addLlmUsage({ provider: "openai", model: "b", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-15", description: null })
db.addLlmUsage({ provider: "openai", model: "c", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-10", description: null })
const entries = db.getLlmUsage()
expect(entries[0].model).toBe("b") // latest first
expect(entries[1].model).toBe("c")
expect(entries[2].model).toBe("a")
})
test("getLlmUsage returns entries ordered by date desc", async () => {
const db = await import("./db.ts")
db.addLlmUsage({ provider: "openai", model: "a", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-01", description: null })
db.addLlmUsage({ provider: "openai", model: "b", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-15", description: null })
db.addLlmUsage({ provider: "openai", model: "c", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-10", description: null })
const entries = db.getLlmUsage()
expect(entries[0].model).toBe("b") // latest first
expect(entries[1].model).toBe("c")
expect(entries[2].model).toBe("a")
})
test("getLlmUsage uses id desc as tie-breaker for same date", async () => {
const db = await import("./db.ts")
db.addLlmUsage({ provider: "openai", model: "first", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-15", description: null })
db.addLlmUsage({ provider: "openai", model: "second", input_tokens: 1, output_tokens: 1, cost: 0.01, date: "2026-06-15", description: null })
const entries = db.getLlmUsage()
expect(entries[0].model).toBe("second")
expect(entries[1].model).toBe("first")
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/db.test.ts` around lines 727 - 737, The test for getLlmUsage
only validates descending date ordering but does not test the tie-breaking
behavior when multiple entries share the same date. To address this, add
additional test entries with identical dates to the test function getLlmUsage
returns entries ordered by date desc and add assertions that verify these
same-date entries are ordered by id in descending order. This ensures the id
DESC tie-break logic in the getLlmUsage query is properly tested.

Source: Coding guidelines

Comment on lines 5 to +6
import { spreadSubscription } from "./display"
import type { SharedArgs } from "./db"
import type { SharedArgs } from "./types.ts"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP 'import\s+.*from\s+"\.{1,2}/[^"]+(?<!\.ts)"' subtrack/src/display.test.ts

Repository: nazozokc/subtrack

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Check the exact content of the test file at the specified lines
sed -n '1,15p' subtrack/src/display.test.ts

Repository: nazozokc/subtrack

Length of output: 550


🏁 Script executed:

#!/bin/bash
# Verify that the display.ts module exists
ls -la subtrack/src/display.ts 2>/dev/null || echo "display.ts not found"

Repository: nazozokc/subtrack

Length of output: 134


🏁 Script executed:

#!/bin/bash
# Check if there are other similar imports in this file
rg -n 'import.*from\s+"[^"]*"' subtrack/src/display.test.ts

Repository: nazozokc/subtrack

Length of output: 361


Use .ts extension for this local import.

Line 5 imports from local module ./display without the .ts extension, inconsistent with the coding guidelines requiring .ts extension for local imports in TypeScript files and conflicting with the correct import pattern shown on line 6.

✅ Suggested fix
-import { spreadSubscription } from "./display"
+import { spreadSubscription } from "./display.ts"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { spreadSubscription } from "./display"
import type { SharedArgs } from "./db"
import type { SharedArgs } from "./types.ts"
import { spreadSubscription } from "./display.ts"
import type { SharedArgs } from "./types.ts"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/display.test.ts` around lines 5 - 6, The import statement for
spreadSubscription from the local module does not include the .ts file
extension, which is inconsistent with the coding guidelines that require .ts
extensions for local imports. Update the import statement on line 5 to add the
.ts extension to the ./display module path, making it consistent with the
pattern used in the types.ts import on line 6.

Source: Coding guidelines

Comment on lines +625 to +633
test("shows API usage in USD without --currency", async () => {
const db = await import("./db.ts")
db.addLlmUsage({
provider: "openai",
model: "gpt-4o",
input_tokens: 1000,
output_tokens: 500,
cost: 50, // 50 cents = $0.50
date: new Date().toISOString().split("T")[0],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid real-time dates in these tests to prevent flakiness.

Using new Date().toISOString().split("T")[0] makes assertions sensitive to timezone/month-boundary behavior. Prefer fixed dates (or mocked time) for deterministic test outcomes.

Also applies to: 645-653

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/display.test.ts` around lines 625 - 633, Replace the dynamic
date generation using new Date().toISOString().split("T")[0] with a fixed
hardcoded date string (for example, "2024-01-15") in the test "shows API usage
in USD without --currency" where addLlmUsage is called with the date parameter.
This ensures deterministic test behavior independent of timezone or when the
test runs. Apply the same fix to all other occurrences of this pattern in the
test file, particularly in the test cases referenced at lines 645-653.

Comment thread subtrack/src/export.ts
Comment on lines +8 to +13
function escapeCsv(value: string): string {
if (value.includes('"') || value.includes(",") || value.includes("\n")) {
return `"${value.replace(/"/g, '""')}"`
}
return value
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden CSV escaping against formula injection.

User-controlled cells starting with =, +, -, or @ can execute formulas in spreadsheet apps when this CSV is opened.

🔒 Suggested fix
 function escapeCsv(value: string): string {
-  if (value.includes('"') || value.includes(",") || value.includes("\n")) {
-    return `"${value.replace(/"/g, '""')}"`
+  const safe = /^[=+\-@]/.test(value) ? `'${value}` : value
+  if (safe.includes('"') || safe.includes(",") || safe.includes("\n")) {
+    return `"${safe.replace(/"/g, '""')}"`
   }
-  return value
+  return safe
 }

Also applies to: 17-20

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/export.ts` around lines 8 - 13, The escapeCsv function does not
protect against formula injection attacks where user input starting with =, +,
-, or @ characters can execute formulas when the CSV is opened in spreadsheet
applications. Modify the escapeCsv function to check if the value starts with
any of these dangerous characters and, if detected, prepend a single quote to
neutralize the formula execution. Apply this check before or after the existing
quote/comma/newline escaping logic to ensure all potentially malicious inputs
are properly neutralized.

Comment on lines +78 to +85
if (fields.length < 5) {
consola.warn(`Line ${i + 1}: skipping (expected 5 fields, got ${fields.length})`)
failed++
continue
}

const [name, cycle, tagsStr, priceStr, currency] = fields

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject rows unless they have exactly 5 fields.

Rows with extra columns are currently accepted and silently truncated by destructuring, which can import incorrect values without a hard failure.

✅ Suggested fix
-    if (fields.length < 5) {
-      consola.warn(`Line ${i + 1}: skipping (expected 5 fields, got ${fields.length})`)
+    if (fields.length !== 5) {
+      consola.warn(`Line ${i + 1}: skipping (expected 5 fields, got ${fields.length})`)
       failed++
       continue
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (fields.length < 5) {
consola.warn(`Line ${i + 1}: skipping (expected 5 fields, got ${fields.length})`)
failed++
continue
}
const [name, cycle, tagsStr, priceStr, currency] = fields
if (fields.length !== 5) {
consola.warn(`Line ${i + 1}: skipping (expected 5 fields, got ${fields.length})`)
failed++
continue
}
const [name, cycle, tagsStr, priceStr, currency] = fields
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/import-csv.ts` around lines 78 - 85, The condition checking the
number of fields in the CSV import function is too permissive. Currently it only
rejects rows with fewer than 5 fields using `fields.length < 5`, but this allows
rows with extra columns to pass through and get silently truncated during
destructuring. Change the condition to use `fields.length !== 5` instead to
ensure rows are rejected unless they have exactly 5 fields, which will prevent
incorrect values from being imported without explicit error handling.

Comment thread subtrack/src/payment.ts
Comment on lines +237 to +246
if (Object.keys(data.monthlyByTag).length > 0) {
consola.log("")
consola.log(pc.bold("Monthly by tag:"))
const sorted = Object.entries(data.monthlyByTag).sort((a, b) => b[1].monthly - a[1].monthly)
for (const [tag, info] of sorted) {
consola.log(
` ${tag.padEnd(16)} ${formatPrice(Math.round(info.monthly), "USD")}/month (${info.count} sub${info.count > 1 ? "s" : ""})`,
)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bug: Tag totals mix currencies without conversion.

monthlyByTag aggregates subscription prices across different currencies without FX conversion, then displays the sum using USD formatting. For example, a "streaming" tag with Netflix ($15.99 USD) and Spotify (¥980 JPY) would show ¥995.99/month which is meaningless.

Either convert all values to a common currency or display per-currency breakdowns within each tag.

🐛 Proposed fix: Track tag totals by currency
 export type SummaryData = {
   totalCount: number
   monthlyByCurrency: Record<string, number>
-  monthlyByTag: Record<string, { count: number; monthly: number }>
+  monthlyByTag: Record<string, { count: number; monthlyCurrency: Record<string, number> }>
   mostExpensive: SharedArgs | undefined
 }

 export function calcSummary(subs: SharedArgs[]): SummaryData {
   const monthlyByCurrency: Record<string, number> = {}
-  const monthlyByTag: Record<string, { count: number; monthly: number }> = {}
+  const monthlyByTag: Record<string, { count: number; monthlyCurrency: Record<string, number> }> = {}

   for (const sub of subs) {
     const monthly = sub.price * periodFactor(sub.cycle, "monthly")

     monthlyByCurrency[sub.currency] = (monthlyByCurrency[sub.currency] ?? 0) + monthly

     for (const tag of sub.tags) {
-      if (!monthlyByTag[tag]) monthlyByTag[tag] = { count: 0, monthly: 0 }
+      if (!monthlyByTag[tag]) monthlyByTag[tag] = { count: 0, monthlyCurrency: {} }
       monthlyByTag[tag].count++
-      monthlyByTag[tag].monthly += monthly
+      monthlyByTag[tag].monthlyCurrency[sub.currency] = (monthlyByTag[tag].monthlyCurrency[sub.currency] ?? 0) + monthly
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/payment.ts` around lines 237 - 246, The monthlyByTag aggregation
mixes currency values without conversion, causing incorrect totals when a single
tag contains subscriptions in different currencies. Refactor the monthlyByTag
data structure to track totals separately by currency for each tag, changing
from a flat monthly amount to a nested structure that groups monthly totals and
counts by currency. Then update the display loop that iterates over sorted
entries to handle the per-currency breakdown, displaying each currency
separately under its tag with the appropriate currency symbol instead of
assuming all values are in USD.

Comment on lines +126 to +138
test("ensurePricingCache returns null when fetch fails and no cache", async () => {
const originalFetch = globalThis.fetch
globalThis.fetch = async () => { throw new Error("Network error") }

// Reset module state by importing fresh
const pricing = await import("./pricing.ts")

// This should fail since there's no cache and fetch fails
// But the implementation has caching logic, so this may need env setup
// For now just verify it doesn't throw

globalThis.fetch = originalFetch
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP '\bensurePricingCache\s*\(' subtrack/src/pricing.test.ts

Repository: nazozokc/subtrack

Length of output: 126


🏁 Script executed:

sed -n '120,165p' subtrack/src/pricing.test.ts

Repository: nazozokc/subtrack

Length of output: 1370


🏁 Script executed:

cat subtrack/src/pricing.ts | head -100

Repository: nazozokc/subtrack

Length of output: 2985


Tests for ensurePricingCache lack function calls and assertions.

Both tests mock globalThis.fetch and import the module, but never invoke ensurePricingCache() or assert its output. The tests pass regardless of implementation correctness.

Per coding guidelines ("Write unit tests for all functions and critical code paths"), add actual function calls and assertions:

Suggested changes
 test("ensurePricingCache returns null when fetch fails and no cache", async () => {
   const originalFetch = globalThis.fetch
-  globalThis.fetch = async () => { throw new Error("Network error") }
-
-  // Reset module state by importing fresh
-  const pricing = await import("./pricing.ts")
-
-  // This should fail since there's no cache and fetch fails
-  // But the implementation has caching logic, so this may need env setup
-  // For now just verify it doesn't throw
-
-  globalThis.fetch = originalFetch
+  try {
+    globalThis.fetch = async () => {
+      throw new Error("Network error")
+    }
+    vi.resetModules()
+    const { ensurePricingCache } = await import("./pricing.ts")
+    await expect(ensurePricingCache()).resolves.toBeNull()
+  } finally {
+    globalThis.fetch = originalFetch
+  }
 })

 test("ensurePricingCache parses GitHub JSON correctly", async () => {
   const mockData: PricingCache = { ... }
   const originalFetch = globalThis.fetch
-  globalThis.fetch = async () =>
-    new Response(JSON.stringify(mockData), {
-      headers: { "Content-Type": "application/json" },
-    })
-
-  const pricing = await import("./pricing.ts")
-  // This call will try to fetch, and we can't easily reset the cache state
-  // Just verifying the function doesn't throw
-
-  globalThis.fetch = originalFetch
+  try {
+    globalThis.fetch = async () =>
+      new Response(JSON.stringify(mockData), {
+        headers: { "Content-Type": "application/json" },
+      })
+    vi.resetModules()
+    const { ensurePricingCache } = await import("./pricing.ts")
+    await expect(ensurePricingCache()).resolves.toEqual(mockData)
+  } finally {
+    globalThis.fetch = originalFetch
+  }
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/pricing.test.ts` around lines 126 - 138, In the test
"ensurePricingCache returns null when fetch fails and no cache", add an actual
invocation of the ensurePricingCache function from the imported pricing module
and include assertions to verify it returns null when fetch fails and no cache
exists. Currently the test mocks globalThis.fetch but never calls the
ensurePricingCache function or asserts its return value, rendering the test
unable to validate the function's actual behavior.

Source: Coding guidelines

Comment thread subtrack/src/pricing.ts
Comment on lines +144 to +154
export function calculateCostCents(
pricing: ModelPricingEntry,
inputTokens: number,
outputTokens: number,
): number {
const inputCost = (pricing.input_cost_per_token ?? 0) * inputTokens
const outputCost = (pricing.output_cost_per_token ?? 0) * outputTokens
const reasoningCost =
(pricing.output_cost_per_reasoning_token ?? 0) * outputTokens
return (inputCost + outputCost + reasoningCost) * 100
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

LiteLLM output_cost_per_reasoning_token vs output_cost_per_token usage

💡 Result:

In LiteLLM, output_cost_per_token and output_cost_per_reasoning_token serve distinct roles in cost calculation for models that support reasoning [1][2]. output_cost_per_token This parameter represents the cost per standard (non-reasoning) completion token [3][2]. It is the base configuration used for typical output text generated by a model [4]. output_cost_per_reasoning_token This parameter is specifically used for models that support "reasoning" or "thinking" tokens (e.g., OpenAI o1/o3 series, DeepSeek R1) [1][5][4]. It allows LiteLLM to apply a different, often higher, pricing tier to the reasoning portion of the output compared to the standard output tokens [6][4]. Key Usage Considerations: 1. Differential Billing: When both keys are defined, LiteLLM attempts to calculate costs by distinguishing between reasoning tokens and standard (text) completion tokens [4]. 2. Calculation Logic: LiteLLM's internal cost utility splits total completion tokens into categories (reasoning tokens vs. text/non-reasoning tokens) [4]. If a provider returns specific reasoning_tokens in the usage metadata, LiteLLM uses output_cost_per_reasoning_token for those tokens and output_cost_per_token for the remainder [4]. 3. Bug Fixes and Accuracy: Historical issues have occurred where LiteLLM miscalculated costs by ignoring non-reasoning tokens when reasoning tokens were present [5][4]. Current versions of LiteLLM correctly calculate costs by ensuring that any completion tokens not classified as reasoning (or other specialized types like audio/image) are billed at the standard output_cost_per_token rate [4]. 4. Custom Configuration: When registering custom model pricing, you can provide both parameters in the model information dictionary to ensure accurate spend tracking for models with specialized reasoning tiers [2][7]. Top results: [6][5][4][2]

Citations:


🏁 Script executed:

cd subtrack && find . -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: nazozokc/subtrack

Length of output: 336


🏁 Script executed:

cd subtrack && rg "calculateCostCents" -A 3 -B 3

Repository: nazozokc/subtrack

Length of output: 3688


🏁 Script executed:

cd subtrack && rg "ModelPricingEntry" -A 5

Repository: nazozokc/subtrack

Length of output: 3807


🏁 Script executed:

cd subtrack && cat -n src/usage.ts | sed -n '1,150p'

Repository: nazozokc/subtrack

Length of output: 5391


🏁 Script executed:

cd subtrack && rg "outputTokens|reasoning" src/usage.ts -B 5 -A 5

Repository: nazozokc/subtrack

Length of output: 1490


🏁 Script executed:

cd subtrack && cat -n src/pricing.test.ts | grep -A 15 "includes reasoning tokens"

Repository: nazozokc/subtrack

Length of output: 762


🏁 Script executed:

cd subtrack && cat -n src/types.ts | grep -A 20 "LlmUsageEntry"

Repository: nazozokc/subtrack

Length of output: 908


🏁 Script executed:

cd subtrack && rg "reasoning" src/ -i

Repository: nazozokc/subtrack

Length of output: 524


🏁 Script executed:

cd subtrack && cat -n src/pricing.test.ts | sed -n '105,125p'

Repository: nazozokc/subtrack

Length of output: 897


🏁 Script executed:

cd subtrack && cat -n src/pricing.ts | sed -n '140,155p'

Repository: nazozokc/subtrack

Length of output: 719


Bug: Double-charging for output tokens when reasoning cost is defined.

The function charges both output_cost_per_token and output_cost_per_reasoning_token against the same outputTokens count. For models like o1 with both fields defined, this results in both rates being applied to every output token.

According to LiteLLM's model, these are meant for different token types: output_cost_per_token for standard completion tokens and output_cost_per_reasoning_token for reasoning-specific tokens tracked separately. The function needs either:

  1. A separate reasoningTokens parameter to distinguish token types, or
  2. Logic to treat them as mutually exclusive (use reasoning price if available, else standard price)

The current test validates the double-charging behavior and must be corrected accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/pricing.ts` around lines 144 - 154, The calculateCostCents
function is applying both outputCost and reasoningCost to the same outputTokens
value, causing double-charging for output tokens. To fix this, either add a
separate reasoningTokens parameter to the function signature and use it for
calculating reasoningCost instead of outputTokens, or modify the reasoningCost
calculation to only apply when output_cost_per_reasoning_token is defined and
make it mutually exclusive with the standard output_cost_per_token calculation.
Additionally, ensure the corresponding test is updated to validate the corrected
behavior instead of the current double-charging scenario.

Comment thread subtrack/src/usage.ts
Comment on lines +88 to +97
// Date
let date: string
const today = new Date().toISOString().split("T")[0]
if (flags.date !== undefined) {
const result = validateDate(flags.date)
if (result !== true) { consola.error(result); return null }
date = flags.date
} else {
date = today
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Empty date string passes validation but is stored as-is instead of defaulting to today.

When flags.date is an empty string "", validateDate("") returns true (since !v.trim() is truthy), but then date = flags.date stores the empty string. This creates an entry with date = "" instead of today's date.

🐛 Proposed fix
   // Date
   let date: string
   const today = new Date().toISOString().split("T")[0]
   if (flags.date !== undefined) {
     const result = validateDate(flags.date)
     if (result !== true) { consola.error(result); return null }
-    date = flags.date
+    date = flags.date.trim() || today
   } else {
     date = today
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Date
let date: string
const today = new Date().toISOString().split("T")[0]
if (flags.date !== undefined) {
const result = validateDate(flags.date)
if (result !== true) { consola.error(result); return null }
date = flags.date
} else {
date = today
}
// Date
let date: string
const today = new Date().toISOString().split("T")[0]
if (flags.date !== undefined) {
const result = validateDate(flags.date)
if (result !== true) { consola.error(result); return null }
date = flags.date.trim() || today
} else {
date = today
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/usage.ts` around lines 88 - 97, The validation function
validateDate returns true for empty strings because !v.trim() evaluates to true
when the trimmed value is empty, but then the empty string gets assigned to the
date variable instead of defaulting to today. After the validateDate check in
the conditional block where flags.date is validated, add an additional check to
ensure the trimmed date value is not empty; if it is empty after trimming, set
date to today instead of storing the empty string. Alternatively, modify the
conditional logic to check that flags.date is not only defined but also has a
non-empty trimmed value before proceeding with validation.

@nazozokc
nazozokc merged commit 39f7825 into main Jun 19, 2026
10 checks passed
@nazozokc
nazozokc deleted the AI-agent branch June 19, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant