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)
This commit is contained in:
2026-01-31 22:22:04 -05:00
parent 37d4a43154
commit a3c407d89a
56 changed files with 7507 additions and 90 deletions

80
src/constants/hooks.ts Normal file
View File

@@ -0,0 +1,80 @@
/**
* Hook System Constants
*
* Constants for lifecycle hooks
*/
import type { HookEventType } from "@/types/hooks";
/**
* Hook configuration file name
*/
export const HOOKS_CONFIG_FILE = "hooks.json";
/**
* Default hook timeout in milliseconds
*/
export const DEFAULT_HOOK_TIMEOUT = 30000;
/**
* Hook exit codes and their meanings
*/
export const HOOK_EXIT_CODES = {
/** Allow execution to continue */
ALLOW: 0,
/** Warn but continue execution */
WARN: 1,
/** Block execution */
BLOCK: 2,
} as const;
/**
* Hook event type labels for display
*/
export const HOOK_EVENT_LABELS: Record<HookEventType, string> = {
PreToolUse: "Pre-Tool Use",
PostToolUse: "Post-Tool Use",
SessionStart: "Session Start",
SessionEnd: "Session End",
UserPromptSubmit: "User Prompt Submit",
Stop: "Stop",
};
/**
* Hook event type descriptions
*/
export const HOOK_EVENT_DESCRIPTIONS: Record<HookEventType, string> = {
PreToolUse: "Runs before a tool is executed. Can modify args or block execution.",
PostToolUse: "Runs after a tool completes. For notifications or logging.",
SessionStart: "Runs when a new session begins.",
SessionEnd: "Runs when a session ends.",
UserPromptSubmit: "Runs when user submits a prompt. Can modify or block.",
Stop: "Runs when execution is stopped (interrupt, complete, or error).",
};
/**
* All available hook event types
*/
export const HOOK_EVENT_TYPES: readonly HookEventType[] = [
"PreToolUse",
"PostToolUse",
"SessionStart",
"SessionEnd",
"UserPromptSubmit",
"Stop",
] as const;
/**
* Maximum output size from hook script in bytes
*/
export const MAX_HOOK_OUTPUT_SIZE = 1024 * 1024; // 1MB
/**
* Hook script execution shell
*/
export const HOOK_SHELL = "/bin/bash";
/**
* Environment variables passed to hooks
*/
export const HOOK_ENV_PREFIX = "CODETYPER_HOOK_";

109
src/constants/plugin.ts Normal file
View File

@@ -0,0 +1,109 @@
/**
* Plugin System Constants
*
* Constants for the plugin architecture
*/
import type { PluginCapability } from "@/types/plugin";
/**
* Plugin directory name
*/
export const PLUGINS_DIR = "plugins";
/**
* Plugin manifest file name
*/
export const PLUGIN_MANIFEST_FILE = "plugin.json";
/**
* Plugin subdirectories
*/
export const PLUGIN_SUBDIRS = {
tools: "tools",
commands: "commands",
hooks: "hooks",
} as const;
/**
* Plugin tool name prefix separator
*/
export const PLUGIN_TOOL_SEPARATOR = ":";
/**
* Maximum plugin load timeout in milliseconds
*/
export const PLUGIN_LOAD_TIMEOUT = 5000;
/**
* Maximum number of plugins to load
*/
export const MAX_PLUGINS = 50;
/**
* Default plugin capabilities
*/
export const DEFAULT_PLUGIN_CAPABILITIES: PluginCapability[] = [];
/**
* All available plugin capabilities
*/
export const ALL_PLUGIN_CAPABILITIES: PluginCapability[] = [
"filesystem",
"network",
"shell",
"mcp",
];
/**
* Plugin capability labels
*/
export const PLUGIN_CAPABILITY_LABELS: Record<PluginCapability, string> = {
filesystem: "File System Access",
network: "Network Access",
shell: "Shell Execution",
mcp: "MCP Integration",
};
/**
* Plugin capability descriptions
*/
export const PLUGIN_CAPABILITY_DESCRIPTIONS: Record<PluginCapability, string> = {
filesystem: "Can read and write files on disk",
network: "Can make network requests",
shell: "Can execute shell commands",
mcp: "Can interact with MCP servers",
};
/**
* Command file extension
*/
export const COMMAND_FILE_EXTENSION = ".md";
/**
* Tool file extension
*/
export const TOOL_FILE_EXTENSION = ".ts";
/**
* Hook script extensions
*/
export const HOOK_SCRIPT_EXTENSIONS = [".sh", ".bash"];
/**
* Command frontmatter delimiter
*/
export const COMMAND_FRONTMATTER_DELIMITER = "---";
/**
* Plugin load errors
*/
export const PLUGIN_ERRORS = {
MANIFEST_NOT_FOUND: "Plugin manifest not found",
MANIFEST_INVALID: "Plugin manifest is invalid",
TOOL_LOAD_FAILED: "Failed to load tool",
COMMAND_LOAD_FAILED: "Failed to load command",
HOOK_LOAD_FAILED: "Failed to load hook",
DUPLICATE_TOOL: "Tool name already exists",
DUPLICATE_COMMAND: "Command name already exists",
} as const;

View File

@@ -0,0 +1,104 @@
/**
* Session Fork Constants
*
* Constants for session snapshots and forks
*/
/**
* File extension for fork files
*/
export const FORK_FILE_EXTENSION = ".fork.json";
/**
* Main fork name
*/
export const MAIN_FORK_NAME = "main";
/**
* Default snapshot name prefix
*/
export const DEFAULT_SNAPSHOT_PREFIX = "snapshot";
/**
* Maximum snapshots per fork
*/
export const MAX_SNAPSHOTS_PER_FORK = 100;
/**
* Maximum forks per session
*/
export const MAX_FORKS_PER_SESSION = 50;
/**
* Fork file version for migration
*/
export const FORK_FILE_VERSION = 1;
/**
* Session fork directory name
*/
export const FORKS_SUBDIR = "sessions";
/**
* Auto-snapshot triggers
*/
export const AUTO_SNAPSHOT_TRIGGERS = {
/** Messages since last snapshot to trigger auto-snapshot */
MESSAGE_THRESHOLD: 10,
/** Time in ms since last snapshot to trigger auto-snapshot */
TIME_THRESHOLD: 300000, // 5 minutes
} as const;
/**
* Commit message templates
*/
export const COMMIT_MESSAGE_TEMPLATES = {
/** Template for code changes */
CODE: "feat(session): {summary} [{count} messages]",
/** Template for fix changes */
FIX: "fix(session): {summary} [{count} messages]",
/** Template for refactor changes */
REFACTOR: "refactor(session): {summary} [{count} messages]",
/** Template for docs changes */
DOCS: "docs(session): {summary} [{count} messages]",
/** Default template */
DEFAULT: "chore(session): {summary} [{count} messages]",
} as const;
/**
* Keywords for detecting commit types
*/
export const COMMIT_TYPE_KEYWORDS = {
CODE: ["implement", "add", "create", "build", "feature"],
FIX: ["fix", "bug", "resolve", "correct", "patch"],
REFACTOR: ["refactor", "restructure", "reorganize", "improve"],
DOCS: ["document", "readme", "comment", "explain"],
} as const;
/**
* Fork command names
*/
export const FORK_COMMANDS = {
SNAPSHOT: "/snapshot",
REWIND: "/rewind",
FORK: "/fork",
FORKS: "/forks",
SWITCH: "/switch",
} as const;
/**
* Error messages for fork operations
*/
export const FORK_ERRORS = {
SESSION_NOT_FOUND: "Session not found",
SNAPSHOT_NOT_FOUND: "Snapshot not found",
FORK_NOT_FOUND: "Fork not found",
MAX_SNAPSHOTS_REACHED: "Maximum snapshots per fork reached",
MAX_FORKS_REACHED: "Maximum forks per session reached",
INVALID_SNAPSHOT_NAME: "Invalid snapshot name",
INVALID_FORK_NAME: "Invalid fork name",
DUPLICATE_SNAPSHOT_NAME: "Snapshot name already exists",
DUPLICATE_FORK_NAME: "Fork name already exists",
CANNOT_REWIND_TO_FUTURE: "Cannot rewind to a future snapshot",
NO_SNAPSHOTS_TO_REWIND: "No snapshots to rewind to",
} as const;

155
src/constants/vim.ts Normal file
View File

@@ -0,0 +1,155 @@
/**
* Vim Mode Constants
*
* Constants for vim-style navigation and editing
*/
import type { VimMode, VimKeyBinding, VimConfig } from "@/types/vim";
/**
* Mode labels for display
*/
export const VIM_MODE_LABELS: Record<VimMode, string> = {
normal: "NORMAL",
insert: "INSERT",
command: "COMMAND",
visual: "VISUAL",
};
/**
* Mode colors for display
*/
export const VIM_MODE_COLORS: Record<VimMode, string> = {
normal: "blue",
insert: "green",
command: "yellow",
visual: "magenta",
};
/**
* Mode hints for status line
*/
export const VIM_MODE_HINTS: Record<VimMode, string> = {
normal: "j/k scroll, i insert, : command",
insert: "Esc to normal",
command: "Enter to execute, Esc to cancel",
visual: "y yank, d delete, Esc cancel",
};
/**
* Default key bindings
*/
export const VIM_DEFAULT_BINDINGS: VimKeyBinding[] = [
// Normal mode - Navigation
{ key: "j", mode: "normal", action: "scroll_down", description: "Scroll down" },
{ key: "k", mode: "normal", action: "scroll_up", description: "Scroll up" },
{ key: "d", mode: "normal", action: "scroll_half_down", ctrl: true, description: "Half page down" },
{ key: "u", mode: "normal", action: "scroll_half_up", ctrl: true, description: "Half page up" },
{ key: "g", mode: "normal", action: "goto_top", description: "Go to top (gg)" },
{ key: "G", mode: "normal", action: "goto_bottom", shift: true, description: "Go to bottom" },
// Normal mode - Mode switching
{ key: "i", mode: "normal", action: "enter_insert", description: "Enter insert mode" },
{ key: "a", mode: "normal", action: "enter_insert", description: "Append (enter insert)" },
{ key: ":", mode: "normal", action: "enter_command", description: "Enter command mode" },
{ key: "v", mode: "normal", action: "enter_visual", description: "Enter visual mode" },
// Normal mode - Search
{ key: "/", mode: "normal", action: "search_start", description: "Start search" },
{ key: "n", mode: "normal", action: "search_next", description: "Next search match" },
{ key: "N", mode: "normal", action: "search_prev", shift: true, description: "Previous search match" },
// Normal mode - Word navigation
{ key: "w", mode: "normal", action: "word_forward", description: "Next word" },
{ key: "b", mode: "normal", action: "word_backward", description: "Previous word" },
{ key: "0", mode: "normal", action: "line_start", description: "Line start" },
{ key: "$", mode: "normal", action: "line_end", description: "Line end" },
// Normal mode - Edit operations
{ key: "y", mode: "normal", action: "yank", description: "Yank (copy)" },
{ key: "p", mode: "normal", action: "paste", description: "Paste" },
{ key: "u", mode: "normal", action: "undo", description: "Undo" },
{ key: "r", mode: "normal", action: "redo", ctrl: true, description: "Redo" },
// Insert mode
{ key: "escape", mode: "insert", action: "exit_mode", description: "Exit to normal mode" },
// Command mode
{ key: "escape", mode: "command", action: "cancel", description: "Cancel command" },
{ key: "return", mode: "command", action: "execute_command", description: "Execute command" },
// Visual mode
{ key: "escape", mode: "visual", action: "exit_mode", description: "Exit visual mode" },
{ key: "y", mode: "visual", action: "yank", description: "Yank selection" },
{ key: "d", mode: "visual", action: "delete", description: "Delete selection" },
];
/**
* Vim commands (: commands)
*/
export const VIM_COMMANDS = {
QUIT: "q",
QUIT_FORCE: "q!",
WRITE: "w",
WRITE_QUIT: "wq",
HELP: "help",
SET: "set",
NOHL: "nohl",
SEARCH: "/",
} as const;
/**
* Vim command aliases
*/
export const VIM_COMMAND_ALIASES: Record<string, string> = {
quit: "q",
exit: "q",
write: "w",
save: "w",
wq: "wq",
x: "wq",
};
/**
* Default vim configuration
*/
export const DEFAULT_VIM_CONFIG: VimConfig = {
enabled: true,
startInNormalMode: true,
showModeIndicator: true,
searchHighlights: true,
};
/**
* Scroll amounts
*/
export const VIM_SCROLL_AMOUNTS = {
/** Lines to scroll with j/k */
LINE: 1,
/** Lines to scroll with Ctrl+d/u */
HALF_PAGE: 10,
/** Lines to scroll with Ctrl+f/b */
FULL_PAGE: 20,
} as const;
/**
* Settings key in config
*/
export const VIM_SETTINGS_KEY = "vim";
/**
* Escape key codes
*/
export const ESCAPE_KEYS = ["escape", "\x1b", "\u001b"];
/**
* Special key names
*/
export const SPECIAL_KEYS = {
ESCAPE: "escape",
RETURN: "return",
BACKSPACE: "backspace",
DELETE: "delete",
TAB: "tab",
SPACE: "space",
} as const;

View File

@@ -0,0 +1,41 @@
/**
* Web Search Tool Constants
*/
export const WEB_SEARCH_DEFAULTS = {
MAX_RESULTS: 10,
TIMEOUT_MS: 15000,
USER_AGENT:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
} as const;
export const WEB_SEARCH_MESSAGES = {
SEARCHING: (query: string) => `Searching: "${query}"`,
NO_RESULTS: "No results found",
SEARCH_ERROR: (error: string) => `Search failed: ${error}`,
TIMEOUT: "Search timed out",
} as const;
export const WEB_SEARCH_TITLES = {
SEARCHING: (query: string) => `Searching: ${query}`,
RESULTS: (count: number) => `Found ${count} result(s)`,
FAILED: "Search failed",
NO_RESULTS: "No results",
} as const;
export const WEB_SEARCH_DESCRIPTION = `Search the web for information.
Use this tool to:
- Find documentation
- Look up error messages
- Research libraries and APIs
- Get current information not in your training data
Parameters:
- query: The search query string
- maxResults: Maximum number of results to return (default: 5)
Example:
- Search for "TypeScript generics tutorial"
- Search for "React useEffect cleanup function"
- Search for "bun test framework documentation"`;