@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,201 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
import _traverse from '@babel/traverse';
|
|
3
|
+
const traverse = _traverse.default || _traverse;
|
|
4
|
+
import _generate from '@babel/generator';
|
|
5
|
+
const generate = _generate.default || _generate;
|
|
6
|
+
|
|
7
|
+
// Lazy-load tree-sitter to avoid breaking when native bindings are unavailable
|
|
8
|
+
let Parser = null;
|
|
9
|
+
let Python = null;
|
|
10
|
+
let Java = null;
|
|
11
|
+
let Kotlin = null;
|
|
12
|
+
let C = null;
|
|
13
|
+
let Rust = null;
|
|
14
|
+
let Go = null;
|
|
15
|
+
|
|
16
|
+
async function loadTreeSitter() {
|
|
17
|
+
if (Parser) return true; // Already loaded
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// We use dynamic imports and check for basic sanity to handle broken native builds (common on Windows)
|
|
21
|
+
const treeSitterModule = await import('tree-sitter').catch(() => null);
|
|
22
|
+
if (!treeSitterModule || !treeSitterModule.default) return false;
|
|
23
|
+
|
|
24
|
+
Parser = treeSitterModule.default;
|
|
25
|
+
|
|
26
|
+
// Load language packs with Promise.allSettled to handle individual failures
|
|
27
|
+
const langs = await Promise.allSettled([
|
|
28
|
+
import('tree-sitter-python'),
|
|
29
|
+
import('tree-sitter-java'),
|
|
30
|
+
import('tree-sitter-kotlin'),
|
|
31
|
+
import('tree-sitter-c'),
|
|
32
|
+
import('tree-sitter-rust'),
|
|
33
|
+
import('tree-sitter-go')
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
Python = langs[0].status === 'fulfilled' ? langs[0].value.default : null;
|
|
37
|
+
Java = langs[1].status === 'fulfilled' ? langs[1].value.default : null;
|
|
38
|
+
Kotlin = langs[2].status === 'fulfilled' ? langs[2].value.default : null;
|
|
39
|
+
C = langs[3].status === 'fulfilled' ? langs[3].value.default : null;
|
|
40
|
+
Rust = langs[4].status === 'fulfilled' ? langs[4].value.default : null;
|
|
41
|
+
Go = langs[5].status === 'fulfilled' ? langs[5].value.default : null;
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// Silently fail, skeletonize will fallback to original content
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Initialize parsers map (will be populated lazily)
|
|
51
|
+
const languages = {
|
|
52
|
+
'.py': () => Python,
|
|
53
|
+
'.java': () => Java,
|
|
54
|
+
'.kt': () => Kotlin,
|
|
55
|
+
'.c': () => C,
|
|
56
|
+
'.h': () => C,
|
|
57
|
+
'.cpp': () => C,
|
|
58
|
+
'.hpp': () => C,
|
|
59
|
+
'.rs': () => Rust,
|
|
60
|
+
'.go': () => Go
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Strips implementation details from code.
|
|
65
|
+
* @param {string} content - Full file content
|
|
66
|
+
* @param {string} filePath - File path to determine language
|
|
67
|
+
* @returns {Promise<string>} - Skeletonized code
|
|
68
|
+
*/
|
|
69
|
+
export async function skeletonize(content, filePath) {
|
|
70
|
+
if (!content) return content;
|
|
71
|
+
|
|
72
|
+
// 1. JS/TS Strategy (Babel is better for JS ecosystem)
|
|
73
|
+
if (/\.(js|jsx|ts|tsx|mjs|cjs)$/.test(filePath)) {
|
|
74
|
+
return skeletonizeJs(content);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Tree-sitter Strategy (Python, Java, Kotlin, C, Rust, Go)
|
|
78
|
+
const ext = filePath.substring(filePath.lastIndexOf('.'));
|
|
79
|
+
if (languages[ext]) {
|
|
80
|
+
// Lazy-load tree-sitter
|
|
81
|
+
const available = await loadTreeSitter();
|
|
82
|
+
const langModule = languages[ext]();
|
|
83
|
+
|
|
84
|
+
// Only attempt tree-sitter if both the parser and the specific language module are ready
|
|
85
|
+
if (available && Parser && langModule) {
|
|
86
|
+
return skeletonizeTreeSitter(content, langModule, ext);
|
|
87
|
+
}
|
|
88
|
+
return content; // Fallback: return original content if tree-sitter unavailable
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Fallback (Return as is)
|
|
92
|
+
return content;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function skeletonizeJs(content) {
|
|
96
|
+
try {
|
|
97
|
+
const ast = parse(content, {
|
|
98
|
+
sourceType: 'module',
|
|
99
|
+
plugins: ['typescript', 'jsx', 'decorators-legacy'],
|
|
100
|
+
errorRecovery: true
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
traverse(ast, {
|
|
104
|
+
Function(path) {
|
|
105
|
+
if (path.node.body && path.node.body.type === 'BlockStatement') {
|
|
106
|
+
// Preserve leading comments (JSDoc) before emptying body
|
|
107
|
+
const leadingComments = path.node.leadingComments || [];
|
|
108
|
+
path.node.body.body = [];
|
|
109
|
+
path.node.body.innerComments = leadingComments.length > 0
|
|
110
|
+
? leadingComments
|
|
111
|
+
: [{ type: 'CommentBlock', value: ' ... ' }];
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
ClassMethod(path) {
|
|
115
|
+
if (path.node.body && path.node.body.type === 'BlockStatement') {
|
|
116
|
+
// Preserve leading comments (JSDoc) before emptying body
|
|
117
|
+
const leadingComments = path.node.leadingComments || [];
|
|
118
|
+
path.node.body.body = [];
|
|
119
|
+
path.node.body.innerComments = leadingComments.length > 0
|
|
120
|
+
? leadingComments
|
|
121
|
+
: [{ type: 'CommentBlock', value: ' ... ' }];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const output = generate(ast, {}, content);
|
|
127
|
+
return output.code;
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return content + '\n// [Skeleton parse error]';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function skeletonizeTreeSitter(content, language, ext) {
|
|
134
|
+
try {
|
|
135
|
+
const parser = new Parser();
|
|
136
|
+
parser.setLanguage(language);
|
|
137
|
+
const tree = parser.parse(content);
|
|
138
|
+
|
|
139
|
+
// Define node types that represent function bodies
|
|
140
|
+
const bodyTypes = ['block', 'function_body', 'compound_statement'];
|
|
141
|
+
const replacements = [];
|
|
142
|
+
|
|
143
|
+
const visit = (node) => {
|
|
144
|
+
const type = node.type;
|
|
145
|
+
let isFunction = false;
|
|
146
|
+
let replacementText = '{ /* ... */ }';
|
|
147
|
+
|
|
148
|
+
// Language specific detection
|
|
149
|
+
if (ext === '.rs') {
|
|
150
|
+
isFunction = ['function_item', 'method_declaration'].includes(type);
|
|
151
|
+
} else if (ext === '.go') {
|
|
152
|
+
isFunction = ['function_declaration', 'method_declaration'].includes(type);
|
|
153
|
+
} else if (ext === '.py') {
|
|
154
|
+
isFunction = type === 'function_definition';
|
|
155
|
+
replacementText = '...';
|
|
156
|
+
} else {
|
|
157
|
+
isFunction = [
|
|
158
|
+
'function_definition',
|
|
159
|
+
'method_declaration',
|
|
160
|
+
'function_declaration'
|
|
161
|
+
].includes(type);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (isFunction) {
|
|
165
|
+
let bodyNode = null;
|
|
166
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
167
|
+
const child = node.child(i);
|
|
168
|
+
if (bodyTypes.includes(child.type)) {
|
|
169
|
+
bodyNode = child;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (bodyNode) {
|
|
175
|
+
replacements.push({
|
|
176
|
+
start: bodyNode.startIndex,
|
|
177
|
+
end: bodyNode.endIndex,
|
|
178
|
+
text: replacementText
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
185
|
+
visit(node.child(i));
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
visit(tree.rootNode);
|
|
190
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
191
|
+
|
|
192
|
+
let currentContent = content;
|
|
193
|
+
for (const rep of replacements) {
|
|
194
|
+
currentContent = currentContent.substring(0, rep.start) + rep.text + currentContent.substring(rep.end);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return currentContent;
|
|
198
|
+
} catch (e) {
|
|
199
|
+
return content + `\n// [Skeleton error: ${e.message}]`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { execa } from 'execa';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* EckSnapshot MCP Server
|
|
20
|
+
* Provides tools for finalizing development tasks with git integration
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
class EckSnapshotMCPServer {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.server = new Server(
|
|
26
|
+
{
|
|
27
|
+
name: 'ecksnapshot-mcp',
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
capabilities: {
|
|
32
|
+
tools: {},
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
this.setupToolHandlers();
|
|
38
|
+
|
|
39
|
+
// Error handling
|
|
40
|
+
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
|
41
|
+
process.on('SIGINT', async () => {
|
|
42
|
+
await this.server.close();
|
|
43
|
+
process.exit(0);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setupToolHandlers() {
|
|
48
|
+
// List available tools
|
|
49
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
50
|
+
tools: [
|
|
51
|
+
{
|
|
52
|
+
name: 'eck_finish_task',
|
|
53
|
+
description: 'Finalize a completed task by updating AnswerToSA.md, creating a git commit, and generating a delta snapshot. This should be called when a task is fully complete, tested, and ready to be committed. The tool automatically syncs context by running eck-snapshot update-auto.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
status: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Status update message for AnswerToSA.md (e.g., "Fixed bug X", "Implemented feature Y")',
|
|
60
|
+
},
|
|
61
|
+
commitMessage: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Git commit message. Should follow conventional commits format (e.g., "feat: add user authentication", "fix: resolve login issue")',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
required: ['status', 'commitMessage'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
// Handle tool calls
|
|
73
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
74
|
+
if (request.params.name !== 'eck_finish_task') {
|
|
75
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { status, commitMessage } = request.params.arguments;
|
|
79
|
+
|
|
80
|
+
if (!status || !commitMessage) {
|
|
81
|
+
throw new Error('Missing required arguments: status and commitMessage are required');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const result = await this.finishTask(status, commitMessage);
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: JSON.stringify(result, null, 2),
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: 'text',
|
|
99
|
+
text: `Error: ${error.message}`,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async finishTask(status, commitMessage) {
|
|
109
|
+
const workDir = process.cwd();
|
|
110
|
+
const answerFilePath = path.join(workDir, '.eck', 'lastsnapshot', 'AnswerToSA.md');
|
|
111
|
+
|
|
112
|
+
const steps = [];
|
|
113
|
+
|
|
114
|
+
// Step 1: Update AnswerToSA.md
|
|
115
|
+
try {
|
|
116
|
+
const timestamp = new Date().toISOString();
|
|
117
|
+
const updateContent = `\n## Update - ${timestamp}\n\n${status}\n`;
|
|
118
|
+
|
|
119
|
+
// Check if file exists
|
|
120
|
+
try {
|
|
121
|
+
await fs.access(answerFilePath);
|
|
122
|
+
// Append to existing file
|
|
123
|
+
await fs.appendFile(answerFilePath, updateContent);
|
|
124
|
+
steps.push({ step: 'update_answer', success: true, message: 'Updated AnswerToSA.md' });
|
|
125
|
+
} catch {
|
|
126
|
+
// Create directory and file if not exists
|
|
127
|
+
await fs.mkdir(path.dirname(answerFilePath), { recursive: true });
|
|
128
|
+
await fs.writeFile(answerFilePath, `# Task Status\n${updateContent}`);
|
|
129
|
+
steps.push({ step: 'update_answer', success: true, message: 'Created AnswerToSA.md' });
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
steps.push({ step: 'update_answer', success: false, error: error.message });
|
|
133
|
+
throw new Error(`Failed to update AnswerToSA.md: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Step 2: Git add AnswerToSA.md
|
|
137
|
+
try {
|
|
138
|
+
await execa('git', ['add', '.eck/lastsnapshot/AnswerToSA.md'], { cwd: workDir, timeout: 30000 });
|
|
139
|
+
steps.push({ step: 'git_add_answer', success: true });
|
|
140
|
+
} catch (error) {
|
|
141
|
+
steps.push({ step: 'git_add_answer', success: false, error: error.message });
|
|
142
|
+
throw new Error(`Failed to git add AnswerToSA.md: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Step 3: Git add all other changes
|
|
146
|
+
try {
|
|
147
|
+
await execa('git', ['add', '.'], { cwd: workDir, timeout: 30000 });
|
|
148
|
+
steps.push({ step: 'git_add_all', success: true });
|
|
149
|
+
} catch (error) {
|
|
150
|
+
steps.push({ step: 'git_add_all', success: false, error: error.message });
|
|
151
|
+
throw new Error(`Failed to git add all changes: ${error.message}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Step 4: Create git commit
|
|
155
|
+
try {
|
|
156
|
+
await execa('git', ['commit', '-m', commitMessage], { cwd: workDir, timeout: 30000 });
|
|
157
|
+
steps.push({ step: 'git_commit', success: true, message: commitMessage });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
steps.push({ step: 'git_commit', success: false, error: error.message });
|
|
160
|
+
throw new Error(`Failed to create commit: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Step 5: ALWAYS generate update snapshot (using update-auto for silent JSON output)
|
|
164
|
+
try {
|
|
165
|
+
const cliPath = path.join(PROJECT_ROOT, 'index.js');
|
|
166
|
+
const { stdout } = await execa('node', [cliPath, 'update-auto'], { cwd: workDir, timeout: 120000 });
|
|
167
|
+
|
|
168
|
+
// Parse JSON output
|
|
169
|
+
let snapshotResult;
|
|
170
|
+
try {
|
|
171
|
+
snapshotResult = JSON.parse(stdout);
|
|
172
|
+
} catch {
|
|
173
|
+
snapshotResult = { raw_output: stdout };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
steps.push({
|
|
177
|
+
step: 'update_snapshot',
|
|
178
|
+
success: snapshotResult.status === 'success',
|
|
179
|
+
snapshot_file: snapshotResult.snapshot_file,
|
|
180
|
+
files_count: snapshotResult.files_count,
|
|
181
|
+
has_agent_report: snapshotResult.has_agent_report,
|
|
182
|
+
message: snapshotResult.message || 'Delta snapshot generated',
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
steps.push({
|
|
186
|
+
step: 'update_snapshot',
|
|
187
|
+
success: false,
|
|
188
|
+
error: error.message,
|
|
189
|
+
message: 'Snapshot generation failed (non-critical)',
|
|
190
|
+
});
|
|
191
|
+
// Don't throw here, snapshot failure shouldn't block task completion
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
success: true,
|
|
196
|
+
message: 'Task finalized successfully',
|
|
197
|
+
steps,
|
|
198
|
+
commitMessage,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async run() {
|
|
203
|
+
const transport = new StdioServerTransport();
|
|
204
|
+
await this.server.connect(transport);
|
|
205
|
+
console.error('EckSnapshot MCP server running on stdio');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Start the server
|
|
210
|
+
const server = new EckSnapshotMCPServer();
|
|
211
|
+
server.run().catch(console.error);
|