feat: add Trae SOLO (ByteDance AI IDE) support - #344
Conversation
Add provider detection for Trae SOLO, ByteDance's VS Code-based AI IDE. - resolveTraePath(): cross-platform path resolver - resolveTraeStoragePath(): locates storage.json - parseTraeIncremental(): reads entitlement data from Local State (iCubeServerData://icube.cloudide) Supported plans: Free, Pro, Pro+, Enterprise Data: identity, solo_builder/coder flags, fast_request_per, waitlist
📝 WalkthroughWalkthrough
ChangesRollout parser updates
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant StorageJson
participant parseTraeIncremental
participant Queue
StorageJson->>parseTraeIncremental: Read iCubeServerData entitlement data
parseTraeIncremental->>Queue: Append synthetic hourly trae_entitlement entry
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
| return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 }; | ||
| } | ||
|
|
||
| const raw = await fs.readFile(targetPath, "utf8"); |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@src/lib/rollout.js`:
- Around line 13275-13296: Update the entitlement processing around
entitlementInfo so parsed serverData without a valid entitlementInfo returns
early, persists cursorState.lastMtime and cursorState.updatedAt, and reports
zero processing counts like the missing serverData path. Only continue to derive
detail and the model when entitlementInfo is present, avoiding synthetic
trae-unknown entries.
- Around line 13252-13329: Update parseTraeIncremental so its cursor mutations
are persisted to the caller by assigning the updated cursorState back to
cursors.trae, following the existing parsePiIncremental pattern. Ensure this
assignment occurs before returning after the cursor is updated, including the
no-server-data path, while preserving the current mtime comparison and queue
behavior.
- Around line 13252-13257: Update parseTraeIncremental to use the supplied
traHome when resolving the fallback target path, rather than always calling
resolveTraeStoragePath with process.env. Preserve storagePath precedence and the
existing early return when the resolved target is missing.
- Around line 13213-13218: Register parseTraeIncremental in the rollout provider
invocation/canonical list alongside the existing Trae symbols so the rollout
dispatcher invokes it and collects Trae entitlement snapshots; keep the existing
export unchanged.
🪄 Autofix (Beta)
❌ Autofix failed (check again to retry)
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: d115aa5a-d2ce-482b-b813-529ee9c3f28f
📒 Files selected for processing (1)
src/lib/rollout.js
|
|
||
| // Trae SOLO (ByteDance AI IDE) | ||
| resolveTraePath, | ||
| resolveTraeStoragePath, | ||
| parseTraeIncremental, | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n "parseTraeIncremental|parsePiIncremental\b" src/lib/rollout.js -B3 -A3
rg -n "resolveTraePath|resolveTraeStoragePath" src -g '!**/rollout.js'Repository: mm7894215/TokenTracker
Length of output: 1216
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the provider/canonical list and Trae parser definition in rollout.js
sed -n '13080,13320p' src/lib/rollout.js | cat -nRepository: mm7894215/TokenTracker
Length of output: 9399
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find all Trae-related references in rollout.js and nearby rollout wiring.
rg -n "Trae|parse[A-Za-z]+Incremental|resolve[A-Za-z]+Path|resolve[A-Za-z]+StoragePath" src/lib/rollout.jsRepository: mm7894215/TokenTracker
Length of output: 8077
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the provider list / dispatcher in rollout.js and any Trae registration.
rg -n "canonical|providers|switch \\(|case .*Trae|Trae SOLO|parseTraeIncremental" src/lib/rollout.js -A2 -B2Repository: mm7894215/TokenTracker
Length of output: 8374
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search the repo for any actual Trae parser wiring/call sites.
rg -n "\bparseTraeIncremental\b|\bresolveTraePath\b|\bresolveTraeStoragePath\b" .Repository: mm7894215/TokenTracker
Length of output: 758
Wire Trae into the rollout dispatcher
parseTraeIncremental is exported here, but it isn’t registered in the provider invocation/canonical list, so Trae entitlement snapshots won’t be collected.
🤖 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 `@src/lib/rollout.js` around lines 13213 - 13218, Register parseTraeIncremental
in the rollout provider invocation/canonical list alongside the existing Trae
symbols so the rollout dispatcher invokes it and collects Trae entitlement
snapshots; keep the existing export unchanged.
Source: Coding guidelines
| async function parseTraeIncremental({ traHome, storagePath, cursors, queuePath, onProgress } = {}) { | ||
| await ensureDir(path.dirname(queuePath)); | ||
| const targetPath = storagePath ?? resolveTraeStoragePath(process.env); | ||
| if (!targetPath || !fssync.existsSync(targetPath)) { | ||
| return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 }; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
traHome parameter is accepted but never used.
The destructured traHome is dead — targetPath always falls back to resolveTraeStoragePath(process.env), ignoring any traHome the caller supplies and bypassing any injected env for testing. If a caller resolves traHome once (e.g. to avoid recomputation or to override for tests), that value is silently discarded.
♻️ Proposed fix
-async function parseTraeIncremental({ traHome, storagePath, cursors, queuePath, onProgress } = {}) {
+async function parseTraeIncremental({ traHome, storagePath, cursors, queuePath, onProgress, env } = {}) {
await ensureDir(path.dirname(queuePath));
- const targetPath = storagePath ?? resolveTraeStoragePath(process.env);
+ const targetPath =
+ storagePath ??
+ (traHome
+ ? path.join(traHome, "User", "globalStorage", "storage.json")
+ : resolveTraeStoragePath(env || process.env));📝 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.
| async function parseTraeIncremental({ traHome, storagePath, cursors, queuePath, onProgress } = {}) { | |
| await ensureDir(path.dirname(queuePath)); | |
| const targetPath = storagePath ?? resolveTraeStoragePath(process.env); | |
| if (!targetPath || !fssync.existsSync(targetPath)) { | |
| return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 }; | |
| } | |
| async function parseTraeIncremental({ traHome, storagePath, cursors, queuePath, onProgress, env } = {}) { | |
| await ensureDir(path.dirname(queuePath)); | |
| const targetPath = | |
| storagePath ?? | |
| (traHome | |
| ? path.join(traHome, "User", "globalStorage", "storage.json") | |
| : resolveTraeStoragePath(env || process.env)); | |
| if (!targetPath || !fssync.existsSync(targetPath)) { | |
| return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 }; | |
| } |
🤖 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 `@src/lib/rollout.js` around lines 13252 - 13257, Update parseTraeIncremental
to use the supplied traHome when resolving the fallback target path, rather than
always calling resolveTraeStoragePath with process.env. Preserve storagePath
precedence and the existing early return when the resolved target is missing.
| async function parseTraeIncremental({ traHome, storagePath, cursors, queuePath, onProgress } = {}) { | ||
| await ensureDir(path.dirname(queuePath)); | ||
| const targetPath = storagePath ?? resolveTraeStoragePath(process.env); | ||
| if (!targetPath || !fssync.existsSync(targetPath)) { | ||
| return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 }; | ||
| } | ||
|
|
||
| const cursorState = cursors?.trae && typeof cursors.trae === "object" ? cursors.trae : {}; | ||
| const lastMtime = cursorState.lastMtime || 0; | ||
|
|
||
| const stat = await fs.stat(targetPath).catch(() => null); | ||
| if (!stat || stat.mtimeMs <= lastMtime) { | ||
| return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 }; | ||
| } | ||
|
|
||
| const raw = await fs.readFile(targetPath, "utf8"); | ||
| let storage; | ||
| try { | ||
| storage = JSON.parse(raw); | ||
| } catch { | ||
| return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 }; | ||
| } | ||
|
|
||
| const serverKey = "iCubeServerData://icube.cloudide"; | ||
| const serverData = storage[serverKey]; | ||
| if (!serverData) { | ||
| cursorState.lastMtime = stat.mtimeMs; | ||
| cursorState.updatedAt = new Date().toISOString(); | ||
| return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 }; | ||
| } | ||
|
|
||
| let ent; | ||
| try { | ||
| ent = typeof serverData === "string" ? JSON.parse(serverData) : serverData; | ||
| } catch { | ||
| return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 }; | ||
| } | ||
|
|
||
| const entitlementInfo = ent.entitlementInfo || {}; | ||
| const detail = entitlementInfo.detail || {}; | ||
| const hourStart = new Date( | ||
| new Date().toISOString().slice(0, 13) + ":00:00.000Z" | ||
| ).toISOString(); | ||
|
|
||
| const model = "trae-" + (entitlementInfo.identityStr || "unknown").toLowerCase(); | ||
|
|
||
| const queueLine = JSON.stringify({ | ||
| source: "trae", | ||
| model, | ||
| hour_start: hourStart, | ||
| input_tokens: 0, | ||
| cached_input_tokens: 0, | ||
| cache_creation_input_tokens: 0, | ||
| output_tokens: 0, | ||
| reasoning_output_tokens: 0, | ||
| total_tokens: 0, | ||
| billable_total_tokens: 0, | ||
| conversation_count: 0, | ||
| trae_entitlement: { | ||
| identity: entitlementInfo.identityStr, | ||
| identity_code: entitlementInfo.identity, | ||
| has_package: entitlementInfo.hasPackage, | ||
| is_dollar_billing: entitlementInfo.isDollarUsageBilling, | ||
| pro_period: entitlementInfo.proPeriod, | ||
| enable_solo_builder: entitlementInfo.enableSoloBuilder, | ||
| enable_solo_coder: entitlementInfo.enableSoloCoder, | ||
| fast_request_per: detail.fastRequestPer, | ||
| in_waitlist: detail.inWaitlist, | ||
| }, | ||
| }); | ||
|
|
||
| await fs.appendFile(queuePath, queueLine + "\n"); | ||
|
|
||
| cursorState.lastMtime = stat.mtimeMs; | ||
| cursorState.updatedAt = new Date().toISOString(); | ||
|
|
||
| return { recordsProcessed: 1, eventsAggregated: 1, bucketsQueued: 1 }; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
Critical: cursors.trae mutation is never persisted back — entitlement snapshot re-queued on every run.
cursorState is derived from cursors?.trae, defaulting to a fresh {} when cursors.trae doesn't exist yet (Line 13259). All later mutations (Lines 13278-13280, 13325-13326) write to this local object, but it is never assigned back to cursors.trae. On the very first run, cursors.trae stays undefined forever, so lastMtime is always read as 0 on the next invocation and the stat.mtimeMs <= lastMtime short-circuit (Line 13263) never triggers — the same entitlement snapshot gets re-parsed and re-appended to the queue on every call. parsePiIncremental avoids this by explicitly reassigning cursors.pi = {...} (Lines 9825-9830); parseTraeIncremental needs the same treatment.
🐛 Proposed fix
if (!serverData) {
cursorState.lastMtime = stat.mtimeMs;
cursorState.updatedAt = new Date().toISOString();
+ cursors.trae = cursorState;
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
}
@@
cursorState.lastMtime = stat.mtimeMs;
cursorState.updatedAt = new Date().toISOString();
+ cursors.trae = cursorState;
return { recordsProcessed: 1, eventsAggregated: 1, bucketsQueued: 1 };🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 13266-13266: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFile(targetPath, "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename)
🤖 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 `@src/lib/rollout.js` around lines 13252 - 13329, Update parseTraeIncremental
so its cursor mutations are persisted to the caller by assigning the updated
cursorState back to cursors.trae, following the existing parsePiIncremental
pattern. Ensure this assignment occurs before returning after the cursor is
updated, including the no-server-data path, while preserving the current mtime
comparison and queue behavior.
| const serverKey = "iCubeServerData://icube.cloudide"; | ||
| const serverData = storage[serverKey]; | ||
| if (!serverData) { | ||
| cursorState.lastMtime = stat.mtimeMs; | ||
| cursorState.updatedAt = new Date().toISOString(); | ||
| return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 }; | ||
| } | ||
|
|
||
| let ent; | ||
| try { | ||
| ent = typeof serverData === "string" ? JSON.parse(serverData) : serverData; | ||
| } catch { | ||
| return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 }; | ||
| } | ||
|
|
||
| const entitlementInfo = ent.entitlementInfo || {}; | ||
| const detail = entitlementInfo.detail || {}; | ||
| const hourStart = new Date( | ||
| new Date().toISOString().slice(0, 13) + ":00:00.000Z" | ||
| ).toISOString(); | ||
|
|
||
| const model = "trae-" + (entitlementInfo.identityStr || "unknown").toLowerCase(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
No guard for missing entitlementInfo inside serverData.
Line 13277-13281 handles the case where serverData itself is absent, but if serverData parses fine yet lacks entitlementInfo, execution falls through to entitlementInfo = {} (Line 13290) and still queues a synthetic trae-unknown entry with all entitlement fields undefined. Consider returning early (and persisting the cursor) when entitlementInfo is missing, mirroring the serverData check above.
🤖 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 `@src/lib/rollout.js` around lines 13275 - 13296, Update the entitlement
processing around entitlementInfo so parsed serverData without a valid
entitlementInfo returns early, persists cursorState.lastMtime and
cursorState.updatedAt, and reports zero processing counts like the missing
serverData path. Only continue to derive detail and the model when
entitlementInfo is present, avoiding synthetic trae-unknown entries.
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. An unexpected error occurred while generating fixes: Not Found - https://docs.github.com/rest/git/refs#get-a-reference |
Code reviewThanks for adding Trae SOLO support! However, this PR currently fails CI and introduces a few regressions that need to be fixed before merging:
Please update the branch once these items are addressed! |
Summary
Add provider detection for Trae SOLO, ByteDance VS Code-based AI IDE.
Changes
Data captured
Tested with Trae SOLO v2.3.55113 on macOS Apple Silicon.
Summary by CodeRabbit
New Features
Bug Fixes