Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

119 lines
4.6 KiB
JavaScript

// File-system access helpers for Trae SOLO local state.
//
// TRAE SOLO stores its conversation / skill / task state on disk under:
// - ~/.trae/ — user-scope (skills, extensions, rules)
// - ~/Library/Application Support/TRAE SOLO/ModularData/ai-agent/
// — per-task storage (snapshots, configs)
// All file accesses here are local-only and read-only by default. Write
// helpers (used by skill-fs-install / task-fs-delete / etc.) require the
// caller's explicit --yes flag, mirroring the safety pattern used by the
// other adapters (grok delete, antigravity delete, etc.).
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { CommandExecutionError } from '@jackwener/opencli/errors';
export const TRAE_USER_DIR = path.join(os.homedir(), '.trae');
export const TRAE_SKILLS_DIR = path.join(TRAE_USER_DIR, 'skills');
export const TRAE_SKILL_CONFIG = path.join(TRAE_USER_DIR, 'skill-config.json');
export const TRAE_USER_RULES = path.join(TRAE_USER_DIR, 'user_rules.md');
export const TRAE_APP_SUPPORT = path.join(
os.homedir(),
'Library/Application Support/TRAE SOLO',
);
export const TRAE_AI_AGENT_DIR = path.join(
TRAE_APP_SUPPORT,
'ModularData/ai-agent',
);
export const TRAE_SNAPSHOT_DIR = path.join(TRAE_AI_AGENT_DIR, 'snapshot');
export const TRAE_AGENTCONFIG_DIR = path.join(TRAE_AI_AGENT_DIR, 'agentconfig');
export const TRAE_WORK_MODE_PROJECTS = path.join(
TRAE_AI_AGENT_DIR,
'work-mode-projects',
);
export const TRAE_DB_WAL = path.join(TRAE_AI_AGENT_DIR, 'database.db-wal');
// Quick existence + readable check.
export function assertReadable(p, label) {
if (!fs.existsSync(p)) {
throw new CommandExecutionError(
`${label} not found: ${p}`,
'Is Trae SOLO installed and run at least once?',
);
}
}
// Parse a Markdown SKILL.md and return its YAML-style front-matter as a
// plain object, plus a one-line description (first non-frontmatter
// non-heading paragraph). Handles missing/malformed front-matter
// gracefully — returns whatever it can.
export function parseSkillMd(skillDir) {
const skillMdPath = path.join(skillDir, 'SKILL.md');
if (!fs.existsSync(skillMdPath)) {
return { name: path.basename(skillDir), description: '', tags: [] };
}
const content = fs.readFileSync(skillMdPath, 'utf-8');
const fm = {};
let body = content;
if (content.startsWith('---\n')) {
const end = content.indexOf('\n---\n', 4);
if (end > 0) {
const fmText = content.slice(4, end);
for (const line of fmText.split('\n')) {
const m = line.match(/^([\w_-]+):\s*(.*)$/);
if (m) fm[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
}
body = content.slice(end + 5);
}
}
// First non-empty, non-heading line of body = description fallback.
if (!fm.description) {
for (const line of body.split('\n')) {
const t = line.trim();
if (!t) continue;
if (t.startsWith('#')) continue;
fm.description = t.slice(0, 200);
break;
}
}
return {
name: fm.name || path.basename(skillDir),
description: fm.description || '',
tags: fm.tags ? fm.tags.split(/\s+/) : [],
version: fm.version || '',
author: fm.author || '',
path: skillDir,
};
}
// Load skill-config.json (Trae's installed-skills registry).
export function readSkillConfig() {
assertReadable(TRAE_SKILL_CONFIG, 'skill-config.json');
return JSON.parse(fs.readFileSync(TRAE_SKILL_CONFIG, 'utf-8'));
}
// Atomically update skill-config.json — read, mutate via callback, write
// via tmp + rename to avoid Trae seeing a half-written file.
export function updateSkillConfig(mutate) {
const conf = readSkillConfig();
mutate(conf);
const tmp = TRAE_SKILL_CONFIG + '.tmp-' + process.pid;
fs.writeFileSync(tmp, JSON.stringify(conf, null, 4));
fs.renameSync(tmp, TRAE_SKILL_CONFIG);
}
// Refuse to mutate ai-agent on-disk state if database.db-wal was touched
// in the last `windowSec` seconds — Trae is probably writing.
export function checkAgentDbQuiet(windowSec = 5) {
if (!fs.existsSync(TRAE_DB_WAL)) return; // first run
const ageMs = Date.now() - fs.statSync(TRAE_DB_WAL).mtimeMs;
if (ageMs < windowSec * 1000) {
throw new CommandExecutionError(
`Trae is actively writing (database.db-wal touched ${(ageMs / 1000).toFixed(1)}s ago).`,
`Wait ${windowSec}+ s for Trae to settle, or quit Trae SOLO before mutating state.`,
);
}
}