gitarsenal-cli 1.9.77 → 1.9.80

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 (26) hide show
  1. package/.venv_status.json +1 -1
  2. package/bin/gitarsenal.js +381 -18
  3. package/digest.txt +11446 -0
  4. package/gitingest-integration.js +274 -0
  5. package/kill_claude/prompts/claude-code-tool-prompts.md +2 -1
  6. package/kill_claude/tools/__pycache__/bash_output_tool.cpython-312.pyc +0 -0
  7. package/kill_claude/tools/__pycache__/bash_tool.cpython-312.pyc +0 -0
  8. package/kill_claude/tools/__pycache__/edit_tool.cpython-312.pyc +0 -0
  9. package/kill_claude/tools/__pycache__/exit_plan_mode_tool.cpython-312.pyc +0 -0
  10. package/kill_claude/tools/__pycache__/glob_tool.cpython-312.pyc +0 -0
  11. package/kill_claude/tools/__pycache__/grep_tool.cpython-312.pyc +0 -0
  12. package/kill_claude/tools/__pycache__/kill_bash_tool.cpython-312.pyc +0 -0
  13. package/kill_claude/tools/__pycache__/ls_tool.cpython-312.pyc +0 -0
  14. package/kill_claude/tools/__pycache__/multiedit_tool.cpython-312.pyc +0 -0
  15. package/kill_claude/tools/__pycache__/notebook_edit_tool.cpython-312.pyc +0 -0
  16. package/kill_claude/tools/__pycache__/read_tool.cpython-312.pyc +0 -0
  17. package/kill_claude/tools/__pycache__/task_tool.cpython-312.pyc +0 -0
  18. package/kill_claude/tools/__pycache__/todo_write_tool.cpython-312.pyc +0 -0
  19. package/kill_claude/tools/__pycache__/web_fetch_tool.cpython-312.pyc +0 -0
  20. package/kill_claude/tools/__pycache__/web_search_tool.cpython-312.pyc +0 -0
  21. package/kill_claude/tools/__pycache__/write_tool.cpython-312.pyc +0 -0
  22. package/package.json +1 -1
  23. package/python/__pycache__/analyze_repo_api_keys.cpython-312.pyc +0 -0
  24. package/python/__pycache__/credentials_manager.cpython-312.pyc +0 -0
  25. package/python/credentials_manager.py +0 -169
  26. package/python/gitarsenal_keys.py +8 -2
package/.venv_status.json CHANGED
@@ -1 +1 @@
1
- {"created":"2025-08-17T08:32:36.269Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
1
+ {"created":"2025-08-18T04:28:55.195Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
package/bin/gitarsenal.js CHANGED
@@ -15,6 +15,7 @@ const { spawn } = require('child_process');
15
15
  const fs = require('fs');
16
16
  const https = require('https');
17
17
  const http = require('http');
18
+ const { fetchGitIngestData } = require('../gitingest-integration');
18
19
 
19
20
  // Function to activate virtual environment
20
21
  function activateVirtualEnvironment() {
@@ -22,7 +23,7 @@ function activateVirtualEnvironment() {
22
23
  const venvPath = path.join(__dirname, '..', '.venv');
23
24
  const statusFile = path.join(__dirname, '..', '.venv_status.json');
24
25
 
25
- // Check if virtual environment exists
26
+ // Check if virtual environment exists
26
27
  if (!fs.existsSync(venvPath)) {
27
28
  console.log(chalk.red('❌ Virtual environment not found. Please reinstall the package:'));
28
29
  console.log(chalk.yellow(' npm uninstall -g gitarsenal-cli'));
@@ -57,13 +58,7 @@ function activateVirtualEnvironment() {
57
58
  const traditionalPipPath = isWindows ?
58
59
  path.join(venvPath, 'Scripts', 'pip.exe') :
59
60
  path.join(venvPath, 'bin', 'pip');
60
-
61
- // Determine which structure exists
62
- // console.log(chalk.gray(`🔍 Checking virtual environment structure:`));
63
- // console.log(chalk.gray(` Python: ${uvPythonPath} (exists: ${fs.existsSync(uvPythonPath)})`));
64
- // console.log(chalk.gray(` Pip: ${uvPipPath} (exists: ${fs.existsSync(uvPipPath)})`));
65
-
66
- // For uv virtual environments, we only need Python to exist
61
+
67
62
  // uv doesn't create a pip executable, it uses 'uv pip' instead
68
63
  if (fs.existsSync(uvPythonPath)) {
69
64
  pythonPath = uvPythonPath;
@@ -261,9 +256,271 @@ function printGpuTorchCudaSummary(result) {
261
256
  if (gpu.notes) console.log(` - Notes: ${gpu.notes}`);
262
257
  console.log();
263
258
  }
259
+
260
+ // Print API key requirements if available
261
+ if (result.requiredApiKeys && Array.isArray(result.requiredApiKeys) && result.requiredApiKeys.length > 0) {
262
+ // Separate critical and optional API keys
263
+ const criticalKeys = result.requiredApiKeys.filter(key => key.priority === 'critical' || key.required);
264
+ const optionalKeys = result.requiredApiKeys.filter(key => key.priority === 'optional' || !key.required);
265
+
266
+ if (criticalKeys.length > 0) {
267
+ console.log(chalk.bold('🔑 CRITICAL API KEYS'));
268
+ criticalKeys.forEach(apiKey => {
269
+ console.log(` - ${apiKey.name} ${chalk.red('(REQUIRED)')}`);
270
+ console.log(` Service: ${apiKey.service}`);
271
+ console.log(` Purpose: ${apiKey.description}`);
272
+ if (apiKey.example) console.log(` Format: ${apiKey.example}`);
273
+ if (apiKey.documentation_url) console.log(` Docs: ${apiKey.documentation_url}`);
274
+ console.log();
275
+ });
276
+ }
277
+
278
+ if (optionalKeys.length > 0) {
279
+ console.log(chalk.bold('🔧 OPTIONAL API KEYS'));
280
+ optionalKeys.forEach(apiKey => {
281
+ console.log(` - ${apiKey.name} ${chalk.yellow('(OPTIONAL)')}`);
282
+ console.log(` Service: ${apiKey.service}`);
283
+ console.log(` Purpose: ${apiKey.description}`);
284
+ if (apiKey.example) console.log(` Format: ${apiKey.example}`);
285
+ if (apiKey.documentation_url) console.log(` Docs: ${apiKey.documentation_url}`);
286
+ console.log();
287
+ });
288
+ }
289
+ }
264
290
  } catch {}
265
291
  }
266
292
 
293
+ // Function to load stored API keys
294
+ async function loadStoredApiKeys() {
295
+ try {
296
+ const scriptPath = path.join(__dirname, '..', 'python', 'gitarsenal_keys.py');
297
+ const pythonExecutable = process.env.PYTHON_EXECUTABLE || 'python';
298
+
299
+ return new Promise((resolve) => {
300
+ const pythonProcess = spawn(pythonExecutable, [
301
+ scriptPath,
302
+ 'list',
303
+ '--json'
304
+ ], { stdio: 'pipe' });
305
+
306
+ let output = '';
307
+ pythonProcess.stdout.on('data', (data) => {
308
+ output += data.toString();
309
+ });
310
+
311
+ pythonProcess.on('close', (code) => {
312
+ if (code === 0) {
313
+ try {
314
+ const keys = JSON.parse(output);
315
+ resolve(keys);
316
+ } catch (e) {
317
+ resolve({});
318
+ }
319
+ } else {
320
+ resolve({});
321
+ }
322
+ });
323
+ });
324
+ } catch (error) {
325
+ return {};
326
+ }
327
+ }
328
+
329
+ // Function to prompt for missing required API keys
330
+ async function promptForMissingApiKeys(requiredApiKeys, storedKeys) {
331
+ if (!requiredApiKeys || !Array.isArray(requiredApiKeys) || requiredApiKeys.length === 0) {
332
+ return {};
333
+ }
334
+
335
+ const missingKeys = {};
336
+
337
+ // Separate critical and optional missing keys
338
+ const criticalMissingKeys = requiredApiKeys.filter(apiKey =>
339
+ (apiKey.priority === 'critical' || apiKey.required) && !storedKeys[apiKey.service.toLowerCase()]
340
+ );
341
+
342
+ const optionalMissingKeys = requiredApiKeys.filter(apiKey =>
343
+ (apiKey.priority === 'optional' || !apiKey.required) && !storedKeys[apiKey.service.toLowerCase()]
344
+ );
345
+
346
+ // Handle critical keys first
347
+ if (criticalMissingKeys.length > 0) {
348
+ console.log(chalk.red('\n🔑 Critical API Keys Required'));
349
+ console.log(chalk.gray('These API keys are required for core functionality:'));
350
+
351
+ for (const apiKey of criticalMissingKeys) {
352
+ console.log(chalk.bold(`\n📝 ${apiKey.name} (${apiKey.service})`));
353
+ console.log(chalk.gray(`Purpose: ${apiKey.description}`));
354
+ if (apiKey.documentation_url) {
355
+ console.log(chalk.blue(`Documentation: ${apiKey.documentation_url}`));
356
+ }
357
+
358
+ const answers = await inquirer.prompt([
359
+ {
360
+ type: 'confirm',
361
+ name: 'provideKey',
362
+ message: `Do you want to provide your ${apiKey.service} API key now?`,
363
+ default: true
364
+ }
365
+ ]);
366
+
367
+ if (answers.provideKey) {
368
+ const keyAnswer = await inquirer.prompt([
369
+ {
370
+ type: 'password',
371
+ name: 'key',
372
+ message: `Enter your ${apiKey.service} API key:`,
373
+ mask: '*',
374
+ validate: (input) => {
375
+ const key = input.trim();
376
+ if (key === '') return `${apiKey.service} API key is required`;
377
+ return true;
378
+ }
379
+ }
380
+ ]);
381
+
382
+ const storeAnswer = await inquirer.prompt([
383
+ {
384
+ type: 'confirm',
385
+ name: 'store',
386
+ message: `Store this API key locally for future use?`,
387
+ default: true
388
+ }
389
+ ]);
390
+
391
+ missingKeys[apiKey.service.toLowerCase()] = keyAnswer.key;
392
+
393
+ if (storeAnswer.store) {
394
+ // Store the key using the existing key management system
395
+ try {
396
+ const scriptPath = path.join(__dirname, '..', 'python', 'gitarsenal_keys.py');
397
+ const pythonExecutable = process.env.PYTHON_EXECUTABLE || 'python';
398
+
399
+ await new Promise((resolve, reject) => {
400
+ const pythonProcess = spawn(pythonExecutable, [
401
+ scriptPath,
402
+ 'add',
403
+ '--service', apiKey.service.toLowerCase(),
404
+ '--key', keyAnswer.key
405
+ ], { stdio: 'pipe' });
406
+
407
+ pythonProcess.on('close', (code) => {
408
+ if (code === 0) {
409
+ console.log(chalk.green(`✅ ${apiKey.service} API key stored successfully`));
410
+ resolve();
411
+ } else {
412
+ console.log(chalk.yellow(`⚠️ Could not store ${apiKey.service} API key locally`));
413
+ resolve(); // Don't fail the whole process
414
+ }
415
+ });
416
+ });
417
+ } catch (error) {
418
+ console.log(chalk.yellow(`⚠️ Could not store ${apiKey.service} API key: ${error.message}`));
419
+ }
420
+ }
421
+ } else {
422
+ console.log(chalk.yellow(`⚠️ Skipping ${apiKey.service} API key. Repository setup may fail without it.`));
423
+ }
424
+ }
425
+ }
426
+
427
+ // Handle optional keys
428
+ if (optionalMissingKeys.length > 0) {
429
+ const setupOptional = await inquirer.prompt([
430
+ {
431
+ type: 'confirm',
432
+ name: 'wantOptional',
433
+ message: `${optionalMissingKeys.length} optional API key(s) available. Set them up for enhanced features?`,
434
+ default: false
435
+ }
436
+ ]);
437
+
438
+ if (setupOptional.wantOptional) {
439
+ console.log(chalk.blue('\n🔧 Optional API Keys Setup'));
440
+ console.log(chalk.gray('These enhance functionality but are not required:'));
441
+
442
+ for (const apiKey of optionalMissingKeys) {
443
+ console.log(chalk.bold(`\n📝 ${apiKey.name} (${apiKey.service}) - Optional`));
444
+ console.log(chalk.gray(`Purpose: ${apiKey.description}`));
445
+ if (apiKey.documentation_url) {
446
+ console.log(chalk.blue(`Documentation: ${apiKey.documentation_url}`));
447
+ }
448
+
449
+ const answers = await inquirer.prompt([
450
+ {
451
+ type: 'confirm',
452
+ name: 'provideKey',
453
+ message: `Provide ${apiKey.service} API key for enhanced features?`,
454
+ default: false
455
+ }
456
+ ]);
457
+
458
+ if (answers.provideKey) {
459
+ const keyAnswer = await inquirer.prompt([
460
+ {
461
+ type: 'password',
462
+ name: 'key',
463
+ message: `Enter your ${apiKey.service} API key:`,
464
+ mask: '*',
465
+ validate: (input) => {
466
+ const key = input.trim();
467
+ if (key === '') return `${apiKey.service} API key cannot be empty`;
468
+ return true;
469
+ }
470
+ }
471
+ ]);
472
+
473
+ const storeAnswer = await inquirer.prompt([
474
+ {
475
+ type: 'confirm',
476
+ name: 'store',
477
+ message: `Store this API key locally for future use?`,
478
+ default: true
479
+ }
480
+ ]);
481
+
482
+ missingKeys[apiKey.service.toLowerCase()] = keyAnswer.key;
483
+
484
+ if (storeAnswer.store) {
485
+ try {
486
+ const scriptPath = path.join(__dirname, '..', 'python', 'gitarsenal_keys.py');
487
+ const pythonExecutable = process.env.PYTHON_EXECUTABLE || 'python';
488
+
489
+ await new Promise((resolve, reject) => {
490
+ const pythonProcess = spawn(pythonExecutable, [
491
+ scriptPath,
492
+ 'add',
493
+ '--service', apiKey.service.toLowerCase(),
494
+ '--key', keyAnswer.key
495
+ ], { stdio: 'pipe' });
496
+
497
+ pythonProcess.on('close', (code) => {
498
+ if (code === 0) {
499
+ console.log(chalk.green(`✅ ${apiKey.service} API key stored successfully`));
500
+ resolve();
501
+ } else {
502
+ console.log(chalk.yellow(`⚠️ Could not store ${apiKey.service} API key locally`));
503
+ resolve();
504
+ }
505
+ });
506
+ });
507
+ } catch (error) {
508
+ console.log(chalk.yellow(`⚠️ Could not store ${apiKey.service} API key: ${error.message}`));
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+ }
515
+
516
+ // Check if all critical keys are handled
517
+ if (criticalMissingKeys.length === 0) {
518
+ console.log(chalk.green('✅ All critical API keys are available'));
519
+ }
520
+
521
+ return missingKeys;
522
+ }
523
+
267
524
  // Helper to derive a default volume name from the repository URL
268
525
  function getDefaultVolumeName(repoUrl) {
269
526
  try {
@@ -321,10 +578,96 @@ function getDefaultVolumeName(repoUrl) {
321
578
  }
322
579
 
323
580
  // Full fetch to get both setup commands and recommendations in one request
324
- async function fetchFullSetupAndRecs(repoUrl) {
325
- // For now, just use the preview function but don't show summary to avoid duplicates
326
- // The Python implementation will handle setup commands
327
- return await previewRecommendations(repoUrl, { showSummary: false, hideSpinner: true });
581
+ async function fetchFullSetupAndRecs(repoUrl, storedCredentials = null) {
582
+ const spinner = ora('Analyzing repository with GitIngest...').start();
583
+
584
+ try {
585
+ // Try to use local GitIngest CLI first
586
+ spinner.text = 'Running GitIngest analysis...';
587
+ const gitingestData = await fetchGitIngestData(repoUrl);
588
+
589
+ let finalGitingestData;
590
+ if (!gitingestData) {
591
+ spinner.warn('GitIngest CLI not available, using basic analysis');
592
+ // Fallback to basic data
593
+ finalGitingestData = {
594
+ system_info: {
595
+ platform: process.platform,
596
+ python_version: process.version,
597
+ detected_language: 'Unknown',
598
+ detected_technologies: [],
599
+ file_count: 0,
600
+ repo_stars: 0,
601
+ repo_forks: 0,
602
+ primary_package_manager: 'Unknown',
603
+ complexity_level: 'Unknown'
604
+ },
605
+ repository_analysis: {
606
+ summary: `Repository: ${repoUrl}`,
607
+ tree: '',
608
+ content_preview: ''
609
+ },
610
+ success: false
611
+ };
612
+ } else {
613
+ finalGitingestData = gitingestData;
614
+ spinner.text = 'GitIngest complete, generating AI recommendations...';
615
+ }
616
+
617
+ const envUrl = process.env.GITARSENAL_API_URL;
618
+ const endpoints = envUrl ? [envUrl] : ['https://www.gitarsenal.dev/api/best_gpu'];
619
+
620
+ const payload = {
621
+ repoUrl,
622
+ gitingestData: finalGitingestData,
623
+ storedCredentials,
624
+ preview: false // This is a full analysis, not preview
625
+ };
626
+
627
+ let data = null;
628
+ let lastErrorText = '';
629
+
630
+ for (const url of endpoints) {
631
+ try {
632
+ spinner.text = `Analyzing repository: ${url}`;
633
+ const res = await fetch(url, {
634
+ method: 'POST',
635
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'GitArsenal-CLI/1.0' },
636
+ body: JSON.stringify(payload),
637
+ redirect: 'follow'
638
+ });
639
+ if (!res.ok) {
640
+ const text = await res.text().catch(() => '');
641
+ lastErrorText = `${res.status} ${text.slice(0, 300)}`;
642
+ continue;
643
+ }
644
+ data = await res.json().catch(() => null);
645
+ if (data) {
646
+ console.log(chalk.gray('🐛 DEBUG: Received response from API:'));
647
+ console.log(chalk.gray(' - Response has commands:', !!data.commands));
648
+ console.log(chalk.gray(' - Commands count:', data.commands ? data.commands.length : 0));
649
+ console.log(chalk.gray(' - Response has API keys:', !!data.requiredApiKeys));
650
+ console.log(chalk.gray(' - API keys count:', data.requiredApiKeys ? data.requiredApiKeys.length : 0));
651
+ console.log(chalk.gray(' - Response has GPU rec:', !!data.gpuRecommendation));
652
+ console.log(chalk.gray(' - Response has CUDA rec:', !!data.cudaRecommendation));
653
+ console.log(chalk.gray(' - Response has Torch rec:', !!data.torchRecommendation));
654
+ spinner.succeed('Repository analysis complete');
655
+ return data;
656
+ }
657
+ } catch (err) {
658
+ lastErrorText = err && err.message ? err.message : 'request failed';
659
+ continue;
660
+ }
661
+ }
662
+
663
+ spinner.fail('Failed to analyze repository');
664
+ if (lastErrorText) console.log(chalk.gray(`Reason: ${lastErrorText}`));
665
+ return null;
666
+
667
+ } catch (e) {
668
+ spinner.fail(`Analysis failed: ${e.message}`);
669
+ return null;
670
+ }
328
671
  }
329
672
 
330
673
  // Function to send user data to web application
@@ -761,8 +1104,15 @@ async function runContainerCommand(options) {
761
1104
  repoUrl = answers.repoUrl;
762
1105
  }
763
1106
 
764
- // Analyze repository for GPU recommendations (repository setup is now handled by Agent)
1107
+ // Analyze repository for GPU recommendations and API key requirements
1108
+ let analysisData = null;
1109
+ let collectedApiKeys = {};
1110
+
765
1111
  if (repoUrl) {
1112
+ // Load stored API keys first
1113
+ console.log(chalk.blue('🔍 Loading stored API keys...'));
1114
+ const storedKeys = await loadStoredApiKeys();
1115
+
766
1116
  // Start a main spinner that will show overall progress
767
1117
  const mainSpinner = ora('Analyzing repository...').start();
768
1118
 
@@ -773,16 +1123,24 @@ async function runContainerCommand(options) {
773
1123
  mainSpinner.text = 'Analyzing repository for GPU/Torch/CUDA recommendations...';
774
1124
  const previewPromise = previewRecommendations(repoUrl, { showSummary: false, abortSignal: previewAbort.signal, hideSpinner: true }).catch(() => null);
775
1125
 
776
- // Run full fetch in parallel; prefer its results if available.
777
- mainSpinner.text = 'Finding the best machine for your code...';
778
- const fullData = await fetchFullSetupAndRecs(repoUrl).catch(() => null);
1126
+ // Run full fetch in parallel with stored credentials; prefer its results if available.
1127
+ mainSpinner.text = 'Finding the best machine for your code and detecting API requirements...';
1128
+ const fullData = await fetchFullSetupAndRecs(repoUrl, storedKeys).catch(() => null);
779
1129
 
780
1130
  if (fullData) {
781
1131
  // Stop preview spinner immediately since we have a response
782
1132
  previewAbort.abort();
783
1133
  mainSpinner.succeed('Analysis complete!');
784
1134
  printGpuTorchCudaSummary(fullData);
785
- // Repository setup will be handled by Agent in container
1135
+ analysisData = fullData;
1136
+
1137
+ // Handle API key requirements
1138
+ if (fullData.requiredApiKeys && Array.isArray(fullData.requiredApiKeys) && fullData.requiredApiKeys.length > 0) {
1139
+ const missingKeys = await promptForMissingApiKeys(fullData.requiredApiKeys, storedKeys);
1140
+ collectedApiKeys = { ...storedKeys, ...missingKeys };
1141
+ } else {
1142
+ collectedApiKeys = storedKeys;
1143
+ }
786
1144
  } else {
787
1145
  // Full fetch failed, wait for preview and show its results
788
1146
  mainSpinner.text = 'Waiting for preview analysis to complete...';
@@ -790,16 +1148,19 @@ async function runContainerCommand(options) {
790
1148
  if (previewData) {
791
1149
  mainSpinner.succeed('Preview analysis complete!');
792
1150
  printGpuTorchCudaSummary(previewData);
1151
+ analysisData = previewData;
793
1152
  } else {
794
1153
  mainSpinner.fail('Analysis failed - both preview and full analysis timed out or failed');
795
1154
  console.log(chalk.yellow('⚠️ Unable to analyze repository automatically.'));
796
1155
  console.log(chalk.gray('Repository setup will still be handled by Agent in container.'));
797
1156
  }
1157
+ collectedApiKeys = storedKeys;
798
1158
  }
799
1159
  } catch (error) {
800
1160
  mainSpinner.fail(`Analysis failed: ${error.message}`);
801
1161
  console.log(chalk.yellow('⚠️ Unable to analyze repository automatically.'));
802
1162
  console.log(chalk.gray('Repository setup will still be handled by Agent in container.'));
1163
+ collectedApiKeys = await loadStoredApiKeys();
803
1164
  }
804
1165
  }
805
1166
 
@@ -934,7 +1295,9 @@ async function runContainerCommand(options) {
934
1295
  yes: skipConfirmation,
935
1296
  userId,
936
1297
  userName,
937
- userEmail
1298
+ userEmail,
1299
+ apiKeys: collectedApiKeys,
1300
+ analysisData
938
1301
  });
939
1302
 
940
1303
  } catch (containerError) {