d48cda4081
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled
200 lines
6.3 KiB
JavaScript
200 lines
6.3 KiB
JavaScript
/**
|
|
* Resolve ECC agent data home (memory persistence root) across harnesses.
|
|
*
|
|
* Docstring policy: public entry points here are documented; small internal
|
|
* helpers (e.g. `expandHomePath`, `readProjectConfigAt`) are left undocumented on
|
|
* purpose, consistent with ECC script modules elsewhere. Automated PR reviewers
|
|
* (e.g. CodeRabbit) may still flag low JSDoc coverage against a high threshold on
|
|
* the diff—that check is informational for this repo, not a bar every helper in
|
|
* touched files must meet. Prefer clarity in code and tests over blanket JSDoc on
|
|
* private helpers unless maintainers adopt a project-wide coverage rule.
|
|
*
|
|
* @see https://github.com/affaan-m/ECC/issues/2065
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const AGENT_DATA_HOME_ENV = 'ECC_AGENT_DATA_HOME';
|
|
const DEFAULT_CLAUDE_DIR_NAME = '.claude';
|
|
const DEFAULT_CURSOR_ECC_DIR_SEGMENTS = ['.cursor', 'ecc'];
|
|
const PROJECT_CONFIG_RELATIVE = path.join('.cursor', 'ecc-agent-data.json');
|
|
|
|
/**
|
|
* Home directory for tilde expansion and default agent-data paths.
|
|
*
|
|
* Intentionally mirrors `getHomeDir()` in `scripts/lib/utils.js` (HOME/USERPROFILE,
|
|
* then `os.homedir()`). Do not import `utils.getHomeDir` here: `utils.js` already
|
|
* requires this module (`resolveAgentDataHome`), which would create a circular
|
|
* dependency and risk divergent defaults for `~/.cursor/ecc` vs `~/.claude`.
|
|
*
|
|
* If consolidation is needed later, prefer one of:
|
|
*
|
|
* | Approach | Tradeoff |
|
|
* | --- | --- |
|
|
* | Shared `scripts/lib/home-dir.js` imported by both | Clean; breaks the cycle |
|
|
* | Keep duplicate + cross-reference comment (this file) | Zero require risk |
|
|
* | Move all resolution here; thin-wrap from `utils` | Larger refactor |
|
|
*/
|
|
function getHomeDirFromEnv() {
|
|
const explicitHome = process.env.HOME || process.env.USERPROFILE;
|
|
if (explicitHome && String(explicitHome).trim().length > 0) {
|
|
return path.resolve(explicitHome);
|
|
}
|
|
return require('os').homedir();
|
|
}
|
|
|
|
function expandHomePath(value, baseDir) {
|
|
if (!value || typeof value !== 'string') return null;
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return null;
|
|
if (trimmed.startsWith('~')) {
|
|
const remainder = trimmed.slice(1).replace(/^[/\\]+/, '');
|
|
return remainder ? path.join(getHomeDirFromEnv(), remainder) : getHomeDirFromEnv();
|
|
}
|
|
if (path.isAbsolute(trimmed)) {
|
|
return path.resolve(trimmed);
|
|
}
|
|
const base = baseDir && String(baseDir).trim()
|
|
? path.resolve(baseDir)
|
|
: process.cwd();
|
|
return path.resolve(base, trimmed);
|
|
}
|
|
|
|
/**
|
|
* Project root for a config file under .cursor/ecc-agent-data.json.
|
|
*/
|
|
function resolveProjectRootFromConfigPath(configPath) {
|
|
const configDir = path.dirname(path.resolve(configPath));
|
|
if (path.basename(configDir) === '.cursor') {
|
|
return path.dirname(configDir);
|
|
}
|
|
return configDir;
|
|
}
|
|
|
|
/**
|
|
* True when the current process is a Cursor hook subprocess.
|
|
* Cursor documents CURSOR_VERSION and CURSOR_PROJECT_DIR for hook scripts.
|
|
*/
|
|
function isCursorHookRuntime() {
|
|
if (process.env.CURSOR_VERSION && String(process.env.CURSOR_VERSION).trim()) {
|
|
return true;
|
|
}
|
|
if (process.env.CURSOR_PROJECT_DIR && String(process.env.CURSOR_PROJECT_DIR).trim()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getDefaultCursorAgentDataHome() {
|
|
return path.join(getHomeDirFromEnv(), ...DEFAULT_CURSOR_ECC_DIR_SEGMENTS);
|
|
}
|
|
|
|
function getDefaultClaudeAgentDataHome() {
|
|
return path.join(getHomeDirFromEnv(), DEFAULT_CLAUDE_DIR_NAME);
|
|
}
|
|
|
|
function readProjectConfigAt(configPath) {
|
|
if (!configPath || typeof configPath !== 'string') return null;
|
|
if (!fs.existsSync(configPath)) return null;
|
|
|
|
try {
|
|
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
const candidate = parsed.agentDataHome || parsed.ECC_AGENT_DATA_HOME;
|
|
if (typeof candidate !== 'string' || !candidate.trim()) return null;
|
|
const projectRoot = resolveProjectRootFromConfigPath(configPath);
|
|
return expandHomePath(candidate, projectRoot);
|
|
} catch (error) {
|
|
console.error(
|
|
`[ECC] Failed to read or parse agent data config at ${configPath}: ${error.message}`
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function readProjectConfig(projectDir) {
|
|
if (!projectDir || typeof projectDir !== 'string') return null;
|
|
return readProjectConfigAt(path.join(path.resolve(projectDir), PROJECT_CONFIG_RELATIVE));
|
|
}
|
|
|
|
function resolveProjectDir() {
|
|
const candidates = [
|
|
process.env.CURSOR_PROJECT_DIR,
|
|
process.env.CLAUDE_PROJECT_DIR,
|
|
process.cwd(),
|
|
];
|
|
|
|
for (const candidate of candidates) {
|
|
if (!candidate || typeof candidate !== 'string') continue;
|
|
const resolved = path.resolve(candidate);
|
|
if (fs.existsSync(path.join(resolved, '.cursor'))) {
|
|
return resolved;
|
|
}
|
|
}
|
|
|
|
return process.cwd();
|
|
}
|
|
|
|
/**
|
|
* Resolve agent data home without mutating process.env.
|
|
*/
|
|
function resolveAgentDataHome(options = {}) {
|
|
const fromEnv = expandHomePath(process.env[AGENT_DATA_HOME_ENV]);
|
|
if (fromEnv) return fromEnv;
|
|
|
|
const projectDir = options.projectDir || resolveProjectDir();
|
|
const fromProject = readProjectConfig(projectDir);
|
|
if (fromProject) return fromProject;
|
|
|
|
if (options.preferCursorDefault === true || isCursorHookRuntime()) {
|
|
return getDefaultCursorAgentDataHome();
|
|
}
|
|
|
|
return getDefaultClaudeAgentDataHome();
|
|
}
|
|
|
|
/**
|
|
* Set ECC_AGENT_DATA_HOME on the current process when unset (hook subprocess safety net).
|
|
* @returns {string} Resolved agent data home
|
|
*/
|
|
function ensureAgentDataHomeEnv(options = {}) {
|
|
const resolved = resolveAgentDataHome(options);
|
|
if (!expandHomePath(process.env[AGENT_DATA_HOME_ENV])) {
|
|
process.env[AGENT_DATA_HOME_ENV] = resolved;
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
/**
|
|
* Build Cursor sessionStart hook output env payload.
|
|
*/
|
|
function getCursorSessionEnvPayload(options = {}) {
|
|
const agentDataHome = resolveAgentDataHome({
|
|
...options,
|
|
preferCursorDefault: true,
|
|
});
|
|
|
|
return {
|
|
ECC_AGENT_DATA_HOME: agentDataHome,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
AGENT_DATA_HOME_ENV,
|
|
DEFAULT_CLAUDE_DIR_NAME,
|
|
DEFAULT_CURSOR_ECC_DIR_SEGMENTS,
|
|
PROJECT_CONFIG_RELATIVE,
|
|
expandHomePath,
|
|
resolveProjectRootFromConfigPath,
|
|
isCursorHookRuntime,
|
|
getDefaultCursorAgentDataHome,
|
|
getDefaultClaudeAgentDataHome,
|
|
readProjectConfig,
|
|
readProjectConfigAt,
|
|
resolveProjectDir,
|
|
resolveAgentDataHome,
|
|
ensureAgentDataHomeEnv,
|
|
getCursorSessionEnvPayload,
|
|
};
|