@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.

Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/index.js +14 -0
  4. package/package.json +64 -9
  5. package/scripts/mcp-eck-core.js +101 -0
  6. package/scripts/mcp-glm-zai-worker.mjs +243 -0
  7. package/scripts/verify_changes.js +68 -0
  8. package/setup.json +845 -0
  9. package/src/cli/cli.js +369 -0
  10. package/src/cli/commands/claudeSettings.js +93 -0
  11. package/src/cli/commands/consilium.js +86 -0
  12. package/src/cli/commands/createSnapshot.js +906 -0
  13. package/src/cli/commands/detectProfiles.js +98 -0
  14. package/src/cli/commands/detectProject.js +112 -0
  15. package/src/cli/commands/doctor.js +60 -0
  16. package/src/cli/commands/envSync.js +319 -0
  17. package/src/cli/commands/generateProfileGuide.js +144 -0
  18. package/src/cli/commands/pruneSnapshot.js +106 -0
  19. package/src/cli/commands/restoreSnapshot.js +173 -0
  20. package/src/cli/commands/setupGemini.js +149 -0
  21. package/src/cli/commands/setupGemini.test.js +115 -0
  22. package/src/cli/commands/setupMcp.js +269 -0
  23. package/src/cli/commands/showFile.js +39 -0
  24. package/src/cli/commands/trainTokens.js +38 -0
  25. package/src/cli/commands/updateSnapshot.js +219 -0
  26. package/src/config.js +125 -0
  27. package/src/core/skeletonizer.js +201 -0
  28. package/src/mcp-server/index.js +211 -0
  29. package/src/services/claudeCliService.js +626 -0
  30. package/src/services/claudeCliService.test.js +267 -0
  31. package/src/templates/agent-prompt.template.md +43 -0
  32. package/src/templates/architect-prompt.template.md +164 -0
  33. package/src/templates/claude-code/README.md +105 -0
  34. package/src/templates/claude-code/mcp-config-template.json +11 -0
  35. package/src/templates/claude-code/mcp-server-template.js +206 -0
  36. package/src/templates/claude-code/settings-claude.json +1 -0
  37. package/src/templates/envScanRequest.md +4 -0
  38. package/src/templates/gitWorkflow.md +32 -0
  39. package/src/templates/multiAgent.md +118 -0
  40. package/src/templates/opencode/coder.template.md +22 -0
  41. package/src/templates/opencode/junior-architect.template.md +85 -0
  42. package/src/templates/skeleton-instruction.md +16 -0
  43. package/src/templates/update-prompt.template.md +19 -0
  44. package/src/utils/aiHeader.js +678 -0
  45. package/src/utils/claudeMdGenerator.js +148 -0
  46. package/src/utils/eckProtocolParser.js +221 -0
  47. package/src/utils/fileUtils.js +1017 -0
  48. package/src/utils/gitUtils.js +44 -0
  49. package/src/utils/opencodeAgentsGenerator.js +271 -0
  50. package/src/utils/projectDetector.js +704 -0
  51. 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);