From 78fe5c8470a0dd82ec4fb4f0ba1415b55aa1b790 Mon Sep 17 00:00:00 2001 From: Kewin Wereszczynski Date: Wed, 21 Feb 2024 13:41:10 +0100 Subject: [PATCH 1/3] feat: add init command --- package.json | 4 +- src/commands/init/index.ts | 93 ++++++++++++++++++++++++++++++++++++ src/commands/prompt/index.ts | 17 ++++++- src/config-file.ts | 17 +++---- src/output.ts | 8 ++++ yarn.lock | 90 ++++++---------------------------- 6 files changed, 138 insertions(+), 91 deletions(-) create mode 100644 src/commands/init/index.ts diff --git a/package.json b/package.json index f9bf58b..a784bc8 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "dependencies": { "chalk": "^4.1.2", "dotenv": "^16.3.1", - "inquirer": "^8.2.4", "openai": "^4.24.1", + "prompts": "^2.4.2", "yargs": "^17.7.2", "zod": "^3.22.4" }, @@ -54,8 +54,8 @@ "@commitlint/config-conventional": "^17.0.2", "@evilmartians/lefthook": "^1.5.0", "@release-it/conventional-changelog": "^5.0.0", - "@types/inquirer": "^9.0.7", "@types/jest": "^28.1.2", + "@types/prompts": "^2.4.9", "commitlint": "^17.0.2", "del-cli": "^5.0.0", "eslint": "^8.4.1", diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts new file mode 100644 index 0000000..704e6c3 --- /dev/null +++ b/src/commands/init/index.ts @@ -0,0 +1,93 @@ +import prompts from 'prompts'; +import { checkIfConfigExists, createConfigFile } from '../../config-file'; +import * as output from '../../output'; + +export async function init() { + try { + await initInternal(); + } catch (error) { + output.clearLine(); + output.outputError(error); + process.exit(1); + } +} + +async function initInternal() { + const configExists = checkIfConfigExists(); + + if (configExists) { + const response = await prompts({ + type: 'confirm', + message: 'Config found, do you want to re-initialize it?', + name: 'reinitialize', + }); + + if (!response.reinitialize) { + output.outputBold('Cancelling initialization'); + return; + } + } + + output.outputBold("Welcome to AI CLI. Let's set you up quickly."); + + const response = await prompts([ + { + type: 'select', + name: 'provider', + message: 'Which inference provider would you like to set up:', + choices: [ + { title: 'OpenAI', value: 'openAi' }, + { title: 'Perplexity', value: 'perplexity' }, + ], + initial: 0, + hint: '', + }, + { + type: 'confirm', + message: 'Do you already have OpenAI API key?', + name: 'hasApiKey', + }, + { + type: (prev) => (prev ? 'password' : null), + name: 'apiKey', + message: 'Paste your OpenAI key here:', + mask: '', + validate: (value) => (value === '' ? 'Api key cannot be an empty string' : true), + }, + ]); + + if (!response.hasApiKey) { + output.outputDefault('You can get it from here:'); + switch (response.provider) { + case 'perplexity': { + output.outputDefault('https://www.perplexity.ai/settings/api'); + return; + } + case 'openAi': { + output.outputDefault('https://www.platform.openai.com/api-keys'); + return; + } + } + return; + } + + await createConfigFile({ + providers: { + [response.provider]: { + apiKey: response.apiKey, + }, + }, + }); + + output.outputBold( + "\nI have written your settings into '~/.airc.json` file. You can now start using AI CLI.\n" + ); + output.outputBold('For one of questions ask the prompt as command params'); + output.outputDefault('$ ai "Tell me a joke" \n'); + + output.outputBold('For interactive session use `-i` (or "--interactive") option. '); + output.outputDefault('$ ai -i "Tell me an interesting fact about JavaScript"\n'); + + output.outputBold("Or just start 'ai' without any parameters."); + output.outputDefault('$ ai \n'); +} diff --git a/src/commands/prompt/index.ts b/src/commands/prompt/index.ts index 080e175..15f836b 100644 --- a/src/commands/prompt/index.ts +++ b/src/commands/prompt/index.ts @@ -1,9 +1,10 @@ import type { CommandModule } from 'yargs'; -import { parseConfigFile } from '../../config-file'; +import { checkIfConfigExists, parseConfigFile } from '../../config-file'; import { type Message } from '../../inference'; import { inputLine } from '../../input'; import * as output from '../../output'; import { providers, providerOptions, resolveProviderName } from '../../providers'; +import { init } from '../init'; import { processCommand } from './commands'; export interface PromptOptions { @@ -48,7 +49,13 @@ export const command: CommandModule<{}, PromptOptions> = { default: false, describe: 'Verbose output', }), - handler: (args) => run(args._.join(' '), args), + handler: (args) => { + if (args._[0] === 'init') { + return init(); + } else { + return run(args._.join(' '), args); + } + }, }; export async function run(initialPrompt: string, options: PromptOptions) { @@ -66,6 +73,12 @@ async function runInternal(initialPrompt: string, options: PromptOptions) { output.setVerbose(true); } + const configExists = await checkIfConfigExists(); + if (!configExists) { + await init(); + return; + } + const configFile = await parseConfigFile(); output.outputVerbose(`Config: ${JSON.stringify(configFile, filterOutApiKey, 2)}`); diff --git a/src/config-file.ts b/src/config-file.ts index aaf9207..cbe0545 100644 --- a/src/config-file.ts +++ b/src/config-file.ts @@ -37,8 +37,6 @@ export type ConfigFile = z.infer; export async function parseConfigFile() { const configPath = path.join(os.homedir(), CONFIG_FILENAME); - await writeEmptyConfigFileIfNeeded(); - const content = await fs.promises.readFile(configPath); const json = JSON.parse(content.toString()); @@ -51,15 +49,12 @@ export async function parseConfigFile() { return typedConfig; } -const emptyConfigContents = { - providers: {}, -}; - -export async function writeEmptyConfigFileIfNeeded() { +export async function createConfigFile(configContents: ConfigFile) { const configPath = path.join(os.homedir(), CONFIG_FILENAME); - if (fs.existsSync(configPath)) { - return; - } + await fs.promises.writeFile(configPath, JSON.stringify(configContents, null, 2) + '\n'); +} - await fs.promises.writeFile(configPath, JSON.stringify(emptyConfigContents, null, 2) + '\n'); +export function checkIfConfigExists() { + const configPath = path.join(os.homedir(), CONFIG_FILENAME); + return fs.existsSync(configPath); } diff --git a/src/output.ts b/src/output.ts index 4837785..2d4e537 100644 --- a/src/output.ts +++ b/src/output.ts @@ -33,6 +33,14 @@ export function outputInfo(message: string, ...args: unknown[]) { console.log(chalk.dim(message, ...args)); } +export function outputBold(message: string, ...args: unknown[]) { + console.log(chalk.bold(message, ...args)); +} + +export function outputDefault(message: string, ...args: unknown[]) { + console.log(message, ...args); +} + export function outputError(error: unknown, ...args: unknown[]) { const message = extractErrorMessage(error); if (error === message) { diff --git a/yarn.lock b/yarn.lock index bdb63dd..0224adc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1531,8 +1531,8 @@ __metadata: "@commitlint/config-conventional": ^17.0.2 "@evilmartians/lefthook": ^1.5.0 "@release-it/conventional-changelog": ^5.0.0 - "@types/inquirer": ^9.0.7 "@types/jest": ^28.1.2 + "@types/prompts": ^2.4.9 chalk: ^4.1.2 commitlint: ^17.0.2 del-cli: ^5.0.0 @@ -1540,10 +1540,10 @@ __metadata: eslint: ^8.4.1 eslint-config-prettier: ^8.5.0 eslint-plugin-prettier: ^4.0.0 - inquirer: ^8.2.4 jest: ^28.1.1 openai: ^4.24.1 prettier: ^2.0.5 + prompts: ^2.4.2 react-native-builder-bob: ^0.20.0 release-it: ^15.0.0 typescript: ^5.0.2 @@ -2580,16 +2580,6 @@ __metadata: languageName: node linkType: hard -"@types/inquirer@npm:^9.0.7": - version: 9.0.7 - resolution: "@types/inquirer@npm:9.0.7" - dependencies: - "@types/through": "*" - rxjs: ^7.2.0 - checksum: c14c7a52797606a08ca7450d9263c01518b30e0b7610d0817fb530bc4fae7b3ffdcc9d50bb9f7befc2b220bb54195507cc423de3c8f320bbe9f7e5e8881a0226 - languageName: node - linkType: hard - "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -2702,6 +2692,16 @@ __metadata: languageName: node linkType: hard +"@types/prompts@npm:^2.4.9": + version: 2.4.9 + resolution: "@types/prompts@npm:2.4.9" + dependencies: + "@types/node": "*" + kleur: ^3.0.3 + checksum: 69b8372f4c790b45fea16a46ff8d1bcc71b14579481776b67bd6263637118a7ecb1f12e1311506c29fadc81bf618dc64f1a91f903cfd5be67a0455a227b3e462 + languageName: node + linkType: hard + "@types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": version: 7.5.6 resolution: "@types/semver@npm:7.5.6" @@ -2716,15 +2716,6 @@ __metadata: languageName: node linkType: hard -"@types/through@npm:*": - version: 0.0.33 - resolution: "@types/through@npm:0.0.33" - dependencies: - "@types/node": "*" - checksum: fd0b73f873a64ed5366d1d757c42e5dbbb2201002667c8958eda7ca02fff09d73de91360572db465ee00240c32d50c6039ea736d8eca374300f9664f93e8da39 - languageName: node - linkType: hard - "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -3740,7 +3731,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": +"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -3847,13 +3838,6 @@ __metadata: languageName: node linkType: hard -"cli-width@npm:^3.0.0": - version: 3.0.0 - resolution: "cli-width@npm:3.0.0" - checksum: 4c94af3769367a70e11ed69aa6095f1c600c0ff510f3921ab4045af961820d57c0233acfa8b6396037391f31b4c397e1f614d234294f979ff61430a6c166c3f6 - languageName: node - linkType: hard - "cli-width@npm:^4.0.0": version: 4.1.0 resolution: "cli-width@npm:4.1.0" @@ -5492,15 +5476,6 @@ __metadata: languageName: node linkType: hard -"figures@npm:^3.0.0": - version: 3.2.0 - resolution: "figures@npm:3.2.0" - dependencies: - escape-string-regexp: ^1.0.5 - checksum: 85a6ad29e9aca80b49b817e7c89ecc4716ff14e3779d9835af554db91bac41c0f289c418923519392a1e582b4d10482ad282021330cd045bb7b80c84152f2a2b - languageName: node - linkType: hard - "figures@npm:^5.0.0": version: 5.0.0 resolution: "figures@npm:5.0.0" @@ -6404,29 +6379,6 @@ __metadata: languageName: node linkType: hard -"inquirer@npm:^8.2.4": - version: 8.2.6 - resolution: "inquirer@npm:8.2.6" - dependencies: - ansi-escapes: ^4.2.1 - chalk: ^4.1.1 - cli-cursor: ^3.1.0 - cli-width: ^3.0.0 - external-editor: ^3.0.3 - figures: ^3.0.0 - lodash: ^4.17.21 - mute-stream: 0.0.8 - ora: ^5.4.1 - run-async: ^2.4.0 - rxjs: ^7.5.5 - string-width: ^4.1.0 - strip-ansi: ^6.0.0 - through: ^2.3.6 - wrap-ansi: ^6.0.1 - checksum: 387ffb0a513559cc7414eb42c57556a60e302f820d6960e89d376d092e257a919961cd485a1b4de693dbb5c0de8bc58320bfd6247dfd827a873aa82a4215a240 - languageName: node - linkType: hard - "internal-slot@npm:^1.0.4, internal-slot@npm:^1.0.5": version: 1.0.6 resolution: "internal-slot@npm:1.0.6" @@ -8353,13 +8305,6 @@ __metadata: languageName: node linkType: hard -"mute-stream@npm:0.0.8": - version: 0.0.8 - resolution: "mute-stream@npm:0.0.8" - checksum: ff48d251fc3f827e5b1206cda0ffdaec885e56057ee86a3155e1951bc940fd5f33531774b1cc8414d7668c10a8907f863f6561875ee6e8768931a62121a531a1 - languageName: node - linkType: hard - "mute-stream@npm:1.0.0": version: 1.0.0 resolution: "mute-stream@npm:1.0.0" @@ -9759,13 +9704,6 @@ __metadata: languageName: node linkType: hard -"run-async@npm:^2.4.0": - version: 2.4.1 - resolution: "run-async@npm:2.4.1" - checksum: a2c88aa15df176f091a2878eb840e68d0bdee319d8d97bbb89112223259cebecb94bc0defd735662b83c2f7a30bed8cddb7d1674eb48ae7322dc602b22d03797 - languageName: node - linkType: hard - "run-async@npm:^3.0.0": version: 3.0.0 resolution: "run-async@npm:3.0.0" @@ -9782,7 +9720,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.2.0, rxjs@npm:^7.5.5, rxjs@npm:^7.8.1": +"rxjs@npm:^7.8.1": version: 7.8.1 resolution: "rxjs@npm:7.8.1" dependencies: From 398af017513013869cb8a47e99d0b3a1806e0047 Mon Sep 17 00:00:00 2001 From: Kewin Wereszczynski Date: Thu, 22 Feb 2024 13:15:31 +0100 Subject: [PATCH 2/3] chore: cr fixes and small providers refaktor --- src/bin.ts | 3 +- src/commands/init/index.ts | 101 +++-------------------------------- src/commands/init/init.ts | 87 ++++++++++++++++++++++++++++++ src/commands/prompt/index.ts | 23 +++----- src/config-file.ts | 9 +++- src/providers/index.ts | 40 +++++++++----- src/providers/openAi.ts | 36 ++++++++----- src/providers/perplexity.ts | 38 +++++++------ 8 files changed, 185 insertions(+), 152 deletions(-) create mode 100644 src/commands/init/init.ts diff --git a/src/bin.ts b/src/bin.ts index 90525cf..393fedb 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -3,5 +3,6 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { command as prompt } from './commands/prompt'; +import { command as init } from './commands/init'; -void yargs(hideBin(process.argv)).command(prompt).help().demandCommand(1).parse(); +void yargs(hideBin(process.argv)).command(init).command(prompt).help().demandCommand(1).parse(); diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index 704e6c3..9b0525e 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -1,93 +1,8 @@ -import prompts from 'prompts'; -import { checkIfConfigExists, createConfigFile } from '../../config-file'; -import * as output from '../../output'; - -export async function init() { - try { - await initInternal(); - } catch (error) { - output.clearLine(); - output.outputError(error); - process.exit(1); - } -} - -async function initInternal() { - const configExists = checkIfConfigExists(); - - if (configExists) { - const response = await prompts({ - type: 'confirm', - message: 'Config found, do you want to re-initialize it?', - name: 'reinitialize', - }); - - if (!response.reinitialize) { - output.outputBold('Cancelling initialization'); - return; - } - } - - output.outputBold("Welcome to AI CLI. Let's set you up quickly."); - - const response = await prompts([ - { - type: 'select', - name: 'provider', - message: 'Which inference provider would you like to set up:', - choices: [ - { title: 'OpenAI', value: 'openAi' }, - { title: 'Perplexity', value: 'perplexity' }, - ], - initial: 0, - hint: '', - }, - { - type: 'confirm', - message: 'Do you already have OpenAI API key?', - name: 'hasApiKey', - }, - { - type: (prev) => (prev ? 'password' : null), - name: 'apiKey', - message: 'Paste your OpenAI key here:', - mask: '', - validate: (value) => (value === '' ? 'Api key cannot be an empty string' : true), - }, - ]); - - if (!response.hasApiKey) { - output.outputDefault('You can get it from here:'); - switch (response.provider) { - case 'perplexity': { - output.outputDefault('https://www.perplexity.ai/settings/api'); - return; - } - case 'openAi': { - output.outputDefault('https://www.platform.openai.com/api-keys'); - return; - } - } - return; - } - - await createConfigFile({ - providers: { - [response.provider]: { - apiKey: response.apiKey, - }, - }, - }); - - output.outputBold( - "\nI have written your settings into '~/.airc.json` file. You can now start using AI CLI.\n" - ); - output.outputBold('For one of questions ask the prompt as command params'); - output.outputDefault('$ ai "Tell me a joke" \n'); - - output.outputBold('For interactive session use `-i` (or "--interactive") option. '); - output.outputDefault('$ ai -i "Tell me an interesting fact about JavaScript"\n'); - - output.outputBold("Or just start 'ai' without any parameters."); - output.outputDefault('$ ai \n'); -} +import type { CommandModule } from 'yargs'; +import { init } from './init'; + +export const command: CommandModule<{}> = { + command: ['$0', 'init'], + describe: 'initialize the AI Cli', + handler: () => init(), +}; diff --git a/src/commands/init/init.ts b/src/commands/init/init.ts new file mode 100644 index 0000000..9a5370c --- /dev/null +++ b/src/commands/init/init.ts @@ -0,0 +1,87 @@ +import prompts from 'prompts'; +import { checkIfConfigExists, createConfigFile } from '../../config-file'; +import * as output from '../../output'; +import { resolveProvider } from '../../providers'; + +export async function init() { + try { + await initInternal(); + } catch (error) { + output.clearLine(); + output.outputError(error); + process.exit(1); + } +} + +async function initInternal() { + const configExists = checkIfConfigExists(); + + if (configExists) { + const response = await prompts({ + type: 'confirm', + message: 'Config found, do you want to re-initialize it?', + name: 'reinitialize', + }); + + if (!response.reinitialize) { + output.outputBold('Cancelling initialization'); + return; + } + } + + output.outputBold("Welcome to AI CLI. Let's set you up quickly."); + + const response = await prompts([ + { + type: 'select', + name: 'provider', + message: 'Which inference provider would you like to set up:', + choices: [ + { title: 'OpenAI', value: 'openai' }, + { title: 'Perplexity', value: 'perplexity' }, + ], + initial: 0, + hint: '', + }, + { + type: 'confirm', + message: (_, { provider }) => + `Do you already have ${resolveProvider(provider).label} API key?`, + name: 'hasApiKey', + }, + { + type: (prev) => (prev ? 'password' : null), + name: 'apiKey', + message: (_, { provider }) => `Paste ${resolveProvider(provider).label} API key here:`, + mask: '', + validate: (value) => (value === '' ? 'API key cannot be an empty string' : true), + }, + ]); + + if (!response.hasApiKey) { + const provider = resolveProvider(response.provider); + output.outputDefault(`You can get API key for ${provider.label} from here:`); + output.outputDefault(provider.apiKeyUrl); + return; + } + + await createConfigFile({ + providers: { + [response.provider]: { + apiKey: response.apiKey, + }, + }, + }); + + output.outputBold( + "\nI have written your settings into '~/.airc.json` file. You can now start using AI CLI.\n" + ); + output.outputBold('For one of questions ask the prompt as command params'); + output.outputDefault('$ ai "Tell me a joke" \n'); + + output.outputBold('For interactive session use `-i` (or "--interactive") option. '); + output.outputDefault('$ ai -i "Tell me an interesting fact about JavaScript"\n'); + + output.outputBold("Or just start 'ai' without any parameters."); + output.outputDefault('$ ai \n'); +} diff --git a/src/commands/prompt/index.ts b/src/commands/prompt/index.ts index 15f836b..2fec112 100644 --- a/src/commands/prompt/index.ts +++ b/src/commands/prompt/index.ts @@ -3,8 +3,8 @@ import { checkIfConfigExists, parseConfigFile } from '../../config-file'; import { type Message } from '../../inference'; import { inputLine } from '../../input'; import * as output from '../../output'; -import { providers, providerOptions, resolveProviderName } from '../../providers'; -import { init } from '../init'; +import { providerOptions, resolveProvider } from '../../providers'; +import { init } from '../init/init'; import { processCommand } from './commands'; export interface PromptOptions { @@ -49,13 +49,7 @@ export const command: CommandModule<{}, PromptOptions> = { default: false, describe: 'Verbose output', }), - handler: (args) => { - if (args._[0] === 'init') { - return init(); - } else { - return run(args._.join(' '), args); - } - }, + handler: (args) => run(args._.join(' '), args), }; export async function run(initialPrompt: string, options: PromptOptions) { @@ -82,13 +76,12 @@ async function runInternal(initialPrompt: string, options: PromptOptions) { const configFile = await parseConfigFile(); output.outputVerbose(`Config: ${JSON.stringify(configFile, filterOutApiKey, 2)}`); - const providerName = resolveProviderName(options.provider, configFile); - const provider = providers[providerName]; - output.outputVerbose(`Using provider: ${providerName}`); + const provider = resolveProvider(options.provider, configFile); + output.outputVerbose(`Using provider: ${provider.label}`); - const initialConfig = configFile.providers[providerName]; + const initialConfig = configFile.providers[provider.name]; if (!initialConfig) { - throw new Error(`Provider config not found: ${providerName}.`); + throw new Error(`Provider config not found: ${provider.name}.`); } const config = { @@ -127,7 +120,7 @@ async function runInternal(initialPrompt: string, options: PromptOptions) { // eslint-disable-next-line no-constant-condition while (true) { const userPrompt = await inputLine('me: '); - const isCommand = processCommand(userPrompt, { messages, providerName, config }); + const isCommand = processCommand(userPrompt, { messages, providerName: provider.name, config }); if (isCommand) { continue; } diff --git a/src/config-file.ts b/src/config-file.ts index cbe0545..fc4a16c 100644 --- a/src/config-file.ts +++ b/src/config-file.ts @@ -9,7 +9,8 @@ import { } from './default-config'; import * as output from './output'; -const CONFIG_FILENAME = '.airc'; +const LEGACY_CONFIG_FILENAME = '.airc'; +const CONFIG_FILENAME = '.airc.json'; const ProvidersSchema = z.object({ openAi: z.optional( @@ -55,6 +56,12 @@ export async function createConfigFile(configContents: ConfigFile) { } export function checkIfConfigExists() { + const legacyConfigPath = path.join(os.homedir(), LEGACY_CONFIG_FILENAME); const configPath = path.join(os.homedir(), CONFIG_FILENAME); + + if (fs.existsSync(legacyConfigPath)) { + fs.renameSync(legacyConfigPath, configPath); + } + return fs.existsSync(configPath); } diff --git a/src/providers/index.ts b/src/providers/index.ts index e182367..e8cc2af 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1,24 +1,33 @@ import type { ConfigFile } from '../config-file'; -import * as openAi from './openAi'; -import * as perplexity from './perplexity'; +import type { Message } from '../inference'; +import type { ProviderConfig } from './config'; +import openAi from './openAi'; +import perplexity from './perplexity'; export const providerNames = ['openAi', 'perplexity'] as const; export type ProviderName = (typeof providerNames)[number]; -export const providers = { +export type Provider = { + name: ProviderName; + label: string; + apiKeyUrl: string; + getChatCompletion: (config: ProviderConfig, messages: Message[]) => any; +}; + +const providers = { openAi, perplexity, }; -const providerOptionMapping: Record = { - openai: 'openAi', - perplexity: 'perplexity', - pplx: 'perplexity', +const providerOptionMapping: Record = { + openai: openAi, + perplexity: perplexity, + pplx: perplexity, }; export const providerOptions = Object.keys(providerOptionMapping); -export function resolveProviderName(option: string | undefined, config: ConfigFile): ProviderName { +export function resolveProvider(option: string | undefined, config?: ConfigFile): Provider { if (option != null) { const provider = providerOptionMapping[option]; if (!provider) { @@ -28,10 +37,15 @@ export function resolveProviderName(option: string | undefined, config: ConfigFi return provider; } - const providers = Object.keys(config.providers) as ProviderName[]; - if (providers.length === 0) { - throw new Error('No providers found in ~/.airc file.'); + if (config) { + const providerNames = Object.keys(config.providers) as ProviderName[]; + const providerName = providerNames ? providerNames[0] : undefined; + if (providerName) { + return providers[providerName]!; + } else { + throw new Error('No providers found in ~/.airc file.'); + } + } else { + throw new Error('No config file found.'); } - - return providers[0]!; } diff --git a/src/providers/openAi.ts b/src/providers/openAi.ts index 66aac29..e153083 100644 --- a/src/providers/openAi.ts +++ b/src/providers/openAi.ts @@ -1,21 +1,29 @@ import OpenAI from 'openai'; import { type Message } from '../inference'; import type { ProviderConfig } from './config'; +import type { Provider } from '.'; -export async function getChatCompletion(config: ProviderConfig, messages: Message[]) { - const openai = new OpenAI({ - apiKey: config.apiKey, - }); +const OpenAi: Provider = { + label: 'OpenAI', + name: 'openAi', + apiKeyUrl: 'https://www.platform.openai.com/api-keys', + getChatCompletion: async (config: ProviderConfig, messages: Message[]) => { + const openai = new OpenAI({ + apiKey: config.apiKey, + }); - const systemMessage: Message = { - role: 'system', - content: config.systemPrompt, - }; + const systemMessage: Message = { + role: 'system', + content: config.systemPrompt, + }; - const response = await openai.chat.completions.create({ - messages: [systemMessage, ...messages], - model: config.model, - }); + const response = await openai.chat.completions.create({ + messages: [systemMessage, ...messages], + model: config.model, + }); - return [response.choices[0]?.message.content ?? null, response] as const; -} + return [response.choices[0]?.message.content ?? null, response] as const; + }, +}; + +export default OpenAi; diff --git a/src/providers/perplexity.ts b/src/providers/perplexity.ts index f4ed06a..f8eebfd 100644 --- a/src/providers/perplexity.ts +++ b/src/providers/perplexity.ts @@ -1,22 +1,30 @@ import OpenAI from 'openai'; import { type Message } from '../inference'; import type { ProviderConfig } from './config'; +import type { Provider } from '.'; -export async function getChatCompletion(config: ProviderConfig, messages: Message[]) { - const perplexity = new OpenAI({ - apiKey: config.apiKey, - baseURL: 'https://api.perplexity.ai', - }); +const Perplexity: Provider = { + label: 'Perplexity', + name: 'perplexity', + apiKeyUrl: 'https://www.perplexity.ai/settings/api', + getChatCompletion: async (config: ProviderConfig, messages: Message[]) => { + const perplexity = new OpenAI({ + apiKey: config.apiKey, + baseURL: 'https://api.perplexity.ai', + }); - const systemMessage: Message = { - role: 'system', - content: config.systemPrompt, - }; + const systemMessage: Message = { + role: 'system', + content: config.systemPrompt, + }; - const response = await perplexity.chat.completions.create({ - messages: [systemMessage, ...messages], - model: config.model, - }); + const response = await perplexity.chat.completions.create({ + messages: [systemMessage, ...messages], + model: config.model, + }); - return [response.choices[0]?.message.content ?? null, response] as const; -} + return [response.choices[0]?.message.content ?? null, response] as const; + }, +}; + +export default Perplexity; From 89310ee6a31ed8eb212ea694a02bf2a37f4b45cf Mon Sep 17 00:00:00 2001 From: Kewin Wereszczynski Date: Thu, 22 Feb 2024 14:57:36 +0100 Subject: [PATCH 3/3] chore: cr fixes --- src/commands/init/index.ts | 2 +- src/commands/init/init.ts | 10 +++++----- src/config-file.ts | 2 +- src/providers/index.ts | 19 ++++++++++--------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index 9b0525e..9ba5895 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -3,6 +3,6 @@ import { init } from './init'; export const command: CommandModule<{}> = { command: ['$0', 'init'], - describe: 'initialize the AI Cli', + describe: 'User-friendly config setup', handler: () => init(), }; diff --git a/src/commands/init/init.ts b/src/commands/init/init.ts index 9a5370c..1a19be0 100644 --- a/src/commands/init/init.ts +++ b/src/commands/init/init.ts @@ -35,7 +35,7 @@ async function initInternal() { { type: 'select', name: 'provider', - message: 'Which inference provider would you like to set up:', + message: 'Which inference provider would you like to use:', choices: [ { title: 'OpenAI', value: 'openai' }, { title: 'Perplexity', value: 'perplexity' }, @@ -60,7 +60,7 @@ async function initInternal() { if (!response.hasApiKey) { const provider = resolveProvider(response.provider); - output.outputDefault(`You can get API key for ${provider.label} from here:`); + output.outputDefault(`You can get your ${provider.label} API key here:`); output.outputDefault(provider.apiKeyUrl); return; } @@ -76,12 +76,12 @@ async function initInternal() { output.outputBold( "\nI have written your settings into '~/.airc.json` file. You can now start using AI CLI.\n" ); - output.outputBold('For one of questions ask the prompt as command params'); + output.outputBold('For a single question and answer just pass the prompt as param'); output.outputDefault('$ ai "Tell me a joke" \n'); - output.outputBold('For interactive session use `-i` (or "--interactive") option. '); + output.outputBold('For interactive session use "-i" (or "--interactive") option. '); output.outputDefault('$ ai -i "Tell me an interesting fact about JavaScript"\n'); - output.outputBold("Or just start 'ai' without any parameters."); + output.outputBold('or just start "ai" without any params.'); output.outputDefault('$ ai \n'); } diff --git a/src/config-file.ts b/src/config-file.ts index fc4a16c..f1b413c 100644 --- a/src/config-file.ts +++ b/src/config-file.ts @@ -59,7 +59,7 @@ export function checkIfConfigExists() { const legacyConfigPath = path.join(os.homedir(), LEGACY_CONFIG_FILENAME); const configPath = path.join(os.homedir(), CONFIG_FILENAME); - if (fs.existsSync(legacyConfigPath)) { + if (fs.existsSync(legacyConfigPath) && !fs.existsSync(configPath)) { fs.renameSync(legacyConfigPath, configPath); } diff --git a/src/providers/index.ts b/src/providers/index.ts index e8cc2af..41a2c1f 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -37,15 +37,16 @@ export function resolveProvider(option: string | undefined, config?: ConfigFile) return provider; } - if (config) { - const providerNames = Object.keys(config.providers) as ProviderName[]; - const providerName = providerNames ? providerNames[0] : undefined; - if (providerName) { - return providers[providerName]!; - } else { - throw new Error('No providers found in ~/.airc file.'); - } - } else { + if (!config) { throw new Error('No config file found.'); } + + const providerNames = Object.keys(config.providers) as ProviderName[]; + const providerName = providerNames ? providerNames[0] : undefined; + + if (!providerName) { + throw new Error('No providers found in ~/.airc file.'); + } + + return providers[providerName]!; }