env-ai
Version:
AI Assistant for Your Local Environment
1,522 lines (1,435 loc) • 48.7 kB
JavaScript
import ollama from 'ollama';
import { OllamaEmbedding, Ollama } from '@llamaindex/ollama';
import { JSONReader } from '@llamaindex/readers/json';
import { Settings, Document, VectorStoreIndex, ContextChatEngine } from 'llamaindex';
import { deserialize as deserialize$1 } from '@structium/toml';
import { deserialize } from '@structium/yaml';
import { globby } from 'globby';
import { readFile, writeFile, stat } from 'node:fs/promises';
import { join, isAbsolute, basename, extname, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { p as process } from '../shared/env-ai.D0G3pQvO.mjs';
import { z } from 'zod';
import { b as blue, r as red, g as grayBright, m as magenta, c } from '../shared/env-ai.CeXlYJPG.mjs';
import * as prompts$1 from '@clack/prompts';
import { block } from '@clack/core';
import isUnicodeSupported from 'is-unicode-supported';
import process$1 from 'node:process';
import { cursor, erase } from 'sisteransi';
import 'node:util';
function _mergeNamespaces(n, m) {
for (var i = 0; i < m.length; i++) {
const e = m[i];
if (typeof e !== 'string' && !Array.isArray(e)) { for (const k in e) {
if (k !== 'default' && !(k in n)) {
n[k] = e[k];
}
} }
}
return n;
}
const name = "env-ai";
const version = "0.4.5";
const description = "AI Assistant for Your Local Environment";
const bugs = {
url: "https://github.com/pigeonposse/env-ai/issues"};
const projectDesc = description;
const bugsUrl = bugs.url;
const projectName = name;
const overwrite = {
always: "always",
ask: "ask",
last: "last"
};
const theme = {
custom: "custom",
explain: "explain",
docs: "docs",
fix: "fix",
performance: "performance",
refactor: "refactor",
test: "tests"
};
const themeDesc = {
custom: "Adapt the system to your needs.",
explain: "Provide clear explanations of code and functionality.",
docs: "Generate user guides and technical documentation.",
fix: "Identify and resolve bugs for accurate output.",
performance: "Improve the efficiency of your code and reduce response times.",
refactor: "Improve code structure for better readability and maintainability.",
test: "Create tests and ensure quality."
};
const PROMPT_VARS = { CONTENT: "content" };
const consts = {
__proto__: null,
PROMPT_VARS: PROMPT_VARS,
bugsUrl: bugsUrl,
overwrite: overwrite,
projectDesc: projectDesc,
projectName: projectName,
theme: theme,
themeDesc: themeDesc,
version: version
};
const isPath = (str) => {
if (isAbsolute(str) || /^(\.\/|\.\.\/|[A-Za-z]:\\|\/)/.test(str)) {
if (isAbsolute(str) || /^(\.\/|\.\.\/|[A-Za-z]:\\|\/)/.test(str)) {
if (/\s(?!\\)/.test(str) && !/\\\s/.test(str))
return false;
try {
const normalizedPath = join(str);
return normalizedPath !== "";
} catch {
return false;
}
}
}
return false;
};
class Sys {
path = {
resolve,
extname,
basename,
isAbsolute,
join
};
readFile = readFile;
writeFile = writeFile;
isAbsolute = isAbsolute;
join = join;
isPath = isPath;
getPaths = globby;
extname = extname;
async existsFile(filePath) {
try {
const fileStats = await stat(filePath);
return fileStats.isFile();
} catch {
return false;
}
}
/**
* Reads a configuration file and returns the parsed content.
*
* @param {string} filePath - The path to the configuration file.
* @returns {Promise<object>} - The parsed content of the configuration file.
* @throws {Error} - If the file extension is not supported.
*/
async readConfigFile(filePath) {
const ext = this.extname(filePath);
const content = await this.readFile(filePath, "utf8");
let res;
if (ext === ".json") {
res = JSON.parse(content);
} else if (ext === ".yml" || ext === ".yaml") {
res = deserialize(content);
} else if (ext === ".toml" || ext === ".tml") {
res = deserialize$1(content);
} else if (ext === ".js" || ext === ".mjs") {
const modulePath = pathToFileURL(filePath).href;
res = (await import(modulePath)).default;
} else {
throw new Error(`Unsupported file extension: ${ext}`);
}
return res;
}
}
const replacePlaceholders = async (content, params, customParams) => {
const regex = /{{\s*([\w:/.-]+)\s*(?:\(\s*['"]([^'"]+)['"]\s*\))?\s*}}/g;
const matches = [];
let match;
while ((match = regex.exec(content)) !== null) {
const [
placeholder,
key,
arg
] = match;
matches.push({
placeholder,
key,
arg
});
}
for (const {
placeholder,
key,
arg
} of matches) {
const value = params[key];
let replacement = "";
if (typeof value === "function") {
replacement = await value(arg || "");
} else if (value !== void 0) {
replacement = value;
} else if (customParams) {
replacement = await customParams(key);
} else {
replacement = placeholder;
}
content = content.replace(placeholder, replacement);
}
return content;
};
const setErrorString = (error) => {
const v = {
// @ts-ignore
code: error?.code,
// @ts-ignore
name: error?.name,
// @ts-ignore
message: error?.message,
// @ts-ignore
stack: error?.stack ? error.stack.split("\n") : [],
...error instanceof Object ? error : {}
};
if (Array.isArray(v.stack) && v.stack.length > 0) return v.stack.map((line) => ` ${line}`).join("\n");
return JSON.stringify(v, null, " ");
};
const sanitizeContent = (content) => {
return content;
};
const getTextPlainFromURL = async (url) => {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
const data = await response.text();
return data;
} catch (error) {
throw new Error(`Failed to fetch URL [${url}]: ${error?.message || "Unexpected error"}`);
}
};
const getStringType = (value) => {
if (isUrl(value)) return "url";
if (isPath(value)) return "path";
return "text";
};
const isUrl = (value) => {
try {
new URL(value);
return true;
} catch {
return false;
}
};
const string = {
__proto__: null,
getStringType: getStringType,
getTextPlainFromURL: getTextPlainFromURL,
replacePlaceholders: replacePlaceholders,
sanitizeContent: sanitizeContent,
setErrorString: setErrorString
};
class AiVectored {
#embedModel;
#llm;
#sys;
#systemPrompt;
#chatEngine;
constructor(args) {
this.#embedModel = new OllamaEmbedding({ model: "nomic-embed-text" });
this.#sys = new Sys();
this.#systemPrompt = args.system;
this.#llm = new Ollama({
model: args.model,
options: { temperature: 0.5 }
});
this.#chatEngine = void 0;
Settings.embedModel = this.#embedModel;
Settings.llm = this.#llm;
Settings.chunkSize = 300;
Settings.chunkOverlap = 20;
}
async #createContentFromJSONContent(contentJson) {
const content = new TextEncoder().encode(contentJson);
const reader = new JSONReader({ levelsBack: 0 });
const res = await reader.loadDataAsContent(content);
return res;
}
async generateChat(lcDocs) {
if (lcDocs.length == 0) {
const defaultDoc = new Document({
id_: "default_doc",
text: "There are no documents loaded.Starting chat without documentation.",
metadata: {
file_path: "default",
file_name: "default"
}
});
lcDocs.push({
content: defaultDoc.text,
path: defaultDoc.id_
});
}
const isJSON = (str) => {
try {
const parsed = JSON.parse(str);
return typeof parsed === "object" && parsed !== null;
} catch {
return false;
}
};
const docsPromise = lcDocs.map(async (lcDoc) => {
const is_url = getStringType(lcDoc.path) === "url";
const isJSONUrl = is_url && isJSON(lcDoc.content);
const is_JSON_content = lcDoc.path.endsWith(".json") || isJSONUrl;
const sharedProps = {
id_: lcDoc.path,
metadata: {
is_url,
is_JSON_content,
is_local_path: !is_url,
file_path: is_url ? lcDoc.path : this.#sys.path.resolve(lcDoc.path),
file_name: is_url ? lcDoc.path : this.#sys.path.basename(lcDoc.path)
}
};
if (is_JSON_content) {
const data = await this.#createContentFromJSONContent(lcDoc.content);
const res = {
...data[0],
...sharedProps
};
return new Document(res);
}
return new Document({
...{
text: lcDoc.content,
id_: lcDoc.path
},
...sharedProps
});
});
const docs = await Promise.all(docsPromise);
const index = await VectorStoreIndex.fromDocuments(docs);
const retriever = index.asRetriever({ similarityTopK: 3 });
if (this.#chatEngine) this.#chatEngine.reset();
this.#chatEngine = new ContextChatEngine({
retriever,
chatModel: this.#llm,
systemPrompt: this.#systemPrompt
});
}
async chat(query) {
if (!this.#chatEngine) return;
const response = await this.#chatEngine.chat({
message: query,
stream: true
});
return response;
}
async resetChatEngine() {
if (this.#chatEngine) this.#chatEngine.reset();
}
}
class Ai {
#ollama;
error = { NO_MODELS: "no-models" };
constructor() {
this.#ollama = ollama;
}
async getModels() {
const output = await this.#ollama.list();
if (!output.models || output.models.length === 0) throw new Error(this.error.NO_MODELS);
return output.models.map((model) => model.name);
}
async installModel(name) {
return await ollama.pull({
model: name,
stream: true
});
}
async chatVectored(opts) {
const vectored = new AiVectored({
model: opts.model,
system: opts.system
});
await vectored.generateChat(opts.docs);
return {
send: vectored.chat.bind(vectored),
reset: vectored.resetChatEngine.bind(vectored)
};
}
async chat(opts) {
const ollama2 = this.#ollama;
const messages = [
{
role: "system",
content: opts.system
}
];
const sendMessage = async (prompt) => {
messages.push({
role: "user",
content: prompt
});
const response = await ollama2.chat({
stream: true,
model: opts.model,
messages,
options: { temperature: 0 }
});
return response;
};
return {
send: sendMessage,
addAssistantMessage: (content) => messages.push({
role: "assistant",
content
})
};
}
}
const catchError = async (promise) => {
return promise.then((value) => [void 0, value]).catch((error) => [error]);
};
class TypedError extends Error {
data;
constructor(message, data) {
super(message);
this.data = data;
Error.captureStackTrace(this, this.constructor);
}
}
const validate = z;
const createLiteralUnion = (values) => {
return z.union(values.map((value) => z.literal(value)));
};
const OverwriteTypes = createLiteralUnion(Object.values(overwrite));
const ThemeTypes = createLiteralUnion(Object.values(theme));
const argvSchema = validate.object({
output: validate.string().optional(),
input: validate.array(validate.string()).optional(),
overwrite: OverwriteTypes.optional(),
model: validate.string().optional(),
prompt: validate.string().optional(),
system: validate.string().optional(),
theme: ThemeTypes.optional(),
single: validate.boolean().optional(),
debug: validate.boolean().optional(),
config: validate.string().optional()
});
const ErroClass = class CoreError extends TypedError {
};
class CoreSuper {
_c;
_p;
_argv;
_sys = new Sys();
_ai = new Ai();
_string = string;
_const = consts;
_process = process;
_catchError = catchError;
Error = ErroClass;
ERROR_ID = {
CANCELLED: "CANCELLED",
RESPONSE_CANCELLED: "RESPONSE_CANCELLED"
};
title = "core";
description;
argvSceham = argvSchema;
constructor({
argv,
c,
p
}) {
this._c = c;
this._p = p;
this._argv = argv;
}
_successRes(title, res, onlyText = false) {
const text = title + (res !== "" ? "\n" + this._c.gray(res) : "");
if (onlyText) return text;
this._p.log.success(title + "\n" + this._c.gray(res));
}
_errorRes(title, res, onlyText = false) {
const text = title + (res !== "" ? "\n" + this._c.gray(res) : "");
if (onlyText) return text;
this._p.log.error(text);
}
_setDebug(msg) {
this._p.log.debug(this._c.section(this.title.toUpperCase()), msg);
}
_setTitle() {
const description = this.description ? "\n\n" + this._c.gray(this.description) : "";
this._p.log.info(this._c.section(this.title.toUpperCase()) + description);
}
_setErrorMessage(e, unknownMessage = "Unexpected error") {
if (e instanceof Error) return e.message;
else return unknownMessage;
}
cancel(msg) {
this._p.log.warn(this._c.warn(msg));
this.exit();
}
exit(code = 0) {
console.log("");
if (code === "error") this._process.exit(1);
else this._process.exit(code);
}
async _textPrompt(args) {
const prompt = await this._p.text({
message: args.message,
placeholder: args.placeholder,
validate(value) {
if (!value || value.trim() === "") return "No empty value is allowed.";
}
});
if (this._p.isCancel(prompt)) throw new this.Error(this.ERROR_ID.CANCELLED);
return prompt;
}
async _confirmPrompt(args) {
const prompt = await this._p.confirm({ message: args.message });
if (this._p.isCancel(prompt)) throw new this.Error(this.ERROR_ID.CANCELLED);
return prompt;
}
async _selectPrompt(args) {
const prompt = await this._p.select({
message: args.message,
options: args.opts.map((opt) => ({
value: opt.value,
label: opt.title,
hint: opt.desc
}))
});
if (this._p.isCancel(prompt)) throw new this.Error(this.ERROR_ID.CANCELLED);
return prompt;
}
_setProcessPath(path) {
return this._sys.path.resolve(this._process.cwd(), path);
}
async _validateContent(v) {
const validatePath = async (v2) => {
const path = this._setProcessPath(v2);
const exist = await this._sys.existsFile(path);
if (!exist) throw new this.Error(`Path "${v2}" doesn't exist or is not found.`);
const res2 = await this._sys.readFile(path, "utf-8");
if (res2 && typeof res2 === "string") return res2;
throw new this.Error(`Path "${v2}" has unexpected content type: ${typeof res2}`);
};
const validateURL = async (v2) => {
const res2 = await this._string.getTextPlainFromURL(v2);
if (res2 && typeof res2 === "string") return res2;
throw new this.Error(`URL "${v2}" has unexpected content type: ${typeof res2}`);
};
const convertString = async (value) => {
const stringType = this._string.getStringType(value);
return stringType === "path" ? await validatePath(value) : stringType === "url" ? await validateURL(value) : value;
};
const content = await convertString(v);
const res = await this._string.replacePlaceholders(
content,
{
url: async (v2) => await validateURL(v2),
path: async (v2) => await validatePath(v2)
},
async (v2) => await convertString(v2)
);
return res;
}
}
class CoreConfig extends CoreSuper {
validateProperties(properties) {
const res = this.argvSceham.parse(properties);
return res;
}
/**
* Overwrite argv
*
* @description
* Overwrite argv if exist argv.config with a config file. files supported: [.mjs|.js|.json|.yml|.yaml|.toml|.tml]
*/
async set() {
const argv = this._argv;
const config = argv.config;
if (!config) return;
try {
const props = await this._sys.readConfigFile(config);
const data = await this.validateProperties(props);
for (const key in data) {
if (!argv[key]) argv[key] = data[key];
}
} catch (e) {
this._p.log.warn("Error setting config properties: " + this._setErrorMessage(e));
}
}
}
class CoreInputs extends CoreSuper {
title = "Input";
description = "Inputs to add context to your chat. This in not required.";
errorTitle = this._c.error(this.title);
async _getDataContent(inputs, type) {
type = type ?? "path";
const load = this._p.spinner();
load.start("Preparing inputs...");
let res = [];
if (type === "url") {
load.message("Reading and sanitizing url...");
const urlContents = {};
for (const input of inputs) {
const [error, data] = await this._catchError((async () => {
load.message("Reading " + input);
const content = await this._string.getTextPlainFromURL(input.toString());
return this._string.sanitizeContent(content);
})());
if (error) {
load.stop("Error reading url: " + this._c.gray(input.toString()), 1);
throw error;
} else
urlContents[input.toString()] = data;
}
load.message("Getting source for all urls...");
res = [
...res,
...Object.entries(urlContents).map(([id, content]) => ({
content,
path: id
}))
];
load.stop("Source urls obtained!");
}
if (type === "path") {
load.message("Reading and sanitizing files...");
const fileContents = {};
for (const file of inputs) {
load.message("Reading " + file);
const content = await this._sys.readFile(file, "utf-8");
fileContents[file.toString()] = this._string.sanitizeContent(content);
}
load.message("Getting source for all files...");
res = [
...res,
...Object.entries(fileContents).map(([id, content]) => ({
content,
path: id
}))
];
load.stop("Source files obtained!");
}
return res;
}
async getContent(inputs) {
const setError = () => this._errorRes(this.errorTitle, "Unexpected error! Missing required arguments. Please provide all required arguments.");
if (!inputs) {
setError();
inputs = await this.getInputs(void 0, void 0);
return await this.getContent(inputs);
}
try {
const urlContent = !(!inputs.urls || !inputs.urls.length) ? await this._getDataContent(inputs.urls, "url") : void 0;
const fileContent = !(!inputs.paths || !inputs.paths.length) ? await this._getDataContent(inputs.paths, "path") : void 0;
if (urlContent && fileContent) return [...urlContent, ...fileContent];
else if (urlContent) return urlContent;
else if (fileContent) return fileContent;
else return [];
} catch {
inputs = await this.getInputs(void 0, void 0);
return await this.getContent(inputs);
}
}
#separateInputs(inputs) {
const urls = [];
const paths = [];
for (const input of inputs) {
const type = this._string.getStringType(input);
if (type === "url") urls.push(new URL(input));
else paths.push(input);
}
if (!paths.length && !urls.length) return void 0;
return {
urls,
paths
// text,
};
}
async #choiceIncludes(placeholder) {
const prompt = await this._textPrompt({
message: "Enter path patterns or URLs to be processed (comma-separated):",
placeholder
});
return prompt.split(",").map((path) => path.trim());
}
async getInputs(includesPaths, includePlaceholder) {
const input = includesPaths ? (this._successRes(`Inputs selected:`, includesPaths.length ? includesPaths.join(", ") : "none"), includesPaths) : await this.#choiceIncludes(includePlaceholder);
let i = this.#separateInputs(input);
const errorMsg = () => this._errorRes("No files or urls were found matching the inclusion patterns in:", input.join(", "));
if (!i) {
i = {
urls: [],
paths: []
};
} else {
i.paths = await this._sys.getPaths(i.paths);
if (!i || !i.paths.length && !i.urls.length) {
errorMsg();
i = await this.getInputs(void 0, input.join(", "));
}
}
return i;
}
async get() {
this._setTitle();
const inputs = await this.getInputs(this._argv.input || []);
const res = await this.getContent(inputs);
this._setDebug(JSON.stringify(res, null, 2));
return res;
}
}
class CoreModel extends CoreSuper {
title = "Model";
description = "LLM model to use for your chat.";
async installModel(modelName) {
const spin = this._p.spinner();
const msg = (v) => `[${modelName}] Installation: ` + v;
try {
spin.start(msg("Initializing..."));
const response = await this._ai.installModel(modelName);
if (response[Symbol.asyncIterator]) {
this._process.onSIGNIT(() => {
this._process.stdout.write("\n");
this.cancel("Exit from installation");
});
for await (const part of response) {
spin.message(msg(part.status));
}
spin.stop(msg("Model installed successfully! \u2728"));
} else {
throw new this.Error("Response is not iterable");
}
} catch (e) {
spin.stop(msg("Error installing model"), 1);
throw new Error(`Error installing [${modelName}] model: ${this._setErrorMessage(e)}`);
}
}
async get() {
const modelArgv = this._argv.model;
const { gray } = this._c;
this._setTitle();
const getModels = async () => {
const [error, output] = await this._catchError(this._ai.getModels());
if (error) {
const ollamaInfo = `
For more information about Ollama please visit ${this._c.gray("https://ollama.com/")}`;
this._errorRes("Error retrieving model from Ollama.", "");
if (error instanceof Error && error.message === this._ai.error.NO_MODELS) throw new this.Error("No Ollama models found.\n Please download at least one model." + ollamaInfo);
else throw new this.Error("Ollama is not installed or is not running.\n Please install/start Ollama to use this tool." + ollamaInfo);
} else
return output;
};
const promptTile = "Select a LLM model:";
const models = await getModels();
if (modelArgv && models.includes(modelArgv)) {
this._p.log.success(`${promptTile}
${gray(modelArgv)}`);
return modelArgv;
} else if (modelArgv && !models.includes(modelArgv)) this._p.log.error(`Model [${modelArgv}] not exists or is not installed.`);
const embedModelName = "nomic-embed-text";
if (!models.some((m) => m.startsWith(embedModelName))) {
this._p.log.warn(`Model [${embedModelName}] must be installed for create embeddings`);
const nomic = await this._confirmPrompt({ message: `Do you want to install [${embedModelName}] model now?` });
if (nomic === false) throw new this.Error(`Model [${embedModelName}] must be installed.
Read more about it at https://ollama.com/library/nomic-embed-text`);
else await this.installModel(embedModelName);
const checkModelsAgain = await getModels();
if (!checkModelsAgain.some((model2) => model2.startsWith(embedModelName)))
throw new this.Error(`Model [${embedModelName}] failed to install unexpectedly`);
}
const model = await this._selectPrompt({
message: promptTile,
opts: models.filter((model2) => !model2.startsWith(embedModelName)).map((modelName) => ({
value: modelName,
title: modelName
}))
});
const res = model;
this._setDebug(res);
return res;
}
}
class CoreOutput extends CoreSuper {
title = "Output";
description = "Output configuration for save the generated content.";
async getOverwrite() {
const argv = this._argv.overwrite;
const {
ask,
...values
} = this._const.overwrite;
const title = "Select overwrite type:";
if (argv && argv !== ask) {
if (argv === "always" || argv === "last") {
this._successRes(title, argv);
return argv;
} else this._errorRes("Invalid overwrite type: ", argv);
}
const prompt = await this._selectPrompt({
message: title,
opts: Object.values(values).map((v) => ({
value: v,
title: v
}))
});
return prompt;
}
async getSingle() {
const argv = this._argv.single;
if (!argv || argv !== true) return false;
this._successRes(`Response type:`, "Single");
return argv;
}
async getPath() {
const set = async (initValue, placeholder) => {
const value = initValue ? (this._successRes(`Output path:`, initValue), initValue) : await this._textPrompt({
message: "Enter output path:",
placeholder
});
const res = this._setProcessPath(value);
return res;
};
const argv = this._argv.output;
return argv ? await set(argv) : void 0;
}
async get() {
this._setTitle();
const path = await this.getPath();
const single = await this.getSingle();
const overwrite = path ? await this.getOverwrite() : void 0;
const res = {
path,
overwrite,
single
};
if (!path && !single) this._p.log.success("No output path provided");
this._setDebug(JSON.stringify(res, null, 2));
return res;
}
}
const prompts = {
[theme.docs]: { system: `You are a helpful assistant explaining how to use the provided code library and provide detailed documentation.
Generate a complete and user-oriented README in Markdown format for the provided code library.
Assume the documentation will be viewed by end-users who seek a clear understanding of how to install, configure, and use the library effectively.
If a package.json file is provided, extract relevant information such as project name, description, installation steps, available binaries, author(s), contributor(s), license, exports, and dependencies, integrating these details seamlessly into the documentation.
Include examples and details for each aspect of the library, aiming to cover both beginners and advanced users.
structure:
# Project Title
> A concise project description explaining the library's purpose and main features.
## Table of Contents
- Overview
- Installation
- Usage
- Setup & Configuration
- API Reference
- Code Examples
- Additional Information
- Author & Contributors
- Contributing
- License
## Overview
Provide a high-level explanation of the library's functionality, its key advantages, and core use cases.
## Installation
Explain installation in detail:
1. List prerequisites, if any.
2. Provide step-by-step installation instructions using npm/yarn or other methods, based on package.json information.
## Usage
### Setup & Configuration
Outline any configuration steps required post-installation, using examples if needed.
### API Reference
Document all exposed functions, classes, or modules:
- **Function/Class Name**: Describe its purpose and main use.
- **Parameters**: Detail expected parameters with types and optional/default values.
- **Return Value**: Describe the return type and any specific details.
- **Examples**: Provide clear, context-rich examples.
### Code Examples
Include multiple examples showcasing the library in action, covering different scenarios.
## Additional Information
### Author
List the main author as found in package.json.
### Contributors
Provide a contributors list, if available.
### Contributing
Explain how to contribute, with links to contributing guidelines if they exist.
### License
Detail the license information as specified in package.json.
---
The output should be written in strict Markdown format and provide a comprehensive, user-friendly guide.
` },
[theme.fix]: { system: `You are a helpful assistant explaining how to identify and fix issues in the provided code library.
Analyze the provided code library and generate a detailed report in Markdown format on common issues and their solutions.
If a package.json file is provided, extract relevant information such as project name, description, and installation steps, integrating these details seamlessly into the report.
Include examples and details for each aspect of the library, aiming to cover both simple and complex issues.
structure:
# Project Title
> A concise project description explaining the library's purpose and main features.
## Table of Contents
- Overview of Common Issues
- Solutions
- Additional Information
## Overview of Common Issues
Provide a summary of typical problems encountered when using the library.
## Solutions
Outline solutions to each identified issue, with step-by-step instructions and code examples.
## Additional Information
### Author
List the main author as found in package.json.
### Contributors
Provide a contributors list, if available.
### License
Detail the license information as specified in package.json.
---
The output should be written in strict Markdown format and provide a comprehensive guide to troubleshooting the library.` },
[theme.performance]: { system: `You are a helpful assistant explaining how to optimize the performance of the provided code library.` },
[theme.refactor]: { system: `You are a helpful assistant explaining how to refactor code for the provided code library.
Generate a detailed guide in Markdown format on how to refactor the provided code library.
Include best practices and principles for writing clean, maintainable code.
structure:
# Project Title
> A concise project description explaining the library's purpose and main features.
## Table of Contents
- Overview of Refactoring
- Refactoring Techniques
- Example Refactorings
- Additional Information
## Overview of Refactoring
Discuss what refactoring is and its importance in software development.
## Refactoring Techniques
Describe common refactoring techniques, providing examples for each.
## Example Refactorings
Provide concrete examples of code refactorings applied to the library, with before and after comparisons.
## Additional Information
### Author
List the main author as found in package.json.
### Contributors
Provide a contributors list, if available.
### License
Detail the license information as specified in package.json.
---
The output should be written in strict Markdown format and provide a comprehensive guide to code refactoring.` },
[theme.explain]: { system: `You are a helpful assistant providing explanations for the provided code library.
Create a detailed explanation in Markdown format for the provided code library, focusing on its design, structure, and functionality.
Include examples to illustrate key concepts and usage.
structure:
# Project Title
> A concise project description explaining the library's purpose and main features.
## Table of Contents
- Overview of the Code Library
- Key Components
- Detailed Explanations
- Additional Information
## Overview of the Code Library
Provide an overview of the library, including its main features and intended use cases.
## Key Components
Describe the key components of the library, explaining their roles and how they interact.
## Detailed Explanations
Provide detailed explanations of important concepts, including code snippets to clarify usage.
## Additional Information
### Author
List the main author as found in package.json.
### Contributors
Provide a contributors list, if available.
### License
Detail the license information as specified in package.json.
---
The output should be written in strict Markdown format and provide a comprehensive guide to understanding the library.` },
[theme.test]: { system: `You are a helpful assistant guiding users on testing the provided code library.
Develop a detailed guide in Markdown format on testing the provided code library.
Assume the audience consists of developers who want to ensure their code is well-tested.
structure:
# Project Title
> A concise project description explaining the library's purpose and main features.
## Table of Contents
- Overview of Testing
- Testing Frameworks and Tools
- Test Cases and Examples
- Additional Information
## Overview of Testing
Discuss the importance of testing in software development and provide an overview of the library's testing approach.
## Testing Frameworks and Tools
Describe the testing frameworks and tools available for use with the library, providing examples of how to set them up.
## Test Cases and Examples
Provide examples of test cases, including unit tests and integration tests, demonstrating how to write and run tests.
## Additional Information
### Author
List the main author as found in package.json.
### Contributors
Provide a contributors list, if available.
### License
Detail the license information as specified in package.json.
---
The output should be written in strict Markdown format and provide a comprehensive guide to testing the library.` }
};
class CorePrompt extends CoreSuper {
title = "Prompt";
description = "The prompt configuration for your chat.";
defaults = prompts;
async #getSystem(defaultValue) {
const message = `System prompt ${this._c.italic("(Enter text path or url)")}:`;
const setPrompt = async (value, placeholder) => {
let res2, prompt;
try {
if (value) res2 = await this._validateContent(value);
else {
prompt = await this._textPrompt({
message,
placeholder: placeholder ? placeholder : "You are an expert code programmer with extensive knowledge of the content of this library"
});
res2 = await this._validateContent(prompt);
}
if (!res2) throw new Error();
} catch (e) {
if (e instanceof this.Error && e.message === this.ERROR_ID.CANCELLED)
throw new this.Error(this.ERROR_ID.CANCELLED);
else this._errorRes(`System prompt error:`, this._setErrorMessage(e));
res2 = await setPrompt(void 0, res2 || prompt);
}
return res2;
};
const argv = this._argv.system;
const res = argv ? (this._successRes(message, argv), await setPrompt(argv)) : defaultValue ? defaultValue : await setPrompt(void 0);
return res;
}
async getTheme() {
const theme = this._argv.theme;
const message = `Select theme:`;
const res = theme ? (this._successRes(message, theme), theme) : await this._selectPrompt({
message,
opts: Object.entries(this._const.theme).map(([k, v]) => ({
value: v,
title: v,
// @ts-ignore
desc: this._const.themeDesc[k] || void 0
}))
});
return res;
}
async get() {
this._setTitle();
const theme = await this.getTheme();
const system = await this.#getSystem(theme !== "custom" ? this.defaults[theme].system : void 0);
const res = {
theme,
system
};
this._setDebug(JSON.stringify(res, null, 2));
return res;
}
}
class CoreResponse extends CoreSuper {
title = "Chat";
#chat;
#line = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
async #setChatResponse(prompt) {
this._setDebug(prompt);
const line = this.#line;
const chatName = "Assistant";
const title = chatName + ":";
const lastLine = () => (console.log("\n\n\n"), this._p.intro(line));
const firstLine = () => (this._p.outro(line), console.log("\n"));
const spin = this._p.spinnerCustom();
let output = "", exitResponse = false;
if (!this.#chat) throw new this.Error("Chat not initialized");
spin.start(title + " Thinking...");
const response = await this.#chat.send(prompt);
spin.message(title + " Thinking...");
if (response && response[Symbol.asyncIterator]) {
spin.stop(title);
firstLine();
this._process.onSIGNIT(async () => exitResponse = true);
for await (const part of response) {
if (exitResponse) {
this._process.stdout.write("\n");
lastLine();
throw new this.Error(this.ERROR_ID.RESPONSE_CANCELLED);
} else {
this._process.stdout.write(part.message.content.toString());
output += part.message.content;
}
}
lastLine();
} else {
spin.stop(title + "There has been an error in the chat process");
throw new this.Error("Chat unexpeted error");
}
return output;
}
async #generate({
system,
model,
docs
}) {
const spin = this._p.spinner();
spin.start("starting...");
try {
const spinMsg = "Generating chat";
spin.message(spinMsg);
const capturedMessages = [];
this.#chat = await this._process.onOutputWrite({
fn: async () => await this._ai.chatVectored({
system,
model,
docs
}),
on: async (value) => {
if (!value.includes(spinMsg) && !value.includes("\x1B[999D") && !value.includes("\x1B[J"))
capturedMessages.push(value);
else return value;
}
});
spin.stop("Chat successfully generated! \u2728");
if (capturedMessages.length || capturedMessages.join("\n").trim() !== "")
this._p.note(capturedMessages.join("\n"), "Notifications");
else this._p.log.message(this.#line);
} catch (e) {
this._p.log.error("Error generating repsonse: " + this._setErrorMessage(e));
throw e;
}
}
async #reply(args = void 0) {
const first = args?.first;
let res = "";
try {
const prompt = args?.prompt && first ? (this._successRes(`You:`, args.prompt), args.prompt) : await this._textPrompt({
message: `You:`,
placeholder: first ? "Write your first prompt here" : "Write your next prompt here"
});
res = await this._validateContent(prompt);
res = await this.#setChatResponse(res);
} catch (e) {
if (e instanceof this.Error && e.message === this.ERROR_ID.CANCELLED)
throw new this.Error(this.ERROR_ID.CANCELLED);
else if (e instanceof this.Error && e.message === this.ERROR_ID.RESPONSE_CANCELLED)
await this.#reply();
this._errorRes(this.title, this._setErrorMessage(e));
res = await this.#reply();
}
return res;
}
async #write(outputPath, content, overrides) {
if (overrides === "last") {
const exists = await this._sys.existsFile(outputPath);
if (exists) {
const existingContent = await this._sys.readFile(outputPath, "utf-8");
content = `${existingContent === "" ? "" : existingContent + "\n\n---\n\n"}${content}`;
}
}
await this._sys.writeFile(outputPath, content);
this._successRes(`Response saved in:`, outputPath);
}
async #recursiveReply(outputPath, overrides) {
const response = await this.#reply();
if (outputPath) await this.#write(outputPath, response, overrides || "last");
await this.#recursiveReply(outputPath, overrides);
}
async get({
system,
model,
docs,
output
}) {
this._setTitle();
await this.#generate({
system,
model,
docs
});
const prompt = this._argv.prompt;
const response = await this.#reply({
first: true,
prompt
});
if (output.path && output.overwrite) await this.#write(output.path, response, output.overwrite);
if (!output.single) await this.#recursiveReply(output?.path, output?.overwrite);
}
}
const coreMessages = {};
class Core {
prompt;
model;
config;
input;
response;
output;
Error;
ERROR_ID;
exit;
cancel;
constructor(args) {
this.config = new CoreConfig(args);
this.prompt = new CorePrompt(args);
this.model = new CoreModel(args);
this.input = new CoreInputs(args);
this.output = new CoreOutput(args);
this.response = new CoreResponse(args);
this.Error = this.input.Error;
this.ERROR_ID = this.input.ERROR_ID;
this.exit = this.input.exit.bind(this.input);
this.cancel = this.input.cancel.bind(this.input);
}
}
const setLine = async (core, args) => {
const { group } = core.p;
const {
onCancel,
onError
} = args;
return { list: async (list) => {
try {
await group(list, { onCancel: async () => await onCancel() });
} catch (e) {
await onError(e);
}
} };
};
const unicode = isUnicodeSupported();
const s = (c, fallback) => unicode ? c : fallback;
const S_STEP_CANCEL = s("\u25A0", "x");
const S_STEP_ERROR = s("\u25B2", "x");
const S_STEP_SUBMIT = s("\u25C7", "o");
const S_BAR = s("\u2502", "|");
const spinnerCustom = () => {
const frames = unicode ? [
"\u25D2",
"\u25D0",
"\u25D3",
"\u25D1"
] : [
"\u2022",
"o",
"O",
"0"
];
const delay = unicode ? 80 : 120;
let unblock, loop, isSpinnerActive = false, _message = "";
const handleExit = (code) => {
const msg = code > 1 ? "Something went wrong" : "Canceled";
if (isSpinnerActive) stop(msg, code);
};
const errorEventHandler = () => handleExit(2);
const signalEventHandler = () => handleExit(1);
const registerHooks = () => {
process$1.on("uncaughtExceptionMonitor", errorEventHandler);
process$1.on("unhandledRejection", errorEventHandler);
process$1.on("SIGINT", signalEventHandler);
process$1.on("SIGTERM", signalEventHandler);
process$1.on("exit", handleExit);
};
const clearHooks = () => {
process$1.removeListener("uncaughtExceptionMonitor", errorEventHandler);
process$1.removeListener("unhandledRejection", errorEventHandler);
process$1.removeListener("SIGINT", signalEventHandler);
process$1.removeListener("SIGTERM", signalEventHandler);
process$1.removeListener("exit", handleExit);
};
const start = (msg = "") => {
isSpinnerActive = true;
unblock = block();
_message = msg.replace(/\.+$/, "");
process$1.stdout.write(`${grayBright(S_BAR)}
`);
let frameIndex = 0, dotsTimer = 0;
registerHooks();
loop = setInterval(() => {
const frame = magenta(frames[frameIndex]);
const loadingDots = ".".repeat(Math.floor(dotsTimer)).slice(0, 3);
process$1.stdout.write(cursor.move(-999, 0));
process$1.stdout.write(erase.down(1));
process$1.stdout.write(`${frame} ${_message}${loadingDots}`);
frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0;
dotsTimer = dotsTimer < frames.length ? dotsTimer + 0.125 : 0;
}, delay);
};
const stop = (msg = "", code = 0) => {
_message = msg ?? _message;
isSpinnerActive = false;
clearInterval(loop);
const step = code === 0 ? blue(S_STEP_SUBMIT) : code === 1 ? red(S_STEP_CANCEL) : red(S_STEP_ERROR);
process$1.stdout.write(cursor.move(-999, 0));
process$1.stdout.write(erase.down(1));
process$1.stdout.write(`${step} ${_message}
`);
clearHooks();
unblock();
};
const message = (msg = "") => {
_message = msg ?? _message;
};
return {
start,
stop,
message
};
};
const p = /*#__PURE__*/_mergeNamespaces({
__proto__: null,
spinnerCustom: spinnerCustom
}, [prompts$1]);
class CLI {
#argv;
c = c;
p = p;
_const = consts;
name = "";
desc = "";
// Not default values for options
options = {
// inputs
input: {
alias: "i",
describe: "Path patterns or URLs to be processed",
type: "array"
},
// ai
model: {
alias: "m",
describe: "Ollama LLM model name",
type: "string"
},
system: {
alias: "s",
describe: "System message (text, path or url)",
type: "string"
},
prompt: {
alias: "p",
describe: "Fist prompt to generate a response (text, path or url)",
type: "string"
},
theme: {
alias: "t",
describe: "Set a theme for your chat.",
choices: Object.values(theme)
},
// response
output: {
alias: "o",
describe: "Output path for the generated response",
type: "string"
},
overwrite: {
describe: "Behavior when output file exists",
choices: Object.values(overwrite)
},
single: {
describe: "Only one response",
type: "boolean"
},
// others
config: {
alias: "c",
describe: "Path to config file. Files supported: [.mjs|.js|.json|.yml|.yaml|.toml|.tml]",
type: "string"
},
// 'non-interactive' : {
// alias : 'n',
// describe : 'Non-interactive mode. Do not prompt for user input',
// type : 'boolean',
// default : false,
// },
debug: {
describe: "Debug mode",
type: "boolean"
}
};
message = {
intro: "Line execution",
outro: "Operation completed!",
cancel: "Operation cancelled",
error: {
general: `Error`,
debug: `Debug Error`,
debugFlag: (flag) => `You can debug the error with flag: ${flag}`,
debugContact: (url) => `Or contact developers at: ${url}`,
unexpected: "Unexpected error"
},
...coreMessages
};
constructor(argv) {
this.#argv = argv;
}
async fn(params) {
return await this.#handler(params);
}
async #handler(argv) {
const p2 = this.p;
const c2 = this.c;
this.#argv;
const prompts = {
...p2,
log: {
...p2.log,
debug: (title, msg) => {
if (argv.debug) p2.log.info(c2.debug("DEBUG") + " " + c2.gray(title) + "\n\n" + msg);
}
}
};
const core = new Core({
argv,
c: c2,
p: prompts
});
const cancel = () => core.cancel(this.message.cancel);
const { list } = await setLine({
p: prompts
}, {
onCancel: async () => cancel(),
onError: async (error) => {
const e = typeof error === "string" ? error : setErrorString(error);
const isCoreError = error instanceof core.Error;
const errorMsg = error instanceof Error ? error.message : typeof error === "string" ? error : this.message.error.unexpected;
const isDebug = argv.debug;
const set = (v, d) => (p2.log.step(""), p2.log.error(c2.error(v.toUpperCase())), p2.log.step(""), p2.cancel(d));
if (isCoreError) {
if (error.message === core.ERROR_ID.CANCELLED) cancel();
else set(this.message.error.general, errorMsg);
} else if (isDebug) set(this.message.error.debug, e.trim());
else
set(
this.message.error.general,
`${errorMsg}
${this.message.error.debugFlag(c2.italic("--debug"))}
${this.message.error.debugContact(c2.link(this._const.bugsUrl))}`
);
core.exit("error");
}
});
const UnexpectedError = new Error(this.message.error.unexpected);
await list({
intro: async () => p2.intro(c2.introColor(this.message.intro.toUpperCase())),
config: async () => await core.config.set(),
model: async () => await core.model.get(),
content: async () => await core.input.get(),
ai: async () => await core.prompt.get(),
output: async () => await core.output.get(),
response: async ({ results }) => {
if (!results.ai || !results.output || !results.model || !results.content) throw UnexpectedError;
const res = await core.response.get({
system: results.ai.system,
model: results.model,
docs: results.content,
output: results.output
});
return res;
},
outro: async () => (p2.log.step(""), p2.outro(c2.success(this.message.outro.toUpperCase())))
});
}
run() {
return {
command: this.name,
describe: this.desc,
builder: (yargs) => yargs.options(this.options),
handler: this.#handler.bind(this)
};
}
}
class chatCLI extends CLI {
constructor(cli) {
super(cli);
const projectName = this._const.projectName;
this.name = "chat";
this.desc = `Chat with the ${projectName} assistant`;
this.message.intro = `${projectName} assistant`;
this.message.cancel = `${projectName} assistant cancelled!`;
this.message.outro = `${projectName} assistant completed!`;
}
}
export { chatCLI as default };