├── bin ├── package.json └── index.js ├── .gitignore ├── addToPath.sh ├── package.json └── README.md /bin/package.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | session.json 2 | config.json 3 | payload.json 4 | node_modules 5 | release/** 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /addToPath.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the current directory 4 | CURRENT_DIR=$(dirname "$(readlink -f "$0")") 5 | 6 | # Check if the current directory is already in the PATH 7 | if [[ ":$PATH:" == *":$CURRENT_DIR:"* ]]; then 8 | echo "Current directory '$CURRENT_DIR' is already in the PATH." 9 | else 10 | # Add the current directory to the PATH in ~/.bashrc 11 | echo "export PATH=$CURRENT_DIR:\$PATH" >> ~/.bashrc 12 | echo "Current directory '$CURRENT_DIR' added to the PATH in ~/.bashrc. Changes will take effect after restarting the terminal." 13 | fi 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "greptile", 3 | "version": "1.0.6", 4 | "main": "index.js", 5 | "bin": { 6 | "greptile": "./bin/index.js" 7 | }, 8 | "type": "module", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "assets": [ 13 | "bin/config.json" 14 | ], 15 | "author": "greptile.com", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@octokit/rest": "^20.0.2", 19 | "axios": "^1.6.7", 20 | "cli-spinners": "^2.9.2", 21 | "clipboardy": "^2.3.0", 22 | "express": "^4.18.2", 23 | "js-base64": "^3.7.6", 24 | "node-fetch": "^2.7.0", 25 | "open": "^8.4.0", 26 | "ora": "^5.4.1", 27 | "punycode": "^2.3.1", 28 | "shelljs": "^0.8.4", 29 | "yargs": "^17.7.2" 30 | }, 31 | "description": "", 32 | "pkg": { 33 | "targets": [ 34 | "node14-linux-x64", 35 | "node14-macos-x64", 36 | "node14-win-x64" 37 | ], 38 | "outputPath": "release", 39 | "scripts": [ 40 | "bin/index.js", 41 | "bin/config.json" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Greptile CLI 3 | 4 | Greptile is a Command Line Interface (CLI) that enables developers to search and understand complex codebases in English. Learn more at [greptile.com](https://greptile.com). 5 | 6 | ## Quickstart 7 | 8 | 1. **Install greptile via `npm`:** 9 | 10 | ```bash 11 | npm install -g greptile 12 | greptile addPath 13 | ``` 14 | 15 | This ensures Greptile is in your system's PATH for global usage. Without the addPath command, you will not be able to use the CLI tool.. 16 | 17 | 2. **Authenticate with GitHub:** 18 | 19 | ```bash 20 | greptile auth 21 | ``` 22 | 23 | 3. **Add repositories to the chat session:** 24 | 25 | ```bash 26 | greptile add [repo link or owner/repo] 27 | ``` 28 | 29 | - For example: 30 | 31 | ```bash 32 | greptile add https://github.com/facebook/react 33 | ``` 34 | 35 | - or 36 | 37 | ```bash 38 | greptile add facebook/react 39 | ``` 40 | 41 | You can add up to 10 repositories to the session. 42 | 43 | 4. **Begin!** 44 | 45 | ```bash 46 | greptile start 47 | ``` 48 | 49 | This launches a shell allowing you to ask questions to Greptile's AI with full context of the provided repositories. 50 | 51 | If you have any questions or comments, email us at founders@greptile.com. 52 | 53 | ## How to Run 54 | 55 | You can run Greptile using the following commands: 56 | 57 | ### Authentication 58 | 59 | To authenticate with GitHub, use the following command: 60 | 61 | ```bash 62 | greptile auth 63 | ``` 64 | 65 | ### Adding a Repository 66 | 67 | To add a repository to the session, use the following command: 68 | 69 | ```bash 70 | greptile add 71 | ``` 72 | 73 | Replace `` with the GitHub repository URL or `` with the owner and repository name. 74 | 75 | ### Listing Repositories 76 | 77 | To list repositories in the current session, use the following command: 78 | 79 | ```bash 80 | greptile list 81 | ``` 82 | 83 | ### Removing a Repository 84 | 85 | To remove a repository from the session, use the following command: 86 | 87 | ```bash 88 | greptile remove 89 | ``` 90 | 91 | Replace `` with the GitHub repository URL or `` with the owner and repository name. 92 | 93 | ### Starting Greptile 94 | 95 | To start the Greptile application and interact with the repositories, use the following command: 96 | 97 | ```bash 98 | greptile start 99 | ``` 100 | 101 | Follow the prompts to ask questions and retrieve information from the added repositories. 102 | 103 | ### Help 104 | 105 | To display help information, use the following command: 106 | 107 | ```bash 108 | greptile help 109 | ``` 110 | 111 | Feel free to explore and interact with Greptile to manage your repositories efficiently! 112 | ``` 113 | 114 | This version focuses on the essential information and improves the overall organization. 115 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const yargs = require("yargs"); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | // const configPath = 'config.json'; 6 | const currentDirectory = __dirname; 7 | const configPath = path.join(currentDirectory, 'config.json'); 8 | const sessionPath = path.join(currentDirectory, 'session.json'); 9 | const fetch = require('node-fetch'); 10 | const { Base64 } = require('js-base64'); 11 | const open = require('open'); 12 | const readline = require('readline'); 13 | const clipboardy = require('clipboardy'); 14 | const ora = require('ora'); 15 | const spinner = ora(); 16 | const exec = require('child_process').exec; 17 | // GitHub credentials 18 | let clientId = '3b18d3e6e037d70908ac'; 19 | 20 | clientId = 'Iv1.1bfb3337c164d452' 21 | // clientId = '42a2bd08980b5a89a820' 22 | let firstTime = true; 23 | const scope = 'read:user user:email'; 24 | const githubEndpoint = 'https://github.com/login/device/code'; 25 | let access_token = null; 26 | const { promisify } = require('util'); 27 | const setTimeoutPromise = promisify(setTimeout); 28 | const debugMode = false; 29 | const payloadFilePath = path.resolve(__dirname, 'payload.json'); 30 | 31 | const executeAuthCommand = async () => { 32 | if (!isAuthenticated()) { 33 | console.log("Redirecting to GitHub authentication..."); 34 | let deviceCode; 35 | let userCode; 36 | const pollingInterval = 10000; 37 | let intervalId; 38 | 39 | const initiateDeviceAuthorization = async () => { 40 | try { 41 | const response = await fetch(githubEndpoint, { 42 | method: 'POST', 43 | headers: { 44 | 'Content-Type': 'application/json', 45 | 'Accept': 'application/json' 46 | }, 47 | body: JSON.stringify({ client_id: clientId, scope: scope }) 48 | }); 49 | const data = await response.json(); 50 | 51 | // console.log('Response:', data); 52 | 53 | deviceCode = data.device_code; 54 | userCode = data.user_code; 55 | 56 | console.log(`Please go to ${data.verification_uri} and enter the code: ${userCode}`); 57 | clipboardy.writeSync(userCode); 58 | await setTimeoutPromise(3000); 59 | await open(data.verification_uri); 60 | intervalId = setInterval(checkAuthorization, pollingInterval); 61 | } catch (error) { 62 | if (debugMode) { console.log("Error:", error); } 63 | } 64 | }; 65 | 66 | const checkAuthorization = async () => { 67 | try { 68 | const response = await fetch('https://github.com/login/oauth/access_token', { 69 | method: 'POST', 70 | headers: { 71 | 'Content-Type': 'application/json', 72 | 'Accept': 'application/json' 73 | }, 74 | body: JSON.stringify({ 75 | client_id: clientId, 76 | device_code: deviceCode, 77 | grant_type: 'urn:ietf:params:oauth:grant-type:device_code' 78 | }) 79 | }); 80 | 81 | const data = await response.json(); 82 | 83 | if (data.access_token) { 84 | clearInterval(intervalId); 85 | access_token = data.access_token; 86 | writeConfig(access_token); 87 | spinner.succeed('Authorization successful'); 88 | process.exit(0); 89 | } else if (data.error && data.error === 'authorization_pending') { 90 | if (firstTime) { 91 | spinner.start('Authorization pending'); 92 | firstTime = false; 93 | } 94 | } else { 95 | clearInterval(intervalId); 96 | spinner.fail('Authorization failed or expired:', data.error_description) 97 | process.exit(1); 98 | } 99 | } catch (error) { 100 | if (debugMode) { 101 | console.error('Error:', error); 102 | } 103 | } 104 | }; 105 | 106 | await initiateDeviceAuthorization(); 107 | 108 | 109 | } else { 110 | console.log("You are already authenticated"); 111 | process.exit(0); 112 | } 113 | }; 114 | 115 | // Command line options and commands 116 | const usage = "\nUsage: greptile "; 117 | const options = yargs 118 | .usage(usage) 119 | .command("help", "Display help information") 120 | .command("add ", "Add a repository to the session") 121 | .command("list", "List repositories in the current session") 122 | .command("remove ", "Remove a repository from the session") 123 | .command("start", "Start Greptile application") 124 | .command("auth", "Redirect to GitHub authentication") 125 | .command("addPath", "Adds greptie to your Path") 126 | .demandCommand(1, "Please specify a command.") 127 | .help(true) 128 | .argv; 129 | 130 | // Command execution based on user input 131 | async function main() { 132 | if (!fs.existsSync(configPath)) { 133 | const defaultConfig = { 134 | github: { 135 | access_token: null, 136 | }, 137 | }; 138 | fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf-8'); 139 | } 140 | 141 | // Check if session.json exists, create with default content if not 142 | if (!fs.existsSync(sessionPath)) { 143 | const defaultSession = { 144 | repositories: [], 145 | }; 146 | fs.writeFileSync(sessionPath, JSON.stringify(defaultSession, null, 2), 'utf-8'); 147 | } 148 | 149 | // Check if payload.json exists, create with default content if not 150 | if (!fs.existsSync(payloadFilePath)) { 151 | const defaultPayload = { 152 | messages: [], 153 | repositories: [], 154 | sessionId: '', 155 | user: { 156 | email: '', 157 | token: { 158 | github: '', 159 | }, 160 | }, 161 | }; 162 | fs.writeFileSync(payloadFilePath, JSON.stringify(defaultPayload, null, 2), 'utf-8'); 163 | } 164 | 165 | const command = options._[0]; 166 | switch (command) { 167 | 168 | case "addPath": 169 | addToPath() 170 | break; 171 | 172 | case "add": 173 | await executeAddCommand(options.repository); 174 | process.exit(); 175 | break; 176 | 177 | case "help": 178 | executeHelpCommand(); 179 | process.exit(); 180 | break; 181 | 182 | case "start": 183 | async function runLoop() { 184 | let isDone = false; 185 | while (!isDone) { 186 | 187 | let userQuestion = await getUserQuestion(); 188 | if (userQuestion === "exit") { 189 | isDone = true; 190 | } else { 191 | if (hasNoRepositories()) { 192 | console.log("Please first add repositories to the session using greptile add ") 193 | process.exit(-1) 194 | 195 | } 196 | else { 197 | await executeStartCommand(userQuestion); 198 | } 199 | 200 | } 201 | } 202 | } 203 | runLoop(); 204 | break; 205 | 206 | case "auth": 207 | executeAuthCommand(); 208 | 209 | break; 210 | 211 | case "list": 212 | executeListCommand(); 213 | process.exit(); 214 | break; 215 | 216 | case "remove": 217 | executeRemoveCommand(options.repository); 218 | process.exit(); 219 | break; 220 | 221 | default: 222 | console.error("Invalid command. Use 'greptile help' for assistance."); 223 | process.exit(); 224 | break; 225 | 226 | } 227 | } 228 | 229 | function executeHelpCommand() { 230 | console.log("Executing help command..."); 231 | } 232 | 233 | async function executeAddCommand(repositoryLink) { 234 | if (!isAuthenticated()) { 235 | console.error("Error: Please authenticate with GitHub first. Use 'greptile auth' to authenticate."); 236 | process.exit(1); 237 | } else { 238 | if (!repositoryLink) { 239 | console.error("Error: Please provide a repository name. Example: greptile add owner/repository"); 240 | process.exit(1); 241 | } 242 | 243 | // Load existing session data 244 | let sessionData; 245 | try { 246 | const sessionFile = fs.readFileSync(sessionPath, 'utf-8'); 247 | sessionData = JSON.parse(sessionFile); 248 | } catch (error) { 249 | if (debugMode) { 250 | console.log(error) 251 | } 252 | // If the file doesn't exist or has invalid JSON, start with an empty session 253 | sessionData = { 254 | repositories: [] 255 | }; 256 | } 257 | // Add the new repository to the session 258 | const parsedRepo = parseIdentifier(repositoryLink) 259 | try { 260 | repository = parsedRepo.repository; 261 | remote = parsedRepo.remote; 262 | branch = parsedRepo.branch || await getDefaultBranch(remote, repository); 263 | } 264 | catch (error) { 265 | console.log("There was an error processing the repository link. Please check your repository link again") 266 | process.exit(-1) 267 | } 268 | if (typeof repository === 'undefined') { 269 | console.log("Error: Invalid repository name. Enter github link, e.g. https://github.com/facebook/react") 270 | process.exit(-1) 271 | } 272 | const repoInfo = await getRepoInfo(repository, remote, branch); 273 | 274 | try { 275 | if (debugMode) { 276 | console.log(repoInfo) 277 | } 278 | 279 | if (repoInfo.responses[0]) { 280 | await writeRepoToFile(repositoryLink); 281 | } 282 | else { 283 | // Check whether this is supposed to be here 284 | if (repoInfo.failed[0] && repoInfo.failed[0].repository == repository) { 285 | if (repoInfo.failed[0].statusCode === 400) { 286 | console.log(`Error ${repoInfo.failed[0].statusCode}: Bad Request`); 287 | } else if (repoInfo.failed[0].statusCode === 401) { 288 | console.log(`Error ${repoInfo.failed[0].statusCode}: Unauthorized`); 289 | } else if (repoInfo.failed[0].statusCode === 404) { 290 | if (repoInfo.failed[0].message && repoInfo.failed[0].message == "Repository not processed by Greptile.") { 291 | await writeRepoToFile(repositoryLink); 292 | const processRepo = await getRepo(repository); 293 | if (debugMode) { 294 | console.log(processRepo) 295 | } 296 | } 297 | else { 298 | console.log(`Error ${repoInfo.failed[0].statusCode}: Not Found`); 299 | } 300 | } else if (repoInfo.failed[0].statusCode === 500) { 301 | console.log(`Error ${repoInfo.failed[0].statusCode}: Internal Server Error`); 302 | } else { 303 | console.log(`Error ${repoInfo.failed[0].statusCode}: Unhandled Status Code`); 304 | } 305 | process.exit(1) 306 | } 307 | await getRepo(repository); 308 | } 309 | } catch (error) { 310 | if (debugMode) { console.error(error) } 311 | if (repoInfo.failed[0] && repoInfo.failed[0].repository == repository) { 312 | if (repoInfo.failed[0].statusCode === 400) { 313 | console.log(`Error ${repoInfo.failed[0].statusCode}: Bad Request`); 314 | } else if (repoInfo.failed[0].statusCode === 401) { 315 | console.log(`Error ${repoInfo.failed[0].statusCode}: Unauthorized`); 316 | } else if (repoInfo.failed[0].statusCode === 404) { 317 | if (repoInfo.failed[0].message && repoInfo.failed[0].message == "Repository not processed by Greptile.") { 318 | await writeRepoToFile(repositoryLink); 319 | const processRepo = await getRepo(repository); 320 | if (debugMode) { console.log(processRepo) } 321 | } 322 | else { 323 | console.log(`Error ${repoInfo.failed[0].statusCode}: Not Found`); 324 | } 325 | } else if (repoInfo.failed[0].statusCode === 500) { 326 | console.log(`Error ${repoInfo.failed[0].statusCode}: Internal Server Error`); 327 | } else { 328 | console.log(`Error ${repoInfo.failed[0].statusCode}: Unhandled Status Code`); 329 | } 330 | process.exit(1) 331 | } 332 | } 333 | // console.log(response) 334 | // Write the updated session data back to the file 335 | } 336 | } 337 | 338 | async function writeRepoToFile(repositoryLink) { 339 | let sessionData; 340 | try { 341 | const sessionFile = fs.readFileSync(sessionPath, 'utf-8'); 342 | sessionData = JSON.parse(sessionFile); 343 | } catch (error) { 344 | // If the file doesn't exist or has invalid JSON, start with an empty session 345 | sessionData = { 346 | repositories: [] 347 | }; 348 | } 349 | 350 | // Check if the repository link already exists 351 | if (!sessionData.repositories.includes(repositoryLink)) { 352 | try { 353 | sessionData.repositories.push(repositoryLink); 354 | const sessionFile = JSON.stringify(sessionData, null, 2); 355 | fs.writeFileSync(sessionPath, sessionFile, 'utf-8'); 356 | console.log(`Repository '${repositoryLink}' added to the session.`); 357 | 358 | // Update payload.json with the new session data 359 | const payload = await createPayload2("", createSessionId()); 360 | writePayloadToFile(payload); 361 | 362 | } catch (error) { 363 | console.error('Error writing session data to file:', error); 364 | } 365 | } else { 366 | console.log(`Repository '${repositoryLink}' already exists in the session.`); 367 | } 368 | } 369 | 370 | function executeListCommand() { 371 | if (!isAuthenticated()) { 372 | console.error("Error: Please authenticate with GitHub first. Use 'greptile auth' to authenticate."); 373 | process.exit(1); 374 | } else { 375 | // Load existing session data 376 | let sessionData; 377 | try { 378 | const sessionFile = fs.readFileSync(sessionPath, 'utf-8'); 379 | sessionData = JSON.parse(sessionFile); 380 | } catch (error) { 381 | // If the file doesn't exist or has invalid JSON, start with an empty session 382 | sessionData = { 383 | repositories: [] 384 | }; 385 | } 386 | 387 | // Display the list of repositories in the current session 388 | const repositories = sessionData.repositories; 389 | if (repositories.length === 0) { 390 | console.log("No repositories in the current session."); 391 | } else { 392 | console.log("Repositories in the current session:"); 393 | repositories.forEach((repoLink, index) => { 394 | const repo = parseIdentifier(repoLink).repository; 395 | console.log(`${index + 1}. ${repo}`); 396 | }); 397 | } 398 | } 399 | } 400 | 401 | function executeRemoveCommand(repository) { 402 | if (!isAuthenticated()) { 403 | console.error("Error: Please authenticate with GitHub first. Use 'greptile auth' to authenticate."); 404 | process.exit(1); 405 | } else { 406 | if (!repository) { 407 | console.error("Error: Please provide a repository name. Example: greptile remove owner/repository or https://github.com/facebook/react"); 408 | process.exit(1); 409 | } 410 | 411 | // Load existing session data 412 | let sessionData; 413 | try { 414 | const sessionFile = fs.readFileSync(sessionPath, 'utf-8'); 415 | sessionData = JSON.parse(sessionFile); 416 | } catch (error) { 417 | // If the file doesn't exist or has invalid JSON, start with an empty session 418 | sessionData = { 419 | repositories: [] 420 | }; 421 | } 422 | 423 | // Check if the repository exists in the session 424 | const index = sessionData.repositories.findIndex((repo) => repo.includes(repository)); 425 | if (index === -1) { 426 | console.log(`Repository '${repository}' not found in the current session.`); 427 | } else { 428 | // Remove the repository from the session 429 | sessionData.repositories.splice(index, 1); 430 | 431 | // Write the updated session data back to the file 432 | try { 433 | const sessionFile = JSON.stringify(sessionData, null, 2); 434 | fs.writeFileSync(sessionPath, sessionFile, 'utf-8'); 435 | console.log(`Repository '${repository}' removed from the session.`); 436 | } catch (error) { 437 | if (debugMode) { 438 | console.error('Error writing session data to file:', error); 439 | } 440 | } 441 | } 442 | } 443 | } 444 | // Function to execute the start command 445 | // Inside executeStartCommand function 446 | async function executeStartCommand(userQuestion) { 447 | try { 448 | if (!isAuthenticated()) { 449 | console.error("Error: Please authenticate with GitHub first. Use 'greptile auth' to authenticate."); 450 | process.exit(1); 451 | } else { 452 | 453 | await useChatApi(userQuestion); 454 | } 455 | } 456 | 457 | catch (error) { 458 | if (debugMode) { 459 | console.log(error) 460 | } 461 | process.exit(-1) 462 | 463 | } 464 | } 465 | 466 | async function getUserQuestion() { 467 | const rl = readline.createInterface({ 468 | input: process.stdin, 469 | output: process.stdout 470 | }); 471 | function getActualQuestion() { 472 | return new Promise((resolve) => { 473 | rl.question('\n\n Question: (Hint: Type "exit" to exit) ', (answer) => { 474 | resolve(answer); 475 | }); 476 | 477 | }); 478 | } 479 | const userQuestion = await getActualQuestion(); 480 | rl.close(); 481 | return userQuestion; 482 | 483 | 484 | } 485 | async function getRepo(repo, branch = "main", remote = "github") { 486 | try { 487 | const body = JSON.stringify({ 488 | "remote": remote, // one of "github", "gitlab" for now 489 | "repository": repo, // formatted as owner/repository 490 | // "branch": "main", // optional, defaults to repo default on GH/GL 491 | // "reload": true, // optional, if false will not reprocess if previously successful, default true 492 | // "notify": true // optional, whether to notify the user when finished, default true 493 | }) 494 | const repoInfo = await fetch("https://api.greptile.com/v1/repositories", { 495 | method: "POST", 496 | body: body, 497 | headers: { 498 | "Content-Type": "application/json", 499 | "Authorization": "Bearer " + getAccessToken() 500 | }, 501 | }); 502 | 503 | const repoInfoJson = await repoInfo.json(); 504 | return repoInfoJson; 505 | } catch (error) { 506 | if (debugMode) { 507 | console.log("Error:", error); 508 | } 509 | return null; 510 | } 511 | } 512 | 513 | async function getRepoInfo(repo, remote, branch) { 514 | // console.log("Called getRepoInfo with:", repo, remote, branch) 515 | const repoInfo = await fetch('https://api.greptile.com/v1/repositories/batch?repositories=' + getBase64(remote, repo, branch), { 516 | method: "GET", 517 | headers: { 518 | "Content-Type": "application/json", 519 | "Authorization": "Bearer " + getAccessToken() 520 | }, 521 | }); 522 | 523 | const repoInfoJson = await repoInfo.json(); 524 | 525 | return repoInfoJson; 526 | } 527 | 528 | async function useChatApi(userQuestion) { 529 | const session_id = createSessionId(); 530 | // userQuestion = "What does this repo do?" 531 | // repository = 'onboardai/onboard-vscode' 532 | // branch = 'main' 533 | let payload = readPayloadFromFile(); 534 | 535 | // If the payload is empty, create a new payload with the user's question 536 | if (payload.messages.length === 0) { 537 | if (debugMode) { 538 | console.log("Payload is Empty, creating new Payload") 539 | } 540 | payload = await createPayload2(userQuestion, session_id); 541 | } else { 542 | if (debugMode) { 543 | console.log("Appending user Message to Payload") 544 | } 545 | // If the payload already has messages, append the user's new question 546 | payload = appendMessageToPayload(payload, userQuestion); 547 | } 548 | if (debugMode) { 549 | console.log(payload) 550 | } 551 | 552 | try { 553 | const response = await fetch("https://api.greptile.com/v1/query", { 554 | method: 'POST', 555 | headers: { 556 | 'Content-Type': 'application/json', 557 | "Authorization": "Bearer " + getAccessToken() 558 | }, 559 | body: JSON.stringify(payload), 560 | }) 561 | if (debugMode) { 562 | console.log("Response: ", response) 563 | } 564 | 565 | let buffer = ''; 566 | decoder = new TextDecoder(); 567 | fullResponse = "" 568 | for await (const chunk of response.body) { 569 | const chunkText = decoder.decode(chunk); 570 | // console.log(chunkText) 571 | buffer += chunkText; 572 | const lines = buffer.split(/\r?\n/); 573 | for (let i = 0; i < lines.length - 1; i++) { 574 | const line = lines[i].trim(); 575 | 576 | if (line.length > 0) { 577 | try { 578 | const jsonData = JSON.parse(line); 579 | // console.log('JSONDATA: ',jsonData) 580 | if (jsonData.type == "status") { 581 | if (jsonData.message == '') { 582 | // console.log(" d :, ", fullResponse) 583 | appendMessageToPayload(payload, fullResponse); 584 | // process.exit(0) 585 | } 586 | console.log(jsonData.message) 587 | if (jsonData.message == "Started processing request") { 588 | spinner.start(); 589 | } 590 | if (jsonData.message == "Writing response") { 591 | spinner.succeed('Request processed successfully'); 592 | } 593 | 594 | } 595 | else { 596 | if (typeof jsonData.message === 'string') { 597 | fullResponse += jsonData.message; 598 | } 599 | process.stdout.write(jsonData.message) 600 | } 601 | } catch (error) { 602 | // console.error('Error parsing JSON:', error); 603 | } 604 | } 605 | } 606 | 607 | buffer = lines[lines.length - 1]; 608 | } 609 | 610 | 611 | } catch (error) { 612 | 613 | if (debugMode) { 614 | console.error('Error:', error.message); 615 | } 616 | // Handle errors here 617 | } 618 | } 619 | 620 | function isAuthenticated() { 621 | try { 622 | const configFile = fs.readFileSync(configPath, 'utf-8'); 623 | const configFileData = JSON.parse(configFile) 624 | 625 | if (configFileData.github.access_token != null) { 626 | access_token = configFileData.github.access_token 627 | return true; 628 | } 629 | else { 630 | return false; 631 | } 632 | } catch (error) { 633 | if (debugMode) { 634 | console.log(error) 635 | } 636 | return {}; 637 | } 638 | } 639 | 640 | function getAccessToken() { 641 | try { 642 | const configFile = fs.readFileSync(configPath, 'utf-8'); 643 | const configFileData = JSON.parse(configFile) 644 | 645 | if (configFileData.github.access_token != null) { 646 | access_token = configFileData.github.access_token 647 | return access_token; 648 | } 649 | else { 650 | return null; 651 | } 652 | } catch (error) { 653 | if (debugMode) { 654 | console.log(error) 655 | } 656 | return {}; 657 | } 658 | } 659 | 660 | async function getDefaultBranch(remote, repository) { 661 | // console.log("Called getDefaultBranch with:", remote, repository) 662 | 663 | const token = getAccessToken(); 664 | const url = remote === "github" ? `https://api.github.com/repos/${repository}` 665 | // TODO: Add full support for other remotes 666 | // : remote === "gitlab" ? `https://gitlab.com/api/v4/projects/${repository}` 667 | // : remote === "bitbucket" ? `https://api.bitbucket.org/2.0/repositories/${repository}` 668 | // : remote === "azure" ? `https://dev.azure.com/${repository}/_apis/git/repositories` 669 | // : remote === "visualstudio" ? `https://dev.azure.com/${repository}/_apis/git/repositories` 670 | : null; 671 | 672 | if (!url) return "main"; 673 | 674 | try { 675 | const data = await fetch(url, { 676 | headers: { 677 | Authorization: `Bearer ${token}`, 678 | }, 679 | }).then((response) => { 680 | return response.json(); 681 | }); 682 | return data.default_branch; 683 | } catch (error) { 684 | if (debugMode) { 685 | console.error('Error:', error); 686 | } 687 | return "main"; 688 | } 689 | } 690 | 691 | function writeConfig(access__token) { 692 | // Create and write to the file 693 | const config = { 694 | "github": { 695 | "access_token": access__token 696 | } 697 | }; 698 | const configFile = JSON.stringify(config, null, 2); 699 | 700 | try { 701 | fs.writeFileSync(configPath, configFile, 'utf-8'); 702 | if (debugMode) { 703 | console.log('File written successfully'); 704 | } 705 | } catch (err) { 706 | if (debugMode) { 707 | console.error('Error writing to the file:', err); 708 | } 709 | } 710 | } 711 | 712 | function getBase64(remote, repository, branch) { 713 | let repo = remote + ":" + repository + ":" + branch; 714 | if (debugMode) { 715 | console.log(repo) 716 | } 717 | return (Base64.encode(repo)) 718 | } 719 | 720 | function createSessionId() { 721 | return Math.random().toString(36).substring(2, 15) + 722 | Math.random().toString(36).substring(2, 15); 723 | } 724 | 725 | async function createPayload2(userQuestion, session_id, remote = "github", branch = "main", external = false) { 726 | // Load existing session data 727 | let sessionData; 728 | try { 729 | const sessionFile = fs.readFileSync(sessionPath, 'utf-8'); 730 | sessionData = JSON.parse(sessionFile); 731 | } catch (error) { 732 | // If the file doesn't exist or has invalid JSON, start with an empty session 733 | sessionData = { 734 | repositories: [] 735 | }; 736 | } 737 | 738 | const payload = { 739 | messages: [ 740 | { 741 | id: '1', 742 | role: "user", 743 | content: userQuestion 744 | } 745 | ], 746 | repositories: await Promise.all(sessionData.repositories.map(async (repo) => { 747 | const parsedRepo = parseIdentifier(repo); 748 | return { 749 | remote: parsedRepo.remote, 750 | name: parsedRepo.repository, 751 | branch: parsedRepo.branch || await getDefaultBranch(parsedRepo.remote, parsedRepo.repository), 752 | name: parsedRepo.repository, 753 | external: external, 754 | }; 755 | })), 756 | sessionId: session_id, 757 | } 758 | 759 | return payload; 760 | } 761 | 762 | function readPayloadFromFile() { 763 | try { 764 | const payloadFile = fs.readFileSync(payloadFilePath, 'utf-8'); 765 | const payload = JSON.parse(payloadFile); 766 | 767 | return payload; 768 | } catch (error) { 769 | // If the file doesn't exist or has invalid JSON, return an empty payload 770 | return { 771 | messages: [], 772 | repositories: [], 773 | sessionId: '', 774 | user: { 775 | email: '', 776 | token: { 777 | github: '', 778 | }, 779 | }, 780 | }; 781 | } 782 | } 783 | 784 | function writePayloadToFile(payload) { 785 | try { 786 | const payloadFile = JSON.stringify(payload, null, 2); 787 | fs.writeFileSync(payloadFilePath, payloadFile, 'utf-8'); 788 | } catch (error) { 789 | if (debugMode) { 790 | console.error('Error writing payload to file:', error); 791 | } 792 | } 793 | } 794 | 795 | function appendMessageToPayload(payload, content) { 796 | payload.messages.push({ 797 | id: (payload.messages.length + 1).toString(), 798 | role: 'user', 799 | content: content, 800 | }); 801 | writePayloadToFile(payload); 802 | return payload; 803 | } 804 | 805 | function hasNoRepositories() { 806 | try { 807 | const sessionFile = fs.readFileSync(sessionPath, 'utf-8'); 808 | const sessionData = JSON.parse(sessionFile); 809 | if (debugMode) { 810 | console.log(sessionData.repositories.length) 811 | } 812 | return sessionData.repositories.length === 0; 813 | } catch (error) { 814 | if (debugMode) { 815 | console.log(error) 816 | } 817 | // If the file doesn't exist or has invalid JSON, return true (no repositories) 818 | return true; 819 | } 820 | } 821 | 822 | function parseIdentifier(input) { 823 | if (!isDomain(input)) { 824 | const regex = /^(([^:]*):([^:]*):|[^:]*)([^:]*)$/; 825 | const match = input.match(regex); 826 | if (!match) return null; 827 | const keys = input.split(":"); 828 | if (keys.length === 1) 829 | return { 830 | remote: "github", 831 | branch: "", 832 | repository: keys[0], 833 | }; 834 | if (keys.length === 3) { 835 | let remote = keys[0], 836 | branch = keys[1], 837 | repository = keys[2]; 838 | if (remote === "azure" && repository.split("/").length == 2) { 839 | let repository_list = repository.split("/"); 840 | repository_list.push(repository_list[1]); 841 | repository = repository_list.join("/"); 842 | } 843 | return { 844 | remote: remote, 845 | branch: branch, 846 | repository: repository, 847 | }; 848 | } 849 | return null; // only 2 entries may be ambiguous (1 might be as well...) 850 | } 851 | if (!input.startsWith("http")) input = "https://" + input; 852 | if (input.endsWith(".git")) input = input.slice(0, -4); 853 | try { 854 | const url = new URL(input); 855 | let remote = (() => { 856 | try { 857 | const services = ["github", "gitlab", "bitbucket", "azure", "visualstudio"]; 858 | return (services.find((service) => url.hostname.includes(service)) || null) 859 | } catch (e) { 860 | return null; 861 | } 862 | })(); 863 | if (!remote) return null; 864 | let repository, branch, regex, match; 865 | switch (remote) { 866 | case "github": 867 | regex = 868 | /([a-zA-Z0-9\._-]+\/[a-zA-Z0-9\%\._-]+)[\/tree\/]*([a-zA-Z0-0\._-]+)?/; 869 | match = url.pathname.match(regex); 870 | repository = decodeURIComponent(match?.[1] || ""); 871 | branch = match?.[2]; 872 | break; 873 | case "gitlab": 874 | regex = 875 | /([a-zA-Z0-9\._-]+\/[a-zA-Z0-9\%\._-]+)(?:\/\-)?(?:(?:\/tree\/)([a-zA-Z0-0\._-]+))?/; 876 | match = url.pathname.match(regex); 877 | repository = decodeURIComponent(match?.[1] || ""); 878 | branch = match?.[2]; 879 | break; 880 | 881 | case "azure": 882 | regex = /([a-zA-Z0-9\%\.\/_-]+)/; 883 | match = url.pathname.match(regex); 884 | repository = 885 | match?.[1].split("/").filter((x) => x !== "_git" && x !== "") || []; 886 | repository.push(repository?.slice(-1)[0]); 887 | repository = decodeURIComponent(repository.slice(0, 3).join("/")); 888 | branch = url.searchParams.get("version")?.slice(2); // remove 'GB' from the beginning 889 | break; 890 | 891 | case "visualstudio": 892 | remote = "azure" 893 | regex = /([a-zA-Z0-9\%\.\/_-]+)/; 894 | const org = url.hostname.split(".")[0]; 895 | match = url.pathname.match(regex); 896 | repository = 897 | match?.[1].split("/").filter((x) => x !== "_git" && x !== "") || []; 898 | repository = decodeURIComponent([org, ...(repository.slice(0, 2))].join("/")); 899 | branch = url.searchParams.get("version")?.slice(2); // remove 'GB' from the beginning 900 | break; 901 | default: 902 | return url.hostname; 903 | } 904 | if (!repository) return null; 905 | // console.log(remote,branch,repository) 906 | return { remote, branch, repository }; 907 | } catch (e) { 908 | return null; 909 | } 910 | }; 911 | 912 | function isDomain(input) { 913 | try { 914 | new URL(input); 915 | const regex = /^(([^:]*):([^:]*):|[^:]*)([^:]*)$/; 916 | const match = input.match(regex); 917 | if (match) return false; 918 | return true; 919 | } catch (e) { 920 | return false; 921 | } 922 | } 923 | 924 | function addToPath() { 925 | // Execute the Bash script 926 | bashFile = path.join(currentDirectory.replace("/bin", "") + "/addToPath.sh") 927 | exec(bashFile, (error, stdout, stderr) => { 928 | if (error) { 929 | console.error(`Error: ${error.message}`); 930 | return; 931 | } 932 | if (stderr) { 933 | console.error(`Error: ${stderr}`); 934 | return; 935 | } 936 | console.log(stdout); 937 | }); 938 | } 939 | 940 | main() 941 | --------------------------------------------------------------------------------