mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-12 20:53:34 +08:00
feat(ecc2): infer tracked write modifications
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
const {
|
const {
|
||||||
appendFile,
|
appendFile,
|
||||||
getClaudeDir,
|
getClaudeDir,
|
||||||
@@ -161,6 +162,33 @@ function buildPatchPreviewFromContent(content, prefix) {
|
|||||||
return lines.map(line => `${prefix} ${line}`).join('\n');
|
return lines.map(line => `${prefix} ${line}`).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDiffPreviewFromPatchPreview(patchPreview) {
|
||||||
|
if (typeof patchPreview !== 'string' || !patchPreview.trim()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = patchPreview
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const removed = lines.find(line => line.startsWith('- ') || line.startsWith('-'));
|
||||||
|
const added = lines.find(line => line.startsWith('+ ') || line.startsWith('+'));
|
||||||
|
|
||||||
|
if (!removed && !added) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const before = removed ? removed.replace(/^- ?/, '') : '';
|
||||||
|
const after = added ? added.replace(/^\+ ?/, '') : '';
|
||||||
|
if (before && after) {
|
||||||
|
return `${before} -> ${after}`;
|
||||||
|
}
|
||||||
|
if (before) {
|
||||||
|
return `${before} ->`;
|
||||||
|
}
|
||||||
|
return `-> ${after}`;
|
||||||
|
}
|
||||||
|
|
||||||
function inferDefaultFileAction(toolName) {
|
function inferDefaultFileAction(toolName) {
|
||||||
const normalized = String(toolName || '').trim().toLowerCase();
|
const normalized = String(toolName || '').trim().toLowerCase();
|
||||||
if (normalized.includes('read')) {
|
if (normalized.includes('read')) {
|
||||||
@@ -269,6 +297,104 @@ function fileEventPatchPreview(value, action) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runGit(args, cwd) {
|
||||||
|
const result = spawnSync('git', args, {
|
||||||
|
cwd,
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 2500,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error || result.status !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(result.stdout || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitRepoRoot(cwd) {
|
||||||
|
return runGit(['rev-parse', '--show-toplevel'], cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
function repoRelativePath(repoRoot, filePath) {
|
||||||
|
const absolute = path.isAbsolute(filePath)
|
||||||
|
? path.resolve(filePath)
|
||||||
|
: path.resolve(process.cwd(), filePath);
|
||||||
|
const relative = path.relative(repoRoot, absolute);
|
||||||
|
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return relative.split(path.sep).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchPreviewFromGitDiff(repoRoot, repoRelative) {
|
||||||
|
const patch = runGit(
|
||||||
|
['diff', '--no-ext-diff', '--no-color', '--unified=1', '--', repoRelative],
|
||||||
|
repoRoot
|
||||||
|
);
|
||||||
|
if (!patch) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relevant = patch
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.filter(line =>
|
||||||
|
line.startsWith('@@')
|
||||||
|
|| (line.startsWith('+') && !line.startsWith('+++'))
|
||||||
|
|| (line.startsWith('-') && !line.startsWith('---'))
|
||||||
|
)
|
||||||
|
.slice(0, 6);
|
||||||
|
|
||||||
|
if (relevant.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return relevant.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackedInGit(repoRoot, repoRelative) {
|
||||||
|
return runGit(['ls-files', '--error-unmatch', '--', repoRelative], repoRoot) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enrichFileEventFromWorkingTree(toolName, event) {
|
||||||
|
if (!event || typeof event !== 'object' || !event.path) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoRoot = gitRepoRoot(process.cwd());
|
||||||
|
if (!repoRoot) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoRelative = repoRelativePath(repoRoot, event.path);
|
||||||
|
if (!repoRelative) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = String(toolName || '').trim().toLowerCase();
|
||||||
|
const tracked = trackedInGit(repoRoot, repoRelative);
|
||||||
|
const patchPreview = patchPreviewFromGitDiff(repoRoot, repoRelative) || event.patch_preview;
|
||||||
|
const diffPreview = buildDiffPreviewFromPatchPreview(patchPreview) || event.diff_preview;
|
||||||
|
|
||||||
|
if (tool.includes('write')) {
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
action: tracked ? 'modify' : event.action,
|
||||||
|
diff_preview: diffPreview,
|
||||||
|
patch_preview: patchPreview,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracked && patchPreview) {
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
diff_preview: diffPreview,
|
||||||
|
patch_preview: patchPreview,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
function collectFileEvents(toolName, value, events, key = null, parentValue = null) {
|
function collectFileEvents(toolName, value, events, key = null, parentValue = null) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
@@ -375,7 +501,9 @@ function buildActivityRow(input, env = process.env) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toolInput = input?.tool_input || {};
|
const toolInput = input?.tool_input || {};
|
||||||
const fileEvents = extractFileEvents(toolName, toolInput);
|
const fileEvents = extractFileEvents(toolName, toolInput).map(event =>
|
||||||
|
enrichFileEventFromWorkingTree(toolName, event)
|
||||||
|
);
|
||||||
const filePaths = fileEvents.length > 0
|
const filePaths = fileEvents.length > 0
|
||||||
? [...new Set(fileEvents.map(event => event.path))]
|
? [...new Set(fileEvents.map(event => event.path))]
|
||||||
: extractFilePaths(toolInput);
|
: extractFilePaths(toolInput);
|
||||||
|
|||||||
@@ -40,13 +40,14 @@ function withTempHome(homeDir) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function runScript(input, envOverrides = {}) {
|
function runScript(input, envOverrides = {}, options = {}) {
|
||||||
const inputStr = typeof input === 'string' ? input : JSON.stringify(input);
|
const inputStr = typeof input === 'string' ? input : JSON.stringify(input);
|
||||||
const result = spawnSync('node', [script], {
|
const result = spawnSync('node', [script], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
input: inputStr,
|
input: inputStr,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
env: { ...process.env, ...envOverrides },
|
env: { ...process.env, ...envOverrides },
|
||||||
|
cwd: options.cwd,
|
||||||
});
|
});
|
||||||
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
|
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
|
||||||
}
|
}
|
||||||
@@ -210,6 +211,55 @@ function runTests() {
|
|||||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||||
}) ? passed++ : failed++);
|
}) ? passed++ : failed++);
|
||||||
|
|
||||||
|
(test('reclassifies tracked Write activity as modify using git diff context', () => {
|
||||||
|
const tmpHome = makeTempDir();
|
||||||
|
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'session-activity-tracker-repo-'));
|
||||||
|
|
||||||
|
spawnSync('git', ['init'], { cwd: repoDir, encoding: 'utf8' });
|
||||||
|
spawnSync('git', ['config', 'user.email', 'ecc@example.com'], { cwd: repoDir, encoding: 'utf8' });
|
||||||
|
spawnSync('git', ['config', 'user.name', 'ECC Tests'], { cwd: repoDir, encoding: 'utf8' });
|
||||||
|
|
||||||
|
const srcDir = path.join(repoDir, 'src');
|
||||||
|
fs.mkdirSync(srcDir, { recursive: true });
|
||||||
|
const trackedFile = path.join(srcDir, 'app.ts');
|
||||||
|
fs.writeFileSync(trackedFile, 'const count = 1;\n', 'utf8');
|
||||||
|
spawnSync('git', ['add', 'src/app.ts'], { cwd: repoDir, encoding: 'utf8' });
|
||||||
|
spawnSync('git', ['commit', '-m', 'init'], { cwd: repoDir, encoding: 'utf8' });
|
||||||
|
|
||||||
|
fs.writeFileSync(trackedFile, 'const count = 2;\n', 'utf8');
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
tool_name: 'Write',
|
||||||
|
tool_input: {
|
||||||
|
file_path: 'src/app.ts',
|
||||||
|
content: 'const count = 2;\n',
|
||||||
|
},
|
||||||
|
tool_output: { output: 'updated src/app.ts' },
|
||||||
|
};
|
||||||
|
const result = runScript(input, {
|
||||||
|
...withTempHome(tmpHome),
|
||||||
|
CLAUDE_HOOK_EVENT_NAME: 'PostToolUse',
|
||||||
|
ECC_SESSION_ID: 'ecc-session-write-modify',
|
||||||
|
}, {
|
||||||
|
cwd: repoDir,
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.code, 0);
|
||||||
|
|
||||||
|
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'tool-usage.jsonl');
|
||||||
|
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
|
||||||
|
assert.deepStrictEqual(row.file_events, [
|
||||||
|
{
|
||||||
|
path: 'src/app.ts',
|
||||||
|
action: 'modify',
|
||||||
|
diff_preview: 'const count = 1; -> const count = 2;',
|
||||||
|
patch_preview: '@@ -1 +1 @@\n-const count = 1;\n+const count = 2;',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||||
|
fs.rmSync(repoDir, { recursive: true, force: true });
|
||||||
|
}) ? passed++ : failed++);
|
||||||
|
|
||||||
(test('prefers ECC_SESSION_ID over CLAUDE_SESSION_ID and redacts bash summaries', () => {
|
(test('prefers ECC_SESSION_ID over CLAUDE_SESSION_ID and redacts bash summaries', () => {
|
||||||
const tmpHome = makeTempDir();
|
const tmpHome = makeTempDir();
|
||||||
const input = {
|
const input = {
|
||||||
|
|||||||
Reference in New Issue
Block a user