@xelth/eck-snapshot 6.5.1 → 6.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/cli/cli.js +4 -0
- package/src/cli/commands/createSnapshot.js +18 -9
- package/src/cli/commands/recon.js +308 -283
- package/src/cli/commands/setupMcp.js +2 -0
- package/src/cli/commands/updateSnapshot.js +126 -114
- package/src/utils/fileUtils.js +1084 -1081
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import ora from 'ora';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import isBinaryPath from 'is-binary-path';
|
|
6
|
-
import { getGitAnchor, getChangedFiles } from '../../utils/gitUtils.js';
|
|
7
|
-
import { loadSetupConfig } from '../../config.js';
|
|
8
|
-
import { readFileWithSizeCheck, parseSize, formatSize, matchesPattern, loadGitignore, generateTimestamp, getShortRepoName } from '../../utils/fileUtils.js';
|
|
9
|
-
import { detectProjectType, getProjectSpecificFiltering } from '../../utils/projectDetector.js';
|
|
10
|
-
import { execa } from 'execa';
|
|
11
|
-
import { fileURLToPath } from 'url';
|
|
12
|
-
import { pushTelemetry } from '../../utils/telemetry.js';
|
|
13
|
-
import { syncTokenWeights } from '../../utils/tokenEstimator.js';
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import isBinaryPath from 'is-binary-path';
|
|
6
|
+
import { getGitAnchor, getChangedFiles } from '../../utils/gitUtils.js';
|
|
7
|
+
import { loadSetupConfig } from '../../config.js';
|
|
8
|
+
import { readFileWithSizeCheck, parseSize, formatSize, matchesPattern, loadGitignore, generateTimestamp, getShortRepoName, ensureSnapshotsInGitignore, readMlModelMetadata } from '../../utils/fileUtils.js';
|
|
9
|
+
import { detectProjectType, getProjectSpecificFiltering } from '../../utils/projectDetector.js';
|
|
10
|
+
import { execa } from 'execa';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { pushTelemetry } from '../../utils/telemetry.js';
|
|
13
|
+
import { syncTokenWeights } from '../../utils/tokenEstimator.js';
|
|
14
14
|
|
|
15
15
|
// Mirror the same hidden-path guard used in createSnapshot.js
|
|
16
16
|
function isHiddenPath(filePath) {
|
|
@@ -52,95 +52,105 @@ async function generateSnapshotContent(repoPath, changedFiles, anchor, config, g
|
|
|
52
52
|
let includedCount = 0;
|
|
53
53
|
const fileList = [];
|
|
54
54
|
|
|
55
|
-
// Include Agent Report if it exists and hasn't been embedded yet
|
|
56
|
-
let agentReport = null;
|
|
57
|
-
const reportPath = path.join(repoPath, '.eck', 'lastsnapshot', 'AnswerToSA.md');
|
|
58
|
-
const lockPath = path.join(repoPath, '.eck', 'lastsnapshot', 'AnswerToSA.lock');
|
|
59
|
-
try {
|
|
60
|
-
// Use atomic directory creation as a lock to prevent race conditions
|
|
61
|
-
await fs.mkdir(lockPath);
|
|
62
|
-
const reportContent = await fs.readFile(reportPath, 'utf-8');
|
|
63
|
-
|
|
64
|
-
if (!reportContent.includes('[SYSTEM: EMBEDDED]')) {
|
|
65
|
-
agentReport = reportContent;
|
|
66
|
-
|
|
67
|
-
// Immediately mark as embedded to release the race window
|
|
68
|
-
await fs.appendFile(reportPath, '\n\n[SYSTEM: EMBEDDED]\n', 'utf-8');
|
|
69
|
-
|
|
70
|
-
// Auto-Journaling: prepend agent report to JOURNAL.md
|
|
71
|
-
const journalPath = path.join(repoPath, '.eck', 'JOURNAL.md');
|
|
72
|
-
try {
|
|
73
|
-
const dateStr = new Date().toISOString().split('T')[0];
|
|
74
|
-
const journalEntry = `## ${dateStr} — Agent Report\n\n${reportContent.trim()}\n`;
|
|
75
|
-
|
|
76
|
-
let existingJournal = '';
|
|
77
|
-
try {
|
|
78
|
-
existingJournal = await fs.readFile(journalPath, 'utf-8');
|
|
79
|
-
} catch (e) { /* might not exist */ }
|
|
80
|
-
|
|
81
|
-
const insertPos = existingJournal.indexOf('\n## ');
|
|
82
|
-
if (insertPos !== -1) {
|
|
83
|
-
const newJournal = existingJournal.slice(0, insertPos) + '\n\n' + journalEntry + existingJournal.slice(insertPos);
|
|
84
|
-
await fs.writeFile(journalPath, newJournal, 'utf-8');
|
|
85
|
-
} else {
|
|
86
|
-
await fs.writeFile(journalPath, (existingJournal ? existingJournal + '\n\n' : '') + journalEntry + '\n', 'utf-8');
|
|
87
|
-
}
|
|
88
|
-
} catch (je) {
|
|
89
|
-
console.warn('Could not auto-update JOURNAL.md', je.message);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
await fs.rmdir(lockPath);
|
|
93
|
-
} catch (e) {
|
|
94
|
-
// File not found or locked by another process
|
|
95
|
-
try { await fs.rmdir(lockPath); } catch (_) {}
|
|
96
|
-
}
|
|
55
|
+
// Include Agent Report if it exists and hasn't been embedded yet
|
|
56
|
+
let agentReport = null;
|
|
57
|
+
const reportPath = path.join(repoPath, '.eck', 'lastsnapshot', 'AnswerToSA.md');
|
|
58
|
+
const lockPath = path.join(repoPath, '.eck', 'lastsnapshot', 'AnswerToSA.lock');
|
|
59
|
+
try {
|
|
60
|
+
// Use atomic directory creation as a lock to prevent race conditions
|
|
61
|
+
await fs.mkdir(lockPath);
|
|
62
|
+
const reportContent = await fs.readFile(reportPath, 'utf-8');
|
|
63
|
+
|
|
64
|
+
if (!reportContent.includes('[SYSTEM: EMBEDDED]')) {
|
|
65
|
+
agentReport = reportContent;
|
|
66
|
+
|
|
67
|
+
// Immediately mark as embedded to release the race window
|
|
68
|
+
await fs.appendFile(reportPath, '\n\n[SYSTEM: EMBEDDED]\n', 'utf-8');
|
|
69
|
+
|
|
70
|
+
// Auto-Journaling: prepend agent report to JOURNAL.md
|
|
71
|
+
const journalPath = path.join(repoPath, '.eck', 'JOURNAL.md');
|
|
72
|
+
try {
|
|
73
|
+
const dateStr = new Date().toISOString().split('T')[0];
|
|
74
|
+
const journalEntry = `## ${dateStr} — Agent Report\n\n${reportContent.trim()}\n`;
|
|
75
|
+
|
|
76
|
+
let existingJournal = '';
|
|
77
|
+
try {
|
|
78
|
+
existingJournal = await fs.readFile(journalPath, 'utf-8');
|
|
79
|
+
} catch (e) { /* might not exist */ }
|
|
80
|
+
|
|
81
|
+
const insertPos = existingJournal.indexOf('\n## ');
|
|
82
|
+
if (insertPos !== -1) {
|
|
83
|
+
const newJournal = existingJournal.slice(0, insertPos) + '\n\n' + journalEntry + existingJournal.slice(insertPos);
|
|
84
|
+
await fs.writeFile(journalPath, newJournal, 'utf-8');
|
|
85
|
+
} else {
|
|
86
|
+
await fs.writeFile(journalPath, (existingJournal ? existingJournal + '\n\n' : '') + journalEntry + '\n', 'utf-8');
|
|
87
|
+
}
|
|
88
|
+
} catch (je) {
|
|
89
|
+
console.warn('Could not auto-update JOURNAL.md', je.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await fs.rmdir(lockPath);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// File not found or locked by another process
|
|
95
|
+
try { await fs.rmdir(lockPath); } catch (_) {}
|
|
96
|
+
}
|
|
97
97
|
|
|
98
98
|
const cleanDirsToIgnore = (config.dirsToIgnore || []).map(d => d.replace(/\/$/, ''));
|
|
99
99
|
|
|
100
|
-
for (const filePath of changedFiles) {
|
|
101
|
-
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
102
|
-
|
|
103
|
-
// Skip hidden paths (.idea/, .vscode/, etc.) — mirrors createSnapshot.js
|
|
104
|
-
if (isHiddenPath(normalizedPath)) continue;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
100
|
+
for (const filePath of changedFiles) {
|
|
101
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
102
|
+
|
|
103
|
+
// Skip hidden paths (.idea/, .vscode/, etc.) — mirrors createSnapshot.js
|
|
104
|
+
if (isHiddenPath(normalizedPath)) continue;
|
|
105
|
+
|
|
106
|
+
const mlExt = path.extname(filePath).toLowerCase();
|
|
107
|
+
const ML_EXTENSIONS = ['.safetensors', '.onnx', '.pt', '.pth', '.h5', '.pb', '.bin', '.ckpt', '.gguf'];
|
|
108
|
+
const isMlModel = ML_EXTENSIONS.includes(mlExt);
|
|
109
|
+
|
|
110
|
+
// Skip binary files — mirrors createSnapshot.js
|
|
111
|
+
if (isBinaryPath(filePath) && !isMlModel) continue;
|
|
112
|
+
|
|
113
|
+
const pathParts = normalizedPath.split('/');
|
|
114
|
+
let isIgnoredDir = false;
|
|
115
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
116
|
+
if (cleanDirsToIgnore.includes(pathParts[i])) {
|
|
117
|
+
isIgnoredDir = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (isIgnoredDir) continue;
|
|
122
|
+
|
|
123
|
+
const fileExt = path.extname(filePath);
|
|
124
|
+
// Use matchesPattern (glob support) instead of exact includes() — mirrors createSnapshot.js
|
|
125
|
+
if (config.filesToIgnore && matchesPattern(normalizedPath, config.filesToIgnore)) continue;
|
|
126
|
+
if (fileExt && config.extensionsToIgnore?.includes(fileExt)) continue;
|
|
127
|
+
if (gitignore.ignores(normalizedPath)) continue;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const fullPath = path.join(repoPath, filePath);
|
|
131
|
+
|
|
132
|
+
// Explicitly check if file was deleted
|
|
133
|
+
try {
|
|
134
|
+
await fs.access(fullPath);
|
|
135
|
+
} catch (accessErr) {
|
|
136
|
+
contentOutput += `--- File: /${normalizedPath} ---\n\n[FILE DELETED]\n\n`;
|
|
137
|
+
fileList.push(`- ${normalizedPath} (Deleted)`);
|
|
138
|
+
includedCount++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let content;
|
|
143
|
+
if (isMlModel) {
|
|
144
|
+
content = await readMlModelMetadata(fullPath);
|
|
145
|
+
} else {
|
|
146
|
+
content = await readFileWithSizeCheck(fullPath, parseSize(config.maxFileSize));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
contentOutput += `--- File: /${normalizedPath} ---\n\n${content}\n\n`;
|
|
150
|
+
fileList.push(`- ${normalizedPath} (Modified/Added)`);
|
|
151
|
+
includedCount++;
|
|
152
|
+
} catch (e) { /* Skip */ }
|
|
153
|
+
}
|
|
144
154
|
|
|
145
155
|
// Load Template
|
|
146
156
|
const templatePath = path.join(__dirname, '../../templates/update-prompt.template.md');
|
|
@@ -196,11 +206,11 @@ export async function updateSnapshot(repoPath, options) {
|
|
|
196
206
|
|
|
197
207
|
spinner.start('Generating update snapshot...');
|
|
198
208
|
|
|
199
|
-
const changedFiles = await getChangedFiles(repoPath, anchor, options.fail);
|
|
200
|
-
if (changedFiles.length === 0) {
|
|
201
|
-
spinner.succeed('No changes detected since last full snapshot.');
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
209
|
+
const changedFiles = await getChangedFiles(repoPath, anchor, options.fail);
|
|
210
|
+
if (changedFiles.length === 0) {
|
|
211
|
+
spinner.succeed('No changes detected since last full snapshot.');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
204
214
|
|
|
205
215
|
const setupConfig = await loadSetupConfig();
|
|
206
216
|
let config = { ...setupConfig.fileFiltering, ...setupConfig.performance, ...options };
|
|
@@ -247,6 +257,7 @@ export async function updateSnapshot(repoPath, options) {
|
|
|
247
257
|
const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
|
|
248
258
|
|
|
249
259
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
260
|
+
await ensureSnapshotsInGitignore(repoPath);
|
|
250
261
|
await fs.writeFile(outputPath, fullContent);
|
|
251
262
|
|
|
252
263
|
spinner.succeed(`Update snapshot created: .eck/snapshots/${outputFilename}`);
|
|
@@ -352,6 +363,7 @@ export async function updateSnapshotJson(repoPath, options = {}) {
|
|
|
352
363
|
const outputFilename = `eck${shortRepoName}${timestamp}_${anchor.substring(0, 7)}_up${seqStr}_${sizeKB}kb.md`;
|
|
353
364
|
const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
|
|
354
365
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
366
|
+
await ensureSnapshotsInGitignore(repoPath);
|
|
355
367
|
await fs.writeFile(outputPath, fullContent);
|
|
356
368
|
|
|
357
369
|
// --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
|
|
@@ -374,16 +386,16 @@ export async function updateSnapshotJson(repoPath, options = {}) {
|
|
|
374
386
|
}
|
|
375
387
|
// --------------------------------------------
|
|
376
388
|
|
|
377
|
-
console.log(JSON.stringify({
|
|
378
|
-
status: "success",
|
|
379
|
-
snapshot_file: `.eck/snapshots/${outputFilename}`,
|
|
380
|
-
files_count: includedCount,
|
|
381
|
-
timestamp: timestamp
|
|
382
|
-
}));
|
|
383
|
-
|
|
384
|
-
// Auto-push telemetry and sync weights (fire and forget so it doesn't break JSON output)
|
|
385
|
-
pushTelemetry(repoPath, true).catch(() => {});
|
|
386
|
-
syncTokenWeights(true).catch(() => {});
|
|
389
|
+
console.log(JSON.stringify({
|
|
390
|
+
status: "success",
|
|
391
|
+
snapshot_file: `.eck/snapshots/${outputFilename}`,
|
|
392
|
+
files_count: includedCount,
|
|
393
|
+
timestamp: timestamp
|
|
394
|
+
}));
|
|
395
|
+
|
|
396
|
+
// Auto-push telemetry and sync weights (fire and forget so it doesn't break JSON output)
|
|
397
|
+
pushTelemetry(repoPath, true).catch(() => {});
|
|
398
|
+
syncTokenWeights(true).catch(() => {});
|
|
387
399
|
|
|
388
400
|
} catch (error) {
|
|
389
401
|
console.log(JSON.stringify({ status: "error", message: error.message }));
|