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:
80
src/constants/hooks.ts
Normal file
80
src/constants/hooks.ts
Normal 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
109
src/constants/plugin.ts
Normal 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;
|
||||
104
src/constants/session-fork.ts
Normal file
104
src/constants/session-fork.ts
Normal 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
155
src/constants/vim.ts
Normal 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;
|
||||
41
src/constants/web-search.ts
Normal file
41
src/constants/web-search.ts
Normal 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"`;
|
||||
Reference in New Issue
Block a user