minovative-mind-cli 2.2.4 → 2.3.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.
Files changed (36) hide show
  1. package/LICENSE.md +2 -0
  2. package/README.md +19 -20
  3. package/dist/commands/logout.js +1 -1
  4. package/dist/services/agent/slashCommands.js +54 -15
  5. package/dist/services/agent/toolLoop.js +4 -1
  6. package/dist/services/agent/types.d.ts +4 -0
  7. package/dist/services/agent-tools.js +55 -27
  8. package/dist/services/agent.d.ts +4 -0
  9. package/dist/services/agent.js +79 -62
  10. package/dist/services/ai.d.ts +1 -2
  11. package/dist/services/ai.js +21 -19
  12. package/dist/services/auth.d.ts +1 -1
  13. package/dist/services/auth.js +7 -28
  14. package/dist/services/chatHistoryService.d.ts +8 -0
  15. package/dist/services/contextAgent.js +18 -1
  16. package/dist/services/investigationComplexity.d.ts +1 -1
  17. package/dist/services/investigationComplexity.js +1 -1
  18. package/dist/services/orchestration/investigationAgent.d.ts +6 -2
  19. package/dist/services/orchestration/investigationAgent.js +35 -10
  20. package/dist/services/orchestration/investigationOrchestrator.js +3 -1
  21. package/dist/services/orchestration/orchestrator.d.ts +1 -1
  22. package/dist/services/orchestration/orchestrator.js +39 -16
  23. package/dist/services/orchestration/subAgent.d.ts +5 -1
  24. package/dist/services/orchestration/subAgent.js +33 -17
  25. package/dist/services/proxyClient.d.ts +16 -0
  26. package/dist/services/proxyClient.js +32 -0
  27. package/dist/utils/analysisRunner.js +2 -2
  28. package/dist/utils/config.d.ts +3 -4
  29. package/dist/utils/config.js +3 -4
  30. package/dist/utils/credentialStore.d.ts +30 -0
  31. package/dist/utils/credentialStore.js +540 -0
  32. package/dist/utils/projectStorage.js +70 -0
  33. package/dist/utils/systemPrompts.d.ts +3 -3
  34. package/dist/utils/systemPrompts.js +4 -4
  35. package/oclif.manifest.json +1 -1
  36. package/package.json +2 -1
@@ -0,0 +1,540 @@
1
+ import { execFile } from 'node:child_process';
2
+ import * as crypto from 'node:crypto';
3
+ import * as fs from 'node:fs';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import { debugLog } from './logger.js';
7
+ // ─── Constants ───────────────────────────────────────────────────────
8
+ const SERVICE_NAME = 'minovative-mind-cli';
9
+ const ACCOUNT_NAME = 'default';
10
+ const KEYCHAIN_LABEL = 'Minovative Mind CLI';
11
+ /**
12
+ * Maximum time (ms) to wait for an OS keychain CLI call before falling back.
13
+ * Prevents indefinite hangs over SSH or headless sessions where the keychain
14
+ * may prompt for GUI interaction that will never arrive.
15
+ */
16
+ const KEYCHAIN_TIMEOUT_MS = 3000;
17
+ /** Encrypted fallback file path. */
18
+ const ENCRYPTED_FILE = path.join(os.homedir(), '.minovative-mind-cli.enc');
19
+ /** Machine-specific salt used for AES key derivation in fallback mode. */
20
+ const SALT_FILE = path.join(os.homedir(), '.minovative-mind-cli-salt');
21
+ /** Legacy plaintext file path (for migration). */
22
+ export const LEGACY_CONFIG_FILE = path.join(os.homedir(), '.minovative-mind-cli.json');
23
+ // ─── Backend Detection ───────────────────────────────────────────────
24
+ let cachedBackend = null;
25
+ /**
26
+ * Determines the best available credential storage backend for the current OS.
27
+ * Results are cached after the first successful probe.
28
+ */
29
+ async function detectBackend() {
30
+ if (cachedBackend)
31
+ return cachedBackend;
32
+ const platform = process.platform;
33
+ if (platform === 'darwin') {
34
+ if (await isCommandAvailable('security')) {
35
+ cachedBackend = 'keychain-macos';
36
+ debugLog('Credential store: using macOS Keychain');
37
+ return cachedBackend;
38
+ }
39
+ }
40
+ if (platform === 'linux') {
41
+ if (await isCommandAvailable('secret-tool')) {
42
+ cachedBackend = 'libsecret-linux';
43
+ debugLog('Credential store: using libsecret (secret-tool)');
44
+ return cachedBackend;
45
+ }
46
+ }
47
+ if (platform === 'win32') {
48
+ // DPAPI is always available on Windows via PowerShell
49
+ if (await isCommandAvailable('powershell')) {
50
+ cachedBackend = 'dpapi-windows';
51
+ debugLog('Credential store: using Windows DPAPI');
52
+ return cachedBackend;
53
+ }
54
+ }
55
+ cachedBackend = 'encrypted-file';
56
+ debugLog('Credential store: using AES-256-GCM encrypted file fallback');
57
+ return cachedBackend;
58
+ }
59
+ /**
60
+ * Executes a command with a timeout. If the command doesn't complete within
61
+ * the timeout, the child process is killed and the promise rejects.
62
+ * This prevents hangs in headless/SSH sessions where OS keychain tools
63
+ * might wait for GUI prompts that will never arrive.
64
+ */
65
+ async function execWithTimeout(command, args, timeoutMs = KEYCHAIN_TIMEOUT_MS) {
66
+ return new Promise((resolve, reject) => {
67
+ const child = execFile(command, args, { timeout: timeoutMs }, (error, stdout, stderr) => {
68
+ if (error) {
69
+ reject(error);
70
+ }
71
+ else {
72
+ resolve({ stdout: stdout, stderr: stderr });
73
+ }
74
+ });
75
+ // Belt-and-suspenders: kill if the built-in timeout doesn't fire
76
+ const timer = setTimeout(() => {
77
+ child.kill('SIGTERM');
78
+ reject(new Error(`${command} timed out after ${timeoutMs}ms (possible GUI prompt in headless session)`));
79
+ }, timeoutMs + 500);
80
+ child.on('close', () => clearTimeout(timer));
81
+ });
82
+ }
83
+ /**
84
+ * Checks whether a given CLI command is available on PATH.
85
+ */
86
+ async function isCommandAvailable(command) {
87
+ try {
88
+ const which = process.platform === 'win32' ? 'where' : 'which';
89
+ await execWithTimeout(which, [command], 2000);
90
+ return true;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ // ─── macOS Keychain ──────────────────────────────────────────────────
97
+ async function macSave(json) {
98
+ try {
99
+ // -U flag updates the item if it already exists instead of erroring
100
+ await execWithTimeout('security', [
101
+ 'add-generic-password',
102
+ '-a', ACCOUNT_NAME,
103
+ '-s', SERVICE_NAME,
104
+ '-l', KEYCHAIN_LABEL,
105
+ '-w', json,
106
+ '-U',
107
+ ]);
108
+ }
109
+ catch (err) {
110
+ throw new Error(`macOS Keychain save failed: ${err.message}`);
111
+ }
112
+ }
113
+ async function macLoad() {
114
+ try {
115
+ const { stdout } = await execWithTimeout('security', [
116
+ 'find-generic-password',
117
+ '-a', ACCOUNT_NAME,
118
+ '-s', SERVICE_NAME,
119
+ '-w',
120
+ ]);
121
+ return stdout.trim();
122
+ }
123
+ catch {
124
+ // Item not found (exit code 44) or keychain locked
125
+ return null;
126
+ }
127
+ }
128
+ async function macClear() {
129
+ try {
130
+ await execWithTimeout('security', [
131
+ 'delete-generic-password',
132
+ '-a', ACCOUNT_NAME,
133
+ '-s', SERVICE_NAME,
134
+ ]);
135
+ }
136
+ catch {
137
+ // Item not found — already cleared, ignore
138
+ }
139
+ }
140
+ // ─── Linux libsecret ────────────────────────────────────────────────
141
+ async function linuxSave(json) {
142
+ try {
143
+ // secret-tool reads the secret value from stdin
144
+ const child = execFile('secret-tool', [
145
+ 'store',
146
+ '--label', KEYCHAIN_LABEL,
147
+ 'service', SERVICE_NAME,
148
+ 'account', ACCOUNT_NAME,
149
+ ]);
150
+ if (child.stdin) {
151
+ child.stdin.write(json);
152
+ child.stdin.end();
153
+ }
154
+ await new Promise((resolve, reject) => {
155
+ const timer = setTimeout(() => {
156
+ child.kill('SIGTERM');
157
+ reject(new Error('secret-tool store timed out (possible missing DBus session)'));
158
+ }, KEYCHAIN_TIMEOUT_MS);
159
+ child.on('close', (code) => {
160
+ clearTimeout(timer);
161
+ if (code === 0)
162
+ resolve();
163
+ else
164
+ reject(new Error(`secret-tool store exited with code ${code}`));
165
+ });
166
+ child.on('error', (err) => {
167
+ clearTimeout(timer);
168
+ reject(err);
169
+ });
170
+ });
171
+ }
172
+ catch (err) {
173
+ throw new Error(`libsecret save failed: ${err.message}`);
174
+ }
175
+ }
176
+ async function linuxLoad() {
177
+ try {
178
+ const { stdout } = await execWithTimeout('secret-tool', [
179
+ 'lookup',
180
+ 'service', SERVICE_NAME,
181
+ 'account', ACCOUNT_NAME,
182
+ ]);
183
+ return stdout.trim() || null;
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ }
189
+ async function linuxClear() {
190
+ try {
191
+ await execWithTimeout('secret-tool', [
192
+ 'clear',
193
+ 'service', SERVICE_NAME,
194
+ 'account', ACCOUNT_NAME,
195
+ ]);
196
+ }
197
+ catch {
198
+ // Already cleared
199
+ }
200
+ }
201
+ // ─── Windows DPAPI ──────────────────────────────────────────────────
202
+ const WIN_CREDENTIAL_DIR = path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), SERVICE_NAME);
203
+ const WIN_CREDENTIAL_FILE = path.join(WIN_CREDENTIAL_DIR, 'credentials.enc');
204
+ async function windowsSave(json) {
205
+ try {
206
+ fs.mkdirSync(WIN_CREDENTIAL_DIR, { recursive: true });
207
+ // Use PowerShell with DPAPI to encrypt. The output is a Base64 string.
208
+ const psScript = `
209
+ Add-Type -AssemblyName System.Security
210
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes('${json.replace(/'/g, "''")}')
211
+ $encrypted = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
212
+ [Convert]::ToBase64String($encrypted)
213
+ `;
214
+ const { stdout } = await execWithTimeout('powershell', ['-NoProfile', '-NonInteractive', '-Command', psScript], 5000);
215
+ fs.writeFileSync(WIN_CREDENTIAL_FILE, stdout.trim(), 'utf-8');
216
+ }
217
+ catch (err) {
218
+ throw new Error(`Windows DPAPI save failed: ${err.message}`);
219
+ }
220
+ }
221
+ async function windowsLoad() {
222
+ try {
223
+ if (!fs.existsSync(WIN_CREDENTIAL_FILE))
224
+ return null;
225
+ const encrypted = fs.readFileSync(WIN_CREDENTIAL_FILE, 'utf-8').trim();
226
+ const psScript = `
227
+ Add-Type -AssemblyName System.Security
228
+ $encrypted = [Convert]::FromBase64String('${encrypted}')
229
+ $bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($encrypted, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
230
+ [System.Text.Encoding]::UTF8.GetString($bytes)
231
+ `;
232
+ const { stdout } = await execWithTimeout('powershell', ['-NoProfile', '-NonInteractive', '-Command', psScript], 5000);
233
+ return stdout.trim() || null;
234
+ }
235
+ catch {
236
+ return null;
237
+ }
238
+ }
239
+ async function windowsClear() {
240
+ try {
241
+ if (fs.existsSync(WIN_CREDENTIAL_FILE)) {
242
+ fs.unlinkSync(WIN_CREDENTIAL_FILE);
243
+ }
244
+ }
245
+ catch {
246
+ // Ignore
247
+ }
248
+ }
249
+ // ─── AES-256-GCM Encrypted File Fallback ────────────────────────────
250
+ //
251
+ // Security model note: The encrypted file fallback is intentionally weaker
252
+ // than OS-native keychain storage. The AES key is derived from machine
253
+ // identifiers (hostname + username) which are not secret. An attacker with
254
+ // filesystem access to both the salt file and encrypted file, plus knowledge
255
+ // of the derivation scheme, could reconstruct the key. This is by design —
256
+ // the fallback targets environments (Docker, CI, headless servers) where OS
257
+ // keychains are unavailable. It prevents casual exposure (e.g., `cat` the
258
+ // file, dotfile backups) but is not equivalent to hardware-backed storage.
259
+ // For maximum security, use a desktop environment where OS keychain is available.
260
+ /**
261
+ * Derives a 256-bit AES key from machine-specific identifiers.
262
+ * The salt is generated once and persisted to prevent key drift.
263
+ */
264
+ function deriveEncryptionKey() {
265
+ let salt;
266
+ if (fs.existsSync(SALT_FILE)) {
267
+ salt = Buffer.from(fs.readFileSync(SALT_FILE, 'utf-8'), 'hex');
268
+ }
269
+ else {
270
+ salt = crypto.randomBytes(32);
271
+ fs.writeFileSync(SALT_FILE, salt.toString('hex'), 'utf-8');
272
+ setFilePermissions(SALT_FILE);
273
+ }
274
+ // Combine machine-specific identifiers for key derivation material
275
+ let username = 'unknown';
276
+ try {
277
+ username = os.userInfo().username;
278
+ }
279
+ catch {
280
+ // os.userInfo() can throw in some minimal Docker containers (ENOENT on /etc/passwd)
281
+ }
282
+ const machineId = `${os.hostname()}:${username}:${SERVICE_NAME}`;
283
+ return crypto.pbkdf2Sync(machineId, salt, 100_000, 32, 'sha512');
284
+ }
285
+ function encryptAES(plaintext) {
286
+ const key = deriveEncryptionKey();
287
+ const iv = crypto.randomBytes(16);
288
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
289
+ let encrypted = cipher.update(plaintext, 'utf8', 'hex');
290
+ encrypted += cipher.final('hex');
291
+ const authTag = cipher.getAuthTag().toString('hex');
292
+ // Store as iv:authTag:ciphertext
293
+ return `${iv.toString('hex')}:${authTag}:${encrypted}`;
294
+ }
295
+ function decryptAES(blob) {
296
+ const parts = blob.split(':');
297
+ if (parts.length !== 3)
298
+ throw new Error('Invalid encrypted credential format');
299
+ const [ivHex, authTagHex, ciphertext] = parts;
300
+ const key = deriveEncryptionKey();
301
+ const iv = Buffer.from(ivHex, 'hex');
302
+ const authTag = Buffer.from(authTagHex, 'hex');
303
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
304
+ decipher.setAuthTag(authTag);
305
+ let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
306
+ decrypted += decipher.final('utf8');
307
+ return decrypted;
308
+ }
309
+ async function fileSave(json) {
310
+ const encrypted = encryptAES(json);
311
+ fs.writeFileSync(ENCRYPTED_FILE, encrypted, 'utf-8');
312
+ setFilePermissions(ENCRYPTED_FILE);
313
+ }
314
+ async function fileLoad() {
315
+ try {
316
+ if (!fs.existsSync(ENCRYPTED_FILE))
317
+ return null;
318
+ const blob = fs.readFileSync(ENCRYPTED_FILE, 'utf-8').trim();
319
+ return decryptAES(blob);
320
+ }
321
+ catch {
322
+ return null;
323
+ }
324
+ }
325
+ async function fileClear() {
326
+ try {
327
+ if (fs.existsSync(ENCRYPTED_FILE))
328
+ fs.unlinkSync(ENCRYPTED_FILE);
329
+ if (fs.existsSync(SALT_FILE))
330
+ fs.unlinkSync(SALT_FILE);
331
+ }
332
+ catch {
333
+ // Ignore
334
+ }
335
+ }
336
+ /**
337
+ * Sets restrictive file permissions (owner read/write only) on Unix systems.
338
+ */
339
+ function setFilePermissions(filePath) {
340
+ if (process.platform !== 'win32') {
341
+ try {
342
+ fs.chmodSync(filePath, 0o600);
343
+ }
344
+ catch {
345
+ // Best effort
346
+ }
347
+ }
348
+ }
349
+ // ─── Public API ─────────────────────────────────────────────────────
350
+ /**
351
+ * Persists authentication credentials to the most secure available store.
352
+ *
353
+ * Strategy (tried in order):
354
+ * 1. macOS Keychain (`security` CLI)
355
+ * 2. Linux libsecret (`secret-tool` CLI)
356
+ * 3. Windows DPAPI (PowerShell)
357
+ * 4. AES-256-GCM encrypted file with 0600 permissions
358
+ */
359
+ export async function saveCredentials(data) {
360
+ const current = await loadCredentials();
361
+ const merged = { ...current, ...data };
362
+ const json = JSON.stringify(merged);
363
+ const backend = await detectBackend();
364
+ try {
365
+ switch (backend) {
366
+ case 'keychain-macos':
367
+ await macSave(json);
368
+ break;
369
+ case 'libsecret-linux':
370
+ await linuxSave(json);
371
+ break;
372
+ case 'dpapi-windows':
373
+ await windowsSave(json);
374
+ break;
375
+ case 'encrypted-file':
376
+ await fileSave(json);
377
+ break;
378
+ }
379
+ }
380
+ catch (err) {
381
+ debugLog(`Primary credential save failed (${backend}): ${err.message}. Falling back to encrypted file.`);
382
+ // If the OS-native backend fails, fall back to encrypted file
383
+ if (backend !== 'encrypted-file') {
384
+ await fileSave(json);
385
+ }
386
+ else {
387
+ throw err;
388
+ }
389
+ }
390
+ }
391
+ /**
392
+ * Loads authentication credentials from the secure store.
393
+ * Returns an empty object if no credentials are found.
394
+ *
395
+ * On first run after upgrade, silently migrates any legacy plaintext
396
+ * `~/.minovative-mind-cli.json` into the secure store and deletes the old file.
397
+ */
398
+ export async function loadCredentials() {
399
+ const backend = await detectBackend();
400
+ let json = null;
401
+ try {
402
+ switch (backend) {
403
+ case 'keychain-macos':
404
+ json = await macLoad();
405
+ break;
406
+ case 'libsecret-linux':
407
+ json = await linuxLoad();
408
+ break;
409
+ case 'dpapi-windows':
410
+ json = await windowsLoad();
411
+ break;
412
+ case 'encrypted-file':
413
+ json = await fileLoad();
414
+ break;
415
+ }
416
+ }
417
+ catch (err) {
418
+ debugLog(`Primary credential load failed (${backend}): ${err.message}`);
419
+ }
420
+ // If the primary backend yielded nothing or threw an error, check the fallback file.
421
+ // This handles the case where saveCredentials timed out on the keychain and wrote
422
+ // to the fallback file instead.
423
+ if (!json && backend !== 'encrypted-file') {
424
+ try {
425
+ json = await fileLoad();
426
+ }
427
+ catch (err) {
428
+ debugLog(`Fallback credential load failed: ${err.message}`);
429
+ }
430
+ }
431
+ if (json) {
432
+ try {
433
+ return JSON.parse(json);
434
+ }
435
+ catch {
436
+ debugLog('Failed to parse stored credentials — returning empty');
437
+ return {};
438
+ }
439
+ }
440
+ // No credentials found in secure store — check for legacy plaintext file to migrate
441
+ const migrated = await migrateLegacyCredentials();
442
+ if (migrated)
443
+ return migrated;
444
+ return {};
445
+ }
446
+ /**
447
+ * Removes all stored credentials from the secure store.
448
+ * Also cleans up any legacy plaintext file if it still exists.
449
+ */
450
+ export async function clearCredentials() {
451
+ const backend = await detectBackend();
452
+ try {
453
+ switch (backend) {
454
+ case 'keychain-macos':
455
+ await macClear();
456
+ break;
457
+ case 'libsecret-linux':
458
+ await linuxClear();
459
+ break;
460
+ case 'dpapi-windows':
461
+ await windowsClear();
462
+ break;
463
+ case 'encrypted-file':
464
+ await fileClear();
465
+ break;
466
+ }
467
+ }
468
+ catch (err) {
469
+ debugLog(`Credential clear failed (${backend}): ${err.message}`);
470
+ }
471
+ // Always attempt to clear the fallback file, as a previous saveCredentials
472
+ // might have timed out on the OS keychain and written to the fallback instead.
473
+ if (backend !== 'encrypted-file') {
474
+ await fileClear();
475
+ }
476
+ // Always clean up legacy file on logout
477
+ removeLegacyFile();
478
+ }
479
+ // ─── Migration ──────────────────────────────────────────────────────
480
+ /**
481
+ * Silently migrates credentials from the legacy plaintext
482
+ * `~/.minovative-mind-cli.json` file into the secure store.
483
+ * Deletes the plaintext file after successful migration.
484
+ */
485
+ async function migrateLegacyCredentials() {
486
+ try {
487
+ if (!fs.existsSync(LEGACY_CONFIG_FILE))
488
+ return null;
489
+ const raw = fs.readFileSync(LEGACY_CONFIG_FILE, 'utf-8');
490
+ const data = JSON.parse(raw);
491
+ if (!data.idToken && !data.refreshToken)
492
+ return null;
493
+ // Save to the new secure store
494
+ const json = JSON.stringify(data);
495
+ const backend = await detectBackend();
496
+ try {
497
+ switch (backend) {
498
+ case 'keychain-macos':
499
+ await macSave(json);
500
+ break;
501
+ case 'libsecret-linux':
502
+ await linuxSave(json);
503
+ break;
504
+ case 'dpapi-windows':
505
+ await windowsSave(json);
506
+ break;
507
+ case 'encrypted-file':
508
+ await fileSave(json);
509
+ break;
510
+ }
511
+ }
512
+ catch (err) {
513
+ debugLog(`Primary credential save failed during migration (${backend}): ${err.message}. Falling back to encrypted file.`);
514
+ if (backend !== 'encrypted-file') {
515
+ await fileSave(json);
516
+ }
517
+ else {
518
+ throw err;
519
+ }
520
+ }
521
+ // Remove the legacy plaintext file
522
+ removeLegacyFile();
523
+ debugLog('Successfully migrated legacy plaintext credentials to secure storage');
524
+ return data;
525
+ }
526
+ catch (err) {
527
+ debugLog(`Legacy credential migration failed: ${err.message}`);
528
+ return null;
529
+ }
530
+ }
531
+ function removeLegacyFile() {
532
+ try {
533
+ if (fs.existsSync(LEGACY_CONFIG_FILE)) {
534
+ fs.unlinkSync(LEGACY_CONFIG_FILE);
535
+ }
536
+ }
537
+ catch {
538
+ // Best effort
539
+ }
540
+ }
@@ -60,6 +60,76 @@ export function ensureIgnored(workspaceRoot) {
60
60
  }
61
61
  }
62
62
  }
63
+ ensureMinovativeMindIgnore(workspaceRoot);
64
+ }
65
+ /**
66
+ * Creates a default .minovativemindignore file if it doesn't exist, pre-populated with
67
+ * sensible defaults for files that the AI should ignore but Git might track.
68
+ */
69
+ function ensureMinovativeMindIgnore(workspaceRoot) {
70
+ const filePath = path.join(workspaceRoot, '.minovativemindignore');
71
+ if (!fs.existsSync(filePath)) {
72
+ const template = `# Minovative Mind CLI Ignore File
73
+
74
+ # This file works exactly like .gitignore, but applies ONLY to the Minovative Mind CLI AI agent.
75
+
76
+ # Use it to block the CLI AI agent from reading large/redundant files or any files/folders that you would like the AI agent not to read but still want tracked by Git.
77
+
78
+ # Lockfiles (Huge token wasters, AI should read package.json instead)
79
+ package-lock.json
80
+ yarn.lock
81
+ pnpm-lock.yaml
82
+ poetry.lock
83
+ Cargo.lock
84
+
85
+ # Massive Data Dumps
86
+ *.sql
87
+ *.sqlite
88
+ *.dump
89
+ *.csv
90
+
91
+ # Built/Minified Assets
92
+ *.min.js
93
+ *.bundle.js
94
+ *.map
95
+
96
+ # Media / Binary
97
+ *.png
98
+ *.jpg
99
+ *.jpeg
100
+ *.gif
101
+ *.mp4
102
+ *.mp3
103
+ *.wav
104
+ *.ico
105
+ *.svg
106
+ *.pdf
107
+
108
+ # Archives
109
+ *.zip
110
+ *.tar
111
+ *.tar.gz
112
+ *.rar
113
+ *.7z
114
+
115
+ # Compiled Binaries
116
+ *.exe
117
+ *.dll
118
+ *.so
119
+ *.dylib
120
+ *.class
121
+ *.pyc
122
+
123
+ # Logs
124
+ *.log
125
+ `;
126
+ try {
127
+ fs.writeFileSync(filePath, template, 'utf-8');
128
+ }
129
+ catch (e) {
130
+ // ignore
131
+ }
132
+ }
63
133
  }
64
134
  /**
65
135
  * Reads a JSON file from the project's .minovativemind directory.