├── src ├── commands │ ├── model.command.ts │ ├── version.command.ts │ ├── config.command.ts │ ├── help.command.ts │ ├── api-key.command.ts │ ├── main.command.ts │ └── provider.command.ts ├── mask.ts ├── providers │ ├── custom.provider.ts │ ├── index.ts │ ├── common.ts │ ├── ollama.provider.ts │ ├── openai.provider.ts │ ├── anthropic.provider.ts │ └── groq.provider.ts ├── config.ts ├── systemPrompt.ts ├── loader.ts └── index.ts ├── bun.lockb ├── demos ├── demo.gif ├── api-key-demo.gif └── provider-demo.gif ├── .gitignore ├── scripts ├── release.sh ├── compile.sh └── install.sh ├── package.json └── README.md /src/commands/model.command.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kynnyhsap/how/HEAD/bun.lockb -------------------------------------------------------------------------------- /demos/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kynnyhsap/how/HEAD/demos/demo.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | .env 4 | 5 | out/ 6 | 7 | how 8 | 9 | .DS_Store -------------------------------------------------------------------------------- /demos/api-key-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kynnyhsap/how/HEAD/demos/api-key-demo.gif -------------------------------------------------------------------------------- /demos/provider-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kynnyhsap/how/HEAD/demos/provider-demo.gif -------------------------------------------------------------------------------- /src/commands/version.command.ts: -------------------------------------------------------------------------------- 1 | const VERSION = `v0.1.0`; 2 | 3 | export function versionCommand() { 4 | console.log(VERSION); 5 | } 6 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION="$1" 4 | 5 | OUT_DIR="./out/*" 6 | 7 | gh release create $VERSION \ 8 | --title $VERSION \ 9 | --target main \ 10 | --generate-notes \ 11 | --draft \ 12 | $OUT_DIR -------------------------------------------------------------------------------- /src/mask.ts: -------------------------------------------------------------------------------- 1 | const N = 3; 2 | 3 | export function mask(s: string) { 4 | if (!s) { 5 | return ""; 6 | } 7 | 8 | if (s.length < N * 2) { 9 | return "*".repeat(s.length); 10 | } 11 | 12 | return s.slice(0, N) + "*".repeat(s.length - N * 2) + s.slice(-N); 13 | } 14 | -------------------------------------------------------------------------------- /src/providers/custom.provider.ts: -------------------------------------------------------------------------------- 1 | import { ProviderSpec, ProviderType } from "./common"; 2 | 3 | export const customProviderSpec: ProviderSpec = { 4 | type: ProviderType.Custom, 5 | name: "Custom", 6 | 7 | func: async (prompt, config) => { 8 | throw new Error("custom not implemented yet"); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/commands/config.command.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from "../config"; 2 | import { mask } from "../mask"; 3 | 4 | export async function configCommand() { 5 | const config = await getConfig(); 6 | 7 | if (config.apiKey) { 8 | config.apiKey = mask(config.apiKey); 9 | } 10 | 11 | console.log(JSON.stringify(config, null, 2)); 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/help.command.ts: -------------------------------------------------------------------------------- 1 | const HELP = `Usage: how to [prompt...] 2 | 3 | Options 4 | -k, --key Set API key 5 | -p, --provider Set provider 6 | -m, --model Set model 7 | -c, --config Print config 8 | -v, --version Print version 9 | -h, --help Get help for commands`; 10 | 11 | export function helpCommand() { 12 | console.log(HELP); 13 | } 14 | -------------------------------------------------------------------------------- /scripts/compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SOURCE_FILE="./src/index.ts" 4 | 5 | OUT_DIR="./out" 6 | 7 | mkdir -p "$OUT_DIR" 8 | 9 | # array of targets 10 | targets=( 11 | "darwin-x64" 12 | "darwin-arm64" 13 | "linux-x64" 14 | "linux-arm64" 15 | ) 16 | 17 | for target in "${targets[@]}"; do 18 | echo "Compiling for $target..." 19 | 20 | bun build --compile --minify --sourcemap --target="bun-$target" --outfile "$OUT_DIR/how-$target" $SOURCE_FILE 21 | done 22 | 23 | echo "Compilation complete for all targets!" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "how", 3 | "version": "0.1.1", 4 | "description": "Ask your terminal (AI) about cli commands", 5 | "module": "./src/index.ts", 6 | "type": "module", 7 | "scripts": { 8 | "how": "bun run ./src/index.ts --", 9 | "compile-dev": "bun build ./src/index.ts --compile --minify --sourcemap --outfile how && chmod +x ./how" 10 | }, 11 | "dependencies": { 12 | "@inquirer/prompts": "^5.1.2", 13 | "chalk": "^5.3.0", 14 | "clipboardy": "^4.0.0" 15 | }, 16 | "devDependencies": { 17 | "@types/bun": "latest" 18 | }, 19 | "peerDependencies": { 20 | "typescript": "^5.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | import path from "path"; 3 | import { DEFAULT_PROVIDER, ProviderType } from "./providers/common"; 4 | 5 | export const CONFIG_PATH = path.join(os.homedir(), ".how/config.json"); 6 | 7 | export type Config = { 8 | provider?: ProviderType | string; 9 | url?: string; 10 | model?: string; 11 | apiKey?: string; 12 | }; 13 | 14 | export async function getConfig(): Promise { 15 | const configFile = Bun.file(CONFIG_PATH); 16 | 17 | const exists = await configFile.exists(); 18 | 19 | const config = exists ? await configFile.json() : {}; 20 | 21 | if (!config.provider) { 22 | config.provider = DEFAULT_PROVIDER; 23 | } 24 | 25 | return config; 26 | } 27 | 28 | export async function saveConfig(config: Config) { 29 | await Bun.write(CONFIG_PATH, JSON.stringify(config, null, 2)); 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/api-key.command.ts: -------------------------------------------------------------------------------- 1 | import { getConfig, saveConfig } from "../config"; 2 | import { mask } from "../mask"; 3 | 4 | import { password } from "@inquirer/prompts"; 5 | import { getProviderSpec } from "../providers"; 6 | 7 | export async function apiKeyCommand() { 8 | const config = await getConfig(); 9 | 10 | const { name, apiKeyRequired, apiKeyLink } = getProviderSpec(config.provider); 11 | 12 | if (!apiKeyRequired) { 13 | console.log(`API key is not required for the ${name} provider`); 14 | return; 15 | } 16 | 17 | const message = `Enter API key for ${name} (${apiKeyLink}) \n`; 18 | 19 | const newApiKey = await password({ 20 | message, 21 | mask: true, 22 | }); 23 | 24 | config.apiKey = newApiKey; 25 | 26 | await saveConfig(config); 27 | 28 | console.log(`API key for ${name} updated: ${mask(newApiKey)}`); 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/main.command.ts: -------------------------------------------------------------------------------- 1 | import clipboard from "clipboardy"; 2 | import { getConfig } from "../config"; 3 | import { getProviderSpec } from "../providers"; 4 | import chalk from "chalk"; 5 | import { loader } from "../loader"; 6 | 7 | export async function mainCommand() { 8 | const config = await getConfig(); 9 | 10 | const prompt = "how " + Bun.argv.slice(2).join(" "); 11 | 12 | loader.start(); 13 | 14 | const { func } = getProviderSpec(config.provider); 15 | const { commands, description } = await func(prompt, config); 16 | 17 | loader.stop(); 18 | 19 | console.log(""); 20 | console.log(chalk.green.bold(description)); 21 | console.log(""); 22 | 23 | commands.forEach((command) => { 24 | console.log(chalk.blue.bold(" $ " + command)); 25 | }); 26 | 27 | console.log(""); 28 | console.log(chalk.gray.italic("copied to clipboard")); 29 | 30 | clipboard.writeSync(commands.join(" && ")); 31 | } 32 | -------------------------------------------------------------------------------- /src/systemPrompt.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | 3 | export const SYSTEM_PROMPT = ` 4 | You are "how", a CLI command generator. 5 | 6 | User will ask how to do something in terminal and you will provide the CLI commands to achieve that. 7 | 8 | Respond in JSON format with "description" - a one sentence description of the task and "commands" - an array of CLI commands to execute. 9 | 10 | Do not add anything else, only raw JSON. 11 | 12 | System Info: ${os.platform()} ${os.arch()} 13 | 14 | Example input: 15 | how to get current datetime 16 | 17 | Example output: 18 | { 19 | "description": "Getting the current time in the terminal", 20 | "commands": [ 21 | "date +"%Y-%m-%d %H:%M:%S"" 22 | ] 23 | } 24 | 25 | Example input: 26 | how to build and install go binary 27 | 28 | Example output: 29 | { 30 | "description": "Building and installing a Go binary", 31 | "commands": [ 32 | "go build main.go", 33 | "go install main" 34 | ] 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /src/loader.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import readline from "readline"; 3 | 4 | export class Loader { 5 | private frames: string[]; 6 | private interval: Timer | undefined; 7 | private currentFrame: number; 8 | 9 | constructor() { 10 | this.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; 11 | this.currentFrame = 0; 12 | } 13 | 14 | start(message = "thinking...") { 15 | this.currentFrame = 0; 16 | 17 | this.interval = setInterval(() => { 18 | readline.clearLine(process.stdout, 0); 19 | readline.cursorTo(process.stdout, 0); 20 | 21 | const str = `${this.frames[this.currentFrame]} ${message}`; 22 | process.stdout.write(chalk.gray(str)); 23 | 24 | this.currentFrame = (this.currentFrame + 1) % this.frames.length; 25 | }, 80); 26 | } 27 | 28 | stop() { 29 | clearInterval(this.interval); 30 | 31 | readline.clearLine(process.stdout, 0); 32 | 33 | readline.cursorTo(process.stdout, 0); 34 | } 35 | } 36 | 37 | export const loader = new Loader(); 38 | -------------------------------------------------------------------------------- /src/commands/provider.command.ts: -------------------------------------------------------------------------------- 1 | import { select } from "@inquirer/prompts"; 2 | import { getProviderSpec, PROVIDER_SPECS } from "../providers"; 3 | import { getConfig, saveConfig } from "../config"; 4 | import chalk from "chalk"; 5 | 6 | export async function providerCommand() { 7 | const config = await getConfig(); 8 | 9 | const { name } = getProviderSpec(config.provider); 10 | 11 | const choices = Object.entries(PROVIDER_SPECS).map(([value, { name }]) => ({ 12 | name, 13 | value, 14 | })); 15 | 16 | const providerMessage = name 17 | ? chalk.gray.italic(` ( current config.provider is ${name} ) `) 18 | : chalk.red.italic(` ( current config.provider is invalid ) `); 19 | 20 | const message = "Select provider" + providerMessage; 21 | 22 | config.provider = await select({ 23 | message, 24 | choices, 25 | }); 26 | 27 | await saveConfig(config); 28 | 29 | console.log(""); 30 | console.log("Provider updated to " + chalk.green.bold(config.provider)); 31 | console.log("Don't forget to update your API key if needed."); 32 | } 33 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_PROVIDER, ProviderSpec, ProviderType } from "./common"; 2 | 3 | import { anthropicProviderSpec } from "./anthropic.provider"; 4 | import { customProviderSpec } from "./custom.provider"; 5 | import { groqProviderSpec } from "./groq.provider"; 6 | import { ollamaProviderSpec } from "./ollama.provider"; 7 | import { openaiProviderSpec } from "./openai.provider"; 8 | 9 | export const PROVIDER_SPECS: Record = { 10 | [ProviderType.Ollama]: ollamaProviderSpec, 11 | 12 | [ProviderType.Groq]: groqProviderSpec, 13 | [ProviderType.OpenAI]: openaiProviderSpec, 14 | [ProviderType.Anthropic]: anthropicProviderSpec, 15 | 16 | [ProviderType.Custom]: customProviderSpec, 17 | }; 18 | 19 | export function getProviderSpec(provider: ProviderType | string | undefined) { 20 | if (!Object.values(ProviderType).includes(provider as ProviderType)) { 21 | console.warn( 22 | `Unknown provider "${provider}" detected in config. Defaulting to "${DEFAULT_PROVIDER}"`, 23 | ); 24 | 25 | return PROVIDER_SPECS[DEFAULT_PROVIDER]; 26 | } 27 | 28 | return PROVIDER_SPECS[provider as ProviderType]; 29 | } 30 | -------------------------------------------------------------------------------- /src/providers/common.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "../config"; 2 | 3 | export type ProviderResult = { 4 | commands: string[]; 5 | description: string; 6 | }; 7 | 8 | type ProviderFunction = ( 9 | prompt: string, 10 | config: Config, 11 | ) => Promise; 12 | 13 | export enum ProviderType { 14 | Ollama = "ollama", 15 | 16 | Groq = "groq", 17 | OpenAI = "openai", 18 | Anthropic = "anthropic", 19 | 20 | Custom = "custom", 21 | } 22 | 23 | export const DEFAULT_PROVIDER = ProviderType.OpenAI; 24 | 25 | export function parseJSONResponse(json: string): ProviderResult { 26 | try { 27 | const { description, commands } = JSON.parse(json); 28 | 29 | return { 30 | description: description ?? "", 31 | commands: commands ?? [], 32 | }; 33 | } catch (error) { 34 | throw new Error( 35 | `Failed to parse "description" and "commands" from LLM json response: ${json}. \n${error.message}`, 36 | ); 37 | } 38 | } 39 | 40 | export type ProviderSpec = { 41 | type: ProviderType; 42 | name: string; 43 | 44 | defaultModel?: string; 45 | 46 | link?: string; 47 | docsLink?: string; 48 | apiKeyLink?: string; 49 | 50 | apiKeyRequired?: boolean; 51 | 52 | func: ProviderFunction; 53 | }; 54 | -------------------------------------------------------------------------------- /src/providers/ollama.provider.ts: -------------------------------------------------------------------------------- 1 | import { parseJSONResponse, ProviderSpec, ProviderType } from "./common"; 2 | import { Config } from "../config"; 3 | import { SYSTEM_PROMPT } from "../systemPrompt"; 4 | 5 | const OLLAMA_API_URL = "http://localhost:11434/v1/chat/completions"; 6 | 7 | const DEFAULT_MODEL = "llama3"; 8 | 9 | export const ollamaProviderSpec: ProviderSpec = { 10 | type: ProviderType.Ollama, 11 | name: "Ollama", 12 | 13 | defaultModel: DEFAULT_MODEL, 14 | 15 | link: "https://ollama.com", 16 | docsLink: "https://ollama.com/blog/openai-compatibility", 17 | 18 | func: async (prompt: string, config: Config) => { 19 | const resp = await fetch(OLLAMA_API_URL, { 20 | method: "POST", 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | body: JSON.stringify({ 25 | model: config.model ?? DEFAULT_MODEL, 26 | 27 | max_tokens: 1024, 28 | 29 | response_format: { 30 | type: "json_object", 31 | }, 32 | 33 | messages: [ 34 | { role: "system", content: SYSTEM_PROMPT }, 35 | { role: "user", content: prompt }, 36 | ], 37 | }), 38 | }); 39 | 40 | const data = await resp.json(); 41 | 42 | if (resp.status !== 200) { 43 | throw new Error(data.error.message); 44 | } 45 | 46 | return parseJSONResponse(data.choices?.[0]?.message?.content); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { parseArgs } from "util"; 2 | 3 | import { providerCommand } from "./commands/provider.command"; 4 | import { configCommand } from "./commands/config.command"; 5 | import { apiKeyCommand } from "./commands/api-key.command"; 6 | import { versionCommand } from "./commands/version.command"; 7 | import { mainCommand } from "./commands/main.command"; 8 | import { helpCommand } from "./commands/help.command"; 9 | 10 | const { values } = parseArgs({ 11 | args: Bun.argv, 12 | strict: false, 13 | allowPositionals: true, 14 | options: { 15 | help: { 16 | type: "boolean", 17 | short: "h", 18 | description: "Get help for commands", 19 | }, 20 | version: { 21 | type: "boolean", 22 | short: "v", 23 | }, 24 | config: { 25 | type: "boolean", 26 | short: "c", 27 | }, 28 | key: { 29 | type: "string", 30 | short: "k", 31 | }, 32 | provider: { 33 | type: "string", 34 | short: "p", 35 | }, 36 | model: { 37 | type: "string", 38 | short: "m", 39 | }, 40 | }, 41 | }); 42 | 43 | if (values.version) { 44 | versionCommand(); 45 | } else if (values.help) { 46 | helpCommand(); 47 | } else if (values.config) { 48 | await configCommand(); 49 | } else if (values.key) { 50 | await apiKeyCommand(); 51 | } else if (values.provider) { 52 | await providerCommand(); 53 | } else { 54 | try { 55 | await mainCommand(); 56 | } catch (e) { 57 | console.error(e.message); 58 | process.exit(1); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # enable strict error handling 4 | set -euo pipefail 5 | 6 | PLATFORM=$(uname -ms) 7 | 8 | # determine the target platform 9 | case $PLATFORM in 10 | 'Darwin x86_64') 11 | TARGET=darwin-x64 12 | ;; 13 | 'Darwin arm64') 14 | TARGET=darwin-arm64 15 | ;; 16 | 'Linux aarch64' | 'Linux arm64') 17 | TARGET=linux-arm64 18 | ;; 19 | 'Linux x86_64' | *) 20 | TARGET=linux-x64 21 | ;; 22 | esac 23 | 24 | 25 | # create installation directory if it doesn't exist 26 | INSTALL_DIR="$HOME/.how/bin" 27 | 28 | mkdir -p "$INSTALL_DIR" 29 | 30 | 31 | # download binary from GitHub 32 | BINARY_URL="https://github.com/kynnyhsap/how/releases/latest/download/how-$TARGET" 33 | 34 | EXECUTABLE_NAME="how" 35 | 36 | curl -L "$BINARY_URL" -o "$INSTALL_DIR/$EXECUTABLE_NAME" 37 | 38 | # make the binary executable 39 | chmod +x "$INSTALL_DIR/$EXECUTABLE_NAME" 40 | 41 | # update PATH in shell profile 42 | 43 | if [[ "$SHELL" == */zsh ]]; then 44 | PROFILE="$HOME/.zshrc" 45 | elif [[ "$SHELL" == */bash ]]; then 46 | PROFILE="$HOME/.bashrc" 47 | else 48 | echo "Unsupported shell. Please add $INSTALL_DIR to your PATH manually." 49 | exit 1 50 | fi 51 | 52 | if ! grep -q "$INSTALL_DIR" "$PROFILE"; then 53 | echo -e "\n\n# how cli" >> "$PROFILE" 54 | echo "export PATH=\$PATH:$INSTALL_DIR" >> "$PROFILE" 55 | 56 | echo "Added $INSTALL_DIR to PATH in $PROFILE" 57 | else 58 | echo "PATH already includes $INSTALL_DIR" 59 | fi 60 | 61 | echo "Installation complete." 62 | 63 | echo "Please restart your terminal or run 'source $PROFILE' to use the command." 64 | 65 | echo "Run 'how --help' to get started." 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/providers/openai.provider.ts: -------------------------------------------------------------------------------- 1 | import { parseJSONResponse, ProviderSpec, ProviderType } from "./common"; 2 | import { Config } from "../config"; 3 | import { SYSTEM_PROMPT } from "../systemPrompt"; 4 | 5 | const OPEN_AI_URL = "https://api.openai.com/v1/chat/completions"; 6 | const DEFAULT_MODEL = "gpt-4o"; 7 | 8 | export const openaiProviderSpec: ProviderSpec = { 9 | type: ProviderType.OpenAI, 10 | name: "OpenAI", 11 | 12 | defaultModel: DEFAULT_MODEL, 13 | 14 | link: "https://openai.com", 15 | docsLink: "", 16 | apiKeyLink: "https://platform.openai.com/account/api-keys", 17 | 18 | apiKeyRequired: true, 19 | 20 | func: async (prompt: string, config: Config) => { 21 | if (!config.apiKey) { 22 | throw new Error( 23 | "API key isn't set. Please run `how -k ` to set OpenAI API key. You can get your API here https://platform.openai.com/account/api-keys", 24 | ); 25 | } 26 | 27 | const resp = await fetch(OPEN_AI_URL, { 28 | method: "POST", 29 | headers: { 30 | "Content-Type": "application/json", 31 | Authorization: `Bearer ${config.apiKey}`, 32 | }, 33 | body: JSON.stringify({ 34 | model: config.model ?? DEFAULT_MODEL, 35 | 36 | max_tokens: 1024, 37 | 38 | response_format: { 39 | type: "json_object", 40 | }, 41 | 42 | messages: [ 43 | { role: "system", content: SYSTEM_PROMPT }, 44 | { role: "user", content: prompt }, 45 | ], 46 | }), 47 | }); 48 | 49 | const data = await resp.json(); 50 | 51 | if (resp.status !== 200) { 52 | throw new Error(data.error.message); 53 | } 54 | 55 | return parseJSONResponse(data.choices?.[0]?.message?.content); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/providers/anthropic.provider.ts: -------------------------------------------------------------------------------- 1 | import { parseJSONResponse, ProviderSpec, ProviderType } from "./common"; 2 | import { Config } from "../config"; 3 | import { SYSTEM_PROMPT } from "../systemPrompt"; 4 | 5 | const ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages"; 6 | const DEFAULT_MODEL = "claude-3-5-sonnet-20240620"; 7 | 8 | export const anthropicProviderSpec: ProviderSpec = { 9 | type: ProviderType.Anthropic, 10 | name: "Anthropic", 11 | 12 | defaultModel: DEFAULT_MODEL, 13 | 14 | link: "https://anthropic.com/", 15 | docsLink: "https://docs.anthropic.com/", 16 | apiKeyLink: "https://console.anthropic.com/settings/keys", 17 | 18 | apiKeyRequired: true, 19 | 20 | func: async (prompt: string, config: Config) => { 21 | if (!config.apiKey) { 22 | throw new Error( 23 | "API key isn't set. Please run `how -k` to set Anthopic API key. You can get your API key here https://console.anthropic.com/settings/keys", 24 | ); 25 | } 26 | 27 | const resp = await fetch(ANTHROPIC_API_URL, { 28 | method: "POST", 29 | 30 | headers: { 31 | "Content-Type": "application/json", 32 | "anthropic-version": "2023-06-01", // https://docs.anthropic.com/en/api/versioning 33 | "x-api-key": config.apiKey, 34 | }, 35 | 36 | body: JSON.stringify({ 37 | model: config.model ?? DEFAULT_MODEL, 38 | 39 | system: SYSTEM_PROMPT, 40 | 41 | max_tokens: 1024, 42 | 43 | messages: [{ role: "user", content: prompt }], 44 | }), 45 | }); 46 | 47 | const data = await resp.json(); 48 | 49 | if (resp.status !== 200) { 50 | throw new Error(data.error.message); 51 | } 52 | 53 | return parseJSONResponse(data.content?.[0].text); 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /src/providers/groq.provider.ts: -------------------------------------------------------------------------------- 1 | import { parseJSONResponse, ProviderSpec, ProviderType } from "./common"; 2 | import { Config } from "../config"; 3 | import { SYSTEM_PROMPT } from "../systemPrompt"; 4 | 5 | const GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions"; 6 | const DEFAULT_MODEL = "llama3-8b-8192"; 7 | 8 | export const groqProviderSpec: ProviderSpec = { 9 | type: ProviderType.Groq, 10 | name: "Groq", 11 | 12 | defaultModel: DEFAULT_MODEL, 13 | 14 | link: "https://groq.com/", 15 | docsLink: "https://console.groq.com/docs/api-keys", 16 | apiKeyLink: "https://console.groq.com/docs/api-keys", 17 | 18 | apiKeyRequired: true, 19 | 20 | func: async (prompt: string, config: Config) => { 21 | if (!config.apiKey) { 22 | throw new Error( 23 | "API key isn't set. Please run `how -k` to set Groq API key. You can get an API key here https://console.groq.com/docs/api-keys", 24 | ); 25 | } 26 | 27 | const resp = await fetch(GROQ_API_URL, { 28 | method: "POST", 29 | headers: { 30 | "Content-Type": "application/json", 31 | Authorization: `Bearer ${config.apiKey}`, 32 | }, 33 | body: JSON.stringify({ 34 | model: config.model ?? DEFAULT_MODEL, 35 | 36 | max_tokens: 1024, 37 | 38 | response_format: { 39 | type: "json_object", 40 | }, 41 | 42 | messages: [ 43 | { role: "system", content: SYSTEM_PROMPT }, 44 | { role: "user", content: prompt }, 45 | ], 46 | }), 47 | }); 48 | 49 | const data = await resp.json(); 50 | 51 | if (resp.status !== 200) { 52 | throw new Error(data.error.message); 53 | } 54 | 55 | return parseJSONResponse(data.choices?.[0]?.message?.content); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # how-cli 2 | 3 | Ask your terminal (AI) about cli commands 4 | 5 | 6 | 7 | Yes, it's just an LLM wrapper. It saves me a lot of time. It will for you too. 8 | 9 | ## Installation 10 | 11 | Run this command to install `how`: 12 | 13 | ```bash 14 | curl -fsSL https://raw.githubusercontent.com/kynnyhsap/how/main/scripts/install.sh | bash 15 | ``` 16 | 17 | > This will fetch and run the script located in [./scripts/install.sh](/scripts/install.sh). 18 | 19 | ## Usage 20 | 21 | Make sure to set **api key** first with `--key` flag: 22 | 23 | ```bash 24 | how --key 25 | ``` 26 | 27 | 28 | 29 | > Default provider is `openai`. You can change it with `--provider` flag. See [providers](#providers) below for more info. 30 | 31 | Now you can prompt and adk `how` about cli commands: 32 | 33 | ```bash 34 | how to [prompt...] 35 | ``` 36 | 37 | ### Providers 38 | 39 | The default provider is `openai`, but you can change it with `--provider` flag: 40 | 41 | ```bash 42 | how --provider 43 | ``` 44 | 45 | 46 | 47 | Changing provider means you also need to update the api key with `--key` flag. 48 | 49 | Supported providers: 50 | 51 | - [x] `openai` - [OpenAPI GPT](https://chatgpt.com/) models (default) 52 | - [x] `anthropic` - [Anthropic](https://claude.ai/) models 53 | - [x] `groq` - [Groq](https://groq.com/) models 54 | - [x] `ollama` - [Ollama](https://ollama.com/) models, on-device inference, no api key required 55 | - [ ] `custom` - Custom provider script 56 | 57 | ### Config 58 | 59 | Api key and provider info is stored in `~/.how/config.json`. This config is also used for other options. You can view it with: 60 | 61 | ```bash 62 | how --config 63 | ``` 64 | 65 | ### Help 66 | 67 | To see all available commands and options: 68 | 69 | ```bash 70 | how --help 71 | ``` 72 | 73 | ## Examples 74 | 75 | ```bash 76 | how to create a git branch 77 | ``` 78 | 79 | ```bash 80 | how to convert video to gif with ffmpeg 81 | ``` 82 | 83 | ```bash 84 | how to compile a c file 85 | ``` 86 | 87 | 88 | 89 | ## Development 90 | 91 | > You will need [bun](https://bun.sh/) for this. 92 | 93 | To install dependencies: 94 | 95 | ```bash 96 | bun install 97 | ``` 98 | 99 | To run from source: 100 | 101 | ```bash 102 | bun how [arguments...] 103 | ``` 104 | 105 | To compile executable from source: 106 | 107 | ```bash 108 | bun compile-dev 109 | ``` 110 | 111 | ## Benchmarks 112 | 113 | What I observed using differnet models: 114 | 115 | - `groq` (llama3-8b) is the fastest, **average response time is under half a second**. 116 | - `ollama` (llama3-8b) is the slowest, **average response time is about 3 seconds**. And also there is a cold start that takes about 8 seconds (I guess the model loads itself into RAM) 117 | - `openai` (gpt-4o) and `anthropic` (claude-3-5-sonnet) are in between, **average response time is about 2 seconds**. They also seem to have better results than llama3-8b. 118 | 119 | ## Cross-Compile 120 | 121 | Thre is a `compile.sh` script to cross-compile for multiple platforms. You can run it with: 122 | 123 | ```bash 124 | ./scripts/compile.sh 125 | ``` 126 | 127 | ## Releases 128 | 129 | I do releases when I feel like it. There is a script to automate in in `scripts/release.sh`. 130 | 131 | Later I will add command to upgrade cli to latest version. 132 | 133 | ## License 134 | 135 | MIT, you can go nuts with it. 136 | --------------------------------------------------------------------------------