Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
PORT=19900
TRAE_DATA_DIR=

# === Trae edition & data/install paths (all optional, cross-platform) ===
#
# TRAE_EDITION selects which Trae edition to use. Default: cn
# cn = Trae CN (China) sg = Trae (Singapore/global)
TRAE_EDITION=cn
#
# Data directory override (per-edition explicit, highest priority):
# CN edition -> TRAE_CN_DATA_DIR
# SG edition -> TRAE_SG_DATA_DIR
# Generic fallback for any edition (lower priority, backward compat):
# TRAE_DATA_DIR
# When all are unset, the OS-default path is used:
# Windows: %APPDATA%\<folder> (e.g. C:\Users\<you>\AppData\Roaming\Trae CN)
# macOS: ~/Library/Application Support/<folder>
# Linux: $XDG_CONFIG_HOME/<folder> (or ~/.config/<folder>)
# TRAE_CN_DATA_DIR=
# TRAE_SG_DATA_DIR=
# TRAE_DATA_DIR=
#
# Folder names under the OS base dir (only used when no *_DATA_DIR override is set).
# Override these if your Trae install uses non-default folder names.
# TRAE_CN_FOLDER=Trae CN
# TRAE_SG_FOLDER=Trae
#
# Install directory folder names (used to locate manifest.json for IDE version):
# TRAE_CN_INSTALL_FOLDER=Trae-CN
# TRAE_SG_INSTALL_FOLDER=Trae

TRAE_API_HOST=https://trae-api-cn.mchost.guru
TRAE_AUTH_HOST=https://trae-api-cn.mchost.guru
TRAE_MANUAL_TOKEN=
Expand Down Expand Up @@ -42,9 +69,6 @@ MAX_CONTINUES=5
# TRAE_OS_NAME=windows
# TRAE_OS_VERSION=Windows 10

# Optional: Trae CN data directory for fetch-models-vscdb.js
# TRAE_CN_DATA_DIR=C:\Users\<you>\AppData\Roaming\Trae CN

# Web Portal v2: sessions SQLite DB path (defaults to ${WORKSPACE_DIR}/.trae-api/sessions.db)
# Uses node:sqlite (Node 22.5+ built-in) — no native compilation required.
# SESSIONS_DB_PATH=
Expand Down
17 changes: 12 additions & 5 deletions scripts/fetch-models-vscdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const fs = require('fs');
const path = require('path');
const os = require('os');

// Load .env so TRAE_CN_DATA_DIR (and other vars) are available when run standalone.
require('dotenv').config();

// Try to load better-sqlite3, fallback to a manual approach
let dbLib = null;
try {
Expand All @@ -20,10 +23,14 @@ try {

const { getModelConfig, saveModelConfig, rebuildDerivedMaps } = require('../src/trae-client');
const { getAuthInfo } = require('../src/auth');
const { getTraeDataDir, getDefaultEdition } = require('../src/paths');

// Trae CN data dir — support env override for non-default installations
const TRAE_CN_DATA_DIR = process.env.TRAE_CN_DATA_DIR || path.join(os.homedir(), 'AppData', 'Roaming', 'Trae CN');
const VSCDB_PATH = path.join(TRAE_CN_DATA_DIR, 'User', 'globalStorage', 'state.vscdb');
// Trae data dir — resolved cross-platform via paths.js.
// Edition comes from TRAE_EDITION env var (default 'cn'); the data dir itself can
// be overridden via TRAE_CN_DATA_DIR / TRAE_SG_DATA_DIR / TRAE_DATA_DIR env vars.
const TRAE_EDITION = getDefaultEdition();
const TRAE_DATA_DIR = getTraeDataDir();
const VSCDB_PATH = path.join(TRAE_DATA_DIR, 'User', 'globalStorage', 'state.vscdb');

// Build tier mapping dynamically from model-config.json instead of hardcoding.
// This ensures consistency: if model-config.json tiers change, the script picks them up.
Expand Down Expand Up @@ -88,12 +95,12 @@ else:
async function main() {
const doUpdate = process.argv.includes('--update');

console.log('[fetch-models-vscdb] Reading Trae CN state.vscdb cache...');
console.log(`[fetch-models-vscdb] Reading Trae ${TRAE_EDITION.toUpperCase()} state.vscdb cache...`);
console.log(` Path: ${VSCDB_PATH}`);

if (!fs.existsSync(VSCDB_PATH)) {
console.error(`[fetch-models-vscdb] ERROR: state.vscdb not found at ${VSCDB_PATH}`);
console.error(' Ensure Trae CN IDE is installed and has been opened at least once.');
console.error(` Ensure Trae ${TRAE_EDITION.toUpperCase()} IDE is installed and has been opened at least once.`);
process.exit(1);
}

Expand Down
35 changes: 7 additions & 28 deletions src/auth.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const fetch = require('node-fetch');
const { v4: uuidv4 } = require('./uuid');
const { decryptAuthData: decryptTcAuthData, isTcEncrypted } = require('./trae-decrypt');

function getTraeDataDir() {
const envDir = process.env.TRAE_DATA_DIR;
if (envDir) return envDir;
const edition = detectEdition();
if (edition === 'cn') {
return path.join(os.homedir(), 'AppData', 'Roaming', 'Trae CN');
}
return path.join(os.homedir(), 'AppData', 'Roaming', 'Trae');
}
const { getTraeDataDir, getTraeInstallDir } = require('./paths');

function detectEdition() {
const envEdition = process.env.TRAE_EDITION;
if (envEdition) return envEdition.toLowerCase();

const cnPath = path.join(os.homedir(), 'AppData', 'Roaming', 'Trae CN', 'User', 'globalStorage', 'storage.json');
const sgPath = path.join(os.homedir(), 'AppData', 'Roaming', 'Trae', 'User', 'globalStorage', 'storage.json');
const cnPath = path.join(getTraeDataDir('cn'), 'User', 'globalStorage', 'storage.json');
const sgPath = path.join(getTraeDataDir('sg'), 'User', 'globalStorage', 'storage.json');

const cnExists = fs.existsSync(cnPath);
const sgExists = fs.existsSync(sgPath);
Expand All @@ -41,10 +31,7 @@ function detectEdition() {

function getStorageJsonPath(edition) {
const ed = edition || detectEdition();
const dataDir = ed === 'cn'
? path.join(os.homedir(), 'AppData', 'Roaming', 'Trae CN')
: path.join(os.homedir(), 'AppData', 'Roaming', 'Trae');
return path.join(dataDir, 'User', 'globalStorage', 'storage.json');
return path.join(getTraeDataDir(ed), 'User', 'globalStorage', 'storage.json');
}

function readStorageJson() {
Expand All @@ -64,10 +51,7 @@ function isEncryptedAuthData(raw) {
}

function readStorageJsonByEdition(edition) {
const dataDir = edition === 'cn'
? path.join(os.homedir(), 'AppData', 'Roaming', 'Trae CN')
: path.join(os.homedir(), 'AppData', 'Roaming', 'Trae');
const storagePath = path.join(dataDir, 'User', 'globalStorage', 'storage.json');
const storagePath = path.join(getTraeDataDir(edition), 'User', 'globalStorage', 'storage.json');
if (!fs.existsSync(storagePath)) return null;
const raw = fs.readFileSync(storagePath, 'utf-8');
return JSON.parse(raw);
Expand All @@ -85,9 +69,7 @@ function getAuthInfo() {

for (const ed of editions) {
try {
const dataDir = ed === 'cn'
? path.join(os.homedir(), 'AppData', 'Roaming', 'Trae CN')
: path.join(os.homedir(), 'AppData', 'Roaming', 'Trae');
const dataDir = getTraeDataDir(ed);

try {
const auth = decryptTcAuthData(dataDir);
Expand Down Expand Up @@ -400,10 +382,7 @@ function getIdeVersion() {
// Try to read from Trae's manifest.json for accurate version
try {
const edition = detectEdition();
const baseDir = edition === 'cn'
? path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Trae-CN')
: path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Trae');
const manifestPath = path.join(baseDir, 'manifest.json');
const manifestPath = path.join(getTraeInstallDir(edition), 'manifest.json');
if (fs.existsSync(manifestPath)) {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
if (manifest.appVersion) {
Expand Down
158 changes: 158 additions & 0 deletions src/paths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Centralized cross-platform path resolution for Trae data/install directories.
// Used by auth.js, trae-decrypt.js, and scripts/fetch-models-vscdb.js so that
// path logic is defined in exactly one place.
//
// Everything that used to be hardcoded (edition, folder names, data dir) is now
// env-configurable with sensible OS defaults, so non-default installations and
// non-Windows platforms work without code changes.
const os = require('os');
const path = require('path');

/**
* Normalize an edition value. Accepts 'cn' / 'sg' (case-insensitive); anything
* else falls back to the configured default edition (TRAE_EDITION, or 'cn').
* @param {string} [edition]
* @returns {'cn'|'sg'}
*/
function normalizeEdition(edition) {
if (typeof edition === 'string') {
const ed = edition.toLowerCase();
if (ed === 'cn' || ed === 'sg') return ed;
}
return getDefaultEdition();
}

/**
* Default edition, read from TRAE_EDITION env var (defaults to 'cn').
* Kept as a function (not a module-level const) so changes to process.env
* after process start are honoured.
* @returns {'cn'|'sg'}
*/
function getDefaultEdition() {
const envEdition = process.env.TRAE_EDITION;
if (envEdition) {
const ed = String(envEdition).toLowerCase();
if (ed === 'cn' || ed === 'sg') return ed;
}
return 'cn';
}

/**
* Base app-data directory for VS Code-based (Electron) apps, cross-platform.
* - Windows: %APPDATA% (e.g. C:\Users\<user>\AppData\Roaming)
* - macOS: ~/Library/Application Support
* - Linux: $XDG_CONFIG_HOME or ~/.config
*/
function getAppDataBaseDir() {
const platform = process.platform;
if (platform === 'win32') {
return process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
}
if (platform === 'darwin') {
return path.join(os.homedir(), 'Library', 'Application Support');
}
// linux / other unix-like
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
}

/**
* Base directory for installed programs, cross-platform.
* - Windows: %LOCALAPPDATA%\Programs
* - macOS: /Applications
* - Linux: /opt
*/
function getProgramsBaseDir() {
const platform = process.platform;
if (platform === 'win32') {
return process.env.LOCALAPPDATA
? path.join(process.env.LOCALAPPDATA, 'Programs')
: path.join(os.homedir(), 'AppData', 'Local', 'Programs');
}
if (platform === 'darwin') {
return '/Applications';
}
return '/opt';
}

/**
* Folder name used for an edition's data directory under the app-data base dir.
* Configurable via env so non-default install folder names are supported:
* - CN: TRAE_CN_FOLDER (default "Trae CN")
* - SG: TRAE_SG_FOLDER (default "Trae")
* @param {'cn'|'sg'} edition
* @returns {string}
*/
function getDataFolderName(edition) {
const ed = normalizeEdition(edition);
if (ed === 'cn') {
return process.env.TRAE_CN_FOLDER || 'Trae CN';
}
return process.env.TRAE_SG_FOLDER || 'Trae';
}

/**
* Folder name used for an edition's install directory under the programs base dir.
* Configurable via env:
* - CN: TRAE_CN_INSTALL_FOLDER (default "Trae-CN")
* - SG: TRAE_SG_INSTALL_FOLDER (default "Trae")
* @param {'cn'|'sg'} edition
* @returns {string}
*/
function getInstallFolderName(edition) {
const ed = normalizeEdition(edition);
if (ed === 'cn') {
return process.env.TRAE_CN_INSTALL_FOLDER || 'Trae-CN';
}
return process.env.TRAE_SG_INSTALL_FOLDER || 'Trae';
}

/**
* Resolve the Trae data directory for a given edition.
*
* Priority (highest first):
* 1. Edition-specific env override:
* - CN: TRAE_CN_DATA_DIR
* - SG: TRAE_SG_DATA_DIR
* 2. TRAE_DATA_DIR env var — generic override (backward compat, any edition)
* 3. OS-default path: <appDataBaseDir>/<edition folder name>
*
* @param {'cn'|'sg'} [edition] — defaults to TRAE_EDITION env var, or 'cn'
* @returns {string} absolute path to the Trae data directory
*/
function getTraeDataDir(edition) {
const ed = normalizeEdition(edition);
// 1. Edition-specific explicit override (highest priority)
if (ed === 'cn' && process.env.TRAE_CN_DATA_DIR) {
return process.env.TRAE_CN_DATA_DIR;
}
if (ed === 'sg' && process.env.TRAE_SG_DATA_DIR) {
return process.env.TRAE_SG_DATA_DIR;
}
// 2. Generic override (backward compatibility with auth.js)
if (process.env.TRAE_DATA_DIR) {
return process.env.TRAE_DATA_DIR;
}
// 3. OS defaults
return path.join(getAppDataBaseDir(), getDataFolderName(ed));
}

/**
* Resolve the Trae install directory (where manifest.json lives) for a given edition.
* @param {'cn'|'sg'} [edition] — defaults to TRAE_EDITION env var, or 'cn'
* @returns {string} absolute path to the Trae install directory
*/
function getTraeInstallDir(edition) {
const ed = normalizeEdition(edition);
return path.join(getProgramsBaseDir(), getInstallFolderName(ed));
}

module.exports = {
normalizeEdition,
getDefaultEdition,
getAppDataBaseDir,
getProgramsBaseDir,
getDataFolderName,
getInstallFolderName,
getTraeDataDir,
getTraeInstallDir,
};
16 changes: 4 additions & 12 deletions src/trae-decrypt.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { getTraeDataDir } = require('./paths');

const HEADER_SIZE = 6;
const RANDOM_BYTES_LEN = 32;
Expand Down Expand Up @@ -121,18 +121,14 @@ function decryptStorageValue(base64Value) {

function getStorageJsonPath(dataDir) {
if (!dataDir) {
dataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'Trae');
dataDir = getTraeDataDir();
}
return path.join(dataDir, 'User', 'globalStorage', 'storage.json');
}

function decryptAuthData(dataDir) {
if (!dataDir) {
const cnPath = path.join(os.homedir(), 'AppData', 'Roaming', 'Trae CN', 'User', 'globalStorage', 'storage.json');
const sgPath = path.join(os.homedir(), 'AppData', 'Roaming', 'Trae', 'User', 'globalStorage', 'storage.json');
if (fs.existsSync(cnPath)) dataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'Trae CN');
else if (fs.existsSync(sgPath)) dataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'Trae');
else throw new Error('No Trae data directory found');
dataDir = getTraeDataDir();
}

const storagePath = getStorageJsonPath(dataDir);
Expand All @@ -158,11 +154,7 @@ function decryptAuthData(dataDir) {

function decryptAllEncryptedValues(dataDir) {
if (!dataDir) {
const cnPath = path.join(os.homedir(), 'AppData', 'Roaming', 'Trae CN', 'User', 'globalStorage', 'storage.json');
const sgPath = path.join(os.homedir(), 'AppData', 'Roaming', 'Trae', 'User', 'globalStorage', 'storage.json');
if (fs.existsSync(cnPath)) dataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'Trae CN');
else if (fs.existsSync(sgPath)) dataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'Trae');
else throw new Error('No Trae data directory found');
dataDir = getTraeDataDir();
}

const storagePath = getStorageJsonPath(dataDir);
Expand Down