fix: resolve four bug reports (#2290, #2282, #2276, #2272)

- #2290 suggest-compact: honor ECC_CONTEXT_WINDOW_TOKENS / CLAUDE_CODE_AUTO_COMPACT_WINDOW
  so 400k-window models (Opus 4.x) no longer report ~double context usage; add
  override + isolation tests in transcript-context.test.js.
- #2282 install: bare-language syntax is legacy-only by design, but the error
  now distinguishes a supported-but-wrong-mode target (gemini/codex/…) from a
  genuinely unknown one and points to --profile/--modules/--skills.
- #2276 cost-report: the command + cost-tracking skill targeted a SQLite DB no
  tracker writes. Repoint both at the real ~/.claude/metrics/costs.jsonl (JSONL,
  estimated_cost_usd), reduce cumulative-per-session snapshots to latest-per-session,
  and use node instead of sqlite3 for cross-platform support.
- #2272 gateguard: make the 'confirm no existing file' checklist item
  tool-agnostic (Glob/Grep or find/grep via Bash) so hosts without a Glob tool
  don't get a dead tool call.

Full suite 2839/2839; lint green.
This commit is contained in:
Affaan Mustafa
2026-06-18 16:49:58 -04:00
parent 51184b692e
commit b3268fef80
7 changed files with 325 additions and 424 deletions
+21 -30
View File
@@ -25,11 +25,7 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const {
extractCommandSubstitutions,
extractSubshellGroups,
extractBraceGroups
} = require('../lib/shell-substitution');
const { extractCommandSubstitutions, extractSubshellGroups, extractBraceGroups } = require('../lib/shell-substitution');
// Session state — scoped per session to avoid cross-session races.
const STATE_DIR = process.env.GATEGUARD_STATE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard');
@@ -88,10 +84,10 @@ function getExtraDestructiveRegex() {
extraDestructiveCacheRegex = null;
if (!extraDestructiveWarnLogged) {
try {
process.stderr.write(
`[gateguard-fact-force] ignoring invalid GATEGUARD_BASH_EXTRA_DESTRUCTIVE regex: ${err.message}\n`
);
} catch (_) { /* stderr write failure is non-fatal */ }
process.stderr.write(`[gateguard-fact-force] ignoring invalid GATEGUARD_BASH_EXTRA_DESTRUCTIVE regex: ${err.message}\n`);
} catch (_) {
/* stderr write failure is non-fatal */
}
extraDestructiveWarnLogged = true;
}
}
@@ -112,9 +108,7 @@ function isRoutineBashGateDisabled() {
* @returns {string}
*/
function stripQuotedStrings(input) {
return input
.replace(/'(?:[^'\\]|\\.)*'/g, "''")
.replace(/"(?:[^"\\]|\\.)*"/g, '""');
return input.replace(/'(?:[^'\\]|\\.)*'/g, "''").replace(/"(?:[^"\\]|\\.)*"/g, '""');
}
/**
@@ -168,7 +162,6 @@ function tokenize(segment) {
return segment.split(/\s+/).filter(Boolean);
}
/**
* Tokenize a short allowlisted shell command while preserving quoted
* arguments. This is intentionally smaller than a full shell parser: the
@@ -236,7 +229,10 @@ function tokenizeAllowlistedShellWords(input) {
*/
function commandBasename(token) {
if (!token) return '';
return token.replace(/^.*[\\/]/, '').replace(/\.exe$/i, '').toLowerCase();
return token
.replace(/^.*[\\/]/, '')
.replace(/\.exe$/i, '')
.toLowerCase();
}
/**
@@ -553,7 +549,10 @@ function isDestructiveBash(command) {
// that false-negative while also catching `&&`, `;`, `|`, and `||` compound forms.
const bodies = collectExecutableBodies(raw);
for (const body of bodies) {
for (const rawSeg of body.split(/[;|&]+/).map(s => s.trim()).filter(Boolean)) {
for (const rawSeg of body
.split(/[;|&]+/)
.map(s => s.trim())
.filter(Boolean)) {
if (isDestructiveFindExec(rawSeg)) return true;
}
}
@@ -573,7 +572,9 @@ function isDestructiveBash(command) {
// --- State management (per-session, atomic writes, bounded) ---
function normalizeEnvValue(value) {
return String(value || '').trim().toLowerCase();
return String(value || '')
.trim()
.toLowerCase();
}
function isGateGuardDisabled() {
@@ -886,8 +887,7 @@ function isReadOnlyGitIntrospection(command) {
if (args.length === 2) {
const [first, second] = args;
// ref + flag
if (!first.startsWith('--') && /^[a-zA-Z0-9._:/ -]+$/.test(first) &&
(second === '--stat' || second === '--name-only')) {
if (!first.startsWith('--') && /^[a-zA-Z0-9._:/ -]+$/.test(first) && (second === '--stat' || second === '--name-only')) {
return true;
}
return false;
@@ -932,7 +932,7 @@ function writeGateMsg(filePath) {
`Before creating ${safe}, present these facts:`,
'',
'1. Name the file(s) and line(s) that will call this new file',
'2. Confirm no existing file serves the same purpose (use Glob)',
'2. Confirm no existing file serves the same purpose (search the tree — Glob/Grep, or find/grep via Bash)',
'3. If this file reads/writes data files, show field names, structure, and date format (use redacted or synthetic values, not raw production data)',
"4. Quote the user's current instruction verbatim",
'',
@@ -983,11 +983,7 @@ function routineBashMsg() {
function withRecoveryHint(message, hookIds = [EDIT_WRITE_HOOK_ID]) {
const disableTargets = hookIds.map(hookId => `\`${hookId}\``).join(' or ');
return [
message,
'',
`Recovery: if GateGuard is blocking setup or repair work, run this session with \`ECC_GATEGUARD=off\` or add ${disableTargets} to \`ECC_DISABLED_HOOKS\`.`
].join('\n');
return [message, '', `Recovery: if GateGuard is blocking setup or repair work, run this session with \`ECC_GATEGUARD=off\` or add ${disableTargets} to \`ECC_DISABLED_HOOKS\`.`].join('\n');
}
function isSubagentInvocation(data) {
@@ -995,12 +991,7 @@ function isSubagentInvocation(data) {
return false;
}
const candidates = [
data.agent_id,
data.agentId,
data.parent_tool_use_id,
data.parentToolUseId
];
const candidates = [data.agent_id, data.agentId, data.parent_tool_use_id, data.parentToolUseId];
return candidates.some(candidate => typeof candidate === 'string' && candidate.trim());
}
+132 -154
View File
@@ -5,20 +5,12 @@ const { execFileSync } = require('child_process');
const { toCursorAgentRelativePath } = require('./cursor-agent-names');
const { LEGACY_INSTALL_TARGETS, parseInstallArgs } = require('./install/request');
const {
SUPPORTED_INSTALL_TARGETS,
listLegacyCompatibilityLanguages,
resolveLegacyCompatibilitySelection,
resolveInstallPlan,
} = require('./install-manifests');
const { SUPPORTED_INSTALL_TARGETS, listLegacyCompatibilityLanguages, resolveLegacyCompatibilitySelection, resolveInstallPlan } = require('./install-manifests');
const { getInstallTargetAdapter } = require('./install-targets/registry');
const LANGUAGE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
const CLAUDE_ECC_NAMESPACE = 'ecc';
const EXCLUDED_GENERATED_SOURCE_SUFFIXES = [
'/ecc-install-state.json',
'/ecc/install-state.json',
];
const EXCLUDED_GENERATED_SOURCE_SUFFIXES = ['/ecc-install-state.json', '/ecc/install-state.json'];
function getSourceRoot() {
return path.join(__dirname, '../..');
@@ -26,9 +18,7 @@ function getSourceRoot() {
function getPackageVersion(sourceRoot) {
try {
const packageJson = JSON.parse(
fs.readFileSync(path.join(sourceRoot, 'package.json'), 'utf8')
);
const packageJson = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'package.json'), 'utf8'));
return packageJson.version || null;
} catch (_error) {
return null;
@@ -37,9 +27,7 @@ function getPackageVersion(sourceRoot) {
function getManifestVersion(sourceRoot) {
try {
const modulesManifest = JSON.parse(
fs.readFileSync(path.join(sourceRoot, 'manifests', 'install-modules.json'), 'utf8')
);
const modulesManifest = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'manifests', 'install-modules.json'), 'utf8'));
return modulesManifest.version || 1;
} catch (_error) {
return 1;
@@ -52,7 +40,7 @@ function getRepoCommit(sourceRoot) {
cwd: sourceRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5000,
timeout: 5000
}).trim();
} catch (_error) {
return null;
@@ -64,32 +52,35 @@ function readDirectoryNames(dirPath) {
return [];
}
return fs.readdirSync(dirPath, { withFileTypes: true })
return fs
.readdirSync(dirPath, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => entry.name)
.sort();
}
function listAvailableLanguages(sourceRoot = getSourceRoot()) {
return [...new Set([
...listLegacyCompatibilityLanguages(),
...readDirectoryNames(path.join(sourceRoot, 'rules'))
.filter(name => name !== 'common'),
])].sort();
return [...new Set([...listLegacyCompatibilityLanguages(), ...readDirectoryNames(path.join(sourceRoot, 'rules')).filter(name => name !== 'common')])].sort();
}
function validateLegacyTarget(target) {
if (!LEGACY_INSTALL_TARGETS.includes(target)) {
if (LEGACY_INSTALL_TARGETS.includes(target)) {
return;
}
// A target can be fully supported yet not installable via the bare-language
// positional syntax (which is legacy-only). Guide the user to the right mode
// instead of implying the target is unknown (#2282).
if (SUPPORTED_INSTALL_TARGETS.includes(target)) {
throw new Error(
`Unknown install target: ${target}. Expected one of ${LEGACY_INSTALL_TARGETS.join(', ')}`
`Target '${target}' is supported, but the bare-language install syntax only accepts ${LEGACY_INSTALL_TARGETS.join(', ')}. ` +
`Install '${target}' with a component selection instead, e.g. \`install.sh --target ${target} --profile full\` ` +
`(or --modules <id,...> / --skills <id,...>).`
);
}
throw new Error(`Unknown install target: ${target}. Expected one of ${SUPPORTED_INSTALL_TARGETS.join(', ')}`);
}
const IGNORED_DIRECTORY_NAMES = new Set([
'node_modules',
'.git',
]);
const IGNORED_DIRECTORY_NAMES = new Set(['node_modules', '.git']);
function listFilesRecursive(dirPath) {
if (!fs.existsSync(dirPath)) {
@@ -141,7 +132,7 @@ function buildCopyFileOperation({ moduleId, sourcePath, sourceRelativePath, dest
destinationPath,
strategy,
ownership: 'managed',
scaffoldOnly: false,
scaffoldOnly: false
};
}
@@ -156,20 +147,20 @@ function addRecursiveCopyOperations(operations, options) {
for (const relativeFile of relativeFiles) {
const sourceRelativePath = path.join(options.sourceRelativeDir, relativeFile);
const sourcePath = path.join(options.sourceRoot, sourceRelativePath);
const destinationRelativePath = typeof options.destinationRelativePathTransform === 'function'
? options.destinationRelativePathTransform(relativeFile, sourceRelativePath)
: relativeFile;
const destinationRelativePath = typeof options.destinationRelativePathTransform === 'function' ? options.destinationRelativePathTransform(relativeFile, sourceRelativePath) : relativeFile;
if (!destinationRelativePath) {
continue;
}
const destinationPath = path.join(options.destinationDir, destinationRelativePath);
operations.push(buildCopyFileOperation({
moduleId: options.moduleId,
sourcePath,
sourceRelativePath,
destinationPath,
strategy: options.strategy || 'preserve-relative-path',
}));
operations.push(
buildCopyFileOperation({
moduleId: options.moduleId,
sourcePath,
sourceRelativePath,
destinationPath,
strategy: options.strategy || 'preserve-relative-path'
})
);
}
return relativeFiles.length;
@@ -181,13 +172,15 @@ function addFileCopyOperation(operations, options) {
return false;
}
operations.push(buildCopyFileOperation({
moduleId: options.moduleId,
sourcePath,
sourceRelativePath: options.sourceRelativePath,
destinationPath: options.destinationPath,
strategy: options.strategy || 'preserve-relative-path',
}));
operations.push(
buildCopyFileOperation({
moduleId: options.moduleId,
sourcePath,
sourceRelativePath: options.sourceRelativePath,
destinationPath: options.destinationPath,
strategy: options.strategy || 'preserve-relative-path'
})
);
return true;
}
@@ -218,7 +211,7 @@ function addCursorAgentDataScaffoldOperations(operations, options) {
sourceRoot: options.sourceRoot,
sourceRelativePath: path.join('scaffolds', 'cursor', 'ecc-agent-data.json'),
destinationPath: path.join(options.targetRoot, 'ecc-agent-data.json'),
strategy: 'preserve-relative-path',
strategy: 'preserve-relative-path'
});
addFileCopyOperation(operations, {
@@ -226,21 +219,17 @@ function addCursorAgentDataScaffoldOperations(operations, options) {
sourceRoot: options.sourceRoot,
sourceRelativePath: path.join('scaffolds', 'cursor', 'rules', 'ecc-agent-data-home.mdc'),
destinationPath: path.join(options.targetRoot, 'rules', 'ecc-agent-data-home.mdc'),
strategy: 'preserve-relative-path',
strategy: 'preserve-relative-path'
});
addJsonMergeOperation(operations, {
moduleId: options.moduleId,
sourceRoot: options.sourceRoot,
sourceRelativePath: path.join('scaffolds', 'cursor', 'hooks.json'),
destinationPath: path.join(options.targetRoot, 'hooks.json'),
destinationPath: path.join(options.targetRoot, 'hooks.json')
});
const cursorSessionHookDeps = [
path.join('scripts', 'hooks', 'cursor-session-env.js'),
path.join('scripts', 'lib', 'agent-data-home.js'),
path.join('scripts', 'lib', 'utils.js'),
];
const cursorSessionHookDeps = [path.join('scripts', 'hooks', 'cursor-session-env.js'), path.join('scripts', 'lib', 'agent-data-home.js'), path.join('scripts', 'lib', 'utils.js')];
for (const sourceRelativePath of cursorSessionHookDeps) {
addFileCopyOperation(operations, {
@@ -248,7 +237,7 @@ function addCursorAgentDataScaffoldOperations(operations, options) {
sourceRoot: options.sourceRoot,
sourceRelativePath,
destinationPath: path.join(options.targetRoot, sourceRelativePath),
strategy: 'preserve-relative-path',
strategy: 'preserve-relative-path'
});
}
}
@@ -267,7 +256,7 @@ function addJsonMergeOperation(operations, options) {
strategy: 'merge-json',
ownership: 'managed',
scaffoldOnly: false,
mergePayload: readJsonObject(sourcePath, options.sourceRelativePath),
mergePayload: readJsonObject(sourcePath, options.sourceRelativePath)
});
return true;
@@ -279,7 +268,8 @@ function addMatchingRuleOperations(operations, options) {
return 0;
}
const files = fs.readdirSync(sourceDir, { withFileTypes: true })
const files = fs
.readdirSync(sourceDir, { withFileTypes: true })
.filter(entry => entry.isFile() && options.matcher(entry.name))
.map(entry => entry.name)
.sort();
@@ -287,18 +277,17 @@ function addMatchingRuleOperations(operations, options) {
for (const fileName of files) {
const sourceRelativePath = path.join(options.sourceRelativeDir, fileName);
const sourcePath = path.join(options.sourceRoot, sourceRelativePath);
const destinationPath = path.join(
options.destinationDir,
options.rename ? options.rename(fileName) : fileName
);
const destinationPath = path.join(options.destinationDir, options.rename ? options.rename(fileName) : fileName);
operations.push(buildCopyFileOperation({
moduleId: options.moduleId,
sourcePath,
sourceRelativePath,
destinationPath,
strategy: options.strategy || 'flatten-copy',
}));
operations.push(
buildCopyFileOperation({
moduleId: options.moduleId,
sourcePath,
sourceRelativePath,
destinationPath,
strategy: options.strategy || 'flatten-copy'
})
);
}
return files.length;
@@ -324,7 +313,7 @@ function planClaudeStyleLegacyInstall(context, { adapterId, adapterRootInput, ru
moduleId: 'legacy-claude-rules',
sourceRoot: context.sourceRoot,
sourceRelativeDir: path.join('rules', 'common'),
destinationDir: path.join(rulesDir, 'common'),
destinationDir: path.join(rulesDir, 'common')
});
for (const language of context.languages) {
@@ -343,7 +332,7 @@ function planClaudeStyleLegacyInstall(context, { adapterId, adapterRootInput, ru
moduleId: 'legacy-claude-rules',
sourceRoot: context.sourceRoot,
sourceRelativeDir: path.join('rules', language),
destinationDir: path.join(rulesDir, language),
destinationDir: path.join(rulesDir, language)
});
}
@@ -356,7 +345,7 @@ function planClaudeStyleLegacyInstall(context, { adapterId, adapterRootInput, ru
installStatePath,
operations,
warnings,
selectedModules: ['legacy-claude-rules'],
selectedModules: ['legacy-claude-rules']
};
}
@@ -364,7 +353,7 @@ function planClaudeLegacyInstall(context) {
return planClaudeStyleLegacyInstall(context, {
adapterId: 'claude',
adapterRootInput: { homeDir: context.homeDir },
rulesDir: context.claudeRulesDir || null,
rulesDir: context.claudeRulesDir || null
});
}
@@ -372,7 +361,7 @@ function planClaudeProjectLegacyInstall(context) {
return planClaudeStyleLegacyInstall(context, {
adapterId: 'claude-project',
adapterRootInput: { repoRoot: context.projectRoot },
rulesDir: null,
rulesDir: null
});
}
@@ -388,14 +377,12 @@ function planCursorLegacyInstall(context) {
sourceRoot: context.sourceRoot,
sourceRelativeDir: path.join('.cursor', 'rules'),
destinationDir: path.join(targetRoot, 'rules'),
matcher: fileName => /^common-.*\.md$/.test(fileName),
matcher: fileName => /^common-.*\.md$/.test(fileName)
});
for (const language of context.languages) {
if (!LANGUAGE_NAME_PATTERN.test(language)) {
warnings.push(
`Invalid language name '${language}'. Only alphanumeric, dash, and underscore are allowed`
);
warnings.push(`Invalid language name '${language}'. Only alphanumeric, dash, and underscore are allowed`);
continue;
}
@@ -404,7 +391,7 @@ function planCursorLegacyInstall(context) {
sourceRoot: context.sourceRoot,
sourceRelativeDir: path.join('.cursor', 'rules'),
destinationDir: path.join(targetRoot, 'rules'),
matcher: fileName => fileName.startsWith(`${language}-`) && fileName.endsWith('.md'),
matcher: fileName => fileName.startsWith(`${language}-`) && fileName.endsWith('.md')
});
if (matches === 0) {
@@ -417,44 +404,44 @@ function planCursorLegacyInstall(context) {
sourceRoot: context.sourceRoot,
sourceRelativeDir: path.join('.cursor', 'agents'),
destinationDir: path.join(targetRoot, 'agents'),
destinationRelativePathTransform: toCursorAgentRelativePath,
destinationRelativePathTransform: toCursorAgentRelativePath
});
addRecursiveCopyOperations(operations, {
moduleId: 'legacy-cursor-install',
sourceRoot: context.sourceRoot,
sourceRelativeDir: path.join('.cursor', 'skills'),
destinationDir: path.join(targetRoot, 'skills'),
destinationDir: path.join(targetRoot, 'skills')
});
addRecursiveCopyOperations(operations, {
moduleId: 'legacy-cursor-install',
sourceRoot: context.sourceRoot,
sourceRelativeDir: path.join('.cursor', 'commands'),
destinationDir: path.join(targetRoot, 'commands'),
destinationDir: path.join(targetRoot, 'commands')
});
addRecursiveCopyOperations(operations, {
moduleId: 'legacy-cursor-install',
sourceRoot: context.sourceRoot,
sourceRelativeDir: path.join('.cursor', 'hooks'),
destinationDir: path.join(targetRoot, 'hooks'),
destinationDir: path.join(targetRoot, 'hooks')
});
addFileCopyOperation(operations, {
moduleId: 'legacy-cursor-install',
sourceRoot: context.sourceRoot,
sourceRelativePath: path.join('.cursor', 'hooks.json'),
destinationPath: path.join(targetRoot, 'hooks.json'),
destinationPath: path.join(targetRoot, 'hooks.json')
});
addJsonMergeOperation(operations, {
moduleId: 'legacy-cursor-install',
sourceRoot: context.sourceRoot,
sourceRelativePath: '.mcp.json',
destinationPath: path.join(targetRoot, 'mcp.json'),
destinationPath: path.join(targetRoot, 'mcp.json')
});
addCursorAgentDataScaffoldOperations(operations, {
moduleId: 'legacy-cursor-install',
sourceRoot: context.sourceRoot,
targetRoot,
targetRoot
});
return {
@@ -466,7 +453,7 @@ function planCursorLegacyInstall(context) {
installStatePath,
operations,
warnings,
selectedModules: ['legacy-cursor-install'],
selectedModules: ['legacy-cursor-install']
};
}
@@ -478,9 +465,7 @@ function planAntigravityLegacyInstall(context) {
const warnings = [];
if (isDirectoryNonEmpty(path.join(targetRoot, 'rules'))) {
warnings.push(
`Destination ${path.join(targetRoot, 'rules')}/ already exists and files may be overwritten`
);
warnings.push(`Destination ${path.join(targetRoot, 'rules')}/ already exists and files may be overwritten`);
}
addMatchingRuleOperations(operations, {
@@ -489,14 +474,12 @@ function planAntigravityLegacyInstall(context) {
sourceRelativeDir: path.join('rules', 'common'),
destinationDir: path.join(targetRoot, 'rules'),
matcher: fileName => fileName.endsWith('.md'),
rename: fileName => `common-${fileName}`,
rename: fileName => `common-${fileName}`
});
for (const language of context.languages) {
if (!LANGUAGE_NAME_PATTERN.test(language)) {
warnings.push(
`Invalid language name '${language}'. Only alphanumeric, dash, and underscore are allowed`
);
warnings.push(`Invalid language name '${language}'. Only alphanumeric, dash, and underscore are allowed`);
continue;
}
@@ -512,7 +495,7 @@ function planAntigravityLegacyInstall(context) {
sourceRelativeDir: path.join('rules', language),
destinationDir: path.join(targetRoot, 'rules'),
matcher: fileName => fileName.endsWith('.md'),
rename: fileName => `${language}-${fileName}`,
rename: fileName => `${language}-${fileName}`
});
}
@@ -520,19 +503,19 @@ function planAntigravityLegacyInstall(context) {
moduleId: 'legacy-antigravity-install',
sourceRoot: context.sourceRoot,
sourceRelativeDir: 'commands',
destinationDir: path.join(targetRoot, 'workflows'),
destinationDir: path.join(targetRoot, 'workflows')
});
addRecursiveCopyOperations(operations, {
moduleId: 'legacy-antigravity-install',
sourceRoot: context.sourceRoot,
sourceRelativeDir: 'agents',
destinationDir: path.join(targetRoot, 'skills'),
destinationDir: path.join(targetRoot, 'skills')
});
addRecursiveCopyOperations(operations, {
moduleId: 'legacy-antigravity-install',
sourceRoot: context.sourceRoot,
sourceRelativeDir: 'skills',
destinationDir: path.join(targetRoot, 'skills'),
destinationDir: path.join(targetRoot, 'skills')
});
return {
@@ -544,7 +527,7 @@ function planAntigravityLegacyInstall(context) {
installStatePath,
operations,
warnings,
selectedModules: ['legacy-antigravity-install'],
selectedModules: ['legacy-antigravity-install']
};
}
@@ -561,7 +544,7 @@ function createLegacyInstallPlan(options = {}) {
projectRoot,
homeDir,
languages: Array.isArray(options.languages) ? options.languages : [],
claudeRulesDir: options.claudeRulesDir || process.env.CLAUDE_RULES_DIR || null,
claudeRulesDir: options.claudeRulesDir || process.env.CLAUDE_RULES_DIR || null
};
let plan;
@@ -578,7 +561,7 @@ function createLegacyInstallPlan(options = {}) {
const source = {
repoVersion: getPackageVersion(sourceRoot),
repoCommit: getRepoCommit(sourceRoot),
manifestVersion: getManifestVersion(sourceRoot),
manifestVersion: getManifestVersion(sourceRoot)
};
const statePreview = createStatePreview({
@@ -589,14 +572,14 @@ function createLegacyInstallPlan(options = {}) {
profile: null,
modules: [],
legacyLanguages: context.languages,
legacyMode: true,
legacyMode: true
},
resolution: {
selectedModules: plan.selectedModules,
skippedModules: [],
skippedModules: []
},
operations: plan.operations,
source,
source
});
return {
@@ -605,7 +588,7 @@ function createLegacyInstallPlan(options = {}) {
adapter: {
id: plan.adapter.id,
target: plan.adapter.target,
kind: plan.adapter.kind,
kind: plan.adapter.kind
},
targetRoot: plan.targetRoot,
installRoot: plan.installRoot,
@@ -613,7 +596,7 @@ function createLegacyInstallPlan(options = {}) {
warnings: plan.warnings,
languages: context.languages,
operations: plan.operations,
statePreview,
statePreview
};
}
@@ -621,19 +604,15 @@ function createLegacyCompatInstallPlan(options = {}) {
const sourceRoot = options.sourceRoot || getSourceRoot();
const projectRoot = options.projectRoot || process.cwd();
const target = options.target || 'claude';
const includeComponentIds = Array.isArray(options.includeComponentIds)
? [...options.includeComponentIds]
: [];
const excludeComponentIds = Array.isArray(options.excludeComponentIds)
? [...options.excludeComponentIds]
: [];
const includeComponentIds = Array.isArray(options.includeComponentIds) ? [...options.includeComponentIds] : [];
const excludeComponentIds = Array.isArray(options.excludeComponentIds) ? [...options.excludeComponentIds] : [];
validateLegacyTarget(target);
const selection = resolveLegacyCompatibilitySelection({
repoRoot: sourceRoot,
target,
legacyLanguages: options.legacyLanguages || [],
legacyLanguages: options.legacyLanguages || []
});
return createManifestInstallPlan({
@@ -651,25 +630,24 @@ function createLegacyCompatInstallPlan(options = {}) {
requestModuleIds: [],
requestIncludeComponentIds: includeComponentIds,
requestExcludeComponentIds: excludeComponentIds,
mode: 'legacy-compat',
mode: 'legacy-compat'
});
}
function materializeScaffoldOperation(sourceRoot, operation) {
if (operation.kind === 'merge-json') {
return [{
kind: 'merge-json',
moduleId: operation.moduleId,
sourceRelativePath: operation.sourceRelativePath,
destinationPath: operation.destinationPath,
strategy: operation.strategy || 'merge-json',
ownership: operation.ownership || 'managed',
scaffoldOnly: Object.hasOwn(operation, 'scaffoldOnly') ? operation.scaffoldOnly : false,
mergePayload: readJsonObject(
path.join(sourceRoot, operation.sourceRelativePath),
operation.sourceRelativePath
),
}];
return [
{
kind: 'merge-json',
moduleId: operation.moduleId,
sourceRelativePath: operation.sourceRelativePath,
destinationPath: operation.destinationPath,
strategy: operation.strategy || 'merge-json',
ownership: operation.ownership || 'managed',
scaffoldOnly: Object.hasOwn(operation, 'scaffoldOnly') ? operation.scaffoldOnly : false,
mergePayload: readJsonObject(path.join(sourceRoot, operation.sourceRelativePath), operation.sourceRelativePath)
}
];
}
const sourcePath = path.join(sourceRoot, operation.sourceRelativePath);
@@ -683,13 +661,15 @@ function materializeScaffoldOperation(sourceRoot, operation) {
const stat = fs.statSync(sourcePath);
if (stat.isFile()) {
return [buildCopyFileOperation({
moduleId: operation.moduleId,
sourcePath,
sourceRelativePath: operation.sourceRelativePath,
destinationPath: operation.destinationPath,
strategy: operation.strategy,
})];
return [
buildCopyFileOperation({
moduleId: operation.moduleId,
sourcePath,
sourceRelativePath: operation.sourceRelativePath,
destinationPath: operation.destinationPath,
strategy: operation.strategy
})
];
}
const relativeFiles = listFilesRecursive(sourcePath).filter(relativeFile => {
@@ -703,7 +683,7 @@ function materializeScaffoldOperation(sourceRoot, operation) {
sourcePath: path.join(sourcePath, relativeFile),
sourceRelativePath,
destinationPath: path.join(operation.destinationPath, relativeFile),
strategy: operation.strategy,
strategy: operation.strategy
});
});
}
@@ -712,21 +692,19 @@ function createManifestInstallPlan(options = {}) {
const sourceRoot = options.sourceRoot || getSourceRoot();
const projectRoot = options.projectRoot || process.cwd();
const target = options.target || 'claude';
const legacyLanguages = Array.isArray(options.legacyLanguages)
? [...options.legacyLanguages]
: [];
const requestProfileId = Object.hasOwn(options, 'requestProfileId')
? options.requestProfileId
: (options.profileId || null);
const requestModuleIds = Object.hasOwn(options, 'requestModuleIds')
? [...options.requestModuleIds]
: (Array.isArray(options.moduleIds) ? [...options.moduleIds] : []);
const legacyLanguages = Array.isArray(options.legacyLanguages) ? [...options.legacyLanguages] : [];
const requestProfileId = Object.hasOwn(options, 'requestProfileId') ? options.requestProfileId : options.profileId || null;
const requestModuleIds = Object.hasOwn(options, 'requestModuleIds') ? [...options.requestModuleIds] : Array.isArray(options.moduleIds) ? [...options.moduleIds] : [];
const requestIncludeComponentIds = Object.hasOwn(options, 'requestIncludeComponentIds')
? [...options.requestIncludeComponentIds]
: (Array.isArray(options.includeComponentIds) ? [...options.includeComponentIds] : []);
: Array.isArray(options.includeComponentIds)
? [...options.includeComponentIds]
: [];
const requestExcludeComponentIds = Object.hasOwn(options, 'requestExcludeComponentIds')
? [...options.requestExcludeComponentIds]
: (Array.isArray(options.excludeComponentIds) ? [...options.excludeComponentIds] : []);
: Array.isArray(options.excludeComponentIds)
? [...options.excludeComponentIds]
: [];
const plan = resolveInstallPlan({
repoRoot: sourceRoot,
projectRoot,
@@ -735,14 +713,14 @@ function createManifestInstallPlan(options = {}) {
moduleIds: options.moduleIds || [],
includeComponentIds: options.includeComponentIds || [],
excludeComponentIds: options.excludeComponentIds || [],
target,
target
});
const adapter = getInstallTargetAdapter(target);
const operations = plan.operations.flatMap(operation => materializeScaffoldOperation(sourceRoot, operation));
const source = {
repoVersion: getPackageVersion(sourceRoot),
repoCommit: getRepoCommit(sourceRoot),
manifestVersion: getManifestVersion(sourceRoot),
manifestVersion: getManifestVersion(sourceRoot)
};
const statePreview = createStatePreview({
adapter,
@@ -754,14 +732,14 @@ function createManifestInstallPlan(options = {}) {
includeComponents: requestIncludeComponentIds,
excludeComponents: requestExcludeComponentIds,
legacyLanguages,
legacyMode: Boolean(options.legacyMode),
legacyMode: Boolean(options.legacyMode)
},
resolution: {
selectedModules: plan.selectedModuleIds,
skippedModules: plan.skippedModuleIds,
skippedModules: plan.skippedModuleIds
},
operations,
source,
source
});
return {
@@ -770,7 +748,7 @@ function createManifestInstallPlan(options = {}) {
adapter: {
id: adapter.id,
target: adapter.target,
kind: adapter.kind,
kind: adapter.kind
},
targetRoot: plan.targetRoot,
installRoot: plan.targetRoot,
@@ -787,7 +765,7 @@ function createManifestInstallPlan(options = {}) {
skippedModuleIds: plan.skippedModuleIds,
excludedModuleIds: plan.excludedModuleIds,
operations,
statePreview,
statePreview
};
}
@@ -800,5 +778,5 @@ module.exports = {
createLegacyInstallPlan,
getSourceRoot,
listAvailableLanguages,
parseInstallArgs,
parseInstallArgs
};
+14 -15
View File
@@ -98,9 +98,7 @@ function readLatestContextTokens(transcriptPath, options = {}) {
return null;
}
const tailBytes = Number.isInteger(options.tailBytes) && options.tailBytes > 0
? options.tailBytes
: DEFAULT_TRANSCRIPT_TAIL_BYTES;
const tailBytes = Number.isInteger(options.tailBytes) && options.tailBytes > 0 ? options.tailBytes : DEFAULT_TRANSCRIPT_TAIL_BYTES;
const tail = readFileTail(transcriptPath, tailBytes);
if (!tail) {
@@ -124,9 +122,7 @@ function readLatestContextTokens(transcriptPath, options = {}) {
const tokens = extractUsageTokens(record);
if (tokens > 0) {
const model = record.message && typeof record.message.model === 'string'
? record.message.model
: '';
const model = record.message && typeof record.message.model === 'string' ? record.message.model : '';
return { tokens, model };
}
}
@@ -141,6 +137,15 @@ function readLatestContextTokens(transcriptPath, options = {}) {
* suffix); otherwise the standard 200k window.
*/
function resolveContextWindowTokens(tokens, model) {
// Explicit window override wins: 400k models (e.g. Opus 4.x) match neither the
// 200k default nor the 1M marker and would otherwise report ~double usage (#2290).
// Honor ECC's own knob and Claude Code's native CLAUDE_CODE_AUTO_COMPACT_WINDOW.
const env = (typeof process !== 'undefined' && process.env) || {};
const envWindow = Number.parseInt(env.ECC_CONTEXT_WINDOW_TOKENS || env.CLAUDE_CODE_AUTO_COMPACT_WINDOW || '', 10);
if (Number.isInteger(envWindow) && envWindow > 0) {
return envWindow;
}
if (typeof model === 'string' && model.includes(LARGE_WINDOW_MODEL_MARKER)) {
return LARGE_CONTEXT_WINDOW_TOKENS;
}
@@ -169,9 +174,7 @@ function resolveContextThreshold(env, windowTokens) {
}
}
return windowTokens >= LARGE_CONTEXT_WINDOW_TOKENS
? DEFAULT_CONTEXT_THRESHOLD_LARGE
: DEFAULT_CONTEXT_THRESHOLD_STANDARD;
return windowTokens >= LARGE_CONTEXT_WINDOW_TOKENS ? DEFAULT_CONTEXT_THRESHOLD_LARGE : DEFAULT_CONTEXT_THRESHOLD_STANDARD;
}
/**
@@ -181,9 +184,7 @@ function resolveContextThreshold(env, windowTokens) {
function resolveContextInterval(env) {
const raw = env && env.COMPACT_CONTEXT_INTERVAL;
const parsed = Number.parseInt(raw, 10);
return Number.isInteger(parsed) && parsed > 0 && parsed <= MAX_TOKEN_SETTING
? parsed
: DEFAULT_CONTEXT_INTERVAL_TOKENS;
return Number.isInteger(parsed) && parsed > 0 && parsed <= MAX_TOKEN_SETTING ? parsed : DEFAULT_CONTEXT_INTERVAL_TOKENS;
}
/**
@@ -205,9 +206,7 @@ function computeContextBucket(tokens, threshold, interval) {
* Human-readable label for a context window size (e.g. "200k", "1M").
*/
function formatWindowLabel(windowTokens) {
return windowTokens >= LARGE_CONTEXT_WINDOW_TOKENS
? '1M'
: `${Math.round(windowTokens / 1000)}k`;
return windowTokens >= LARGE_CONTEXT_WINDOW_TOKENS ? '1M' : `${Math.round(windowTokens / 1000)}k`;
}
module.exports = {