Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add plugin definer #92

Closed
wants to merge 8 commits into from
68 changes: 68 additions & 0 deletions __tests__/plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-ignore
import clearPlugin from "../src/commands/clear";
import exitPlugin from "../src/commands/exit";
import chalk from "chalk";

// Force chalk to enable color output in test environment
chalk.level = 1;

describe("Clear Plugin", () => {
let originalStdoutWrite: typeof process.stdout.write;

beforeEach(() => {
originalStdoutWrite = process.stdout.write;
process.stdout.write = jest.fn();
});

afterEach(() => {
process.stdout.write = originalStdoutWrite;
});

it("should have the correct name, keyword, and description", () => {
expect(clearPlugin.name).toBe("clear");
expect(clearPlugin.keyword).toBe("@clear");
expect(clearPlugin.description).toBe("Clears the terminal screen");
});

it("should clear the terminal screen when executed", async () => {
const result = await clearPlugin.execute({});
expect(process.stdout.write).toHaveBeenCalledWith("\x1Bc");
expect(result).toBe("Terminal screen cleared.");
});
});

describe("Exit Plugin", () => {
let consoleLogSpy: jest.SpyInstance;
let processExitSpy: jest.SpyInstance;

beforeEach(() => {
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});
processExitSpy = jest
.spyOn(process, "exit")
.mockImplementation((code?: number) => {
throw new Error(`Process.exit called with code: ${code}`);
});
});

afterEach(() => {
consoleLogSpy.mockRestore();
processExitSpy.mockRestore();
});

it("should have the correct name, keyword, and description", () => {
expect(exitPlugin.name).toBe("exit");
expect(exitPlugin.keyword).toBe("@exit");
expect(exitPlugin.description).toBe("Exits the application");
});

it("should log goodbye message and exit the process when executed", async () => {
await expect(exitPlugin.execute({})).rejects.toThrow(
"Process.exit called with code: 0"
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("Goodbye!")
);
expect(processExitSpy).toHaveBeenCalledWith(0);
});
});
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"clipboardy": "2.3.0",
"commander": "^9.5.0",
"compromise": "^14.8.1",
"execa": "^9.3.1",
"gradient-string": "^2.0.2",
"hnswlib-node": "^3.0.0",
"lowdb": "^5.1.0",
Expand All @@ -59,6 +60,7 @@
"@types/chalk": "^2.2.0",
"@types/clipboardy": "2.0.1",
"@types/eslint": "^8.44.2",
"@types/execa": "^2.0.0",
"@types/jest": "^29.5.4",
"@types/ora": "^3.2.0",
"@typescript-eslint/eslint-plugin": "^6.4.1",
Expand Down
Empty file removed src/commands/autossugestion.ts
Empty file.
14 changes: 11 additions & 3 deletions src/commands/clear.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
const clearFunc = () => {
process.stdout.write("\x1Bc");
import { Plugin } from "./index";

const clearPlugin: Plugin = {
name: "clear",
keyword: "@clear",
description: "Clears the terminal screen",
execute: async () => {
process.stdout.write("\x1Bc");
return "Terminal screen cleared.";
},
};

export default clearFunc;
export default clearPlugin;
14 changes: 10 additions & 4 deletions src/commands/exit.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { Plugin } from "./index";
import chalk from "chalk";

const exitFunc = () => {
console.log(chalk.yellow("Goodbye!"));
process.exit(0);
const exitPlugin: Plugin = {
name: "exit",
keyword: "@exit",
description: "Exits the application",
execute: async () => {
console.log(chalk.yellow("Goodbye!"));
process.exit(0);
},
};

export default exitFunc;
export default exitPlugin;
49 changes: 33 additions & 16 deletions src/commands/file.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import chalk from "chalk";
import { Plugin } from "./index";
import { handleFileReference } from "../handlers/fileHandler"; // Assuming this function exists
import { apiKeyPrompt, promptResponse } from "../utils"; // Assuming this function exists
import { promptResponse } from "../utils"; // Assuming this function exists

const fileFunc = async (userInput: string) => {
const creds = await apiKeyPrompt();
// we need to call file handler here
const [, filePath, ...promptParts] = userInput.split(" ");
const promptText = promptParts.join(" ");
if (filePath) {
await handleFileReference(filePath, promptText);
if (creds.apiKey != null) {
await promptResponse(creds.engine, creds.apiKey, userInput, {});
const filePlugin: Plugin = {
name: "file",
keyword: "@file",
description: "Handles file operations and references",
execute: async (context: {
userInput: string;
engine: string;
apiKey: string;
opts: any;
}) => {
const { userInput, engine, apiKey, opts } = context;
const [, filePath, ...promptParts] = userInput.split(" ");
const promptText = promptParts.join(" ");

if (filePath) {
try {
await handleFileReference(filePath, promptText);
const response = await promptResponse(engine, apiKey, userInput, opts);
return response;
} catch (error) {
console.error(chalk.red(`Error handling file: ${error}`));
return `Error: ${error}`;
}
} else {
console.log(
chalk.yellow("Please provide a file path. Usage: @file <path> [prompt]")
);
return "Error: No file path provided";
}
} else {
console.log(
chalk.yellow("Please provide a file path. Usage: @file <path> [prompt]")
);
}
},
};

export default fileFunc;
export default filePlugin;
33 changes: 33 additions & 0 deletions src/commands/fileTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as fs from "fs";
import * as path from "path";
import { Plugin } from "./index";

const fileScannerPlugin: Plugin = {
name: "fileScanner",
keyword: "@filetree",
description: "Scans the project directory and returns important files",
execute: async () => {
const projectRoot = process.cwd();
const importantFiles = [
"package.json",
"README.md",
"tsconfig.json",
".gitignore",
];
const result: { path: string; content: string }[] = []; // Specify the type here

for (const file of importantFiles) {
const filePath = path.join(projectRoot, file);
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, "utf-8");
result.push({ path: file, content });
}
}

console.log("File tree scan result:", result);
return result;
},
};

export default fileScannerPlugin;
88 changes: 61 additions & 27 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,77 @@
import exitFunc from "./exit";
import clearFunc from "./clear";
import fileFunc from "./file";
import webFunc from "./web";
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as fs from "fs";
import * as path from "path";

export interface Plugin {
name: string;
keyword: string;
execute: (userInput: string) => Promise<void> | void;
description: string;
execute: (context: any) => Promise<any>; // Changed from (context: string) => Promise<void>
}

const plugins: Plugin[] = [];

const registerPlugin = (
keyword: string,
execute: (userInput: string) => Promise<void> | void
) => {
plugins.push({ keyword, execute });
const loadPlugins = () => {
const pluginDir = path.join(__dirname);
const files = fs.readdirSync(pluginDir);

// Clear existing plugins to prevent duplicates on reload
plugins.length = 0;

const loadedPlugins = new Set<string>(); // To track loaded plugins

files.forEach((file) => {
if (file.endsWith(".ts") && file !== "index.ts") {
const pluginPath = path.join(pluginDir, file);
const pluginName = path.basename(file, ".ts");

// Check if plugin has already been loaded
if (!loadedPlugins.has(pluginName)) {
try {
const plugin = require(pluginPath).default;
if (isValidPlugin(plugin)) {
plugins.push(plugin);
loadedPlugins.add(pluginName);
} else {
console.warn(`Invalid plugin structure in ${file}`);
}
} catch (error) {
console.error(`Error loading plugin ${file}:`, error);
}
}
}
});
};

const isValidPlugin = (plugin: any): plugin is Plugin => {
return (
plugin &&
typeof plugin.name === "string" &&
typeof plugin.keyword === "string" &&
typeof plugin.description === "string" &&
typeof plugin.execute === "function"
);
};

export const getPlugins = (): Plugin[] => {
return plugins;
};

export const mapPlugins = (userInput: string): Plugin | undefined => {
export const findPlugin = (userInput: string): Plugin | undefined => {
return plugins.find((plugin) => userInput.startsWith(plugin.keyword));
};

export const initializePlugins = () => {
// Register your plugins here
registerPlugin("exit", exitFunc);
registerPlugin("clear", clearFunc);
registerPlugin("@file", fileFunc);
registerPlugin("@web", webFunc);
export const executePlugin = async (
plugin: Plugin,
context: any
): Promise<any> => {
return await plugin.execute(context);
};

export const executeCommand = (userInput: string): boolean => {
const command = plugins.find((plugin) =>
userInput.startsWith(plugin.keyword)
);
if (command) {
command.execute(userInput);
return true;
}
return false;
export const initializePlugins = () => {
loadPlugins();
};

export default executeCommand;
// Load plugins when this module is imported
initializePlugins();
22 changes: 22 additions & 0 deletions src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import chalk from "chalk";
import { Plugin } from "./index";
import { getPlugins } from "./index";

const listPlugin: Plugin = {
name: "list",
keyword: "@list",
description: "Lists all available plugins",
execute: async () => {
console.log(chalk.cyan("Available plugins:"));
const plugins = getPlugins();
plugins.forEach((plugin) => {
console.log(
chalk.cyan(
`- ${plugin.name} : ${plugin.description} : ${plugin.keyword}`
)
);
});
},
};

export default listPlugin;
50 changes: 50 additions & 0 deletions src/commands/scrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import chalk from "chalk";
import { Plugin } from "./index";
import { handleWebResearch } from "../handlers/webHandler";
import { promptResponse } from "../utils";

const scrapperPlugin: Plugin = {
name: "scrapper",
keyword: "@scrapper",
description: "Scrapes / Reads a website and returns the content",
execute: async (context: {
userInput: string;
engine: string;
apiKey: string;
opts: any;
}) => {
const { userInput, engine, apiKey, opts } = context;
const url = userInput.slice(5).trim(); // Remove "@scrapper " from the input

if (url) {
try {
const researchResults = await handleWebResearch(url, userInput);
console.log(chalk.cyan("Web research results:"));
console.log(researchResults);

// Use the research results to generate a response
const enhancedPrompt = `Based on the following web research results, please provide a summary or answer:
${researchResults}

User query: ${url}`;

const response = await promptResponse(
engine,
apiKey,
enhancedPrompt,
opts
);
return response;
} catch (error) {
console.error(chalk.red(`Error during web research: ${error}`));
return `Error: ${error}`;
}
} else {
console.log(chalk.yellow("Please provide a URL. Usage: @scrapper <URL>"));
return "Error: No URL provided";
}
},
};

export default scrapperPlugin;
Loading
Loading