Files
codetyper.cli/src/services/plugin-service.ts
Carlos Gutierrez a3c407d89a feat: implement hooks, plugins, session forks, and vim motions
Add 4 major features to codetyper-cli:

- Hooks System: Lifecycle hooks (PreToolUse, PostToolUse, SessionStart,
  SessionEnd, UserPromptSubmit, Stop) with exit code control flow
- Plugin System: Custom tools, commands, and hooks via plugin manifest
- Session Forking: Snapshots, rewind, fork, and switch between branches
- Vim Motions: Normal/Insert/Command/Visual modes with keyboard navigation

New files:
- src/types/{hooks,plugin,session-fork,vim}.ts
- src/constants/{hooks,plugin,session-fork,vim}.ts
- src/services/{hooks-service,plugin-loader,plugin-service,session-fork-service}.ts
- src/stores/vim-store.ts (vanilla)
- src/tui/hooks/{useVimMode,useVimStore,useTodoStore,useThemeStore}.ts
- src/tui/components/VimStatusLine.tsx

Modified:
- src/services/agent.ts (hook integration)
- src/tools/index.ts (plugin tool registration)
- src/stores/{todo-store,theme-store}.ts (converted to vanilla)
- TUI components (updated hook imports)
2026-01-31 22:22:04 -05:00

279 lines
5.4 KiB
TypeScript

/**
* Plugin Service
*
* Manages plugin lifecycle and provides access to plugin tools and commands
*/
import type {
LoadedPlugin,
PluginRegistry,
PluginCommandDefinition,
PluginLoadResult,
} from "@/types/plugin";
import type { FunctionDefinition, ToolDefinition } from "@tools/types";
import type { HookDefinition } from "@/types/hooks";
import {
discoverPlugins,
parseManifest,
loadPluginTools,
loadPluginCommands,
loadPluginHooks,
} from "@services/plugin-loader";
import {
PLUGIN_TOOL_SEPARATOR,
PLUGIN_ERRORS,
} from "@constants/plugin";
/**
* Plugin registry singleton
*/
const registry: PluginRegistry = {
plugins: new Map(),
tools: new Map(),
commands: new Map(),
initialized: false,
};
/**
* Load a single plugin
*/
const loadPlugin = async (
_name: string,
path: string,
manifestPath: string
): Promise<PluginLoadResult> => {
const manifest = await parseManifest(manifestPath);
if (!manifest) {
return {
success: false,
error: PLUGIN_ERRORS.MANIFEST_INVALID,
};
}
const [tools, commands, hooks] = await Promise.all([
loadPluginTools(path, manifest),
loadPluginCommands(path, manifest),
loadPluginHooks(path, manifest),
]);
const plugin: LoadedPlugin = {
manifest,
path,
tools,
commands,
hooks,
enabled: true,
};
return {
success: true,
plugin,
};
};
/**
* Initialize the plugin system
*/
export const initializePlugins = async (workingDir: string): Promise<void> => {
if (registry.initialized) {
return;
}
const discoveredPlugins = await discoverPlugins(workingDir);
for (const discovered of discoveredPlugins) {
const result = await loadPlugin(
discovered.name,
discovered.path,
discovered.manifestPath
);
if (result.success && result.plugin) {
registry.plugins.set(discovered.name, result.plugin);
// Register tools with prefixed names
for (const [toolName, toolDef] of result.plugin.tools) {
const prefixedName = `${discovered.name}${PLUGIN_TOOL_SEPARATOR}${toolName}`;
registry.tools.set(prefixedName, toolDef);
}
// Register commands
for (const [cmdName, cmdDef] of result.plugin.commands) {
registry.commands.set(cmdName, cmdDef);
}
}
}
registry.initialized = true;
};
/**
* Refresh plugins (reload all)
*/
export const refreshPlugins = async (workingDir: string): Promise<void> => {
registry.plugins.clear();
registry.tools.clear();
registry.commands.clear();
registry.initialized = false;
await initializePlugins(workingDir);
};
/**
* Check if a tool is a plugin tool
*/
export const isPluginTool = (name: string): boolean => {
return registry.tools.has(name);
};
/**
* Get a plugin tool by name
*/
export const getPluginTool = (name: string): ToolDefinition | undefined => {
const pluginTool = registry.tools.get(name);
if (!pluginTool) {
return undefined;
}
return pluginTool as unknown as ToolDefinition;
};
/**
* Get all plugin tools for API
*/
export const getPluginToolsForApi = (): {
type: "function";
function: FunctionDefinition;
}[] => {
const tools: {
type: "function";
function: FunctionDefinition;
}[] = [];
for (const [name, tool] of registry.tools) {
tools.push({
type: "function",
function: {
name,
description: tool.description,
parameters: {
type: "object",
properties: {},
},
},
});
}
return tools;
};
/**
* Get a plugin command by name
*/
export const getPluginCommand = (
name: string
): PluginCommandDefinition | undefined => {
return registry.commands.get(name);
};
/**
* Check if a command is a plugin command
*/
export const isPluginCommand = (name: string): boolean => {
return registry.commands.has(name);
};
/**
* Get all plugin commands
*/
export const getAllPluginCommands = (): PluginCommandDefinition[] => {
return Array.from(registry.commands.values());
};
/**
* Get all plugin hooks
*/
export const getAllPluginHooks = (): HookDefinition[] => {
const hooks: HookDefinition[] = [];
for (const plugin of registry.plugins.values()) {
if (plugin.enabled) {
hooks.push(...plugin.hooks);
}
}
return hooks;
};
/**
* Get all loaded plugins
*/
export const getAllPlugins = (): LoadedPlugin[] => {
return Array.from(registry.plugins.values());
};
/**
* Get a specific plugin by name
*/
export const getPlugin = (name: string): LoadedPlugin | undefined => {
return registry.plugins.get(name);
};
/**
* Enable a plugin
*/
export const enablePlugin = (name: string): boolean => {
const plugin = registry.plugins.get(name);
if (!plugin) {
return false;
}
plugin.enabled = true;
return true;
};
/**
* Disable a plugin
*/
export const disablePlugin = (name: string): boolean => {
const plugin = registry.plugins.get(name);
if (!plugin) {
return false;
}
plugin.enabled = false;
return true;
};
/**
* Check if plugins are initialized
*/
export const isPluginsInitialized = (): boolean => {
return registry.initialized;
};
/**
* Get plugin count
*/
export const getPluginCount = (): number => {
return registry.plugins.size;
};
/**
* Get plugin tool count
*/
export const getPluginToolCount = (): number => {
return registry.tools.size;
};
/**
* Get plugin command count
*/
export const getPluginCommandCount = (): number => {
return registry.commands.size;
};