gitarsenal-cli 1.9.77 → 1.9.78

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 (25) hide show
  1. package/.venv_status.json +1 -1
  2. package/bin/gitarsenal.js +276 -18
  3. package/gitingest-integration.js +274 -0
  4. package/kill_claude/prompts/claude-code-tool-prompts.md +2 -1
  5. package/kill_claude/tools/__pycache__/bash_output_tool.cpython-312.pyc +0 -0
  6. package/kill_claude/tools/__pycache__/bash_tool.cpython-312.pyc +0 -0
  7. package/kill_claude/tools/__pycache__/edit_tool.cpython-312.pyc +0 -0
  8. package/kill_claude/tools/__pycache__/exit_plan_mode_tool.cpython-312.pyc +0 -0
  9. package/kill_claude/tools/__pycache__/glob_tool.cpython-312.pyc +0 -0
  10. package/kill_claude/tools/__pycache__/grep_tool.cpython-312.pyc +0 -0
  11. package/kill_claude/tools/__pycache__/kill_bash_tool.cpython-312.pyc +0 -0
  12. package/kill_claude/tools/__pycache__/ls_tool.cpython-312.pyc +0 -0
  13. package/kill_claude/tools/__pycache__/multiedit_tool.cpython-312.pyc +0 -0
  14. package/kill_claude/tools/__pycache__/notebook_edit_tool.cpython-312.pyc +0 -0
  15. package/kill_claude/tools/__pycache__/read_tool.cpython-312.pyc +0 -0
  16. package/kill_claude/tools/__pycache__/task_tool.cpython-312.pyc +0 -0
  17. package/kill_claude/tools/__pycache__/todo_write_tool.cpython-312.pyc +0 -0
  18. package/kill_claude/tools/__pycache__/web_fetch_tool.cpython-312.pyc +0 -0
  19. package/kill_claude/tools/__pycache__/web_search_tool.cpython-312.pyc +0 -0
  20. package/kill_claude/tools/__pycache__/write_tool.cpython-312.pyc +0 -0
  21. package/package.json +1 -1
  22. package/python/__pycache__/analyze_repo_api_keys.cpython-312.pyc +0 -0
  23. package/python/__pycache__/credentials_manager.cpython-312.pyc +0 -0
  24. package/python/credentials_manager.py +0 -169
  25. 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-17T17:20:43.416Z","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,156 @@ 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
+ console.log(chalk.bold('🔑 REQUIRED API KEYS'));
263
+ result.requiredApiKeys.forEach(apiKey => {
264
+ const status = apiKey.required ? chalk.red('REQUIRED') : chalk.yellow('OPTIONAL');
265
+ console.log(` - ${apiKey.name} (${status})`);
266
+ console.log(` Service: ${apiKey.service}`);
267
+ console.log(` Purpose: ${apiKey.description}`);
268
+ if (apiKey.example) console.log(` Format: ${apiKey.example}`);
269
+ if (apiKey.documentation_url) console.log(` Docs: ${apiKey.documentation_url}`);
270
+ console.log();
271
+ });
272
+ }
264
273
  } catch {}
265
274
  }
266
275
 
276
+ // Function to load stored API keys
277
+ async function loadStoredApiKeys() {
278
+ try {
279
+ const scriptPath = path.join(__dirname, '..', 'python', 'gitarsenal_keys.py');
280
+ const pythonExecutable = process.env.PYTHON_EXECUTABLE || 'python';
281
+
282
+ return new Promise((resolve) => {
283
+ const pythonProcess = spawn(pythonExecutable, [
284
+ scriptPath,
285
+ 'list',
286
+ '--json'
287
+ ], { stdio: 'pipe' });
288
+
289
+ let output = '';
290
+ pythonProcess.stdout.on('data', (data) => {
291
+ output += data.toString();
292
+ });
293
+
294
+ pythonProcess.on('close', (code) => {
295
+ if (code === 0) {
296
+ try {
297
+ const keys = JSON.parse(output);
298
+ resolve(keys);
299
+ } catch (e) {
300
+ resolve({});
301
+ }
302
+ } else {
303
+ resolve({});
304
+ }
305
+ });
306
+ });
307
+ } catch (error) {
308
+ return {};
309
+ }
310
+ }
311
+
312
+ // Function to prompt for missing required API keys
313
+ async function promptForMissingApiKeys(requiredApiKeys, storedKeys) {
314
+ if (!requiredApiKeys || !Array.isArray(requiredApiKeys) || requiredApiKeys.length === 0) {
315
+ return {};
316
+ }
317
+
318
+ const missingKeys = {};
319
+ const requiredMissingKeys = requiredApiKeys.filter(apiKey =>
320
+ apiKey.required && !storedKeys[apiKey.service.toLowerCase()]
321
+ );
322
+
323
+ if (requiredMissingKeys.length === 0) {
324
+ console.log(chalk.green('✅ All required API keys are already stored'));
325
+ return {};
326
+ }
327
+
328
+ console.log(chalk.yellow('\n🔑 Missing Required API Keys'));
329
+ console.log(chalk.gray('The following API keys are required for this repository:'));
330
+
331
+ for (const apiKey of requiredMissingKeys) {
332
+ console.log(chalk.bold(`\n📝 ${apiKey.name} (${apiKey.service})`));
333
+ console.log(chalk.gray(`Purpose: ${apiKey.description}`));
334
+ if (apiKey.documentation_url) {
335
+ console.log(chalk.blue(`Documentation: ${apiKey.documentation_url}`));
336
+ }
337
+
338
+ const answers = await inquirer.prompt([
339
+ {
340
+ type: 'confirm',
341
+ name: 'provideKey',
342
+ message: `Do you want to provide your ${apiKey.service} API key now?`,
343
+ default: true
344
+ }
345
+ ]);
346
+
347
+ if (answers.provideKey) {
348
+ const keyAnswer = await inquirer.prompt([
349
+ {
350
+ type: 'password',
351
+ name: 'key',
352
+ message: `Enter your ${apiKey.service} API key:`,
353
+ mask: '*',
354
+ validate: (input) => {
355
+ const key = input.trim();
356
+ if (key === '') return `${apiKey.service} API key is required`;
357
+ return true;
358
+ }
359
+ }
360
+ ]);
361
+
362
+ const storeAnswer = await inquirer.prompt([
363
+ {
364
+ type: 'confirm',
365
+ name: 'store',
366
+ message: `Store this API key locally for future use?`,
367
+ default: true
368
+ }
369
+ ]);
370
+
371
+ missingKeys[apiKey.service.toLowerCase()] = keyAnswer.key;
372
+
373
+ if (storeAnswer.store) {
374
+ // Store the key using the existing key management system
375
+ try {
376
+ const scriptPath = path.join(__dirname, '..', 'python', 'gitarsenal_keys.py');
377
+ const pythonExecutable = process.env.PYTHON_EXECUTABLE || 'python';
378
+
379
+ await new Promise((resolve, reject) => {
380
+ const pythonProcess = spawn(pythonExecutable, [
381
+ scriptPath,
382
+ 'add',
383
+ '--service', apiKey.service.toLowerCase(),
384
+ '--key', keyAnswer.key
385
+ ], { stdio: 'pipe' });
386
+
387
+ pythonProcess.on('close', (code) => {
388
+ if (code === 0) {
389
+ console.log(chalk.green(`✅ ${apiKey.service} API key stored successfully`));
390
+ resolve();
391
+ } else {
392
+ console.log(chalk.yellow(`⚠️ Could not store ${apiKey.service} API key locally`));
393
+ resolve(); // Don't fail the whole process
394
+ }
395
+ });
396
+ });
397
+ } catch (error) {
398
+ console.log(chalk.yellow(`⚠️ Could not store ${apiKey.service} API key: ${error.message}`));
399
+ }
400
+ }
401
+ } else {
402
+ console.log(chalk.yellow(`⚠️ Skipping ${apiKey.service} API key. Repository setup may fail without it.`));
403
+ }
404
+ }
405
+
406
+ return missingKeys;
407
+ }
408
+
267
409
  // Helper to derive a default volume name from the repository URL
268
410
  function getDefaultVolumeName(repoUrl) {
269
411
  try {
@@ -321,10 +463,106 @@ function getDefaultVolumeName(repoUrl) {
321
463
  }
322
464
 
323
465
  // 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 });
466
+ async function fetchFullSetupAndRecs(repoUrl, storedCredentials = null) {
467
+ const spinner = ora('Analyzing repository with GitIngest...').start();
468
+
469
+ try {
470
+ // Try to use local GitIngest CLI first
471
+ spinner.text = 'Running GitIngest analysis...';
472
+ const gitingestData = await fetchGitIngestData(repoUrl);
473
+
474
+ let finalGitingestData;
475
+ if (!gitingestData) {
476
+ spinner.warn('GitIngest CLI not available, using basic analysis');
477
+ // Fallback to basic data
478
+ finalGitingestData = {
479
+ system_info: {
480
+ platform: process.platform,
481
+ python_version: process.version,
482
+ detected_language: 'Unknown',
483
+ detected_technologies: [],
484
+ file_count: 0,
485
+ repo_stars: 0,
486
+ repo_forks: 0,
487
+ primary_package_manager: 'Unknown',
488
+ complexity_level: 'Unknown'
489
+ },
490
+ repository_analysis: {
491
+ summary: `Repository: ${repoUrl}`,
492
+ tree: '',
493
+ content_preview: ''
494
+ },
495
+ success: false
496
+ };
497
+ } else {
498
+ finalGitingestData = gitingestData;
499
+ spinner.text = 'GitIngest complete, generating AI recommendations...';
500
+ }
501
+
502
+ const envUrl = process.env.GITARSENAL_API_URL;
503
+ const endpoints = envUrl ? [envUrl] : ['https://www.gitarsenal.dev/api/best_gpu'];
504
+
505
+ const payload = {
506
+ repoUrl,
507
+ gitingestData: finalGitingestData,
508
+ storedCredentials,
509
+ preview: false // This is a full analysis, not preview
510
+ };
511
+
512
+ // console.log(chalk.gray('🐛 DEBUG: Payload being sent to API:'));
513
+ // console.log(chalk.gray(' - Repo URL:', repoUrl));
514
+ // console.log(chalk.gray(' - GitIngest Data Summary Length:', payload.gitingestData.repository_analysis.summary.length));
515
+ // console.log(chalk.gray(' - GitIngest Data Tree Length:', payload.gitingestData.repository_analysis.tree.length));
516
+ // console.log(chalk.gray(' - GitIngest Data Content Length:', payload.gitingestData.repository_analysis.content_preview.length));
517
+ // console.log(chalk.gray(' - Detected Language:', payload.gitingestData.system_info.detected_language));
518
+ // console.log(chalk.gray(' - Detected Technologies:', payload.gitingestData.system_info.detected_technologies.join(', ')));
519
+ // console.log(chalk.gray(' - Stored Credentials:', storedCredentials ? Object.keys(storedCredentials).length + ' keys' : 'none'));
520
+ // console.log(chalk.gray(' - Preview Mode:', payload.preview));
521
+
522
+ let data = null;
523
+ let lastErrorText = '';
524
+
525
+ for (const url of endpoints) {
526
+ try {
527
+ spinner.text = `Analyzing repository: ${url}`;
528
+ const res = await fetch(url, {
529
+ method: 'POST',
530
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'GitArsenal-CLI/1.0' },
531
+ body: JSON.stringify(payload),
532
+ redirect: 'follow'
533
+ });
534
+ if (!res.ok) {
535
+ const text = await res.text().catch(() => '');
536
+ lastErrorText = `${res.status} ${text.slice(0, 300)}`;
537
+ continue;
538
+ }
539
+ data = await res.json().catch(() => null);
540
+ if (data) {
541
+ console.log(chalk.gray('🐛 DEBUG: Received response from API:'));
542
+ console.log(chalk.gray(' - Response has commands:', !!data.commands));
543
+ console.log(chalk.gray(' - Commands count:', data.commands ? data.commands.length : 0));
544
+ console.log(chalk.gray(' - Response has API keys:', !!data.requiredApiKeys));
545
+ console.log(chalk.gray(' - API keys count:', data.requiredApiKeys ? data.requiredApiKeys.length : 0));
546
+ console.log(chalk.gray(' - Response has GPU rec:', !!data.gpuRecommendation));
547
+ console.log(chalk.gray(' - Response has CUDA rec:', !!data.cudaRecommendation));
548
+ console.log(chalk.gray(' - Response has Torch rec:', !!data.torchRecommendation));
549
+ spinner.succeed('Repository analysis complete');
550
+ return data;
551
+ }
552
+ } catch (err) {
553
+ lastErrorText = err && err.message ? err.message : 'request failed';
554
+ continue;
555
+ }
556
+ }
557
+
558
+ spinner.fail('Failed to analyze repository');
559
+ if (lastErrorText) console.log(chalk.gray(`Reason: ${lastErrorText}`));
560
+ return null;
561
+
562
+ } catch (e) {
563
+ spinner.fail(`Analysis failed: ${e.message}`);
564
+ return null;
565
+ }
328
566
  }
329
567
 
330
568
  // Function to send user data to web application
@@ -761,8 +999,15 @@ async function runContainerCommand(options) {
761
999
  repoUrl = answers.repoUrl;
762
1000
  }
763
1001
 
764
- // Analyze repository for GPU recommendations (repository setup is now handled by Agent)
1002
+ // Analyze repository for GPU recommendations and API key requirements
1003
+ let analysisData = null;
1004
+ let collectedApiKeys = {};
1005
+
765
1006
  if (repoUrl) {
1007
+ // Load stored API keys first
1008
+ console.log(chalk.blue('🔍 Loading stored API keys...'));
1009
+ const storedKeys = await loadStoredApiKeys();
1010
+
766
1011
  // Start a main spinner that will show overall progress
767
1012
  const mainSpinner = ora('Analyzing repository...').start();
768
1013
 
@@ -773,16 +1018,24 @@ async function runContainerCommand(options) {
773
1018
  mainSpinner.text = 'Analyzing repository for GPU/Torch/CUDA recommendations...';
774
1019
  const previewPromise = previewRecommendations(repoUrl, { showSummary: false, abortSignal: previewAbort.signal, hideSpinner: true }).catch(() => null);
775
1020
 
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);
1021
+ // Run full fetch in parallel with stored credentials; prefer its results if available.
1022
+ mainSpinner.text = 'Finding the best machine for your code and detecting API requirements...';
1023
+ const fullData = await fetchFullSetupAndRecs(repoUrl, storedKeys).catch(() => null);
779
1024
 
780
1025
  if (fullData) {
781
1026
  // Stop preview spinner immediately since we have a response
782
1027
  previewAbort.abort();
783
1028
  mainSpinner.succeed('Analysis complete!');
784
1029
  printGpuTorchCudaSummary(fullData);
785
- // Repository setup will be handled by Agent in container
1030
+ analysisData = fullData;
1031
+
1032
+ // Handle API key requirements
1033
+ if (fullData.requiredApiKeys && Array.isArray(fullData.requiredApiKeys) && fullData.requiredApiKeys.length > 0) {
1034
+ const missingKeys = await promptForMissingApiKeys(fullData.requiredApiKeys, storedKeys);
1035
+ collectedApiKeys = { ...storedKeys, ...missingKeys };
1036
+ } else {
1037
+ collectedApiKeys = storedKeys;
1038
+ }
786
1039
  } else {
787
1040
  // Full fetch failed, wait for preview and show its results
788
1041
  mainSpinner.text = 'Waiting for preview analysis to complete...';
@@ -790,16 +1043,19 @@ async function runContainerCommand(options) {
790
1043
  if (previewData) {
791
1044
  mainSpinner.succeed('Preview analysis complete!');
792
1045
  printGpuTorchCudaSummary(previewData);
1046
+ analysisData = previewData;
793
1047
  } else {
794
1048
  mainSpinner.fail('Analysis failed - both preview and full analysis timed out or failed');
795
1049
  console.log(chalk.yellow('⚠️ Unable to analyze repository automatically.'));
796
1050
  console.log(chalk.gray('Repository setup will still be handled by Agent in container.'));
797
1051
  }
1052
+ collectedApiKeys = storedKeys;
798
1053
  }
799
1054
  } catch (error) {
800
1055
  mainSpinner.fail(`Analysis failed: ${error.message}`);
801
1056
  console.log(chalk.yellow('⚠️ Unable to analyze repository automatically.'));
802
1057
  console.log(chalk.gray('Repository setup will still be handled by Agent in container.'));
1058
+ collectedApiKeys = await loadStoredApiKeys();
803
1059
  }
804
1060
  }
805
1061
 
@@ -934,7 +1190,9 @@ async function runContainerCommand(options) {
934
1190
  yes: skipConfirmation,
935
1191
  userId,
936
1192
  userName,
937
- userEmail
1193
+ userEmail,
1194
+ apiKeys: collectedApiKeys,
1195
+ analysisData
938
1196
  });
939
1197
 
940
1198
  } catch (containerError) {
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const chalk = require('chalk');
5
+
6
+ // Function to check if GitIngest CLI is available and working
7
+ async function checkGitIngestCLI() {
8
+ try {
9
+ // Try a simple help command first
10
+ const checkProcess = spawn('gitingest', ['--version'], {
11
+ stdio: 'pipe',
12
+ timeout: 5000 // 5 second timeout
13
+ });
14
+
15
+ let stderr = '';
16
+ checkProcess.stderr.on('data', (data) => {
17
+ stderr += data.toString();
18
+ });
19
+
20
+ return new Promise((resolve) => {
21
+ checkProcess.on('close', (code) => {
22
+ // If there are Python errors in stderr, consider it failed even if exit code is 0
23
+ if (stderr.includes('TypeError') || stderr.includes('Traceback') || stderr.includes('Error')) {
24
+ // console.log(chalk.yellow('⚠️ GitIngest CLI has Python compatibility issues'));
25
+ resolve(false);
26
+ } else if (code === 0) {
27
+ resolve(true);
28
+ } else {
29
+ resolve(false);
30
+ }
31
+ });
32
+
33
+ checkProcess.on('error', () => {
34
+ resolve(false);
35
+ });
36
+
37
+ // Handle timeout
38
+ setTimeout(() => {
39
+ if (!checkProcess.killed) {
40
+ checkProcess.kill();
41
+ resolve(false);
42
+ }
43
+ }, 5000);
44
+ });
45
+ } catch (error) {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ // Function to fetch GitIngest data using local GitIngest CLI
51
+ async function fetchGitIngestData(repoUrl) {
52
+ try {
53
+ // First check if GitIngest CLI is available
54
+ const gitingestAvailable = await checkGitIngestCLI();
55
+ if (!gitingestAvailable) {
56
+ // console.log(chalk.yellow('⚠️ GitIngest CLI not available or has compatibility issues.'));
57
+ // console.log(chalk.blue('💡 For best results, install with: pipx install gitingest'));
58
+ // console.log(chalk.blue(' Alternative: pip install gitingest (requires Python 3.10+)'));
59
+ // console.log(chalk.blue('📖 More info: https://github.com/coderamp-labs/gitingest'));
60
+ // console.log(chalk.gray(' Falling back to basic repository analysis...'));
61
+ return null;
62
+ }
63
+
64
+ console.log(chalk.gray('📥 Running GitIngest locally...'));
65
+
66
+ // Run GitIngest CLI command with optimal settings for AI analysis
67
+ const gitingestProcess = spawn('gitingest', [
68
+ repoUrl,
69
+ '-o', '-', // Output to stdout
70
+ ], {
71
+ stdio: ['pipe', 'pipe', 'pipe']
72
+ });
73
+
74
+ let gitingestOutput = '';
75
+ let errorOutput = '';
76
+
77
+ gitingestProcess.stdout.on('data', (data) => {
78
+ gitingestOutput += data.toString();
79
+ });
80
+
81
+ gitingestProcess.stderr.on('data', (data) => {
82
+ errorOutput += data.toString();
83
+ });
84
+
85
+ return new Promise((resolve) => {
86
+ gitingestProcess.on('close', (code) => {
87
+ if (code === 0 && gitingestOutput.trim().length > 0) {
88
+ console.log(chalk.green('✅ GitIngest analysis complete'));
89
+ console.log(chalk.gray(`📊 Captured ${gitingestOutput.length} characters of repository content`));
90
+ resolve(parseGitIngestOutput(gitingestOutput, repoUrl));
91
+ } else {
92
+ console.log(chalk.yellow(`⚠️ GitIngest failed (exit code: ${code})`));
93
+ if (errorOutput) {
94
+ console.log(chalk.gray(`Error details: ${errorOutput.slice(0, 300)}`));
95
+ }
96
+ resolve(null);
97
+ }
98
+ });
99
+
100
+ gitingestProcess.on('error', (error) => {
101
+ console.log(chalk.yellow(`⚠️ GitIngest CLI error: ${error.message}`));
102
+ resolve(null);
103
+ });
104
+ });
105
+
106
+ } catch (error) {
107
+ console.log(chalk.yellow(`⚠️ GitIngest execution failed: ${error.message}`));
108
+ return null;
109
+ }
110
+ }
111
+
112
+ // Function to parse GitIngest text output into structured data
113
+ function parseGitIngestOutput(gitingestText, repoUrl) {
114
+ try {
115
+ // Extract repository info from URL
116
+ const urlMatch = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
117
+ const owner = urlMatch ? urlMatch[1] : 'unknown';
118
+ const repo = urlMatch ? urlMatch[2].replace('.git', '') : 'unknown';
119
+
120
+ // GitIngest output format:
121
+ // Repository: owner/repo-name
122
+ // Files analyzed: 42
123
+ // Estimated tokens: 15.2k
124
+ //
125
+ // Directory structure:
126
+ // └── project-name/
127
+ // ├── src/
128
+ // │ ├── main.py
129
+ // └── README.md
130
+ //
131
+ // ================================================
132
+ // FILE: src/main.py
133
+ // ================================================
134
+ // [file content]
135
+
136
+ const lines = gitingestText.split('\n');
137
+ let summary = '';
138
+ let tree = '';
139
+ let content_preview = '';
140
+ let detectedLanguage = 'Unknown';
141
+ let detectedTechnologies = [];
142
+ let primaryPackageManager = 'Unknown';
143
+
144
+ // Find sections
145
+ let summaryEnd = -1;
146
+ let treeStart = -1;
147
+ let treeEnd = -1;
148
+ let contentStart = -1;
149
+
150
+ for (let i = 0; i < lines.length; i++) {
151
+ const line = lines[i];
152
+
153
+ if (line.startsWith('Repository:') && summaryEnd === -1) {
154
+ // Find end of summary (first empty line after Repository line)
155
+ for (let j = i; j < lines.length; j++) {
156
+ if (lines[j].trim() === '' && j > i) {
157
+ summaryEnd = j;
158
+ break;
159
+ }
160
+ }
161
+ }
162
+
163
+ if (line.startsWith('Directory structure:')) {
164
+ treeStart = i;
165
+ }
166
+
167
+ if (line.includes('===') && line.includes('FILE:')) {
168
+ if (treeStart > -1 && treeEnd === -1) {
169
+ treeEnd = i;
170
+ }
171
+ if (contentStart === -1) {
172
+ contentStart = i;
173
+ }
174
+ }
175
+ }
176
+
177
+ // Extract sections
178
+ if (summaryEnd > 0) {
179
+ summary = lines.slice(0, summaryEnd).join('\n');
180
+ } else {
181
+ // Fallback: take first 10 lines as summary
182
+ summary = lines.slice(0, 10).join('\n');
183
+ }
184
+
185
+ if (treeStart > -1) {
186
+ const endIdx = treeEnd > -1 ? treeEnd : (contentStart > -1 ? contentStart : Math.min(treeStart + 50, lines.length));
187
+ tree = lines.slice(treeStart, endIdx).join('\n');
188
+ }
189
+
190
+ if (contentStart > -1) {
191
+ // Take first 300 lines of content to provide good context without overwhelming
192
+ content_preview = lines.slice(contentStart, Math.min(contentStart + 300, lines.length)).join('\n');
193
+ }
194
+
195
+ // Detect technologies from content
196
+ const contentLower = gitingestText.toLowerCase();
197
+
198
+ // Language detection
199
+ if (contentLower.includes('import torch') || contentLower.includes('pytorch') || contentLower.includes('def ') || contentLower.includes('import ')) {
200
+ detectedLanguage = 'Python';
201
+ primaryPackageManager = 'pip';
202
+ } else if (contentLower.includes('package.json') || contentLower.includes('require(') || contentLower.includes('import ') || contentLower.includes('function ')) {
203
+ detectedLanguage = 'JavaScript';
204
+ primaryPackageManager = 'npm';
205
+ } else if (contentLower.includes('cargo.toml') || contentLower.includes('fn ') || contentLower.includes('use ')) {
206
+ detectedLanguage = 'Rust';
207
+ primaryPackageManager = 'cargo';
208
+ } else if (contentLower.includes('go.mod') || contentLower.includes('func ') || contentLower.includes('package ')) {
209
+ detectedLanguage = 'Go';
210
+ primaryPackageManager = 'go mod';
211
+ }
212
+
213
+ // AI/ML Technology detection
214
+ if (contentLower.includes('torch') || contentLower.includes('pytorch')) {
215
+ detectedTechnologies.push('PyTorch');
216
+ }
217
+ if (contentLower.includes('tensorflow') || contentLower.includes('tf.')) {
218
+ detectedTechnologies.push('TensorFlow');
219
+ }
220
+ if (contentLower.includes('transformers') || contentLower.includes('huggingface')) {
221
+ detectedTechnologies.push('Hugging Face');
222
+ }
223
+ if (contentLower.includes('numpy') || contentLower.includes('np.')) {
224
+ detectedTechnologies.push('NumPy');
225
+ }
226
+ if (contentLower.includes('openai') && (contentLower.includes('import openai') || contentLower.includes('openai.')) && !contentLower.includes('# example') && !contentLower.includes('# TODO')) {
227
+ detectedTechnologies.push('OpenAI API');
228
+ }
229
+ if (contentLower.includes('anthropic') && contentLower.includes('import anthropic')) {
230
+ detectedTechnologies.push('Anthropic API');
231
+ }
232
+
233
+ // Count files from summary
234
+ const filesMatch = summary.match(/Files analyzed: (\d+)/);
235
+ const fileCount = filesMatch ? parseInt(filesMatch[1]) : 0;
236
+
237
+ const structuredData = {
238
+ system_info: {
239
+ platform: process.platform,
240
+ python_version: process.version,
241
+ detected_language: detectedLanguage,
242
+ detected_technologies: detectedTechnologies,
243
+ file_count: fileCount,
244
+ repo_stars: 0, // Would need GitHub API
245
+ repo_forks: 0, // Would need GitHub API
246
+ primary_package_manager: primaryPackageManager,
247
+ complexity_level: fileCount > 50 ? 'high' : fileCount > 20 ? 'medium' : 'low'
248
+ },
249
+ repository_analysis: {
250
+ summary: summary || `Repository: ${owner}/${repo}\nAnalyzed with GitIngest`,
251
+ tree: tree || 'Directory structure not available',
252
+ content_preview: content_preview || 'Content preview not available'
253
+ },
254
+ success: true
255
+ };
256
+
257
+ console.log(chalk.gray('🔍 Analysis Summary:'));
258
+ console.log(chalk.gray(` - Language: ${detectedLanguage}`));
259
+ console.log(chalk.gray(` - Technologies: ${detectedTechnologies.join(', ') || 'None detected'}`));
260
+ console.log(chalk.gray(` - Files: ${fileCount}`));
261
+ console.log(chalk.gray(` - Package Manager: ${primaryPackageManager}`));
262
+
263
+ return structuredData;
264
+
265
+ } catch (error) {
266
+ console.log(chalk.yellow(`⚠️ Failed to parse GitIngest output: ${error.message}`));
267
+ return null;
268
+ }
269
+ }
270
+
271
+ module.exports = {
272
+ fetchGitIngestData,
273
+ checkGitIngestCLI
274
+ };
@@ -44,7 +44,8 @@ Before executing the command, please follow these steps:
44
44
  - cd /Users/name/My Documents (incorrect - will fail)
45
45
  - python "/path/with spaces/script.py" (correct)
46
46
  - python /path/with spaces/script.py (incorrect - will fail)
47
- - After ensuring proper quoting, execute the command.
47
+ - **PLACEHOLDER HANDLING**: When executing commands that contain placeholder values (e.g., `<filename>`, `{variable}`, `[value]`), you MUST first use the Task tool with a general-purpose agent to read the relevant files and gather the necessary information to fill in the placeholder values before executing the command.
48
+ - After ensuring proper quoting and placeholder resolution, execute the command.
48
49
  - Capture the output of the command.
49
50
 
50
51
  Usage notes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.9.77",
3
+ "version": "1.9.78",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -113,176 +113,7 @@ class CredentialsManager:
113
113
  except Exception as e:
114
114
  print(f"❌ Error getting input: {e}")
115
115
  return None
116
-
117
- def get_openai_api_key(self):
118
- """Get OpenAI API key with validation - for user's repository execution"""
119
- def validate_openai_key(key):
120
- # Basic validation - OpenAI keys usually start with "sk-" and are 51 chars
121
- return key.startswith("sk-") and len(key) > 40
122
-
123
- # First check stored credentials (user's key)
124
- credentials = self.load_credentials()
125
- if "openai_api_key" in credentials:
126
- stored_key = credentials["openai_api_key"]
127
- if validate_openai_key(stored_key):
128
- return stored_key
129
-
130
- # Then check environment variable
131
- env_key = os.environ.get("OPENAI_API_KEY")
132
- if env_key and validate_openai_key(env_key):
133
- return env_key
134
-
135
- # For user's repository execution, prompt if no key found
136
- prompt = "An OpenAI API key is needed to run this repository.\nYou can get your API key from: https://platform.openai.com/api-keys"
137
- return self.get_credential("openai_api_key", prompt, is_password=True, validate_func=validate_openai_key)
138
-
139
- def get_gitarsenal_openai_api_key(self):
140
- """Get GitArsenal's OpenAI API key for debugging - never prompts user"""
141
- def validate_openai_key(key):
142
- # Basic validation - OpenAI keys usually start with "sk-" and are 51 chars
143
- return key.startswith("sk-") and len(key) > 40
144
-
145
- # First try to fetch from server using fetch_modal_tokens (GitArsenal's key)
146
- try:
147
- from fetch_modal_tokens import get_tokens
148
- _, _, api_key, _, _ = get_tokens()
149
- if api_key and validate_openai_key(api_key):
150
- # Set in environment for future use
151
- os.environ["OPENAI_API_KEY"] = api_key
152
- return api_key
153
- except ImportError:
154
- pass
155
- except Exception as e:
156
- print(f"⚠️ Error fetching GitArsenal API key from server: {e}")
157
-
158
- # Then check environment variable (for development/testing)
159
- env_key = os.environ.get("GITARSENAL_OPENAI_API_KEY")
160
- if env_key and validate_openai_key(env_key):
161
- return env_key
162
-
163
- # Check for GitArsenal's key in credentials (for development)
164
- credentials = self.load_credentials()
165
- if "gitarsenal_openai_api_key" in credentials:
166
- stored_key = credentials["gitarsenal_openai_api_key"]
167
- if validate_openai_key(stored_key):
168
- return stored_key
169
-
170
- # If no GitArsenal key found, return None (don't prompt user)
171
- print("⚠️ GitArsenal's OpenAI API key not available for debugging")
172
- return None
173
-
174
- def get_modal_token(self):
175
- """Get Modal token with basic validation"""
176
- # First check if we have a built-in token from setup_modal_token.py
177
- try:
178
- from setup_modal_token import BUILT_IN_MODAL_TOKEN
179
- return BUILT_IN_MODAL_TOKEN
180
- except ImportError:
181
- pass
182
-
183
- # Fall back to credentials file if needed
184
- credentials = self.load_credentials()
185
- if "modal_token" in credentials:
186
- return credentials["modal_token"]
187
-
188
- # Return the built-in token as a last resort
189
- return "mo-abcdef1234567890abcdef1234567890" # Same as in setup_modal_token.py
190
-
191
- def get_huggingface_token(self):
192
- """Get Hugging Face token with basic validation"""
193
- def validate_hf_token(token):
194
- # HF tokens are typically non-empty strings
195
- return bool(token) and len(token) > 8
196
-
197
- # First check stored credentials
198
- credentials = self.load_credentials()
199
- if "huggingface_token" in credentials:
200
- stored_token = credentials["huggingface_token"]
201
- if validate_hf_token(stored_token):
202
- return stored_token
203
-
204
- prompt = "A Hugging Face token is required.\nYou can get your token from: https://huggingface.co/settings/tokens"
205
- return self.get_credential("huggingface_token", prompt, is_password=True, validate_func=validate_hf_token)
206
-
207
- def get_wandb_api_key(self):
208
- """Get Weights & Biases API key with validation"""
209
- def validate_wandb_key(key):
210
- # W&B API keys are typically 40 characters
211
- return len(key) == 40
212
-
213
- # First check stored credentials
214
- credentials = self.load_credentials()
215
- if "wandb_api_key" in credentials:
216
- stored_key = credentials["wandb_api_key"]
217
- if validate_wandb_key(stored_key):
218
- return stored_key
219
-
220
- prompt = "A Weights & Biases API key is required.\nYou can get your API key from: https://wandb.ai/authorize"
221
- return self.get_credential("wandb_api_key", prompt, is_password=True, validate_func=validate_wandb_key)
222
-
223
- def get_anthropic_api_key(self):
224
- """Get Anthropic API key with validation"""
225
- def validate_anthropic_key(key):
226
- # Anthropic keys usually start with "sk-ant-" and are typically 48+ characters
227
- return key.startswith("sk-ant-") and len(key) > 40
228
-
229
- # First check stored credentials
230
- credentials = self.load_credentials()
231
- if "anthropic_api_key" in credentials:
232
- stored_key = credentials["anthropic_api_key"]
233
- if validate_anthropic_key(stored_key):
234
- return stored_key
235
-
236
- # Then check environment variable
237
- env_key = os.environ.get("ANTHROPIC_API_KEY")
238
- if env_key and validate_anthropic_key(env_key):
239
- return env_key
240
-
241
- prompt = "An Anthropic API key is required.\nYou can get your API key from: https://console.anthropic.com/"
242
- return self.get_credential("anthropic_api_key", prompt, is_password=True, validate_func=validate_anthropic_key)
243
116
 
244
- def get_groq_api_key(self):
245
- """Get Groq API key with validation"""
246
- def validate_groq_key(key):
247
- # Groq keys are non-empty; basic length check
248
- return bool(key) and len(key) > 20
249
-
250
- # First check stored credentials
251
- credentials = self.load_credentials()
252
- if "groq_api_key" in credentials:
253
- stored_key = credentials["groq_api_key"]
254
- if validate_groq_key(stored_key):
255
- return stored_key
256
-
257
- # Then check environment variable
258
- env_key = os.environ.get("GROQ_API_KEY")
259
- if env_key and validate_groq_key(env_key):
260
- return env_key
261
-
262
- prompt = "A Groq API key is required for Groq models.\nYou can get your key from: https://console.groq.com/keys"
263
- return self.get_credential("groq_api_key", prompt, is_password=True, validate_func=validate_groq_key)
264
-
265
- def get_exa_api_key(self):
266
- """Get Exa API key with validation"""
267
- def validate_exa_key(key):
268
- # Exa API keys are typically 32+ characters
269
- return len(key) >= 32
270
-
271
- # First check stored credentials
272
- credentials = self.load_credentials()
273
- if "exa_api_key" in credentials:
274
- stored_key = credentials["exa_api_key"]
275
- if validate_exa_key(stored_key):
276
- return stored_key
277
-
278
- # Then check environment variable
279
- env_key = os.environ.get("EXA_API_KEY")
280
- if env_key and validate_exa_key(env_key):
281
- return env_key
282
-
283
- prompt = "An Exa API key is required for web search functionality.\nYou can get your API key from: https://exa.ai/"
284
- return self.get_credential("exa_api_key", prompt, is_password=True, validate_func=validate_exa_key)
285
-
286
117
  def clear_credential(self, key):
287
118
  """Remove a specific credential"""
288
119
  credentials = self.load_credentials()
@@ -16,6 +16,7 @@ def main():
16
16
  parser.add_argument('command', choices=['add', 'list', 'view', 'delete'], help='Command to execute')
17
17
  parser.add_argument('--service', help='Service name (openai_api_key, WANDB_API_KEY, HUGGINGFACE_TOKEN, modal_token)')
18
18
  parser.add_argument('--key', help='API key (for add command)')
19
+ parser.add_argument('--json', action='store_true', help='Output in JSON format (for list command)')
19
20
 
20
21
  args = parser.parse_args()
21
22
 
@@ -25,7 +26,7 @@ def main():
25
26
  if args.command == 'add':
26
27
  handle_add(credentials_manager, args)
27
28
  elif args.command == 'list':
28
- handle_list(credentials_manager)
29
+ handle_list(credentials_manager, args)
29
30
  elif args.command == 'view':
30
31
  handle_view(credentials_manager, args)
31
32
  elif args.command == 'delete':
@@ -94,10 +95,15 @@ def handle_add(credentials_manager, args):
94
95
  print("❌ Failed to save API key")
95
96
  sys.exit(1)
96
97
 
97
- def handle_list(credentials_manager):
98
+ def handle_list(credentials_manager, args=None):
98
99
  """Handle listing all stored API keys"""
99
100
  credentials = credentials_manager.load_credentials()
100
101
 
102
+ if args and args.json:
103
+ # Return JSON format for programmatic access
104
+ print(json.dumps(credentials))
105
+ return
106
+
101
107
  if not credentials:
102
108
  print("📭 No API keys stored")
103
109
  return