How to Create an AI-Powered CLI with Pieces

Arindam Majumder - Jun 24 - - Dev Community

Introduction

Integrating AI into Command Line Interfaces (CLIs) can transform how developers interact with their tools, making tasks more efficient and intelligent.

In this article, we'll learn how to use Pieces to build a CLI that can ask questions, get formatted responses, and search Stack Overflow for relevant coding issues

Sounds Interesting?

So, without delaying further, Let's START!

What is Pieces OS Client?

Pieces.app

Pieces OS Client is a flexible database that helps us to save, create, and improve code snippets throughout our development process. It includes built-in Local Large Language Models that can answer questions and generate strong code solutions, no matter which development tool we use.

Pieces offers different language-based SDKs to utilize the wide range of functionalities provided by Pieces OS. For this tutorial, we'll be using the TypeScript SDK.

What Can our CLI Do?

Here is a high-level list of some of the feature, our CLI will be able to perform by the end of this Article:

  1. Answer Coding Questions: Provide answers to coding-related questions using its built-in language models.

  2. Search Stack Overflow: Search Stack Overflow for relevant answers to coding issues.

  3. Interactive Mode: An interactive mode where developers can have a continuous conversation with the AI.

You can perform several other actions through the Pieces OS Client and can discover the other endpoints and SDKs through the Open Source by Pieces Repo.

Building Pieces CLI

Prerequisites

Before Starting the Project, make sure you've Pieces OS running in the background. This is crucial because the Pieces OS will serve as the backbone of our CLI, providing the necessary database and language model functionalities.

You can Download Pieces OS from here.

Basic Setup

First, we'll create a Simple Nodejs Project with the following Command:

npm init -y
Enter fullscreen mode Exit fullscreen mode

This command will create a package.json file with default settings. The -y flag automatically answers "yes" to all prompts, allowing for a quick setup.

Next, we'll install the required packages by running the following command:

npm i @pieces.app/pieces-os-client os
Enter fullscreen mode Exit fullscreen mode

Now, We'll create an index.js file in the root directory of our project. This file will serve as the entry point for our CLI application.We'll also import the required packages that we have just installed

#!/usr/bin/env node

import * as Pieces from '@pieces.app/pieces-os-client';
import os from 'os';
Enter fullscreen mode Exit fullscreen mode

💡The #!/usr/bin/env node line at the top of the file is called a shebang. It allows the script to be run as an executable from the command line.

Pieces OS uses different localhost ports depending on the operating system. We'll check the user's platform and set the appropriate port. Add the following code to index.js:

const platform = os.platform();
const port = platform === 'linux' ? 5323 : 1000;
Enter fullscreen mode Exit fullscreen mode

Here, the os.platform() gets the user's OS and changes the port accordingly.

Next, we'll create a instance of the QGPTApi with Pieces Configuration.This instance will allow us to interact with the Pieces OS API.

const configuration = new Pieces.Configuration({
  basePath: `http://localhost:${port}`
});
const apiInstance = new Pieces.QGPTApi(configuration);
Enter fullscreen mode Exit fullscreen mode

In this code, we create a new Configuration object with the basePath set to the appropriate localhost URL based on the user's platform.

With that, we have completed the basic setup of our project.

(optional) To verify that everything is working correctly, write this code in the index.js file

const healthInstance = new Pieces.WellKnownApi(configuration)

async function fetchHealthData() {
  try {
    const data = await healthInstance.getWellKnownHealth();
    console.log(data); // ok
  } catch (error) {
    console.error(error);
  }
}

fetchHealthData();

and Run the following script:

node index.js

If everything is set up correctly, you should see a message ie ok indicating that the connection to Pieces OS was successful.

Implementing the AskQuestion Function

Next we'll create our main askQuestion function which will communicate with API instance of the QGPTApi that we have created previously.

To improve the User experience we'll install a package ora.

npm i ora
Enter fullscreen mode Exit fullscreen mode

This package provides a nice animation while generating the response.

Now, we will define the askQuestion function. This function will take a query as an argument, send it to the QGPTApi, and return the response.

const askQuestion = async (query) => {
  // Check if the CLI is not in interactive mode
  if (!interactiveMode) {
    // Start the spinner animation
    const spinner = ora('Generating response...').start();
  }

  // Define the parameters for the API request
  const params = {
    query,
    relevant: {
      iterable: []
    },
  };

  try {
    // Send the query to the API and get the result
    const result = await apiInstance.question({ qGPTQuestionInput: params });

    // If the CLI is not in interactive mode, stop the spinner and display success message
    if (!interactiveMode) {
      spinner.succeed('Response generated.');
    }

    // Return the text of the first answer
    return result.answers.iterable[0].text;
  } catch (error) {
    // If an error occurs, stop the spinner and display an error message
    if (!interactiveMode) {
      spinner.fail('Error generating response.');
    }
    console.error('Error calling API:', error);
    throw error;
  }
};
Enter fullscreen mode Exit fullscreen mode

Note:

The QGPT endpoint handles queries and provide relevant responses using the QGPT model. This endpoint is part of the QGPTApi and is used to interact with the model by sending questions and receiving answers.

The endpoint processes the input query, utilizes relevant context if provided, and generates a response based on the QGPT model's capabilities.

For more information, check out the docs.

Displaying Help and Version Information

While building a CLI, one of the crucial steps is providing a clear instructions on how to use the tool. We usually provide a help message to show all commands that a user can use with that CLI tool.

Let's Implement that in our tool:

const displayHelp = () => {
  console.log(
`\x1b[1mWelcome to Pieces CLI | By Arindam\x1b[0m

Usage: pieces-cli [options] [query]

Options:
  -i, --interactive  Enter interactive mode
  -h, --help         Display this help message
  -v, --version      Display the version number

Examples:
  pieces-cli "What is the capital of France?"
  pieces-cli -i
  pieces-cli --help
  pieces-cli --version`
  );
};

const isHelp = process.argv.includes("-h") || process.argv.includes("--help");

if(isHelp){
  displayHelp();
  process.exit(0);
}
Enter fullscreen mode Exit fullscreen mode

With this, if the user uses -h or --help in the command, it will show the help message.

Similarly, we'll display the Version of the tool when the user uses the -v or --version command. Here's the code for that:

import pkgJSON from './package.json';

const version = pkgJSON.version || '1.0.4';

const isVersion = process.argv.includes("-v") || process.argv.includes("--version");

if(isVersion){
  console.log(`pieces-cli version: ${version}`);
  process.exit(0);
}
Enter fullscreen mode Exit fullscreen mode

This enhances the user experience and makes our tool more accessible.

Adding StackOverflow Search

In this section, we'll add a function to search Stack Overflow for relevant coding issues based on a query. This will improve the User Experience by providing the links to relevant Stack Overflow discussions based on their queries.

For that, let's install the axios package to make HTTP requests using the following command:

npm i axios
Enter fullscreen mode Exit fullscreen mode

Now, we'll define the searchStackOverflow function. This function will take a query as an argument, send it to the Stack Overflow API, and return a list of relevant links.

const searchStackOverflow = async (query) => {
  // Check if the CLI is not in interactive mode
  if (!interactiveMode) {
    // Start the spinner animation
    const spinner = ora('Searching Stack Overflow...').start();
  }

  try {
    // Send the query to the Stack Overflow API and get the response
    const response = await axios.get('https://api.stackexchange.com/2.3/search/advanced', {
      params: {
        order: 'desc',
        sort: 'relevance',
        q: query,
        site: 'stackoverflow'
      }
    });

    // If the CLI is not in interactive mode, stop the spinner and display success message
    if (!interactiveMode) {
      spinner.succeed('Stack Overflow search completed.');
    }

    // Return the list of links to relevant Stack Overflow discussions
    return response.data.items.map(item => item.link);
  } catch (error) {
    // If an error occurs, stop the spinner and display an error message
    if (!interactiveMode) {
      spinner.fail('Error searching Stack Overflow.');
    }
    console.error('Error searching Stack Overflow:', error.message);
    return [];
  }
};
Enter fullscreen mode Exit fullscreen mode

By following these steps, we have successfully implemented the searchStackOverflow function. This feature can be particularly useful for developers looking for solutions to coding problems.

Implementing Interactive Mode

Next, we'll create a Interactive mode that will allow users to continuously interact with the CLI. This feature will be super helpful for tasks that require ongoing input and feedback.

First, we'll install readline package to handle input output:

npm i readline
Enter fullscreen mode Exit fullscreen mode

Next, we'll check if the user has passed the -i or --interactive flag to enter interactive mode:

const isInteractiveMode = process.argv.includes("-i") || process.argv.includes("--interactive");
Enter fullscreen mode Exit fullscreen mode

Now, we'll use the readline module to create an interface for reading input from the user. This interface will handle input and output streams and provide a prompt for the user.

import readline from 'readline';

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  prompt: 'pieces-cli> ',
  historySize: 100,
  completer: (line) => {
    const completions = ['exit', 'help', 'version', 'clear', 'model'];
    const hits = completions.filter((c) => c.startsWith(line));
    return [hits.length ? hits : completions, line];
  }
});
Enter fullscreen mode Exit fullscreen mode

We'll then call rl.prompt() to display the prompt to the user.

rl.prompt();
Enter fullscreen mode Exit fullscreen mode

After that, we'll set up an event listener for the line event, which is triggered whenever the user enters a line of input. Here we will define the logic to handle specific commands.

rl.on('line', async (line) => {
  const input = line.trim();
  switch (input.toLowerCase()) {
    case 'exit':
      rl.close();
      break;
    case 'help':
      console.log("Available commands: exit, help, version, clear, model");
      rl.prompt();
      break;
    case 'version':
      console.log('pieces-cli version: 1.0.3'); 
      rl.prompt();
      break;
    case 'clear':
      console.clear();
      rl.prompt();
      break;
    default:
      // ...default case
  }
}).on('close', () => {
  console.log('Exiting interactive mode.');
  process.exit(0);
});
Enter fullscreen mode Exit fullscreen mode

Here, we have defined the logic to handle specific commands:

  • exit: Close the readline interface and exit the process.

  • help: Display a list of available commands.

  • version: Display the version number of the CLI.

  • clear: Clear the terminal screen.

For any other input, we assume it might be a question or command that needs further processing. We call an asynchronous function askQuestion(input) to handle the input and print the formatted response using formatResponse(response).

// default case   
 try {
        const response = await askQuestion(input);
        console.log(formatResponse(response));

        // Check if the query is related to coding and search Stack Overflow if necessary
        if (/code|error|exception|bug|issue|problem|function|method|class|variable|syntax|compile|runtime/i.test(input)) {
          const stackOverflowLinks = await searchStackOverflow(input);
          if (stackOverflowLinks.length > 0) {
            console.log(chalk.blue("\nRelevant Stack Overflow links:"));
            stackOverflowLinks.forEach(link => console.log(chalk.blue(link)));
          } else {
            console.log(chalk.yellow("\nNo relevant Stack Overflow links found."));
          }
        }
      } catch (error) {
        console.error('Error calling API:', error);
      }
      rl.prompt();
      break;
Enter fullscreen mode Exit fullscreen mode

If the input seems related to coding (determined by a regular expression test), it search Stack Overflow for relevant links. If links are found, we'll print them; otherwise, we'll indicate that no relevant links were found.

Finally, when the readline interface is closed (e.g., by the exit command), the close event is triggered. We'll then log a message indicating that we are exiting interactive mode and then exit the process.

.on('close', () => {
  console.log('Exiting interactive mode.');
  process.exit(0);
});
Enter fullscreen mode Exit fullscreen mode

Formatting Responses

When working with text responses, especially those that include code snippets, it's important to format them in a way that's easy to read and understand. It improves the user experience of the tool.

Formatting Responses

When working with text responses, especially those that include code snippets, it's important to format them in a way that's easy to read and understand. It improves the user experience of the tool.

Now let's Implement this in our CLI tool.

First, we'll create a formatResponse function takes a text input and formats it by applying different styles to different parts of the text, such as code blocks, headers, and list items.

const formatResponse = (text) => {
  const lines = text.split('\n');
  let formattedText = '';
  let inCodeBlock = false;
  let codeLang = '';

  lines.forEach(line => {
    if (line.startsWith('```

')) {
      inCodeBlock = !inCodeBlock;
      codeLang = inCodeBlock ? line.slice(3).trim() : '';
    } else if (inCodeBlock) {
      const highlighted = codeLang ? hljs.highlight(line, { language: codeLang }).value : hljs.highlightAuto(line).value;
      formattedText += applyChalk(highlighted) + '\n';
    } else if (line.startsWith('# ')) {
      formattedText += chalk.bold(line) + '\n';
    } else if (line.startsWith('* ')) {
      formattedText += chalk.green('• ') + line.slice(2) + '\n';
    } else if (line.startsWith('**') && line.endsWith('**')) {
      formattedText += chalk.bold(line.slice(2, -2)) + '\n';
    } else {
      formattedText += line + '\n';
    }
  });
  return formattedText;
};


Enter fullscreen mode Exit fullscreen mode

Here, if a line starts with triple backticks (```) it toggles the inCodeBlock flag and sets the codeLang if entering a code block.

Note: This Function Made the code blocks wrapped in HTML code so, we need to remove those unwanted HTML tags.

So, we'll create the cleanHtml function that uses regular expression to remove HTML tags from a string and replaces HTML entities with their corresponding characters.


javascript
const cleanHtml = (rawHtml) => {
  let cleanText = rawHtml.replace(/<[^>]*>/g, '');

  const htmlEntities = {
    '&quot;': '"',
    '&amp;': '&',
    '&lt;': '<',
    '&gt;': '>',
    '&nbsp;': ' ',
    '&apos;': "'",
    '&cent;': '¢',
    '&pound;': '£',
    '&yen;': '¥',
    '&euro;': '€',
    '&copy;': '©',
    '&reg;': '®'
  };

  cleanText = cleanText.replace(/&[a-zA-Z]+;/g, (match) => htmlEntities[match] || match);
  cleanText = cleanText.replace(/\s+/g, ' ').trim();

  return cleanText;
};


Enter fullscreen mode Exit fullscreen mode

Finally, we'll add the applyChalk function that applies chalk styles to highlighted code by replacing HTML span tags with corresponding chalk styles.


javascript
const applyChalk = (highlighted) => {
  return cleanHtml(highlighted.replace(/<span class="hljs-(\w+)">(.*?)<\/span>/g, (match, p1, p2) => {
    switch (p1) {
      case 'keyword': return chalk.blue(p2);
      case 'string': return chalk.green(p2);
      case 'built_in': return chalk.cyan(p2);
      case 'comment': return chalk.gray(p2);
      case 'title': return chalk.yellow(p2);
      case 'params': return chalk.magenta(p2);
      case 'function': return chalk.red(p2);
      case 'operator': return chalk.white(p2);
      default: return p2;
    }
  }));
};


Enter fullscreen mode Exit fullscreen mode

And with that We've successfully created our CLI tool using Pieces!!

Kudos to us!!

What's Next?

Now that you've built a basic CLI tool using Pieces OS Client, you can improve and add more features to it.

I've also published the CLI tool as an npm package, which you can check out and use right away.

The code for this project is also public, and you can check it out on GitHub. Feel free to contribute by fixing bugs, adding new features, or improving the documentation.

Your contributions can help make this tool even more powerful and user-friendly.

Conclusion

Overall, integrating AI into your CLI using Pieces OS Client provides a seamless and efficient way to enhance your terminal workflow. It not only improves productivity but also enables more sophisticated interactions with AI models.

Install it today and transform your workflow with the ease of AI at your fingertips

If you found this blog post helpful, please consider sharing it with others who might benefit.

For Paid collaboration mail me at : arindammajumder2020@gmail.com

Connect with me on Twitter, LinkedIn, Youtube and GitHub.

Thank you for Reading!

Thank You

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .