@xelth/eck-snapshot 4.1.0 → 4.2.1

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 CHANGED
@@ -12,7 +12,7 @@ This tool is built for a modern workflow where you act as the architect, guiding
12
12
  `eckSnapshot` is designed to support a two-part AI workflow for maximum efficiency and quality:
13
13
 
14
14
  1. **The Architect LLM (Gemini, GPT-4, Grok):** A model with a large context window. You provide it with a snapshot of your project, and it analyzes the codebase to create a high-level plan for new features or refactoring.
15
- 2. **The Coder LLM (Claude Code, OpenAI Codex):** A model specialized in writing and modifying code. It takes the detailed instructions generated by the Architect and performs the hands-on coding tasks.
15
+ 2. **The Coder LLM (Claude Code, Minimax, etc.):** A model specialized in writing and modifying code. It takes the detailed instructions generated by the Architect and performs the hands-on coding tasks.
16
16
 
17
17
  ## Requirements
18
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xelth/eck-snapshot",
3
- "version": "4.1.0",
3
+ "version": "4.2.1",
4
4
  "description": "A powerful CLI tool to create and restore single-file text snapshots of Git repositories and directories. Optimized for AI context and LLM workflows.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -19,8 +19,7 @@
19
19
  "test": "vitest",
20
20
  "test:ui": "vitest --ui",
21
21
  "test:run": "vitest run",
22
- "docs:auto": "node index.js docs-auto",
23
- "test:gpt": "vitest src/services/gptService.test.js"
22
+ "docs:auto": "node index.js docs-auto"
24
23
  },
25
24
  "author": "xelth-com",
26
25
  "license": "MIT",
@@ -41,15 +40,20 @@
41
40
  "inquirer": "^9.2.20",
42
41
  "is-binary-path": "^2.1.0",
43
42
  "micromatch": "^4.0.8",
43
+ "minimatch": "^9.0.3",
44
44
  "ora": "^8.1.0",
45
45
  "p-limit": "^5.0.0",
46
46
  "p-retry": "^6.2.1",
47
+ "which": "^4.0.0"
48
+ },
49
+ "optionalDependencies": {
47
50
  "tree-sitter": "^0.25.0",
48
51
  "tree-sitter-c": "^0.24.1",
52
+ "tree-sitter-go": "^0.23.4",
49
53
  "tree-sitter-java": "^0.23.5",
50
54
  "tree-sitter-kotlin": "^0.3.8",
51
55
  "tree-sitter-python": "^0.25.0",
52
- "which": "^4.0.0"
56
+ "tree-sitter-rust": "^0.23.2"
53
57
  },
54
58
  "devDependencies": {
55
59
  "jsdom": "^24.0.0",
package/setup.json CHANGED
@@ -231,16 +231,13 @@
231
231
  },
232
232
  "contextProfiles": {
233
233
  "backend": {
234
- "description": "Backend API, database, business logic",
234
+ "description": "Backend API, services, business logic",
235
235
  "include": [
236
236
  "packages/backend/**",
237
- "packages/core/**",
238
- "knexfile.js",
239
- "migrations/**"
237
+ "packages/core/**"
240
238
  ],
241
239
  "exclude": [
242
240
  "**/*.test.*",
243
- "**/*.sqlite*",
244
241
  "node_modules/**"
245
242
  ]
246
243
  },
@@ -301,8 +298,7 @@
301
298
  "description": "Database schema and migrations only",
302
299
  "include": [
303
300
  "**/migrations/**",
304
- "**/knexfile.js",
305
- "**/schema.sql"
301
+ "**/schema/**"
306
302
  ]
307
303
  },
308
304
  "deployment": {
@@ -544,6 +540,10 @@
544
540
  "maxDepth": 10,
545
541
  "concurrency": 10
546
542
  },
543
+ "security": {
544
+ "scanForSecrets": true,
545
+ "_comment": "Automatically detects and redacts API keys, tokens, and credentials in snapshots to prevent accidental exposure"
546
+ },
547
547
  "output": {
548
548
  "defaultFormat": "md",
549
549
  "defaultPath": "./.eck/snapshots",
@@ -671,25 +671,6 @@
671
671
  "no hardware debugging interfaces"
672
672
  ]
673
673
  },
674
- "ci_cd": {
675
- "active": false,
676
- "name": "CI/CD Pipeline Agent (AGENT_CI_CD)",
677
- "description": "Automated testing and deployment pipeline",
678
- "guiSupport": false,
679
- "capabilities": [
680
- "npm ci",
681
- "npm test",
682
- "npm run build",
683
- "docker build",
684
- "artifact generation"
685
- ],
686
- "restrictions": [
687
- "no interactive commands",
688
- "no GUI applications",
689
- "no watch modes",
690
- "no development servers"
691
- ]
692
- },
693
674
  "gemini_wsl": {
694
675
  "active": true,
695
676
  "name": "Gemini WSL Agent (Junior Architect)",
@@ -763,7 +744,6 @@
763
744
  "envScanRequest": "src/templates/envScanRequest.md",
764
745
  "gitWorkflow": "src/templates/gitWorkflow.md",
765
746
  "multiAgent": "src/templates/multiAgent.md",
766
- "vectorMode": "src/templates/vectorMode.md",
767
747
  "agent": "src/templates/agent-prompt.template.md"
768
748
  }
769
749
  },
@@ -802,11 +782,11 @@
802
782
  },
803
783
  "database_expert": {
804
784
  "active": true,
805
- "modelName": "GPT-4/Claude",
785
+ "modelName": "Claude",
806
786
  "role": "Database Specialist",
807
787
  "strengths": [
808
- "PostgreSQL optimization",
809
- "Knex migrations",
788
+ "Schema design",
789
+ "Query optimization",
810
790
  "data integrity"
811
791
  ]
812
792
  },
package/src/cli/cli.js CHANGED
@@ -13,14 +13,13 @@ import { pruneSnapshot } from './commands/pruneSnapshot.js';
13
13
  import { generateConsilium } from './commands/consilium.js';
14
14
  import { detectProject, testFileParsing } from './commands/detectProject.js';
15
15
  import { trainTokens, showTokenStats } from './commands/trainTokens.js';
16
- import { askGpt } from './commands/askGpt.js';
17
- import { ask as askGptService } from '../services/gptService.js';
18
16
  import { executePrompt, executePromptWithSession } from '../services/claudeCliService.js';
19
17
  import { detectProfiles } from './commands/detectProfiles.js';
20
18
  import { generateProfileGuide } from './commands/generateProfileGuide.js';
21
19
  import { setupGemini } from './commands/setupGemini.js';
22
20
  import { generateAutoDocs } from './commands/autoDocs.js';
23
21
  import { showFile } from './commands/showFile.js';
22
+ import { runDoctor } from './commands/doctor.js';
24
23
  import inquirer from 'inquirer';
25
24
  import ora from 'ora';
26
25
  import { execa } from 'execa';
@@ -112,7 +111,7 @@ Option C: Using Profiles
112
111
 
113
112
  - restore: Restore files from a snapshot to disk.
114
113
  - prune: Use AI to shrink a snapshot file by importance.
115
- - ask-gpt/claude: Delegate tasks to agents directly (via API).
114
+ - ask-claude: Delegate tasks to Claude CLI agent.
116
115
  - setup-gemini: Configure gemini-cli integration.
117
116
  `;
118
117
 
@@ -246,47 +245,6 @@ Creating Custom Profiles:
246
245
  console.log(JSON.stringify(result, null, 2));
247
246
  });
248
247
 
249
- program
250
- .command('ask-gpt')
251
- .description('Delegate tasks to OpenAI Codex agent with automatic authentication')
252
- .argument('<payload>', 'JSON payload string (e.g. \'{"objective": "Calculate 5+2"}\')')
253
- .option('-v, --verbose', 'Enable verbose logging and detailed execution output')
254
- .option('--model <name>', 'Model to use (default: gpt-5-codex)', 'gpt-5-codex')
255
- .option('--reasoning <level>', 'Reasoning level: low, medium, high (default: high)', 'high')
256
- .action((payloadArg, cmd) => askGpt(payloadArg, cmd))
257
- .addHelpText('after', `
258
- Examples:
259
- Ask a simple question:
260
- eck-snapshot ask-gpt '{"objective": "What is 5+2?"}'
261
-
262
- Request code changes with context:
263
- eck-snapshot ask-gpt '{
264
- "target_agent": "local_dev",
265
- "task_id": "feature-123",
266
- "payload": {
267
- "objective": "Add error handling to login function",
268
- "files_to_modify": [{"path": "src/auth.js", "action": "modify"}]
269
- },
270
- "post_execution_steps": {
271
- "journal_entry": {
272
- "type": "feat",
273
- "scope": "auth",
274
- "summary": "Add error handling"
275
- }
276
- }
277
- }' --verbose
278
-
279
- Prerequisites:
280
- 1. Install Codex CLI: npm install -g @openai/codex
281
- 2. Login: codex login (requires ChatGPT Plus/Pro subscription)
282
- 3. The command automatically loads .eck project context
283
-
284
- Authentication:
285
- - Uses your existing 'codex login' credentials
286
- - Auto-retries on authentication errors
287
- - Supports ChatGPT Plus/Pro subscriptions
288
- `);
289
-
290
248
  // Project detection command
291
249
  program
292
250
  .command('detect')
@@ -344,16 +302,8 @@ Authentication:
344
302
  const result = await executePrompt(prompt, options.continue);
345
303
  console.log(JSON.stringify(result, null, 2));
346
304
  } catch (error) {
347
- console.warn(`⚠️ Claude failed: ${error.message}`);
348
- console.log('🔄 Failing over to GPT for task...');
349
- try {
350
- const payload = (typeof prompt === 'string' && prompt.startsWith('{')) ? prompt : JSON.stringify({ objective: prompt });
351
- const gptResult = await askGptService(payload, { verbose: false });
352
- console.log(JSON.stringify(gptResult, null, 2));
353
- } catch (gptError) {
354
- console.error('Failed to execute prompt with both Claude and GPT:', gptError.message);
355
- process.exit(1);
356
- }
305
+ console.error(`Failed to execute prompt: ${error.message}`);
306
+ process.exit(1);
357
307
  }
358
308
  });
359
309
 
@@ -413,5 +363,12 @@ Authentication:
413
363
  .argument('<filePaths...>', 'Space-separated paths to files')
414
364
  .action(showFile);
415
365
 
366
+ // Doctor command (health check for manifests)
367
+ program
368
+ .command('doctor')
369
+ .description('Check project health and detect unfinished manifest stubs')
370
+ .argument('[repoPath]', 'Path to the repository', process.cwd())
371
+ .action(runDoctor);
372
+
416
373
  program.parse(process.argv);
417
374
  }
@@ -19,38 +19,7 @@ export async function generateAutoDocs() {
19
19
  await fs.access(extensionsDir);
20
20
  } catch (error) {
21
21
  console.log(`Extensions directory not found at: ${extensionsDir}`);
22
- console.log('Creating example structure...');
23
-
24
- // Create the directory structure
25
- await fs.mkdir(extensionsDir, { recursive: true });
26
-
27
- // Create a sample gemini-extension.json file for demonstration
28
- const sampleExtension = {
29
- name: "sample-extension",
30
- description: "Sample Gemini extension for demonstration",
31
- commands: [
32
- {
33
- name: "sample-command",
34
- description: "A sample command for testing auto-docs",
35
- usage: "sample-command [options]",
36
- examples: ["sample-command --help"]
37
- }
38
- ],
39
- tools: [
40
- {
41
- name: "sample-tool",
42
- description: "A sample tool for testing auto-docs",
43
- usage: "Use this tool for sample operations"
44
- }
45
- ]
46
- };
47
-
48
- await fs.writeFile(
49
- path.join(extensionsDir, 'sample-extension.json'),
50
- JSON.stringify(sampleExtension, null, 2)
51
- );
52
-
53
- console.log('Created sample extension at:', path.join(extensionsDir, 'sample-extension.json'));
22
+ return;
54
23
  }
55
24
 
56
25
  // Read all JSON files in the extensions directory
@@ -8,12 +8,14 @@ import zlib from 'zlib';
8
8
  import { promisify } from 'util';
9
9
  import ora from 'ora';
10
10
  import micromatch from 'micromatch';
11
+ import chalk from 'chalk';
11
12
 
12
13
  import {
13
14
  parseSize, formatSize, matchesPattern, checkGitRepository,
14
15
  scanDirectoryRecursively, loadGitignore, readFileWithSizeCheck,
15
16
  generateDirectoryTree, loadConfig, displayProjectInfo, loadProjectEckManifest,
16
- ensureSnapshotsInGitignore, initializeEckManifest
17
+ ensureSnapshotsInGitignore, initializeEckManifest, generateTimestamp,
18
+ SecretScanner
17
19
  } from '../../utils/fileUtils.js';
18
20
  import { detectProjectType, getProjectSpecificFiltering } from '../../utils/projectDetector.js';
19
21
  import { estimateTokensWithPolynomial, generateTrainingCommand } from '../../utils/tokenEstimator.js';
@@ -131,11 +133,77 @@ import { generateEnhancedAIHeader } from '../../utils/aiHeader.js';
131
133
 
132
134
  const gzip = promisify(zlib.gzip);
133
135
 
136
+ /**
137
+ * Check if a path is a hidden directory/folder (starts with '.')
138
+ * This excludes all hidden folders like .git, .eck, .claude, .gemini from snapshots
139
+ * @param {string} filePath - File or directory path
140
+ * @returns {boolean} True if path is hidden
141
+ */
142
+ function isHiddenPath(filePath) {
143
+ // Check if path or any parent directory starts with '.'
144
+ const parts = filePath.split('/');
145
+ return parts.some(part => part.startsWith('.'));
146
+ }
147
+
148
+ /**
149
+ * Scans the .eck directory for confidential files
150
+ * @param {string} projectPath - Path to the project
151
+ * @param {object} config - Configuration object
152
+ * @returns {Promise<string[]>} Array of confidential file paths
153
+ */
154
+ async function scanEckForConfidentialFiles(projectPath, config) {
155
+ const eckPath = path.join(projectPath, '.eck');
156
+
157
+ try {
158
+ await fs.access(eckPath);
159
+ } catch {
160
+ return []; // .eck directory doesn't exist
161
+ }
162
+
163
+ const result = await scanDirectoryRecursively(eckPath, config, projectPath, null, true);
164
+ return result.confidentialFiles || [];
165
+ }
166
+
167
+ /**
168
+ * Generates CLAUDE.md content with references to confidential files
169
+ * @param {string[]} confidentialFiles - Array of confidential file paths
170
+ * @param {string} repoPath - Path to the repository
171
+ * @returns {string} CLAUDE.md content
172
+ */
173
+ function generateClaudeMdContent(confidentialFiles, repoPath) {
174
+ const content = [`# Project Access & Credentials Reference`, ``];
175
+
176
+ if (confidentialFiles.length === 0) {
177
+ content.push('No confidential files found in .eck directory.');
178
+ return content.join('\n');
179
+ }
180
+
181
+ content.push('## Access & Credentials');
182
+ content.push('');
183
+ content.push('The following confidential files are available locally but not included in snapshots:');
184
+ content.push('');
185
+
186
+ for (const file of confidentialFiles) {
187
+ const absolutePath = path.join(repoPath, file);
188
+ const fileName = path.basename(file);
189
+ content.push(`- **${fileName}**: \`${absolutePath}\``);
190
+ }
191
+
192
+ content.push('');
193
+ content.push('> **Note**: These files contain sensitive information and should only be accessed when needed.');
194
+ content.push('> They are excluded from snapshots for security reasons but can be referenced on demand.');
195
+
196
+ return content.join('\n');
197
+ }
198
+
134
199
  async function getProjectFiles(projectPath, config) {
135
200
  const isGitRepo = await checkGitRepository(projectPath);
136
201
  if (isGitRepo) {
137
202
  const { stdout } = await execa('git', ['ls-files'], { cwd: projectPath });
138
- return stdout.split('\n').filter(Boolean);
203
+ const gitFiles = stdout.split('\n').filter(Boolean);
204
+ // Filter out hidden directories/files (starting with '.')
205
+ const filteredFiles = gitFiles.filter(file => !isHiddenPath(file));
206
+ return filteredFiles;
139
207
  }
140
208
  return scanDirectoryRecursively(projectPath, config);
141
209
  }
@@ -231,6 +299,7 @@ async function processProjectFiles(repoPath, options, config, projectType = null
231
299
  ignoredFiles: 0,
232
300
  totalSize: 0,
233
301
  processedSize: 0,
302
+ secretsRedacted: 0,
234
303
  errors: [],
235
304
  skipReasons: new Map(),
236
305
  skippedFilesDetails: new Map()
@@ -242,6 +311,11 @@ async function processProjectFiles(repoPath, options, config, projectType = null
242
311
  console.log('🔍 Scanning repository...');
243
312
  let allFiles = await getProjectFiles(repoPath, config);
244
313
 
314
+ // Filter the raw file list immediately so ignored files don't show up in the Tree
315
+ if (config.filesToIgnore && config.filesToIgnore.length > 0) {
316
+ allFiles = allFiles.filter(file => !matchesPattern(file, config.filesToIgnore));
317
+ }
318
+
245
319
  if (options.profile) {
246
320
  console.log(`Applying profile filter: '${options.profile}'...`);
247
321
  allFiles = await applyProfileFilter(allFiles, options.profile, repoPath);
@@ -277,6 +351,13 @@ async function processProjectFiles(repoPath, options, config, projectType = null
277
351
  progressBar.update(index + 1, { filename: normalizedPath.slice(0, 50) });
278
352
 
279
353
  try {
354
+ // Skip all hidden directories and files (starting with '.')
355
+ if (isHiddenPath(normalizedPath)) {
356
+ stats.ignoredFiles++;
357
+ trackSkippedFile(normalizedPath, 'Hidden directories/files');
358
+ return null;
359
+ }
360
+
280
361
  // Check if file should be ignored by directory patterns
281
362
  if (config.dirsToIgnore.some(dir => normalizedPath.startsWith(dir))) {
282
363
  stats.ignoredFiles++;
@@ -325,6 +406,17 @@ async function processProjectFiles(repoPath, options, config, projectType = null
325
406
  }
326
407
 
327
408
  let content = await readFileWithSizeCheck(fullPath, maxFileSize);
409
+
410
+ // Security scan for secrets
411
+ if (config.security?.scanForSecrets !== false) {
412
+ const scanResult = SecretScanner.redact(content, normalizedPath);
413
+ if (scanResult.found.length > 0) {
414
+ stats.secretsRedacted += scanResult.found.length;
415
+ console.log(chalk.yellow(`\n ⚠️ Security: Found ${scanResult.found.join(', ')} in ${normalizedPath}. Redacting...`));
416
+ content = scanResult.content;
417
+ }
418
+ }
419
+
328
420
  stats.includedFiles++;
329
421
  stats.processedSize += fileStats.size;
330
422
 
@@ -399,8 +491,8 @@ export async function createRepoSnapshot(repoPath, options) {
399
491
  if (status) {
400
492
  spinner.text = 'Unstaged changes detected. Auto-committing...';
401
493
  await execa('git', ['add', '.'], { cwd: repoPath });
402
- const timestamp = new Date().toISOString().slice(0, 19).replace('T', '_').replace(/:/g, '-');
403
- await execa('git', ['commit', '-m', `chore(snapshot): Auto-commit before snapshot [${timestamp}]`], { cwd: repoPath });
494
+ const commitTimestamp = generateTimestamp();
495
+ await execa('git', ['commit', '-m', `chore(snapshot): Auto-commit before snapshot [${commitTimestamp}]`], { cwd: repoPath });
404
496
  spinner.info('Auto-commit complete.');
405
497
  } else {
406
498
  // No changes, do nothing. Logging this would be too verbose.
@@ -434,6 +526,15 @@ export async function createRepoSnapshot(repoPath, options) {
434
526
  ...options // Command-line options have the final say
435
527
  };
436
528
 
529
+ // If NOT in Junior Architect mode, hide JA-specific documentation to prevent context pollution
530
+ if (!options.withJa) {
531
+ if (!config.filesToIgnore) config.filesToIgnore = [];
532
+ config.filesToIgnore.push(
533
+ 'COMMANDS_REFERENCE.md',
534
+ 'codex_delegation_snapshot.md'
535
+ );
536
+ }
537
+
437
538
  // Apply defaults for options that may not be provided via command line
438
539
  if (!config.output) {
439
540
  config.output = setupConfig.output?.defaultPath || './snapshots';
@@ -468,8 +569,8 @@ export async function createRepoSnapshot(repoPath, options) {
468
569
  process.chdir(processedRepoPath); // Go back to repo path for git hash and tree
469
570
 
470
571
  try {
471
- // --- Common Data ---
472
- const timestamp = new Date().toISOString().slice(0, 19).replace('T', '_').replace(/:/g, '-');
572
+ // --- Common Data ---
573
+ const timestamp = generateTimestamp();
473
574
  const repoName = path.basename(processedRepoPath);
474
575
  const gitHash = await getGitCommitHash(processedRepoPath);
475
576
  const fileExtension = options.format || config.defaultFormat || 'md';
@@ -531,6 +632,17 @@ export async function createRepoSnapshot(repoPath, options) {
531
632
  // Save git anchor for future delta updates
532
633
  await saveGitAnchor(processedRepoPath);
533
634
 
635
+ // --- Generate CLAUDE.md with confidential file references ---
636
+ console.log('🔐 Scanning for confidential files in .eck directory...');
637
+ const confidentialFiles = await scanEckForConfidentialFiles(processedRepoPath, config);
638
+
639
+ if (confidentialFiles.length > 0) {
640
+ const claudeMdContent = generateClaudeMdContent(confidentialFiles, processedRepoPath);
641
+ const claudeMdPath = path.join(processedRepoPath, 'CLAUDE.md');
642
+ await fs.writeFile(claudeMdPath, claudeMdContent);
643
+ console.log(`📝 Generated CLAUDE.md with ${confidentialFiles.length} confidential file reference(s)`);
644
+ }
645
+
534
646
  // --- Combined Report ---
535
647
  console.log('\n✅ Snapshot generation complete!');
536
648
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
@@ -562,6 +674,13 @@ export async function createRepoSnapshot(repoPath, options) {
562
674
  }
563
675
  }
564
676
 
677
+ // Security Report Section
678
+ if (stats.secretsRedacted > 0) {
679
+ console.log('\n🔐 Security:');
680
+ console.log('---------------------------------');
681
+ console.log(chalk.yellow(` ⚠️ ${stats.secretsRedacted} secret(s) detected and redacted`));
682
+ }
683
+
565
684
  // Excluded/Skipped Files Section
566
685
  const hasExcludedContent = stats.excludedFiles > 0 || stats.binaryFiles > 0 || stats.oversizedFiles > 0 || stats.ignoredFiles > 0 || stats.errors.length > 0;
567
686
  if (hasExcludedContent) {
@@ -0,0 +1,60 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ /**
6
+ * Scans .eck directory for files containing [STUB] markers
7
+ */
8
+ export async function runDoctor(repoPath = process.cwd()) {
9
+ const eckDir = path.join(repoPath, '.eck');
10
+ console.log(chalk.blue('🏥 Checking project health and manifest integrity...'));
11
+
12
+ try {
13
+ await fs.access(eckDir);
14
+ } catch {
15
+ console.log(chalk.yellow('⚠️ .eck directory not found. Nothing to check.'));
16
+ return;
17
+ }
18
+
19
+ const stubFiles = [];
20
+ const scannedFiles = [];
21
+
22
+ async function scan(dir) {
23
+ const entries = await fs.readdir(dir, { withFileTypes: true });
24
+ for (const entry of entries) {
25
+ const fullPath = path.join(dir, entry.name);
26
+ if (entry.isDirectory()) {
27
+ await scan(fullPath);
28
+ } else if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.json'))) {
29
+ scannedFiles.push(fullPath);
30
+ const content = await fs.readFile(fullPath, 'utf-8');
31
+ if (content.includes('[STUB:')) {
32
+ stubFiles.push({
33
+ path: path.relative(repoPath, fullPath),
34
+ type: 'STUB'
35
+ });
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ await scan(eckDir);
42
+
43
+ if (stubFiles.length === 0) {
44
+ console.log(chalk.green(`\n✅ All clear! Found ${scannedFiles.length} manifest files and no stubs.`));
45
+ } else {
46
+ console.log(chalk.red(`\n❌ Found ${stubFiles.length} files that need attention:`));
47
+ stubFiles.forEach(file => {
48
+ console.log(chalk.yellow(` - ${file.path} `) + chalk.gray('(contains [STUB] marker)'));
49
+ });
50
+ console.log(chalk.cyan('\n💡 Tip: Instruct your Coder agent to "Finalize these stubs by analyzing the code".'));
51
+ }
52
+
53
+ // Cross-platform tree-sitter check
54
+ try {
55
+ const ts = await import('tree-sitter');
56
+ console.log(chalk.green('✅ tree-sitter: Installed and loadable.'));
57
+ } catch (e) {
58
+ console.log(chalk.yellow('ℹ️ tree-sitter: Not available (Skeleton mode will be limited for non-JS files).'));
59
+ }
60
+ }
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import ora from 'ora';
4
4
  import { getGitAnchor, getChangedFiles, getGitDiffOutput } from '../../utils/gitUtils.js';
5
5
  import { loadSetupConfig } from '../../config.js';
6
- import { readFileWithSizeCheck, parseSize, formatSize, matchesPattern, loadGitignore } from '../../utils/fileUtils.js';
6
+ import { readFileWithSizeCheck, parseSize, formatSize, matchesPattern, loadGitignore, generateTimestamp } from '../../utils/fileUtils.js';
7
7
  import { fileURLToPath } from 'url';
8
8
 
9
9
  const __filename = fileURLToPath(import.meta.url);
@@ -53,14 +53,14 @@ export async function updateSnapshot(repoPath, options) {
53
53
  const templatePath = path.join(__dirname, '../../templates/update-prompt.template.md');
54
54
  let header = await fs.readFile(templatePath, 'utf-8');
55
55
  header = header.replace('{{anchor}}', anchor.substring(0, 7))
56
- .replace('{{timestamp}}', new Date().toISOString())
56
+ .replace('{{timestamp}}', new Date().toLocaleString())
57
57
  .replace('{{fileList}}', fileList.join('\n'));
58
58
 
59
59
  // Add Git Diff at the end for context
60
60
  const diffOutput = await getGitDiffOutput(repoPath, anchor);
61
61
  const diffSection = `\n--- GIT DIFF (For Context) ---\n\n\`\`\`diff\n${diffOutput}\n\`\`\``;
62
62
 
63
- const outputFilename = `update_${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.md`;
63
+ const outputFilename = `update_${generateTimestamp()}.md`;
64
64
  const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
65
65
 
66
66
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
package/src/config.js CHANGED
@@ -16,6 +16,10 @@ export async function loadSetupConfig() {
16
16
  const setupPath = path.join(__dirname, '..', 'setup.json');
17
17
  const setupContent = await fs.readFile(setupPath, 'utf-8');
18
18
  cachedConfig = JSON.parse(setupContent);
19
+
20
+ // Basic schema validation for critical fields
21
+ validateConfigSchema(cachedConfig);
22
+
19
23
  return cachedConfig;
20
24
  } catch (error) {
21
25
  console.error('Error loading setup.json:', error.message);
@@ -23,6 +27,46 @@ export async function loadSetupConfig() {
23
27
  }
24
28
  }
25
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
+
26
70
  /**
27
71
  * Loads and merges all profiles (local-first).
28
72
  */