@xelth/eck-snapshot 4.2.4 ā 5.4.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.
Potentially problematic release.
This version of @xelth/eck-snapshot might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/index.js +14 -0
- package/package.json +64 -9
- package/scripts/mcp-eck-core.js +101 -0
- package/scripts/mcp-glm-zai-worker.mjs +243 -0
- package/scripts/verify_changes.js +68 -0
- package/setup.json +845 -0
- package/src/cli/cli.js +369 -0
- package/src/cli/commands/claudeSettings.js +93 -0
- package/src/cli/commands/consilium.js +86 -0
- package/src/cli/commands/createSnapshot.js +906 -0
- package/src/cli/commands/detectProfiles.js +98 -0
- package/src/cli/commands/detectProject.js +112 -0
- package/src/cli/commands/doctor.js +60 -0
- package/src/cli/commands/envSync.js +319 -0
- package/src/cli/commands/generateProfileGuide.js +144 -0
- package/src/cli/commands/pruneSnapshot.js +106 -0
- package/src/cli/commands/restoreSnapshot.js +173 -0
- package/src/cli/commands/setupGemini.js +149 -0
- package/src/cli/commands/setupGemini.test.js +115 -0
- package/src/cli/commands/setupMcp.js +269 -0
- package/src/cli/commands/showFile.js +39 -0
- package/src/cli/commands/trainTokens.js +38 -0
- package/src/cli/commands/updateSnapshot.js +219 -0
- package/src/config.js +125 -0
- package/src/core/skeletonizer.js +201 -0
- package/src/mcp-server/index.js +211 -0
- package/src/services/claudeCliService.js +626 -0
- package/src/services/claudeCliService.test.js +267 -0
- package/src/templates/agent-prompt.template.md +43 -0
- package/src/templates/architect-prompt.template.md +164 -0
- package/src/templates/claude-code/README.md +105 -0
- package/src/templates/claude-code/mcp-config-template.json +11 -0
- package/src/templates/claude-code/mcp-server-template.js +206 -0
- package/src/templates/claude-code/settings-claude.json +1 -0
- package/src/templates/envScanRequest.md +4 -0
- package/src/templates/gitWorkflow.md +32 -0
- package/src/templates/multiAgent.md +118 -0
- package/src/templates/opencode/coder.template.md +22 -0
- package/src/templates/opencode/junior-architect.template.md +85 -0
- package/src/templates/skeleton-instruction.md +16 -0
- package/src/templates/update-prompt.template.md +19 -0
- package/src/utils/aiHeader.js +678 -0
- package/src/utils/claudeMdGenerator.js +148 -0
- package/src/utils/eckProtocolParser.js +221 -0
- package/src/utils/fileUtils.js +1017 -0
- package/src/utils/gitUtils.js +44 -0
- package/src/utils/opencodeAgentsGenerator.js +271 -0
- package/src/utils/projectDetector.js +704 -0
- package/src/utils/tokenEstimator.js +201 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { execa } from 'execa';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Setup / Restore MCP servers for Claude Code and OpenCode.
|
|
14
|
+
* Registers:
|
|
15
|
+
* 1. eck-core (eck_finish_task) - commit + snapshot
|
|
16
|
+
* 2. glm-zai (glm_zai_*) - GLM-4.7 coding workers
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* eck-snapshot setup-mcp # Auto-detect and register for Claude Code
|
|
20
|
+
* eck-snapshot setup-mcp --opencode # Register for OpenCode
|
|
21
|
+
* eck-snapshot setup-mcp --both # Register for both
|
|
22
|
+
*/
|
|
23
|
+
export async function setupMcp(options = {}) {
|
|
24
|
+
const packageRoot = path.resolve(__dirname, '../../..');
|
|
25
|
+
const eckCorePath = path.join(packageRoot, 'scripts', 'mcp-eck-core.js');
|
|
26
|
+
const glmZaiPath = path.join(packageRoot, 'scripts', 'mcp-glm-zai-worker.mjs');
|
|
27
|
+
const mcpServerPath = path.join(packageRoot, 'src', 'mcp-server', 'index.js');
|
|
28
|
+
|
|
29
|
+
const targets = [];
|
|
30
|
+
if (options.opencode && !options.both) {
|
|
31
|
+
targets.push('opencode');
|
|
32
|
+
} else if (options.both) {
|
|
33
|
+
targets.push('claude', 'opencode');
|
|
34
|
+
} else {
|
|
35
|
+
targets.push('claude');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(chalk.blue.bold('\nš§ EckSnapshot MCP Setup\n'));
|
|
39
|
+
|
|
40
|
+
for (const target of targets) {
|
|
41
|
+
if (target === 'claude') {
|
|
42
|
+
await setupForClaude(packageRoot, eckCorePath, glmZaiPath, mcpServerPath, options);
|
|
43
|
+
} else {
|
|
44
|
+
await setupForOpenCode(packageRoot, eckCorePath, glmZaiPath, options);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Print summary
|
|
49
|
+
console.log(chalk.green.bold('\nā
MCP Setup Complete!\n'));
|
|
50
|
+
console.log(chalk.white('Registered MCP servers:'));
|
|
51
|
+
console.log(chalk.cyan(' 1. eck-core') + chalk.gray(' ā eck_finish_task (commit + snapshot)'));
|
|
52
|
+
console.log(chalk.cyan(' 2. glm-zai') + chalk.gray(' ā glm_zai_backend, glm_zai_frontend, glm_zai_qa, glm_zai_refactor, glm_zai_general'));
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(chalk.yellow('Requirements:'));
|
|
55
|
+
console.log(chalk.white(' ⢠ZAI_API_KEY environment variable must be set for GLM Z.AI workers'));
|
|
56
|
+
console.log(chalk.white(' ⢠Get your key at https://z.ai'));
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(chalk.yellow('Next steps:'));
|
|
59
|
+
console.log(chalk.white(' 1. Restart your AI coding tool (Claude Code / OpenCode)'));
|
|
60
|
+
console.log(chalk.white(' 2. The tools will be available automatically'));
|
|
61
|
+
console.log(chalk.white(' 3. Use --jas or --jao flags to generate CLAUDE.md with delegation protocol'));
|
|
62
|
+
console.log('');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register MCP servers for Claude Code using `claude mcp add`
|
|
67
|
+
*/
|
|
68
|
+
async function setupForClaude(packageRoot, eckCorePath, glmZaiPath, mcpServerPath, options) {
|
|
69
|
+
const spinner = ora();
|
|
70
|
+
|
|
71
|
+
console.log(chalk.blue('š¦ Setting up for Claude Code...\n'));
|
|
72
|
+
|
|
73
|
+
// Method 1: Try `claude mcp add` commands (preferred)
|
|
74
|
+
let usedCliRegistration = false;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
spinner.start('Checking if `claude` CLI is available...');
|
|
78
|
+
await execa('claude', ['--version']);
|
|
79
|
+
spinner.succeed('Claude CLI found');
|
|
80
|
+
|
|
81
|
+
// Helper to register a single MCP server via claude CLI
|
|
82
|
+
async function registerClaudeMcp(name, scriptPath) {
|
|
83
|
+
spinner.start(`Registering ${name} MCP server...`);
|
|
84
|
+
try {
|
|
85
|
+
await execa('claude', ['mcp', 'add', name, '--', 'node', scriptPath]);
|
|
86
|
+
spinner.succeed(`${name} registered`);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
// May already exist - try remove then add
|
|
89
|
+
try {
|
|
90
|
+
await execa('claude', ['mcp', 'remove', name]);
|
|
91
|
+
await execa('claude', ['mcp', 'add', name, '--', 'node', scriptPath]);
|
|
92
|
+
spinner.succeed(`${name} re-registered`);
|
|
93
|
+
} catch (e2) {
|
|
94
|
+
spinner.warn(`${name} registration via CLI failed: ${e2.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await registerClaudeMcp('eck-core', eckCorePath);
|
|
100
|
+
await registerClaudeMcp('glm-zai', glmZaiPath);
|
|
101
|
+
await registerClaudeMcp('ecksnapshot', mcpServerPath);
|
|
102
|
+
|
|
103
|
+
usedCliRegistration = true;
|
|
104
|
+
} catch {
|
|
105
|
+
spinner.info('Claude CLI not found, will use config file method');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Method 2: Also update the config file as fallback / alternative
|
|
109
|
+
spinner.start('Updating Claude Code config file...');
|
|
110
|
+
|
|
111
|
+
const homeDir = os.homedir();
|
|
112
|
+
const platform = process.platform;
|
|
113
|
+
|
|
114
|
+
let possibleConfigPaths = [];
|
|
115
|
+
if (platform === 'win32') {
|
|
116
|
+
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
|
|
117
|
+
possibleConfigPaths = [
|
|
118
|
+
path.join(appData, 'Claude', 'config.json'),
|
|
119
|
+
path.join(homeDir, '.claude', 'config.json'),
|
|
120
|
+
path.join(homeDir, '.config', 'claude', 'config.json'),
|
|
121
|
+
];
|
|
122
|
+
} else {
|
|
123
|
+
possibleConfigPaths = [
|
|
124
|
+
path.join(homeDir, '.config', 'claude', 'config.json'),
|
|
125
|
+
path.join(homeDir, '.claude', 'config.json'),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let claudeConfigPath = null;
|
|
130
|
+
for (const configPath of possibleConfigPaths) {
|
|
131
|
+
try {
|
|
132
|
+
await fs.access(configPath);
|
|
133
|
+
claudeConfigPath = configPath;
|
|
134
|
+
break;
|
|
135
|
+
} catch { /* continue */ }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!claudeConfigPath) {
|
|
139
|
+
claudeConfigPath = possibleConfigPaths[0];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let config = {};
|
|
143
|
+
try {
|
|
144
|
+
const content = await fs.readFile(claudeConfigPath, 'utf-8');
|
|
145
|
+
config = JSON.parse(content);
|
|
146
|
+
} catch { /* new config */ }
|
|
147
|
+
|
|
148
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
149
|
+
|
|
150
|
+
config.mcpServers.ecksnapshot = {
|
|
151
|
+
command: 'node',
|
|
152
|
+
args: [mcpServerPath],
|
|
153
|
+
env: {},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
config.mcpServers['eck-core'] = {
|
|
157
|
+
command: 'node',
|
|
158
|
+
args: [eckCorePath],
|
|
159
|
+
env: {},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
config.mcpServers['glm-zai'] = {
|
|
163
|
+
command: 'node',
|
|
164
|
+
args: [glmZaiPath],
|
|
165
|
+
env: {},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Remove old minimax-worker if present
|
|
169
|
+
if (config.mcpServers['minimax-worker']) {
|
|
170
|
+
delete config.mcpServers['minimax-worker'];
|
|
171
|
+
console.log(chalk.gray(' Removed old minimax-worker MCP server'));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await fs.mkdir(path.dirname(claudeConfigPath), { recursive: true });
|
|
175
|
+
await fs.writeFile(claudeConfigPath, JSON.stringify(config, null, 2));
|
|
176
|
+
|
|
177
|
+
spinner.succeed(`Config saved: ${chalk.cyan(claudeConfigPath)}`);
|
|
178
|
+
|
|
179
|
+
// Also update the local .eck/claude-mcp-config.json
|
|
180
|
+
const localConfigPath = path.join(process.cwd(), '.eck', 'claude-mcp-config.json');
|
|
181
|
+
const localConfig = {
|
|
182
|
+
mcpServers: {
|
|
183
|
+
ecksnapshot: config.mcpServers.ecksnapshot,
|
|
184
|
+
'eck-core': config.mcpServers['eck-core'],
|
|
185
|
+
'glm-zai': config.mcpServers['glm-zai'],
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await fs.mkdir(path.dirname(localConfigPath), { recursive: true });
|
|
191
|
+
await fs.writeFile(localConfigPath, JSON.stringify(localConfig, null, 2));
|
|
192
|
+
spinner.succeed(`Local config updated: ${chalk.cyan(localConfigPath)}`);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
spinner.warn(`Could not update local config: ${e.message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Register MCP servers for OpenCode.
|
|
200
|
+
* OpenCode uses opencode.json at project root with its own format:
|
|
201
|
+
* { "mcp": { "name": { "type": "local", "command": [...], "enabled": true } } }
|
|
202
|
+
* The CLI (`opencode mcp add`) is interactive (TUI), so we write the config directly.
|
|
203
|
+
*/
|
|
204
|
+
async function setupForOpenCode(packageRoot, eckCorePath, glmZaiPath, options) {
|
|
205
|
+
const spinner = ora();
|
|
206
|
+
|
|
207
|
+
console.log(chalk.blue('š¦ Setting up for OpenCode...\n'));
|
|
208
|
+
|
|
209
|
+
const configPath = path.join(process.cwd(), 'opencode.json');
|
|
210
|
+
|
|
211
|
+
spinner.start('Updating OpenCode config (opencode.json)...');
|
|
212
|
+
|
|
213
|
+
// Read existing config or create new
|
|
214
|
+
let config = {};
|
|
215
|
+
try {
|
|
216
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
217
|
+
config = JSON.parse(content);
|
|
218
|
+
} catch { /* new config */ }
|
|
219
|
+
|
|
220
|
+
// Ensure $schema and base structure
|
|
221
|
+
if (!config.$schema) {
|
|
222
|
+
config.$schema = 'https://opencode.ai/config.json';
|
|
223
|
+
}
|
|
224
|
+
if (!config.mcp) {
|
|
225
|
+
config.mcp = {};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Register eck-core
|
|
229
|
+
config.mcp['eck-core'] = {
|
|
230
|
+
type: 'local',
|
|
231
|
+
command: ['node', eckCorePath],
|
|
232
|
+
enabled: true,
|
|
233
|
+
timeout: 30000,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Register glm-zai
|
|
237
|
+
config.mcp['glm-zai'] = {
|
|
238
|
+
type: 'local',
|
|
239
|
+
command: ['node', glmZaiPath],
|
|
240
|
+
enabled: true,
|
|
241
|
+
timeout: 120000,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Preserve ZAI_API_KEY in environment if it was set before
|
|
245
|
+
if (process.env.ZAI_API_KEY) {
|
|
246
|
+
config.mcp['glm-zai'].environment = {
|
|
247
|
+
ZAI_API_KEY: process.env.ZAI_API_KEY,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Remove old minimax entries if present
|
|
252
|
+
if (config.mcp['minimax-worker']) {
|
|
253
|
+
delete config.mcp['minimax-worker'];
|
|
254
|
+
console.log(chalk.gray(' Removed old minimax-worker from opencode.json'));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Ensure AGENTS.md is in instructions
|
|
258
|
+
if (!config.instructions) {
|
|
259
|
+
config.instructions = ['AGENTS.md'];
|
|
260
|
+
} else if (!config.instructions.includes('AGENTS.md')) {
|
|
261
|
+
config.instructions.push('AGENTS.md');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
265
|
+
spinner.succeed(`OpenCode config updated: ${chalk.cyan(configPath)}`);
|
|
266
|
+
|
|
267
|
+
console.log(chalk.gray('\n OpenCode will read MCP servers from opencode.json on next start.'));
|
|
268
|
+
console.log(chalk.gray(' Use `eck-snapshot --jas` or `--jao` to generate AGENTS.md for OpenCode.\n'));
|
|
269
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Show the full content of specific files
|
|
7
|
+
* Used for AI lazy loading when skeleton mode is active
|
|
8
|
+
* @param {string[]} filePaths - Array of paths to the files to display
|
|
9
|
+
*/
|
|
10
|
+
export async function showFile(filePaths) {
|
|
11
|
+
// Ensure input is array (commander passes array for variadic args)
|
|
12
|
+
const files = Array.isArray(filePaths) ? filePaths : [filePaths];
|
|
13
|
+
|
|
14
|
+
if (files.length === 0) {
|
|
15
|
+
console.error(chalk.yellow('No files specified. Usage: eck-snapshot show <file1> [file2] ...'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const filePath of files) {
|
|
20
|
+
try {
|
|
21
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
22
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
23
|
+
|
|
24
|
+
console.log(chalk.green(`\n--- FULL CONTENT: ${filePath} ---\n`));
|
|
25
|
+
|
|
26
|
+
// Detect file extension for syntax highlighting hint
|
|
27
|
+
const ext = path.extname(filePath).slice(1);
|
|
28
|
+
console.log('```' + ext);
|
|
29
|
+
console.log(content);
|
|
30
|
+
console.log('```');
|
|
31
|
+
|
|
32
|
+
console.log(chalk.green(`\n--- END OF FILE: ${filePath} ---\n`));
|
|
33
|
+
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(chalk.red(`Failed to read file ${filePath}: ${error.message}`));
|
|
36
|
+
// Continue to next file even if one fails
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { addTrainingPoint, showEstimationStats } from '../../utils/tokenEstimator.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Train token estimation with actual results
|
|
5
|
+
* @param {string} projectType - Type of project (android, nodejs, etc.)
|
|
6
|
+
* @param {string} fileSizeStr - File size in bytes
|
|
7
|
+
* @param {string} estimatedStr - Estimated tokens
|
|
8
|
+
* @param {string} actualStr - Actual tokens (from user input)
|
|
9
|
+
*/
|
|
10
|
+
export async function trainTokens(projectType, fileSizeStr, estimatedStr, actualStr) {
|
|
11
|
+
try {
|
|
12
|
+
const fileSizeInBytes = parseInt(fileSizeStr, 10);
|
|
13
|
+
const estimatedTokens = parseInt(estimatedStr, 10);
|
|
14
|
+
|
|
15
|
+
// Parse actual tokens from user input (remove any text like "tokens", commas, etc.)
|
|
16
|
+
const actualTokens = parseInt(actualStr.replace(/[^\d]/g, ''), 10);
|
|
17
|
+
|
|
18
|
+
if (isNaN(fileSizeInBytes) || isNaN(estimatedTokens) || isNaN(actualTokens)) {
|
|
19
|
+
throw new Error('Invalid numeric values provided');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await addTrainingPoint(projectType, fileSizeInBytes, estimatedTokens, actualTokens);
|
|
23
|
+
|
|
24
|
+
console.log('\nš Updated polynomial coefficients for improved estimation.');
|
|
25
|
+
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(`ā Error training token estimation: ${error.message}`);
|
|
28
|
+
console.error('Usage: eck-snapshot train-tokens <project-type> <file-size-bytes> <estimated-tokens> <actual-tokens>');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Show token estimation statistics
|
|
35
|
+
*/
|
|
36
|
+
export async function showTokenStats() {
|
|
37
|
+
await showEstimationStats();
|
|
38
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { getGitAnchor, getChangedFiles, getGitDiffOutput } from '../../utils/gitUtils.js';
|
|
6
|
+
import { loadSetupConfig } from '../../config.js';
|
|
7
|
+
import { readFileWithSizeCheck, parseSize, formatSize, matchesPattern, loadGitignore, generateTimestamp, getShortRepoName } from '../../utils/fileUtils.js';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
// Shared logic to generate the snapshot content string
|
|
14
|
+
async function generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore) {
|
|
15
|
+
let contentOutput = '';
|
|
16
|
+
let includedCount = 0;
|
|
17
|
+
const fileList = [];
|
|
18
|
+
|
|
19
|
+
// Check for Agent Report in .eck/lastsnapshot/AnswerToSA.md (STRICT LOCATION)
|
|
20
|
+
const reportPath = path.join(repoPath, '.eck', 'lastsnapshot', 'AnswerToSA.md');
|
|
21
|
+
let agentReport = null;
|
|
22
|
+
try {
|
|
23
|
+
agentReport = await fs.readFile(reportPath, 'utf-8');
|
|
24
|
+
if (!changedFiles.includes('.eck/lastsnapshot/AnswerToSA.md')) {
|
|
25
|
+
changedFiles.push('.eck/lastsnapshot/AnswerToSA.md');
|
|
26
|
+
}
|
|
27
|
+
} catch (e) { /* No report */ }
|
|
28
|
+
|
|
29
|
+
for (const filePath of changedFiles) {
|
|
30
|
+
if (config.dirsToIgnore.some(d => filePath.startsWith(d))) continue;
|
|
31
|
+
if (gitignore.ignores(filePath) && filePath !== '.eck/lastsnapshot/AnswerToSA.md') continue;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const fullPath = path.join(repoPath, filePath);
|
|
35
|
+
const content = await readFileWithSizeCheck(fullPath, parseSize(config.maxFileSize));
|
|
36
|
+
contentOutput += `--- File: /${filePath} ---\n\n${content}\n\n`;
|
|
37
|
+
fileList.push(`- ${filePath}`);
|
|
38
|
+
includedCount++;
|
|
39
|
+
} catch (e) { /* Skip */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Load Template
|
|
43
|
+
const templatePath = path.join(__dirname, '../../templates/update-prompt.template.md');
|
|
44
|
+
let header = await fs.readFile(templatePath, 'utf-8');
|
|
45
|
+
|
|
46
|
+
// Inject Agent Report
|
|
47
|
+
let reportSection = '';
|
|
48
|
+
if (agentReport) {
|
|
49
|
+
reportSection = `\n#######################################################\n# šØ MESSAGE FROM EXECUTION AGENT (Claude)\n#######################################################\n${agentReport}\n#######################################################\n\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
header = header.replace('{{anchor}}', anchor.substring(0, 7))
|
|
53
|
+
.replace('{{timestamp}}', new Date().toLocaleString())
|
|
54
|
+
.replace('{{fileList}}', fileList.join('\n'));
|
|
55
|
+
|
|
56
|
+
header = reportSection + header;
|
|
57
|
+
|
|
58
|
+
const diffOutput = await getGitDiffOutput(repoPath, anchor);
|
|
59
|
+
const diffSection = `\n--- GIT DIFF (For Context) ---\n\n\`\`\`diff\n${diffOutput}\n\`\`\``;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
fullContent: header + contentOutput + diffSection,
|
|
63
|
+
includedCount,
|
|
64
|
+
anchor,
|
|
65
|
+
agentReport
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function updateSnapshot(repoPath, options) {
|
|
70
|
+
const spinner = ora('Generating update snapshot...').start();
|
|
71
|
+
try {
|
|
72
|
+
const anchor = await getGitAnchor(repoPath);
|
|
73
|
+
if (!anchor) {
|
|
74
|
+
throw new Error('No snapshot anchor found. Run a full snapshot first: eck-snapshot snapshot');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const changedFiles = await getChangedFiles(repoPath, anchor);
|
|
78
|
+
if (changedFiles.length === 0) {
|
|
79
|
+
spinner.succeed('No changes detected since last full snapshot.');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const setupConfig = await loadSetupConfig();
|
|
84
|
+
const config = { ...setupConfig.fileFiltering, ...setupConfig.performance, ...options };
|
|
85
|
+
const gitignore = await loadGitignore(repoPath);
|
|
86
|
+
|
|
87
|
+
const { fullContent, includedCount, agentReport } = await generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore);
|
|
88
|
+
|
|
89
|
+
// Determine sequence number
|
|
90
|
+
let seqNum = 1;
|
|
91
|
+
const counterPath = path.join(repoPath, '.eck', 'update_seq');
|
|
92
|
+
try {
|
|
93
|
+
const seqData = await fs.readFile(counterPath, 'utf-8');
|
|
94
|
+
const [savedHash, savedCount] = seqData.split(':');
|
|
95
|
+
if (savedHash && savedHash.trim() === anchor.substring(0, 7).trim()) {
|
|
96
|
+
seqNum = parseInt(savedCount || '0') + 1;
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await fs.writeFile(counterPath, `${anchor.substring(0, 7)}:${seqNum}`);
|
|
102
|
+
} catch (e) {}
|
|
103
|
+
|
|
104
|
+
const timestamp = generateTimestamp();
|
|
105
|
+
const shortRepoName = getShortRepoName(path.basename(repoPath));
|
|
106
|
+
const outputFilename = `eck${shortRepoName}${timestamp}_${anchor.substring(0, 7)}_up${seqNum}.md`;
|
|
107
|
+
const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
|
|
108
|
+
|
|
109
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
110
|
+
await fs.writeFile(outputPath, fullContent);
|
|
111
|
+
|
|
112
|
+
spinner.succeed(`Update snapshot created: .eck/snapshots/${outputFilename}`);
|
|
113
|
+
|
|
114
|
+
// --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
|
|
115
|
+
try {
|
|
116
|
+
const snapDir = path.join(repoPath, '.eck', 'lastsnapshot');
|
|
117
|
+
await fs.mkdir(snapDir, { recursive: true });
|
|
118
|
+
|
|
119
|
+
// 1. Clean up OLD snapshots
|
|
120
|
+
const existingFiles = await fs.readdir(snapDir);
|
|
121
|
+
for (const file of existingFiles) {
|
|
122
|
+
if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
|
|
123
|
+
await fs.unlink(path.join(snapDir, file));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. Save new file
|
|
128
|
+
await fs.writeFile(path.join(snapDir, outputFilename), fullContent);
|
|
129
|
+
console.log(chalk.cyan(`š Active snapshot updated in .eck/lastsnapshot/: ${outputFilename}`));
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// Non-critical failure
|
|
132
|
+
}
|
|
133
|
+
// --------------------------------------------
|
|
134
|
+
|
|
135
|
+
// Check if agent report was included
|
|
136
|
+
if (agentReport) {
|
|
137
|
+
console.log(chalk.green('šØ Included Agent Report (.eck/lastsnapshot/AnswerToSA.md)'));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(`š¦ Included ${includedCount} changed files.`);
|
|
141
|
+
|
|
142
|
+
} catch (error) {
|
|
143
|
+
spinner.fail(`Update failed: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// New Silent/JSON command for Agents
|
|
148
|
+
export async function updateSnapshotJson(repoPath) {
|
|
149
|
+
try {
|
|
150
|
+
const anchor = await getGitAnchor(repoPath);
|
|
151
|
+
if (!anchor) {
|
|
152
|
+
console.log(JSON.stringify({ status: "error", message: "No snapshot anchor found" }));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const changedFiles = await getChangedFiles(repoPath, anchor);
|
|
157
|
+
if (changedFiles.length === 0) {
|
|
158
|
+
console.log(JSON.stringify({ status: "no_changes", message: "No changes detected" }));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const setupConfig = await loadSetupConfig();
|
|
163
|
+
const config = { ...setupConfig.fileFiltering, ...setupConfig.performance };
|
|
164
|
+
const gitignore = await loadGitignore(repoPath);
|
|
165
|
+
|
|
166
|
+
const { fullContent, includedCount, agentReport } = await generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore);
|
|
167
|
+
|
|
168
|
+
let seqNum = 1;
|
|
169
|
+
const counterPath = path.join(repoPath, '.eck', 'update_seq');
|
|
170
|
+
try {
|
|
171
|
+
const seqData = await fs.readFile(counterPath, 'utf-8');
|
|
172
|
+
const [savedHash, savedCount] = seqData.split(':');
|
|
173
|
+
if (savedHash && savedHash.trim() === anchor.substring(0, 7).trim()) {
|
|
174
|
+
seqNum = parseInt(savedCount || '0') + 1;
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await fs.writeFile(counterPath, `${anchor.substring(0, 7)}:${seqNum}`);
|
|
180
|
+
} catch (e) {}
|
|
181
|
+
|
|
182
|
+
const timestamp = generateTimestamp();
|
|
183
|
+
const shortRepoName = getShortRepoName(path.basename(repoPath));
|
|
184
|
+
const outputFilename = `eck${shortRepoName}${timestamp}_${anchor.substring(0, 7)}_up${seqNum}.md`;
|
|
185
|
+
const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
|
|
186
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
187
|
+
await fs.writeFile(outputPath, fullContent);
|
|
188
|
+
|
|
189
|
+
// --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
|
|
190
|
+
try {
|
|
191
|
+
const snapDir = path.join(repoPath, '.eck', 'lastsnapshot');
|
|
192
|
+
await fs.mkdir(snapDir, { recursive: true });
|
|
193
|
+
|
|
194
|
+
// 1. Clean up OLD snapshots
|
|
195
|
+
const existingFiles = await fs.readdir(snapDir);
|
|
196
|
+
for (const file of existingFiles) {
|
|
197
|
+
if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
|
|
198
|
+
await fs.unlink(path.join(snapDir, file));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 2. Save new file
|
|
203
|
+
await fs.writeFile(path.join(snapDir, outputFilename), fullContent);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
// Non-critical failure
|
|
206
|
+
}
|
|
207
|
+
// --------------------------------------------
|
|
208
|
+
|
|
209
|
+
console.log(JSON.stringify({
|
|
210
|
+
status: "success",
|
|
211
|
+
snapshot_file: `.eck/snapshots/${outputFilename}`,
|
|
212
|
+
files_count: includedCount,
|
|
213
|
+
timestamp: timestamp
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.log(JSON.stringify({ status: "error", message: error.message }));
|
|
218
|
+
}
|
|
219
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
let cachedConfig = null;
|
|
9
|
+
|
|
10
|
+
export async function loadSetupConfig() {
|
|
11
|
+
if (cachedConfig) {
|
|
12
|
+
return cachedConfig;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const setupPath = path.join(__dirname, '..', 'setup.json');
|
|
17
|
+
const setupContent = await fs.readFile(setupPath, 'utf-8');
|
|
18
|
+
cachedConfig = JSON.parse(setupContent);
|
|
19
|
+
|
|
20
|
+
// Basic schema validation for critical fields
|
|
21
|
+
validateConfigSchema(cachedConfig);
|
|
22
|
+
|
|
23
|
+
return cachedConfig;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Error loading setup.json:', error.message);
|
|
26
|
+
throw new Error('Failed to load setup.json configuration file');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validates critical config fields and warns if missing or invalid
|
|
32
|
+
*/
|
|
33
|
+
function validateConfigSchema(config) {
|
|
34
|
+
const warnings = [];
|
|
35
|
+
|
|
36
|
+
// Validate fileFiltering section
|
|
37
|
+
if (!config.fileFiltering) {
|
|
38
|
+
warnings.push('Missing "fileFiltering" section');
|
|
39
|
+
} else {
|
|
40
|
+
if (!Array.isArray(config.fileFiltering.filesToIgnore)) {
|
|
41
|
+
warnings.push('"fileFiltering.filesToIgnore" must be an array');
|
|
42
|
+
}
|
|
43
|
+
if (!Array.isArray(config.fileFiltering.dirsToIgnore)) {
|
|
44
|
+
warnings.push('"fileFiltering.dirsToIgnore" must be an array');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Validate aiInstructions section
|
|
49
|
+
if (!config.aiInstructions) {
|
|
50
|
+
warnings.push('Missing "aiInstructions" section');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Legacy support
|
|
54
|
+
if (!config.filesToIgnore || !Array.isArray(config.filesToIgnore)) {
|
|
55
|
+
warnings.push('filesToIgnore missing or not an array - using defaults');
|
|
56
|
+
config.filesToIgnore = DEFAULT_CONFIG.filesToIgnore;
|
|
57
|
+
}
|
|
58
|
+
if (!config.dirsToIgnore || !Array.isArray(config.dirsToIgnore)) {
|
|
59
|
+
warnings.push('dirsToIgnore missing or not an array - using defaults');
|
|
60
|
+
config.dirsToIgnore = DEFAULT_CONFIG.dirsToIgnore;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (warnings.length > 0) {
|
|
64
|
+
console.warn('\nā ļø Config Validation Warnings:');
|
|
65
|
+
warnings.forEach(w => console.warn(` - ${w}`));
|
|
66
|
+
console.warn(' (Falling back to defaults for missing values where possible)\n');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Loads and merges all profiles (local-first).
|
|
72
|
+
*/
|
|
73
|
+
export async function getAllProfiles(repoPath) {
|
|
74
|
+
const globalConfig = await loadSetupConfig();
|
|
75
|
+
const globalProfiles = globalConfig.contextProfiles || {};
|
|
76
|
+
|
|
77
|
+
let localProfiles = {};
|
|
78
|
+
const localProfilePath = path.join(repoPath, '.eck', 'profiles.json');
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const localProfileContent = await fs.readFile(localProfilePath, 'utf-8');
|
|
82
|
+
localProfiles = JSON.parse(localProfileContent);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// No local profiles.json found, which is fine.
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Local profiles override global profiles
|
|
88
|
+
return { ...globalProfiles, ...localProfiles };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Smart profile loader (Step 2 of dynamic profiles).
|
|
93
|
+
* Reads local .eck/profiles.json first, then falls back to global setup.json profiles.
|
|
94
|
+
*/
|
|
95
|
+
export async function getProfile(profileName, repoPath) {
|
|
96
|
+
const globalConfig = await loadSetupConfig();
|
|
97
|
+
const globalProfiles = globalConfig.contextProfiles || {};
|
|
98
|
+
|
|
99
|
+
let localProfiles = {};
|
|
100
|
+
const localProfilePath = path.join(repoPath, '.eck', 'profiles.json');
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const localProfileContent = await fs.readFile(localProfilePath, 'utf-8');
|
|
104
|
+
localProfiles = JSON.parse(localProfileContent);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
// No local profiles.json found, which is fine. We just use globals.
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Local profiles override global profiles
|
|
110
|
+
const allProfiles = { ...globalProfiles, ...localProfiles };
|
|
111
|
+
|
|
112
|
+
return allProfiles[profileName] || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fallback default config for backwards compatibility
|
|
116
|
+
export const DEFAULT_CONFIG = {
|
|
117
|
+
smartModeTokenThreshold: 200000,
|
|
118
|
+
filesToIgnore: ['package-lock.json', '*.log', 'yarn.lock'],
|
|
119
|
+
extensionsToIgnore: ['.sqlite3', '.db', '.DS_Store', '.env', '.pyc'],
|
|
120
|
+
dirsToIgnore: ['node_modules/', '.git/', 'dist/', 'build/'],
|
|
121
|
+
maxFileSize: '10MB',
|
|
122
|
+
maxTotalSize: '100MB',
|
|
123
|
+
maxDepth: 10,
|
|
124
|
+
concurrency: 10
|
|
125
|
+
};
|