From c839fc4d6814aa66fa0d3a2413c9c3b5df2504d5 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Mon, 2 Feb 2026 13:25:38 -0500 Subject: [PATCH] Add BRAIN_DISABLED flag and fix Ollama tool call formatting Features: - Add BRAIN_DISABLED feature flag to hide all Brain functionality - When enabled, hides Brain banner, status indicator, menu, and commands - Flag location: src/constants/brain.ts Fixes: - Fix Ollama 400 error by properly formatting tool_calls in messages - Update OllamaMessage type to include tool_calls field - Fix Brain menu keyboard not working (add missing modes to isMenuOpen) UI Changes: - Remove "^Tab toggle mode" hint from status bar - Remove "ctrl+t to hide todos" hint from status bar Files modified: - src/constants/brain.ts (add BRAIN_DISABLED flag) - src/types/ollama.ts (add tool_calls to OllamaMessage) - src/providers/ollama/chat.ts (format tool_calls in messages) - src/tui-solid/components/header.tsx (hide Brain UI when disabled) - src/tui-solid/components/status-bar.tsx (remove hints) - src/tui-solid/components/command-menu.tsx (filter brain command) - src/tui-solid/components/input-area.tsx (fix isMenuOpen modes) - src/tui-solid/routes/session.tsx (skip brain menu when disabled) - src/services/brain.ts (early return when disabled) - src/services/chat-tui/initialize.ts (skip brain init when disabled) --- README.md | 8 +- assets/Codetyper_logo.png | Bin 0 -> 489752 bytes assets/ascii-art.txt | 55 ++ src/api/brain/index.ts | 367 ++++++++++ src/api/index.ts | 1 + src/commands/components/execute/execute.tsx | 43 ++ src/constants/agent-definition.ts | 66 ++ src/constants/apply-patch.ts | 100 +++ src/constants/background-task.ts | 62 ++ src/constants/brain-cloud.ts | 107 +++ src/constants/brain-mcp.ts | 75 ++ src/constants/brain-project.ts | 69 ++ src/constants/brain.ts | 94 +++ src/constants/confidence-filter.ts | 33 + src/constants/feature-dev.ts | 275 +++++++ src/constants/help-content.ts | 8 +- src/constants/home.ts | 23 +- src/constants/multi-edit.ts | 54 ++ src/constants/parallel.ts | 108 +++ src/constants/paths.ts | 6 + src/constants/pr-review.ts | 207 ++++++ src/constants/skills.ts | 132 ++++ src/constants/token.ts | 55 ++ src/constants/tui-components.ts | 7 + src/constants/web-fetch.ts | 75 ++ src/prompts/system/ask.ts | 2 +- src/providers/copilot/chat.ts | 1 + src/providers/copilot/token.ts | 46 +- src/providers/ollama/chat.ts | 35 +- src/services/agent-definition-loader.ts | 289 ++++++++ src/services/background-task-service.ts | 389 ++++++++++ src/services/brain.ts | 688 ++++++++++++++++++ src/services/brain/cloud-sync.ts | 523 +++++++++++++ src/services/brain/conflict-resolver.ts | 249 +++++++ src/services/brain/mcp-server.ts | 354 +++++++++ src/services/brain/offline-queue.ts | 270 +++++++ src/services/brain/project-service.ts | 384 ++++++++++ src/services/chat-tui/initialize.ts | 58 +- src/services/chat-tui/message-handler.ts | 2 +- src/services/confidence-filter.ts | 209 ++++++ src/services/config.ts | 42 +- .../feature-dev/checkpoint-handler.ts | 209 ++++++ src/services/feature-dev/context-builder.ts | 292 ++++++++ src/services/feature-dev/index.ts | 290 ++++++++ src/services/feature-dev/phase-executor.ts | 345 +++++++++ src/services/index.ts | 1 + src/services/model-routing.ts | 225 ++++++ src/services/parallel/conflict-detector.ts | 241 ++++++ src/services/parallel/index.ts | 351 +++++++++ src/services/parallel/resource-manager.ts | 274 +++++++ src/services/parallel/result-aggregator.ts | 280 +++++++ src/services/pr-review/diff-parser.ts | 309 ++++++++ src/services/pr-review/index.ts | 215 ++++++ src/services/pr-review/report-generator.ts | 410 +++++++++++ src/services/pr-review/reviewers/logic.ts | 240 ++++++ .../pr-review/reviewers/performance.ts | 208 ++++++ src/services/pr-review/reviewers/security.ts | 182 +++++ src/services/pr-review/reviewers/style.ts | 267 +++++++ src/services/prompt-builder.ts | 51 +- src/services/session-compaction.ts | 318 ++++++++ src/services/skill-loader.ts | 435 +++++++++++ src/services/skill-registry.ts | 407 +++++++++++ src/skills/commit/SKILL.md | 107 +++ src/skills/explain/SKILL.md | 217 ++++++ src/skills/feature-dev/SKILL.md | 254 +++++++ src/skills/review-pr/SKILL.md | 200 +++++ src/tools/apply-patch/execute.ts | 366 ++++++++++ src/tools/apply-patch/index.ts | 60 ++ src/tools/apply-patch/matcher.ts | 304 ++++++++ src/tools/apply-patch/params.ts | 43 ++ src/tools/apply-patch/parser.ts | 387 ++++++++++ src/tools/index.ts | 10 + src/tools/multi-edit/execute.ts | 343 +++++++++ src/tools/multi-edit/index.ts | 13 + src/tools/multi-edit/params.ts | 21 + src/tools/web-fetch/execute.ts | 346 +++++++++ src/tools/web-fetch/index.ts | 8 + src/tools/web-fetch/params.ts | 19 + src/tools/web-search/execute.ts | 108 +-- src/tui-solid/app.tsx | 20 +- src/tui-solid/components/brain-menu.tsx | 411 +++++++++++ src/tui-solid/components/command-menu.tsx | 10 +- src/tui-solid/components/debug-log-panel.tsx | 4 +- src/tui-solid/components/header.tsx | 230 ++++-- src/tui-solid/components/input-area.tsx | 12 +- src/tui-solid/components/log-panel.tsx | 21 +- src/tui-solid/components/status-bar.tsx | 9 - src/tui-solid/context/app.tsx | 107 +++ src/tui-solid/routes/home.tsx | 3 +- src/tui-solid/routes/session.tsx | 37 + src/tui/App.tsx | 95 ++- src/tui/components/CommandMenu.tsx | 72 +- src/tui/components/MCPSelect.tsx | 57 +- src/tui/components/home/SessionHeader.tsx | 145 +++- src/tui/components/log-panel/index.tsx | 12 +- src/tui/store.ts | 75 ++ src/types/agent-definition.ts | 91 +++ src/types/apply-patch.ts | 145 ++++ src/types/background-task.ts | 113 +++ src/types/brain-cloud.ts | 194 +++++ src/types/brain-mcp.ts | 228 ++++++ src/types/brain-project.ts | 113 +++ src/types/brain.ts | 232 ++++++ src/types/confidence-filter.ts | 61 ++ src/types/feature-dev.ts | 236 ++++++ src/types/home-screen.ts | 11 + src/types/hooks.ts | 13 + src/types/index.ts | 233 ++++++ src/types/ollama.ts | 1 + src/types/parallel.ts | 173 +++++ src/types/pr-review.ts | 230 ++++++ src/types/skills.ts | 127 ++++ src/types/tui.ts | 27 +- src/types/usage.ts | 41 ++ 114 files changed, 17243 insertions(+), 273 deletions(-) create mode 100644 assets/Codetyper_logo.png create mode 100644 assets/ascii-art.txt create mode 100644 src/api/brain/index.ts create mode 100644 src/constants/agent-definition.ts create mode 100644 src/constants/apply-patch.ts create mode 100644 src/constants/background-task.ts create mode 100644 src/constants/brain-cloud.ts create mode 100644 src/constants/brain-mcp.ts create mode 100644 src/constants/brain-project.ts create mode 100644 src/constants/brain.ts create mode 100644 src/constants/confidence-filter.ts create mode 100644 src/constants/feature-dev.ts create mode 100644 src/constants/multi-edit.ts create mode 100644 src/constants/parallel.ts create mode 100644 src/constants/pr-review.ts create mode 100644 src/constants/skills.ts create mode 100644 src/constants/token.ts create mode 100644 src/constants/web-fetch.ts create mode 100644 src/services/agent-definition-loader.ts create mode 100644 src/services/background-task-service.ts create mode 100644 src/services/brain.ts create mode 100644 src/services/brain/cloud-sync.ts create mode 100644 src/services/brain/conflict-resolver.ts create mode 100644 src/services/brain/mcp-server.ts create mode 100644 src/services/brain/offline-queue.ts create mode 100644 src/services/brain/project-service.ts create mode 100644 src/services/confidence-filter.ts create mode 100644 src/services/feature-dev/checkpoint-handler.ts create mode 100644 src/services/feature-dev/context-builder.ts create mode 100644 src/services/feature-dev/index.ts create mode 100644 src/services/feature-dev/phase-executor.ts create mode 100644 src/services/model-routing.ts create mode 100644 src/services/parallel/conflict-detector.ts create mode 100644 src/services/parallel/index.ts create mode 100644 src/services/parallel/resource-manager.ts create mode 100644 src/services/parallel/result-aggregator.ts create mode 100644 src/services/pr-review/diff-parser.ts create mode 100644 src/services/pr-review/index.ts create mode 100644 src/services/pr-review/report-generator.ts create mode 100644 src/services/pr-review/reviewers/logic.ts create mode 100644 src/services/pr-review/reviewers/performance.ts create mode 100644 src/services/pr-review/reviewers/security.ts create mode 100644 src/services/pr-review/reviewers/style.ts create mode 100644 src/services/session-compaction.ts create mode 100644 src/services/skill-loader.ts create mode 100644 src/services/skill-registry.ts create mode 100644 src/skills/commit/SKILL.md create mode 100644 src/skills/explain/SKILL.md create mode 100644 src/skills/feature-dev/SKILL.md create mode 100644 src/skills/review-pr/SKILL.md create mode 100644 src/tools/apply-patch/execute.ts create mode 100644 src/tools/apply-patch/index.ts create mode 100644 src/tools/apply-patch/matcher.ts create mode 100644 src/tools/apply-patch/params.ts create mode 100644 src/tools/apply-patch/parser.ts create mode 100644 src/tools/multi-edit/execute.ts create mode 100644 src/tools/multi-edit/index.ts create mode 100644 src/tools/multi-edit/params.ts create mode 100644 src/tools/web-fetch/execute.ts create mode 100644 src/tools/web-fetch/index.ts create mode 100644 src/tools/web-fetch/params.ts create mode 100644 src/tui-solid/components/brain-menu.tsx create mode 100644 src/types/agent-definition.ts create mode 100644 src/types/apply-patch.ts create mode 100644 src/types/background-task.ts create mode 100644 src/types/brain-cloud.ts create mode 100644 src/types/brain-mcp.ts create mode 100644 src/types/brain-project.ts create mode 100644 src/types/brain.ts create mode 100644 src/types/confidence-filter.ts create mode 100644 src/types/feature-dev.ts create mode 100644 src/types/parallel.ts create mode 100644 src/types/pr-review.ts create mode 100644 src/types/skills.ts diff --git a/README.md b/README.md index 62df09a..2720428 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # CodeTyper CLI -An AI-powered terminal coding agent with an interactive TUI. CodeTyper autonomously executes coding tasks using tool calls with granular permission controls and intelligent provider routing. +

+ CodeTyper Logo +

-![CodeTyper Welcome Screen](assets/CodetyperLogin.png) +An AI-powered terminal coding agent with an interactive TUI. CodeTyper autonomously executes coding tasks using tool calls with granular permission controls and intelligent provider routing. ## How It Works @@ -85,7 +87,7 @@ Full-screen terminal interface with real-time streaming responses. - `Enter` - Send message - `Shift+Enter` - New line - `/` - Open command menu -- `Ctrl+Tab` - Toggle interaction mode +- `Ctrl+M` - Toggle interaction mode - `Ctrl+T` - Toggle todo panel - `Shift+Up/Down` - Scroll log panel - `Ctrl+C` (twice) - Exit diff --git a/assets/Codetyper_logo.png b/assets/Codetyper_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d79cec4ddf3e5d00ac276a8e8f3ff82219b1cb23 GIT binary patch literal 489752 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

-Wswl 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

b*G&uL>0cF^%97!VhHK9vjI4LVTHY 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

Rbtd&XZpymRTK3O4M~Y>e$n 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 z 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

4j%qjFh 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 doaiXk4D$V>t5 => ({ + [BRAIN_HEADERS.CONTENT_TYPE]: "application/json", + ...(apiKey ? { [BRAIN_HEADERS.API_KEY]: apiKey } : {}), + ...(accessToken + ? { [BRAIN_HEADERS.AUTHORIZATION]: `Bearer ${accessToken}` } + : {}), +}); + +/** + * Get base URL for Brain API + */ +const getBaseUrl = (customUrl?: string): string => { + return customUrl ?? BRAIN_DEFAULTS.BASE_URL; +}; + +// ============================================================================ +// Health Check +// ============================================================================ + +/** + * Check if Brain service is healthy + */ +export const checkHealth = async ( + baseUrl?: string, +): Promise => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.HEALTH}`; + const response = await got + .get(url, { + timeout: { request: BRAIN_TIMEOUTS.HEALTH }, + }) + .json(); + + return response; +}; + +// ============================================================================ +// Authentication +// ============================================================================ + +/** + * Register a new user + */ +export const register = async ( + email: string, + password: string, + displayName: string, + baseUrl?: string, +): Promise => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.AUTH_REGISTER}`; + const response = await got + .post(url, { + json: { email, password, display_name: displayName }, + timeout: { request: BRAIN_TIMEOUTS.AUTH }, + }) + .json(); + + return response; +}; + +/** + * Login with email and password + */ +export const login = async ( + email: string, + password: string, + baseUrl?: string, +): Promise => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.AUTH_LOGIN}`; + const response = await got + .post(url, { + json: { email, password }, + timeout: { request: BRAIN_TIMEOUTS.AUTH }, + }) + .json(); + + return response; +}; + +/** + * Logout (revoke refresh token) + */ +export const logout = async ( + refreshToken: string, + baseUrl?: string, +): Promise => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.AUTH_LOGOUT}`; + await got.post(url, { + json: { refresh_token: refreshToken }, + timeout: { request: BRAIN_TIMEOUTS.AUTH }, + }); +}; + +/** + * Refresh access token + */ +export const refreshToken = async ( + refreshTokenValue: string, + baseUrl?: string, +): Promise => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.AUTH_REFRESH}`; + const response = await got + .post(url, { + json: { refresh_token: refreshTokenValue }, + timeout: { request: BRAIN_TIMEOUTS.AUTH }, + }) + .json(); + + return response; +}; + +/** + * Get current authenticated user + */ +export const getCurrentUser = async ( + accessToken: string, + baseUrl?: string, +): Promise> => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.AUTH_ME}`; + const response = await got + .get(url, { + ...{ headers: buildHeaders(undefined, accessToken) }, + timeout: { request: BRAIN_TIMEOUTS.AUTH }, + }) + .json>(); + + return response; +}; + +// ============================================================================ +// Knowledge Graph +// ============================================================================ + +/** + * Recall relevant concepts from the knowledge graph + */ +export const recallKnowledge = async ( + request: BrainRecallRequest, + apiKey: string, + baseUrl?: string, +): Promise => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.KNOWLEDGE_RECALL}`; + const response = await got + .post(url, { + ...{ headers: buildHeaders(apiKey) }, + json: request, + timeout: { request: BRAIN_TIMEOUTS.KNOWLEDGE }, + }) + .json(); + + return response; +}; + +/** + * Learn/store a concept in the knowledge graph + */ +export const learnConcept = async ( + request: BrainLearnConceptRequest, + apiKey: string, + baseUrl?: string, +): Promise> => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.KNOWLEDGE_LEARN}`; + const response = await got + .post(url, { + ...{ headers: buildHeaders(apiKey) }, + json: request, + timeout: { request: BRAIN_TIMEOUTS.KNOWLEDGE }, + }) + .json>(); + + return response; +}; + +/** + * Build context string for prompt injection + */ +export const buildContext = async ( + request: BrainContextRequest, + apiKey: string, + baseUrl?: string, +): Promise => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.KNOWLEDGE_CONTEXT}`; + const response = await got + .post(url, { + ...{ headers: buildHeaders(apiKey) }, + json: request, + timeout: { request: BRAIN_TIMEOUTS.KNOWLEDGE }, + }) + .json(); + + return response; +}; + +/** + * Extract concepts from text content + */ +export const extractConcepts = async ( + request: BrainExtractRequest, + apiKey: string, + baseUrl?: string, +): Promise => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.KNOWLEDGE_EXTRACT}`; + const response = await got + .post(url, { + ...{ headers: buildHeaders(apiKey) }, + json: request, + timeout: { request: BRAIN_TIMEOUTS.EXTRACT }, + }) + .json(); + + return response; +}; + +/** + * Get knowledge stats for a project + */ +export const getKnowledgeStats = async ( + projectId: number, + apiKey: string, + baseUrl?: string, +): Promise> => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.KNOWLEDGE_STATS}?project_id=${projectId}`; + const response = await got + .get(url, { + ...{ headers: buildHeaders(apiKey) }, + timeout: { request: BRAIN_TIMEOUTS.KNOWLEDGE }, + }) + .json>(); + + return response; +}; + +/** + * List all concepts for a project + */ +export const listConcepts = async ( + projectId: number, + apiKey: string, + baseUrl?: string, +): Promise> => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.KNOWLEDGE_CONCEPTS}?project_id=${projectId}`; + const response = await got + .get(url, { + ...{ headers: buildHeaders(apiKey) }, + timeout: { request: BRAIN_TIMEOUTS.KNOWLEDGE }, + }) + .json>(); + + return response; +}; + +// ============================================================================ +// Memory +// ============================================================================ + +/** + * Search for relevant memories + */ +export const searchMemories = async ( + request: BrainMemorySearchRequest, + apiKey: string, + baseUrl?: string, +): Promise => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.MEMORY_SEARCH}`; + const response = await got + .post(url, { + ...{ headers: buildHeaders(apiKey) }, + json: request, + timeout: { request: BRAIN_TIMEOUTS.MEMORY }, + }) + .json(); + + return response; +}; + +/** + * Store a memory + */ +export const storeMemory = async ( + request: BrainStoreMemoryRequest, + apiKey: string, + baseUrl?: string, +): Promise> => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.MEMORY_STORE}`; + const response = await got + .post(url, { + ...{ headers: buildHeaders(apiKey) }, + json: request, + timeout: { request: BRAIN_TIMEOUTS.MEMORY }, + }) + .json>(); + + return response; +}; + +/** + * Get memory stats + */ +export const getMemoryStats = async ( + apiKey: string, + baseUrl?: string, +): Promise => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.MEMORY_STATS}`; + const response = await got + .get(url, { + ...{ headers: buildHeaders(apiKey) }, + timeout: { request: BRAIN_TIMEOUTS.MEMORY }, + }) + .json(); + + return response; +}; + +/** + * Check memory status + */ +export const checkMemoryStatus = async ( + baseUrl?: string, +): Promise> => { + const url = `${getBaseUrl(baseUrl)}${BRAIN_ENDPOINTS.MEMORY_STATUS}`; + const response = await got + .get(url, { + timeout: { request: BRAIN_TIMEOUTS.MEMORY }, + }) + .json>(); + + return response; +}; diff --git a/src/api/index.ts b/src/api/index.ts index cb93198..e92078a 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -7,3 +7,4 @@ export * as copilotApi from "@api/copilot"; export * as ollamaApi from "@api/ollama"; +export * as brainApi from "@api/brain"; diff --git a/src/commands/components/execute/execute.tsx b/src/commands/components/execute/execute.tsx index 3c30ac4..a81b07d 100644 --- a/src/commands/components/execute/execute.tsx +++ b/src/commands/components/execute/execute.tsx @@ -1,6 +1,7 @@ import { tui, appStore } from "@tui/index"; import { getProviderInfo } from "@services/chat-tui-service"; import { addServer, connectServer } from "@services/mcp/index"; +import * as brainService from "@services/brain"; import type { ChatServiceState } from "@services/chat-tui-service"; import type { AgentConfig } from "@/types/agent-config"; import type { PermissionScope, LearningScope } from "@/types/tui"; @@ -32,6 +33,9 @@ export interface RenderAppProps { scope?: LearningScope, editedContent?: string, ) => void; + handleBrainSetJwtToken?: (jwtToken: string) => Promise; + handleBrainSetApiKey?: (apiKey: string) => Promise; + handleBrainLogout?: () => Promise; handleExit: () => void; showBanner: boolean; state: ChatServiceState; @@ -65,6 +69,42 @@ const defaultHandleMCPAdd = async (data: MCPAddFormData): Promise => { await connectServer(data.name); }; +const defaultHandleBrainSetJwtToken = async (jwtToken: string): Promise => { + await brainService.setJwtToken(jwtToken); + const connected = await brainService.connect(); + if (connected) { + const state = brainService.getState(); + appStore.setBrainStatus("connected"); + appStore.setBrainUser(state.user); + appStore.setBrainCounts(state.knowledgeCount, state.memoryCount); + appStore.setBrainShowBanner(false); + } else { + throw new Error("Failed to connect with the provided JWT token."); + } +}; + +const defaultHandleBrainSetApiKey = async (apiKey: string): Promise => { + await brainService.setApiKey(apiKey); + const connected = await brainService.connect(); + if (connected) { + const state = brainService.getState(); + appStore.setBrainStatus("connected"); + appStore.setBrainUser(state.user); + appStore.setBrainCounts(state.knowledgeCount, state.memoryCount); + appStore.setBrainShowBanner(false); + } else { + throw new Error("Failed to connect with the provided API key."); + } +}; + +const defaultHandleBrainLogout = async (): Promise => { + await brainService.logout(); + appStore.setBrainStatus("disconnected"); + appStore.setBrainUser(null); + appStore.setBrainCounts(0, 0); + appStore.setBrainShowBanner(true); +}; + export const renderApp = async (props: RenderAppProps): Promise => { const { displayName, model: defaultModel } = getProviderInfo( props.state.provider, @@ -95,6 +135,9 @@ export const renderApp = async (props: RenderAppProps): Promise => { onMCPAdd: props.handleMCPAdd ?? defaultHandleMCPAdd, onPermissionResponse: props.handlePermissionResponse ?? (() => {}), onLearningResponse: props.handleLearningResponse ?? (() => {}), + onBrainSetJwtToken: props.handleBrainSetJwtToken ?? defaultHandleBrainSetJwtToken, + onBrainSetApiKey: props.handleBrainSetApiKey ?? defaultHandleBrainSetApiKey, + onBrainLogout: props.handleBrainLogout ?? defaultHandleBrainLogout, plan: props.plan, }); diff --git a/src/constants/agent-definition.ts b/src/constants/agent-definition.ts new file mode 100644 index 0000000..85489bb --- /dev/null +++ b/src/constants/agent-definition.ts @@ -0,0 +1,66 @@ +/** + * Agent definition constants + */ + +export const AGENT_DEFINITION = { + FILE_EXTENSION: ".md", + DIRECTORY_NAME: "agents", + FRONTMATTER_DELIMITER: "---", + MAX_NAME_LENGTH: 50, + MAX_DESCRIPTION_LENGTH: 500, + MAX_TOOLS: 20, + MAX_TRIGGER_PHRASES: 10, +} as const; + +export const AGENT_DEFINITION_PATHS = { + PROJECT: ".codetyper/agents", + GLOBAL: "~/.config/codetyper/agents", + BUILTIN: "src/agents", +} as const; + +export const AGENT_DEFAULT_TOOLS = { + EXPLORE: ["read", "glob", "grep"], + PLAN: ["read", "glob", "grep", "web_search"], + CODE: ["read", "write", "edit", "glob", "grep", "bash"], + REVIEW: ["read", "glob", "grep", "lsp"], + BASH: ["bash", "read"], +} as const; + +export const AGENT_COLORS = { + RED: "\x1b[31m", + GREEN: "\x1b[32m", + BLUE: "\x1b[34m", + YELLOW: "\x1b[33m", + CYAN: "\x1b[36m", + MAGENTA: "\x1b[35m", + WHITE: "\x1b[37m", + GRAY: "\x1b[90m", + RESET: "\x1b[0m", +} as const; + +export const AGENT_TIER_CONFIG = { + fast: { + model: "gpt-4o-mini", + maxTurns: 5, + timeout: 30000, + }, + balanced: { + model: "gpt-4o", + maxTurns: 10, + timeout: 60000, + }, + thorough: { + model: "o1", + maxTurns: 20, + timeout: 120000, + }, +} as const; + +export const AGENT_MESSAGES = { + LOADING: "Loading agent definitions...", + LOADED: "Agent definitions loaded", + NOT_FOUND: "Agent definition not found", + INVALID_FRONTMATTER: "Invalid YAML frontmatter", + MISSING_REQUIRED: "Missing required field", + INVALID_TOOL: "Invalid tool specified", +} as const; diff --git a/src/constants/apply-patch.ts b/src/constants/apply-patch.ts new file mode 100644 index 0000000..a1f54aa --- /dev/null +++ b/src/constants/apply-patch.ts @@ -0,0 +1,100 @@ +/** + * Apply Patch Constants + * + * Configuration for unified diff parsing and application. + */ + +/** + * Default configuration for patch application + */ +export const PATCH_DEFAULTS = { + FUZZ: 2, + MAX_FUZZ: 3, + IGNORE_WHITESPACE: false, + IGNORE_CASE: false, + CONTEXT_LINES: 3, +} as const; + +/** + * Patch file patterns + */ +export const PATCH_PATTERNS = { + HUNK_HEADER: /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/, + FILE_HEADER_OLD: /^--- (.+?)(?:\t.*)?$/, + FILE_HEADER_NEW: /^\+\+\+ (.+?)(?:\t.*)?$/, + GIT_DIFF: /^diff --git a\/(.+) b\/(.+)$/, + INDEX_LINE: /^index [a-f0-9]+\.\.[a-f0-9]+(?: \d+)?$/, + BINARY_FILE: /^Binary files .+ differ$/, + NEW_FILE: /^new file mode \d+$/, + DELETED_FILE: /^deleted file mode \d+$/, + RENAME_FROM: /^rename from (.+)$/, + RENAME_TO: /^rename to (.+)$/, + NO_NEWLINE: /^\\ No newline at end of file$/, +} as const; + +/** + * Line type prefixes + */ +export const LINE_PREFIXES = { + CONTEXT: " ", + ADDITION: "+", + DELETION: "-", +} as const; + +/** + * Error messages + */ +export const PATCH_ERRORS = { + INVALID_PATCH: "Invalid patch format", + PARSE_FAILED: (detail: string) => `Failed to parse patch: ${detail}`, + HUNK_FAILED: (index: number, reason: string) => + `Hunk #${index + 1} failed: ${reason}`, + FILE_NOT_FOUND: (path: string) => `Target file not found: ${path}`, + CONTEXT_MISMATCH: (line: number) => + `Context mismatch at line ${line}`, + FUZZY_MATCH_FAILED: (hunk: number) => + `Could not find match for hunk #${hunk + 1} even with fuzzy matching`, + ALREADY_APPLIED: "Patch appears to be already applied", + REVERSED_PATCH: "Patch appears to be reversed", + BINARY_NOT_SUPPORTED: "Binary patches are not supported", + WRITE_FAILED: (path: string, error: string) => + `Failed to write patched file ${path}: ${error}`, +} as const; + +/** + * Success messages + */ +export const PATCH_MESSAGES = { + PARSING: "Parsing patch...", + APPLYING: (file: string) => `Applying patch to ${file}`, + APPLIED: (files: number, hunks: number) => + `Successfully applied ${hunks} hunk(s) to ${files} file(s)`, + DRY_RUN: (files: number, hunks: number) => + `Dry run: ${hunks} hunk(s) would be applied to ${files} file(s)`, + FUZZY_APPLIED: (hunk: number, offset: number) => + `Hunk #${hunk + 1} applied with fuzzy offset of ${offset}`, + ROLLBACK_AVAILABLE: "Rollback is available if needed", + SKIPPED_BINARY: (file: string) => `Skipped binary file: ${file}`, +} as const; + +/** + * Tool titles + */ +export const PATCH_TITLES = { + APPLYING: (file: string) => `Patching: ${file}`, + SUCCESS: (files: number) => `Patched ${files} file(s)`, + PARTIAL: (success: number, failed: number) => + `Partial success: ${success} patched, ${failed} failed`, + FAILED: "Patch failed", + DRY_RUN: "Patch dry run", + VALIDATING: "Validating patch", +} as const; + +/** + * Special path values + */ +export const SPECIAL_PATHS = { + DEV_NULL: "/dev/null", + A_PREFIX: "a/", + B_PREFIX: "b/", +} as const; diff --git a/src/constants/background-task.ts b/src/constants/background-task.ts new file mode 100644 index 0000000..b7ef70c --- /dev/null +++ b/src/constants/background-task.ts @@ -0,0 +1,62 @@ +/** + * Background task constants + */ + +export const BACKGROUND_TASK = { + MAX_CONCURRENT: 3, + DEFAULT_TIMEOUT: 300000, // 5 minutes + MAX_TIMEOUT: 3600000, // 1 hour + POLL_INTERVAL: 1000, // 1 second + MAX_RETRIES: 3, + RETRY_DELAY: 5000, // 5 seconds + HISTORY_LIMIT: 100, +} as const; + +export const BACKGROUND_TASK_STORAGE = { + DIRECTORY: ".codetyper/tasks", + FILE_EXTENSION: ".json", + MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB +} as const; + +export const BACKGROUND_TASK_SHORTCUTS = { + START: "ctrl+b", + LIST: "ctrl+shift+b", + CANCEL: "ctrl+shift+c", + PAUSE: "ctrl+shift+p", + RESUME: "ctrl+shift+r", +} as const; + +export const BACKGROUND_TASK_COMMANDS = { + START: "/background", + LIST: "/tasks", + CANCEL: "/task cancel", + STATUS: "/task status", + CLEAR: "/task clear", +} as const; + +export const BACKGROUND_TASK_STATUS_ICONS = { + pending: "\u23F3", // hourglass + running: "\u25B6", // play + paused: "\u23F8", // pause + completed: "\u2705", // check + failed: "\u274C", // cross + cancelled: "\u23F9", // stop +} as const; + +export const BACKGROUND_TASK_MESSAGES = { + STARTED: "Task started in background", + COMPLETED: "Background task completed", + FAILED: "Background task failed", + CANCELLED: "Background task cancelled", + PAUSED: "Background task paused", + RESUMED: "Background task resumed", + QUEUE_FULL: "Task queue is full", + NOT_FOUND: "Task not found", + ALREADY_RUNNING: "Task is already running", +} as const; + +export const BACKGROUND_TASK_NOTIFICATIONS = { + SOUND_ENABLED: true, + DESKTOP_ENABLED: true, + INLINE_ENABLED: true, +} as const; diff --git a/src/constants/brain-cloud.ts b/src/constants/brain-cloud.ts new file mode 100644 index 0000000..d3984c1 --- /dev/null +++ b/src/constants/brain-cloud.ts @@ -0,0 +1,107 @@ +/** + * Brain Cloud Sync Constants + * + * Configuration for cloud synchronization of brain data. + */ + +import type { CloudBrainConfig } from "@/types/brain-cloud"; + +/** + * Default cloud configuration + */ +export const CLOUD_BRAIN_DEFAULTS: CloudBrainConfig = { + enabled: false, + endpoint: "https://brain.codetyper.dev/api/v1", + syncOnSessionEnd: true, + syncInterval: 300000, // 5 minutes + conflictStrategy: "local-wins", + retryAttempts: 3, + retryDelay: 1000, +} as const; + +/** + * Cloud API endpoints + */ +export const CLOUD_ENDPOINTS = { + PUSH: "/sync/push", + PULL: "/sync/pull", + STATUS: "/sync/status", + CONFLICTS: "/sync/conflicts", + RESOLVE: "/sync/resolve", + HEALTH: "/health", +} as const; + +/** + * Sync configuration + */ +export const SYNC_CONFIG = { + MAX_BATCH_SIZE: 100, + MAX_QUEUE_SIZE: 1000, + STALE_ITEM_AGE_MS: 86400000, // 24 hours + VERSION_KEY: "brain_sync_version", + QUEUE_KEY: "brain_offline_queue", +} as const; + +/** + * Error messages + */ +export const CLOUD_ERRORS = { + NOT_CONFIGURED: "Cloud sync is not configured", + OFFLINE: "Device is offline", + SYNC_IN_PROGRESS: "Sync already in progress", + PUSH_FAILED: (error: string) => `Push failed: ${error}`, + PULL_FAILED: (error: string) => `Pull failed: ${error}`, + CONFLICT_UNRESOLVED: (count: number) => + `${count} conflict(s) require manual resolution`, + QUEUE_FULL: "Offline queue is full", + VERSION_MISMATCH: "Version mismatch - full sync required", + AUTH_REQUIRED: "Authentication required for cloud sync", + INVALID_RESPONSE: "Invalid response from server", +} as const; + +/** + * Status messages + */ +export const CLOUD_MESSAGES = { + STARTING_SYNC: "Starting cloud sync...", + PUSHING: (count: number) => `Pushing ${count} change(s)...`, + PULLING: (count: number) => `Pulling ${count} change(s)...`, + RESOLVING_CONFLICTS: (count: number) => `Resolving ${count} conflict(s)...`, + SYNC_COMPLETE: "Cloud sync complete", + SYNC_SKIPPED: "No changes to sync", + QUEUED_OFFLINE: (count: number) => `Queued ${count} change(s) for later sync`, + RETRYING: (attempt: number, max: number) => + `Retrying sync (${attempt}/${max})...`, +} as const; + +/** + * Titles for UI + */ +export const CLOUD_TITLES = { + SYNCING: "Syncing with cloud", + SYNCED: "Cloud sync complete", + OFFLINE: "Offline - changes queued", + CONFLICT: "Sync conflicts", + ERROR: "Sync failed", +} as const; + +/** + * Conflict resolution labels + */ +export const CONFLICT_LABELS = { + "local-wins": "Keep local version", + "remote-wins": "Use remote version", + manual: "Resolve manually", + merge: "Attempt to merge", +} as const; + +/** + * HTTP request configuration + */ +export const CLOUD_HTTP_CONFIG = { + TIMEOUT_MS: 30000, + HEADERS: { + "Content-Type": "application/json", + "X-Client": "codetyper-cli", + }, +} as const; diff --git a/src/constants/brain-mcp.ts b/src/constants/brain-mcp.ts new file mode 100644 index 0000000..7bf5a76 --- /dev/null +++ b/src/constants/brain-mcp.ts @@ -0,0 +1,75 @@ +/** + * Brain MCP Server constants + */ + +export const BRAIN_MCP_SERVER = { + DEFAULT_PORT: 5002, + DEFAULT_HOST: "localhost", + REQUEST_TIMEOUT: 30000, + MAX_CONNECTIONS: 100, + HEARTBEAT_INTERVAL: 30000, +} as const; + +export const BRAIN_MCP_RATE_LIMIT = { + ENABLED: true, + MAX_REQUESTS: 100, + WINDOW_MS: 60000, // 1 minute + BLOCK_DURATION: 300000, // 5 minutes +} as const; + +export const BRAIN_MCP_AUTH = { + HEADER: "X-Brain-API-Key", + TOKEN_PREFIX: "Bearer", + SESSION_DURATION: 3600000, // 1 hour +} as const; + +export const BRAIN_MCP_COMMANDS = { + START: "/brain mcp start", + STOP: "/brain mcp stop", + STATUS: "/brain mcp status", + LOGS: "/brain mcp logs", + CONFIG: "/brain mcp config", +} as const; + +export const BRAIN_MCP_TOOL_NAMES = { + RECALL: "brain_recall", + LEARN: "brain_learn", + SEARCH: "brain_search", + RELATE: "brain_relate", + CONTEXT: "brain_context", + STATS: "brain_stats", + PROJECTS: "brain_projects", +} as const; + +export const BRAIN_MCP_MESSAGES = { + SERVER_STARTED: "Brain MCP server started", + SERVER_STOPPED: "Brain MCP server stopped", + SERVER_ALREADY_RUNNING: "Brain MCP server is already running", + SERVER_NOT_RUNNING: "Brain MCP server is not running", + CLIENT_CONNECTED: "MCP client connected", + CLIENT_DISCONNECTED: "MCP client disconnected", + TOOL_EXECUTED: "Tool executed successfully", + TOOL_FAILED: "Tool execution failed", + UNAUTHORIZED: "Unauthorized request", + RATE_LIMITED: "Rate limit exceeded", + INVALID_REQUEST: "Invalid MCP request", +} as const; + +export const BRAIN_MCP_ERRORS = { + PARSE_ERROR: { code: -32700, message: "Parse error" }, + INVALID_REQUEST: { code: -32600, message: "Invalid request" }, + METHOD_NOT_FOUND: { code: -32601, message: "Method not found" }, + INVALID_PARAMS: { code: -32602, message: "Invalid params" }, + INTERNAL_ERROR: { code: -32603, message: "Internal error" }, + TOOL_NOT_FOUND: { code: -32001, message: "Tool not found" }, + UNAUTHORIZED: { code: -32002, message: "Unauthorized" }, + RATE_LIMITED: { code: -32003, message: "Rate limited" }, + BRAIN_UNAVAILABLE: { code: -32004, message: "Brain service unavailable" }, +} as const; + +export const BRAIN_MCP_LOG_LEVELS = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, +} as const; diff --git a/src/constants/brain-project.ts b/src/constants/brain-project.ts new file mode 100644 index 0000000..56cedda --- /dev/null +++ b/src/constants/brain-project.ts @@ -0,0 +1,69 @@ +/** + * Multi-project Brain constants + */ + +export const BRAIN_PROJECT = { + MAX_PROJECTS: 100, + NAME_MIN_LENGTH: 2, + NAME_MAX_LENGTH: 100, + DESCRIPTION_MAX_LENGTH: 500, + DEFAULT_RECALL_LIMIT: 5, + DEFAULT_SYNC_INTERVAL: 30, // minutes +} as const; + +export const BRAIN_PROJECT_STORAGE = { + CONFIG_FILE: "brain-projects.json", + EXPORT_EXTENSION: ".brain-export.json", + BACKUP_EXTENSION: ".brain-backup.json", +} as const; + +export const BRAIN_PROJECT_PATHS = { + LOCAL: ".codetyper/brain", + GLOBAL: "~/.local/share/codetyper/brain", + EXPORTS: "~/.local/share/codetyper/brain/exports", + BACKUPS: "~/.local/share/codetyper/brain/backups", +} as const; + +export const BRAIN_PROJECT_COMMANDS = { + LIST: "/brain projects", + CREATE: "/brain project create", + SWITCH: "/brain project switch", + DELETE: "/brain project delete", + EXPORT: "/brain project export", + IMPORT: "/brain project import", + SYNC: "/brain project sync", +} as const; + +export const BRAIN_PROJECT_API = { + LIST: "/api/projects", + CREATE: "/api/projects", + GET: "/api/projects/:id", + UPDATE: "/api/projects/:id", + DELETE: "/api/projects/:id", + SWITCH: "/api/projects/:id/switch", + EXPORT: "/api/projects/:id/export", + IMPORT: "/api/projects/import", + SYNC: "/api/projects/:id/sync", +} as const; + +export const BRAIN_PROJECT_MESSAGES = { + CREATED: "Brain project created successfully", + SWITCHED: "Switched to project", + DELETED: "Brain project deleted", + EXPORTED: "Brain project exported", + IMPORTED: "Brain project imported", + SYNCED: "Brain project synced", + NOT_FOUND: "Brain project not found", + ALREADY_EXISTS: "Project with this name already exists", + INVALID_NAME: "Invalid project name", + SWITCH_FAILED: "Failed to switch project", + EXPORT_FAILED: "Failed to export project", + IMPORT_FAILED: "Failed to import project", +} as const; + +export const BRAIN_PROJECT_DEFAULTS = { + AUTO_LEARN: true, + AUTO_RECALL: true, + CONTEXT_INJECTION: true, + SYNC_ENABLED: false, +} as const; diff --git a/src/constants/brain.ts b/src/constants/brain.ts new file mode 100644 index 0000000..5fdb071 --- /dev/null +++ b/src/constants/brain.ts @@ -0,0 +1,94 @@ +/** + * Brain API Constants + * + * Configuration constants for the CodeTyper Brain service + */ + +/** + * Feature flag to disable all Brain functionality. + * Set to true to hide Brain menu, disable Brain API calls, + * and remove Brain-related UI elements. + */ +export const BRAIN_DISABLED = true; + +export const BRAIN_PROVIDER_NAME = "brain" as const; +export const BRAIN_DISPLAY_NAME = "CodeTyper Brain"; + +export const BRAIN_DEFAULTS = { + BASE_URL: "http://localhost:5001", + PROJECT_ID: 1, +} as const; + +export const BRAIN_ENDPOINTS = { + // Health + HEALTH: "/", + + // Authentication + AUTH_REGISTER: "/auth/register", + AUTH_LOGIN: "/auth/login", + AUTH_LOGOUT: "/auth/logout", + AUTH_REFRESH: "/auth/refresh", + AUTH_ME: "/auth/me", + + // Knowledge Graph + KNOWLEDGE_LEARN: "/api/knowledge/learn", + KNOWLEDGE_RECALL: "/api/knowledge/recall", + KNOWLEDGE_RELATE: "/api/knowledge/relate", + KNOWLEDGE_EXTRACT: "/api/knowledge/extract", + KNOWLEDGE_CONTEXT: "/api/knowledge/context", + KNOWLEDGE_CONCEPTS: "/api/knowledge/concepts", + KNOWLEDGE_STATS: "/api/knowledge/stats", + + // Memory + MEMORY_STATUS: "/api/memory/status", + MEMORY_STATS: "/api/memory/stats", + MEMORY_SEARCH: "/api/memory/search", + MEMORY_STORE: "/api/memory/store", + MEMORY_TOP: "/api/memory/top", + MEMORY_FEEDBACK: "/api/memory/feedback", + + // GraphQL (unified endpoint) + GRAPHQL: "/graphql", +} as const; + +export const BRAIN_TIMEOUTS = { + HEALTH: 3000, + AUTH: 10000, + KNOWLEDGE: 15000, + MEMORY: 10000, + EXTRACT: 30000, +} as const; + +export const BRAIN_ERRORS = { + NOT_RUNNING: "Brain service not available. Start the API server at localhost:5001", + NOT_AUTHENTICATED: "Not authenticated. Please login or set an API key.", + INVALID_API_KEY: "Invalid API key. Please check your credentials.", + CONNECTION_FAILED: "Failed to connect to Brain service.", + RECALL_FAILED: "Failed to recall knowledge from Brain.", + LEARN_FAILED: "Failed to store knowledge in Brain.", + EXTRACT_FAILED: "Failed to extract concepts from content.", +} as const; + +export const BRAIN_MESSAGES = { + CONNECTED: "Brain connected", + CONNECTING: "Connecting to Brain...", + DISCONNECTED: "Brain disconnected", + LEARNING: "Learning concept...", + RECALLING: "Recalling knowledge...", + EXTRACTING: "Extracting concepts...", +} as const; + +export const BRAIN_BANNER = { + TITLE: "CodeTyper has a Brain!", + CTA: "Login and get an API key to enable long-term memory", + URL: "http://localhost:5001", + LOGIN_URL: "http://localhost:5173/docs/login", + EMOJI_CONNECTED: "🧠", + EMOJI_DISCONNECTED: "💤", +} as const; + +export const BRAIN_HEADERS = { + API_KEY: "api-key", + AUTHORIZATION: "Authorization", + CONTENT_TYPE: "Content-Type", +} as const; diff --git a/src/constants/confidence-filter.ts b/src/constants/confidence-filter.ts new file mode 100644 index 0000000..18d9000 --- /dev/null +++ b/src/constants/confidence-filter.ts @@ -0,0 +1,33 @@ +/** + * Confidence filtering constants + */ + +export const CONFIDENCE_FILTER = { + DEFAULT_THRESHOLD: 80, + MIN_THRESHOLD: 0, + MAX_THRESHOLD: 100, + VALIDATION_TIMEOUT: 30000, + MAX_BATCH_SIZE: 50, +} as const; + +export const CONFIDENCE_WEIGHTS = { + PATTERN_MATCH: 0.3, + CONTEXT_RELEVANCE: 0.25, + SEVERITY_LEVEL: 0.2, + CODE_ANALYSIS: 0.15, + HISTORICAL_ACCURACY: 0.1, +} as const; + +export const CONFIDENCE_MESSAGES = { + BELOW_THRESHOLD: "Filtered out due to low confidence", + VALIDATION_FAILED: "Confidence adjusted after validation", + VALIDATION_PASSED: "Confidence validated successfully", + NO_FACTORS: "No confidence factors available", +} as const; + +export const CONFIDENCE_COLORS = { + LOW: "#808080", + MEDIUM: "#FFA500", + HIGH: "#00FF00", + CRITICAL: "#FF0000", +} as const; diff --git a/src/constants/feature-dev.ts b/src/constants/feature-dev.ts new file mode 100644 index 0000000..8a5e7c2 --- /dev/null +++ b/src/constants/feature-dev.ts @@ -0,0 +1,275 @@ +/** + * Feature-Dev Workflow Constants + * + * Configuration and prompts for the 7-phase development workflow. + */ + +import type { FeatureDevPhase, FeatureDevConfig } from "@/types/feature-dev"; + +/** + * Default workflow configuration + */ +export const FEATURE_DEV_CONFIG: FeatureDevConfig = { + requireCheckpoints: true, + autoRunTests: true, + autoCommit: false, + maxExplorationDepth: 3, + parallelExplorations: 3, +} as const; + +/** + * Phase order for workflow progression + */ +export const PHASE_ORDER: FeatureDevPhase[] = [ + "understand", + "explore", + "plan", + "implement", + "verify", + "review", + "finalize", +] as const; + +/** + * Phase descriptions + */ +export const PHASE_DESCRIPTIONS: Record = { + understand: "Clarify requirements and gather context", + explore: "Search codebase for relevant code and patterns", + plan: "Design the implementation approach", + implement: "Write the code changes", + verify: "Run tests and validate changes", + review: "Self-review the implementation", + finalize: "Commit changes and cleanup", +} as const; + +/** + * Phase prompts for guiding the agent + */ +export const PHASE_PROMPTS: Record = { + understand: `You are in the UNDERSTAND phase of feature development. + +Your goal is to fully understand what needs to be built before writing any code. + +Tasks: +1. Analyze the user's feature request +2. Identify unclear or ambiguous requirements +3. Ask clarifying questions if needed +4. Document the understood requirements + +Output a summary of: +- What the feature should do +- User-facing behavior +- Technical requirements +- Edge cases to consider +- Any assumptions made + +If anything is unclear, ask the user for clarification before proceeding.`, + + explore: `You are in the EXPLORE phase of feature development. + +Your goal is to understand the existing codebase before making changes. + +Tasks: +1. Search for related code using grep and glob +2. Identify files that will need to be modified +3. Understand existing patterns and conventions +4. Find similar implementations to reference +5. Identify potential dependencies or impacts + +Run multiple parallel searches to gather context efficiently. + +Document your findings: +- Relevant files and their purposes +- Existing patterns to follow +- Code that might be affected +- Useful examples in the codebase`, + + plan: `You are in the PLAN phase of feature development. + +Your goal is to create a detailed implementation plan before writing code. + +Tasks: +1. Design the solution architecture +2. List files to create, modify, or delete +3. Define the order of changes +4. Identify risks and dependencies +5. Plan the testing approach + +Create a plan that includes: +- Summary of the approach +- Step-by-step implementation order +- File changes with descriptions +- Potential risks and mitigations +- Test cases to verify the feature + +Present this plan for user approval before proceeding.`, + + implement: `You are in the IMPLEMENT phase of feature development. + +Your goal is to write the code according to the approved plan. + +Tasks: +1. Follow the implementation plan step by step +2. Write clean, well-documented code +3. Follow existing code patterns and conventions +4. Create necessary files and make required changes +5. Track all changes made + +Guidelines: +- Implement one step at a time +- Test each change locally if possible +- Keep changes focused and minimal +- Add comments for complex logic +- Update imports and exports as needed`, + + verify: `You are in the VERIFY phase of feature development. + +Your goal is to ensure the implementation works correctly. + +Tasks: +1. Run the test suite +2. Add new tests for the feature +3. Fix any failing tests +4. Check for regressions +5. Verify edge cases + +Report: +- Test results (pass/fail counts) +- Coverage information if available +- Any issues discovered +- Additional tests needed`, + + review: `You are in the REVIEW phase of feature development. + +Your goal is to self-review the implementation for quality. + +Tasks: +1. Review all changes made +2. Check for code quality issues +3. Verify documentation is complete +4. Look for potential bugs +5. Ensure best practices are followed + +Review criteria: +- Code clarity and readability +- Error handling +- Edge cases covered +- Performance considerations +- Security implications +- Documentation completeness + +Report any findings that need attention.`, + + finalize: `You are in the FINALIZE phase of feature development. + +Your goal is to complete the feature implementation. + +Tasks: +1. Create a commit with appropriate message +2. Update any documentation +3. Clean up temporary files +4. Prepare summary of changes + +Output: +- Final list of changes +- Commit message (if committing) +- Any follow-up tasks recommended +- Success confirmation`, +} as const; + +/** + * Checkpoint configuration per phase + */ +export const PHASE_CHECKPOINTS: Record< + FeatureDevPhase, + { required: boolean; title: string } +> = { + understand: { + required: true, + title: "Requirements Confirmation", + }, + explore: { + required: false, + title: "Exploration Summary", + }, + plan: { + required: true, + title: "Implementation Plan Approval", + }, + implement: { + required: false, + title: "Implementation Progress", + }, + verify: { + required: true, + title: "Test Results Review", + }, + review: { + required: true, + title: "Code Review Findings", + }, + finalize: { + required: true, + title: "Final Approval", + }, +} as const; + +/** + * Error messages + */ +export const FEATURE_DEV_ERRORS = { + INVALID_PHASE: (phase: string) => `Invalid phase: ${phase}`, + INVALID_TRANSITION: (from: FeatureDevPhase, to: FeatureDevPhase) => + `Cannot transition from ${from} to ${to}`, + CHECKPOINT_REQUIRED: (phase: FeatureDevPhase) => + `User approval required for ${phase} phase`, + PHASE_FAILED: (phase: FeatureDevPhase, reason: string) => + `Phase ${phase} failed: ${reason}`, + WORKFLOW_ABORTED: (reason: string) => `Workflow aborted: ${reason}`, + NO_PLAN: "Cannot implement without an approved plan", + TEST_FAILURE: "Tests failed - review required before proceeding", +} as const; + +/** + * Status messages + */ +export const FEATURE_DEV_MESSAGES = { + STARTING: (phase: FeatureDevPhase) => `Starting ${phase} phase...`, + COMPLETED: (phase: FeatureDevPhase) => `Completed ${phase} phase`, + AWAITING_APPROVAL: (phase: FeatureDevPhase) => + `Awaiting approval for ${phase}`, + CHECKPOINT: (title: string) => `Checkpoint: ${title}`, + EXPLORING: (query: string) => `Exploring: ${query}`, + IMPLEMENTING_STEP: (step: number, total: number) => + `Implementing step ${step}/${total}`, + RUNNING_TESTS: "Running tests...", + REVIEWING: "Reviewing changes...", + FINALIZING: "Finalizing changes...", +} as const; + +/** + * Allowed phase transitions + */ +export const ALLOWED_TRANSITIONS: Record = { + understand: ["explore", "plan"], // Can skip explore if simple + explore: ["plan", "understand"], // Can go back to understand + plan: ["implement", "explore", "understand"], // Can go back + implement: ["verify", "plan"], // Can revise plan + verify: ["review", "implement"], // Can fix issues + review: ["finalize", "implement"], // Can fix issues + finalize: [], // Terminal state +} as const; + +/** + * Phase timeout configuration (in ms) + */ +export const PHASE_TIMEOUTS: Record = { + understand: 120000, + explore: 180000, + plan: 120000, + implement: 600000, + verify: 300000, + review: 120000, + finalize: 60000, +} as const; diff --git a/src/constants/help-content.ts b/src/constants/help-content.ts index 3154893..e0d23de 100644 --- a/src/constants/help-content.ts +++ b/src/constants/help-content.ts @@ -89,7 +89,7 @@ export const HELP_TOPICS: HelpTopic[] = [ fullDescription: "Switch between Agent (full access), Ask (read-only), and Code Review modes.", usage: "/mode", - shortcuts: ["Ctrl+Tab"], + shortcuts: ["Ctrl+M"], category: "commands", }, { @@ -166,11 +166,11 @@ export const HELP_TOPICS: HelpTopic[] = [ category: "shortcuts", }, { - id: "shortcut-ctrltab", - name: "Ctrl+Tab", + id: "shortcut-ctrlm", + name: "Ctrl+M", shortDescription: "Cycle modes", fullDescription: "Cycle through interaction modes.", - shortcuts: ["Ctrl+Tab"], + shortcuts: ["Ctrl+M"], category: "shortcuts", }, ]; diff --git a/src/constants/home.ts b/src/constants/home.ts index 54ef3b8..0f33a37 100644 --- a/src/constants/home.ts +++ b/src/constants/home.ts @@ -1,4 +1,23 @@ export const HOME_VARS = { - title: "Welcome to CodeTyper - Your AI Coding Assistant", - subTitle: "Type a prompt below to start a new session", + subTitle: "Type a prompt below to start", }; + +/** CODETYPER text logo */ +export const ASCII_LOGO = [ + " ██████╗ ██████╗ ██████╗ ███████╗ ████████╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗ ", + "██╔════╝ ██╔═══██╗ ██╔══██╗ ██╔════╝ ╚══██╔══╝ ╚██╗ ██╔╝ ██╔══██╗ ██╔════╝ ██╔══██╗", + "██║ ██║ ██║ ██║ ██║ █████╗ ██║ ╚████╔╝ ██████╔╝ █████╗ ██████╔╝", + "██║ ██║ ██║ ██║ ██║ ██╔══╝ ██║ ╚██╔╝ ██╔═══╝ ██╔══╝ ██╔══██╗", + "╚██████╗ ╚██████╔╝ ██████╔╝ ███████╗ ██║ ██║ ██║ ███████╗ ██║ ██║", + " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝", +]; + +/** Gradient colors for CODETYPER text - from top to bottom */ +export const ASCII_LOGO_GRADIENT = [ + "#00FFFF", // Cyan + "#00D4FF", // Light blue + "#00AAFF", // Blue + "#0080FF", // Medium blue + "#0055FF", // Deep blue + "#AA00FF", // Purple +]; diff --git a/src/constants/multi-edit.ts b/src/constants/multi-edit.ts new file mode 100644 index 0000000..995cd70 --- /dev/null +++ b/src/constants/multi-edit.ts @@ -0,0 +1,54 @@ +/** + * MultiEdit Tool Constants + * + * Configuration for batch file editing operations + */ + +export const MULTI_EDIT_DEFAULTS = { + MAX_EDITS: 50, // Maximum number of edits in a single batch + MAX_FILE_SIZE: 1024 * 1024, // 1MB max file size +} as const; + +export const MULTI_EDIT_TITLES = { + VALIDATING: (count: number) => `Validating ${count} edits...`, + APPLYING: (current: number, total: number) => + `Applying edit ${current}/${total}`, + SUCCESS: (count: number) => `Applied ${count} edits`, + PARTIAL: (success: number, failed: number) => + `Applied ${success} edits, ${failed} failed`, + FAILED: "Multi-edit failed", + ROLLBACK: "Rolling back changes...", +} as const; + +export const MULTI_EDIT_MESSAGES = { + NO_EDITS: "No edits provided", + TOO_MANY_EDITS: (max: number) => `Too many edits (max: ${max})`, + VALIDATION_FAILED: "Validation failed for one or more edits", + ATOMIC_FAILURE: "Atomic edit failed - all changes rolled back", + DUPLICATE_FILE: (path: string) => + `Multiple edits to same file must be ordered: ${path}`, + OLD_STRING_NOT_FOUND: (path: string, preview: string) => + `Old string not found in ${path}: "${preview}..."`, + OLD_STRING_NOT_UNIQUE: (path: string, count: number) => + `Old string found ${count} times in ${path} (must be unique)`, + FILE_NOT_FOUND: (path: string) => `File not found: ${path}`, + FILE_TOO_LARGE: (path: string) => `File too large: ${path}`, +} as const; + +export const MULTI_EDIT_DESCRIPTION = `Edit multiple files in a single atomic operation. + +Use this tool when you need to: +- Make related changes across multiple files +- Refactor code that spans several files +- Apply consistent changes to many files + +All edits are validated before any changes are applied. +If any edit fails validation, no changes are made. + +Each edit requires: +- file_path: Absolute path to the file +- old_string: The exact text to find and replace +- new_string: The replacement text + +The old_string must be unique in the file. If it appears multiple times, +provide more context to make it unique.`; diff --git a/src/constants/parallel.ts b/src/constants/parallel.ts new file mode 100644 index 0000000..b993f73 --- /dev/null +++ b/src/constants/parallel.ts @@ -0,0 +1,108 @@ +/** + * Parallel Agent Execution Constants + * + * Configuration for concurrent task execution, resource limits, + * and conflict detection. + */ + +import type { ResourceLimits, TaskPriority } from "@/types/parallel"; + +/** + * Default resource limits + */ +export const PARALLEL_DEFAULTS: ResourceLimits = { + maxConcurrentTasks: 5, + maxQueueSize: 50, + defaultTimeout: 60000, + maxRetries: 2, +} as const; + +/** + * Priority weights for task ordering + */ +export const PRIORITY_WEIGHTS: Record = { + critical: 100, + high: 75, + normal: 50, + low: 25, +} as const; + +/** + * Task type concurrency limits + * Some task types should have lower concurrency + */ +export const TASK_TYPE_LIMITS = { + explore: 5, + analyze: 4, + execute: 2, + search: 3, +} as const; + +/** + * Conflict detection configuration + */ +export const CONFLICT_CONFIG = { + ENABLE_PATH_CONFLICT: true, + CONFLICT_CHECK_TIMEOUT_MS: 5000, + AUTO_RESOLVE_READ_CONFLICTS: true, +} as const; + +/** + * Timeout values for different task types + */ +export const TASK_TIMEOUTS = { + explore: 30000, + analyze: 45000, + execute: 120000, + search: 15000, +} as const; + +/** + * Error messages for parallel execution + */ +export const PARALLEL_ERRORS = { + QUEUE_FULL: "Task queue is full", + TIMEOUT: (taskId: string) => `Task ${taskId} timed out`, + CONFLICT: (taskId: string, paths: string[]) => + `Task ${taskId} conflicts with paths: ${paths.join(", ")}`, + MAX_RETRIES: (taskId: string, retries: number) => + `Task ${taskId} failed after ${retries} retries`, + CANCELLED: (taskId: string) => `Task ${taskId} was cancelled`, + INVALID_TASK: "Invalid task configuration", + EXECUTOR_ABORTED: "Executor was aborted", +} as const; + +/** + * Status messages for parallel execution + */ +export const PARALLEL_MESSAGES = { + STARTING: (count: number) => `Starting ${count} parallel task(s)`, + COMPLETED: (success: number, failed: number) => + `Completed: ${success} successful, ${failed} failed`, + QUEUED: (taskId: string, position: number) => + `Task ${taskId} queued at position ${position}`, + RUNNING: (taskId: string) => `Running task: ${taskId}`, + WAITING_CONFLICT: (taskId: string) => + `Task ${taskId} waiting for conflict resolution`, + RETRYING: (taskId: string, attempt: number) => + `Retrying task ${taskId} (attempt ${attempt})`, +} as const; + +/** + * Deduplication configuration + */ +export const DEDUP_CONFIG = { + ENABLE_CONTENT_DEDUP: true, + SIMILARITY_THRESHOLD: 0.95, + MAX_RESULTS_PER_TYPE: 100, +} as const; + +/** + * Read-only task types (no conflict with each other) + */ +export const READ_ONLY_TASK_TYPES = new Set(["explore", "analyze", "search"]); + +/** + * Modifying task types (conflict with all tasks on same paths) + */ +export const MODIFYING_TASK_TYPES = new Set(["execute"]); diff --git a/src/constants/paths.ts b/src/constants/paths.ts index 8dfc1e4..af180a6 100644 --- a/src/constants/paths.ts +++ b/src/constants/paths.ts @@ -58,12 +58,18 @@ export const FILES = { /** Provider credentials (stored in data, not config) */ credentials: join(DIRS.data, "credentials.json"), + /** Environment variables and tokens (API keys, JWT tokens, etc.) */ + vars: join(DIRS.config, "vars.json"), + /** Command history */ history: join(DIRS.data, "history.json"), /** Models cache */ modelsCache: join(DIRS.cache, "models.json"), + /** Copilot token cache */ + copilotTokenCache: join(DIRS.cache, "copilot-token.json"), + /** Frecency cache for file/command suggestions */ frecency: join(DIRS.cache, "frecency.json"), diff --git a/src/constants/pr-review.ts b/src/constants/pr-review.ts new file mode 100644 index 0000000..af86452 --- /dev/null +++ b/src/constants/pr-review.ts @@ -0,0 +1,207 @@ +/** + * PR Review Toolkit Constants + * + * Configuration for multi-agent code review. + */ + +import type { + PRReviewConfig, + ReviewSeverity, + ReviewFindingType, +} from "@/types/pr-review"; + +/** + * Minimum confidence threshold for reporting findings + * Only report findings with confidence >= 80% + */ +export const MIN_CONFIDENCE_THRESHOLD = 80; + +/** + * Default review configuration + */ +export const DEFAULT_REVIEW_CONFIG: PRReviewConfig = { + minConfidence: MIN_CONFIDENCE_THRESHOLD, + reviewers: [ + { name: "security", type: "security", enabled: true, minConfidence: 80 }, + { name: "performance", type: "performance", enabled: true, minConfidence: 80 }, + { name: "style", type: "style", enabled: true, minConfidence: 85 }, + { name: "logic", type: "logic", enabled: true, minConfidence: 80 }, + ], + security: { + checkInjection: true, + checkXSS: true, + checkAuth: true, + checkSecrets: true, + checkDependencies: true, + }, + performance: { + checkComplexity: true, + checkMemory: true, + checkQueries: true, + checkCaching: true, + checkRenders: true, + }, + style: { + checkNaming: true, + checkFormatting: true, + checkConsistency: true, + checkComments: true, + }, + logic: { + checkEdgeCases: true, + checkNullHandling: true, + checkErrorHandling: true, + checkConcurrency: true, + checkTypes: true, + }, + excludePatterns: [ + "**/node_modules/**", + "**/*.min.js", + "**/*.bundle.js", + "**/dist/**", + "**/build/**", + "**/*.lock", + "**/package-lock.json", + "**/yarn.lock", + "**/pnpm-lock.yaml", + ], + maxFindings: 50, +} as const; + +/** + * Severity emoji indicators + */ +export const SEVERITY_ICONS: Record = { + critical: "🔴", + warning: "🟠", + suggestion: "🟡", + nitpick: "🟢", +} as const; + +/** + * Severity labels + */ +export const SEVERITY_LABELS: Record = { + critical: "CRITICAL", + warning: "WARNING", + suggestion: "SUGGESTION", + nitpick: "NITPICK", +} as const; + +/** + * Finding type labels + */ +export const FINDING_TYPE_LABELS: Record = { + security: "Security", + performance: "Performance", + style: "Style", + logic: "Logic", + documentation: "Documentation", + testing: "Testing", +} as const; + +/** + * Reviewer prompts + */ +export const REVIEWER_PROMPTS: Record = { + security: `You are a security reviewer. Analyze the code changes for: +- SQL injection, XSS, command injection vulnerabilities +- Authentication and authorization issues +- Sensitive data exposure (API keys, passwords, tokens) +- Input validation and sanitization problems +- Insecure dependencies + +Only report findings with high confidence (≥80%). For each issue: +- Describe the vulnerability +- Explain the potential impact +- Suggest a specific fix`, + + performance: `You are a performance reviewer. Analyze the code changes for: +- Algorithmic complexity issues (O(n²) or worse operations) +- Memory usage problems (leaks, excessive allocations) +- Database query efficiency (N+1 queries, missing indexes) +- Unnecessary re-renders (React) or DOM manipulations +- Missing caching opportunities + +Only report findings with high confidence (≥80%). For each issue: +- Describe the performance impact +- Provide complexity analysis if applicable +- Suggest optimization`, + + style: `You are a code style reviewer. Analyze the code changes for: +- Naming convention violations +- Inconsistent formatting +- Code organization issues +- Missing or unclear documentation +- Deviations from project patterns + +Only report significant style issues that affect readability or maintainability. +Skip minor formatting issues that could be auto-fixed.`, + + logic: `You are a logic reviewer. Analyze the code changes for: +- Edge cases not handled +- Null/undefined reference risks +- Error handling gaps +- Race conditions or concurrency issues +- Type safety violations + +Only report findings with high confidence (≥80%). For each issue: +- Describe the bug or potential bug +- Explain how it could manifest +- Suggest a fix with example code`, +} as const; + +/** + * Rating thresholds + */ +export const RATING_THRESHOLDS = { + 5: { maxCritical: 0, maxWarning: 0 }, + 4: { maxCritical: 0, maxWarning: 3 }, + 3: { maxCritical: 0, maxWarning: 10 }, + 2: { maxCritical: 1, maxWarning: 20 }, + 1: { maxCritical: Infinity, maxWarning: Infinity }, +} as const; + +/** + * Recommendation thresholds + */ +export const RECOMMENDATION_THRESHOLDS = { + approve: { maxCritical: 0, maxWarning: 0, maxSuggestion: 5 }, + approve_with_suggestions: { maxCritical: 0, maxWarning: 3, maxSuggestion: Infinity }, + request_changes: { maxCritical: 1, maxWarning: Infinity, maxSuggestion: Infinity }, + needs_discussion: { maxCritical: Infinity, maxWarning: Infinity, maxSuggestion: Infinity }, +} as const; + +/** + * Error messages + */ +export const PR_REVIEW_ERRORS = { + NO_DIFF: "No diff content to review", + PARSE_FAILED: (error: string) => `Failed to parse diff: ${error}`, + REVIEWER_FAILED: (reviewer: string, error: string) => + `Reviewer ${reviewer} failed: ${error}`, + NO_FILES: "No files in diff to review", + EXCLUDED_ALL: "All files excluded by pattern", +} as const; + +/** + * Status messages + */ +export const PR_REVIEW_MESSAGES = { + STARTING: "Starting PR review...", + PARSING_DIFF: "Parsing diff...", + REVIEWING: (reviewer: string) => `Running ${reviewer} review...`, + AGGREGATING: "Aggregating results...", + COMPLETED: (findings: number) => `Review complete: ${findings} finding(s)`, + NO_FINDINGS: "No issues found", +} as const; + +/** + * Report titles + */ +export const PR_REVIEW_TITLES = { + REPORT: "Pull Request Review", + FINDINGS: "Findings", + SUMMARY: "Summary", + RECOMMENDATION: "Recommendation", +} as const; diff --git a/src/constants/skills.ts b/src/constants/skills.ts new file mode 100644 index 0000000..fb1ef70 --- /dev/null +++ b/src/constants/skills.ts @@ -0,0 +1,132 @@ +/** + * Skill System Constants + * + * Constants for skill loading, matching, and execution. + */ + +import { join } from "path"; +import { DIRS } from "@constants/paths"; + +/** + * Skill file configuration + */ +export const SKILL_FILE = { + NAME: "SKILL.md", + FRONTMATTER_DELIMITER: "---", + ENCODING: "utf-8", +} as const; + +/** + * Skill directories + */ +export const SKILL_DIRS = { + BUILTIN: join(__dirname, "..", "skills"), + USER: join(DIRS.config, "skills"), + PROJECT: ".codetyper/skills", +} as const; + +/** + * Skill loading configuration + */ +export const SKILL_LOADING = { + CACHE_TTL_MS: 60000, + MAX_SKILLS: 100, + MAX_FILE_SIZE_BYTES: 100000, +} as const; + +/** + * Skill matching configuration + */ +export const SKILL_MATCHING = { + MIN_CONFIDENCE: 0.7, + EXACT_MATCH_BONUS: 0.3, + COMMAND_PREFIX: "/", + FUZZY_THRESHOLD: 0.6, +} as const; + +/** + * Default skill metadata values + */ +export const SKILL_DEFAULTS = { + VERSION: "1.0.0", + TRIGGER_TYPE: "command" as const, + AUTO_TRIGGER: false, + REQUIRED_TOOLS: [] as string[], +} as const; + +/** + * Skill error messages + */ +export const SKILL_ERRORS = { + NOT_FOUND: (id: string) => `Skill not found: ${id}`, + INVALID_FRONTMATTER: (file: string) => `Invalid frontmatter in: ${file}`, + MISSING_REQUIRED_FIELD: (field: string, file: string) => + `Missing required field '${field}' in: ${file}`, + LOAD_FAILED: (file: string, error: string) => + `Failed to load skill from ${file}: ${error}`, + NO_MATCH: "No matching skill found for input", + EXECUTION_FAILED: (id: string, error: string) => + `Skill execution failed for ${id}: ${error}`, +} as const; + +/** + * Skill titles for UI + */ +export const SKILL_TITLES = { + LOADING: (name: string) => `Loading skill: ${name}`, + EXECUTING: (name: string) => `Executing skill: ${name}`, + MATCHED: (name: string, confidence: number) => + `Matched skill: ${name} (${(confidence * 100).toFixed(0)}%)`, + COMPLETED: (name: string) => `Skill completed: ${name}`, + FAILED: (name: string) => `Skill failed: ${name}`, +} as const; + +/** + * Built-in skill IDs + */ +export const BUILTIN_SKILLS = { + COMMIT: "commit", + REVIEW_PR: "review-pr", + EXPLAIN: "explain", + FEATURE_DEV: "feature-dev", +} as const; + +/** + * Skill trigger patterns for common commands + */ +export const SKILL_TRIGGER_PATTERNS = { + COMMIT: [ + "/commit", + "commit changes", + "commit this", + "git commit", + "make a commit", + ], + REVIEW_PR: [ + "/review-pr", + "/review", + "review pr", + "review this pr", + "review pull request", + "code review", + ], + EXPLAIN: [ + "/explain", + "explain this", + "explain code", + "what does this do", + "how does this work", + ], + FEATURE_DEV: [ + "/feature", + "/feature-dev", + "implement feature", + "new feature", + "build feature", + ], +} as const; + +/** + * Required fields in skill frontmatter + */ +export const SKILL_REQUIRED_FIELDS = ["id", "name", "description", "triggers"] as const; diff --git a/src/constants/token.ts b/src/constants/token.ts new file mode 100644 index 0000000..9fff32b --- /dev/null +++ b/src/constants/token.ts @@ -0,0 +1,55 @@ +/** + * Token Counting Constants + * + * Configuration for token estimation and context management + */ + +// Token estimation ratios +export const CHARS_PER_TOKEN = 4; +export const TOKENS_PER_CHAR = 0.25; + +// Context warning thresholds +export const TOKEN_WARNING_THRESHOLD = 0.75; // 75% - yellow warning +export const TOKEN_CRITICAL_THRESHOLD = 0.90; // 90% - red warning +export const TOKEN_OVERFLOW_THRESHOLD = 0.95; // 95% - trigger compaction + +// Pruning thresholds (following OpenCode pattern) +export const PRUNE_MINIMUM_TOKENS = 20000; // Min tokens to actually prune +export const PRUNE_PROTECT_TOKENS = 40000; // Threshold before marking for pruning +export const PRUNE_RECENT_TURNS = 2; // Protect last N user turns + +// Protected tools that should never be pruned +export const PRUNE_PROTECTED_TOOLS = new Set([ + "skill", + "todo_read", + "todo_write", +]); + +// Default context sizes +export const DEFAULT_MAX_CONTEXT_TOKENS = 128000; +export const DEFAULT_OUTPUT_TOKENS = 16000; + +// Token display formatting +export const TOKEN_DISPLAY = { + SEPARATOR: "/", + UNIT_K: "K", + FORMAT_DECIMALS: 1, +} as const; + +// Token status colors (semantic keys for theme lookup) +export const TOKEN_STATUS_COLORS = { + NORMAL: "textDim", + WARNING: "warning", + CRITICAL: "error", + COMPACTING: "info", +} as const; + +// Messages +export const TOKEN_MESSAGES = { + CONTEXT_LOW: "Context running low", + CONTEXT_CRITICAL: "Context nearly full", + COMPACTION_STARTING: "Starting context compaction...", + COMPACTION_COMPLETE: (saved: number) => + `Compaction complete: ${saved.toLocaleString()} tokens freed`, + OVERFLOW_WARNING: "Context overflow detected", +} as const; diff --git a/src/constants/tui-components.ts b/src/constants/tui-components.ts index 836a599..a4e8c65 100644 --- a/src/constants/tui-components.ts +++ b/src/constants/tui-components.ts @@ -49,6 +49,8 @@ export const MODE_DISPLAY_CONFIG: Record = { learning_prompt: { text: "Save Learning?", color: "cyan" }, help_menu: { text: "Help", color: "cyan" }, help_detail: { text: "Help Detail", color: "cyan" }, + brain_menu: { text: "Brain Settings", color: "magenta" }, + brain_login: { text: "Brain Login", color: "magenta" }, } as const; export const DEFAULT_MODE_DISPLAY: ModeDisplayConfig = { @@ -219,6 +221,11 @@ export const SLASH_COMMANDS: SlashCommand[] = [ description: "Sign out from provider", category: "account", }, + { + name: "brain", + description: "Configure CodeTyper Brain (memory & knowledge)", + category: "account", + }, ]; export const COMMAND_CATEGORIES = [ diff --git a/src/constants/web-fetch.ts b/src/constants/web-fetch.ts new file mode 100644 index 0000000..6f79111 --- /dev/null +++ b/src/constants/web-fetch.ts @@ -0,0 +1,75 @@ +/** + * WebFetch Tool Constants + * + * Configuration for the web content fetching tool + */ + +export const WEB_FETCH_DEFAULTS = { + TIMEOUT_MS: 30000, + MAX_CONTENT_LENGTH: 500000, // 500KB max + 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_FETCH_TITLES = { + FETCHING: (url: string) => `Fetching: ${url}`, + SUCCESS: "Content fetched", + FAILED: "Fetch failed", + TIMEOUT: "Fetch timed out", +} as const; + +export const WEB_FETCH_MESSAGES = { + URL_REQUIRED: "URL is required", + INVALID_URL: (url: string) => `Invalid URL: ${url}`, + TIMEOUT: "Request timed out", + FETCH_ERROR: (error: string) => `Fetch failed: ${error}`, + CONTENT_TOO_LARGE: "Content exceeds maximum size limit", + REDIRECT_DETECTED: (from: string, to: string) => + `Redirected from ${from} to ${to}`, +} as const; + +export const WEB_FETCH_DESCRIPTION = `Fetch content from a URL and convert HTML to markdown. + +Use this tool when you need to: +- Read documentation from a URL +- Fetch API responses +- Get content from web pages + +The content will be converted to markdown for readability. +HTML will be cleaned and converted. JSON responses are formatted. + +Note: This tool cannot access authenticated or private URLs. +For GitHub URLs, prefer using the \`bash\` tool with \`gh\` CLI instead.`; + +// Supported content types for conversion +export const SUPPORTED_CONTENT_TYPES = { + HTML: ["text/html", "application/xhtml+xml"], + JSON: ["application/json", "text/json"], + TEXT: ["text/plain", "text/markdown", "text/csv"], + XML: ["text/xml", "application/xml"], +} as const; + +// HTML elements to remove (scripts, styles, etc.) +export const HTML_REMOVE_ELEMENTS = [ + "script", + "style", + "noscript", + "iframe", + "svg", + "canvas", + "video", + "audio", + "nav", + "footer", + "aside", +]; + +// HTML elements to convert to markdown +export const HTML_BLOCK_ELEMENTS = [ + "p", + "div", + "section", + "article", + "main", + "header", +]; diff --git a/src/prompts/system/ask.ts b/src/prompts/system/ask.ts index 5b73e33..b5b1bc5 100644 --- a/src/prompts/system/ask.ts +++ b/src/prompts/system/ask.ts @@ -207,7 +207,7 @@ Read-only tools only: - You are in READ-ONLY mode - you cannot modify files - Always search before answering questions about the codebase -- If asked to make changes, explain that you're in Ask mode and suggest switching to Agent mode (Ctrl+Tab) +- If asked to make changes, explain that you're in Ask mode and suggest switching to Agent mode (Ctrl+M) - For general programming questions, you can answer without searching`; /** diff --git a/src/providers/copilot/chat.ts b/src/providers/copilot/chat.ts index 64be4c3..27feeab 100644 --- a/src/providers/copilot/chat.ts +++ b/src/providers/copilot/chat.ts @@ -246,6 +246,7 @@ const executeStream = ( if (delta?.tool_calls) { for (const tc of delta.tool_calls) { addDebugLog("api", `Tool call chunk: ${JSON.stringify(tc)}`); + console.log("Debug: Tool call chunk received:", JSON.stringify(tc)); onChunk({ type: "tool_call", toolCall: tc }); } } diff --git a/src/providers/copilot/token.ts b/src/providers/copilot/token.ts index 2a6548e..6b71f68 100644 --- a/src/providers/copilot/token.ts +++ b/src/providers/copilot/token.ts @@ -2,13 +2,14 @@ * Copilot token management */ -import { readFile } from "fs/promises"; +import { readFile, writeFile, mkdir } from "fs/promises"; import { existsSync } from "fs"; import { homedir, platform } from "os"; import { join } from "path"; import got from "got"; import { COPILOT_AUTH_URL } from "@constants/copilot"; +import { FILES, DIRS } from "@constants/paths"; import { getState, setOAuthToken, @@ -16,6 +17,36 @@ import { } from "@providers/copilot/state"; import type { CopilotToken } from "@/types/copilot"; +/** + * Load cached Copilot token from disk + */ +const loadCachedToken = async (): Promise => { + try { + const data = await readFile(FILES.copilotTokenCache, "utf-8"); + const token = JSON.parse(data) as CopilotToken; + + // Check if token is still valid (with 60 second buffer) + if (token.expires_at > Date.now() / 1000 + 60) { + return token; + } + } catch { + // Cache doesn't exist or is invalid + } + return null; +}; + +/** + * Save Copilot token to disk cache + */ +const saveCachedToken = async (token: CopilotToken): Promise => { + try { + await mkdir(DIRS.cache, { recursive: true }); + await writeFile(FILES.copilotTokenCache, JSON.stringify(token), "utf-8"); + } catch { + // Silently fail - caching is optional + } +}; + const getConfigDir = (): string => { const home = homedir(); const os = platform(); @@ -88,6 +119,7 @@ export const refreshToken = async (): Promise => { const currentState = getState(); + // Check in-memory cache first if ( currentState.githubToken && currentState.githubToken.expires_at > Date.now() / 1000 @@ -95,6 +127,14 @@ export const refreshToken = async (): Promise => { return currentState.githubToken; } + // Check disk cache to avoid network request on startup + const cachedToken = await loadCachedToken(); + if (cachedToken) { + setGitHubToken(cachedToken); + return cachedToken; + } + + // Fetch new token from GitHub const response = await got .get(COPILOT_AUTH_URL, { headers: { @@ -109,6 +149,10 @@ export const refreshToken = async (): Promise => { } setGitHubToken(response); + + // Cache to disk for faster startup next time + saveCachedToken(response).catch(() => {}); + return response; }; diff --git a/src/providers/ollama/chat.ts b/src/providers/ollama/chat.ts index de9c3ee..dc6f08d 100644 --- a/src/providers/ollama/chat.ts +++ b/src/providers/ollama/chat.ts @@ -22,15 +22,36 @@ import type { OllamaChatResponse, OllamaToolCall, OllamaToolDefinition, + OllamaMessage, } from "@/types/ollama"; -const formatMessages = ( - messages: Message[], -): Array<{ role: string; content: string }> => - messages.map((msg) => ({ - role: msg.role, - content: msg.content, - })); +/** + * Format messages for Ollama API + * Handles regular messages, assistant messages with tool_calls, and tool response messages + */ +const formatMessages = (messages: Message[]): OllamaMessage[] => + messages.map((msg) => { + const formatted: OllamaMessage = { + role: msg.role, + content: msg.content, + }; + + // Include tool_calls for assistant messages that made tool calls + if (msg.tool_calls && msg.tool_calls.length > 0) { + formatted.tool_calls = msg.tool_calls.map((tc) => ({ + id: tc.id, + function: { + name: tc.function.name, + arguments: + typeof tc.function.arguments === "string" + ? JSON.parse(tc.function.arguments) + : tc.function.arguments, + }, + })); + } + + return formatted; + }); const formatTools = ( tools: ChatCompletionOptions["tools"], diff --git a/src/services/agent-definition-loader.ts b/src/services/agent-definition-loader.ts new file mode 100644 index 0000000..840adaa --- /dev/null +++ b/src/services/agent-definition-loader.ts @@ -0,0 +1,289 @@ +/** + * Agent definition loader service + * Loads agent definitions from markdown files with YAML frontmatter + */ + +import { readFile, readdir } from "node:fs/promises"; +import { join, basename, extname } from "node:path"; +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; + +import type { + AgentDefinition, + AgentFrontmatter, + AgentDefinitionFile, + AgentRegistry, + AgentLoadResult, + AgentTier, + AgentColor, +} from "@src/types/agent-definition"; +import { DEFAULT_AGENT_DEFINITION, AGENT_DEFINITION_SCHEMA } from "@src/types/agent-definition"; +import { AGENT_DEFINITION, AGENT_DEFINITION_PATHS, AGENT_MESSAGES } from "@src/constants/agent-definition"; + +const parseFrontmatter = (content: string): { frontmatter: Record; body: string } | null => { + const delimiter = AGENT_DEFINITION.FRONTMATTER_DELIMITER; + const lines = content.split("\n"); + + if (lines[0]?.trim() !== delimiter) { + return null; + } + + const endIndex = lines.findIndex((line, index) => index > 0 && line.trim() === delimiter); + if (endIndex === -1) { + return null; + } + + const frontmatterLines = lines.slice(1, endIndex); + const body = lines.slice(endIndex + 1).join("\n").trim(); + + // Simple YAML parser for frontmatter + const frontmatter: Record = {}; + let currentKey = ""; + let currentArray: string[] | null = null; + + frontmatterLines.forEach((line) => { + const trimmed = line.trim(); + + if (trimmed.startsWith("- ") && currentArray !== null) { + currentArray.push(trimmed.slice(2)); + return; + } + + if (currentArray !== null) { + frontmatter[currentKey] = currentArray; + currentArray = null; + } + + const colonIndex = trimmed.indexOf(":"); + if (colonIndex === -1) return; + + const key = trimmed.slice(0, colonIndex).trim(); + const value = trimmed.slice(colonIndex + 1).trim(); + + if (value === "") { + currentKey = key; + currentArray = []; + } else if (value.startsWith("[") && value.endsWith("]")) { + frontmatter[key] = value + .slice(1, -1) + .split(",") + .map((s) => s.trim().replace(/^["']|["']$/g, "")); + } else if (value === "true") { + frontmatter[key] = true; + } else if (value === "false") { + frontmatter[key] = false; + } else if (!isNaN(Number(value))) { + frontmatter[key] = Number(value); + } else { + frontmatter[key] = value.replace(/^["']|["']$/g, ""); + } + }); + + if (currentArray !== null) { + frontmatter[currentKey] = currentArray; + } + + return { frontmatter, body }; +}; + +const validateFrontmatter = (frontmatter: Record): AgentFrontmatter | null => { + const { required } = AGENT_DEFINITION_SCHEMA; + + for (const field of required) { + if (!(field in frontmatter)) { + return null; + } + } + + const name = frontmatter.name; + const description = frontmatter.description; + const tools = frontmatter.tools; + + if (typeof name !== "string" || typeof description !== "string" || !Array.isArray(tools)) { + return null; + } + + return { + name, + description, + tools: tools as ReadonlyArray, + tier: (frontmatter.tier as AgentTier) || DEFAULT_AGENT_DEFINITION.tier, + color: (frontmatter.color as AgentColor) || DEFAULT_AGENT_DEFINITION.color, + maxTurns: (frontmatter.maxTurns as number) || DEFAULT_AGENT_DEFINITION.maxTurns, + triggerPhrases: (frontmatter.triggerPhrases as ReadonlyArray) || [], + capabilities: (frontmatter.capabilities as ReadonlyArray) || [], + allowedPaths: frontmatter.allowedPaths as ReadonlyArray | undefined, + deniedPaths: frontmatter.deniedPaths as ReadonlyArray | undefined, + }; +}; + +const frontmatterToDefinition = (frontmatter: AgentFrontmatter, content: string): AgentDefinition => ({ + name: frontmatter.name, + description: frontmatter.description, + tools: frontmatter.tools, + tier: frontmatter.tier || (DEFAULT_AGENT_DEFINITION.tier as AgentTier), + color: frontmatter.color || (DEFAULT_AGENT_DEFINITION.color as AgentColor), + maxTurns: frontmatter.maxTurns || DEFAULT_AGENT_DEFINITION.maxTurns, + systemPrompt: content || undefined, + triggerPhrases: frontmatter.triggerPhrases || [], + capabilities: frontmatter.capabilities || [], + permissions: { + allowedPaths: frontmatter.allowedPaths, + deniedPaths: frontmatter.deniedPaths, + }, +}); + +export const loadAgentDefinitionFile = async (filePath: string): Promise => { + try { + const content = await readFile(filePath, "utf-8"); + const parsed = parseFrontmatter(content); + + if (!parsed) { + return { success: false, error: AGENT_MESSAGES.INVALID_FRONTMATTER, filePath }; + } + + const frontmatter = validateFrontmatter(parsed.frontmatter); + + if (!frontmatter) { + return { success: false, error: AGENT_MESSAGES.MISSING_REQUIRED, filePath }; + } + + const agent = frontmatterToDefinition(frontmatter, parsed.body); + + return { success: true, agent, filePath }; + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return { success: false, error: message, filePath }; + } +}; + +export const loadAgentDefinitionsFromDirectory = async ( + directoryPath: string +): Promise> => { + const resolvedPath = directoryPath.replace("~", homedir()); + + if (!existsSync(resolvedPath)) { + return []; + } + + try { + const files = await readdir(resolvedPath); + const mdFiles = files.filter( + (file) => extname(file) === AGENT_DEFINITION.FILE_EXTENSION + ); + + const results = await Promise.all( + mdFiles.map((file) => loadAgentDefinitionFile(join(resolvedPath, file))) + ); + + return results; + } catch { + return []; + } +}; + +export const loadAllAgentDefinitions = async ( + projectPath: string +): Promise => { + const agents = new Map(); + const byTrigger = new Map(); + const byCapability = new Map(); + + // Load from all paths in priority order (project > global > builtin) + const paths = [ + join(projectPath, AGENT_DEFINITION_PATHS.PROJECT), + AGENT_DEFINITION_PATHS.GLOBAL, + ]; + + for (const path of paths) { + const results = await loadAgentDefinitionsFromDirectory(path); + + results.forEach((result) => { + if (result.success && result.agent) { + const { agent } = result; + + // Don't override if already loaded (project takes precedence) + if (!agents.has(agent.name)) { + agents.set(agent.name, agent); + + // Index by trigger phrases + agent.triggerPhrases?.forEach((phrase) => { + byTrigger.set(phrase.toLowerCase(), agent.name); + }); + + // Index by capabilities + agent.capabilities?.forEach((capability) => { + const existing = byCapability.get(capability) || []; + byCapability.set(capability, [...existing, agent.name]); + }); + } + } + }); + } + + return { agents, byTrigger, byCapability }; +}; + +export const findAgentByTrigger = ( + registry: AgentRegistry, + text: string +): AgentDefinition | undefined => { + const normalized = text.toLowerCase(); + + for (const [phrase, agentName] of registry.byTrigger) { + if (normalized.includes(phrase)) { + return registry.agents.get(agentName); + } + } + + return undefined; +}; + +export const findAgentsByCapability = ( + registry: AgentRegistry, + capability: string +): ReadonlyArray => { + const agentNames = registry.byCapability.get(capability) || []; + return agentNames + .map((name) => registry.agents.get(name)) + .filter((a): a is AgentDefinition => a !== undefined); +}; + +export const getAgentByName = ( + registry: AgentRegistry, + name: string +): AgentDefinition | undefined => registry.agents.get(name); + +export const listAllAgents = (registry: AgentRegistry): ReadonlyArray => + Array.from(registry.agents.values()); + +export const createAgentDefinitionContent = (agent: AgentDefinition): string => { + const frontmatter = [ + "---", + `name: ${agent.name}`, + `description: ${agent.description}`, + `tools: [${agent.tools.join(", ")}]`, + `tier: ${agent.tier}`, + `color: ${agent.color}`, + ]; + + if (agent.maxTurns) { + frontmatter.push(`maxTurns: ${agent.maxTurns}`); + } + + if (agent.triggerPhrases && agent.triggerPhrases.length > 0) { + frontmatter.push("triggerPhrases:"); + agent.triggerPhrases.forEach((phrase) => frontmatter.push(` - ${phrase}`)); + } + + if (agent.capabilities && agent.capabilities.length > 0) { + frontmatter.push("capabilities:"); + agent.capabilities.forEach((cap) => frontmatter.push(` - ${cap}`)); + } + + frontmatter.push("---"); + + const content = agent.systemPrompt || `# ${agent.name}\n\n${agent.description}`; + + return `${frontmatter.join("\n")}\n\n${content}`; +}; diff --git a/src/services/background-task-service.ts b/src/services/background-task-service.ts new file mode 100644 index 0000000..62b0ee9 --- /dev/null +++ b/src/services/background-task-service.ts @@ -0,0 +1,389 @@ +/** + * Background task service + * Manages background task execution, queue, and lifecycle + */ + +import { randomUUID } from "node:crypto"; +import { writeFile, readFile, mkdir, readdir, unlink } from "node:fs/promises"; +import { join } from "node:path"; +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; + +import type { + BackgroundTask, + BackgroundTaskStatus, + BackgroundTaskPriority, + BackgroundTaskConfig, + TaskProgress, + TaskResult, + TaskError, + TaskMetadata, + TaskNotification, + TaskStep, + TaskArtifact, +} from "@src/types/background-task"; +import { DEFAULT_BACKGROUND_TASK_CONFIG, BACKGROUND_TASK_PRIORITIES } from "@src/types/background-task"; +import { + BACKGROUND_TASK, + BACKGROUND_TASK_STORAGE, + BACKGROUND_TASK_MESSAGES, + BACKGROUND_TASK_STATUS_ICONS, +} from "@src/constants/background-task"; + +type TaskHandler = (task: BackgroundTask, updateProgress: (progress: Partial) => void) => Promise; +type NotificationHandler = (notification: TaskNotification) => void; + +interface BackgroundTaskState { + tasks: Map; + queue: string[]; + running: string[]; + handlers: Map; + notificationHandlers: NotificationHandler[]; + config: BackgroundTaskConfig; +} + +const state: BackgroundTaskState = { + tasks: new Map(), + queue: [], + running: [], + handlers: new Map(), + notificationHandlers: [], + config: DEFAULT_BACKGROUND_TASK_CONFIG, +}; + +const getStoragePath = (): string => { + const basePath = join(homedir(), ".local", "share", "codetyper", "tasks"); + return basePath; +}; + +const ensureStorageDirectory = async (): Promise => { + const storagePath = getStoragePath(); + if (!existsSync(storagePath)) { + await mkdir(storagePath, { recursive: true }); + } +}; + +const persistTask = async (task: BackgroundTask): Promise => { + if (!state.config.persistTasks) return; + + await ensureStorageDirectory(); + const filePath = join(getStoragePath(), `${task.id}${BACKGROUND_TASK_STORAGE.FILE_EXTENSION}`); + await writeFile(filePath, JSON.stringify(task, null, 2)); +}; + +const removePersistedTask = async (taskId: string): Promise => { + const filePath = join(getStoragePath(), `${taskId}${BACKGROUND_TASK_STORAGE.FILE_EXTENSION}`); + if (existsSync(filePath)) { + await unlink(filePath); + } +}; + +const loadPersistedTasks = async (): Promise => { + const storagePath = getStoragePath(); + if (!existsSync(storagePath)) return; + + const files = await readdir(storagePath); + const taskFiles = files.filter((f) => f.endsWith(BACKGROUND_TASK_STORAGE.FILE_EXTENSION)); + + for (const file of taskFiles) { + try { + const content = await readFile(join(storagePath, file), "utf-8"); + const task = JSON.parse(content) as BackgroundTask; + + // Re-queue pending/running tasks that were interrupted + if (task.status === "pending" || task.status === "running") { + const updatedTask: BackgroundTask = { + ...task, + status: "pending", + }; + state.tasks.set(task.id, updatedTask); + state.queue.push(task.id); + } else { + state.tasks.set(task.id, task); + } + } catch { + // Skip corrupted task files + } + } +}; + +const notify = (taskId: string, type: TaskNotification["type"], message: string): void => { + const notification: TaskNotification = { + taskId, + type, + message, + timestamp: Date.now(), + }; + + state.notificationHandlers.forEach((handler) => handler(notification)); +}; + +const createInitialProgress = (): TaskProgress => ({ + current: 0, + total: 100, + percentage: 0, + message: "Starting...", + steps: [], +}); + +const processQueue = async (): Promise => { + while ( + state.queue.length > 0 && + state.running.length < state.config.maxConcurrent + ) { + // Sort by priority + state.queue.sort((a, b) => { + const taskA = state.tasks.get(a); + const taskB = state.tasks.get(b); + if (!taskA || !taskB) return 0; + return BACKGROUND_TASK_PRIORITIES[taskB.priority] - BACKGROUND_TASK_PRIORITIES[taskA.priority]; + }); + + const taskId = state.queue.shift(); + if (!taskId) continue; + + const task = state.tasks.get(taskId); + if (!task) continue; + + await executeTask(task); + } +}; + +const executeTask = async (task: BackgroundTask): Promise => { + const handler = state.handlers.get(task.name); + if (!handler) { + await updateTaskStatus(task.id, "failed", { + code: "HANDLER_NOT_FOUND", + message: `No handler registered for task: ${task.name}`, + recoverable: false, + }); + return; + } + + state.running.push(task.id); + + const updatedTask: BackgroundTask = { + ...task, + status: "running", + startedAt: Date.now(), + }; + state.tasks.set(task.id, updatedTask); + await persistTask(updatedTask); + + notify(task.id, "started", BACKGROUND_TASK_MESSAGES.STARTED); + + const updateProgress = (partial: Partial): void => { + const currentTask = state.tasks.get(task.id); + if (!currentTask) return; + + const newProgress: TaskProgress = { + ...currentTask.progress, + ...partial, + percentage: partial.current !== undefined && partial.total !== undefined + ? Math.round((partial.current / partial.total) * 100) + : currentTask.progress.percentage, + }; + + const progressTask: BackgroundTask = { + ...currentTask, + progress: newProgress, + }; + state.tasks.set(task.id, progressTask); + + notify(task.id, "progress", newProgress.message); + }; + + try { + const result = await Promise.race([ + handler(updatedTask, updateProgress), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Task timeout")), state.config.defaultTimeout) + ), + ]); + + await completeTask(task.id, result); + } catch (error) { + const taskError: TaskError = { + code: "EXECUTION_ERROR", + message: error instanceof Error ? error.message : "Unknown error", + stack: error instanceof Error ? error.stack : undefined, + recoverable: true, + }; + + await updateTaskStatus(task.id, "failed", taskError); + } finally { + state.running = state.running.filter((id) => id !== task.id); + processQueue(); + } +}; + +const completeTask = async (taskId: string, result: TaskResult): Promise => { + const task = state.tasks.get(taskId); + if (!task) return; + + const completedTask: BackgroundTask = { + ...task, + status: "completed", + completedAt: Date.now(), + result, + progress: { + ...task.progress, + current: task.progress.total, + percentage: 100, + message: "Completed", + }, + }; + + state.tasks.set(taskId, completedTask); + await persistTask(completedTask); + + notify(taskId, "completed", BACKGROUND_TASK_MESSAGES.COMPLETED); +}; + +const updateTaskStatus = async ( + taskId: string, + status: BackgroundTaskStatus, + error?: TaskError +): Promise => { + const task = state.tasks.get(taskId); + if (!task) return; + + const updatedTask: BackgroundTask = { + ...task, + status, + error, + completedAt: ["completed", "failed", "cancelled"].includes(status) ? Date.now() : undefined, + }; + + state.tasks.set(taskId, updatedTask); + await persistTask(updatedTask); + + if (status === "failed") { + notify(taskId, "failed", error?.message || BACKGROUND_TASK_MESSAGES.FAILED); + } +}; + +// Public API + +export const initialize = async (config?: Partial): Promise => { + state.config = { ...DEFAULT_BACKGROUND_TASK_CONFIG, ...config }; + await loadPersistedTasks(); + processQueue(); +}; + +export const registerHandler = (name: string, handler: TaskHandler): void => { + state.handlers.set(name, handler); +}; + +export const onNotification = (handler: NotificationHandler): () => void => { + state.notificationHandlers.push(handler); + return () => { + state.notificationHandlers = state.notificationHandlers.filter((h) => h !== handler); + }; +}; + +export const createTask = async ( + name: string, + description: string, + metadata: TaskMetadata, + priority: BackgroundTaskPriority = "normal" +): Promise => { + const task: BackgroundTask = { + id: randomUUID(), + name, + description, + status: "pending", + priority, + createdAt: Date.now(), + progress: createInitialProgress(), + metadata, + }; + + state.tasks.set(task.id, task); + state.queue.push(task.id); + + await persistTask(task); + processQueue(); + + return task; +}; + +export const cancelTask = async (taskId: string): Promise => { + const task = state.tasks.get(taskId); + if (!task) return false; + + if (task.status === "running") { + await updateTaskStatus(taskId, "cancelled"); + state.running = state.running.filter((id) => id !== taskId); + notify(taskId, "failed", BACKGROUND_TASK_MESSAGES.CANCELLED); + return true; + } + + if (task.status === "pending") { + state.queue = state.queue.filter((id) => id !== taskId); + await updateTaskStatus(taskId, "cancelled"); + return true; + } + + return false; +}; + +export const pauseTask = async (taskId: string): Promise => { + const task = state.tasks.get(taskId); + if (!task || task.status !== "running") return false; + + await updateTaskStatus(taskId, "paused"); + state.running = state.running.filter((id) => id !== taskId); + notify(taskId, "progress", BACKGROUND_TASK_MESSAGES.PAUSED); + return true; +}; + +export const resumeTask = async (taskId: string): Promise => { + const task = state.tasks.get(taskId); + if (!task || task.status !== "paused") return false; + + state.queue.unshift(taskId); + await updateTaskStatus(taskId, "pending"); + notify(taskId, "progress", BACKGROUND_TASK_MESSAGES.RESUMED); + processQueue(); + return true; +}; + +export const getTask = (taskId: string): BackgroundTask | undefined => + state.tasks.get(taskId); + +export const listTasks = (filter?: { status?: BackgroundTaskStatus }): ReadonlyArray => { + let tasks = Array.from(state.tasks.values()); + + if (filter?.status) { + tasks = tasks.filter((t) => t.status === filter.status); + } + + return tasks.sort((a, b) => b.createdAt - a.createdAt); +}; + +export const clearCompletedTasks = async (): Promise => { + const completed = Array.from(state.tasks.values()).filter( + (t) => t.status === "completed" || t.status === "failed" || t.status === "cancelled" + ); + + for (const task of completed) { + state.tasks.delete(task.id); + await removePersistedTask(task.id); + } + + return completed.length; +}; + +export const getTaskStatusIcon = (status: BackgroundTaskStatus): string => + BACKGROUND_TASK_STATUS_ICONS[status]; + +export const formatTaskSummary = (task: BackgroundTask): string => { + const icon = getTaskStatusIcon(task.status); + const progress = task.status === "running" ? ` (${task.progress.percentage}%)` : ""; + return `${icon} ${task.name}${progress} - ${task.description}`; +}; + +export const getQueueLength = (): number => state.queue.length; + +export const getRunningCount = (): number => state.running.length; diff --git a/src/services/brain.ts b/src/services/brain.ts new file mode 100644 index 0000000..dc90a05 --- /dev/null +++ b/src/services/brain.ts @@ -0,0 +1,688 @@ +/** + * Brain Service + * + * Business logic layer for the CodeTyper Brain integration. + * Provides context injection, knowledge recall, and learning capabilities. + */ + +import fs from "fs/promises"; +import { DIRS, FILES } from "@constants/paths"; +import { BRAIN_DEFAULTS, BRAIN_ERRORS, BRAIN_DISABLED } from "@constants/brain"; +import * as brainApi from "@api/brain"; +import type { + BrainCredentials, + BrainState, + BrainConnectionStatus, + BrainUser, + BrainConcept, + BrainRecallResponse, + BrainExtractResponse, +} from "@/types/brain"; + +// ============================================================================ +// State Management (Singleton via Closure) +// ============================================================================ + +interface VarsFile { + brainApiKey?: string; + brainJwtToken?: string; +} + +let brainState: BrainState = { + status: "disconnected", + user: null, + projectId: BRAIN_DEFAULTS.PROJECT_ID, + knowledgeCount: 0, + memoryCount: 0, + lastError: null, +}; + +let cachedCredentials: BrainCredentials | null = null; +let cachedVars: VarsFile | null = null; + +// ============================================================================ +// Vars File Management +// ============================================================================ + +/** + * Load vars file from disk + */ +const loadVarsFile = async (): Promise => { + if (cachedVars) { + return cachedVars; + } + + try { + const data = await fs.readFile(FILES.vars, "utf-8"); + cachedVars = JSON.parse(data) as VarsFile; + return cachedVars; + } catch { + return {}; + } +}; + +/** + * Save vars file to disk + */ +const saveVarsFile = async (vars: VarsFile): Promise => { + try { + await fs.mkdir(DIRS.config, { recursive: true }); + await fs.writeFile(FILES.vars, JSON.stringify(vars, null, 2), "utf-8"); + cachedVars = vars; + } catch (error) { + throw new Error(`Failed to save vars file: ${error}`); + } +}; + +// ============================================================================ +// Credentials Management +// ============================================================================ + +/** + * Get path to brain credentials file + */ +const getCredentialsPath = (): string => { + return `${DIRS.data}/brain-credentials.json`; +}; + +/** + * Load brain credentials from disk + */ +export const loadCredentials = async (): Promise => { + if (cachedCredentials) { + return cachedCredentials; + } + + try { + const data = await fs.readFile(getCredentialsPath(), "utf-8"); + cachedCredentials = JSON.parse(data) as BrainCredentials; + return cachedCredentials; + } catch { + return null; + } +}; + +/** + * Save brain credentials to disk + */ +export const saveCredentials = async ( + credentials: BrainCredentials, +): Promise => { + try { + await fs.mkdir(DIRS.data, { recursive: true }); + await fs.writeFile( + getCredentialsPath(), + JSON.stringify(credentials, null, 2), + "utf-8", + ); + cachedCredentials = credentials; + } catch (error) { + throw new Error(`Failed to save brain credentials: ${error}`); + } +}; + +/** + * Clear brain credentials + */ +export const clearCredentials = async (): Promise => { + try { + await fs.unlink(getCredentialsPath()); + cachedCredentials = null; + } catch { + // File may not exist, ignore + } + + // Also clear vars file entries + try { + const vars = await loadVarsFile(); + await saveVarsFile({ + ...vars, + brainApiKey: undefined, + brainJwtToken: undefined, + }); + } catch { + // Ignore errors + } +}; + +/** + * Get API key from vars file or environment + */ +export const getApiKey = async (): Promise => { + // First check environment variable + const envKey = process.env.CODETYPER_BRAIN_API_KEY; + if (envKey) { + return envKey; + } + + // Then check vars file + const vars = await loadVarsFile(); + return vars.brainApiKey; +}; + +/** + * Get JWT token from vars file + */ +export const getJwtToken = async (): Promise => { + const vars = await loadVarsFile(); + return vars.brainJwtToken; +}; + +/** + * Set API key in vars file + */ +export const setApiKey = async (apiKey: string): Promise => { + const vars = await loadVarsFile(); + await saveVarsFile({ ...vars, brainApiKey: apiKey }); +}; + +/** + * Set JWT token in vars file + */ +export const setJwtToken = async (jwtToken: string): Promise => { + const vars = await loadVarsFile(); + await saveVarsFile({ ...vars, brainJwtToken: jwtToken }); +}; + +// ============================================================================ +// Authentication +// ============================================================================ + +/** + * Login to Brain service + */ +export const login = async ( + email: string, + password: string, +): Promise<{ success: boolean; user?: BrainUser; error?: string }> => { + try { + updateState({ status: "connecting" }); + + const response = await brainApi.login(email, password); + + if (response.success && response.data) { + const credentials: BrainCredentials = { + accessToken: response.data.access_token, + refreshToken: response.data.refresh_token, + expiresAt: response.data.expires_at, + user: response.data.user, + }; + + await saveCredentials(credentials); + + updateState({ + status: "connected", + user: response.data.user, + lastError: null, + }); + + return { success: true, user: response.data.user }; + } + + updateState({ status: "error", lastError: "Login failed" }); + return { success: false, error: "Login failed" }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + updateState({ status: "error", lastError: errorMessage }); + return { success: false, error: errorMessage }; + } +}; + +/** + * Register a new account + */ +export const register = async ( + email: string, + password: string, + displayName: string, +): Promise<{ success: boolean; user?: BrainUser; error?: string }> => { + try { + updateState({ status: "connecting" }); + + const response = await brainApi.register(email, password, displayName); + + if (response.success && response.data) { + const credentials: BrainCredentials = { + accessToken: response.data.access_token, + refreshToken: response.data.refresh_token, + expiresAt: response.data.expires_at, + user: response.data.user, + }; + + await saveCredentials(credentials); + + updateState({ + status: "connected", + user: response.data.user, + lastError: null, + }); + + return { success: true, user: response.data.user }; + } + + updateState({ status: "error", lastError: "Registration failed" }); + return { success: false, error: "Registration failed" }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + updateState({ status: "error", lastError: errorMessage }); + return { success: false, error: errorMessage }; + } +}; + +/** + * Logout from Brain service + */ +export const logout = async (): Promise => { + try { + const credentials = await loadCredentials(); + if (credentials?.refreshToken) { + await brainApi.logout(credentials.refreshToken); + } + } catch { + // Ignore logout errors + } finally { + await clearCredentials(); + updateState({ + status: "disconnected", + user: null, + knowledgeCount: 0, + memoryCount: 0, + }); + } +}; + +// ============================================================================ +// Connection Management +// ============================================================================ + +/** + * Get authentication token (API key or JWT token) + */ +export const getAuthToken = async (): Promise => { + const apiKey = await getApiKey(); + if (apiKey) { + return apiKey; + } + return getJwtToken(); +}; + +/** + * Check if Brain service is available and connect + */ +export const connect = async (): Promise => { + // Skip connection when Brain is disabled + if (BRAIN_DISABLED) { + return false; + } + + try { + updateState({ status: "connecting" }); + + // First check if service is healthy + await brainApi.checkHealth(); + + // Then check if we have valid credentials (API key or JWT token) + const authToken = await getAuthToken(); + if (!authToken) { + updateState({ status: "disconnected", lastError: null }); + return false; + } + + // Try to get stats to verify credentials are valid + const projectId = brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID; + const statsResponse = await brainApi.getKnowledgeStats(projectId, authToken); + + if (statsResponse.success && statsResponse.data) { + updateState({ + status: "connected", + knowledgeCount: statsResponse.data.total_concepts, + lastError: null, + }); + + // Also try to get memory stats + try { + const memoryStats = await brainApi.getMemoryStats(authToken); + updateState({ memoryCount: memoryStats.totalNodes }); + } catch { + // Memory stats are optional + } + + return true; + } + + updateState({ status: "error", lastError: BRAIN_ERRORS.INVALID_API_KEY }); + return false; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : BRAIN_ERRORS.CONNECTION_FAILED; + updateState({ status: "error", lastError: errorMessage }); + return false; + } +}; + +/** + * Disconnect from Brain service + */ +export const disconnect = (): void => { + updateState({ + status: "disconnected", + knowledgeCount: 0, + memoryCount: 0, + lastError: null, + }); +}; + +/** + * Check if connected to Brain + */ +export const isConnected = (): boolean => { + if (BRAIN_DISABLED) return false; + return brainState.status === "connected"; +}; + +// ============================================================================ +// Knowledge Operations +// ============================================================================ + +/** + * Recall relevant knowledge for a query + */ +export const recall = async ( + query: string, + limit = 5, +): Promise => { + if (!isConnected()) { + return null; + } + + try { + const apiKey = await getApiKey(); + if (!apiKey) { + return null; + } + + const response = await brainApi.recallKnowledge( + { + query, + project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, + limit, + }, + apiKey, + ); + + return response; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : BRAIN_ERRORS.RECALL_FAILED; + updateState({ lastError: errorMessage }); + return null; + } +}; + +/** + * Get context string for prompt injection + */ +export const getContext = async ( + query: string, + maxConcepts = 3, +): Promise => { + if (!isConnected()) { + return null; + } + + try { + const apiKey = await getApiKey(); + if (!apiKey) { + return null; + } + + const response = await brainApi.buildContext( + { + query, + project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, + max_concepts: maxConcepts, + }, + apiKey, + ); + + if (response.success && response.data.has_knowledge) { + return response.data.context; + } + + return null; + } catch { + return null; + } +}; + +/** + * Learn a concept + */ +export const learn = async ( + name: string, + whatItDoes: string, + options?: { + howItWorks?: string; + patterns?: string[]; + files?: string[]; + keyFunctions?: string[]; + aliases?: string[]; + }, +): Promise => { + if (!isConnected()) { + return null; + } + + try { + const apiKey = await getApiKey(); + if (!apiKey) { + return null; + } + + const response = await brainApi.learnConcept( + { + project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, + name, + what_it_does: whatItDoes, + how_it_works: options?.howItWorks, + patterns: options?.patterns, + files: options?.files, + key_functions: options?.keyFunctions, + aliases: options?.aliases, + }, + apiKey, + ); + + if (response.success && response.data) { + // Update knowledge count + updateState({ knowledgeCount: brainState.knowledgeCount + 1 }); + return response.data; + } + + return null; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : BRAIN_ERRORS.LEARN_FAILED; + updateState({ lastError: errorMessage }); + return null; + } +}; + +/** + * Extract and learn concepts from content + */ +export const extractAndLearn = async ( + content: string, + source = "conversation", +): Promise => { + if (!isConnected()) { + return null; + } + + try { + const apiKey = await getApiKey(); + if (!apiKey) { + return null; + } + + const response = await brainApi.extractConcepts( + { + content, + project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, + source, + }, + apiKey, + ); + + if (response.success) { + // Update knowledge count + const newCount = + brainState.knowledgeCount + response.data.stored + response.data.updated; + updateState({ knowledgeCount: newCount }); + return response; + } + + return null; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : BRAIN_ERRORS.EXTRACT_FAILED; + updateState({ lastError: errorMessage }); + return null; + } +}; + +// ============================================================================ +// Memory Operations +// ============================================================================ + +/** + * Search memories + */ +export const searchMemories = async ( + query: string, + limit = 10, +): Promise<{ memories: Array<{ content: string; similarity: number }> } | null> => { + if (!isConnected()) { + return null; + } + + try { + const apiKey = await getApiKey(); + if (!apiKey) { + return null; + } + + const response = await brainApi.searchMemories( + { + query, + limit, + project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, + }, + apiKey, + ); + + return { + memories: response.memories.map((m) => ({ + content: m.content, + similarity: m.similarity ?? 0, + })), + }; + } catch { + return null; + } +}; + +/** + * Store a memory + */ +export const storeMemory = async ( + content: string, + type: "fact" | "pattern" | "correction" | "preference" | "context" = "context", +): Promise => { + if (!isConnected()) { + return false; + } + + try { + const apiKey = await getApiKey(); + if (!apiKey) { + return false; + } + + const response = await brainApi.storeMemory( + { + content, + type, + project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, + }, + apiKey, + ); + + if (response.success) { + updateState({ memoryCount: brainState.memoryCount + 1 }); + return true; + } + + return false; + } catch { + return false; + } +}; + +// ============================================================================ +// State Accessors +// ============================================================================ + +/** + * Get current brain state + */ +export const getState = (): BrainState => { + return { ...brainState }; +}; + +/** + * Update brain state + */ +const updateState = (updates: Partial): void => { + brainState = { ...brainState, ...updates }; +}; + +/** + * Set project ID + */ +export const setProjectId = (projectId: number): void => { + updateState({ projectId }); +}; + +/** + * Get connection status + */ +export const getStatus = (): BrainConnectionStatus => { + return brainState.status; +}; + +/** + * Check if authenticated (has API key or JWT token) + */ +export const isAuthenticated = async (): Promise => { + const apiKey = await getApiKey(); + const jwtToken = await getJwtToken(); + return apiKey !== undefined || jwtToken !== undefined; +}; + +// ============================================================================ +// Initialization +// ============================================================================ + +/** + * Initialize brain service (auto-connect if credentials available) + */ +export const initialize = async (): Promise => { + const hasAuth = await isAuthenticated(); + if (hasAuth) { + return connect(); + } + return false; +}; diff --git a/src/services/brain/cloud-sync.ts b/src/services/brain/cloud-sync.ts new file mode 100644 index 0000000..52aa931 --- /dev/null +++ b/src/services/brain/cloud-sync.ts @@ -0,0 +1,523 @@ +/** + * Cloud Sync Service + * + * Handles push/pull synchronization with the cloud brain service. + */ + +import { + CLOUD_BRAIN_DEFAULTS, + CLOUD_ENDPOINTS, + CLOUD_ERRORS, + CLOUD_MESSAGES, + CLOUD_HTTP_CONFIG, + SYNC_CONFIG, +} from "@constants/brain-cloud"; +import { + enqueue, + enqueueBatch, + dequeue, + markProcessed, + markFailed, + hasQueuedItems, + getQueueSize, + clearQueue, +} from "@services/brain/offline-queue"; +import { + createConflict, + resolveAllConflicts, + getPendingConflicts, + hasUnresolvedConflicts, + clearResolvedConflicts, +} from "@services/brain/conflict-resolver"; +import type { + BrainSyncState, + CloudBrainConfig, + SyncItem, + SyncResult, + SyncOptions, + PushRequest, + PushResponse, + PullRequest, + PullResponse, +} from "@/types/brain-cloud"; + +// Sync state +let syncState: BrainSyncState = { + status: "synced", + lastSyncAt: null, + lastPushAt: null, + lastPullAt: null, + pendingChanges: 0, + conflictCount: 0, + syncErrors: [], +}; + +// Cloud configuration +let cloudConfig: CloudBrainConfig = { ...CLOUD_BRAIN_DEFAULTS }; + +// Sync lock to prevent concurrent syncs +let syncInProgress = false; + +// Local version tracking +let localVersion = 0; + +/** + * Configure cloud sync + */ +export const configure = (config: Partial): void => { + cloudConfig = { ...cloudConfig, ...config }; +}; + +/** + * Get current sync state + */ +export const getSyncState = (): BrainSyncState => ({ ...syncState }); + +/** + * Get cloud configuration + */ +export const getConfig = (): CloudBrainConfig => ({ ...cloudConfig }); + +/** + * Check if cloud sync is enabled + */ +export const isEnabled = (): boolean => cloudConfig.enabled; + +/** + * Check if device is online + */ +const isOnline = (): boolean => { + // In Node.js/Bun, we'll assume online unless proven otherwise + return true; +}; + +/** + * Perform a full sync (push then pull) + */ +export const sync = async ( + authToken: string, + projectId: number, + options: SyncOptions = {}, +): Promise => { + if (!cloudConfig.enabled) { + throw new Error(CLOUD_ERRORS.NOT_CONFIGURED); + } + + if (syncInProgress) { + throw new Error(CLOUD_ERRORS.SYNC_IN_PROGRESS); + } + + if (!isOnline()) { + syncState.status = "offline"; + throw new Error(CLOUD_ERRORS.OFFLINE); + } + + syncInProgress = true; + syncState.status = "syncing"; + syncState.syncErrors = []; + + const startTime = Date.now(); + const result: SyncResult = { + success: true, + direction: options.direction ?? "both", + itemsSynced: 0, + itemsFailed: 0, + conflicts: [], + errors: [], + duration: 0, + timestamp: startTime, + }; + + try { + const direction = options.direction ?? "both"; + + // Push local changes + if (direction === "push" || direction === "both") { + options.onProgress?.({ + phase: "pushing", + current: 0, + total: await getQueueSize(), + message: CLOUD_MESSAGES.STARTING_SYNC, + }); + + const pushResult = await pushChanges(authToken, projectId, options); + result.itemsSynced += pushResult.itemsSynced; + result.itemsFailed += pushResult.itemsFailed; + result.conflicts.push(...pushResult.conflicts); + result.errors.push(...pushResult.errors); + + if (pushResult.errors.length > 0) { + result.success = false; + } + } + + // Pull remote changes + if (direction === "pull" || direction === "both") { + options.onProgress?.({ + phase: "pulling", + current: 0, + total: 0, + message: CLOUD_MESSAGES.PULLING(0), + }); + + const pullResult = await pullChanges(authToken, projectId, options); + result.itemsSynced += pullResult.itemsSynced; + result.itemsFailed += pullResult.itemsFailed; + result.conflicts.push(...pullResult.conflicts); + result.errors.push(...pullResult.errors); + + if (pullResult.errors.length > 0) { + result.success = false; + } + } + + // Handle conflicts if any + if (result.conflicts.length > 0) { + options.onProgress?.({ + phase: "resolving", + current: 0, + total: result.conflicts.length, + message: CLOUD_MESSAGES.RESOLVING_CONFLICTS(result.conflicts.length), + }); + + const strategy = options.conflictStrategy ?? cloudConfig.conflictStrategy; + + if (strategy !== "manual") { + resolveAllConflicts(strategy); + result.conflicts = getPendingConflicts(); + } + + if (hasUnresolvedConflicts()) { + syncState.status = "conflict"; + syncState.conflictCount = result.conflicts.length; + } + } + + // Update state + result.duration = Date.now() - startTime; + + if (result.success && result.conflicts.length === 0) { + syncState.status = "synced"; + syncState.lastSyncAt = Date.now(); + } else if (result.conflicts.length > 0) { + syncState.status = "conflict"; + } else { + syncState.status = "error"; + } + + syncState.pendingChanges = await getQueueSize(); + syncState.syncErrors = result.errors; + + options.onProgress?.({ + phase: "completing", + current: result.itemsSynced, + total: result.itemsSynced, + message: CLOUD_MESSAGES.SYNC_COMPLETE, + }); + + return result; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + syncState.status = "error"; + syncState.syncErrors.push(message); + + result.success = false; + result.errors.push(message); + result.duration = Date.now() - startTime; + + return result; + } finally { + syncInProgress = false; + clearResolvedConflicts(); + } +}; + +/** + * Push local changes to cloud + */ +const pushChanges = async ( + authToken: string, + projectId: number, + options: SyncOptions, +): Promise> => { + const result = { + success: true, + itemsSynced: 0, + itemsFailed: 0, + conflicts: [] as SyncResult["conflicts"], + errors: [] as string[], + duration: 0, + }; + + // Get queued items + const queuedItems = await dequeue(SYNC_CONFIG.MAX_BATCH_SIZE); + + if (queuedItems.length === 0) { + return result; + } + + options.onProgress?.({ + phase: "pushing", + current: 0, + total: queuedItems.length, + message: CLOUD_MESSAGES.PUSHING(queuedItems.length), + }); + + const items = queuedItems.map((q) => q.item); + + try { + const response = await pushToCloud(authToken, projectId, items); + + if (response.success) { + result.itemsSynced = response.accepted; + result.itemsFailed = response.rejected; + + // Mark successful items as processed + const successIds = queuedItems + .slice(0, response.accepted) + .map((q) => q.id); + await markProcessed(successIds); + + // Handle conflicts + for (const conflict of response.conflicts) { + result.conflicts.push(conflict); + } + + syncState.lastPushAt = Date.now(); + } else { + result.success = false; + result.errors.push(...(response.errors ?? [])); + + // Mark all as failed + await markFailed( + queuedItems.map((q) => q.id), + response.errors?.[0], + ); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + result.success = false; + result.errors.push(CLOUD_ERRORS.PUSH_FAILED(message)); + + // Queue for retry + await markFailed( + queuedItems.map((q) => q.id), + message, + ); + } + + return result; +}; + +/** + * Pull remote changes from cloud + */ +const pullChanges = async ( + authToken: string, + projectId: number, + options: SyncOptions, +): Promise> => { + const result = { + success: true, + itemsSynced: 0, + itemsFailed: 0, + conflicts: [] as SyncResult["conflicts"], + errors: [] as string[], + duration: 0, + }; + + try { + const response = await pullFromCloud( + authToken, + projectId, + localVersion, + syncState.lastPullAt ?? 0, + ); + + if (response.success) { + options.onProgress?.({ + phase: "pulling", + current: response.items.length, + total: response.items.length, + message: CLOUD_MESSAGES.PULLING(response.items.length), + }); + + // Process pulled items + for (const item of response.items) { + // Check for conflicts with local changes + const hasConflict = await checkLocalConflict(item); + + if (hasConflict) { + // Create conflict entry + const localItem = await getLocalItem(item.id, item.type); + if (localItem) { + const conflict = createConflict(localItem, item); + result.conflicts.push(conflict); + } + } else { + // Apply remote change locally + await applyRemoteChange(item); + result.itemsSynced++; + } + } + + // Update local version + localVersion = response.serverVersion; + syncState.lastPullAt = Date.now(); + } else { + result.success = false; + result.errors.push(...(response.errors ?? [])); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + result.success = false; + result.errors.push(CLOUD_ERRORS.PULL_FAILED(message)); + } + + return result; +}; + +/** + * Push items to cloud API + */ +const pushToCloud = async ( + authToken: string, + projectId: number, + items: SyncItem[], +): Promise => { + const url = `${cloudConfig.endpoint}${CLOUD_ENDPOINTS.PUSH}`; + + const request: PushRequest = { + items, + projectId, + clientVersion: "1.0.0", + }; + + const response = await fetch(url, { + method: "POST", + headers: { + ...CLOUD_HTTP_CONFIG.HEADERS, + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify(request), + signal: AbortSignal.timeout(CLOUD_HTTP_CONFIG.TIMEOUT_MS), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response.json() as Promise; +}; + +/** + * Pull items from cloud API + */ +const pullFromCloud = async ( + authToken: string, + projectId: number, + sinceVersion: number, + sinceTimestamp: number, +): Promise => { + const url = `${cloudConfig.endpoint}${CLOUD_ENDPOINTS.PULL}`; + + const request: PullRequest = { + projectId, + sinceVersion, + sinceTimestamp, + limit: SYNC_CONFIG.MAX_BATCH_SIZE, + }; + + const response = await fetch(url, { + method: "POST", + headers: { + ...CLOUD_HTTP_CONFIG.HEADERS, + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify(request), + signal: AbortSignal.timeout(CLOUD_HTTP_CONFIG.TIMEOUT_MS), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response.json() as Promise; +}; + +/** + * Check if pulled item conflicts with local changes + */ +const checkLocalConflict = async ( + _item: SyncItem, +): Promise => { + // Check if we have pending changes for this item + const queued = await hasQueuedItems(); + return queued; +}; + +/** + * Get local item by ID and type + */ +const getLocalItem = async ( + _id: string, + _type: "concept" | "memory" | "relation", +): Promise => { + // This would retrieve the local item from the brain service + // Placeholder implementation + return null; +}; + +/** + * Apply a remote change locally + */ +const applyRemoteChange = async (_item: SyncItem): Promise => { + // This would apply the change to the local brain storage + // Placeholder implementation +}; + +/** + * Queue a change for sync + */ +export const queueChange = async (item: SyncItem): Promise => { + await enqueue(item); + syncState.pendingChanges = await getQueueSize(); + syncState.status = "pending"; +}; + +/** + * Queue multiple changes + */ +export const queueChanges = async (items: SyncItem[]): Promise => { + const added = await enqueueBatch(items); + syncState.pendingChanges = await getQueueSize(); + syncState.status = "pending"; + return added; +}; + +/** + * Force sync now + */ +export const syncNow = async ( + authToken: string, + projectId: number, +): Promise => { + return sync(authToken, projectId, { force: true }); +}; + +/** + * Reset sync state + */ +export const resetSyncState = async (): Promise => { + await clearQueue(); + syncState = { + status: "synced", + lastSyncAt: null, + lastPushAt: null, + lastPullAt: null, + pendingChanges: 0, + conflictCount: 0, + syncErrors: [], + }; + localVersion = 0; +}; diff --git a/src/services/brain/conflict-resolver.ts b/src/services/brain/conflict-resolver.ts new file mode 100644 index 0000000..9028672 --- /dev/null +++ b/src/services/brain/conflict-resolver.ts @@ -0,0 +1,249 @@ +/** + * Conflict Resolver + * + * Handles sync conflicts between local and remote brain data. + */ + +import { + CONFLICT_LABELS, +} from "@constants/brain-cloud"; +import type { + SyncConflict, + ConflictStrategy, + SyncItem, +} from "@/types/brain-cloud"; + +// In-memory conflict storage +const pendingConflicts = new Map(); + +/** + * Create a conflict from local and remote items + */ +export const createConflict = ( + localItem: SyncItem, + remoteItem: SyncItem, +): SyncConflict => { + const conflict: SyncConflict = { + id: generateConflictId(), + itemId: localItem.id, + itemType: localItem.type, + localData: localItem.data, + remoteData: remoteItem.data, + localVersion: localItem.localVersion, + remoteVersion: remoteItem.remoteVersion ?? 0, + localTimestamp: localItem.timestamp, + remoteTimestamp: remoteItem.timestamp, + resolved: false, + }; + + pendingConflicts.set(conflict.id, conflict); + return conflict; +}; + +/** + * Resolve a conflict using the specified strategy + */ +export const resolveConflict = ( + conflictId: string, + strategy: ConflictStrategy, +): SyncConflict | null => { + const conflict = pendingConflicts.get(conflictId); + if (!conflict) return null; + + const resolver = resolvers[strategy]; + const resolvedData = resolver(conflict); + + conflict.resolved = true; + conflict.resolution = strategy; + conflict.resolvedData = resolvedData; + + return conflict; +}; + +/** + * Resolve all pending conflicts with a single strategy + */ +export const resolveAllConflicts = ( + strategy: ConflictStrategy, +): SyncConflict[] => { + const resolved: SyncConflict[] = []; + + for (const [id, conflict] of pendingConflicts) { + if (!conflict.resolved) { + const result = resolveConflict(id, strategy); + if (result) { + resolved.push(result); + } + } + } + + return resolved; +}; + +/** + * Conflict resolution strategies + */ +const resolvers: Record unknown> = { + "local-wins": (conflict) => conflict.localData, + + "remote-wins": (conflict) => conflict.remoteData, + + manual: (_conflict) => { + // Manual resolution returns null - requires user input + return null; + }, + + merge: (conflict) => { + // Attempt to merge the data + return mergeData(conflict.localData, conflict.remoteData); + }, +}; + +/** + * Attempt to merge two data objects + */ +const mergeData = (local: unknown, remote: unknown): unknown => { + // If both are objects, merge their properties + if (isObject(local) && isObject(remote)) { + const localObj = local as Record; + const remoteObj = remote as Record; + + const merged: Record = { ...remoteObj }; + + for (const key of Object.keys(localObj)) { + // Local wins for non-timestamp fields that differ + if (key !== "updatedAt" && key !== "timestamp") { + merged[key] = localObj[key]; + } + } + + // Use most recent timestamp + const localTime = (localObj.updatedAt ?? localObj.timestamp ?? 0) as number; + const remoteTime = (remoteObj.updatedAt ?? remoteObj.timestamp ?? 0) as number; + merged.updatedAt = Math.max(localTime, remoteTime); + + return merged; + } + + // For non-objects, prefer local (or most recent) + return local; +}; + +/** + * Check if value is an object + */ +const isObject = (value: unknown): value is Record => { + return typeof value === "object" && value !== null && !Array.isArray(value); +}; + +/** + * Get pending conflicts + */ +export const getPendingConflicts = (): SyncConflict[] => { + return Array.from(pendingConflicts.values()).filter((c) => !c.resolved); +}; + +/** + * Get all conflicts + */ +export const getAllConflicts = (): SyncConflict[] => { + return Array.from(pendingConflicts.values()); +}; + +/** + * Get conflict by ID + */ +export const getConflict = (id: string): SyncConflict | undefined => { + return pendingConflicts.get(id); +}; + +/** + * Clear resolved conflicts + */ +export const clearResolvedConflicts = (): number => { + let cleared = 0; + + for (const [id, conflict] of pendingConflicts) { + if (conflict.resolved) { + pendingConflicts.delete(id); + cleared++; + } + } + + return cleared; +}; + +/** + * Clear all conflicts + */ +export const clearAllConflicts = (): void => { + pendingConflicts.clear(); +}; + +/** + * Get conflict count + */ +export const getConflictCount = (): number => { + return getPendingConflicts().length; +}; + +/** + * Check if there are unresolved conflicts + */ +export const hasUnresolvedConflicts = (): boolean => { + return getPendingConflicts().length > 0; +}; + +/** + * Get suggested resolution for a conflict + */ +export const suggestResolution = (conflict: SyncConflict): ConflictStrategy => { + // If remote is newer, suggest remote-wins + if (conflict.remoteTimestamp > conflict.localTimestamp) { + return "remote-wins"; + } + + // If local is newer, suggest local-wins + if (conflict.localTimestamp > conflict.remoteTimestamp) { + return "local-wins"; + } + + // If timestamps are equal, try merge + return "merge"; +}; + +/** + * Format conflict for display + */ +export const formatConflict = (conflict: SyncConflict): string => { + const lines: string[] = []; + + lines.push(`**Conflict: ${conflict.itemId}**`); + lines.push(`Type: ${conflict.itemType}`); + lines.push(`Local version: ${conflict.localVersion}`); + lines.push(`Remote version: ${conflict.remoteVersion}`); + lines.push(""); + lines.push("Local data:"); + lines.push("```json"); + lines.push(JSON.stringify(conflict.localData, null, 2)); + lines.push("```"); + lines.push(""); + lines.push("Remote data:"); + lines.push("```json"); + lines.push(JSON.stringify(conflict.remoteData, null, 2)); + lines.push("```"); + + if (conflict.resolved) { + lines.push(""); + lines.push(`Resolution: ${CONFLICT_LABELS[conflict.resolution!]}`); + } + + return lines.join("\n"); +}; + +/** + * Generate unique conflict ID + */ +const generateConflictId = (): string => { + return `conflict_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +}; diff --git a/src/services/brain/mcp-server.ts b/src/services/brain/mcp-server.ts new file mode 100644 index 0000000..0e3a99a --- /dev/null +++ b/src/services/brain/mcp-server.ts @@ -0,0 +1,354 @@ +/** + * Brain MCP Server service + * Exposes Brain as an MCP server for external tools + */ + +import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http"; +import { randomUUID } from "node:crypto"; + +import type { + BrainMcpServerConfig, + BrainMcpRequest, + BrainMcpResponse, + BrainMcpServerStatus, + BrainMcpToolName, + McpContent, + McpError, +} from "@src/types/brain-mcp"; +import { + DEFAULT_BRAIN_MCP_SERVER_CONFIG, + BRAIN_MCP_TOOLS, + MCP_ERROR_CODES, +} from "@src/types/brain-mcp"; +import { + BRAIN_MCP_SERVER, + BRAIN_MCP_MESSAGES, + BRAIN_MCP_ERRORS, + BRAIN_MCP_AUTH, +} from "@src/constants/brain-mcp"; + +type BrainService = { + recall: (query: string, limit?: number) => Promise; + learn: (name: string, whatItDoes: string, options?: unknown) => Promise; + searchMemories: (query: string, limit?: number, type?: string) => Promise; + relate: (source: string, target: string, type: string, weight?: number) => Promise; + getContext: (query: string, maxConcepts?: number) => Promise; + getStats: () => Promise; + isConnected: () => boolean; +}; + +interface McpServerState { + server: Server | null; + config: BrainMcpServerConfig; + brainService: BrainService | null; + connectedClients: number; + startTime: number | null; + requestsServed: number; + lastRequestAt: number | null; + rateLimitMap: Map; + apiKeys: Set; +} + +const state: McpServerState = { + server: null, + config: DEFAULT_BRAIN_MCP_SERVER_CONFIG, + brainService: null, + connectedClients: 0, + startTime: null, + requestsServed: 0, + lastRequestAt: null, + rateLimitMap: new Map(), + apiKeys: new Set(), +}; + +const createMcpError = (code: number, message: string, data?: unknown): McpError => ({ + code, + message, + data, +}); + +const createMcpResponse = ( + id: string | number, + content?: ReadonlyArray, + error?: McpError +): BrainMcpResponse => { + if (error) { + return { id, error }; + } + + return { + id, + result: { + content: content || [], + }, + }; +}; + +const checkRateLimit = (clientIp: string): boolean => { + if (!state.config.rateLimit.enabled) return true; + + const now = Date.now(); + const clientLimit = state.rateLimitMap.get(clientIp); + + if (!clientLimit || now > clientLimit.resetAt) { + state.rateLimitMap.set(clientIp, { + count: 1, + resetAt: now + state.config.rateLimit.windowMs, + }); + return true; + } + + if (clientLimit.count >= state.config.rateLimit.maxRequests) { + return false; + } + + state.rateLimitMap.set(clientIp, { + ...clientLimit, + count: clientLimit.count + 1, + }); + + return true; +}; + +const validateApiKey = (req: IncomingMessage): boolean => { + if (!state.config.enableAuth) return true; + + const apiKey = req.headers[state.config.apiKeyHeader.toLowerCase()] as string | undefined; + + if (!apiKey) return false; + + // If no API keys configured, accept any key for now + if (state.apiKeys.size === 0) return true; + + return state.apiKeys.has(apiKey); +}; + +const handleToolCall = async ( + toolName: BrainMcpToolName, + args: Record +): Promise => { + if (!state.brainService) { + throw createMcpError(MCP_ERROR_CODES.BRAIN_UNAVAILABLE, BRAIN_MCP_MESSAGES.SERVER_NOT_RUNNING); + } + + if (!state.brainService.isConnected()) { + throw createMcpError(MCP_ERROR_CODES.BRAIN_UNAVAILABLE, "Brain service not connected"); + } + + const tool = BRAIN_MCP_TOOLS.find((t) => t.name === toolName); + if (!tool) { + throw createMcpError(MCP_ERROR_CODES.TOOL_NOT_FOUND, `Tool not found: ${toolName}`); + } + + let result: unknown; + + const toolHandlers: Record Promise> = { + brain_recall: () => state.brainService!.recall(args.query as string, args.limit as number | undefined), + brain_learn: () => state.brainService!.learn( + args.name as string, + args.whatItDoes as string, + { keywords: args.keywords, patterns: args.patterns, files: args.files } + ), + brain_search: () => state.brainService!.searchMemories( + args.query as string, + args.limit as number | undefined, + args.type as string | undefined + ), + brain_relate: () => state.brainService!.relate( + args.sourceConcept as string, + args.targetConcept as string, + args.relationType as string, + args.weight as number | undefined + ), + brain_context: () => state.brainService!.getContext( + args.query as string, + args.maxConcepts as number | undefined + ), + brain_stats: () => state.brainService!.getStats(), + brain_projects: async () => { + // Import dynamically to avoid circular dependency + const { listProjects } = await import("@src/services/brain/project-service"); + return listProjects(); + }, + }; + + const handler = toolHandlers[toolName]; + if (!handler) { + throw createMcpError(MCP_ERROR_CODES.TOOL_NOT_FOUND, `No handler for tool: ${toolName}`); + } + + result = await handler(); + + return [ + { + type: "text", + text: typeof result === "string" ? result : JSON.stringify(result, null, 2), + }, + ]; +}; + +const handleRequest = async ( + req: IncomingMessage, + res: ServerResponse +): Promise => { + // Set CORS headers + res.setHeader("Access-Control-Allow-Origin", state.config.allowedOrigins.join(",")); + res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", `Content-Type, ${state.config.apiKeyHeader}`); + + // Handle preflight + if (req.method === "OPTIONS") { + res.writeHead(204); + res.end(); + return; + } + + if (req.method !== "POST") { + res.writeHead(405); + res.end(JSON.stringify(createMcpResponse("", undefined, BRAIN_MCP_ERRORS.INVALID_REQUEST))); + return; + } + + // Get client IP for rate limiting + const clientIp = req.socket.remoteAddress || "unknown"; + + // Check rate limit + if (!checkRateLimit(clientIp)) { + res.writeHead(429); + res.end(JSON.stringify(createMcpResponse("", undefined, BRAIN_MCP_ERRORS.RATE_LIMITED))); + return; + } + + // Validate API key + if (!validateApiKey(req)) { + res.writeHead(401); + res.end(JSON.stringify(createMcpResponse("", undefined, BRAIN_MCP_ERRORS.UNAUTHORIZED))); + return; + } + + // Parse request body + let body = ""; + req.on("data", (chunk) => { + body += chunk; + }); + + req.on("end", async () => { + state.requestsServed++; + state.lastRequestAt = Date.now(); + + let mcpRequest: BrainMcpRequest; + + try { + mcpRequest = JSON.parse(body) as BrainMcpRequest; + } catch { + res.writeHead(400); + res.end(JSON.stringify(createMcpResponse("", undefined, BRAIN_MCP_ERRORS.PARSE_ERROR))); + return; + } + + // Handle MCP request + try { + if (mcpRequest.method === "tools/call") { + const { name, arguments: args } = mcpRequest.params; + const content = await handleToolCall(name, args); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(createMcpResponse(mcpRequest.id, content))); + } else if (mcpRequest.method === "tools/list") { + const tools = BRAIN_MCP_TOOLS.map((tool) => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ + id: mcpRequest.id, + result: { tools }, + })); + } else { + res.writeHead(400); + res.end(JSON.stringify(createMcpResponse(mcpRequest.id, undefined, BRAIN_MCP_ERRORS.METHOD_NOT_FOUND))); + } + } catch (error) { + const mcpError = error instanceof Object && "code" in error + ? error as McpError + : createMcpError(MCP_ERROR_CODES.INTERNAL_ERROR, error instanceof Error ? error.message : "Unknown error"); + + res.writeHead(500); + res.end(JSON.stringify(createMcpResponse(mcpRequest.id, undefined, mcpError))); + } + }); +}; + +// Public API + +export const start = async ( + brainService: BrainService, + config?: Partial +): Promise => { + if (state.server) { + throw new Error(BRAIN_MCP_MESSAGES.SERVER_ALREADY_RUNNING); + } + + state.config = { ...DEFAULT_BRAIN_MCP_SERVER_CONFIG, ...config }; + state.brainService = brainService; + + return new Promise((resolve, reject) => { + state.server = createServer(handleRequest); + + state.server.on("error", (error) => { + state.server = null; + reject(error); + }); + + state.server.listen(state.config.port, state.config.host, () => { + state.startTime = Date.now(); + state.requestsServed = 0; + resolve(); + }); + }); +}; + +export const stop = async (): Promise => { + if (!state.server) { + return; + } + + return new Promise((resolve) => { + state.server!.close(() => { + state.server = null; + state.startTime = null; + state.connectedClients = 0; + state.brainService = null; + resolve(); + }); + }); +}; + +export const getStatus = (): BrainMcpServerStatus => ({ + running: state.server !== null, + port: state.config.port, + host: state.config.host, + connectedClients: state.connectedClients, + uptime: state.startTime ? Date.now() - state.startTime : 0, + requestsServed: state.requestsServed, + lastRequestAt: state.lastRequestAt || undefined, +}); + +export const addApiKey = (key: string): void => { + state.apiKeys.add(key); +}; + +export const removeApiKey = (key: string): void => { + state.apiKeys.delete(key); +}; + +export const isRunning = (): boolean => state.server !== null; + +export const getConfig = (): BrainMcpServerConfig => ({ ...state.config }); + +export const updateConfig = (config: Partial): void => { + state.config = { ...state.config, ...config }; +}; + +export const getAvailableTools = (): ReadonlyArray<{ name: string; description: string }> => + BRAIN_MCP_TOOLS.map((t) => ({ name: t.name, description: t.description })); diff --git a/src/services/brain/offline-queue.ts b/src/services/brain/offline-queue.ts new file mode 100644 index 0000000..6dbca88 --- /dev/null +++ b/src/services/brain/offline-queue.ts @@ -0,0 +1,270 @@ +/** + * Offline Queue + * + * Manages queued changes when offline for later synchronization. + */ + +import fs from "fs/promises"; +import { join } from "path"; +import { DIRS } from "@constants/paths"; +import { SYNC_CONFIG, CLOUD_ERRORS } from "@constants/brain-cloud"; +import type { + SyncItem, + OfflineQueueItem, + OfflineQueueState, + SyncOperationType, +} from "@/types/brain-cloud"; + +// Queue file path +const getQueuePath = (): string => join(DIRS.data, "brain-offline-queue.json"); + +// In-memory queue state +let queueState: OfflineQueueState = { + items: [], + totalSize: 0, + oldestItem: null, +}; + +let loaded = false; + +/** + * Load queue from disk + */ +export const loadQueue = async (): Promise => { + if (loaded) return; + + try { + const data = await fs.readFile(getQueuePath(), "utf-8"); + const parsed = JSON.parse(data) as OfflineQueueState; + queueState = parsed; + loaded = true; + } catch { + // File doesn't exist or is invalid, start fresh + queueState = { + items: [], + totalSize: 0, + oldestItem: null, + }; + loaded = true; + } +}; + +/** + * Save queue to disk + */ +const saveQueue = async (): Promise => { + try { + await fs.mkdir(DIRS.data, { recursive: true }); + await fs.writeFile(getQueuePath(), JSON.stringify(queueState, null, 2)); + } catch (error) { + console.error("Failed to save offline queue:", error); + } +}; + +/** + * Add item to offline queue + */ +export const enqueue = async (item: SyncItem): Promise => { + await loadQueue(); + + // Check queue size limit + if (queueState.items.length >= SYNC_CONFIG.MAX_QUEUE_SIZE) { + throw new Error(CLOUD_ERRORS.QUEUE_FULL); + } + + const queueItem: OfflineQueueItem = { + id: generateQueueId(), + item, + retryCount: 0, + lastAttempt: 0, + }; + + queueState.items.push(queueItem); + queueState.totalSize = queueState.items.length; + queueState.oldestItem = Math.min( + queueState.oldestItem ?? item.timestamp, + item.timestamp, + ); + + await saveQueue(); + return true; +}; + +/** + * Add multiple items to queue + */ +export const enqueueBatch = async (items: SyncItem[]): Promise => { + await loadQueue(); + + let added = 0; + for (const item of items) { + if (queueState.items.length >= SYNC_CONFIG.MAX_QUEUE_SIZE) { + break; + } + + const queueItem: OfflineQueueItem = { + id: generateQueueId(), + item, + retryCount: 0, + lastAttempt: 0, + }; + + queueState.items.push(queueItem); + added++; + } + + queueState.totalSize = queueState.items.length; + if (added > 0) { + queueState.oldestItem = Math.min( + queueState.oldestItem ?? Date.now(), + ...items.map((i) => i.timestamp), + ); + } + + await saveQueue(); + return added; +}; + +/** + * Get items from queue for processing + */ +export const dequeue = async (limit: number = SYNC_CONFIG.MAX_BATCH_SIZE): Promise => { + await loadQueue(); + + // Get items that haven't exceeded retry limit + const available = queueState.items.filter( + (item) => item.retryCount < SYNC_CONFIG.MAX_QUEUE_SIZE, + ); + + return available.slice(0, limit); +}; + +/** + * Mark items as processed (remove from queue) + */ +export const markProcessed = async (ids: string[]): Promise => { + await loadQueue(); + + const idSet = new Set(ids); + queueState.items = queueState.items.filter((item) => !idSet.has(item.id)); + queueState.totalSize = queueState.items.length; + + // Update oldest item + if (queueState.items.length > 0) { + queueState.oldestItem = Math.min( + ...queueState.items.map((i) => i.item.timestamp), + ); + } else { + queueState.oldestItem = null; + } + + await saveQueue(); +}; + +/** + * Mark items as failed (increment retry count) + */ +export const markFailed = async ( + ids: string[], + error?: string, +): Promise => { + await loadQueue(); + + const now = Date.now(); + for (const id of ids) { + const item = queueState.items.find((i) => i.id === id); + if (item) { + item.retryCount++; + item.lastAttempt = now; + item.error = error; + } + } + + await saveQueue(); +}; + +/** + * Get queue state + */ +export const getQueueState = async (): Promise => { + await loadQueue(); + return { ...queueState }; +}; + +/** + * Get queue size + */ +export const getQueueSize = async (): Promise => { + await loadQueue(); + return queueState.items.length; +}; + +/** + * Check if queue has items + */ +export const hasQueuedItems = async (): Promise => { + await loadQueue(); + return queueState.items.length > 0; +}; + +/** + * Clear the entire queue + */ +export const clearQueue = async (): Promise => { + queueState = { + items: [], + totalSize: 0, + oldestItem: null, + }; + await saveQueue(); +}; + +/** + * Remove stale items from queue + */ +export const pruneStaleItems = async (): Promise => { + await loadQueue(); + + const cutoff = Date.now() - SYNC_CONFIG.STALE_ITEM_AGE_MS; + const before = queueState.items.length; + + queueState.items = queueState.items.filter( + (item) => item.item.timestamp > cutoff, + ); + + queueState.totalSize = queueState.items.length; + const removed = before - queueState.items.length; + + if (removed > 0) { + await saveQueue(); + } + + return removed; +}; + +/** + * Get items by type + */ +export const getItemsByType = async ( + type: "concept" | "memory" | "relation", +): Promise => { + await loadQueue(); + return queueState.items.filter((item) => item.item.type === type); +}; + +/** + * Get items by operation + */ +export const getItemsByOperation = async ( + operation: SyncOperationType, +): Promise => { + await loadQueue(); + return queueState.items.filter((item) => item.item.operation === operation); +}; + +/** + * Generate unique queue item ID + */ +const generateQueueId = (): string => { + return `q_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +}; diff --git a/src/services/brain/project-service.ts b/src/services/brain/project-service.ts new file mode 100644 index 0000000..f923703 --- /dev/null +++ b/src/services/brain/project-service.ts @@ -0,0 +1,384 @@ +/** + * Brain project service + * Manages multiple Brain projects/knowledge bases + */ + +import { writeFile, readFile, mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; + +import type { + BrainProject, + BrainProjectStats, + BrainProjectSettings, + BrainProjectCreateInput, + BrainProjectUpdateInput, + BrainProjectSwitchResult, + BrainProjectListResult, + BrainProjectExport, + BrainProjectImportResult, + ExportedConcept, + ExportedMemory, + ExportedRelationship, +} from "@src/types/brain-project"; +import { + DEFAULT_BRAIN_PROJECT_SETTINGS, + BRAIN_PROJECT_EXPORT_VERSION, +} from "@src/types/brain-project"; +import { + BRAIN_PROJECT, + BRAIN_PROJECT_STORAGE, + BRAIN_PROJECT_PATHS, + BRAIN_PROJECT_MESSAGES, + BRAIN_PROJECT_API, +} from "@src/constants/brain-project"; + +interface ProjectServiceState { + projects: Map; + activeProjectId: number | null; + configPath: string; + initialized: boolean; +} + +const state: ProjectServiceState = { + projects: new Map(), + activeProjectId: null, + configPath: join(homedir(), ".local", "share", "codetyper", BRAIN_PROJECT_STORAGE.CONFIG_FILE), + initialized: false, +}; + +const ensureDirectories = async (): Promise => { + const paths = [ + join(homedir(), ".local", "share", "codetyper", "brain"), + join(homedir(), ".local", "share", "codetyper", "brain", "exports"), + join(homedir(), ".local", "share", "codetyper", "brain", "backups"), + ]; + + for (const path of paths) { + if (!existsSync(path)) { + await mkdir(path, { recursive: true }); + } + } +}; + +const loadProjectsFromConfig = async (): Promise => { + if (!existsSync(state.configPath)) { + return; + } + + try { + const content = await readFile(state.configPath, "utf-8"); + const data = JSON.parse(content) as { + projects: BrainProject[]; + activeProjectId: number | null; + }; + + state.projects.clear(); + data.projects.forEach((project) => { + state.projects.set(project.id, project); + }); + state.activeProjectId = data.activeProjectId; + } catch { + // Config file corrupted, start fresh + state.projects.clear(); + state.activeProjectId = null; + } +}; + +const saveProjectsToConfig = async (): Promise => { + await ensureDirectories(); + + const data = { + projects: Array.from(state.projects.values()), + activeProjectId: state.activeProjectId, + version: "1.0.0", + updatedAt: Date.now(), + }; + + await writeFile(state.configPath, JSON.stringify(data, null, 2)); +}; + +const generateProjectId = (): number => { + const existingIds = Array.from(state.projects.keys()); + return existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1; +}; + +const createDefaultStats = (): BrainProjectStats => ({ + conceptCount: 0, + memoryCount: 0, + relationshipCount: 0, + totalTokensUsed: 0, +}); + +// Public API + +export const initialize = async (): Promise => { + if (state.initialized) return; + + await ensureDirectories(); + await loadProjectsFromConfig(); + state.initialized = true; +}; + +export const createProject = async (input: BrainProjectCreateInput): Promise => { + await initialize(); + + // Validate name + if (input.name.length < BRAIN_PROJECT.NAME_MIN_LENGTH) { + throw new Error(BRAIN_PROJECT_MESSAGES.INVALID_NAME); + } + + if (input.name.length > BRAIN_PROJECT.NAME_MAX_LENGTH) { + throw new Error(BRAIN_PROJECT_MESSAGES.INVALID_NAME); + } + + // Check for duplicate names + const existingProject = Array.from(state.projects.values()).find( + (p) => p.name.toLowerCase() === input.name.toLowerCase() + ); + + if (existingProject) { + throw new Error(BRAIN_PROJECT_MESSAGES.ALREADY_EXISTS); + } + + const now = Date.now(); + const project: BrainProject = { + id: generateProjectId(), + name: input.name, + description: input.description || "", + rootPath: input.rootPath, + createdAt: now, + updatedAt: now, + stats: createDefaultStats(), + settings: { + ...DEFAULT_BRAIN_PROJECT_SETTINGS, + ...input.settings, + }, + isActive: false, + }; + + state.projects.set(project.id, project); + await saveProjectsToConfig(); + + return project; +}; + +export const updateProject = async ( + projectId: number, + input: BrainProjectUpdateInput +): Promise => { + await initialize(); + + const project = state.projects.get(projectId); + if (!project) { + throw new Error(BRAIN_PROJECT_MESSAGES.NOT_FOUND); + } + + const updatedProject: BrainProject = { + ...project, + name: input.name ?? project.name, + description: input.description ?? project.description, + settings: input.settings + ? { ...project.settings, ...input.settings } + : project.settings, + updatedAt: Date.now(), + }; + + state.projects.set(projectId, updatedProject); + await saveProjectsToConfig(); + + return updatedProject; +}; + +export const deleteProject = async (projectId: number): Promise => { + await initialize(); + + const project = state.projects.get(projectId); + if (!project) { + return false; + } + + // Can't delete active project + if (state.activeProjectId === projectId) { + state.activeProjectId = null; + } + + state.projects.delete(projectId); + await saveProjectsToConfig(); + + return true; +}; + +export const switchProject = async (projectId: number): Promise => { + await initialize(); + + const newProject = state.projects.get(projectId); + if (!newProject) { + throw new Error(BRAIN_PROJECT_MESSAGES.NOT_FOUND); + } + + const previousProject = state.activeProjectId + ? state.projects.get(state.activeProjectId) + : undefined; + + // Update active status + if (previousProject) { + state.projects.set(previousProject.id, { ...previousProject, isActive: false }); + } + + state.projects.set(projectId, { ...newProject, isActive: true }); + state.activeProjectId = projectId; + + await saveProjectsToConfig(); + + return { + success: true, + previousProject, + currentProject: state.projects.get(projectId)!, + message: `${BRAIN_PROJECT_MESSAGES.SWITCHED} "${newProject.name}"`, + }; +}; + +export const getProject = async (projectId: number): Promise => { + await initialize(); + return state.projects.get(projectId); +}; + +export const getActiveProject = async (): Promise => { + await initialize(); + return state.activeProjectId ? state.projects.get(state.activeProjectId) : undefined; +}; + +export const listProjects = async (): Promise => { + await initialize(); + + return { + projects: Array.from(state.projects.values()).sort((a, b) => b.updatedAt - a.updatedAt), + activeProjectId: state.activeProjectId ?? undefined, + total: state.projects.size, + }; +}; + +export const findProjectByPath = async (rootPath: string): Promise => { + await initialize(); + + return Array.from(state.projects.values()).find((p) => p.rootPath === rootPath); +}; + +export const updateProjectStats = async ( + projectId: number, + stats: Partial +): Promise => { + await initialize(); + + const project = state.projects.get(projectId); + if (!project) return; + + const updatedProject: BrainProject = { + ...project, + stats: { ...project.stats, ...stats }, + updatedAt: Date.now(), + }; + + state.projects.set(projectId, updatedProject); + await saveProjectsToConfig(); +}; + +export const exportProject = async (projectId: number): Promise => { + await initialize(); + + const project = state.projects.get(projectId); + if (!project) { + throw new Error(BRAIN_PROJECT_MESSAGES.NOT_FOUND); + } + + // In a real implementation, this would fetch data from Brain API + // For now, return structure with empty data + const exportData: BrainProjectExport = { + project, + concepts: [], + memories: [], + relationships: [], + exportedAt: Date.now(), + version: BRAIN_PROJECT_EXPORT_VERSION, + }; + + // Save export file + const exportPath = join( + homedir(), + ".local", + "share", + "codetyper", + "brain", + "exports", + `${project.name}-${Date.now()}${BRAIN_PROJECT_STORAGE.EXPORT_EXTENSION}` + ); + + await writeFile(exportPath, JSON.stringify(exportData, null, 2)); + + return exportData; +}; + +export const importProject = async ( + exportData: BrainProjectExport +): Promise => { + await initialize(); + + try { + // Create new project with imported data + const newProject = await createProject({ + name: `${exportData.project.name} (imported)`, + description: exportData.project.description, + rootPath: exportData.project.rootPath, + settings: exportData.project.settings, + }); + + // In a real implementation, this would send data to Brain API + // For now, just return success with counts + + return { + success: true, + project: newProject, + imported: { + concepts: exportData.concepts.length, + memories: exportData.memories.length, + relationships: exportData.relationships.length, + }, + errors: [], + }; + } catch (error) { + return { + success: false, + project: exportData.project, + imported: { concepts: 0, memories: 0, relationships: 0 }, + errors: [error instanceof Error ? error.message : "Import failed"], + }; + } +}; + +export const getProjectSettings = async (projectId: number): Promise => { + await initialize(); + + const project = state.projects.get(projectId); + return project?.settings; +}; + +export const updateProjectSettings = async ( + projectId: number, + settings: Partial +): Promise => { + const project = await updateProject(projectId, { settings }); + return project.settings; +}; + +export const setActiveProjectByPath = async (rootPath: string): Promise => { + const project = await findProjectByPath(rootPath); + + if (project) { + await switchProject(project.id); + return project; + } + + return undefined; +}; diff --git a/src/services/chat-tui/initialize.ts b/src/services/chat-tui/initialize.ts index 5bf2ba6..531b0ce 100644 --- a/src/services/chat-tui/initialize.ts +++ b/src/services/chat-tui/initialize.ts @@ -19,6 +19,8 @@ import { buildCompletePrompt, } from "@services/prompt-builder"; import { initSuggestionService } from "@services/command-suggestion-service"; +import * as brainService from "@services/brain"; +import { BRAIN_DISABLED } from "@constants/brain"; import { addContextFile } from "@services/chat-tui/files"; import type { ProviderName, Message } from "@/types/providers"; import type { ChatSession } from "@/types/index"; @@ -147,6 +149,39 @@ const initializeTheme = async (): Promise => { } }; +/** + * Initialize brain service and update store state + * Skipped when BRAIN_DISABLED flag is true + */ +const initializeBrain = async (): Promise => { + // Skip brain initialization when disabled + if (BRAIN_DISABLED) { + appStore.setBrainStatus("disconnected"); + appStore.setBrainShowBanner(false); + return; + } + + try { + appStore.setBrainStatus("connecting"); + + const connected = await brainService.initialize(); + + if (connected) { + const state = brainService.getState(); + appStore.setBrainStatus("connected"); + appStore.setBrainUser(state.user); + appStore.setBrainCounts(state.knowledgeCount, state.memoryCount); + appStore.setBrainShowBanner(false); + } else { + appStore.setBrainStatus("disconnected"); + appStore.setBrainShowBanner(true); + } + } catch { + appStore.setBrainStatus("disconnected"); + appStore.setBrainShowBanner(true); + } +}; + /** * Rebuild system prompt when interaction mode changes * Updates both the state and the first message in the conversation @@ -178,9 +213,13 @@ export const initializeChatService = async ( const initialMode = appStore.getState().interactionMode; const state = await createInitialState(options, initialMode); - await validateProvider(state); - await buildSystemPrompt(state, options); - await initializeTheme(); + // Run provider validation and system prompt building in parallel + // These are independent and both involve async operations + await Promise.all([ + validateProvider(state), + buildSystemPrompt(state, options), + initializeTheme(), + ]); const session = await initializeSession(state, options); @@ -188,9 +227,18 @@ export const initializeChatService = async ( state.messages.push({ role: "system", content: state.systemPrompt }); } - await addInitialContextFiles(state, options.files); - await initializePermissions(); + // Run these in parallel - they're independent + await Promise.all([ + addInitialContextFiles(state, options.files), + initializePermissions(), + ]); + initSuggestionService(process.cwd()); + // Initialize brain service (non-blocking, errors silently handled) + initializeBrain().catch(() => { + // Silently fail - brain is optional + }); + return { state, session }; }; diff --git a/src/services/chat-tui/message-handler.ts b/src/services/chat-tui/message-handler.ts index f9fac1c..fae92e4 100644 --- a/src/services/chat-tui/message-handler.ts +++ b/src/services/chat-tui/message-handler.ts @@ -366,7 +366,7 @@ export const handleMessage = async ( const modeLabel = interactionMode === "ask" ? "Ask" : "Code Review"; callbacks.onLog( "system", - `${modeLabel} mode: Read-only tools only (Ctrl+Tab to switch modes)`, + `${modeLabel} mode: Read-only tools only (Ctrl+M to switch modes)`, ); } diff --git a/src/services/confidence-filter.ts b/src/services/confidence-filter.ts new file mode 100644 index 0000000..6d90ad2 --- /dev/null +++ b/src/services/confidence-filter.ts @@ -0,0 +1,209 @@ +/** + * Confidence-based filtering service + * Filters PR review issues and agent outputs by confidence score + */ + +import type { + ConfidenceScore, + ConfidenceLevel, + ConfidenceFactor, + ConfidenceFilterConfig, + FilteredResult, + ValidationResult, + ConfidenceFilterStats, +} from "@src/types/confidence-filter"; +import { + CONFIDENCE_LEVELS, + DEFAULT_CONFIDENCE_FILTER_CONFIG, +} from "@src/types/confidence-filter"; +import { CONFIDENCE_FILTER, CONFIDENCE_WEIGHTS } from "@src/constants/confidence-filter"; + +export const calculateConfidenceLevel = (score: number): ConfidenceLevel => { + const levels = Object.entries(CONFIDENCE_LEVELS) as Array<[ConfidenceLevel, { min: number; max: number }]>; + const found = levels.find(([, range]) => score >= range.min && score <= range.max); + return found ? found[0] : "low"; +}; + +export const calculateConfidenceScore = (factors: ReadonlyArray): ConfidenceScore => { + const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0); + const weightedSum = factors.reduce((sum, f) => sum + f.score * f.weight, 0); + const value = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0; + + return { + value, + level: calculateConfidenceLevel(value), + factors, + }; +}; + +export const createConfidenceFactor = ( + name: string, + score: number, + weight: number, + reason: string +): ConfidenceFactor => ({ + name, + score: Math.max(0, Math.min(100, score)), + weight: Math.max(0, Math.min(1, weight)), + reason, +}); + +export const createPatternMatchFactor = (matchCount: number, expectedCount: number): ConfidenceFactor => + createConfidenceFactor( + "Pattern Match", + Math.min(100, (matchCount / Math.max(1, expectedCount)) * 100), + CONFIDENCE_WEIGHTS.PATTERN_MATCH, + `Matched ${matchCount}/${expectedCount} expected patterns` + ); + +export const createContextRelevanceFactor = (relevanceScore: number): ConfidenceFactor => + createConfidenceFactor( + "Context Relevance", + relevanceScore, + CONFIDENCE_WEIGHTS.CONTEXT_RELEVANCE, + `Context relevance score: ${relevanceScore}%` + ); + +export const createSeverityFactor = (severity: "low" | "medium" | "high" | "critical"): ConfidenceFactor => { + const severityScores: Record = { low: 40, medium: 60, high: 80, critical: 95 }; + return createConfidenceFactor( + "Severity Level", + severityScores[severity] ?? 50, + CONFIDENCE_WEIGHTS.SEVERITY_LEVEL, + `Issue severity: ${severity}` + ); +}; + +export const createCodeAnalysisFactor = (analysisScore: number): ConfidenceFactor => + createConfidenceFactor( + "Code Analysis", + analysisScore, + CONFIDENCE_WEIGHTS.CODE_ANALYSIS, + `Static analysis confidence: ${analysisScore}%` + ); + +export const createHistoricalAccuracyFactor = (accuracy: number): ConfidenceFactor => + createConfidenceFactor( + "Historical Accuracy", + accuracy, + CONFIDENCE_WEIGHTS.HISTORICAL_ACCURACY, + `Historical accuracy for similar issues: ${accuracy}%` + ); + +export const filterByConfidence = ( + items: ReadonlyArray<{ item: T; confidence: ConfidenceScore }>, + config: ConfidenceFilterConfig = DEFAULT_CONFIDENCE_FILTER_CONFIG +): ReadonlyArray> => + items.map(({ item, confidence }) => ({ + item, + confidence, + passed: confidence.value >= config.minThreshold, + })); + +export const filterPassedOnly = (results: ReadonlyArray>): ReadonlyArray => + results.filter((r) => r.passed).map((r) => r.item); + +export const groupByConfidenceLevel = ( + results: ReadonlyArray> +): Record>> => ({ + low: results.filter((r) => r.confidence.level === "low"), + medium: results.filter((r) => r.confidence.level === "medium"), + high: results.filter((r) => r.confidence.level === "high"), + critical: results.filter((r) => r.confidence.level === "critical"), +}); + +export const calculateFilterStats = (results: ReadonlyArray>): ConfidenceFilterStats => { + const passed = results.filter((r) => r.passed).length; + const grouped = groupByConfidenceLevel(results); + const totalConfidence = results.reduce((sum, r) => sum + r.confidence.value, 0); + + return { + total: results.length, + passed, + filtered: results.length - passed, + byLevel: { + low: grouped.low.length, + medium: grouped.medium.length, + high: grouped.high.length, + critical: grouped.critical.length, + }, + averageConfidence: results.length > 0 ? Math.round(totalConfidence / results.length) : 0, + }; +}; + +export const validateConfidence = async ( + confidence: ConfidenceScore, + validatorFn: (factors: ReadonlyArray) => Promise<{ validated: boolean; adjustment: number; notes: string }> +): Promise => { + const result = await validatorFn(confidence.factors); + + return { + validated: result.validated, + adjustedConfidence: Math.max(0, Math.min(100, confidence.value + result.adjustment)), + validatorNotes: result.notes, + }; +}; + +export const formatConfidenceScore = (confidence: ConfidenceScore, showFactors: boolean = false): string => { + const levelColors: Record = { + low: "\x1b[90m", + medium: "\x1b[33m", + high: "\x1b[32m", + critical: "\x1b[31m", + }; + const reset = "\x1b[0m"; + const color = levelColors[confidence.level]; + + let result = `${color}[${confidence.value}% - ${confidence.level.toUpperCase()}]${reset}`; + + if (showFactors && confidence.factors.length > 0) { + const factorLines = confidence.factors + .map((f) => ` - ${f.name}: ${f.score}% (weight: ${f.weight})`) + .join("\n"); + result += `\n${factorLines}`; + } + + return result; +}; + +export const mergeConfidenceFactors = ( + existing: ReadonlyArray, + additional: ReadonlyArray +): ReadonlyArray => { + const factorMap = new Map(); + + existing.forEach((f) => factorMap.set(f.name, f)); + additional.forEach((f) => { + const existingFactor = factorMap.get(f.name); + if (existingFactor) { + // Average the scores if factor already exists + factorMap.set(f.name, { + ...f, + score: Math.round((existingFactor.score + f.score) / 2), + }); + } else { + factorMap.set(f.name, f); + } + }); + + return Array.from(factorMap.values()); +}; + +export const adjustThreshold = ( + baseThreshold: number, + context: { isCritical: boolean; isAutomated: boolean; userPreference?: number } +): number => { + let threshold = context.userPreference ?? baseThreshold; + + // Lower threshold for critical contexts + if (context.isCritical) { + threshold = Math.max(CONFIDENCE_FILTER.MIN_THRESHOLD, threshold - 10); + } + + // Higher threshold for automated contexts + if (context.isAutomated) { + threshold = Math.min(CONFIDENCE_FILTER.MAX_THRESHOLD, threshold + 10); + } + + return threshold; +}; diff --git a/src/services/config.ts b/src/services/config.ts index 7b65925..3914f4b 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -44,23 +44,41 @@ const PROVIDER_ENV_VARS: Record = { * Config state (singleton pattern using closure) */ let configState: Config = getDefaults(); +let configLoaded = false; +let configLoadPromise: Promise | null = null; /** - * Load configuration from file + * Load configuration from file (with caching) */ export const loadConfig = async (): Promise => { - try { - const data = await fs.readFile(FILES.config, "utf-8"); - const loaded = JSON.parse(data); - - // Clean up deprecated keys - delete loaded.models; - - configState = { ...getDefaults(), ...loaded }; - } catch { - // Config file doesn't exist or is invalid, use defaults - configState = getDefaults(); + // Return cached config if already loaded + if (configLoaded) { + return; } + + // If loading is in progress, wait for it + if (configLoadPromise) { + return configLoadPromise; + } + + // Start loading + configLoadPromise = (async () => { + try { + const data = await fs.readFile(FILES.config, "utf-8"); + const loaded = JSON.parse(data); + + // Clean up deprecated keys + delete loaded.models; + + configState = { ...getDefaults(), ...loaded }; + } catch { + // Config file doesn't exist or is invalid, use defaults + configState = getDefaults(); + } + configLoaded = true; + })(); + + return configLoadPromise; }; /** diff --git a/src/services/feature-dev/checkpoint-handler.ts b/src/services/feature-dev/checkpoint-handler.ts new file mode 100644 index 0000000..d4be102 --- /dev/null +++ b/src/services/feature-dev/checkpoint-handler.ts @@ -0,0 +1,209 @@ +/** + * Checkpoint Handler + * + * Manages user approval checkpoints during feature development. + */ + +import { + PHASE_CHECKPOINTS, + FEATURE_DEV_ERRORS, +} from "@constants/feature-dev"; +import type { + FeatureDevPhase, + FeatureDevState, + Checkpoint, + CheckpointDecision, + PhaseExecutionContext, +} from "@/types/feature-dev"; + +/** + * Create a checkpoint for user approval + */ +export const createCheckpoint = ( + phase: FeatureDevPhase, + state: FeatureDevState, + details: string[], +): Checkpoint => { + const config = PHASE_CHECKPOINTS[phase]; + + return { + phase, + title: config.title, + summary: buildCheckpointSummary(phase, state), + details, + requiresApproval: config.required, + suggestedAction: "approve", + }; +}; + +/** + * Build summary for checkpoint based on phase + */ +const buildCheckpointSummary = ( + phase: FeatureDevPhase, + state: FeatureDevState, +): string => { + const summaryBuilders: Record string> = { + understand: () => { + const reqCount = state.requirements.length; + const clarCount = state.clarifications.length; + return `${reqCount} requirement(s) identified, ${clarCount} clarification(s) made`; + }, + + explore: () => { + const fileCount = state.relevantFiles.length; + const findingCount = state.explorationResults.reduce( + (sum, r) => sum + r.findings.length, + 0, + ); + return `Found ${fileCount} relevant file(s) with ${findingCount} finding(s)`; + }, + + plan: () => { + if (!state.plan) return "No plan created"; + const stepCount = state.plan.steps.length; + const complexity = state.plan.estimatedComplexity; + return `${stepCount} step(s) planned, ${complexity} complexity`; + }, + + implement: () => { + const changeCount = state.changes.length; + const additions = state.changes.reduce((sum, c) => sum + c.additions, 0); + const deletions = state.changes.reduce((sum, c) => sum + c.deletions, 0); + return `${changeCount} file(s) changed (+${additions}/-${deletions})`; + }, + + verify: () => { + if (!state.testResults) return "Tests not run yet"; + const { passedTests, failedTests, totalTests } = state.testResults; + return `${passedTests}/${totalTests} tests passed, ${failedTests} failed`; + }, + + review: () => { + const issues = state.reviewFindings.filter((f) => f.type === "issue").length; + const suggestions = state.reviewFindings.filter( + (f) => f.type === "suggestion", + ).length; + return `${issues} issue(s), ${suggestions} suggestion(s) found`; + }, + + finalize: () => { + const changeCount = state.changes.length; + return `Ready to commit ${changeCount} file change(s)`; + }, + }; + + return summaryBuilders[phase](); +}; + +/** + * Check if phase requires a checkpoint + */ +export const requiresCheckpoint = (phase: FeatureDevPhase): boolean => { + return PHASE_CHECKPOINTS[phase].required; +}; + +/** + * Request user approval at a checkpoint + */ +export const requestApproval = async ( + checkpoint: Checkpoint, + ctx: PhaseExecutionContext, +): Promise<{ decision: CheckpointDecision; feedback?: string }> => { + // If no checkpoint handler provided, auto-approve non-required checkpoints + if (!ctx.onCheckpoint) { + if (checkpoint.requiresApproval) { + throw new Error(FEATURE_DEV_ERRORS.CHECKPOINT_REQUIRED(checkpoint.phase)); + } + return { decision: "approve" }; + } + + // Request approval from handler + const result = await ctx.onCheckpoint(checkpoint); + + // Record checkpoint in state + ctx.state.checkpoints.push({ + checkpoint, + decision: result.decision, + feedback: result.feedback, + timestamp: Date.now(), + }); + + return result; +}; + +/** + * Process checkpoint decision + */ +export const processCheckpointDecision = ( + decision: CheckpointDecision, + _feedback?: string, +): { proceed: boolean; action?: string } => { + const decisionHandlers: Record< + CheckpointDecision, + () => { proceed: boolean; action?: string } + > = { + approve: () => ({ proceed: true }), + reject: () => ({ proceed: false, action: "rejected" }), + modify: () => ({ proceed: false, action: "modify", }), + skip: () => ({ proceed: true, action: "skipped" }), + abort: () => ({ proceed: false, action: "aborted" }), + }; + + return decisionHandlers[decision](); +}; + +/** + * Format checkpoint for display + */ +export const formatCheckpoint = (checkpoint: Checkpoint): string => { + const lines: string[] = []; + + lines.push(`## ${checkpoint.title}`); + lines.push(""); + lines.push(`**Phase:** ${checkpoint.phase}`); + lines.push(`**Summary:** ${checkpoint.summary}`); + lines.push(""); + + if (checkpoint.details.length > 0) { + lines.push("### Details"); + for (const detail of checkpoint.details) { + lines.push(`- ${detail}`); + } + lines.push(""); + } + + if (checkpoint.requiresApproval) { + lines.push("*This checkpoint requires your approval to proceed.*"); + } + + return lines.join("\n"); +}; + +/** + * Get checkpoint history for a phase + */ +export const getPhaseCheckpoints = ( + state: FeatureDevState, + phase: FeatureDevPhase, +): Array<{ + checkpoint: Checkpoint; + decision: CheckpointDecision; + feedback?: string; + timestamp: number; +}> => { + return state.checkpoints.filter((c) => c.checkpoint.phase === phase); +}; + +/** + * Check if phase was approved + */ +export const wasPhaseApproved = ( + state: FeatureDevState, + phase: FeatureDevPhase, +): boolean => { + const checkpoints = getPhaseCheckpoints(state, phase); + return checkpoints.some( + (c) => c.decision === "approve" || c.decision === "skip", + ); +}; diff --git a/src/services/feature-dev/context-builder.ts b/src/services/feature-dev/context-builder.ts new file mode 100644 index 0000000..5eaf4a0 --- /dev/null +++ b/src/services/feature-dev/context-builder.ts @@ -0,0 +1,292 @@ +/** + * Context Builder + * + * Builds context for each phase of feature development. + */ + +import { + PHASE_PROMPTS, + PHASE_DESCRIPTIONS, +} from "@constants/feature-dev"; +import type { + FeatureDevPhase, + FeatureDevState, +} from "@/types/feature-dev"; + +/** + * Build the full context for a phase execution + */ +export const buildPhaseContext = ( + phase: FeatureDevPhase, + state: FeatureDevState, + userRequest: string, +): string => { + const parts: string[] = []; + + // Phase header + parts.push(`# Feature Development: ${phase.toUpperCase()} Phase`); + parts.push(""); + parts.push(`**Goal:** ${PHASE_DESCRIPTIONS[phase]}`); + parts.push(""); + + // Phase-specific prompt + parts.push("## Instructions"); + parts.push(PHASE_PROMPTS[phase]); + parts.push(""); + + // User's original request + parts.push("## Feature Request"); + parts.push(userRequest); + parts.push(""); + + // Add state context based on phase + const stateContext = buildStateContext(phase, state); + if (stateContext) { + parts.push("## Current State"); + parts.push(stateContext); + parts.push(""); + } + + return parts.join("\n"); +}; + +/** + * Build state context based on accumulated results + */ +const buildStateContext = ( + phase: FeatureDevPhase, + state: FeatureDevState, +): string | null => { + const contextBuilders: Record string | null> = { + understand: () => null, // No prior context + + explore: () => { + if (state.requirements.length === 0) return null; + + const lines: string[] = []; + lines.push("### Understood Requirements"); + for (const req of state.requirements) { + lines.push(`- ${req}`); + } + + if (state.clarifications.length > 0) { + lines.push(""); + lines.push("### Clarifications"); + for (const c of state.clarifications) { + lines.push(`Q: ${c.question}`); + lines.push(`A: ${c.answer}`); + } + } + + return lines.join("\n"); + }, + + plan: () => { + const lines: string[] = []; + + // Requirements + if (state.requirements.length > 0) { + lines.push("### Requirements"); + for (const req of state.requirements) { + lines.push(`- ${req}`); + } + lines.push(""); + } + + // Exploration results + if (state.relevantFiles.length > 0) { + lines.push("### Relevant Files Found"); + for (const file of state.relevantFiles.slice(0, 10)) { + lines.push(`- ${file}`); + } + if (state.relevantFiles.length > 10) { + lines.push(`- ... and ${state.relevantFiles.length - 10} more`); + } + lines.push(""); + } + + // Patterns found + const patterns = state.explorationResults.flatMap((r) => r.patterns); + if (patterns.length > 0) { + lines.push("### Patterns to Follow"); + for (const pattern of [...new Set(patterns)].slice(0, 5)) { + lines.push(`- ${pattern}`); + } + lines.push(""); + } + + return lines.length > 0 ? lines.join("\n") : null; + }, + + implement: () => { + if (!state.plan) return null; + + const lines: string[] = []; + lines.push("### Approved Implementation Plan"); + lines.push(`**Summary:** ${state.plan.summary}`); + lines.push(""); + lines.push("**Steps:**"); + for (const step of state.plan.steps) { + lines.push(`${step.order}. [${step.changeType}] ${step.file}`); + lines.push(` ${step.description}`); + } + + if (state.plan.risks.length > 0) { + lines.push(""); + lines.push("**Risks to Watch:**"); + for (const risk of state.plan.risks) { + lines.push(`- ${risk}`); + } + } + + return lines.join("\n"); + }, + + verify: () => { + if (state.changes.length === 0) return null; + + const lines: string[] = []; + lines.push("### Files Changed"); + for (const change of state.changes) { + lines.push( + `- ${change.path} (${change.changeType}, +${change.additions}/-${change.deletions})`, + ); + } + + if (state.plan?.testStrategy) { + lines.push(""); + lines.push("### Test Strategy"); + lines.push(state.plan.testStrategy); + } + + return lines.join("\n"); + }, + + review: () => { + const lines: string[] = []; + + // Changes to review + if (state.changes.length > 0) { + lines.push("### Changes to Review"); + for (const change of state.changes) { + lines.push( + `- ${change.path} (${change.changeType}, +${change.additions}/-${change.deletions})`, + ); + } + lines.push(""); + } + + // Test results + if (state.testResults) { + lines.push("### Test Results"); + lines.push( + `${state.testResults.passedTests}/${state.testResults.totalTests} tests passed`, + ); + if (state.testResults.failedTests > 0) { + lines.push("**Failures:**"); + for (const failure of state.testResults.failures) { + lines.push(`- ${failure.testName}: ${failure.error}`); + } + } + lines.push(""); + } + + return lines.length > 0 ? lines.join("\n") : null; + }, + + finalize: () => { + const lines: string[] = []; + + // Summary of changes + lines.push("### Summary of Changes"); + for (const change of state.changes) { + lines.push( + `- ${change.path} (${change.changeType}, +${change.additions}/-${change.deletions})`, + ); + } + lines.push(""); + + // Review findings to address + const issues = state.reviewFindings.filter( + (f) => f.type === "issue" && f.severity === "critical", + ); + if (issues.length > 0) { + lines.push("### Outstanding Issues"); + for (const issue of issues) { + lines.push(`- [${issue.severity}] ${issue.message}`); + } + lines.push(""); + } + + // Test status + if (state.testResults) { + const status = state.testResults.passed ? "✓ All tests passing" : "✗ Tests failing"; + lines.push(`### Test Status: ${status}`); + } + + return lines.join("\n"); + }, + }; + + return contextBuilders[phase](); +}; + +/** + * Build summary of current workflow state + */ +export const buildWorkflowSummary = (state: FeatureDevState): string => { + const lines: string[] = []; + + lines.push("# Feature Development Progress"); + lines.push(""); + lines.push(`**Current Phase:** ${state.phase}`); + lines.push(`**Status:** ${state.phaseStatus}`); + lines.push(""); + + // Phase completion status + const phases: FeatureDevPhase[] = [ + "understand", + "explore", + "plan", + "implement", + "verify", + "review", + "finalize", + ]; + + const currentIndex = phases.indexOf(state.phase); + + lines.push("## Progress"); + for (let i = 0; i < phases.length; i++) { + const phase = phases[i]; + const status = + i < currentIndex + ? "✓" + : i === currentIndex + ? state.phaseStatus === "completed" + ? "✓" + : "→" + : "○"; + lines.push(`${status} ${phase}`); + } + + return lines.join("\n"); +}; + +/** + * Extract key information from state for quick reference + */ +export const extractKeyInfo = ( + state: FeatureDevState, +): Record => { + return { + phase: state.phase, + status: state.phaseStatus, + requirementsCount: state.requirements.length, + relevantFilesCount: state.relevantFiles.length, + changesCount: state.changes.length, + reviewFindingsCount: state.reviewFindings.length, + checkpointsCount: state.checkpoints.length, + duration: Date.now() - state.startedAt, + }; +}; diff --git a/src/services/feature-dev/index.ts b/src/services/feature-dev/index.ts new file mode 100644 index 0000000..b36717e --- /dev/null +++ b/src/services/feature-dev/index.ts @@ -0,0 +1,290 @@ +/** + * Feature-Dev Workflow Service + * + * Main orchestrator for the 7-phase feature development workflow. + */ + +import { PHASE_ORDER, FEATURE_DEV_CONFIG, FEATURE_DEV_ERRORS } from "@constants/feature-dev"; +import { + executePhase, + validateTransition, +} from "@services/feature-dev/phase-executor"; +import { buildWorkflowSummary, extractKeyInfo } from "@services/feature-dev/context-builder"; +import type { + FeatureDevPhase, + FeatureDevState, + PhaseExecutionContext, + Checkpoint, + CheckpointDecision, +} from "@/types/feature-dev"; + +// Re-export sub-modules +export * from "@services/feature-dev/phase-executor"; +export * from "@services/feature-dev/checkpoint-handler"; +export * from "@services/feature-dev/context-builder"; + +// Active workflows storage +const activeWorkflows = new Map(); + +/** + * Create a new feature development workflow + */ +export const createWorkflow = ( + id: string, + requirements: string[] = [], +): FeatureDevState => { + const state: FeatureDevState = { + id, + phase: "understand", + phaseStatus: "pending", + startedAt: Date.now(), + updatedAt: Date.now(), + requirements, + clarifications: [], + explorationResults: [], + relevantFiles: [], + changes: [], + reviewFindings: [], + checkpoints: [], + }; + + activeWorkflows.set(id, state); + return state; +}; + +/** + * Get an active workflow by ID + */ +export const getWorkflow = (id: string): FeatureDevState | undefined => { + return activeWorkflows.get(id); +}; + +/** + * Update workflow state + */ +export const updateWorkflow = ( + id: string, + updates: Partial, +): FeatureDevState | undefined => { + const workflow = activeWorkflows.get(id); + if (!workflow) return undefined; + + const updated = { + ...workflow, + ...updates, + updatedAt: Date.now(), + }; + + activeWorkflows.set(id, updated); + return updated; +}; + +/** + * Delete a workflow + */ +export const deleteWorkflow = (id: string): boolean => { + return activeWorkflows.delete(id); +}; + +/** + * Run the complete feature development workflow + */ +export const runWorkflow = async ( + workflowId: string, + userRequest: string, + options: { + config?: Partial; + workingDir: string; + sessionId: string; + abortSignal?: AbortSignal; + onProgress?: (message: string) => void; + onCheckpoint?: (checkpoint: Checkpoint) => Promise<{ + decision: CheckpointDecision; + feedback?: string; + }>; + }, +): Promise<{ + success: boolean; + finalState: FeatureDevState; + error?: string; +}> => { + // Merge config with defaults (kept for future extensibility) + void { ...FEATURE_DEV_CONFIG, ...options.config }; + + // Get or create workflow + let state = getWorkflow(workflowId); + if (!state) { + state = createWorkflow(workflowId); + } + + // Build execution context + const ctx: PhaseExecutionContext = { + state, + workingDir: options.workingDir, + sessionId: options.sessionId, + abortSignal: options.abortSignal, + onProgress: options.onProgress, + onCheckpoint: options.onCheckpoint, + }; + + // Execute phases in order + while (state.phase !== "finalize" || state.phaseStatus !== "completed") { + // Check for abort + if (options.abortSignal?.aborted) { + state.abortReason = "Workflow aborted by user"; + state.phaseStatus = "failed"; + return { + success: false, + finalState: state, + error: FEATURE_DEV_ERRORS.WORKFLOW_ABORTED(state.abortReason), + }; + } + + // Execute current phase + const result = await executePhase(state.phase, ctx, userRequest); + + // Apply state updates + if (result.stateUpdates) { + state = updateWorkflow(workflowId, result.stateUpdates) ?? state; + ctx.state = state; + } + + // Handle phase result + if (!result.success) { + if (state.abortReason) { + // Workflow was aborted + return { + success: false, + finalState: state, + error: result.error, + }; + } + // Phase needs attention (rejected, needs modification, etc.) + // Stay in current phase and let caller handle + continue; + } + + // Move to next phase + if (result.nextPhase) { + const transition = validateTransition({ + fromPhase: state.phase, + toPhase: result.nextPhase, + }); + + if (!transition.valid) { + return { + success: false, + finalState: state, + error: transition.error, + }; + } + + state = updateWorkflow(workflowId, { + phase: result.nextPhase, + phaseStatus: "pending", + }) ?? state; + ctx.state = state; + } else { + // No next phase, workflow complete + break; + } + } + + return { + success: true, + finalState: state, + }; +}; + +/** + * Get workflow progress summary + */ +export const getWorkflowProgress = ( + workflowId: string, +): { summary: string; keyInfo: Record } | undefined => { + const workflow = getWorkflow(workflowId); + if (!workflow) return undefined; + + return { + summary: buildWorkflowSummary(workflow), + keyInfo: extractKeyInfo(workflow), + }; +}; + +/** + * Abort an active workflow + */ +export const abortWorkflow = ( + workflowId: string, + reason: string, +): FeatureDevState | undefined => { + return updateWorkflow(workflowId, { + phaseStatus: "failed", + abortReason: reason, + }); +}; + +/** + * Reset workflow to a specific phase + */ +export const resetToPhase = ( + workflowId: string, + phase: FeatureDevPhase, +): FeatureDevState | undefined => { + const workflow = getWorkflow(workflowId); + if (!workflow) return undefined; + + // Clear state accumulated after this phase + const phaseIndex = PHASE_ORDER.indexOf(phase); + const updates: Partial = { + phase, + phaseStatus: "pending", + }; + + // Clear phase-specific data based on which phase we're resetting to + if (phaseIndex <= PHASE_ORDER.indexOf("explore")) { + updates.explorationResults = []; + updates.relevantFiles = []; + } + if (phaseIndex <= PHASE_ORDER.indexOf("plan")) { + updates.plan = undefined; + } + if (phaseIndex <= PHASE_ORDER.indexOf("implement")) { + updates.changes = []; + } + if (phaseIndex <= PHASE_ORDER.indexOf("verify")) { + updates.testResults = undefined; + } + if (phaseIndex <= PHASE_ORDER.indexOf("review")) { + updates.reviewFindings = []; + } + if (phaseIndex <= PHASE_ORDER.indexOf("finalize")) { + updates.commitHash = undefined; + } + + return updateWorkflow(workflowId, updates); +}; + +/** + * List all active workflows + */ +export const listWorkflows = (): Array<{ + id: string; + phase: FeatureDevPhase; + status: string; + startedAt: number; +}> => { + return Array.from(activeWorkflows.values()).map((w) => ({ + id: w.id, + phase: w.phase, + status: w.phaseStatus, + startedAt: w.startedAt, + })); +}; + +/** + * Create workflow ID + */ +export const createWorkflowId = (): string => { + return `fd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +}; diff --git a/src/services/feature-dev/phase-executor.ts b/src/services/feature-dev/phase-executor.ts new file mode 100644 index 0000000..d086abf --- /dev/null +++ b/src/services/feature-dev/phase-executor.ts @@ -0,0 +1,345 @@ +/** + * Phase Executor + * + * Executes individual phases of the feature development workflow. + */ + +import { + PHASE_ORDER, + ALLOWED_TRANSITIONS, + PHASE_TIMEOUTS, + FEATURE_DEV_ERRORS, + FEATURE_DEV_MESSAGES, +} from "@constants/feature-dev"; +import { + createCheckpoint, + requiresCheckpoint, + requestApproval, + processCheckpointDecision, +} from "@services/feature-dev/checkpoint-handler"; +import { buildPhaseContext } from "@services/feature-dev/context-builder"; +import type { + FeatureDevPhase, + PhaseExecutionContext, + PhaseExecutionResult, + PhaseTransitionRequest, +} from "@/types/feature-dev"; + +/** + * Execute a single phase + */ +export const executePhase = async ( + phase: FeatureDevPhase, + ctx: PhaseExecutionContext, + userRequest: string, +): Promise => { + // Update state to in_progress + ctx.state.phase = phase; + ctx.state.phaseStatus = "in_progress"; + ctx.state.updatedAt = Date.now(); + + ctx.onProgress?.(FEATURE_DEV_MESSAGES.STARTING(phase)); + + try { + // Execute phase-specific logic + const result = await executePhaseLogic(phase, ctx, userRequest); + + // Handle checkpoint if needed + if (requiresCheckpoint(phase) || result.checkpoint) { + const checkpoint = + result.checkpoint ?? createCheckpoint(phase, ctx.state, []); + + ctx.state.phaseStatus = "awaiting_approval"; + + const { decision, feedback } = await requestApproval(checkpoint, ctx); + const { proceed, action } = processCheckpointDecision(decision, feedback); + + if (!proceed) { + if (action === "aborted") { + ctx.state.abortReason = feedback ?? "User aborted"; + return { + success: false, + phase, + error: FEATURE_DEV_ERRORS.WORKFLOW_ABORTED(ctx.state.abortReason), + stateUpdates: { phaseStatus: "failed" }, + }; + } + + // Rejected or modify - stay in current phase + return { + success: false, + phase, + stateUpdates: { phaseStatus: "pending" }, + }; + } + + ctx.state.phaseStatus = "approved"; + } + + // Phase completed successfully + ctx.state.phaseStatus = "completed"; + ctx.state.updatedAt = Date.now(); + + ctx.onProgress?.(FEATURE_DEV_MESSAGES.COMPLETED(phase)); + + return { + success: true, + phase, + nextPhase: getNextPhase(phase), + stateUpdates: { phaseStatus: "completed", ...result.stateUpdates }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + ctx.state.phaseStatus = "failed"; + + return { + success: false, + phase, + error: FEATURE_DEV_ERRORS.PHASE_FAILED(phase, message), + stateUpdates: { phaseStatus: "failed" }, + }; + } +}; + +/** + * Execute phase-specific logic + */ +const executePhaseLogic = async ( + phase: FeatureDevPhase, + ctx: PhaseExecutionContext, + userRequest: string, +): Promise> => { + // Build context for this phase + const phaseContext = buildPhaseContext(phase, ctx.state, userRequest); + + // Phase-specific execution + const phaseExecutors: Record< + FeatureDevPhase, + () => Promise> + > = { + understand: async () => executeUnderstandPhase(ctx, phaseContext), + explore: async () => executeExplorePhase(ctx, phaseContext), + plan: async () => executePlanPhase(ctx, phaseContext), + implement: async () => executeImplementPhase(ctx, phaseContext), + verify: async () => executeVerifyPhase(ctx, phaseContext), + review: async () => executeReviewPhase(ctx, phaseContext), + finalize: async () => executeFinalizePhase(ctx, phaseContext), + }; + + return phaseExecutors[phase](); +}; + +/** + * Understand phase execution + */ +const executeUnderstandPhase = async ( + ctx: PhaseExecutionContext, + _phaseContext: string, +): Promise> => { + // This phase would typically involve LLM interaction to: + // 1. Parse the user's request + // 2. Identify requirements + // 3. Ask clarifying questions + // For now, return a checkpoint for user confirmation + + const checkpoint = createCheckpoint("understand", ctx.state, [ + "Review the identified requirements", + "Provide any clarifications needed", + "Confirm understanding is correct", + ]); + + return { + checkpoint, + stateUpdates: {}, + }; +}; + +/** + * Explore phase execution + */ +const executeExplorePhase = async ( + ctx: PhaseExecutionContext, + _phaseContext: string, +): Promise> => { + ctx.onProgress?.(FEATURE_DEV_MESSAGES.EXPLORING("relevant code patterns")); + + // This phase would use parallel agents to search the codebase + // For now, return a basic result + + return { + stateUpdates: {}, + }; +}; + +/** + * Plan phase execution + */ +const executePlanPhase = async ( + ctx: PhaseExecutionContext, + _phaseContext: string, +): Promise> => { + // This phase would involve LLM to create implementation plan + // The plan must be approved before proceeding + + const checkpoint = createCheckpoint("plan", ctx.state, [ + "Review the implementation plan", + "Check the proposed file changes", + "Verify the approach is correct", + "Consider the identified risks", + ]); + + return { + checkpoint, + stateUpdates: {}, + }; +}; + +/** + * Implement phase execution + */ +const executeImplementPhase = async ( + ctx: PhaseExecutionContext, + _phaseContext: string, +): Promise> => { + // Verify we have a plan + if (!ctx.state.plan) { + throw new Error(FEATURE_DEV_ERRORS.NO_PLAN); + } + + // This phase would execute each step in the plan + const totalSteps = ctx.state.plan.steps.length; + + for (let i = 0; i < totalSteps; i++) { + ctx.onProgress?.(FEATURE_DEV_MESSAGES.IMPLEMENTING_STEP(i + 1, totalSteps)); + // Step execution would happen here + } + + return { + stateUpdates: {}, + }; +}; + +/** + * Verify phase execution + */ +const executeVerifyPhase = async ( + ctx: PhaseExecutionContext, + _phaseContext: string, +): Promise> => { + ctx.onProgress?.(FEATURE_DEV_MESSAGES.RUNNING_TESTS); + + // This phase would run the test suite + // For now, create a checkpoint for test review + + const checkpoint = createCheckpoint("verify", ctx.state, [ + "Review test results", + "Check for any failures", + "Verify coverage is adequate", + ]); + + return { + checkpoint, + stateUpdates: {}, + }; +}; + +/** + * Review phase execution + */ +const executeReviewPhase = async ( + ctx: PhaseExecutionContext, + _phaseContext: string, +): Promise> => { + ctx.onProgress?.(FEATURE_DEV_MESSAGES.REVIEWING); + + // This phase would perform self-review of changes + const checkpoint = createCheckpoint("review", ctx.state, [ + "Review code quality findings", + "Address any critical issues", + "Confirm changes are ready", + ]); + + return { + checkpoint, + stateUpdates: {}, + }; +}; + +/** + * Finalize phase execution + */ +const executeFinalizePhase = async ( + ctx: PhaseExecutionContext, + _phaseContext: string, +): Promise> => { + ctx.onProgress?.(FEATURE_DEV_MESSAGES.FINALIZING); + + // This phase would create the commit + const checkpoint = createCheckpoint("finalize", ctx.state, [ + "Confirm commit message", + "Verify all changes are included", + "Approve final commit", + ]); + + return { + checkpoint, + stateUpdates: {}, + }; +}; + +/** + * Get the next phase in the workflow + */ +export const getNextPhase = ( + currentPhase: FeatureDevPhase, +): FeatureDevPhase | undefined => { + const currentIndex = PHASE_ORDER.indexOf(currentPhase); + if (currentIndex === -1 || currentIndex >= PHASE_ORDER.length - 1) { + return undefined; + } + return PHASE_ORDER[currentIndex + 1]; +}; + +/** + * Get the previous phase in the workflow + */ +export const getPreviousPhase = ( + currentPhase: FeatureDevPhase, +): FeatureDevPhase | undefined => { + const currentIndex = PHASE_ORDER.indexOf(currentPhase); + if (currentIndex <= 0) { + return undefined; + } + return PHASE_ORDER[currentIndex - 1]; +}; + +/** + * Validate a phase transition + */ +export const validateTransition = ( + request: PhaseTransitionRequest, +): { valid: boolean; error?: string } => { + if (request.skipValidation) { + return { valid: true }; + } + + const allowed = ALLOWED_TRANSITIONS[request.fromPhase]; + if (!allowed.includes(request.toPhase)) { + return { + valid: false, + error: FEATURE_DEV_ERRORS.INVALID_TRANSITION( + request.fromPhase, + request.toPhase, + ), + }; + } + + return { valid: true }; +}; + +/** + * Get timeout for a phase + */ +export const getPhaseTimeout = (phase: FeatureDevPhase): number => { + return PHASE_TIMEOUTS[phase]; +}; diff --git a/src/services/index.ts b/src/services/index.ts index a77540c..a6098cc 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -8,3 +8,4 @@ export * from "@services/github-issue-service"; export * from "@services/command-suggestion-service"; export * from "@services/learning-service"; export * from "@services/rules-service"; +export * as brainService from "@services/brain"; diff --git a/src/services/model-routing.ts b/src/services/model-routing.ts new file mode 100644 index 0000000..085e054 --- /dev/null +++ b/src/services/model-routing.ts @@ -0,0 +1,225 @@ +/** + * Model Routing Service + * + * Maps agent tiers to appropriate models based on task complexity. + * Following Claude Code's multi-model strategy: + * - fast: Quick screening, filtering (like Haiku) + * - balanced: Detailed analysis, general tasks (like Sonnet) + * - thorough: Complex reasoning, bug hunting (like Opus) + */ + +import { getModelContextSize } from "@constants/copilot"; +import type { AgentConfig } from "@/types/agent-config"; + +/** + * Model tier for routing decisions + */ +export type ModelTier = "fast" | "balanced" | "thorough"; + +/** + * Model tier mapping to Copilot models + * These are the default mappings - can be overridden by agent config + */ +export const MODEL_TIER_MAPPING: Record = { + // Fast tier: Low cost, quick responses (0x or 0.33x multiplier) + fast: [ + "gpt-5-mini", + "gpt-4o-mini", + "claude-haiku-4.5", + "gemini-3-flash-preview", + "grok-code-fast-1", + ], + // Balanced tier: Good quality, moderate cost (1x multiplier) + balanced: [ + "claude-sonnet-4.5", + "claude-sonnet-4", + "gpt-5", + "gpt-5.1", + "gemini-2.5-pro", + "gpt-4.1", + ], + // Thorough tier: Best quality, higher cost (3x multiplier) + thorough: [ + "claude-opus-4.5", + "gpt-5.2-codex", + "gpt-5.1-codex-max", + ], +}; + +/** + * Tier aliases for agent frontmatter + */ +const TIER_ALIASES: Record = { + haiku: "fast", + fast: "fast", + quick: "fast", + sonnet: "balanced", + balanced: "balanced", + default: "balanced", + opus: "thorough", + thorough: "thorough", + deep: "thorough", +}; + +/** + * Agent type to default tier mapping + */ +const AGENT_TYPE_TIERS: Record = { + explorer: "fast", + explore: "fast", + filter: "fast", + screen: "fast", + architect: "balanced", + planner: "balanced", + plan: "balanced", + coder: "balanced", + general: "balanced", + reviewer: "balanced", + review: "balanced", + "code-reviewer": "balanced", + "bug-hunter": "thorough", + bugs: "thorough", + security: "thorough", + compaction: "fast", + summary: "fast", + title: "fast", +}; + +/** + * Resolve model tier from string (tier name or model ID) + */ +export const resolveTier = (modelOrTier: string): ModelTier | null => { + const lower = modelOrTier.toLowerCase(); + + // Check if it's a tier alias + if (lower in TIER_ALIASES) { + return TIER_ALIASES[lower]; + } + + // Check if it's already a model ID in one of the tiers + for (const [tier, models] of Object.entries(MODEL_TIER_MAPPING)) { + if (models.some((m) => m.toLowerCase() === lower)) { + return tier as ModelTier; + } + } + + return null; +}; + +/** + * Get the best available model for a tier + * Returns the first model in the tier's list (assumed to be preference order) + */ +export const getModelForTier = ( + tier: ModelTier, + availableModels?: string[], +): string => { + const tierModels = MODEL_TIER_MAPPING[tier]; + + if (availableModels && availableModels.length > 0) { + // Find first available model from tier + for (const model of tierModels) { + if (availableModels.includes(model)) { + return model; + } + } + // Fallback to first tier model if none available + return tierModels[0]; + } + + return tierModels[0]; +}; + +/** + * Infer tier from agent type/name + */ +export const inferTierFromAgent = (agent: AgentConfig): ModelTier => { + const idLower = agent.id.toLowerCase(); + const nameLower = agent.name.toLowerCase(); + + // Check agent type mapping + for (const [type, tier] of Object.entries(AGENT_TYPE_TIERS)) { + if (idLower.includes(type) || nameLower.includes(type)) { + return tier; + } + } + + // Default to balanced + return "balanced"; +}; + +/** + * Resolve the model to use for an agent + * + * Priority: + * 1. Explicit model in agent config (full model ID) + * 2. Tier specified in agent config (fast/balanced/thorough) + * 3. Inferred from agent type/name + * 4. Default model passed in + */ +export const resolveAgentModel = ( + agent: AgentConfig, + defaultModel: string, + availableModels?: string[], +): { model: string; tier: ModelTier; source: string } => { + // 1. Check explicit model in agent config + if (agent.model) { + // Check if it's a tier name + const tier = resolveTier(agent.model); + if (tier) { + const model = getModelForTier(tier, availableModels); + return { model, tier, source: "agent-tier" }; + } + + // Otherwise use as model ID + return { + model: agent.model, + tier: resolveTier(agent.model) ?? "balanced", + source: "agent-model", + }; + } + + // 2. Infer from agent type + const inferredTier = inferTierFromAgent(agent); + if (inferredTier !== "balanced") { + const model = getModelForTier(inferredTier, availableModels); + return { model, tier: inferredTier, source: "agent-inferred" }; + } + + // 3. Use default + const defaultTier = resolveTier(defaultModel) ?? "balanced"; + return { model: defaultModel, tier: defaultTier, source: "default" }; +}; + +/** + * Get model context size for routing decisions + */ +export const getRouteContextSize = (modelId: string): number => { + return getModelContextSize(modelId).input; +}; + +/** + * Model routing decision + */ +export interface ModelRoutingDecision { + model: string; + tier: ModelTier; + source: string; + contextSize: number; +} + +/** + * Make routing decision for an agent + */ +export const routeAgent = ( + agent: AgentConfig, + defaultModel: string, + availableModels?: string[], +): ModelRoutingDecision => { + const resolution = resolveAgentModel(agent, defaultModel, availableModels); + + return { + ...resolution, + contextSize: getRouteContextSize(resolution.model), + }; +}; diff --git a/src/services/parallel/conflict-detector.ts b/src/services/parallel/conflict-detector.ts new file mode 100644 index 0000000..7ede153 --- /dev/null +++ b/src/services/parallel/conflict-detector.ts @@ -0,0 +1,241 @@ +/** + * Conflict Detector + * + * Detects conflicts between parallel tasks based on file paths + * and task types. Read-only tasks don't conflict with each other. + */ + +import { CONFLICT_CONFIG, READ_ONLY_TASK_TYPES, MODIFYING_TASK_TYPES } from "@constants/parallel"; +import type { + ParallelTask, + ConflictCheckResult, + ConflictResolution, +} from "@/types/parallel"; + +/** + * Active tasks being tracked for conflicts + */ +const activeTasks = new Map(); + +/** + * Register a task as active + */ +export const registerActiveTask = (task: ParallelTask): void => { + activeTasks.set(task.id, task); +}; + +/** + * Unregister a task when completed + */ +export const unregisterActiveTask = (taskId: string): void => { + activeTasks.delete(taskId); +}; + +/** + * Clear all active tasks + */ +export const clearActiveTasks = (): void => { + activeTasks.clear(); +}; + +/** + * Get all active task IDs + */ +export const getActiveTaskIds = (): string[] => { + return Array.from(activeTasks.keys()); +}; + +/** + * Check if two tasks conflict based on their paths + */ +const checkPathConflict = ( + taskA: ParallelTask, + taskB: ParallelTask, +): string[] => { + const pathsA = taskA.conflictPaths ?? []; + const pathsB = taskB.conflictPaths ?? []; + + const conflictingPaths: string[] = []; + + for (const pathA of pathsA) { + for (const pathB of pathsB) { + if (pathsOverlap(pathA, pathB)) { + conflictingPaths.push(pathA); + } + } + } + + return conflictingPaths; +}; + +/** + * Check if two paths overlap (one contains or equals the other) + */ +const pathsOverlap = (pathA: string, pathB: string): boolean => { + const normalizedA = normalizePath(pathA); + const normalizedB = normalizePath(pathB); + + // Exact match + if (normalizedA === normalizedB) return true; + + // One is parent of the other + if (normalizedA.startsWith(normalizedB + "/")) return true; + if (normalizedB.startsWith(normalizedA + "/")) return true; + + return false; +}; + +/** + * Normalize path for comparison + */ +const normalizePath = (path: string): string => { + return path.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/$/, ""); +}; + +/** + * Check if task types can conflict + */ +const canTypesConflict = (typeA: string, typeB: string): boolean => { + // Read-only tasks don't conflict with each other + if (READ_ONLY_TASK_TYPES.has(typeA) && READ_ONLY_TASK_TYPES.has(typeB)) { + return false; + } + + // Modifying tasks conflict with everything on same paths + if (MODIFYING_TASK_TYPES.has(typeA) || MODIFYING_TASK_TYPES.has(typeB)) { + return true; + } + + return false; +}; + +/** + * Check if a task conflicts with any active tasks + */ +export const checkConflicts = (task: ParallelTask): ConflictCheckResult => { + if (!CONFLICT_CONFIG.ENABLE_PATH_CONFLICT) { + return { + hasConflict: false, + conflictingTaskIds: [], + conflictingPaths: [], + }; + } + + const conflictingTaskIds: string[] = []; + const conflictingPaths: string[] = []; + + for (const [activeId, activeTask] of activeTasks) { + // Skip self + if (activeId === task.id) continue; + + // Check if task types can conflict + if (!canTypesConflict(task.type, activeTask.type)) continue; + + // Check path conflicts + const pathConflicts = checkPathConflict(task, activeTask); + + if (pathConflicts.length > 0) { + conflictingTaskIds.push(activeId); + conflictingPaths.push(...pathConflicts); + } + } + + const hasConflict = conflictingTaskIds.length > 0; + + // Suggest resolution + const resolution = hasConflict ? suggestResolution(task, conflictingTaskIds) : undefined; + + return { + hasConflict, + conflictingTaskIds, + conflictingPaths: [...new Set(conflictingPaths)], + resolution, + }; +}; + +/** + * Suggest a conflict resolution strategy + */ +const suggestResolution = ( + task: ParallelTask, + conflictingTaskIds: string[], +): ConflictResolution => { + // Read-only tasks should wait + if (READ_ONLY_TASK_TYPES.has(task.type)) { + return "wait"; + } + + // High priority tasks may cancel lower priority conflicts + const conflictingTasks = conflictingTaskIds + .map((id) => activeTasks.get(id)) + .filter((t): t is ParallelTask => t !== undefined); + + const allLowerPriority = conflictingTasks.every( + (t) => getPriorityValue(t.priority) < getPriorityValue(task.priority), + ); + + if (allLowerPriority && task.priority === "critical") { + return "cancel"; + } + + // Default to waiting + return "wait"; +}; + +/** + * Get numeric priority value + */ +const getPriorityValue = (priority: string): number => { + const values: Record = { + critical: 100, + high: 75, + normal: 50, + low: 25, + }; + return values[priority] ?? 50; +}; + +/** + * Wait for conflicts to resolve + */ +export const waitForConflictResolution = async ( + taskIds: string[], + timeout: number = CONFLICT_CONFIG.CONFLICT_CHECK_TIMEOUT_MS, +): Promise => { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + const stillActive = taskIds.filter((id) => activeTasks.has(id)); + + if (stillActive.length === 0) { + return true; + } + + // Wait a bit before checking again + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + return false; +}; + +/** + * Get tasks that would be affected by cancelling a task + */ +export const getDependentTasks = (taskId: string): string[] => { + const task = activeTasks.get(taskId); + if (!task) return []; + + const dependents: string[] = []; + + for (const [id, activeTask] of activeTasks) { + if (id === taskId) continue; + + // Check if this task was waiting on the cancelled task + const conflicts = checkPathConflict(activeTask, task); + if (conflicts.length > 0) { + dependents.push(id); + } + } + + return dependents; +}; diff --git a/src/services/parallel/index.ts b/src/services/parallel/index.ts new file mode 100644 index 0000000..43a4731 --- /dev/null +++ b/src/services/parallel/index.ts @@ -0,0 +1,351 @@ +/** + * Parallel Executor + * + * Main orchestrator for parallel task execution. + * Coordinates conflict detection, resource management, and result aggregation. + */ + +import { PARALLEL_DEFAULTS, PARALLEL_ERRORS, TASK_TIMEOUTS } from "@constants/parallel"; +import { + registerActiveTask, + unregisterActiveTask, + checkConflicts, + waitForConflictResolution, + clearActiveTasks, +} from "@services/parallel/conflict-detector"; +import { + initializeResourceManager, + acquireResources, + releaseResources, + canAcceptTask, + cancelWaitingTask, + resetResourceManager, + getResourceState, +} from "@services/parallel/resource-manager"; +import { collectResults } from "@services/parallel/result-aggregator"; +import type { + ParallelTask, + ParallelExecutionResult, + ParallelExecutorOptions, + AggregatedResults, + BatchExecutionRequest, + ConflictResolution, +} from "@/types/parallel"; + +// Re-export utilities +export * from "@services/parallel/conflict-detector"; +export * from "@services/parallel/resource-manager"; +export * from "@services/parallel/result-aggregator"; + +// ============================================================================ +// Task Execution +// ============================================================================ + +/** + * Execute a single task with timeout and error handling + */ +const executeTask = async ( + task: ParallelTask, + executor: (input: TInput) => Promise, + options: ParallelExecutorOptions, +): Promise> => { + const startedAt = Date.now(); + const timeout = task.timeout ?? TASK_TIMEOUTS[task.type] ?? PARALLEL_DEFAULTS.defaultTimeout; + + try { + // Notify task start + options.onTaskStart?.(task); + + // Execute with timeout + const result = await Promise.race([ + executor(task.input), + createTimeout(timeout, task.id), + ]); + + const completedAt = Date.now(); + const executionResult: ParallelExecutionResult = { + taskId: task.id, + status: "completed", + result, + duration: completedAt - startedAt, + startedAt, + completedAt, + }; + + options.onTaskComplete?.(executionResult); + return executionResult; + } catch (error) { + const completedAt = Date.now(); + const isTimeout = error instanceof TimeoutError; + + const executionResult: ParallelExecutionResult = { + taskId: task.id, + status: isTimeout ? "timeout" : "error", + error: error instanceof Error ? error.message : String(error), + duration: completedAt - startedAt, + startedAt, + completedAt, + }; + + options.onTaskError?.(task, error instanceof Error ? error : new Error(String(error))); + return executionResult; + } +}; + +/** + * Create a timeout promise + */ +class TimeoutError extends Error { + constructor(taskId: string) { + super(PARALLEL_ERRORS.TIMEOUT(taskId)); + this.name = "TimeoutError"; + } +} + +const createTimeout = (ms: number, taskId: string): Promise => { + return new Promise((_, reject) => { + setTimeout(() => reject(new TimeoutError(taskId)), ms); + }); +}; + +// ============================================================================ +// Parallel Executor +// ============================================================================ + +/** + * Execute tasks in parallel with conflict detection and resource management + */ +export const executeParallel = async ( + tasks: ParallelTask[], + executor: (input: TInput) => Promise, + options: Partial = {}, +): Promise> => { + const fullOptions: ParallelExecutorOptions = { + limits: options.limits ?? PARALLEL_DEFAULTS, + onTaskStart: options.onTaskStart, + onTaskComplete: options.onTaskComplete, + onTaskError: options.onTaskError, + onConflict: options.onConflict, + abortSignal: options.abortSignal, + }; + + // Initialize resource manager + initializeResourceManager(fullOptions.limits); + + // Track results + const results: ParallelExecutionResult[] = []; + const pendingTasks = new Map>>(); + + // Check if executor was aborted + const checkAbort = (): boolean => { + return fullOptions.abortSignal?.aborted ?? false; + }; + + // Process each task + for (const task of tasks) { + if (checkAbort()) { + results.push({ + taskId: task.id, + status: "cancelled", + error: PARALLEL_ERRORS.EXECUTOR_ABORTED, + duration: 0, + startedAt: Date.now(), + completedAt: Date.now(), + }); + continue; + } + + // Check if we can accept more tasks + if (!canAcceptTask(fullOptions.limits)) { + results.push({ + taskId: task.id, + status: "error", + error: PARALLEL_ERRORS.QUEUE_FULL, + duration: 0, + startedAt: Date.now(), + completedAt: Date.now(), + }); + continue; + } + + // Start task execution + const taskPromise = executeWithConflictHandling( + task, + executor, + fullOptions, + ); + + pendingTasks.set(task.id, taskPromise); + + // Remove from pending when done + taskPromise.then((result) => { + pendingTasks.delete(task.id); + results.push(result); + }); + } + + // Wait for all pending tasks + await Promise.all(pendingTasks.values()); + + // Cleanup + clearActiveTasks(); + + return collectResults(results); +}; + +/** + * Execute a task with conflict handling + */ +const executeWithConflictHandling = async ( + task: ParallelTask, + executor: (input: TInput) => Promise, + options: ParallelExecutorOptions, +): Promise> => { + // Acquire resources + await acquireResources(task); + + try { + // Check for conflicts + const conflicts = checkConflicts(task); + + if (conflicts.hasConflict) { + const resolution = options.onConflict?.(task, conflicts) ?? conflicts.resolution ?? "wait"; + + const handled = await handleConflict(task, conflicts, resolution, options); + if (!handled.continue) { + releaseResources(task, 0, false); + return handled.result; + } + } + + // Register as active + registerActiveTask(task); + + // Execute task + const result = await executeTask(task, executor, options); + + // Unregister and release resources + unregisterActiveTask(task.id); + releaseResources(task, result.duration, result.status === "completed"); + + return result; + } catch (error) { + releaseResources(task, 0, false); + throw error; + } +}; + +/** + * Handle task conflict based on resolution strategy + */ +const handleConflict = async ( + task: ParallelTask, + conflicts: { conflictingTaskIds: string[]; conflictingPaths: string[] }, + resolution: ConflictResolution, + _options: ParallelExecutorOptions, +): Promise<{ continue: boolean; result: ParallelExecutionResult }> => { + const createFailResult = (status: "conflict" | "cancelled", error: string) => ({ + continue: false, + result: { + taskId: task.id, + status, + error, + duration: 0, + startedAt: Date.now(), + completedAt: Date.now(), + } as ParallelExecutionResult, + }); + + const resolutionHandlers: Record< + ConflictResolution, + () => Promise<{ continue: boolean; result: ParallelExecutionResult }> + > = { + wait: async () => { + const resolved = await waitForConflictResolution(conflicts.conflictingTaskIds); + if (resolved) { + return { continue: true, result: {} as ParallelExecutionResult }; + } + return createFailResult("conflict", PARALLEL_ERRORS.CONFLICT(task.id, conflicts.conflictingPaths)); + }, + + cancel: async () => { + // Cancel conflicting tasks + for (const id of conflicts.conflictingTaskIds) { + cancelWaitingTask(id); + } + return { continue: true, result: {} as ParallelExecutionResult }; + }, + + merge: async () => { + // For merge, we continue and let result aggregator handle merging + return { continue: true, result: {} as ParallelExecutionResult }; + }, + + abort: async () => { + return createFailResult("conflict", PARALLEL_ERRORS.CONFLICT(task.id, conflicts.conflictingPaths)); + }, + }; + + return resolutionHandlers[resolution](); +}; + +// ============================================================================ +// Batch Execution +// ============================================================================ + +/** + * Execute a batch of tasks + */ +export const executeBatch = async ( + request: BatchExecutionRequest, + executor: (input: TInput) => Promise, +): Promise> => { + return executeParallel( + request.tasks as ParallelTask[], + executor, + request.options, + ); +}; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Create a task ID + */ +export const createTaskId = (): string => { + return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +}; + +/** + * Create a parallel task + */ +export const createTask = ( + input: TInput, + options: Partial> = {}, +): ParallelTask => ({ + id: options.id ?? createTaskId(), + type: options.type ?? "explore", + agent: options.agent ?? { name: "default" }, + input, + priority: options.priority ?? "normal", + conflictPaths: options.conflictPaths, + timeout: options.timeout, + metadata: options.metadata, +}); + +/** + * Reset the parallel executor + */ +export const resetParallelExecutor = (): void => { + clearActiveTasks(); + resetResourceManager(); +}; + +/** + * Get execution statistics + */ +export const getExecutionStats = () => { + return getResourceState(); +}; diff --git a/src/services/parallel/resource-manager.ts b/src/services/parallel/resource-manager.ts new file mode 100644 index 0000000..8e3c282 --- /dev/null +++ b/src/services/parallel/resource-manager.ts @@ -0,0 +1,274 @@ +/** + * Resource Manager + * + * Manages concurrent task execution limits using a semaphore pattern. + * Handles task queuing, priority ordering, and rate limiting. + */ + +import { + PARALLEL_DEFAULTS, + PRIORITY_WEIGHTS, + TASK_TYPE_LIMITS, + PARALLEL_ERRORS, +} from "@constants/parallel"; +import type { + ParallelTask, + ResourceLimits, + ResourceState, + SemaphoreState, + TaskPriority, +} from "@/types/parallel"; + +// ============================================================================ +// Semaphore Implementation +// ============================================================================ + +interface WaitingTask { + task: ParallelTask; + resolve: () => void; + reject: (reason: Error) => void; +} + +class Semaphore { + private permits: number; + private readonly maxPermits: number; + private waiting: WaitingTask[] = []; + + constructor(permits: number) { + this.permits = permits; + this.maxPermits = permits; + } + + async acquire(task: ParallelTask): Promise { + if (this.permits > 0) { + this.permits--; + return; + } + + return new Promise((resolve, reject) => { + this.waiting.push({ task, resolve, reject }); + // Sort by priority (highest first) + this.waiting.sort( + (a, b) => + PRIORITY_WEIGHTS[b.task.priority] - PRIORITY_WEIGHTS[a.task.priority], + ); + }); + } + + release(): void { + if (this.waiting.length > 0) { + const next = this.waiting.shift(); + if (next) { + next.resolve(); + } + } else { + this.permits = Math.min(this.permits + 1, this.maxPermits); + } + } + + cancelWaiting(taskId: string): boolean { + const index = this.waiting.findIndex((w) => w.task.id === taskId); + if (index === -1) return false; + + const [removed] = this.waiting.splice(index, 1); + removed.reject(new Error(PARALLEL_ERRORS.CANCELLED(taskId))); + return true; + } + + getState(): SemaphoreState { + return { + permits: this.permits, + maxPermits: this.maxPermits, + waiting: this.waiting.length, + }; + } + + clearWaiting(): void { + for (const waiting of this.waiting) { + waiting.reject(new Error(PARALLEL_ERRORS.EXECUTOR_ABORTED)); + } + this.waiting = []; + } +} + +// ============================================================================ +// Resource Manager +// ============================================================================ + +let globalSemaphore: Semaphore | null = null; +const taskTypeSemaphores = new Map(); +let resourceState: ResourceState = { + activeTasks: 0, + queuedTasks: 0, + completedTasks: 0, + failedTasks: 0, + totalDuration: 0, +}; + +/** + * Initialize resource manager with limits + */ +export const initializeResourceManager = ( + limits: ResourceLimits = PARALLEL_DEFAULTS, +): void => { + globalSemaphore = new Semaphore(limits.maxConcurrentTasks); + + // Create per-type semaphores + taskTypeSemaphores.clear(); + for (const [type, limit] of Object.entries(TASK_TYPE_LIMITS)) { + taskTypeSemaphores.set(type, new Semaphore(limit)); + } + + // Reset state + resourceState = { + activeTasks: 0, + queuedTasks: 0, + completedTasks: 0, + failedTasks: 0, + totalDuration: 0, + }; +}; + +/** + * Acquire resources for a task + */ +export const acquireResources = async (task: ParallelTask): Promise => { + if (!globalSemaphore) { + initializeResourceManager(); + } + + resourceState.queuedTasks++; + + try { + // Acquire global permit + await globalSemaphore!.acquire(task); + + // Acquire type-specific permit if exists + const typeSemaphore = taskTypeSemaphores.get(task.type); + if (typeSemaphore) { + await typeSemaphore.acquire(task); + } + + resourceState.queuedTasks--; + resourceState.activeTasks++; + } catch (error) { + resourceState.queuedTasks--; + throw error; + } +}; + +/** + * Release resources after task completion + */ +export const releaseResources = (task: ParallelTask, duration: number, success: boolean): void => { + if (!globalSemaphore) return; + + // Release global permit + globalSemaphore.release(); + + // Release type-specific permit + const typeSemaphore = taskTypeSemaphores.get(task.type); + if (typeSemaphore) { + typeSemaphore.release(); + } + + // Update state + resourceState.activeTasks--; + resourceState.totalDuration += duration; + + if (success) { + resourceState.completedTasks++; + } else { + resourceState.failedTasks++; + } +}; + +/** + * Cancel a waiting task + */ +export const cancelWaitingTask = (taskId: string): boolean => { + if (!globalSemaphore) return false; + + const cancelled = globalSemaphore.cancelWaiting(taskId); + + if (cancelled) { + resourceState.queuedTasks--; + } + + return cancelled; +}; + +/** + * Get current resource state + */ +export const getResourceState = (): ResourceState => ({ + ...resourceState, +}); + +/** + * Get semaphore state for a task type + */ +export const getTypeSemaphoreState = (type: string): SemaphoreState | null => { + const semaphore = taskTypeSemaphores.get(type); + return semaphore ? semaphore.getState() : null; +}; + +/** + * Get global semaphore state + */ +export const getGlobalSemaphoreState = (): SemaphoreState | null => { + return globalSemaphore ? globalSemaphore.getState() : null; +}; + +/** + * Check if we can accept more tasks + */ +export const canAcceptTask = ( + limits: ResourceLimits = PARALLEL_DEFAULTS, +): boolean => { + const totalPending = resourceState.activeTasks + resourceState.queuedTasks; + return totalPending < limits.maxQueueSize; +}; + +/** + * Reset resource manager + */ +export const resetResourceManager = (): void => { + if (globalSemaphore) { + globalSemaphore.clearWaiting(); + } + + for (const semaphore of taskTypeSemaphores.values()) { + semaphore.clearWaiting(); + } + + resourceState = { + activeTasks: 0, + queuedTasks: 0, + completedTasks: 0, + failedTasks: 0, + totalDuration: 0, + }; +}; + +/** + * Get queue position for a task based on priority + */ +export const getQueuePosition = (priority: TaskPriority): number => { + if (!globalSemaphore) return 0; + + const state = globalSemaphore.getState(); + + // Estimate position based on priority + // Higher priority tasks will be processed first + const priorityWeight = PRIORITY_WEIGHTS[priority]; + const avgWeight = + (PRIORITY_WEIGHTS.critical + + PRIORITY_WEIGHTS.high + + PRIORITY_WEIGHTS.normal + + PRIORITY_WEIGHTS.low) / + 4; + + const positionFactor = avgWeight / priorityWeight; + return Math.ceil(state.waiting * positionFactor); +}; diff --git a/src/services/parallel/result-aggregator.ts b/src/services/parallel/result-aggregator.ts new file mode 100644 index 0000000..445b0a3 --- /dev/null +++ b/src/services/parallel/result-aggregator.ts @@ -0,0 +1,280 @@ +/** + * Result Aggregator + * + * Merges and deduplicates results from parallel task execution. + * Supports various aggregation strategies based on task type. + */ + +import { DEDUP_CONFIG } from "@constants/parallel"; +import type { + ParallelExecutionResult, + AggregatedResults, + DeduplicationKey, + DeduplicationResult, +} from "@/types/parallel"; + +// ============================================================================ +// Result Collection +// ============================================================================ + +/** + * Collect results into aggregated structure + */ +export const collectResults = ( + results: ParallelExecutionResult[], +): AggregatedResults => { + const successful = results.filter((r) => r.status === "completed").length; + const failed = results.filter((r) => r.status === "error" || r.status === "timeout").length; + const cancelled = results.filter((r) => r.status === "cancelled").length; + + const totalDuration = results.reduce((sum, r) => sum + r.duration, 0); + + return { + results, + successful, + failed, + cancelled, + totalDuration, + }; +}; + +// ============================================================================ +// Deduplication +// ============================================================================ + +/** + * Create a deduplication key from an object + */ +export const createDeduplicationKey = ( + item: T, + keyExtractor: (item: T) => DeduplicationKey, +): string => { + const key = keyExtractor(item); + return JSON.stringify(key); +}; + +/** + * Deduplicate results based on a key extractor + */ +export const deduplicateResults = ( + items: T[], + keyExtractor: (item: T) => DeduplicationKey, +): DeduplicationResult => { + const seen = new Map(); + let duplicateCount = 0; + let mergedCount = 0; + + for (const item of items) { + const key = createDeduplicationKey(item, keyExtractor); + + if (seen.has(key)) { + duplicateCount++; + // Could implement merging logic here if needed + } else { + seen.set(key, item); + } + } + + return { + unique: Array.from(seen.values()), + duplicateCount, + mergedCount, + }; +}; + +/** + * Deduplicate file results (by path) + */ +export const deduplicateFileResults = ( + results: Array<{ path: string; content?: string }>, +): DeduplicationResult<{ path: string; content?: string }> => { + return deduplicateResults(results, (item) => ({ + type: "file", + path: item.path, + })); +}; + +/** + * Deduplicate search results (by path and content) + */ +export const deduplicateSearchResults = ( + results: T[], +): DeduplicationResult => { + return deduplicateResults(results, (item) => ({ + type: "search", + path: item.path, + content: item.match, + })); +}; + +// ============================================================================ +// Result Merging +// ============================================================================ + +/** + * Merge multiple arrays of results + */ +export const mergeArrayResults = (arrays: T[][]): T[] => { + return arrays.flat(); +}; + +/** + * Merge object results (shallow merge) + */ +export const mergeObjectResults = >( + objects: T[], +): T => { + return objects.reduce((acc, obj) => ({ ...acc, ...obj }), {} as T); +}; + +/** + * Merge results by priority (later results override earlier) + */ +export const mergeByPriority = ( + results: ParallelExecutionResult[], +): T | undefined => { + // Sort by completion time (most recent last) + const sorted = [...results].sort((a, b) => a.completedAt - b.completedAt); + + // Return the most recent successful result + const successful = sorted.filter((r) => r.status === "completed" && r.result !== undefined); + + return successful.length > 0 ? successful[successful.length - 1].result : undefined; +}; + +// ============================================================================ +// Content Similarity +// ============================================================================ + +/** + * Calculate similarity between two strings using Jaccard index + */ +const calculateSimilarity = (a: string, b: string): number => { + if (a === b) return 1; + if (!a || !b) return 0; + + const aTokens = new Set(a.toLowerCase().split(/\s+/)); + const bTokens = new Set(b.toLowerCase().split(/\s+/)); + + const intersection = [...aTokens].filter((token) => bTokens.has(token)); + const union = new Set([...aTokens, ...bTokens]); + + return intersection.length / union.size; +}; + +/** + * Find similar results based on content + */ +export const findSimilarResults = ( + items: T[], + contentExtractor: (item: T) => string, + threshold: number = DEDUP_CONFIG.SIMILARITY_THRESHOLD, +): Map => { + const similarGroups = new Map(); + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const content = contentExtractor(item); + const similar: T[] = []; + + for (let j = i + 1; j < items.length; j++) { + const otherItem = items[j]; + const otherContent = contentExtractor(otherItem); + const similarity = calculateSimilarity(content, otherContent); + + if (similarity >= threshold) { + similar.push(otherItem); + } + } + + if (similar.length > 0) { + similarGroups.set(item, similar); + } + } + + return similarGroups; +}; + +// ============================================================================ +// Aggregation Strategies +// ============================================================================ + +/** + * Aggregate results as a list (concatenate all) + */ +export const aggregateAsList = ( + results: ParallelExecutionResult[], +): T[] => { + const arrays = results + .filter((r) => r.status === "completed" && r.result) + .map((r) => r.result!); + + return mergeArrayResults(arrays); +}; + +/** + * Aggregate results as a map (by key) + */ +export const aggregateAsMap = >( + results: ParallelExecutionResult[], + keyExtractor: (result: T) => string, +): Map => { + const map = new Map(); + + for (const result of results) { + if (result.status === "completed" && result.result) { + const key = keyExtractor(result.result); + map.set(key, result.result); + } + } + + return map; +}; + +/** + * Aggregate results and return first non-empty + */ +export const aggregateFirstNonEmpty = ( + results: ParallelExecutionResult[], +): T | undefined => { + const successful = results + .filter((r) => r.status === "completed" && r.result !== undefined) + .sort((a, b) => a.completedAt - b.completedAt); + + return successful.length > 0 ? successful[0].result : undefined; +}; + +/** + * Aggregate numeric results (sum) + */ +export const aggregateSum = ( + results: ParallelExecutionResult[], +): number => { + return results + .filter((r) => r.status === "completed" && typeof r.result === "number") + .reduce((sum, r) => sum + r.result!, 0); +}; + +/** + * Aggregate boolean results (all true) + */ +export const aggregateAll = ( + results: ParallelExecutionResult[], +): boolean => { + const completed = results.filter( + (r) => r.status === "completed" && typeof r.result === "boolean", + ); + + return completed.length > 0 && completed.every((r) => r.result === true); +}; + +/** + * Aggregate boolean results (any true) + */ +export const aggregateAny = ( + results: ParallelExecutionResult[], +): boolean => { + return results.some( + (r) => r.status === "completed" && r.result === true, + ); +}; diff --git a/src/services/pr-review/diff-parser.ts b/src/services/pr-review/diff-parser.ts new file mode 100644 index 0000000..1f77d0f --- /dev/null +++ b/src/services/pr-review/diff-parser.ts @@ -0,0 +1,309 @@ +/** + * Diff Parser + * + * Parses unified diff format for PR review analysis. + */ + +import type { ParsedDiff, ParsedFileDiff, DiffHunk } from "@/types/pr-review"; + +/** + * Diff parsing patterns + */ +const PATTERNS = { + FILE_HEADER: /^diff --git a\/(.+) b\/(.+)$/, + OLD_FILE: /^--- (.+?)(?:\t.*)?$/, + NEW_FILE: /^\+\+\+ (.+?)(?:\t.*)?$/, + HUNK_HEADER: /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/, + BINARY: /^Binary files .+ differ$/, + NEW_FILE_MODE: /^new file mode \d+$/, + DELETED_FILE_MODE: /^deleted file mode \d+$/, + RENAME_FROM: /^rename from (.+)$/, + RENAME_TO: /^rename to (.+)$/, +} as const; + +/** + * Parse unified diff content + */ +export const parseDiff = (diffContent: string): ParsedDiff => { + const lines = diffContent.split("\n"); + const files: ParsedFileDiff[] = []; + + let currentFile: ParsedFileDiff | null = null; + let currentHunk: DiffHunk | null = null; + let lineIndex = 0; + + while (lineIndex < lines.length) { + const line = lines[lineIndex]; + + // Git diff header + const gitDiffMatch = line.match(PATTERNS.FILE_HEADER); + if (gitDiffMatch) { + if (currentFile) { + if (currentHunk) { + currentFile.hunks.push(currentHunk); + } + files.push(currentFile); + } + currentFile = createEmptyFileDiff(gitDiffMatch[1], gitDiffMatch[2]); + currentHunk = null; + lineIndex++; + continue; + } + + // Old file header + const oldFileMatch = line.match(PATTERNS.OLD_FILE); + if (oldFileMatch) { + if (!currentFile) { + currentFile = createEmptyFileDiff("", ""); + } + currentFile.oldPath = cleanPath(oldFileMatch[1]); + if (currentFile.oldPath === "/dev/null") { + currentFile.isNew = true; + } + lineIndex++; + continue; + } + + // New file header + const newFileMatch = line.match(PATTERNS.NEW_FILE); + if (newFileMatch) { + if (!currentFile) { + currentFile = createEmptyFileDiff("", ""); + } + currentFile.newPath = cleanPath(newFileMatch[1]); + if (currentFile.newPath === "/dev/null") { + currentFile.isDeleted = true; + } + lineIndex++; + continue; + } + + // Binary file + if (PATTERNS.BINARY.test(line)) { + if (currentFile) { + currentFile.isBinary = true; + } + lineIndex++; + continue; + } + + // New file mode + if (PATTERNS.NEW_FILE_MODE.test(line)) { + if (currentFile) { + currentFile.isNew = true; + } + lineIndex++; + continue; + } + + // Deleted file mode + if (PATTERNS.DELETED_FILE_MODE.test(line)) { + if (currentFile) { + currentFile.isDeleted = true; + } + lineIndex++; + continue; + } + + // Rename from + const renameFromMatch = line.match(PATTERNS.RENAME_FROM); + if (renameFromMatch) { + if (currentFile) { + currentFile.isRenamed = true; + currentFile.oldPath = cleanPath(renameFromMatch[1]); + } + lineIndex++; + continue; + } + + // Rename to + const renameToMatch = line.match(PATTERNS.RENAME_TO); + if (renameToMatch) { + if (currentFile) { + currentFile.newPath = cleanPath(renameToMatch[1]); + } + lineIndex++; + continue; + } + + // Hunk header + const hunkMatch = line.match(PATTERNS.HUNK_HEADER); + if (hunkMatch) { + if (currentHunk && currentFile) { + currentFile.hunks.push(currentHunk); + } + currentHunk = { + oldStart: parseInt(hunkMatch[1], 10), + oldLines: hunkMatch[2] ? parseInt(hunkMatch[2], 10) : 1, + newStart: parseInt(hunkMatch[3], 10), + newLines: hunkMatch[4] ? parseInt(hunkMatch[4], 10) : 1, + content: line, + additions: [], + deletions: [], + context: [], + }; + lineIndex++; + continue; + } + + // Content lines + if (currentHunk) { + if (line.startsWith("+") && !line.startsWith("+++")) { + currentHunk.additions.push(line.slice(1)); + if (currentFile) currentFile.additions++; + } else if (line.startsWith("-") && !line.startsWith("---")) { + currentHunk.deletions.push(line.slice(1)); + if (currentFile) currentFile.deletions++; + } else if (line.startsWith(" ") || line === "") { + currentHunk.context.push(line.slice(1) || ""); + } + } + + lineIndex++; + } + + // Push final hunk and file + if (currentHunk && currentFile) { + currentFile.hunks.push(currentHunk); + } + if (currentFile) { + files.push(currentFile); + } + + // Calculate totals + const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0); + const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0); + + return { + files, + totalAdditions, + totalDeletions, + totalFiles: files.length, + }; +}; + +/** + * Create empty file diff structure + */ +const createEmptyFileDiff = (oldPath: string, newPath: string): ParsedFileDiff => ({ + oldPath: cleanPath(oldPath), + newPath: cleanPath(newPath), + hunks: [], + additions: 0, + deletions: 0, + isBinary: false, + isNew: false, + isDeleted: false, + isRenamed: false, +}); + +/** + * Clean path by removing a/ or b/ prefixes + */ +const cleanPath = (path: string): string => { + if (path.startsWith("a/")) return path.slice(2); + if (path.startsWith("b/")) return path.slice(2); + return path; +}; + +/** + * Get the effective path for a file diff + */ +export const getFilePath = (fileDiff: ParsedFileDiff): string => { + if (fileDiff.isNew) return fileDiff.newPath; + if (fileDiff.isDeleted) return fileDiff.oldPath; + return fileDiff.newPath || fileDiff.oldPath; +}; + +/** + * Filter files by pattern + */ +export const filterFiles = ( + files: ParsedFileDiff[], + excludePatterns: string[], +): ParsedFileDiff[] => { + return files.filter((file) => { + const path = getFilePath(file); + return !excludePatterns.some((pattern) => matchPattern(path, pattern)); + }); +}; + +/** + * Simple glob pattern matching + */ +const matchPattern = (path: string, pattern: string): boolean => { + // Convert glob to regex + const regexPattern = pattern + .replace(/\*\*/g, ".*") + .replace(/\*/g, "[^/]*") + .replace(/\?/g, "."); + + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(path); +}; + +/** + * Get added lines with line numbers + */ +export const getAddedLines = ( + fileDiff: ParsedFileDiff, +): Array<{ line: number; content: string }> => { + const result: Array<{ line: number; content: string }> = []; + + for (const hunk of fileDiff.hunks) { + let lineNumber = hunk.newStart; + + for (const addition of hunk.additions) { + result.push({ line: lineNumber, content: addition }); + lineNumber++; + } + } + + return result; +}; + +/** + * Get hunk context (surrounding code) + */ +export const getHunkContext = ( + hunk: DiffHunk, + contextLines: number = 3, +): string => { + const lines: string[] = []; + + // Context before + const beforeContext = hunk.context.slice(0, contextLines); + for (const ctx of beforeContext) { + lines.push(` ${ctx}`); + } + + // Changes + for (const del of hunk.deletions) { + lines.push(`-${del}`); + } + for (const add of hunk.additions) { + lines.push(`+${add}`); + } + + // Context after + const afterContext = hunk.context.slice(-contextLines); + for (const ctx of afterContext) { + lines.push(` ${ctx}`); + } + + return lines.join("\n"); +}; + +/** + * Get diff statistics + */ +export const getDiffStats = ( + diff: ParsedDiff, +): { files: number; additions: number; deletions: number; summary: string } => { + return { + files: diff.totalFiles, + additions: diff.totalAdditions, + deletions: diff.totalDeletions, + summary: `${diff.totalFiles} file(s), +${diff.totalAdditions}/-${diff.totalDeletions}`, + }; +}; diff --git a/src/services/pr-review/index.ts b/src/services/pr-review/index.ts new file mode 100644 index 0000000..588faa7 --- /dev/null +++ b/src/services/pr-review/index.ts @@ -0,0 +1,215 @@ +/** + * PR Review Service + * + * Main orchestrator for multi-agent code review. + */ + +import { + DEFAULT_REVIEW_CONFIG, + PR_REVIEW_ERRORS, + PR_REVIEW_MESSAGES, +} from "@constants/pr-review"; +import { parseDiff, filterFiles, getFilePath } from "@services/pr-review/diff-parser"; +import { generateReport, formatReportMarkdown } from "@services/pr-review/report-generator"; +import * as securityReviewer from "@services/pr-review/reviewers/security"; +import * as performanceReviewer from "@services/pr-review/reviewers/performance"; +import * as logicReviewer from "@services/pr-review/reviewers/logic"; +import * as styleReviewer from "@services/pr-review/reviewers/style"; +import type { + PRReviewReport, + PRReviewRequest, + PRReviewConfig, + ReviewerResult, + ParsedDiff, + ReviewFileContext, +} from "@/types/pr-review"; + +// Re-export utilities +export * from "@services/pr-review/diff-parser"; +export * from "@services/pr-review/report-generator"; + +// Reviewer map +const reviewers = { + security: securityReviewer, + performance: performanceReviewer, + logic: logicReviewer, + style: styleReviewer, +} as const; + +/** + * Run a complete PR review + */ +export const reviewPR = async ( + diffContent: string, + request: PRReviewRequest = {}, + options: { + onProgress?: (message: string) => void; + abortSignal?: AbortSignal; + } = {}, +): Promise => { + const config = { ...DEFAULT_REVIEW_CONFIG, ...request.config }; + + options.onProgress?.(PR_REVIEW_MESSAGES.STARTING); + + // Parse diff + options.onProgress?.(PR_REVIEW_MESSAGES.PARSING_DIFF); + const diff = parseDiff(diffContent); + + if (diff.files.length === 0) { + throw new Error(PR_REVIEW_ERRORS.NO_FILES); + } + + // Filter files + const filteredFiles = filterFiles(diff.files, config.excludePatterns); + + if (filteredFiles.length === 0) { + throw new Error(PR_REVIEW_ERRORS.EXCLUDED_ALL); + } + + // Create filtered diff + const filteredDiff: ParsedDiff = { + files: filteredFiles, + totalAdditions: filteredFiles.reduce((sum, f) => sum + f.additions, 0), + totalDeletions: filteredFiles.reduce((sum, f) => sum + f.deletions, 0), + totalFiles: filteredFiles.length, + }; + + // Run reviewers in parallel + const reviewerResults = await runReviewers( + filteredDiff, + config, + options.onProgress, + options.abortSignal, + ); + + // Generate report + const report = generateReport(reviewerResults, filteredDiff, { + baseBranch: request.baseBranch ?? "main", + headBranch: request.headBranch ?? "HEAD", + commitRange: `${request.baseBranch ?? "main"}...${request.headBranch ?? "HEAD"}`, + }); + + options.onProgress?.(PR_REVIEW_MESSAGES.COMPLETED(report.findings.length)); + + return report; +}; + +/** + * Run all enabled reviewers + */ +const runReviewers = async ( + diff: ParsedDiff, + config: PRReviewConfig, + onProgress?: (message: string) => void, + abortSignal?: AbortSignal, +): Promise => { + const results: ReviewerResult[] = []; + const enabledReviewers = config.reviewers.filter((r) => r.enabled); + + // Run reviewers in parallel + const promises = enabledReviewers.map(async (reviewerConfig) => { + if (abortSignal?.aborted) { + return { + reviewer: reviewerConfig.name, + findings: [], + duration: 0, + error: "Aborted", + }; + } + + onProgress?.(PR_REVIEW_MESSAGES.REVIEWING(reviewerConfig.name)); + + const startTime = Date.now(); + const reviewerModule = reviewers[reviewerConfig.name as keyof typeof reviewers]; + + if (!reviewerModule) { + return { + reviewer: reviewerConfig.name, + findings: [], + duration: 0, + error: `Unknown reviewer: ${reviewerConfig.name}`, + }; + } + + try { + const findings = []; + + for (const fileDiff of diff.files) { + const fileContext: ReviewFileContext = { + path: getFilePath(fileDiff), + diff: fileDiff, + }; + + const fileFindings = reviewerModule.reviewFile(fileContext); + findings.push(...fileFindings); + } + + return { + reviewer: reviewerConfig.name, + findings, + duration: Date.now() - startTime, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + reviewer: reviewerConfig.name, + findings: [], + duration: Date.now() - startTime, + error: message, + }; + } + }); + + const parallelResults = await Promise.all(promises); + results.push(...parallelResults); + + return results; +}; + +/** + * Run a quick review (only critical checks) + */ +export const quickReview = async ( + diffContent: string, + options: { + onProgress?: (message: string) => void; + } = {}, +): Promise => { + return reviewPR( + diffContent, + { + config: { + reviewers: [ + { name: "security", type: "security", enabled: true, minConfidence: 90 }, + { name: "logic", type: "logic", enabled: true, minConfidence: 90 }, + ], + }, + }, + options, + ); +}; + +/** + * Get review report as markdown + */ +export const getReportMarkdown = (report: PRReviewReport): string => { + return formatReportMarkdown(report); +}; + +/** + * Create a review summary for commit messages + */ +export const createReviewSummary = (report: PRReviewReport): string => { + const parts: string[] = []; + + parts.push(`Review: ${report.rating}/5 stars`); + + if (report.findingsBySeverity.critical > 0) { + parts.push(`${report.findingsBySeverity.critical} critical issue(s)`); + } + if (report.findingsBySeverity.warning > 0) { + parts.push(`${report.findingsBySeverity.warning} warning(s)`); + } + + return parts.join(", "); +}; diff --git a/src/services/pr-review/report-generator.ts b/src/services/pr-review/report-generator.ts new file mode 100644 index 0000000..54a5c60 --- /dev/null +++ b/src/services/pr-review/report-generator.ts @@ -0,0 +1,410 @@ +/** + * Report Generator + * + * Aggregates findings and generates the review report. + */ + +import { + DEFAULT_REVIEW_CONFIG, + SEVERITY_ICONS, + SEVERITY_LABELS, + FINDING_TYPE_LABELS, + RATING_THRESHOLDS, + RECOMMENDATION_THRESHOLDS, + PR_REVIEW_TITLES, +} from "@constants/pr-review"; +import type { + PRReviewFinding, + PRReviewReport, + ReviewerResult, + ReviewRating, + ReviewRecommendation, + ReviewSeverity, + ReviewFindingType, + ParsedDiff, +} from "@/types/pr-review"; + +/** + * Generate a complete review report + */ +export const generateReport = ( + reviewerResults: ReviewerResult[], + diff: ParsedDiff, + options: { + baseBranch: string; + headBranch: string; + commitRange: string; + }, +): PRReviewReport => { + // Collect all findings + const allFindings = aggregateFindings(reviewerResults); + + // Filter by confidence threshold + const findings = filterByConfidence( + allFindings, + DEFAULT_REVIEW_CONFIG.minConfidence, + ); + + // Limit total findings + const limitedFindings = limitFindings( + findings, + DEFAULT_REVIEW_CONFIG.maxFindings, + ); + + // Calculate statistics + const findingsBySeverity = countBySeverity(limitedFindings); + const findingsByType = countByType(limitedFindings); + + // Calculate rating and recommendation + const rating = calculateRating(findingsBySeverity); + const recommendation = calculateRecommendation(findingsBySeverity); + + // Generate summary + const summary = generateSummary(limitedFindings, rating, recommendation); + + // Calculate duration + const duration = reviewerResults.reduce((sum, r) => sum + r.duration, 0); + + return { + id: generateReportId(), + timestamp: Date.now(), + duration, + + baseBranch: options.baseBranch, + headBranch: options.headBranch, + commitRange: options.commitRange, + + filesChanged: diff.totalFiles, + additions: diff.totalAdditions, + deletions: diff.totalDeletions, + + findings: limitedFindings, + findingsBySeverity, + findingsByType, + + reviewerResults, + + rating, + recommendation, + summary, + }; +}; + +/** + * Aggregate findings from all reviewers + */ +const aggregateFindings = (results: ReviewerResult[]): PRReviewFinding[] => { + const allFindings: PRReviewFinding[] = []; + + for (const result of results) { + allFindings.push(...result.findings); + } + + // Sort by severity (critical first) then by file + return allFindings.sort((a, b) => { + const severityOrder: Record = { + critical: 0, + warning: 1, + suggestion: 2, + nitpick: 3, + }; + const severityDiff = severityOrder[a.severity] - severityOrder[b.severity]; + if (severityDiff !== 0) return severityDiff; + return a.file.localeCompare(b.file); + }); +}; + +/** + * Filter findings by confidence threshold + */ +const filterByConfidence = ( + findings: PRReviewFinding[], + minConfidence: number, +): PRReviewFinding[] => { + return findings.filter((f) => f.confidence >= minConfidence); +}; + +/** + * Limit total number of findings + */ +const limitFindings = ( + findings: PRReviewFinding[], + maxFindings: number, +): PRReviewFinding[] => { + if (findings.length <= maxFindings) return findings; + + // Prioritize critical and warning findings + const critical = findings.filter((f) => f.severity === "critical"); + const warnings = findings.filter((f) => f.severity === "warning"); + const suggestions = findings.filter((f) => f.severity === "suggestion"); + const nitpicks = findings.filter((f) => f.severity === "nitpick"); + + const result: PRReviewFinding[] = []; + + // Add all critical findings + result.push(...critical); + + // Add warnings up to limit + const remainingWarnings = maxFindings - result.length; + result.push(...warnings.slice(0, remainingWarnings)); + + // Add suggestions if room + const remainingSuggestions = maxFindings - result.length; + result.push(...suggestions.slice(0, remainingSuggestions)); + + // Add nitpicks if room + const remainingNitpicks = maxFindings - result.length; + result.push(...nitpicks.slice(0, remainingNitpicks)); + + return result; +}; + +/** + * Count findings by severity + */ +const countBySeverity = ( + findings: PRReviewFinding[], +): Record => { + const counts: Record = { + critical: 0, + warning: 0, + suggestion: 0, + nitpick: 0, + }; + + for (const finding of findings) { + counts[finding.severity]++; + } + + return counts; +}; + +/** + * Count findings by type + */ +const countByType = ( + findings: PRReviewFinding[], +): Record => { + const counts: Record = { + security: 0, + performance: 0, + style: 0, + logic: 0, + documentation: 0, + testing: 0, + }; + + for (const finding of findings) { + counts[finding.type]++; + } + + return counts; +}; + +/** + * Calculate overall rating (1-5 stars) + */ +const calculateRating = ( + bySeverity: Record, +): ReviewRating => { + for (const rating of [5, 4, 3, 2, 1] as const) { + const threshold = RATING_THRESHOLDS[rating]; + if ( + bySeverity.critical <= threshold.maxCritical && + bySeverity.warning <= threshold.maxWarning + ) { + return rating; + } + } + return 1; +}; + +/** + * Calculate recommendation + */ +const calculateRecommendation = ( + bySeverity: Record, +): ReviewRecommendation => { + if ( + bySeverity.critical === 0 && + bySeverity.warning === 0 && + bySeverity.suggestion <= RECOMMENDATION_THRESHOLDS.approve.maxSuggestion + ) { + return "approve"; + } + + if ( + bySeverity.critical === 0 && + bySeverity.warning <= RECOMMENDATION_THRESHOLDS.approve_with_suggestions.maxWarning + ) { + return "approve_with_suggestions"; + } + + if (bySeverity.critical >= 1) { + return "request_changes"; + } + + return "needs_discussion"; +}; + +/** + * Generate summary text + */ +const generateSummary = ( + findings: PRReviewFinding[], + _rating: ReviewRating, + recommendation: ReviewRecommendation, +): string => { + if (findings.length === 0) { + return "No significant issues found. Code looks good!"; + } + + const parts: string[] = []; + + // Count by severity + const critical = findings.filter((f) => f.severity === "critical").length; + const warnings = findings.filter((f) => f.severity === "warning").length; + const suggestions = findings.filter((f) => f.severity === "suggestion").length; + + if (critical > 0) { + parts.push(`${critical} critical issue(s) must be addressed`); + } + + if (warnings > 0) { + parts.push(`${warnings} warning(s) should be reviewed`); + } + + if (suggestions > 0) { + parts.push(`${suggestions} suggestion(s) for improvement`); + } + + // Add recommendation context + const recommendationText: Record = { + approve: "", + approve_with_suggestions: + "Changes can be merged after addressing suggestions.", + request_changes: "Critical issues must be fixed before merging.", + needs_discussion: "Some items need clarification or discussion.", + }; + + if (recommendationText[recommendation]) { + parts.push(recommendationText[recommendation]); + } + + return parts.join(". "); +}; + +/** + * Format report as markdown + */ +export const formatReportMarkdown = (report: PRReviewReport): string => { + const lines: string[] = []; + + // Header + lines.push(`## ${PR_REVIEW_TITLES.REPORT}`); + lines.push(""); + + // Summary stats + lines.push("### ${PR_REVIEW_TITLES.SUMMARY}"); + lines.push(""); + lines.push(`| Metric | Value |`); + lines.push(`|--------|-------|`); + lines.push(`| Files Changed | ${report.filesChanged} |`); + lines.push(`| Additions | +${report.additions} |`); + lines.push(`| Deletions | -${report.deletions} |`); + lines.push(`| Findings | ${report.findings.length} |`); + lines.push(""); + + // Findings by severity + lines.push("| Severity | Count |"); + lines.push("|----------|-------|"); + for (const severity of ["critical", "warning", "suggestion", "nitpick"] as const) { + const count = report.findingsBySeverity[severity]; + if (count > 0) { + lines.push( + `| ${SEVERITY_ICONS[severity]} ${SEVERITY_LABELS[severity]} | ${count} |`, + ); + } + } + lines.push(""); + + // Rating + const stars = "⭐".repeat(report.rating); + lines.push(`**Rating:** ${stars} (${report.rating}/5)`); + lines.push(""); + + // Recommendation + const recommendationEmoji: Record = { + approve: "✅", + approve_with_suggestions: "✅", + request_changes: "🔴", + needs_discussion: "💬", + }; + lines.push( + `**${PR_REVIEW_TITLES.RECOMMENDATION}:** ${recommendationEmoji[report.recommendation]} ${formatRecommendation(report.recommendation)}`, + ); + lines.push(""); + lines.push(report.summary); + lines.push(""); + + // Findings + if (report.findings.length > 0) { + lines.push(`### ${PR_REVIEW_TITLES.FINDINGS}`); + lines.push(""); + + for (const finding of report.findings) { + lines.push(formatFinding(finding)); + lines.push(""); + } + } + + return lines.join("\n"); +}; + +/** + * Format recommendation for display + */ +const formatRecommendation = (recommendation: ReviewRecommendation): string => { + const labels: Record = { + approve: "Approve", + approve_with_suggestions: "Approve with Suggestions", + request_changes: "Request Changes", + needs_discussion: "Needs Discussion", + }; + return labels[recommendation]; +}; + +/** + * Format a single finding + */ +const formatFinding = (finding: PRReviewFinding): string => { + const lines: string[] = []; + + lines.push( + `${SEVERITY_ICONS[finding.severity]} **[${SEVERITY_LABELS[finding.severity]}]** ${FINDING_TYPE_LABELS[finding.type]}: ${finding.message}`, + ); + lines.push(""); + lines.push(`📍 \`${finding.file}${finding.line ? `:${finding.line}` : ""}\``); + + if (finding.details) { + lines.push(""); + lines.push(`**Issue:** ${finding.details}`); + } + + if (finding.suggestion) { + lines.push(""); + lines.push(`**Suggestion:** ${finding.suggestion}`); + } + + lines.push(""); + lines.push("---"); + + return lines.join("\n"); +}; + +/** + * Generate report ID + */ +const generateReportId = (): string => { + return `review_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +}; diff --git a/src/services/pr-review/reviewers/logic.ts b/src/services/pr-review/reviewers/logic.ts new file mode 100644 index 0000000..e66bfd5 --- /dev/null +++ b/src/services/pr-review/reviewers/logic.ts @@ -0,0 +1,240 @@ +/** + * Logic Reviewer + * + * Analyzes code for logical errors and edge cases. + */ + +import { MIN_CONFIDENCE_THRESHOLD, REVIEWER_PROMPTS } from "@constants/pr-review"; +import type { + PRReviewFinding, + ParsedFileDiff, + ReviewFileContext, +} from "@/types/pr-review"; + +/** + * Logic patterns to check + */ +const LOGIC_PATTERNS = { + MISSING_NULL_CHECK: { + patterns: [ + /\w+\.\w+\.\w+/, // Deep property access without optional chaining + /(\w+)\[['"][^'"]+['"]\]\.\w+/, // Object property followed by method + ], + message: "Potential null/undefined reference", + suggestion: "Use optional chaining (?.) or add null checks", + confidence: 70, + }, + + OPTIONAL_CHAIN_MISSING: { + patterns: [ + /if\s*\([^)]*\)\s*\{[^}]*\w+\./, // Variable used after if check without ?. + ], + message: "Consider using optional chaining", + suggestion: "Replace conditional access with ?. operator", + confidence: 65, + }, + + EMPTY_CATCH: { + patterns: [ + /catch\s*\([^)]*\)\s*\{\s*\}/, + /catch\s*\{\s*\}/, + ], + message: "Empty catch block - errors silently ignored", + suggestion: "Log the error or handle it appropriately", + confidence: 90, + }, + + UNHANDLED_PROMISE: { + patterns: [ + /\basync\s+\w+\s*\([^)]*\)\s*\{[^}]*(?!try)[^}]*await\s+[^}]*\}/, + ], + message: "Async function without try-catch", + suggestion: "Wrap await calls in try-catch or use .catch()", + confidence: 70, + }, + + FLOATING_PROMISE: { + patterns: [ + /^\s*\w+\s*\.\s*then\s*\(/m, + /^\s*\w+\([^)]*\)\.then\s*\(/m, + ], + message: "Floating promise - missing await or error handling", + suggestion: "Use await or add .catch() for error handling", + confidence: 80, + }, + + ARRAY_INDEX_ACCESS: { + patterns: [ + /\[\d+\]/, + /\[0\]/, + /\[-1\]/, + ], + message: "Direct array index access without bounds check", + suggestion: "Consider using .at() or add bounds checking", + confidence: 60, + }, + + EQUALITY_TYPE_COERCION: { + patterns: [ + /[^=!]==[^=]/, + /[^!]!=[^=]/, + ], + message: "Using == instead of === (type coercion)", + suggestion: "Use strict equality (===) to avoid type coercion bugs", + confidence: 85, + }, + + ASYNC_IN_FOREACH: { + patterns: [ + /\.forEach\s*\(\s*async/, + ], + message: "Async callback in forEach - won't await properly", + suggestion: "Use for...of loop or Promise.all with .map()", + confidence: 90, + }, + + MUTATING_PARAMETER: { + patterns: [ + /function\s+\w+\s*\(\w+\)\s*\{[^}]*\w+\s*\.\s*\w+\s*=/, + /\(\w+\)\s*=>\s*\{[^}]*\w+\s*\.\s*push/, + ], + message: "Mutating function parameter", + suggestion: "Create a copy before mutating or use immutable patterns", + confidence: 75, + }, + + RACE_CONDITION: { + patterns: [ + /let\s+\w+\s*=[^;]+;\s*await\s+[^;]+;\s*\w+\s*=/, + ], + message: "Potential race condition with shared state", + suggestion: "Use atomic operations or proper synchronization", + confidence: 70, + }, + + INFINITE_LOOP_RISK: { + patterns: [ + /while\s*\(\s*true\s*\)/, + /for\s*\(\s*;\s*;\s*\)/, + ], + message: "Infinite loop without clear exit condition", + suggestion: "Ensure there's a clear break condition", + confidence: 75, + }, +} as const; + +/** + * Run logic review on a file + */ +export const reviewFile = ( + fileContext: ReviewFileContext, +): PRReviewFinding[] => { + const findings: PRReviewFinding[] = []; + const { diff, path } = fileContext; + + // Get all added lines + const addedLines = getAllAddedLines(diff); + + // Check each pattern + for (const [patternName, config] of Object.entries(LOGIC_PATTERNS)) { + // Skip patterns below threshold + if (config.confidence < MIN_CONFIDENCE_THRESHOLD) { + continue; + } + + for (const { content, lineNumber } of addedLines) { + for (const pattern of config.patterns) { + if (pattern.test(content)) { + findings.push({ + id: generateFindingId(), + type: "logic", + severity: determineSeverity(config.confidence), + file: path, + line: lineNumber, + message: config.message, + details: `Pattern: ${patternName}`, + suggestion: config.suggestion, + confidence: config.confidence, + reviewer: "logic", + }); + break; + } + } + } + } + + // Deduplicate similar findings + return deduplicateFindings(findings); +}; + +/** + * Determine severity based on confidence + */ +const determineSeverity = ( + confidence: number, +): "critical" | "warning" | "suggestion" => { + if (confidence >= 90) return "critical"; + if (confidence >= 80) return "warning"; + return "suggestion"; +}; + +/** + * Get all added lines with line numbers + */ +const getAllAddedLines = ( + diff: ParsedFileDiff, +): Array<{ content: string; lineNumber: number }> => { + const lines: Array<{ content: string; lineNumber: number }> = []; + + for (const hunk of diff.hunks) { + let lineNumber = hunk.newStart; + + for (const addition of hunk.additions) { + lines.push({ + content: addition, + lineNumber, + }); + lineNumber++; + } + } + + return lines; +}; + +/** + * Deduplicate findings with same message on adjacent lines + */ +const deduplicateFindings = (findings: PRReviewFinding[]): PRReviewFinding[] => { + const seen = new Map(); + + for (const finding of findings) { + const key = `${finding.file}:${finding.message}`; + const existing = seen.get(key); + + if (!existing) { + seen.set(key, finding); + } else if (finding.line && existing.line) { + // Keep finding with more specific line number + if (Math.abs(finding.line - existing.line) > 5) { + // Different location, keep both + seen.set(`${key}:${finding.line}`, finding); + } + } + } + + return Array.from(seen.values()); +}; + +/** + * Generate unique finding ID + */ +const generateFindingId = (): string => { + return `logic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +}; + +/** + * Get reviewer prompt + */ +export const getPrompt = (): string => { + return REVIEWER_PROMPTS.logic; +}; diff --git a/src/services/pr-review/reviewers/performance.ts b/src/services/pr-review/reviewers/performance.ts new file mode 100644 index 0000000..8fb028c --- /dev/null +++ b/src/services/pr-review/reviewers/performance.ts @@ -0,0 +1,208 @@ +/** + * Performance Reviewer + * + * Analyzes code for performance issues. + */ + +import { MIN_CONFIDENCE_THRESHOLD, REVIEWER_PROMPTS } from "@constants/pr-review"; +import type { + PRReviewFinding, + ParsedFileDiff, + ReviewFileContext, +} from "@/types/pr-review"; + +/** + * Performance patterns to check + */ +const PERFORMANCE_PATTERNS = { + NESTED_LOOPS: { + patterns: [ + /for\s*\([^)]+\)\s*\{[^}]*for\s*\([^)]+\)/, + /\.forEach\([^)]+\)[^}]*\.forEach\(/, + /\.map\([^)]+\)[^}]*\.map\(/, + /while\s*\([^)]+\)\s*\{[^}]*while\s*\([^)]+\)/, + ], + message: "Nested loops detected - potential O(n²) complexity", + suggestion: "Consider using a Map/Set for O(1) lookups or restructuring the algorithm", + confidence: 75, + }, + + ARRAY_IN_LOOP: { + patterns: [ + /for\s*\([^)]+\)\s*\{[^}]*\.includes\s*\(/, + /for\s*\([^)]+\)\s*\{[^}]*\.indexOf\s*\(/, + /\.forEach\([^)]+\)[^}]*\.includes\s*\(/, + /\.map\([^)]+\)[^}]*\.indexOf\s*\(/, + ], + message: "Array search inside loop - O(n²) complexity", + suggestion: "Convert array to Set for O(1) lookups before the loop", + confidence: 85, + }, + + UNNECESSARY_RERENDER: { + patterns: [ + /useEffect\s*\(\s*\([^)]*\)\s*=>\s*\{[^}]*\},\s*\[\s*\]\s*\)/, + /useState\s*\(\s*\{/, + /useState\s*\(\s*\[/, + /style\s*=\s*\{\s*\{/, + ], + message: "Potential unnecessary re-render in React component", + suggestion: "Use useMemo/useCallback for objects/arrays, extract styles outside component", + confidence: 70, + }, + + MISSING_MEMO: { + patterns: [ + /export\s+(?:default\s+)?function\s+\w+\s*\([^)]*\)\s*\{[^}]*return\s*\(/, + /const\s+\w+\s*=\s*\([^)]*\)\s*=>\s*\{[^}]*return\s*\(/, + ], + message: "Component may benefit from React.memo", + suggestion: "Consider wrapping with React.memo if props rarely change", + confidence: 60, // Below threshold, informational only + }, + + N_PLUS_ONE_QUERY: { + patterns: [ + /for\s*\([^)]+\)\s*\{[^}]*await\s+.*\.(find|query|get)/, + /\.forEach\([^)]+\)[^}]*await\s+.*\.(find|query|get)/, + /\.map\([^)]+\)[^}]*await\s+.*\.(find|query|get)/, + ], + message: "Potential N+1 query problem", + suggestion: "Use batch queries or include/join to fetch related data", + confidence: 85, + }, + + MEMORY_LEAK: { + patterns: [ + /setInterval\s*\([^)]+\)/, + /addEventListener\s*\([^)]+\)/, + /subscribe\s*\([^)]+\)/, + ], + message: "Potential memory leak - subscription/interval without cleanup", + suggestion: "Ensure cleanup in useEffect return or componentWillUnmount", + confidence: 75, + }, + + LARGE_BUNDLE: { + patterns: [ + /import\s+\*\s+as\s+\w+\s+from\s+['"]lodash['"]/, + /import\s+\w+\s+from\s+['"]moment['"]/, + /require\s*\(\s*['"]lodash['"]\s*\)/, + ], + message: "Large library import may increase bundle size", + suggestion: "Use specific imports (lodash/get) or smaller alternatives (date-fns)", + confidence: 80, + }, + + SYNC_FILE_OPERATION: { + patterns: [ + /readFileSync\s*\(/, + /writeFileSync\s*\(/, + /readdirSync\s*\(/, + /existsSync\s*\(/, + ], + message: "Synchronous file operation may block event loop", + suggestion: "Use async versions (readFile, writeFile) for better performance", + confidence: 80, + }, +} as const; + +/** + * Run performance review on a file + */ +export const reviewFile = ( + fileContext: ReviewFileContext, +): PRReviewFinding[] => { + const findings: PRReviewFinding[] = []; + const { diff, path } = fileContext; + + // Get all added lines + const addedLines = getAllAddedLines(diff); + + // Combine lines for multi-line pattern matching + const combinedContent = addedLines.map(l => l.content).join("\n"); + + // Check each pattern + for (const [patternName, config] of Object.entries(PERFORMANCE_PATTERNS)) { + // Skip patterns below threshold + if (config.confidence < MIN_CONFIDENCE_THRESHOLD) { + continue; + } + + for (const pattern of config.patterns) { + // Check in combined content for multi-line patterns + if (pattern.test(combinedContent)) { + // Find the approximate line number + const lineNumber = findPatternLine(addedLines, pattern); + + findings.push({ + id: generateFindingId(), + type: "performance", + severity: config.confidence >= 85 ? "warning" : "suggestion", + file: path, + line: lineNumber, + message: config.message, + details: `Pattern: ${patternName}`, + suggestion: config.suggestion, + confidence: config.confidence, + reviewer: "performance", + }); + break; // One finding per pattern type + } + } + } + + return findings; +}; + +/** + * Get all added lines with line numbers + */ +const getAllAddedLines = ( + diff: ParsedFileDiff, +): Array<{ content: string; lineNumber: number }> => { + const lines: Array<{ content: string; lineNumber: number }> = []; + + for (const hunk of diff.hunks) { + let lineNumber = hunk.newStart; + + for (const addition of hunk.additions) { + lines.push({ + content: addition, + lineNumber, + }); + lineNumber++; + } + } + + return lines; +}; + +/** + * Find the line number where a pattern matches + */ +const findPatternLine = ( + lines: Array<{ content: string; lineNumber: number }>, + pattern: RegExp, +): number | undefined => { + for (const { content, lineNumber } of lines) { + if (pattern.test(content)) { + return lineNumber; + } + } + return lines.length > 0 ? lines[0].lineNumber : undefined; +}; + +/** + * Generate unique finding ID + */ +const generateFindingId = (): string => { + return `perf_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +}; + +/** + * Get reviewer prompt + */ +export const getPrompt = (): string => { + return REVIEWER_PROMPTS.performance; +}; diff --git a/src/services/pr-review/reviewers/security.ts b/src/services/pr-review/reviewers/security.ts new file mode 100644 index 0000000..476e081 --- /dev/null +++ b/src/services/pr-review/reviewers/security.ts @@ -0,0 +1,182 @@ +/** + * Security Reviewer + * + * Analyzes code for security vulnerabilities. + */ + +import { MIN_CONFIDENCE_THRESHOLD, REVIEWER_PROMPTS } from "@constants/pr-review"; +import type { + PRReviewFinding, + ParsedFileDiff, + ReviewFileContext, +} from "@/types/pr-review"; + +/** + * Security patterns to check + */ +const SECURITY_PATTERNS = { + SQL_INJECTION: { + patterns: [ + /`SELECT .* FROM .* WHERE .*\$\{/i, + /`INSERT INTO .* VALUES.*\$\{/i, + /`UPDATE .* SET .*\$\{/i, + /`DELETE FROM .* WHERE .*\$\{/i, + /query\s*\(\s*['"`].*\$\{/i, + /execute\s*\(\s*['"`].*\$\{/i, + ], + message: "Potential SQL injection vulnerability", + suggestion: "Use parameterized queries or prepared statements", + confidence: 90, + }, + + XSS: { + patterns: [ + /innerHTML\s*=\s*[^"'].*\+/, + /dangerouslySetInnerHTML/, + /document\.write\s*\(/, + /\.html\s*\([^)]*\+/, + /v-html\s*=/, + ], + message: "Potential XSS vulnerability", + suggestion: "Sanitize user input before rendering or use text content", + confidence: 85, + }, + + COMMAND_INJECTION: { + patterns: [ + /exec\s*\(\s*['"`].*\$\{/, + /spawn\s*\(\s*['"`].*\$\{/, + /execSync\s*\(\s*['"`].*\$\{/, + /child_process.*\$\{/, + /\$\(.* \+ /, + ], + message: "Potential command injection vulnerability", + suggestion: "Avoid string concatenation in shell commands, use argument arrays", + confidence: 90, + }, + + PATH_TRAVERSAL: { + patterns: [ + /readFile\s*\([^)]*\+/, + /readFileSync\s*\([^)]*\+/, + /fs\..*\([^)]*\+.*req\./, + /path\.join\s*\([^)]*req\./, + ], + message: "Potential path traversal vulnerability", + suggestion: "Validate and sanitize file paths, use path.normalize", + confidence: 85, + }, + + SECRETS_EXPOSURE: { + patterns: [ + /api[_-]?key\s*[:=]\s*['"][^'"]+['"]/i, + /secret\s*[:=]\s*['"][^'"]+['"]/i, + /password\s*[:=]\s*['"][^'"]+['"]/i, + /token\s*[:=]\s*['"][^'"]+['"]/i, + /private[_-]?key\s*[:=]\s*['"][^'"]+['"]/i, + /Bearer\s+[A-Za-z0-9_-]+/, + ], + message: "Potential hardcoded secret", + suggestion: "Use environment variables or a secrets manager", + confidence: 80, + }, + + INSECURE_RANDOM: { + patterns: [ + /Math\.random\s*\(\)/, + ], + message: "Insecure random number generation", + suggestion: "Use crypto.randomBytes or crypto.getRandomValues for security-sensitive operations", + confidence: 70, + }, + + EVAL_USAGE: { + patterns: [ + /\beval\s*\(/, + /new\s+Function\s*\(/, + /setTimeout\s*\(\s*['"`]/, + /setInterval\s*\(\s*['"`]/, + ], + message: "Dangerous use of eval or dynamic code execution", + suggestion: "Avoid eval and dynamic code execution, use safer alternatives", + confidence: 85, + }, +} as const; + +/** + * Run security review on a file + */ +export const reviewFile = ( + fileContext: ReviewFileContext, +): PRReviewFinding[] => { + const findings: PRReviewFinding[] = []; + const { diff, path } = fileContext; + + // Get all added lines + const addedLines = getAllAddedLines(diff); + + // Check each pattern + for (const [patternName, config] of Object.entries(SECURITY_PATTERNS)) { + for (const { content, lineNumber } of addedLines) { + for (const pattern of config.patterns) { + if (pattern.test(content)) { + // Only report if confidence meets threshold + if (config.confidence >= MIN_CONFIDENCE_THRESHOLD) { + findings.push({ + id: generateFindingId(), + type: "security", + severity: config.confidence >= 90 ? "critical" : "warning", + file: path, + line: lineNumber, + message: config.message, + details: `Found pattern: ${patternName}`, + suggestion: config.suggestion, + confidence: config.confidence, + reviewer: "security", + }); + } + break; // One finding per line per pattern type + } + } + } + } + + return findings; +}; + +/** + * Get all added lines with line numbers + */ +const getAllAddedLines = ( + diff: ParsedFileDiff, +): Array<{ content: string; lineNumber: number }> => { + const lines: Array<{ content: string; lineNumber: number }> = []; + + for (const hunk of diff.hunks) { + let lineNumber = hunk.newStart; + + for (const addition of hunk.additions) { + lines.push({ + content: addition, + lineNumber, + }); + lineNumber++; + } + } + + return lines; +}; + +/** + * Generate unique finding ID + */ +const generateFindingId = (): string => { + return `sec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +}; + +/** + * Get reviewer prompt + */ +export const getPrompt = (): string => { + return REVIEWER_PROMPTS.security; +}; diff --git a/src/services/pr-review/reviewers/style.ts b/src/services/pr-review/reviewers/style.ts new file mode 100644 index 0000000..4f8bbe3 --- /dev/null +++ b/src/services/pr-review/reviewers/style.ts @@ -0,0 +1,267 @@ +/** + * Style Reviewer + * + * Analyzes code for style and consistency issues. + */ + +import { MIN_CONFIDENCE_THRESHOLD, REVIEWER_PROMPTS } from "@constants/pr-review"; +import type { + PRReviewFinding, + ParsedFileDiff, + ReviewFileContext, +} from "@/types/pr-review"; + +/** + * Style patterns to check + */ +const STYLE_PATTERNS = { + CONSOLE_LOG: { + patterns: [ + /console\.(log|debug|info)\s*\(/, + ], + message: "Console statement left in code", + suggestion: "Remove console statements before committing or use a logger", + confidence: 85, + }, + + TODO_COMMENT: { + patterns: [ + /\/\/\s*TODO[:\s]/i, + /\/\/\s*FIXME[:\s]/i, + /\/\/\s*HACK[:\s]/i, + /\/\*\s*TODO[:\s]/i, + ], + message: "TODO/FIXME comment found", + suggestion: "Address the TODO or create a tracking issue", + confidence: 70, + }, + + MAGIC_NUMBER: { + patterns: [ + /(?\s*\{[^}]*\)\s*=>\s*\{[^}]*\)\s*=>\s*\{/, + /\.then\([^)]+\.then\([^)]+\.then\(/, + ], + message: "Deeply nested callbacks - callback hell", + suggestion: "Refactor using async/await or extract functions", + confidence: 80, + }, + + ANY_TYPE: { + patterns: [ + /:\s*any\b/, + //, + /as\s+any\b/, + ], + message: "Using 'any' type reduces type safety", + suggestion: "Use specific types or 'unknown' with type guards", + confidence: 75, + }, + + SINGLE_LETTER_VAR: { + patterns: [ + /\b(?:const|let|var)\s+[a-z]\s*=/, + ], + message: "Single-letter variable name", + suggestion: "Use descriptive variable names for clarity", + confidence: 65, + }, + + COMMENTED_CODE: { + patterns: [ + /\/\/\s*(?:const|let|var|function|if|for|while|return)\s+\w+/, + /\/\*\s*(?:const|let|var|function|if|for|while|return)\s+\w+/, + ], + message: "Commented out code detected", + suggestion: "Remove commented code - use version control for history", + confidence: 80, + }, + + DUPLICATE_IMPORT: { + patterns: [ + /import\s+\{[^}]+\}\s+from\s+['"]([^'"]+)['"]/, + ], + message: "Check for duplicate or unused imports", + suggestion: "Consolidate imports from the same module", + confidence: 60, + }, +} as const; + +/** + * Run style review on a file + */ +export const reviewFile = ( + fileContext: ReviewFileContext, +): PRReviewFinding[] => { + const findings: PRReviewFinding[] = []; + const { diff, path } = fileContext; + + // Get all added lines + const addedLines = getAllAddedLines(diff); + + // Check each pattern + for (const [patternName, config] of Object.entries(STYLE_PATTERNS)) { + // Skip patterns below threshold + if (config.confidence < MIN_CONFIDENCE_THRESHOLD) { + continue; + } + + let foundInFile = false; + + for (const { content, lineNumber } of addedLines) { + for (const pattern of config.patterns) { + if (pattern.test(content)) { + // For some patterns, only report once per file + if (shouldReportOncePerFile(patternName)) { + if (!foundInFile) { + findings.push(createFinding(path, lineNumber, config, patternName)); + foundInFile = true; + } + } else { + findings.push(createFinding(path, lineNumber, config, patternName)); + } + break; + } + } + } + } + + // Limit findings per pattern type + return limitFindings(findings, 3); +}; + +/** + * Check if pattern should only be reported once per file + */ +const shouldReportOncePerFile = (patternName: string): boolean => { + const oncePerFile = new Set([ + "INCONSISTENT_QUOTES", + "VAR_DECLARATION", + "ANY_TYPE", + "DUPLICATE_IMPORT", + ]); + return oncePerFile.has(patternName); +}; + +/** + * Create a finding from config + */ +const createFinding = ( + path: string, + lineNumber: number, + config: { message: string; suggestion: string; confidence: number }, + patternName: string, +): PRReviewFinding => ({ + id: generateFindingId(), + type: "style", + severity: config.confidence >= 85 ? "warning" : "nitpick", + file: path, + line: lineNumber, + message: config.message, + details: `Pattern: ${patternName}`, + suggestion: config.suggestion, + confidence: config.confidence, + reviewer: "style", +}); + +/** + * Limit findings per pattern to avoid noise + */ +const limitFindings = ( + findings: PRReviewFinding[], + maxPerPattern: number, +): PRReviewFinding[] => { + const countByMessage = new Map(); + const result: PRReviewFinding[] = []; + + for (const finding of findings) { + const count = countByMessage.get(finding.message) ?? 0; + if (count < maxPerPattern) { + result.push(finding); + countByMessage.set(finding.message, count + 1); + } + } + + return result; +}; + +/** + * Get all added lines with line numbers + */ +const getAllAddedLines = ( + diff: ParsedFileDiff, +): Array<{ content: string; lineNumber: number }> => { + const lines: Array<{ content: string; lineNumber: number }> = []; + + for (const hunk of diff.hunks) { + let lineNumber = hunk.newStart; + + for (const addition of hunk.additions) { + lines.push({ + content: addition, + lineNumber, + }); + lineNumber++; + } + } + + return lines; +}; + +/** + * Generate unique finding ID + */ +const generateFindingId = (): string => { + return `style_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +}; + +/** + * Get reviewer prompt + */ +export const getPrompt = (): string => { + return REVIEWER_PROMPTS.style; +}; diff --git a/src/services/prompt-builder.ts b/src/services/prompt-builder.ts index 7a97e49..30c5bc8 100644 --- a/src/services/prompt-builder.ts +++ b/src/services/prompt-builder.ts @@ -74,7 +74,37 @@ const MODE_PROMPT_BUILDERS: Record< }; /** - * Get git context for prompt building + * Execute git command asynchronously + */ +const execGitCommand = (args: string[]): Promise => { + return new Promise((resolve, reject) => { + const { spawn } = require("child_process"); + const proc = spawn("git", args, { cwd: process.cwd() }); + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (data: Buffer) => { + stdout += data.toString(); + }); + + proc.stderr.on("data", (data: Buffer) => { + stderr += data.toString(); + }); + + proc.on("close", (code: number) => { + if (code === 0) { + resolve(stdout.trim()); + } else { + reject(new Error(stderr || `git exited with code ${code}`)); + } + }); + + proc.on("error", reject); + }); +}; + +/** + * Get git context for prompt building (async, non-blocking) */ export const getGitContext = async (): Promise<{ isGitRepo: boolean; @@ -83,16 +113,15 @@ export const getGitContext = async (): Promise<{ recentCommits?: string[]; }> => { try { - const { execSync } = await import("child_process"); - const branch = execSync("git branch --show-current", { - encoding: "utf-8", - }).trim(); - const status = - execSync("git status --short", { encoding: "utf-8" }).trim() || "(clean)"; - const commits = execSync("git log --oneline -5", { encoding: "utf-8" }) - .trim() - .split("\n") - .filter(Boolean); + // Run all git commands in parallel for faster execution + const [branch, status, commits] = await Promise.all([ + execGitCommand(["branch", "--show-current"]), + execGitCommand(["status", "--short"]).then((s) => s || "(clean)"), + execGitCommand(["log", "--oneline", "-5"]).then((s) => + s.split("\n").filter(Boolean), + ), + ]); + return { isGitRepo: true, branch, status, recentCommits: commits }; } catch { return { isGitRepo: false }; diff --git a/src/services/session-compaction.ts b/src/services/session-compaction.ts new file mode 100644 index 0000000..9daf859 --- /dev/null +++ b/src/services/session-compaction.ts @@ -0,0 +1,318 @@ +/** + * Session Compaction Service + * + * Integrates auto-compaction with the agent loop and hooks system. + * Follows OpenCode's two-tier approach: pruning (remove old tool output) + * and compaction (summarize for fresh context). + */ + +import type { Message } from "@/types/providers"; +import { + CHARS_PER_TOKEN, + TOKEN_OVERFLOW_THRESHOLD, + PRUNE_MINIMUM_TOKENS, + PRUNE_PROTECT_TOKENS, + PRUNE_RECENT_TURNS, + PRUNE_PROTECTED_TOOLS, + TOKEN_MESSAGES, +} from "@constants/token"; +import { getModelContextSize, DEFAULT_CONTEXT_SIZE } from "@constants/copilot"; +import { + compactConversation, + checkCompactionNeeded, + getModelCompactionConfig, + createCompactionSummary, +} from "@services/auto-compaction"; +import { appStore } from "@tui-solid/context/app"; + +/** + * Estimate tokens from content + */ +export const estimateTokens = (content: string): number => { + return Math.max(0, Math.round((content || "").length / CHARS_PER_TOKEN)); +}; + +/** + * Estimate total tokens in message array + */ +export const estimateMessagesTokens = (messages: Message[]): number => { + return messages.reduce((total, msg) => { + const content = + typeof msg.content === "string" + ? msg.content + : JSON.stringify(msg.content); + return total + estimateTokens(content); + }, 0); +}; + +/** + * Check if context overflow is imminent + */ +export const isContextOverflow = ( + messages: Message[], + modelId?: string, +): boolean => { + const contextSize = modelId + ? getModelContextSize(modelId) + : DEFAULT_CONTEXT_SIZE; + const currentTokens = estimateMessagesTokens(messages); + const threshold = contextSize.input * TOKEN_OVERFLOW_THRESHOLD; + return currentTokens >= threshold; +}; + +/** + * Prune old tool outputs from messages + * + * Strategy (following OpenCode): + * 1. Walk backwards through messages + * 2. Skip first N user turns (protect recent context) + * 3. Mark tool outputs for pruning once we accumulate enough tokens + * 4. Only prune if we can free minimum threshold + */ +export const pruneToolOutputs = ( + messages: Message[], + options: { + minTokensToFree?: number; + protectThreshold?: number; + recentTurns?: number; + protectedTools?: Set; + } = {}, +): { messages: Message[]; prunedCount: number; tokensSaved: number } => { + const { + minTokensToFree = PRUNE_MINIMUM_TOKENS, + protectThreshold = PRUNE_PROTECT_TOKENS, + recentTurns = PRUNE_RECENT_TURNS, + protectedTools = PRUNE_PROTECTED_TOOLS, + } = options; + + // Find tool messages to potentially prune + interface PruneCandidate { + index: number; + tokens: number; + } + + const candidates: PruneCandidate[] = []; + let userTurnCount = 0; + let totalPrunableTokens = 0; + + // Walk backwards through messages + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i]; + + // Count user turns + if (msg.role === "user") { + userTurnCount++; + } + + // Skip if in protected recent turns + if (userTurnCount < recentTurns) { + continue; + } + + // Check for tool messages + if (msg.role === "tool") { + // Extract tool name from tool_call_id if possible + const toolName = (msg as { tool_call_id?: string }).tool_call_id + ?.split("-")[0] ?? ""; + + // Skip protected tools + if (protectedTools.has(toolName)) { + continue; + } + + const content = + typeof msg.content === "string" + ? msg.content + : JSON.stringify(msg.content); + const tokens = estimateTokens(content); + totalPrunableTokens += tokens; + + // Only mark for pruning after we've accumulated enough + if (totalPrunableTokens > protectThreshold) { + candidates.push({ index: i, tokens }); + } + } + } + + // Calculate total tokens to save + const tokensSaved = candidates.reduce((sum, c) => sum + c.tokens, 0); + + // Only prune if we can free minimum threshold + if (tokensSaved < minTokensToFree) { + return { messages, prunedCount: 0, tokensSaved: 0 }; + } + + // Create pruned messages + const prunedIndices = new Set(candidates.map((c) => c.index)); + const prunedMessages = messages.map((msg, index) => { + if (prunedIndices.has(index)) { + // Replace content with truncation marker + return { + ...msg, + content: "[Output pruned to save context]", + }; + } + return msg; + }); + + return { + messages: prunedMessages, + prunedCount: candidates.length, + tokensSaved, + }; +}; + +/** + * Perform full session compaction + * + * 1. First try pruning tool outputs + * 2. If still over threshold, do full compaction + */ +export const performSessionCompaction = async ( + messages: Message[], + modelId?: string, + options?: { + onPruneStart?: () => void; + onPruneComplete?: (count: number, saved: number) => void; + onCompactStart?: () => void; + onCompactComplete?: (saved: number) => void; + }, +): Promise<{ + messages: Message[]; + compacted: boolean; + pruned: boolean; + tokensSaved: number; +}> => { + const config = getModelCompactionConfig(modelId); + + // Phase 1: Try pruning first + options?.onPruneStart?.(); + const pruneResult = pruneToolOutputs(messages); + + if (pruneResult.prunedCount > 0) { + options?.onPruneComplete?.(pruneResult.prunedCount, pruneResult.tokensSaved); + + // Check if pruning was enough + const afterPruneCheck = checkCompactionNeeded(pruneResult.messages, config); + if (!afterPruneCheck.needsCompaction) { + return { + messages: pruneResult.messages, + compacted: false, + pruned: true, + tokensSaved: pruneResult.tokensSaved, + }; + } + } + + // Phase 2: Full compaction needed + options?.onCompactStart?.(); + const compactResult = compactConversation( + pruneResult.prunedCount > 0 ? pruneResult.messages : messages, + config, + ); + + if (compactResult.result.compacted) { + options?.onCompactComplete?.(compactResult.result.tokensSaved); + } + + const totalSaved = pruneResult.tokensSaved + compactResult.result.tokensSaved; + + return { + messages: compactResult.messages, + compacted: compactResult.result.compacted, + pruned: pruneResult.prunedCount > 0, + tokensSaved: totalSaved, + }; +}; + +/** + * Create a compaction check middleware for the agent loop + */ +export const createCompactionMiddleware = ( + modelId?: string, +): { + shouldCompact: (messages: Message[]) => boolean; + compact: ( + messages: Message[], + ) => Promise<{ messages: Message[]; summary: string }>; +} => { + return { + shouldCompact: (messages: Message[]) => isContextOverflow(messages, modelId), + + compact: async (messages: Message[]) => { + // Notify UI that compaction is starting + appStore.setIsCompacting(true); + + try { + const result = await performSessionCompaction(messages, modelId, { + onPruneStart: () => { + appStore.setThinkingMessage("Pruning old tool outputs..."); + }, + onPruneComplete: (count, saved) => { + appStore.addLog({ + type: "system", + content: `Pruned ${count} tool outputs (${saved.toLocaleString()} tokens)`, + }); + }, + onCompactStart: () => { + appStore.setThinkingMessage(TOKEN_MESSAGES.COMPACTION_STARTING); + }, + onCompactComplete: (saved) => { + appStore.addLog({ + type: "system", + content: TOKEN_MESSAGES.COMPACTION_COMPLETE(saved), + }); + }, + }); + + // Build summary + const parts: string[] = []; + if (result.pruned) { + parts.push("pruned old outputs"); + } + if (result.compacted) { + parts.push("compacted conversation"); + } + const summary = + parts.length > 0 + ? `Context management: ${parts.join(", ")} (${result.tokensSaved.toLocaleString()} tokens saved)` + : ""; + + return { + messages: result.messages, + summary, + }; + } finally { + appStore.setIsCompacting(false); + appStore.setThinkingMessage(null); + } + }, + }; +}; + +/** + * Get compaction status for display + */ +export const getCompactionStatus = ( + messages: Message[], + modelId?: string, +): { + currentTokens: number; + maxTokens: number; + usagePercent: number; + needsCompaction: boolean; +} => { + const contextSize = modelId + ? getModelContextSize(modelId) + : DEFAULT_CONTEXT_SIZE; + const currentTokens = estimateMessagesTokens(messages); + const maxTokens = contextSize.input; + const usagePercent = maxTokens > 0 ? (currentTokens / maxTokens) * 100 : 0; + + return { + currentTokens, + maxTokens, + usagePercent, + needsCompaction: isContextOverflow(messages, modelId), + }; +}; diff --git a/src/services/skill-loader.ts b/src/services/skill-loader.ts new file mode 100644 index 0000000..9b78401 --- /dev/null +++ b/src/services/skill-loader.ts @@ -0,0 +1,435 @@ +/** + * Skill Loader Service + * + * Parses SKILL.md files with frontmatter and body content. + * Supports progressive disclosure with 3 loading levels. + */ + +import fs from "fs/promises"; +import { join } from "path"; +import { + SKILL_FILE, + SKILL_DIRS, + SKILL_DEFAULTS, + SKILL_ERRORS, + SKILL_REQUIRED_FIELDS, + SKILL_LOADING, +} from "@constants/skills"; +import type { + SkillDefinition, + SkillMetadata, + SkillFrontmatter, + ParsedSkillFile, + SkillExample, + SkillLoadLevel, +} from "@/types/skills"; + +// ============================================================================ +// Frontmatter Parsing +// ============================================================================ + +/** + * Parse YAML-like frontmatter from SKILL.md content + */ +const parseFrontmatter = (content: string): { frontmatter: string; body: string } => { + const delimiter = SKILL_FILE.FRONTMATTER_DELIMITER; + const lines = content.split("\n"); + + if (lines[0]?.trim() !== delimiter) { + return { frontmatter: "", body: content }; + } + + let endIndex = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i]?.trim() === delimiter) { + endIndex = i; + break; + } + } + + if (endIndex === -1) { + return { frontmatter: "", body: content }; + } + + const frontmatter = lines.slice(1, endIndex).join("\n"); + const body = lines.slice(endIndex + 1).join("\n").trim(); + + return { frontmatter, body }; +}; + +/** + * Parse simple YAML-like frontmatter to object + * Supports: strings, arrays (- item), booleans + */ +const parseYamlLike = (yaml: string): Record => { + const result: Record = {}; + const lines = yaml.split("\n"); + let currentKey: string | null = null; + let currentArray: string[] | null = null; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + + // Array item + if (trimmed.startsWith("- ") && currentKey) { + const value = trimmed.slice(2).trim(); + if (!currentArray) { + currentArray = []; + } + // Remove quotes if present + const unquoted = value.replace(/^["']|["']$/g, ""); + currentArray.push(unquoted); + result[currentKey] = currentArray; + continue; + } + + // Key-value pair + const colonIndex = trimmed.indexOf(":"); + if (colonIndex > 0) { + // Save previous array if exists + if (currentArray && currentKey) { + result[currentKey] = currentArray; + } + currentArray = null; + + currentKey = trimmed.slice(0, colonIndex).trim(); + const value = trimmed.slice(colonIndex + 1).trim(); + + if (!value) { + // Empty value, might be followed by array + continue; + } + + // Parse value type + const parsed = parseValue(value); + result[currentKey] = parsed; + } + } + + return result; +}; + +/** + * Parse a single YAML value + */ +const parseValue = (value: string): string | boolean | number => { + // Boolean + if (value === "true") return true; + if (value === "false") return false; + + // Number + const num = Number(value); + if (!isNaN(num) && value !== "") return num; + + // String (remove quotes if present) + return value.replace(/^["']|["']$/g, ""); +}; + +/** + * Validate required fields in frontmatter + */ +const validateFrontmatter = ( + data: Record, + filePath: string, +): SkillFrontmatter => { + for (const field of SKILL_REQUIRED_FIELDS) { + if (!(field in data) || data[field] === undefined || data[field] === "") { + throw new Error(SKILL_ERRORS.MISSING_REQUIRED_FIELD(field, filePath)); + } + } + + // Validate triggers is an array + if (!Array.isArray(data.triggers)) { + throw new Error(SKILL_ERRORS.MISSING_REQUIRED_FIELD("triggers (array)", filePath)); + } + + return { + id: String(data.id), + name: String(data.name), + description: String(data.description), + version: data.version ? String(data.version) : undefined, + triggers: data.triggers as string[], + triggerType: data.triggerType as SkillFrontmatter["triggerType"], + autoTrigger: typeof data.autoTrigger === "boolean" ? data.autoTrigger : undefined, + requiredTools: Array.isArray(data.requiredTools) + ? (data.requiredTools as string[]) + : undefined, + tags: Array.isArray(data.tags) ? (data.tags as string[]) : undefined, + }; +}; + +// ============================================================================ +// Example Parsing +// ============================================================================ + +/** + * Parse examples from skill body + * Examples are in format: + * ## Examples + * ### Example 1 + * Input: ... + * Output: ... + */ +const parseExamples = (body: string): SkillExample[] => { + const examples: SkillExample[] = []; + const exampleSection = body.match(/## Examples([\s\S]*?)(?=##[^#]|$)/i); + + if (!exampleSection) return examples; + + const content = exampleSection[1]; + const exampleBlocks = content.split(/### /); + + for (const block of exampleBlocks) { + if (!block.trim()) continue; + + const inputMatch = block.match(/Input:\s*([\s\S]*?)(?=Output:|$)/i); + const outputMatch = block.match(/Output:\s*([\s\S]*?)(?=###|$)/i); + + if (inputMatch && outputMatch) { + const descMatch = block.match(/^([^\n]+)/); + examples.push({ + input: inputMatch[1].trim(), + output: outputMatch[1].trim(), + description: descMatch ? descMatch[1].trim() : undefined, + }); + } + } + + return examples; +}; + +// ============================================================================ +// File Loading +// ============================================================================ + +/** + * Load and parse a SKILL.md file + */ +export const loadSkillFile = async (filePath: string): Promise => { + try { + const stat = await fs.stat(filePath); + if (stat.size > SKILL_LOADING.MAX_FILE_SIZE_BYTES) { + throw new Error(`Skill file too large: ${filePath}`); + } + + const content = await fs.readFile(filePath, SKILL_FILE.ENCODING); + const { frontmatter, body } = parseFrontmatter(content); + + if (!frontmatter) { + throw new Error(SKILL_ERRORS.INVALID_FRONTMATTER(filePath)); + } + + const data = parseYamlLike(frontmatter); + const validatedFrontmatter = validateFrontmatter(data, filePath); + const examples = parseExamples(body); + + return { + frontmatter: validatedFrontmatter, + body, + examples: examples.length > 0 ? examples : undefined, + filePath, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(SKILL_ERRORS.LOAD_FAILED(filePath, message)); + } +}; + +/** + * Convert parsed skill file to SkillMetadata (Level 1) + */ +export const toSkillMetadata = (parsed: ParsedSkillFile): SkillMetadata => ({ + id: parsed.frontmatter.id, + name: parsed.frontmatter.name, + description: parsed.frontmatter.description, + version: parsed.frontmatter.version ?? SKILL_DEFAULTS.VERSION, + triggers: parsed.frontmatter.triggers, + triggerType: parsed.frontmatter.triggerType ?? SKILL_DEFAULTS.TRIGGER_TYPE, + autoTrigger: parsed.frontmatter.autoTrigger ?? SKILL_DEFAULTS.AUTO_TRIGGER, + requiredTools: parsed.frontmatter.requiredTools ?? SKILL_DEFAULTS.REQUIRED_TOOLS, + tags: parsed.frontmatter.tags, +}); + +/** + * Convert parsed skill file to full SkillDefinition (Level 3) + */ +export const toSkillDefinition = (parsed: ParsedSkillFile): SkillDefinition => { + const metadata = toSkillMetadata(parsed); + + // Extract system prompt and instructions from body + const { systemPrompt, instructions } = parseSkillBody(parsed.body); + + return { + ...metadata, + systemPrompt, + instructions, + examples: parsed.examples, + loadedAt: Date.now(), + }; +}; + +/** + * Parse skill body to extract system prompt and instructions + */ +const parseSkillBody = (body: string): { systemPrompt: string; instructions: string } => { + // Look for ## System Prompt section + const systemPromptMatch = body.match( + /## System Prompt([\s\S]*?)(?=## Instructions|## Examples|$)/i, + ); + + // Look for ## Instructions section + const instructionsMatch = body.match( + /## Instructions([\s\S]*?)(?=## Examples|## System Prompt|$)/i, + ); + + // If no sections found, use the whole body as instructions + const systemPrompt = systemPromptMatch ? systemPromptMatch[1].trim() : ""; + const instructions = instructionsMatch ? instructionsMatch[1].trim() : body.trim(); + + return { systemPrompt, instructions }; +}; + +// ============================================================================ +// Directory Scanning +// ============================================================================ + +/** + * Find all SKILL.md files in a directory + */ +export const findSkillFiles = async (dir: string): Promise => { + const skillFiles: string[] = []; + + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + + if (entry.isDirectory()) { + // Check for SKILL.md in subdirectory + const skillPath = join(fullPath, SKILL_FILE.NAME); + try { + await fs.access(skillPath); + skillFiles.push(skillPath); + } catch { + // No SKILL.md in this directory + } + } else if (entry.name === SKILL_FILE.NAME) { + skillFiles.push(fullPath); + } + } + } catch { + // Directory doesn't exist or isn't accessible + } + + return skillFiles; +}; + +/** + * Load all skills from a directory + */ +export const loadSkillsFromDirectory = async ( + dir: string, + level: SkillLoadLevel = "metadata", +): Promise => { + const skillFiles = await findSkillFiles(dir); + const skills: SkillDefinition[] = []; + + for (const filePath of skillFiles) { + try { + const parsed = await loadSkillFile(filePath); + + if (level === "metadata") { + // Only load metadata, but cast to SkillDefinition for uniform handling + const metadata = toSkillMetadata(parsed); + skills.push({ + ...metadata, + systemPrompt: "", + instructions: "", + }); + } else { + skills.push(toSkillDefinition(parsed)); + } + } catch (error) { + // Log error but continue loading other skills + console.error( + `Failed to load skill: ${filePath}`, + error instanceof Error ? error.message : error, + ); + } + } + + return skills; +}; + +/** + * Load all skills from all skill directories + */ +export const loadAllSkills = async ( + level: SkillLoadLevel = "metadata", +): Promise => { + const allSkills: SkillDefinition[] = []; + const dirs = [SKILL_DIRS.BUILTIN, SKILL_DIRS.USER]; + + // Add project skills if we're in a project directory + const projectSkillsDir = join(process.cwd(), SKILL_DIRS.PROJECT); + + try { + await fs.access(projectSkillsDir); + dirs.push(projectSkillsDir); + } catch { + // No project skills directory + } + + for (const dir of dirs) { + const skills = await loadSkillsFromDirectory(dir, level); + allSkills.push(...skills); + } + + // Deduplicate by ID (later ones override earlier) + const skillMap = new Map(); + for (const skill of allSkills) { + skillMap.set(skill.id, skill); + } + + return Array.from(skillMap.values()); +}; + +/** + * Load a specific skill by ID + */ +export const loadSkillById = async ( + id: string, + level: SkillLoadLevel = "full", +): Promise => { + const dirs = [SKILL_DIRS.BUILTIN, SKILL_DIRS.USER]; + const projectSkillsDir = join(process.cwd(), SKILL_DIRS.PROJECT); + + try { + await fs.access(projectSkillsDir); + dirs.push(projectSkillsDir); + } catch { + // No project skills directory + } + + // Search in reverse order (project > user > builtin) + for (const dir of dirs.reverse()) { + const skillPath = join(dir, id, SKILL_FILE.NAME); + + try { + await fs.access(skillPath); + const parsed = await loadSkillFile(skillPath); + + if (parsed.frontmatter.id === id) { + return level === "metadata" + ? { ...toSkillMetadata(parsed), systemPrompt: "", instructions: "" } + : toSkillDefinition(parsed); + } + } catch { + // Skill not found in this directory + } + } + + return null; +}; diff --git a/src/services/skill-registry.ts b/src/services/skill-registry.ts new file mode 100644 index 0000000..538b1a5 --- /dev/null +++ b/src/services/skill-registry.ts @@ -0,0 +1,407 @@ +/** + * Skill Registry Service + * + * Manages skill registration, matching, and invocation. + * Uses progressive disclosure to load skills on demand. + */ + +import { + SKILL_MATCHING, + SKILL_LOADING, + SKILL_ERRORS, +} from "@constants/skills"; +import { + loadAllSkills, + loadSkillById, +} from "@services/skill-loader"; +import type { + SkillDefinition, + SkillMatch, + SkillContext, + SkillExecutionResult, + SkillRegistryState, +} from "@/types/skills"; + +// ============================================================================ +// State Management +// ============================================================================ + +let registryState: SkillRegistryState = { + skills: new Map(), + lastLoadedAt: null, + loadErrors: [], +}; + +/** + * Get current registry state + */ +export const getRegistryState = (): SkillRegistryState => ({ + skills: new Map(registryState.skills), + lastLoadedAt: registryState.lastLoadedAt, + loadErrors: [...registryState.loadErrors], +}); + +/** + * Check if cache is stale + */ +const isCacheStale = (): boolean => { + if (!registryState.lastLoadedAt) return true; + return Date.now() - registryState.lastLoadedAt > SKILL_LOADING.CACHE_TTL_MS; +}; + +// ============================================================================ +// Skill Registration +// ============================================================================ + +/** + * Initialize skill registry with all available skills + */ +export const initializeRegistry = async (): Promise => { + try { + const skills = await loadAllSkills("metadata"); + registryState.skills.clear(); + registryState.loadErrors = []; + + for (const skill of skills) { + registryState.skills.set(skill.id, skill); + } + + registryState.lastLoadedAt = Date.now(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + registryState.loadErrors.push(message); + } +}; + +/** + * Register a skill manually + */ +export const registerSkill = (skill: SkillDefinition): void => { + registryState.skills.set(skill.id, skill); +}; + +/** + * Unregister a skill + */ +export const unregisterSkill = (skillId: string): boolean => { + return registryState.skills.delete(skillId); +}; + +/** + * Get a skill by ID + */ +export const getSkill = (skillId: string): SkillDefinition | undefined => { + return registryState.skills.get(skillId); +}; + +/** + * Get all registered skills + */ +export const getAllSkills = (): SkillDefinition[] => { + return Array.from(registryState.skills.values()); +}; + +/** + * Refresh registry if cache is stale + */ +export const refreshIfNeeded = async (): Promise => { + if (isCacheStale()) { + await initializeRegistry(); + } +}; + +// ============================================================================ +// Skill Matching +// ============================================================================ + +/** + * Calculate string similarity using Levenshtein distance + */ +const calculateSimilarity = (a: string, b: string): number => { + const aLower = a.toLowerCase(); + const bLower = b.toLowerCase(); + + if (aLower === bLower) return 1; + if (aLower.includes(bLower) || bLower.includes(aLower)) { + return 0.8; + } + + // Simple word overlap for fuzzy matching + const aWords = new Set(aLower.split(/\s+/)); + const bWords = new Set(bLower.split(/\s+/)); + const intersection = [...aWords].filter((word) => bWords.has(word)); + + if (intersection.length === 0) return 0; + + return intersection.length / Math.max(aWords.size, bWords.size); +}; + +/** + * Check if input matches a trigger pattern + */ +const matchTrigger = ( + input: string, + trigger: string, +): { matches: boolean; confidence: number } => { + const inputLower = input.toLowerCase().trim(); + const triggerLower = trigger.toLowerCase().trim(); + + // Exact match (command style) + if (inputLower === triggerLower) { + return { matches: true, confidence: 1.0 }; + } + + // Command prefix match + if (trigger.startsWith(SKILL_MATCHING.COMMAND_PREFIX)) { + if (inputLower.startsWith(triggerLower)) { + return { matches: true, confidence: 0.95 }; + } + } + + // Input starts with trigger + if (inputLower.startsWith(triggerLower)) { + return { matches: true, confidence: 0.9 }; + } + + // Fuzzy match + const similarity = calculateSimilarity(inputLower, triggerLower); + if (similarity >= SKILL_MATCHING.FUZZY_THRESHOLD) { + return { matches: true, confidence: similarity }; + } + + return { matches: false, confidence: 0 }; +}; + +/** + * Find matching skills for user input + */ +export const findMatchingSkills = async (input: string): Promise => { + await refreshIfNeeded(); + + const matches: SkillMatch[] = []; + const inputLower = input.toLowerCase().trim(); + + for (const skill of registryState.skills.values()) { + let bestMatch: { trigger: string; confidence: number } | null = null; + + for (const trigger of skill.triggers) { + const result = matchTrigger(inputLower, trigger); + + if (result.matches) { + if (!bestMatch || result.confidence > bestMatch.confidence) { + bestMatch = { trigger, confidence: result.confidence }; + } + } + } + + if (bestMatch && bestMatch.confidence >= SKILL_MATCHING.MIN_CONFIDENCE) { + matches.push({ + skill, + confidence: bestMatch.confidence, + matchedTrigger: bestMatch.trigger, + matchType: skill.triggerType, + }); + } + } + + // Sort by confidence (highest first) + matches.sort((a, b) => b.confidence - a.confidence); + + return matches; +}; + +/** + * Find the best matching skill for input + */ +export const findBestMatch = async (input: string): Promise => { + const matches = await findMatchingSkills(input); + return matches.length > 0 ? matches[0] : null; +}; + +/** + * Check if input matches a command pattern (starts with /) + */ +export const isCommandInput = (input: string): boolean => { + return input.trim().startsWith(SKILL_MATCHING.COMMAND_PREFIX); +}; + +/** + * Extract command name from input + */ +export const extractCommandName = (input: string): string | null => { + if (!isCommandInput(input)) return null; + + const trimmed = input.trim(); + const spaceIndex = trimmed.indexOf(" "); + + if (spaceIndex === -1) { + return trimmed.slice(1); // Remove leading / + } + + return trimmed.slice(1, spaceIndex); +}; + +// ============================================================================ +// Skill Execution +// ============================================================================ + +/** + * Load full skill definition for execution + */ +export const loadSkillForExecution = async ( + skillId: string, +): Promise => { + // Check if already fully loaded + const existing = registryState.skills.get(skillId); + if (existing && existing.systemPrompt && existing.instructions) { + return existing; + } + + // Load full definition + const fullSkill = await loadSkillById(skillId, "full"); + if (fullSkill) { + registryState.skills.set(skillId, fullSkill); + return fullSkill; + } + + return null; +}; + +/** + * Build prompt with skill context + */ +export const buildSkillPrompt = ( + skill: SkillDefinition, + context: SkillContext, +): string => { + const parts: string[] = []; + + // Add system prompt if present + if (skill.systemPrompt) { + parts.push(skill.systemPrompt); + } + + // Add instructions + if (skill.instructions) { + parts.push("## Instructions\n" + skill.instructions); + } + + // Add examples if present + if (skill.examples && skill.examples.length > 0) { + parts.push("## Examples"); + for (const example of skill.examples) { + if (example.description) { + parts.push(`### ${example.description}`); + } + parts.push(`Input: ${example.input}`); + parts.push(`Output: ${example.output}`); + } + } + + // Add context + parts.push("## Context"); + parts.push(`Working directory: ${context.workingDir}`); + if (context.gitBranch) { + parts.push(`Git branch: ${context.gitBranch}`); + } + parts.push(`User input: ${context.userInput}`); + + return parts.join("\n\n"); +}; + +/** + * Execute a skill + */ +export const executeSkill = async ( + skillId: string, + context: SkillContext, +): Promise => { + try { + const skill = await loadSkillForExecution(skillId); + + if (!skill) { + return { + success: false, + skillId, + prompt: "", + error: SKILL_ERRORS.NOT_FOUND(skillId), + }; + } + + const prompt = buildSkillPrompt(skill, context); + + return { + success: true, + skillId, + prompt, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + success: false, + skillId, + prompt: "", + error: SKILL_ERRORS.EXECUTION_FAILED(skillId, message), + }; + } +}; + +/** + * Execute skill from user input (auto-detect and execute) + */ +export const executeFromInput = async ( + input: string, + context: Omit, +): Promise => { + const match = await findBestMatch(input); + + if (!match) { + return null; + } + + return executeSkill(match.skill.id, { + ...context, + userInput: input, + }); +}; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Get skills that can auto-trigger + */ +export const getAutoTriggerSkills = (): SkillDefinition[] => { + return Array.from(registryState.skills.values()).filter( + (skill) => skill.autoTrigger, + ); +}; + +/** + * Get skills by tag + */ +export const getSkillsByTag = (tag: string): SkillDefinition[] => { + return Array.from(registryState.skills.values()).filter( + (skill) => skill.tags?.includes(tag), + ); +}; + +/** + * Get available command completions + */ +export const getCommandCompletions = (partial: string): string[] => { + const commands: string[] = []; + + for (const skill of registryState.skills.values()) { + for (const trigger of skill.triggers) { + if (trigger.startsWith(SKILL_MATCHING.COMMAND_PREFIX)) { + if (trigger.toLowerCase().startsWith(partial.toLowerCase())) { + commands.push(trigger); + } + } + } + } + + return commands.sort(); +}; diff --git a/src/skills/commit/SKILL.md b/src/skills/commit/SKILL.md new file mode 100644 index 0000000..719a7b4 --- /dev/null +++ b/src/skills/commit/SKILL.md @@ -0,0 +1,107 @@ +--- +id: commit +name: Commit Changes +description: Create a well-crafted git commit message and commit staged changes +version: 1.0.0 +triggers: + - /commit + - commit changes + - commit this + - git commit + - make a commit +triggerType: command +autoTrigger: false +requiredTools: + - bash +tags: + - git + - workflow +--- + +## System Prompt + +You are a commit message expert. You analyze staged changes and craft clear, conventional commit messages that explain the "why" behind changes. + +## Instructions + +Follow this process to create a commit: + +### Step 1: Analyze Changes +Run these commands to understand what's being committed: +1. `git status` - See staged and unstaged files +2. `git diff --cached` - View staged changes in detail +3. `git log --oneline -5` - Check recent commit style + +### Step 2: Determine Commit Type +Based on the changes, identify the commit type: +- **feat**: New feature +- **fix**: Bug fix +- **docs**: Documentation only +- **style**: Formatting, no code change +- **refactor**: Code change that neither fixes a bug nor adds a feature +- **perf**: Performance improvement +- **test**: Adding/updating tests +- **chore**: Maintenance tasks + +### Step 3: Write Commit Message +Format: `(): ` + +Rules: +- Subject line max 50 characters +- Use imperative mood ("Add feature" not "Added feature") +- No period at the end +- Body explains "why" not "what" (the diff shows what) + +### Step 4: Create Commit +Use bash to run: +```bash +git commit -m "(): + +" +``` + +### Important Notes +- Never use `--no-verify` unless explicitly requested +- Never commit sensitive files (.env, credentials, secrets) +- If no changes are staged, inform the user and offer to stage them +- Ask for confirmation before committing if unsure about scope + +## Examples + +### Fix a bug +Input: /commit +Output: +``` +Analyzing staged changes... + +Changes detected: +- Modified: src/utils/date.ts (fixed timezone handling) + +Suggested commit: +fix(utils): handle UTC offset in date formatting + +The date formatter was ignoring timezone offsets, causing +incorrect display for users in non-UTC timezones. + +Proceed with commit? [Y/n] +``` + +### Add new feature +Input: commit changes +Output: +``` +Analyzing staged changes... + +Changes detected: +- Added: src/features/export/index.ts +- Added: src/features/export/csv.ts +- Modified: src/types/index.ts + +Suggested commit: +feat(export): add CSV export functionality + +Users can now export their data as CSV files. +Supports custom delimiters and header configuration. + +Proceed with commit? [Y/n] +``` diff --git a/src/skills/explain/SKILL.md b/src/skills/explain/SKILL.md new file mode 100644 index 0000000..25caeb6 --- /dev/null +++ b/src/skills/explain/SKILL.md @@ -0,0 +1,217 @@ +--- +id: explain +name: Explain Code +description: Provide clear, detailed explanations of code, architecture, or concepts +version: 1.0.0 +triggers: + - /explain + - explain this + - explain code + - what does this do + - how does this work +triggerType: command +autoTrigger: false +requiredTools: + - read + - grep + - glob +tags: + - learning + - documentation +--- + +## System Prompt + +You are a patient, thorough technical educator. You explain code and concepts at the appropriate level for the user, using analogies, examples, and progressive disclosure. You read the actual code before explaining and reference specific files and line numbers. + +## Instructions + +Follow this explanation process: + +### Step 1: Identify What to Explain +1. If a file path is given, read that file +2. If a concept is mentioned, search for related code +3. If unclear, ask the user to clarify + +### Step 2: Read and Understand +1. Read the relevant file(s) completely +2. Identify the key components and their relationships +3. Note any patterns, dependencies, or unusual constructs + +### Step 3: Structure the Explanation + +Use this format: + +``` +## Overview +[1-2 sentence summary of what this code does] + +## Key Concepts +[List the main concepts someone needs to understand] + +## How It Works +[Step-by-step explanation of the code flow] + +## Code Walkthrough +[Annotated code snippets with explanations] + +## Connections +[How this relates to other parts of the codebase] + +## Summary +[Recap of the key points] +``` + +### Explanation Guidelines + +1. **Start Simple**: Begin with a high-level overview before diving into details +2. **Use Analogies**: Relate complex concepts to familiar things +3. **Show, Don't Just Tell**: Include relevant code snippets +4. **Reference Locations**: Always cite `file:line` for code references +5. **Progressive Disclosure**: Start with "what" then go to "how" then "why" +6. **Check Understanding**: End with a summary that reinforces key points + +### Depth Levels + +Adjust explanation depth based on user's apparent level: + +- **Beginner**: More analogies, simpler terms, more context +- **Intermediate**: Balance of concepts and implementation details +- **Advanced**: Focus on nuances, edge cases, design decisions + +## Examples + +### Explain a Function +Input: explain this function in src/utils/debounce.ts +Output: +``` +## Overview + +The `debounce` function delays executing a function until a certain amount of time has passed since it was last called. Think of it like an elevator: it waits for people to stop entering before closing the doors. + +## Key Concepts + +- **Debouncing**: Grouping multiple rapid calls into one +- **Closures**: The returned function "remembers" the timer +- **setTimeout/clearTimeout**: JavaScript's timing mechanisms + +## How It Works + +1. User calls the debounced function +2. Previous timer (if any) is cancelled +3. New timer is set for the delay period +4. If no new calls come in, the original function executes +5. If a new call comes in, repeat from step 2 + +## Code Walkthrough + +\`\`\`typescript +// src/utils/debounce.ts:5-20 + +export function debounce any>( + fn: T, // The function to debounce + delay: number // How long to wait (ms) +): (...args: Parameters) => void { + let timeoutId: ReturnType | null = null; + + // Return a new function that wraps the original + return (...args: Parameters) => { + // Cancel any pending execution + if (timeoutId) { + clearTimeout(timeoutId); + } + + // Schedule new execution + timeoutId = setTimeout(() => { + fn(...args); // Call original with latest args + timeoutId = null; // Clean up + }, delay); + }; +} +\`\`\` + +## Connections + +This utility is used by: +- `src/components/SearchInput.tsx:23` - Debounce search API calls +- `src/hooks/useWindowResize.ts:8` - Debounce resize handlers + +## Summary + +`debounce` prevents a function from being called too frequently by waiting until calls stop coming in. It's essential for performance when handling rapid events like typing or resizing. +``` + +### Explain Architecture +Input: how does authentication work in this project? +Output: +``` +## Overview + +Authentication uses JWT (JSON Web Tokens) with a refresh token flow. Users log in once and receive two tokens: a short-lived access token for API calls and a long-lived refresh token for getting new access tokens. + +## Key Concepts + +- **JWT**: Self-contained tokens with encoded user data +- **Access Token**: Short-lived (15 min), used for API authorization +- **Refresh Token**: Long-lived (7 days), used to get new access tokens +- **HTTP-only Cookies**: Secure storage immune to XSS + +## How It Works + +1. **Login** (`src/auth/login.ts:15`) + - User submits credentials + - Server validates against database + - Server generates access + refresh tokens + - Tokens stored in HTTP-only cookies + +2. **API Request** (`src/middleware/auth.ts:8`) + - Client sends request with cookies + - Middleware extracts access token + - JWT is verified and decoded + - User data attached to request + +3. **Token Refresh** (`src/auth/refresh.ts:22`) + - Access token expires (15 min) + - Client automatically calls /refresh + - Server validates refresh token + - New access token issued + +4. **Logout** (`src/auth/logout.ts:10`) + - Refresh token invalidated in database + - Cookies cleared + +## Code Walkthrough + +\`\`\`typescript +// src/middleware/auth.ts:8-25 + +export const requireAuth = async (req, res, next) => { + const token = req.cookies.accessToken; + + if (!token) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = decoded; // Attach user to request + next(); + } catch (err) { + if (err.name === 'TokenExpiredError') { + return res.status(401).json({ error: 'Token expired' }); + } + return res.status(401).json({ error: 'Invalid token' }); + } +}; +\`\`\` + +## Connections + +- **User Model**: `src/models/User.ts` - Password hashing, user data +- **Protected Routes**: `src/routes/api/*.ts` - Use `requireAuth` middleware +- **Frontend**: `src/hooks/useAuth.ts` - Client-side auth state + +## Summary + +Authentication is handled through JWTs stored in HTTP-only cookies. The access token is short-lived for security, while the refresh token enables seamless re-authentication. The middleware pattern keeps protected routes clean and centralized. +``` diff --git a/src/skills/feature-dev/SKILL.md b/src/skills/feature-dev/SKILL.md new file mode 100644 index 0000000..ef40f86 --- /dev/null +++ b/src/skills/feature-dev/SKILL.md @@ -0,0 +1,254 @@ +--- +id: feature-dev +name: Feature Development +description: Guided 7-phase workflow for implementing new features with checkpoints +version: 1.0.0 +triggers: + - /feature + - /feature-dev + - implement feature + - new feature + - build feature + - develop feature +triggerType: command +autoTrigger: false +requiredTools: + - read + - write + - edit + - bash + - glob + - grep +tags: + - workflow + - feature + - development +--- + +## System Prompt + +You are CodeTyper in Feature Development mode - a structured approach to implementing new features. You guide the user through a 7-phase workflow with checkpoints for approval at critical stages. + +## Instructions + +### The 7 Phases + +1. **UNDERSTAND** - Clarify requirements before coding +2. **EXPLORE** - Search codebase for relevant patterns +3. **PLAN** - Design the implementation approach +4. **IMPLEMENT** - Write the code changes +5. **VERIFY** - Run tests and validate +6. **REVIEW** - Self-review for quality +7. **FINALIZE** - Commit and complete + +### Phase 1: UNDERSTAND + +Goal: Fully understand what needs to be built. + +Tasks: +- Analyze the feature request +- Identify unclear requirements +- Ask clarifying questions +- Document requirements + +Output: +- List of requirements +- Assumptions made +- Questions for user (if any) + +**Checkpoint:** Confirm requirements with user + +### Phase 2: EXPLORE + +Goal: Understand the existing codebase. + +Tasks: +- Search for related code patterns +- Find files that need modification +- Identify conventions to follow +- Find similar implementations + +Use parallel searches for efficiency. + +Output: +- Relevant files list +- Patterns to follow +- Dependencies identified + +### Phase 3: PLAN + +Goal: Create a detailed implementation plan. + +Tasks: +- Design solution architecture +- List files to create/modify/delete +- Define implementation order +- Identify risks +- Plan test strategy + +Output: +- Step-by-step plan +- File change list +- Risk assessment +- Test plan + +**Checkpoint:** Get plan approval before implementing + +### Phase 4: IMPLEMENT + +Goal: Write the code according to plan. + +Tasks: +- Follow plan step by step +- Write clean, documented code +- Follow existing conventions +- Track all changes + +Guidelines: +- One step at a time +- Minimal focused changes +- Add comments for complex logic + +### Phase 5: VERIFY + +Goal: Ensure implementation works correctly. + +Tasks: +- Run existing tests +- Add new tests for feature +- Fix any failures +- Check for regressions + +**Checkpoint:** Review test results + +### Phase 6: REVIEW + +Goal: Quality review of implementation. + +Review for: +- Code clarity +- Error handling +- Edge cases +- Performance +- Security +- Documentation + +**Checkpoint:** Address critical findings + +### Phase 7: FINALIZE + +Goal: Complete the feature. + +Tasks: +- Create commit with good message +- Update documentation +- Clean up temporary files +- Summarize changes + +**Checkpoint:** Approve final commit + +## Examples + +### Simple Feature +Input: /feature add a dark mode toggle to settings + +Output: +``` +# Feature Development: Dark Mode Toggle + +## Phase 1: UNDERSTAND + +I'll help you implement a dark mode toggle. Let me understand the requirements: + +**Requirements identified:** +1. Add toggle switch in settings +2. Persist user preference +3. Apply dark theme when enabled + +**Questions:** +- Should it respect system preference by default? +- Where exactly in settings should it appear? + +[Awaiting user input...] +``` + +### Complex Feature +Input: implement feature for exporting data as CSV + +Output: +``` +# Feature Development: CSV Export + +## Phase 1: UNDERSTAND + +Analyzing the export feature request... + +**Requirements:** +1. Export user data to CSV format +2. Support custom column selection +3. Handle large datasets +4. Provide download mechanism + +**Assumptions:** +- Export from current view/filter +- UTF-8 encoding +- Include headers + +**Clarifications needed:** +- Which data entities should be exportable? +- Any size limits? +- Should it support scheduled exports? + +[Checkpoint: Please confirm requirements] + +--- + +## Phase 2: EXPLORE + +Searching for relevant code... + +**Found:** +- Export utilities: `src/utils/export.ts` +- Data fetching: `src/services/data-service.ts` +- Similar feature: PDF export at `src/features/pdf-export/` + +**Patterns to follow:** +- Use streaming for large files +- Progress callback pattern +- Error boundary wrapping + +[Proceeding to planning...] + +--- + +## Phase 3: PLAN + +**Implementation Plan** + +1. Create `src/features/csv-export/types.ts` + - Define export options interface + - Column configuration types + +2. Create `src/features/csv-export/formatter.ts` + - CSV formatting logic + - Handle special characters + +3. Create `src/features/csv-export/index.ts` + - Main export function + - Streaming support + +4. Modify `src/components/DataTable.tsx` + - Add export button + - Wire up export action + +**Risks:** +- Memory issues with large datasets → Use streaming +- Character encoding → Test with unicode + +**Test Strategy:** +- Unit tests for formatter +- Integration test for full export +- Edge case: empty data, special characters + +[Checkpoint: Approve plan to proceed?] +``` diff --git a/src/skills/review-pr/SKILL.md b/src/skills/review-pr/SKILL.md new file mode 100644 index 0000000..5e30fe4 --- /dev/null +++ b/src/skills/review-pr/SKILL.md @@ -0,0 +1,200 @@ +--- +id: review-pr +name: Review Pull Request +description: Perform a comprehensive code review on a pull request or set of changes +version: 1.0.0 +triggers: + - /review-pr + - /review + - review pr + - review this pr + - review pull request + - code review +triggerType: command +autoTrigger: false +requiredTools: + - bash + - read + - grep +tags: + - git + - review + - workflow +--- + +## System Prompt + +You are an expert code reviewer. You analyze changes thoroughly, looking for bugs, security issues, performance problems, and style inconsistencies. You provide constructive, specific feedback with concrete suggestions for improvement. + +## Instructions + +Follow this structured review process: + +### Step 1: Gather Context +1. Get the diff: `git diff main...HEAD` (or specified base branch) +2. List changed files: `git diff --name-only main...HEAD` +3. Check commit history: `git log --oneline main...HEAD` + +### Step 2: Review Categories + +Analyze changes in these categories (only report findings with ≥80% confidence): + +#### Security Review +- [ ] Input validation and sanitization +- [ ] SQL injection, XSS, command injection risks +- [ ] Authentication and authorization checks +- [ ] Sensitive data exposure +- [ ] Dependency vulnerabilities + +#### Logic Review +- [ ] Edge cases and boundary conditions +- [ ] Error handling completeness +- [ ] Race conditions and concurrency issues +- [ ] Null/undefined handling +- [ ] Type safety + +#### Performance Review +- [ ] Algorithmic complexity (O(n) analysis) +- [ ] Memory usage patterns +- [ ] Unnecessary re-renders (React) +- [ ] Database query efficiency +- [ ] Caching opportunities + +#### Style Review +- [ ] Naming conventions +- [ ] Code organization +- [ ] Documentation completeness +- [ ] Test coverage +- [ ] Consistency with codebase patterns + +### Step 3: Format Findings + +For each finding, provide: +``` +**[SEVERITY]** Category: Brief title + +📍 Location: `file:line` + +**Issue**: Description of the problem + +**Suggestion**: How to fix it + +**Example** (if applicable): +\`\`\` +// Before +problematic code + +// After +improved code +\`\`\` +``` + +Severity levels: +- 🔴 **CRITICAL**: Must fix before merge (security, data loss) +- 🟠 **WARNING**: Should fix (bugs, significant issues) +- 🟡 **SUGGESTION**: Consider fixing (improvements) +- 🟢 **NITPICK**: Optional (style, preferences) + +### Step 4: Summary + +Provide an overall assessment: +- Number of findings by severity +- Overall quality rating (1-5 stars) +- Recommendation: Approve / Request Changes / Comment + +## Examples + +### Security Finding +Input: /review-pr +Output: +``` +## Pull Request Review + +### Findings + +🔴 **CRITICAL** Security: SQL Injection vulnerability + +📍 Location: `src/db/users.ts:45` + +**Issue**: User input is directly interpolated into SQL query without parameterization. + +**Suggestion**: Use parameterized queries or an ORM. + +**Example**: +\`\`\`typescript +// Before +const query = `SELECT * FROM users WHERE id = ${userId}`; + +// After +const query = 'SELECT * FROM users WHERE id = $1'; +const result = await db.query(query, [userId]); +\`\`\` + +--- + +🟠 **WARNING** Logic: Missing null check + +📍 Location: `src/utils/format.ts:23` + +**Issue**: `user.name` accessed without null check, will throw if user is undefined. + +**Suggestion**: Add optional chaining or explicit null check. + +--- + +### Summary + +| Severity | Count | +|----------|-------| +| Critical | 1 | +| Warning | 1 | +| Suggestion | 0 | + +**Rating**: ⭐⭐ (2/5) + +**Recommendation**: 🔴 Request Changes + +The SQL injection vulnerability must be fixed before this can be merged. +``` + +### Clean PR +Input: review this pr +Output: +``` +## Pull Request Review + +### Findings + +🟡 **SUGGESTION** Style: Consider extracting magic number + +📍 Location: `src/config/limits.ts:12` + +**Issue**: The value `86400000` could be more readable. + +**Suggestion**: Extract to named constant. + +\`\`\`typescript +// Before +const CACHE_TTL = 86400000; + +// After +const MS_PER_DAY = 24 * 60 * 60 * 1000; +const CACHE_TTL = MS_PER_DAY; +\`\`\` + +--- + +### Summary + +| Severity | Count | +|----------|-------| +| Critical | 0 | +| Warning | 0 | +| Suggestion | 1 | + +**Rating**: ⭐⭐⭐⭐⭐ (5/5) + +**Recommendation**: ✅ Approve + +Clean implementation with good test coverage. The suggestion is optional. +``` diff --git a/src/tools/apply-patch/execute.ts b/src/tools/apply-patch/execute.ts new file mode 100644 index 0000000..61f4dd8 --- /dev/null +++ b/src/tools/apply-patch/execute.ts @@ -0,0 +1,366 @@ +/** + * Apply Patch Execution + * + * Applies unified diff patches to files with fuzzy matching and rollback support. + */ + +import fs from "fs/promises"; +import { dirname, join, isAbsolute } from "path"; +import { + PATCH_DEFAULTS, + PATCH_ERRORS, + PATCH_MESSAGES, + PATCH_TITLES, +} from "@constants/apply-patch"; +import { parsePatch, validatePatch, getTargetPath, reversePatch } from "@tools/apply-patch/parser"; +import { findHunkPosition, isHunkApplied, previewHunkApplication } from "@tools/apply-patch/matcher"; +import type { ApplyPatchParams } from "@tools/apply-patch/params"; +import type { + FilePatchResult, + HunkApplicationResult, + PatchRollback, + ParsedFilePatch, +} from "@/types/apply-patch"; +import type { ToolContext, ToolResult } from "@tools/types"; + +// Rollback storage (in-memory for session) +const rollbackStore: Map = new Map(); + +/** + * Execute the apply_patch tool + */ +export const executeApplyPatch = async ( + params: ApplyPatchParams, + ctx: ToolContext, +): Promise => { + try { + // Parse the patch + const parsedPatch = parsePatch(params.patch); + + // Validate the patch + const validation = validatePatch(parsedPatch); + if (!validation.valid) { + return { + success: false, + title: PATCH_TITLES.FAILED, + output: "", + error: validation.errors.join("\n"), + }; + } + + // Apply to each file + const results: FilePatchResult[] = []; + let totalPatched = 0; + let totalFailed = 0; + + for (let filePatch of parsedPatch.files) { + // Skip binary files + if (filePatch.isBinary) { + results.push({ + success: true, + filePath: getTargetPath(filePatch), + hunksApplied: 0, + hunksFailed: 0, + hunkResults: [], + error: PATCH_MESSAGES.SKIPPED_BINARY(getTargetPath(filePatch)), + }); + continue; + } + + // Reverse if requested + if (params.reverse) { + filePatch = reversePatch(filePatch); + } + + // Determine target file path + const targetPath = params.targetFile ?? getTargetPath(filePatch); + const absolutePath = isAbsolute(targetPath) + ? targetPath + : join(ctx.workingDir, targetPath); + + // Apply the file patch + const result = await applyFilePatch( + filePatch, + absolutePath, + { + fuzz: params.fuzz ?? PATCH_DEFAULTS.FUZZ, + dryRun: params.dryRun ?? false, + }, + ); + + results.push(result); + + if (result.success) { + totalPatched++; + } else { + totalFailed++; + } + } + + // Build output + const output = formatPatchResults(results, params.dryRun ?? false); + + // Determine overall success + const success = totalFailed === 0; + const title = params.dryRun + ? PATCH_TITLES.DRY_RUN + : totalFailed === 0 + ? PATCH_TITLES.SUCCESS(totalPatched) + : totalPatched > 0 + ? PATCH_TITLES.PARTIAL(totalPatched, totalFailed) + : PATCH_TITLES.FAILED; + + return { + success, + title, + output, + error: success ? undefined : `${totalFailed} file(s) failed to patch`, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + success: false, + title: PATCH_TITLES.FAILED, + output: "", + error: PATCH_ERRORS.PARSE_FAILED(message), + }; + } +}; + +/** + * Apply a patch to a single file + */ +const applyFilePatch = async ( + filePatch: ParsedFilePatch, + targetPath: string, + options: { fuzz: number; dryRun: boolean }, +): Promise => { + const hunkResults: HunkApplicationResult[] = []; + let currentContent: string; + let originalContent: string; + + try { + // Handle new files + if (filePatch.isNew) { + currentContent = ""; + originalContent = ""; + } else { + // Read original file + try { + currentContent = await fs.readFile(targetPath, "utf-8"); + originalContent = currentContent; + } catch { + return { + success: false, + filePath: targetPath, + hunksApplied: 0, + hunksFailed: filePatch.hunks.length, + hunkResults: [], + error: PATCH_ERRORS.FILE_NOT_FOUND(targetPath), + }; + } + } + + // Handle deleted files + if (filePatch.isDeleted) { + if (!options.dryRun) { + // Store rollback info + rollbackStore.set(targetPath, { + filePath: targetPath, + originalContent, + patchedContent: "", + timestamp: Date.now(), + }); + + await fs.unlink(targetPath); + } + + return { + success: true, + filePath: targetPath, + hunksApplied: 1, + hunksFailed: 0, + hunkResults: [ + { + success: true, + hunkIndex: 0, + appliedAt: 0, + }, + ], + newContent: "", + }; + } + + // Apply each hunk + let hunksApplied = 0; + let hunksFailed = 0; + + for (let i = 0; i < filePatch.hunks.length; i++) { + const hunk = filePatch.hunks[i]; + + // Check if already applied + if (isHunkApplied(currentContent, hunk, { fuzz: options.fuzz })) { + hunkResults.push({ + success: true, + hunkIndex: i, + appliedAt: hunk.oldStart - 1, + }); + hunksApplied++; + continue; + } + + // Find position with fuzzy matching + const position = findHunkPosition(currentContent, hunk, { fuzz: options.fuzz }); + + if (!position.found) { + hunkResults.push({ + success: false, + hunkIndex: i, + error: PATCH_ERRORS.FUZZY_MATCH_FAILED(i), + }); + hunksFailed++; + continue; + } + + // Apply the hunk + const preview = previewHunkApplication(currentContent, hunk, position.lineNumber); + + if (!preview.success) { + hunkResults.push({ + success: false, + hunkIndex: i, + error: preview.error ?? PATCH_ERRORS.HUNK_FAILED(i, "unknown"), + }); + hunksFailed++; + continue; + } + + currentContent = preview.preview.join("\n"); + hunksApplied++; + + hunkResults.push({ + success: true, + hunkIndex: i, + appliedAt: position.lineNumber, + fuzzyOffset: position.offset !== 0 ? position.offset : undefined, + }); + } + + // Write the file if not dry run + if (!options.dryRun && hunksApplied > 0) { + // Store rollback info + rollbackStore.set(targetPath, { + filePath: targetPath, + originalContent, + patchedContent: currentContent, + timestamp: Date.now(), + }); + + // Ensure directory exists + await fs.mkdir(dirname(targetPath), { recursive: true }); + + // Write patched content + await fs.writeFile(targetPath, currentContent, "utf-8"); + } + + return { + success: hunksFailed === 0, + filePath: targetPath, + hunksApplied, + hunksFailed, + hunkResults, + newContent: currentContent, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + success: false, + filePath: targetPath, + hunksApplied: 0, + hunksFailed: filePatch.hunks.length, + hunkResults, + error: PATCH_ERRORS.WRITE_FAILED(targetPath, message), + }; + } +}; + +/** + * Format patch results for output + */ +const formatPatchResults = ( + results: FilePatchResult[], + dryRun: boolean, +): string => { + const lines: string[] = []; + + for (const result of results) { + lines.push(`${result.success ? "✓" : "✗"} ${result.filePath}`); + + if (result.hunksApplied > 0 || result.hunksFailed > 0) { + lines.push( + ` ${result.hunksApplied} hunk(s) applied, ${result.hunksFailed} failed`, + ); + } + + // Show fuzzy offsets + for (const hunk of result.hunkResults) { + if (hunk.fuzzyOffset) { + lines.push( + ` ${PATCH_MESSAGES.FUZZY_APPLIED(hunk.hunkIndex, hunk.fuzzyOffset)}`, + ); + } + } + + if (result.error) { + lines.push(` Error: ${result.error}`); + } + } + + if (dryRun) { + lines.push(""); + lines.push("(dry run - no changes were made)"); + } else if (results.some((r) => r.success)) { + lines.push(""); + lines.push(PATCH_MESSAGES.ROLLBACK_AVAILABLE); + } + + return lines.join("\n"); +}; + +/** + * Rollback a patched file + */ +export const rollbackPatch = async (filePath: string): Promise => { + const rollback = rollbackStore.get(filePath); + if (!rollback) { + return false; + } + + try { + if (rollback.originalContent === "") { + // Was a new file, delete it + await fs.unlink(filePath); + } else { + await fs.writeFile(filePath, rollback.originalContent, "utf-8"); + } + + rollbackStore.delete(filePath); + return true; + } catch { + return false; + } +}; + +/** + * Get available rollbacks + */ +export const getAvailableRollbacks = (): string[] => { + return Array.from(rollbackStore.keys()); +}; + +/** + * Clear rollback history + */ +export const clearRollbacks = (): void => { + rollbackStore.clear(); +}; diff --git a/src/tools/apply-patch/index.ts b/src/tools/apply-patch/index.ts new file mode 100644 index 0000000..d779cec --- /dev/null +++ b/src/tools/apply-patch/index.ts @@ -0,0 +1,60 @@ +/** + * Apply Patch Tool + * + * Applies unified diff patches to files with fuzzy matching support. + */ + +import type { ToolDefinition } from "@tools/types"; +import { applyPatchParams } from "@tools/apply-patch/params"; +import { executeApplyPatch } from "@tools/apply-patch/execute"; + +export { applyPatchParams } from "@tools/apply-patch/params"; +export { executeApplyPatch, rollbackPatch, getAvailableRollbacks, clearRollbacks } from "@tools/apply-patch/execute"; +export { parsePatch, validatePatch, getTargetPath, reversePatch } from "@tools/apply-patch/parser"; +export { findHunkPosition, isHunkApplied, previewHunkApplication } from "@tools/apply-patch/matcher"; + +/** + * Tool description + */ +const APPLY_PATCH_DESCRIPTION = `Apply a unified diff patch to one or more files. + +Use this tool to: +- Apply changes from a diff/patch +- Update files based on patch content +- Preview changes before applying (dry run) + +Parameters: +- patch: The unified diff content (required) +- targetFile: Override the target file path (optional) +- dryRun: Preview without applying changes (default: false) +- fuzz: Context line tolerance 0-3 (default: 2) +- reverse: Apply patch in reverse to undo changes (default: false) + +The tool supports: +- Standard unified diff format (git diff, diff -u) +- Fuzzy context matching when lines have shifted +- Creating new files +- Deleting files +- Rollback on failure + +Example patch format: +\`\`\` +--- a/src/example.ts ++++ b/src/example.ts +@@ -10,6 +10,7 @@ function example() { + const a = 1; + const b = 2; ++ const c = 3; + return a + b; + } +\`\`\``; + +/** + * Apply patch tool definition + */ +export const applyPatchTool: ToolDefinition = { + name: "apply_patch", + description: APPLY_PATCH_DESCRIPTION, + parameters: applyPatchParams, + execute: executeApplyPatch, +}; diff --git a/src/tools/apply-patch/matcher.ts b/src/tools/apply-patch/matcher.ts new file mode 100644 index 0000000..923ab60 --- /dev/null +++ b/src/tools/apply-patch/matcher.ts @@ -0,0 +1,304 @@ +/** + * Fuzzy Matcher + * + * Finds patch context in target file with fuzzy matching support. + */ + +import { PATCH_DEFAULTS } from "@constants/apply-patch"; +import type { + PatchHunk, + FuzzyMatchResult, + ContextMatchOptions, +} from "@/types/apply-patch"; + +/** + * Default match options + */ +const DEFAULT_MATCH_OPTIONS: ContextMatchOptions = { + fuzz: PATCH_DEFAULTS.FUZZ, + ignoreWhitespace: PATCH_DEFAULTS.IGNORE_WHITESPACE, + ignoreCase: PATCH_DEFAULTS.IGNORE_CASE, +}; + +/** + * Normalize line for comparison + */ +const normalizeLine = ( + line: string, + options: ContextMatchOptions, +): string => { + let normalized = line; + + if (options.ignoreWhitespace) { + normalized = normalized.replace(/\s+/g, " ").trim(); + } + + if (options.ignoreCase) { + normalized = normalized.toLowerCase(); + } + + return normalized; +}; + +/** + * Extract context and deletion lines from hunk (lines that should exist in original) + */ +const extractOriginalLines = (hunk: PatchHunk): string[] => { + return hunk.lines + .filter((line) => line.type === "context" || line.type === "deletion") + .map((line) => line.content); +}; + +/** + * Check if lines match at a given position + */ +const checkMatchAtPosition = ( + fileLines: string[], + originalLines: string[], + startLine: number, + options: ContextMatchOptions, +): { matches: boolean; confidence: number } => { + let matchCount = 0; + let totalLines = originalLines.length; + + // Can't match if we don't have enough lines + if (startLine + originalLines.length > fileLines.length) { + return { matches: false, confidence: 0 }; + } + + for (let i = 0; i < originalLines.length; i++) { + const fileLine = normalizeLine(fileLines[startLine + i], options); + const patchLine = normalizeLine(originalLines[i], options); + + if (fileLine === patchLine) { + matchCount++; + } + } + + const confidence = totalLines > 0 ? matchCount / totalLines : 0; + + // Require at least (total - fuzz) lines to match + const requiredMatches = Math.max(1, totalLines - options.fuzz); + const matches = matchCount >= requiredMatches; + + return { matches, confidence }; +}; + +/** + * Find the best match position for a hunk in file content + */ +export const findHunkPosition = ( + fileContent: string, + hunk: PatchHunk, + options: Partial = {}, +): FuzzyMatchResult => { + const fullOptions: ContextMatchOptions = { + ...DEFAULT_MATCH_OPTIONS, + ...options, + }; + + const fileLines = fileContent.split("\n"); + const originalLines = extractOriginalLines(hunk); + + // If hunk has no lines to match, use the line number directly + if (originalLines.length === 0) { + const targetLine = Math.min(hunk.oldStart - 1, fileLines.length); + return { + found: true, + lineNumber: targetLine, + offset: 0, + confidence: 1, + }; + } + + // Expected position (0-indexed) + const expectedLine = hunk.oldStart - 1; + + // First, try exact position + const exactMatch = checkMatchAtPosition( + fileLines, + originalLines, + expectedLine, + fullOptions, + ); + + if (exactMatch.matches && exactMatch.confidence === 1) { + return { + found: true, + lineNumber: expectedLine, + offset: 0, + confidence: exactMatch.confidence, + }; + } + + // Search within fuzz range + const maxOffset = fullOptions.fuzz * PATCH_DEFAULTS.CONTEXT_LINES; + let bestMatch: FuzzyMatchResult | null = null; + + for (let offset = 1; offset <= maxOffset; offset++) { + // Try before expected position + const beforePos = expectedLine - offset; + if (beforePos >= 0) { + const beforeMatch = checkMatchAtPosition( + fileLines, + originalLines, + beforePos, + fullOptions, + ); + + if (beforeMatch.matches) { + if (!bestMatch || beforeMatch.confidence > bestMatch.confidence) { + bestMatch = { + found: true, + lineNumber: beforePos, + offset: -offset, + confidence: beforeMatch.confidence, + }; + } + } + } + + // Try after expected position + const afterPos = expectedLine + offset; + if (afterPos < fileLines.length) { + const afterMatch = checkMatchAtPosition( + fileLines, + originalLines, + afterPos, + fullOptions, + ); + + if (afterMatch.matches) { + if (!bestMatch || afterMatch.confidence > bestMatch.confidence) { + bestMatch = { + found: true, + lineNumber: afterPos, + offset: offset, + confidence: afterMatch.confidence, + }; + } + } + } + + // If we found a perfect match, stop searching + if (bestMatch && bestMatch.confidence === 1) { + break; + } + } + + // Return best match if found + if (bestMatch) { + return bestMatch; + } + + // If exact position had a partial match, return it + if (exactMatch.confidence > 0.5) { + return { + found: true, + lineNumber: expectedLine, + offset: 0, + confidence: exactMatch.confidence, + }; + } + + return { + found: false, + lineNumber: -1, + offset: 0, + confidence: 0, + }; +}; + +/** + * Check if a hunk is already applied (deletions are gone, additions are present) + */ +export const isHunkApplied = ( + fileContent: string, + hunk: PatchHunk, + options: Partial = {}, +): boolean => { + const fullOptions: ContextMatchOptions = { + ...DEFAULT_MATCH_OPTIONS, + ...options, + }; + + const fileLines = fileContent.split("\n"); + + // Check if additions are present and deletions are not + let additionsPresent = 0; + let deletionsAbsent = 0; + + for (const line of hunk.lines) { + const normalizedContent = normalizeLine(line.content, fullOptions); + + if (line.type === "addition") { + const found = fileLines.some( + (fl) => normalizeLine(fl, fullOptions) === normalizedContent, + ); + if (found) additionsPresent++; + } + + if (line.type === "deletion") { + const found = fileLines.some( + (fl) => normalizeLine(fl, fullOptions) === normalizedContent, + ); + if (!found) deletionsAbsent++; + } + } + + const totalAdditions = hunk.lines.filter((l) => l.type === "addition").length; + const totalDeletions = hunk.lines.filter((l) => l.type === "deletion").length; + + // Consider applied if most additions are present and most deletions are absent + const additionsMatch = + totalAdditions === 0 || additionsPresent >= totalAdditions * 0.8; + const deletionsMatch = + totalDeletions === 0 || deletionsAbsent >= totalDeletions * 0.8; + + return additionsMatch && deletionsMatch; +}; + +/** + * Calculate the expected result of applying a hunk + */ +export const previewHunkApplication = ( + fileContent: string, + hunk: PatchHunk, + position: number, +): { success: boolean; preview: string[]; error?: string } => { + const fileLines = fileContent.split("\n"); + const resultLines: string[] = []; + + // Copy lines before the hunk + for (let i = 0; i < position; i++) { + resultLines.push(fileLines[i]); + } + + // Calculate how many lines to skip from the original file + let originalLinesConsumed = 0; + for (const line of hunk.lines) { + if (line.type === "context" || line.type === "deletion") { + originalLinesConsumed++; + } + } + + // Apply hunk transformations + for (const line of hunk.lines) { + if (line.type === "context") { + resultLines.push(line.content); + } else if (line.type === "addition") { + resultLines.push(line.content); + } + // Deletions are skipped (not added to result) + } + + // Copy lines after the hunk + for (let i = position + originalLinesConsumed; i < fileLines.length; i++) { + resultLines.push(fileLines[i]); + } + + return { + success: true, + preview: resultLines, + }; +}; diff --git a/src/tools/apply-patch/params.ts b/src/tools/apply-patch/params.ts new file mode 100644 index 0000000..bff997e --- /dev/null +++ b/src/tools/apply-patch/params.ts @@ -0,0 +1,43 @@ +/** + * Apply Patch Tool Parameters + */ + +import { z } from "zod"; +import { PATCH_DEFAULTS } from "@constants/apply-patch"; + +/** + * Zod schema for apply_patch tool parameters + */ +export const applyPatchParams = z.object({ + patch: z + .string() + .describe("The unified diff patch content to apply"), + + targetFile: z + .string() + .optional() + .describe("Override the target file path from the patch header"), + + dryRun: z + .boolean() + .optional() + .default(false) + .describe("Validate and preview changes without actually applying them"), + + fuzz: z + .number() + .int() + .min(0) + .max(PATCH_DEFAULTS.MAX_FUZZ) + .optional() + .default(PATCH_DEFAULTS.FUZZ) + .describe(`Context line tolerance for fuzzy matching (0-${PATCH_DEFAULTS.MAX_FUZZ})`), + + reverse: z + .boolean() + .optional() + .default(false) + .describe("Apply the patch in reverse (undo the changes)"), +}); + +export type ApplyPatchParams = z.infer; diff --git a/src/tools/apply-patch/parser.ts b/src/tools/apply-patch/parser.ts new file mode 100644 index 0000000..1f4949f --- /dev/null +++ b/src/tools/apply-patch/parser.ts @@ -0,0 +1,387 @@ +/** + * Patch Parser + * + * Parses unified diff format patches into structured data. + */ + +import { + PATCH_PATTERNS, + LINE_PREFIXES, + SPECIAL_PATHS, + PATCH_ERRORS, +} from "@constants/apply-patch"; +import type { + ParsedPatch, + ParsedFilePatch, + PatchHunk, + PatchLine, + PatchLineType, + PatchValidationResult, +} from "@/types/apply-patch"; + +/** + * Parse a unified diff patch string + */ +export const parsePatch = (patchContent: string): ParsedPatch => { + const lines = patchContent.split("\n"); + const files: ParsedFilePatch[] = []; + + let currentFile: ParsedFilePatch | null = null; + let currentHunk: PatchHunk | null = null; + let lineIndex = 0; + + while (lineIndex < lines.length) { + const line = lines[lineIndex]; + + // Git diff header + const gitDiffMatch = line.match(PATCH_PATTERNS.GIT_DIFF); + if (gitDiffMatch) { + if (currentFile && (currentFile.hunks.length > 0 || currentFile.isBinary)) { + files.push(currentFile); + } + currentFile = createEmptyFilePatch(gitDiffMatch[1], gitDiffMatch[2]); + currentHunk = null; + lineIndex++; + continue; + } + + // File header old + const oldHeaderMatch = line.match(PATCH_PATTERNS.FILE_HEADER_OLD); + if (oldHeaderMatch) { + if (!currentFile) { + currentFile = createEmptyFilePatch("", ""); + } + currentFile.oldPath = cleanPath(oldHeaderMatch[1]); + if (currentFile.oldPath === SPECIAL_PATHS.DEV_NULL) { + currentFile.isNew = true; + } + lineIndex++; + continue; + } + + // File header new + const newHeaderMatch = line.match(PATCH_PATTERNS.FILE_HEADER_NEW); + if (newHeaderMatch) { + if (!currentFile) { + currentFile = createEmptyFilePatch("", ""); + } + currentFile.newPath = cleanPath(newHeaderMatch[1]); + if (currentFile.newPath === SPECIAL_PATHS.DEV_NULL) { + currentFile.isDeleted = true; + } + lineIndex++; + continue; + } + + // Index line (skip) + if (PATCH_PATTERNS.INDEX_LINE.test(line)) { + lineIndex++; + continue; + } + + // Binary file + if (PATCH_PATTERNS.BINARY_FILE.test(line)) { + if (currentFile) { + currentFile.isBinary = true; + } + lineIndex++; + continue; + } + + // New file mode + if (PATCH_PATTERNS.NEW_FILE.test(line)) { + if (currentFile) { + currentFile.isNew = true; + } + lineIndex++; + continue; + } + + // Deleted file mode + if (PATCH_PATTERNS.DELETED_FILE.test(line)) { + if (currentFile) { + currentFile.isDeleted = true; + } + lineIndex++; + continue; + } + + // Rename from + const renameFromMatch = line.match(PATCH_PATTERNS.RENAME_FROM); + if (renameFromMatch) { + if (currentFile) { + currentFile.isRenamed = true; + currentFile.oldPath = cleanPath(renameFromMatch[1]); + } + lineIndex++; + continue; + } + + // Rename to + const renameToMatch = line.match(PATCH_PATTERNS.RENAME_TO); + if (renameToMatch) { + if (currentFile) { + currentFile.newPath = cleanPath(renameToMatch[1]); + } + lineIndex++; + continue; + } + + // Hunk header + const hunkMatch = line.match(PATCH_PATTERNS.HUNK_HEADER); + if (hunkMatch) { + if (currentHunk && currentFile) { + currentFile.hunks.push(currentHunk); + } + + currentHunk = { + oldStart: parseInt(hunkMatch[1], 10), + oldLines: hunkMatch[2] ? parseInt(hunkMatch[2], 10) : 1, + newStart: parseInt(hunkMatch[3], 10), + newLines: hunkMatch[4] ? parseInt(hunkMatch[4], 10) : 1, + lines: [], + header: line, + }; + lineIndex++; + continue; + } + + // Patch lines (context, addition, deletion) + if (currentHunk) { + const patchLine = parsePatchLine(line, currentHunk); + if (patchLine) { + currentHunk.lines.push(patchLine); + } + } + + lineIndex++; + } + + // Push final hunk and file + if (currentHunk && currentFile) { + currentFile.hunks.push(currentHunk); + } + if (currentFile && (currentFile.hunks.length > 0 || currentFile.isBinary)) { + files.push(currentFile); + } + + return { + files, + rawPatch: patchContent, + }; +}; + +/** + * Create empty file patch structure + */ +const createEmptyFilePatch = (oldPath: string, newPath: string): ParsedFilePatch => ({ + oldPath: cleanPath(oldPath), + newPath: cleanPath(newPath), + hunks: [], + isBinary: false, + isNew: false, + isDeleted: false, + isRenamed: false, +}); + +/** + * Clean path by removing a/ or b/ prefixes + */ +const cleanPath = (path: string): string => { + if (path.startsWith(SPECIAL_PATHS.A_PREFIX)) { + return path.slice(2); + } + if (path.startsWith(SPECIAL_PATHS.B_PREFIX)) { + return path.slice(2); + } + return path; +}; + +/** + * Parse a single patch line + */ +const parsePatchLine = (line: string, _hunk: PatchHunk): PatchLine | null => { + // No newline marker (skip but keep in mind) + if (PATCH_PATTERNS.NO_NEWLINE.test(line)) { + return null; + } + + // Empty line at end of patch + if (line === "") { + return null; + } + + const prefix = line[0]; + const content = line.slice(1); + + const typeMap: Record = { + [LINE_PREFIXES.CONTEXT]: "context", + [LINE_PREFIXES.ADDITION]: "addition", + [LINE_PREFIXES.DELETION]: "deletion", + }; + + const type = typeMap[prefix]; + + if (!type) { + // Unknown line type, treat as context if it looks like content + return { + type: "context", + content: line, + }; + } + + return { + type, + content, + }; +}; + +/** + * Validate a parsed patch + */ +export const validatePatch = (patch: ParsedPatch): PatchValidationResult => { + const errors: string[] = []; + const warnings: string[] = []; + let hunkCount = 0; + + if (patch.files.length === 0) { + errors.push(PATCH_ERRORS.INVALID_PATCH); + return { + valid: false, + errors, + warnings, + fileCount: 0, + hunkCount: 0, + }; + } + + for (const file of patch.files) { + // Check for binary files + if (file.isBinary) { + warnings.push(PATCH_ERRORS.BINARY_NOT_SUPPORTED); + continue; + } + + // Check file paths + if (!file.newPath && !file.isDeleted) { + errors.push(`Missing target path for file`); + } + + // Validate hunks + for (const hunk of file.hunks) { + hunkCount++; + + // Count lines + let contextCount = 0; + let additionCount = 0; + let deletionCount = 0; + + for (const line of hunk.lines) { + if (line.type === "context") contextCount++; + if (line.type === "addition") additionCount++; + if (line.type === "deletion") deletionCount++; + } + + // Verify hunk line counts + const expectedOld = contextCount + deletionCount; + const expectedNew = contextCount + additionCount; + + if (expectedOld !== hunk.oldLines) { + warnings.push( + `Hunk line count mismatch: expected ${hunk.oldLines} old lines, found ${expectedOld}`, + ); + } + + if (expectedNew !== hunk.newLines) { + warnings.push( + `Hunk line count mismatch: expected ${hunk.newLines} new lines, found ${expectedNew}`, + ); + } + } + } + + return { + valid: errors.length === 0, + errors, + warnings, + fileCount: patch.files.length, + hunkCount, + }; +}; + +/** + * Get the target file path from a parsed file patch + */ +export const getTargetPath = (filePatch: ParsedFilePatch): string => { + // For new files, use the new path + if (filePatch.isNew) { + return filePatch.newPath; + } + + // For deleted files, use the old path + if (filePatch.isDeleted) { + return filePatch.oldPath; + } + + // For renames, we want to modify the old file and rename to new + // For regular patches, prefer newPath but fall back to oldPath + return filePatch.newPath || filePatch.oldPath; +}; + +/** + * Check if a patch appears to be reversed + */ +export const isPatchReversed = ( + patch: ParsedFilePatch, + fileContent: string, +): boolean => { + // Simple heuristic: check if the "added" lines are present in the file + // and "deleted" lines are not + const fileLines = new Set(fileContent.split("\n")); + + let addedPresent = 0; + let deletedPresent = 0; + + for (const hunk of patch.hunks) { + for (const line of hunk.lines) { + if (line.type === "addition" && fileLines.has(line.content)) { + addedPresent++; + } + if (line.type === "deletion" && fileLines.has(line.content)) { + deletedPresent++; + } + } + } + + // If added lines are present and deleted lines are not, patch is reversed + return addedPresent > deletedPresent * 2; +}; + +/** + * Reverse a patch (swap additions and deletions) + */ +export const reversePatch = (patch: ParsedFilePatch): ParsedFilePatch => { + return { + ...patch, + oldPath: patch.newPath, + newPath: patch.oldPath, + isNew: patch.isDeleted, + isDeleted: patch.isNew, + hunks: patch.hunks.map((hunk) => ({ + ...hunk, + oldStart: hunk.newStart, + oldLines: hunk.newLines, + newStart: hunk.oldStart, + newLines: hunk.oldLines, + lines: hunk.lines.map((line) => ({ + ...line, + type: + line.type === "addition" + ? "deletion" + : line.type === "deletion" + ? "addition" + : line.type, + })), + })), + }; +}; diff --git a/src/tools/index.ts b/src/tools/index.ts index e6705ec..73488f8 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -12,7 +12,10 @@ export { todoReadTool } from "@tools/todo-read"; export { globToolDefinition } from "@tools/glob/definition"; export { grepToolDefinition } from "@tools/grep/definition"; export { webSearchTool } from "@tools/web-search"; +export { webFetchTool } from "@tools/web-fetch"; +export { multiEditTool } from "@tools/multi-edit"; export { lspTool } from "@tools/lsp"; +export { applyPatchTool } from "@tools/apply-patch"; import type { ToolDefinition, FunctionDefinition } from "@tools/types"; import { toolToFunction } from "@tools/types"; @@ -25,7 +28,10 @@ import { todoReadTool } from "@tools/todo-read"; import { globToolDefinition } from "@tools/glob/definition"; import { grepToolDefinition } from "@tools/grep/definition"; import { webSearchTool } from "@tools/web-search"; +import { webFetchTool } from "@tools/web-fetch"; +import { multiEditTool } from "@tools/multi-edit"; import { lspTool } from "@tools/lsp"; +import { applyPatchTool } from "@tools/apply-patch"; import { isMCPTool, executeMCPTool, @@ -44,12 +50,15 @@ export const tools: ToolDefinition[] = [ readTool, writeTool, editTool, + multiEditTool, globToolDefinition, grepToolDefinition, todoWriteTool, todoReadTool, webSearchTool, + webFetchTool, lspTool, + applyPatchTool, ]; // Tools that are read-only (allowed in chat mode) @@ -59,6 +68,7 @@ const READ_ONLY_TOOLS = new Set([ "grep", "todo_read", "web_search", + "web_fetch", "lsp", ]); diff --git a/src/tools/multi-edit/execute.ts b/src/tools/multi-edit/execute.ts new file mode 100644 index 0000000..e9a9a33 --- /dev/null +++ b/src/tools/multi-edit/execute.ts @@ -0,0 +1,343 @@ +/** + * MultiEdit Tool Execution + * + * Performs batch file editing with atomic transactions + */ + +import fs from "fs/promises"; +import path from "path"; + +import { + MULTI_EDIT_DEFAULTS, + MULTI_EDIT_MESSAGES, + MULTI_EDIT_TITLES, + MULTI_EDIT_DESCRIPTION, +} from "@constants/multi-edit"; +import { isFileOpAllowed, promptFilePermission } from "@services/permissions"; +import { formatDiff, generateDiff } from "@utils/diff"; +import { multiEditParams } from "@tools/multi-edit/params"; +import type { ToolDefinition, ToolContext, ToolResult } from "@/types/tools"; +import type { EditItem, MultiEditParams } from "@tools/multi-edit/params"; + +interface FileBackup { + path: string; + content: string; +} + +interface EditValidation { + valid: boolean; + error?: string; + fileContent?: string; +} + +interface EditResult { + path: string; + success: boolean; + diff?: string; + additions?: number; + deletions?: number; + error?: string; +} + +const createErrorResult = (error: string): ToolResult => ({ + success: false, + title: MULTI_EDIT_TITLES.FAILED, + output: "", + error, +}); + +const createSuccessResult = ( + results: EditResult[], + totalEdits: number, +): ToolResult => { + const successful = results.filter((r) => r.success); + const failed = results.filter((r) => !r.success); + + const diffOutput = successful + .map((r) => `## ${path.basename(r.path)}\n\n${r.diff}`) + .join("\n\n---\n\n"); + + const totalAdditions = successful.reduce((sum, r) => sum + (r.additions ?? 0), 0); + const totalDeletions = successful.reduce((sum, r) => sum + (r.deletions ?? 0), 0); + + const title = + failed.length > 0 + ? MULTI_EDIT_TITLES.PARTIAL(successful.length, failed.length) + : MULTI_EDIT_TITLES.SUCCESS(successful.length); + + let output = diffOutput; + if (failed.length > 0) { + output += + "\n\n## Failed Edits\n\n" + + failed.map((r) => `- ${r.path}: ${r.error}`).join("\n"); + } + + return { + success: failed.length === 0, + title, + output, + metadata: { + totalEdits, + successful: successful.length, + failed: failed.length, + totalAdditions, + totalDeletions, + }, + }; +}; + +/** + * Validate a single edit + */ +const validateEdit = async ( + edit: EditItem, + workingDir: string, +): Promise => { + const fullPath = path.isAbsolute(edit.file_path) + ? edit.file_path + : path.join(workingDir, edit.file_path); + + try { + const stat = await fs.stat(fullPath); + if (!stat.isFile()) { + return { valid: false, error: `Not a file: ${edit.file_path}` }; + } + + if (stat.size > MULTI_EDIT_DEFAULTS.MAX_FILE_SIZE) { + return { valid: false, error: MULTI_EDIT_MESSAGES.FILE_TOO_LARGE(edit.file_path) }; + } + + const content = await fs.readFile(fullPath, "utf-8"); + + // Check if old_string exists + if (!content.includes(edit.old_string)) { + const preview = edit.old_string.slice(0, 50); + return { + valid: false, + error: MULTI_EDIT_MESSAGES.OLD_STRING_NOT_FOUND(edit.file_path, preview), + }; + } + + // Check uniqueness + const occurrences = content.split(edit.old_string).length - 1; + if (occurrences > 1) { + return { + valid: false, + error: MULTI_EDIT_MESSAGES.OLD_STRING_NOT_UNIQUE(edit.file_path, occurrences), + }; + } + + return { valid: true, fileContent: content }; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return { valid: false, error: MULTI_EDIT_MESSAGES.FILE_NOT_FOUND(edit.file_path) }; + } + const message = error instanceof Error ? error.message : String(error); + return { valid: false, error: message }; + } +}; + +/** + * Check permissions for all files + */ +const checkPermissions = async ( + edits: EditItem[], + workingDir: string, + autoApprove: boolean, +): Promise<{ allowed: boolean; denied: string[] }> => { + const denied: string[] = []; + + for (const edit of edits) { + const fullPath = path.isAbsolute(edit.file_path) + ? edit.file_path + : path.join(workingDir, edit.file_path); + + if (!autoApprove && !isFileOpAllowed("Edit", fullPath)) { + const { allowed } = await promptFilePermission( + "Edit", + fullPath, + `Edit file: ${edit.file_path}`, + ); + if (!allowed) { + denied.push(edit.file_path); + } + } + } + + return { allowed: denied.length === 0, denied }; +}; + +/** + * Apply a single edit + */ +const applyEdit = async ( + edit: EditItem, + workingDir: string, + fileContent: string, +): Promise => { + const fullPath = path.isAbsolute(edit.file_path) + ? edit.file_path + : path.join(workingDir, edit.file_path); + + try { + const newContent = fileContent.replace(edit.old_string, edit.new_string); + const diff = generateDiff(fileContent, newContent); + const relativePath = path.relative(workingDir, fullPath); + const diffOutput = formatDiff(diff, relativePath); + + await fs.writeFile(fullPath, newContent, "utf-8"); + + return { + path: edit.file_path, + success: true, + diff: diffOutput, + additions: diff.additions, + deletions: diff.deletions, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + path: edit.file_path, + success: false, + error: message, + }; + } +}; + +/** + * Rollback changes using backups + */ +const rollback = async (backups: FileBackup[]): Promise => { + for (const backup of backups) { + try { + await fs.writeFile(backup.path, backup.content, "utf-8"); + } catch { + // Best effort rollback + } + } +}; + +/** + * Execute multi-edit + */ +export const executeMultiEdit = async ( + args: MultiEditParams, + ctx: ToolContext, +): Promise => { + const { edits } = args; + + // Validate edit count + if (edits.length === 0) { + return createErrorResult(MULTI_EDIT_MESSAGES.NO_EDITS); + } + + if (edits.length > MULTI_EDIT_DEFAULTS.MAX_EDITS) { + return createErrorResult( + MULTI_EDIT_MESSAGES.TOO_MANY_EDITS(MULTI_EDIT_DEFAULTS.MAX_EDITS), + ); + } + + ctx.onMetadata?.({ + title: MULTI_EDIT_TITLES.VALIDATING(edits.length), + status: "running", + }); + + // Phase 1: Validate all edits + const validations = new Map(); + const errors: string[] = []; + + for (const edit of edits) { + const validation = await validateEdit(edit, ctx.workingDir); + validations.set(edit.file_path, { validation, edit }); + + if (!validation.valid) { + errors.push(validation.error ?? "Unknown error"); + } + } + + if (errors.length > 0) { + return createErrorResult( + MULTI_EDIT_MESSAGES.VALIDATION_FAILED + ":\n" + errors.join("\n"), + ); + } + + // Phase 2: Check permissions + const permCheck = await checkPermissions( + edits, + ctx.workingDir, + ctx.autoApprove ?? false, + ); + + if (!permCheck.allowed) { + return createErrorResult( + `Permission denied for: ${permCheck.denied.join(", ")}`, + ); + } + + // Phase 3: Create backups and apply edits atomically + const backups: FileBackup[] = []; + const results: EditResult[] = []; + let failed = false; + + for (let i = 0; i < edits.length; i++) { + const edit = edits[i]; + const data = validations.get(edit.file_path); + if (!data?.validation.fileContent) continue; + + ctx.onMetadata?.({ + title: MULTI_EDIT_TITLES.APPLYING(i + 1, edits.length), + status: "running", + }); + + const fullPath = path.isAbsolute(edit.file_path) + ? edit.file_path + : path.join(ctx.workingDir, edit.file_path); + + // Create backup + backups.push({ + path: fullPath, + content: data.validation.fileContent, + }); + + // Apply edit + const result = await applyEdit(edit, ctx.workingDir, data.validation.fileContent); + results.push(result); + + if (!result.success) { + failed = true; + break; + } + + // Update file content for subsequent edits to same file + if (result.success) { + const newContent = data.validation.fileContent.replace( + edit.old_string, + edit.new_string, + ); + // Update the validation cache for potential subsequent edits to same file + validations.set(edit.file_path, { + ...data, + validation: { ...data.validation, fileContent: newContent }, + }); + } + } + + // Phase 4: Rollback if any edit failed + if (failed) { + ctx.onMetadata?.({ + title: MULTI_EDIT_TITLES.ROLLBACK, + status: "running", + }); + await rollback(backups); + return createErrorResult(MULTI_EDIT_MESSAGES.ATOMIC_FAILURE); + } + + return createSuccessResult(results, edits.length); +}; + +export const multiEditTool: ToolDefinition = { + name: "multi_edit", + description: MULTI_EDIT_DESCRIPTION, + parameters: multiEditParams, + execute: executeMultiEdit, +}; diff --git a/src/tools/multi-edit/index.ts b/src/tools/multi-edit/index.ts new file mode 100644 index 0000000..f12d11f --- /dev/null +++ b/src/tools/multi-edit/index.ts @@ -0,0 +1,13 @@ +/** + * MultiEdit Tool + * + * Batch file editing with atomic transactions + */ + +export { multiEditTool, executeMultiEdit } from "@tools/multi-edit/execute"; +export { + multiEditParams, + editItemSchema, + type EditItem, + type MultiEditParams, +} from "@tools/multi-edit/params"; diff --git a/src/tools/multi-edit/params.ts b/src/tools/multi-edit/params.ts new file mode 100644 index 0000000..c9cd62e --- /dev/null +++ b/src/tools/multi-edit/params.ts @@ -0,0 +1,21 @@ +/** + * MultiEdit Tool Parameters + */ + +import { z } from "zod"; + +export const editItemSchema = z.object({ + file_path: z.string().describe("Absolute path to the file to edit"), + old_string: z.string().describe("The exact text to find and replace"), + new_string: z.string().describe("The replacement text"), +}); + +export const multiEditParams = z.object({ + edits: z + .array(editItemSchema) + .min(1) + .describe("Array of edits to apply atomically"), +}); + +export type EditItem = z.infer; +export type MultiEditParams = z.infer; diff --git a/src/tools/web-fetch/execute.ts b/src/tools/web-fetch/execute.ts new file mode 100644 index 0000000..a86a6cb --- /dev/null +++ b/src/tools/web-fetch/execute.ts @@ -0,0 +1,346 @@ +/** + * WebFetch Tool Execution + * + * Fetches content from URLs and converts HTML to markdown + */ + +import { + WEB_FETCH_DEFAULTS, + WEB_FETCH_MESSAGES, + WEB_FETCH_TITLES, + WEB_FETCH_DESCRIPTION, + HTML_REMOVE_ELEMENTS, +} from "@constants/web-fetch"; +import { webFetchParams } from "@tools/web-fetch/params"; +import type { ToolDefinition, ToolContext, ToolResult } from "@/types/tools"; +import type { WebFetchParams } from "@tools/web-fetch/params"; + +const createErrorResult = (error: string): ToolResult => ({ + success: false, + title: WEB_FETCH_TITLES.FAILED, + output: "", + error, +}); + +const createSuccessResult = ( + url: string, + content: string, + contentType: string, +): ToolResult => ({ + success: true, + title: WEB_FETCH_TITLES.SUCCESS, + output: content, + metadata: { + url, + contentType, + contentLength: content.length, + }, +}); + +/** + * Validate URL format + */ +const validateUrl = (url: string): URL | null => { + try { + const parsed = new URL(url); + // Upgrade HTTP to HTTPS + if (parsed.protocol === "http:") { + parsed.protocol = "https:"; + } + if (!["https:", "http:"].includes(parsed.protocol)) { + return null; + } + return parsed; + } catch { + return null; + } +}; + +/** + * Remove HTML elements by tag name + */ +const removeElements = (html: string, tags: string[]): string => { + let result = html; + for (const tag of tags) { + // Remove self-closing and regular tags + const selfClosingPattern = new RegExp(`<${tag}[^>]*/>`, "gi"); + const openClosePattern = new RegExp( + `<${tag}[^>]*>[\\s\\S]*?`, + "gi", + ); + result = result.replace(selfClosingPattern, ""); + result = result.replace(openClosePattern, ""); + } + return result; +}; + +/** + * Decode HTML entities + */ +const decodeHtmlEntities = (text: string): string => { + const entities: Record = { + "&": "&", + "<": "<", + ">": ">", + """: '"', + "'": "'", + " ": " ", + "'": "'", + "/": "/", + "—": "—", + "–": "–", + "…": "…", + "’": "'", + "‘": "'", + "”": '"', + "“": '"', + "©": "©", + "®": "®", + "™": "™", + }; + + let decoded = text; + for (const [entity, char] of Object.entries(entities)) { + decoded = decoded.replace(new RegExp(entity, "g"), char); + } + + // Handle numeric entities + decoded = decoded.replace(/&#(\d+);/g, (_, code) => + String.fromCharCode(parseInt(code, 10)), + ); + decoded = decoded.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => + String.fromCharCode(parseInt(code, 16)), + ); + + return decoded; +}; + +/** + * Convert HTML to markdown + */ +const htmlToMarkdown = (html: string): string => { + // Remove unwanted elements + let content = removeElements(html, HTML_REMOVE_ELEMENTS); + + // Extract body content if present + const bodyMatch = content.match(/]*>([\s\S]*)<\/body>/i); + if (bodyMatch) { + content = bodyMatch[1]; + } + + // Convert headers + content = content.replace(/]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n"); + content = content.replace(/]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n"); + content = content.replace(/]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n"); + content = content.replace(/]*>([\s\S]*?)<\/h4>/gi, "\n#### $1\n"); + content = content.replace(/]*>([\s\S]*?)<\/h5>/gi, "\n##### $1\n"); + content = content.replace(/]*>([\s\S]*?)<\/h6>/gi, "\n###### $1\n"); + + // Convert links + content = content.replace( + /]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, + "[$2]($1)", + ); + + // Convert images + content = content.replace( + /]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, + "![$2]($1)", + ); + content = content.replace(/]*src="([^"]*)"[^>]*\/?>/gi, "![]($1)"); + + // Convert emphasis + content = content.replace(/]*>([\s\S]*?)<\/strong>/gi, "**$1**"); + content = content.replace(/]*>([\s\S]*?)<\/b>/gi, "**$1**"); + content = content.replace(/]*>([\s\S]*?)<\/em>/gi, "*$1*"); + content = content.replace(/]*>([\s\S]*?)<\/i>/gi, "*$1*"); + + // Convert code + content = content.replace(/]*>([\s\S]*?)<\/code>/gi, "`$1`"); + content = content.replace( + /]*>([\s\S]*?)<\/pre>/gi, + "\n```\n$1\n```\n", + ); + + // Convert lists + content = content.replace(/]*>([\s\S]*?)<\/li>/gi, "- $1\n"); + content = content.replace(/<\/?[ou]l[^>]*>/gi, "\n"); + + // Convert paragraphs and line breaks + content = content.replace(/]*>([\s\S]*?)<\/p>/gi, "\n$1\n"); + content = content.replace(//gi, "\n"); + content = content.replace(//gi, "\n---\n"); + + // Convert blockquotes + content = content.replace( + /]*>([\s\S]*?)<\/blockquote>/gi, + (_, text) => { + return text + .split("\n") + .map((line: string) => `> ${line}`) + .join("\n"); + }, + ); + + // Remove remaining HTML tags + content = content.replace(/<[^>]+>/g, ""); + + // Decode HTML entities + content = decodeHtmlEntities(content); + + // Clean up whitespace + content = content.replace(/\n{3,}/g, "\n\n"); + content = content.replace(/[ \t]+/g, " "); + content = content.trim(); + + return content; +}; + +/** + * Format JSON for readability + */ +const formatJson = (json: string): string => { + try { + const parsed = JSON.parse(json); + return "```json\n" + JSON.stringify(parsed, null, 2) + "\n```"; + } catch { + return json; + } +}; + +/** + * Process content based on content type + */ +const processContent = (content: string, contentType: string): string => { + const type = contentType.toLowerCase(); + + if (type.includes("json")) { + return formatJson(content); + } + + if (type.includes("html") || type.includes("xhtml")) { + return htmlToMarkdown(content); + } + + // Plain text, markdown, etc. + return content; +}; + +/** + * Truncate content if too large + */ +const truncateContent = (content: string, maxLength: number): string => { + if (content.length <= maxLength) { + return content; + } + + const truncated = content.slice(0, maxLength); + const lastNewline = truncated.lastIndexOf("\n"); + const cutPoint = lastNewline > maxLength * 0.8 ? lastNewline : maxLength; + + return ( + truncated.slice(0, cutPoint) + + "\n\n... (content truncated, showing first " + + Math.round(maxLength / 1000) + + "KB)" + ); +}; + +/** + * Execute web fetch + */ +export const executeWebFetch = async ( + args: WebFetchParams, + ctx: ToolContext, +): Promise => { + const { url, timeout = WEB_FETCH_DEFAULTS.TIMEOUT_MS } = args; + + if (!url || url.trim().length === 0) { + return createErrorResult(WEB_FETCH_MESSAGES.URL_REQUIRED); + } + + const parsedUrl = validateUrl(url); + if (!parsedUrl) { + return createErrorResult(WEB_FETCH_MESSAGES.INVALID_URL(url)); + } + + ctx.onMetadata?.({ + title: WEB_FETCH_TITLES.FETCHING(parsedUrl.hostname), + status: "running", + }); + + try { + // Create abort controller with timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + // Merge with context abort signal + ctx.abort.signal.addEventListener("abort", () => controller.abort()); + + const response = await fetch(parsedUrl.toString(), { + headers: { + "User-Agent": WEB_FETCH_DEFAULTS.USER_AGENT, + Accept: + "text/html,application/xhtml+xml,application/xml;q=0.9,application/json,text/plain;q=0.8", + "Accept-Language": "en-US,en;q=0.9", + }, + signal: controller.signal, + redirect: "follow", + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + return createErrorResult( + WEB_FETCH_MESSAGES.FETCH_ERROR(`HTTP ${response.status}`), + ); + } + + // Check for redirect to different host + const finalUrl = new URL(response.url); + if (finalUrl.host !== parsedUrl.host) { + return { + success: true, + title: WEB_FETCH_TITLES.SUCCESS, + output: WEB_FETCH_MESSAGES.REDIRECT_DETECTED( + parsedUrl.host, + finalUrl.host, + ), + metadata: { + redirectUrl: response.url, + originalUrl: url, + }, + }; + } + + const contentType = response.headers.get("content-type") || "text/plain"; + let content = await response.text(); + + // Check content length + if (content.length > WEB_FETCH_DEFAULTS.MAX_CONTENT_LENGTH) { + content = truncateContent( + content, + WEB_FETCH_DEFAULTS.MAX_CONTENT_LENGTH, + ); + } + + // Process content based on type + const processed = processContent(content, contentType); + + return createSuccessResult(response.url, processed, contentType); + } catch (error) { + if (ctx.abort.signal.aborted) { + return createErrorResult(WEB_FETCH_MESSAGES.TIMEOUT); + } + + const message = error instanceof Error ? error.message : String(error); + return createErrorResult(WEB_FETCH_MESSAGES.FETCH_ERROR(message)); + } +}; + +export const webFetchTool: ToolDefinition = { + name: "web_fetch", + description: WEB_FETCH_DESCRIPTION, + parameters: webFetchParams, + execute: executeWebFetch, +}; diff --git a/src/tools/web-fetch/index.ts b/src/tools/web-fetch/index.ts new file mode 100644 index 0000000..f35e164 --- /dev/null +++ b/src/tools/web-fetch/index.ts @@ -0,0 +1,8 @@ +/** + * WebFetch Tool + * + * Fetch and convert web content to markdown + */ + +export { webFetchTool, executeWebFetch } from "@tools/web-fetch/execute"; +export { webFetchParams, type WebFetchParams } from "@tools/web-fetch/params"; diff --git a/src/tools/web-fetch/params.ts b/src/tools/web-fetch/params.ts new file mode 100644 index 0000000..cb2cf07 --- /dev/null +++ b/src/tools/web-fetch/params.ts @@ -0,0 +1,19 @@ +/** + * WebFetch Tool Parameters + */ + +import { z } from "zod"; + +export const webFetchParams = z.object({ + url: z.string().describe("The URL to fetch content from"), + prompt: z + .string() + .optional() + .describe("Optional prompt to extract specific information from the content"), + timeout: z + .number() + .optional() + .describe("Timeout in milliseconds (default: 30000)"), +}); + +export type WebFetchParams = z.infer; diff --git a/src/tools/web-search/execute.ts b/src/tools/web-search/execute.ts index 9d8e282..93d89aa 100644 --- a/src/tools/web-search/execute.ts +++ b/src/tools/web-search/execute.ts @@ -1,7 +1,7 @@ /** * Web Search Tool Execution * - * Uses DuckDuckGo HTML search (no API key required) + * Uses Bing RSS search (no API key required, no captcha) */ import { @@ -55,69 +55,6 @@ const createSuccessResult = ( }; }; -/** - * Parse DuckDuckGo HTML search results - */ -const parseSearchResults = (html: string, maxResults: number): SearchResult[] => { - const results: SearchResult[] = []; - - // DuckDuckGo lite HTML structure parsing - // Look for result links and snippets - const resultPattern = - /]+class="result-link"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?]*class="result-snippet"[^>]*>([^<]+)/gi; - - // Alternative pattern for standard DuckDuckGo HTML - const altPattern = - /]+rel="nofollow"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?]*>([^<]{20,})/gi; - - // Try result-link pattern first - let match: RegExpExecArray | null; - while ((match = resultPattern.exec(html)) !== null && results.length < maxResults) { - const [, url, title, snippet] = match; - if (url && title && !url.includes("duckduckgo.com")) { - results.push({ - title: decodeHtmlEntities(title.trim()), - url: decodeUrl(url), - snippet: decodeHtmlEntities(snippet.trim()), - }); - } - } - - // If no results, try alternative pattern - if (results.length === 0) { - while ((match = altPattern.exec(html)) !== null && results.length < maxResults) { - const [, url, title, snippet] = match; - if (url && title && !url.includes("duckduckgo.com")) { - results.push({ - title: decodeHtmlEntities(title.trim()), - url: decodeUrl(url), - snippet: decodeHtmlEntities(snippet.trim()), - }); - } - } - } - - // Fallback: extract any external links with reasonable text - if (results.length === 0) { - const linkPattern = /]+href="(https?:\/\/(?!duckduckgo)[^"]+)"[^>]*>([^<]{10,100})<\/a>/gi; - const seenUrls = new Set(); - - while ((match = linkPattern.exec(html)) !== null && results.length < maxResults) { - const [, url, title] = match; - if (!seenUrls.has(url) && !url.includes("duckduckgo")) { - seenUrls.add(url); - results.push({ - title: decodeHtmlEntities(title.trim()), - url: decodeUrl(url), - snippet: "", - }); - } - } - } - - return results; -}; - /** * Decode HTML entities */ @@ -147,21 +84,36 @@ const decodeHtmlEntities = (text: string): string => { }; /** - * Decode DuckDuckGo redirect URLs + * Parse Bing RSS search results */ -const decodeUrl = (url: string): string => { - // DuckDuckGo often wraps URLs in redirects - if (url.includes("uddg=")) { - const match = url.match(/uddg=([^&]+)/); - if (match) { - return decodeURIComponent(match[1]); +const parseRssResults = (rss: string, maxResults: number): SearchResult[] => { + const results: SearchResult[] = []; + + // Parse RSS items + const itemPattern = /([\s\S]*?)<\/item>/gi; + let match: RegExpExecArray | null; + + while ((match = itemPattern.exec(rss)) !== null && results.length < maxResults) { + const itemContent = match[1]; + + const titleMatch = itemContent.match(/([^<]+)<\/title>/); + const linkMatch = itemContent.match(/<link>([^<]+)<\/link>/); + const descMatch = itemContent.match(/<description>([^<]*)<\/description>/); + + if (titleMatch && linkMatch) { + results.push({ + title: decodeHtmlEntities(titleMatch[1].trim()), + url: linkMatch[1].trim(), + snippet: descMatch ? decodeHtmlEntities(descMatch[1].trim()) : "", + }); } } - return url; + + return results; }; /** - * Perform web search using DuckDuckGo + * Perform web search using Bing RSS */ const performSearch = async ( query: string, @@ -170,13 +122,13 @@ const performSearch = async ( ): Promise<SearchResult[]> => { const encodedQuery = encodeURIComponent(query); - // Use DuckDuckGo HTML search (lite version for easier parsing) - const searchUrl = `https://lite.duckduckgo.com/lite/?q=${encodedQuery}`; + // Use Bing RSS search (no captcha, no API key required) + const searchUrl = `https://www.bing.com/search?q=${encodedQuery}&format=rss`; const response = await fetch(searchUrl, { headers: { "User-Agent": WEB_SEARCH_DEFAULTS.USER_AGENT, - Accept: "text/html", + Accept: "application/rss+xml, text/xml", "Accept-Language": "en-US,en;q=0.9", }, signal, @@ -186,8 +138,8 @@ const performSearch = async ( throw new Error(`Search request failed: ${response.status}`); } - const html = await response.text(); - return parseSearchResults(html, maxResults); + const rss = await response.text(); + return parseRssResults(rss, maxResults); }; /** diff --git a/src/tui-solid/app.tsx b/src/tui-solid/app.tsx index 4e5746b..de425d8 100644 --- a/src/tui-solid/app.tsx +++ b/src/tui-solid/app.tsx @@ -62,6 +62,9 @@ interface AppProps extends TuiInput { scope?: LearningScope, editedContent?: string, ) => void; + onBrainSetJwtToken?: (jwtToken: string) => Promise<void>; + onBrainSetApiKey?: (apiKey: string) => Promise<void>; + onBrainLogout?: () => Promise<void>; plan?: { id: string; title: string; @@ -139,9 +142,14 @@ function AppContent(props: AppProps) { app.setCascadeEnabled(props.cascadeEnabled); } - // Navigate to session if resuming - if (props.sessionId) { - route.goToSession(props.sessionId); + // Always navigate to session view (skip home page) + // Use existing sessionId or create a new one + if (!route.isSession()) { + const sessionId = props.sessionId ?? `session-${Date.now()}`; + batch(() => { + app.setSessionInfo(sessionId, app.provider(), app.model()); + route.goToSession(sessionId); + }); } if (props.availableModels && props.availableModels.length > 0) { @@ -375,6 +383,9 @@ function AppContent(props: AppProps) { onCascadeToggle={handleCascadeToggle} onPermissionResponse={handlePermissionResponse} onLearningResponse={handleLearningResponse} + onBrainSetJwtToken={props.onBrainSetJwtToken} + onBrainSetApiKey={props.onBrainSetApiKey} + onBrainLogout={props.onBrainLogout} plan={props.plan} agents={props.agents} currentAgent={props.currentAgent} @@ -429,6 +440,9 @@ export interface TuiRenderOptions extends TuiInput { scope?: LearningScope, editedContent?: string, ) => void; + onBrainSetJwtToken?: (jwtToken: string) => Promise<void>; + onBrainSetApiKey?: (apiKey: string) => Promise<void>; + onBrainLogout?: () => Promise<void>; plan?: { id: string; title: string; diff --git a/src/tui-solid/components/brain-menu.tsx b/src/tui-solid/components/brain-menu.tsx new file mode 100644 index 0000000..acce57d --- /dev/null +++ b/src/tui-solid/components/brain-menu.tsx @@ -0,0 +1,411 @@ +import { createSignal, Show, For } from "solid-js"; +import { useKeyboard } from "@opentui/solid"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; +import { useAppStore } from "@tui-solid/context/app"; +import { BRAIN_BANNER } from "@constants/brain"; + +interface BrainMenuProps { + onSetJwtToken: (jwtToken: string) => Promise<void>; + onSetApiKey: (apiKey: string) => Promise<void>; + onLogout: () => Promise<void>; + onClose: () => void; + isActive?: boolean; +} + +type MenuView = "main" | "login_url" | "jwt_input" | "apikey"; + +interface MenuItem { + id: string; + label: string; + description: string; + action: () => void; + disabled?: boolean; +} + +export function BrainMenu(props: BrainMenuProps) { + const theme = useTheme(); + const app = useAppStore(); + const isActive = () => props.isActive ?? true; + + const [view, setView] = createSignal<MenuView>("main"); + const [selectedIndex, setSelectedIndex] = createSignal(0); + const [jwtToken, setJwtToken] = createSignal(""); + const [apiKey, setApiKey] = createSignal(""); + const [error, setError] = createSignal<string | null>(null); + const [loading, setLoading] = createSignal(false); + + const isConnected = () => app.brain().status === "connected"; + + const menuItems = (): MenuItem[] => { + const items: MenuItem[] = []; + + if (!isConnected()) { + items.push({ + id: "login", + label: "Login with Email", + description: "Get JWT token from the web portal", + action: () => { + setView("login_url"); + setSelectedIndex(0); + }, + }); + items.push({ + id: "apikey", + label: "Use API Key", + description: "Enter your API key directly", + action: () => { + setView("apikey"); + setSelectedIndex(0); + }, + }); + } else { + items.push({ + id: "logout", + label: "Logout", + description: "Disconnect from CodeTyper Brain", + action: async () => { + setLoading(true); + try { + await props.onLogout(); + } catch (err) { + setError(err instanceof Error ? err.message : "Logout failed"); + } finally { + setLoading(false); + } + }, + }); + } + + items.push({ + id: "close", + label: "Close", + description: "Return to session", + action: () => props.onClose(), + }); + + return items; + }; + + const handleJwtSubmit = async (): Promise<void> => { + if (!jwtToken()) { + setError("JWT token is required"); + return; + } + + setLoading(true); + setError(null); + + try { + await props.onSetJwtToken(jwtToken()); + setView("main"); + setJwtToken(""); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to set JWT token"); + } finally { + setLoading(false); + } + }; + + const handleApiKey = async (): Promise<void> => { + if (!apiKey()) { + setError("API key is required"); + return; + } + + setLoading(true); + setError(null); + + try { + await props.onSetApiKey(apiKey()); + setView("main"); + setApiKey(""); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to set API key"); + } finally { + setLoading(false); + } + }; + + useKeyboard((evt) => { + if (!isActive()) return; + + if (evt.name === "escape") { + if (view() !== "main") { + setView("main"); + setError(null); + setSelectedIndex(0); + } else { + props.onClose(); + } + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + // Main menu navigation + if (view() === "main") { + if (evt.name === "up") { + setSelectedIndex((prev) => (prev > 0 ? prev - 1 : menuItems().length - 1)); + evt.preventDefault(); + return; + } + + if (evt.name === "down") { + setSelectedIndex((prev) => (prev < menuItems().length - 1 ? prev + 1 : 0)); + evt.preventDefault(); + return; + } + + if (evt.name === "return") { + const item = menuItems()[selectedIndex()]; + if (item && !item.disabled) { + item.action(); + } + evt.preventDefault(); + return; + } + } + + // Login URL view - press Enter to go to JWT input + if (view() === "login_url") { + if (evt.name === "return") { + setView("jwt_input"); + evt.preventDefault(); + return; + } + } + + // JWT token input handling + if (view() === "jwt_input") { + if (evt.name === "return") { + handleJwtSubmit(); + evt.preventDefault(); + return; + } + + if (evt.name === "backspace") { + setJwtToken((prev) => prev.slice(0, -1)); + evt.preventDefault(); + return; + } + + if (evt.name.length === 1 && !evt.ctrl && !evt.meta) { + setJwtToken((prev) => prev + evt.name); + evt.preventDefault(); + return; + } + } + + // API key form handling + if (view() === "apikey") { + if (evt.name === "return") { + handleApiKey(); + evt.preventDefault(); + return; + } + + if (evt.name === "backspace") { + setApiKey((prev) => prev.slice(0, -1)); + evt.preventDefault(); + return; + } + + if (evt.name.length === 1 && !evt.ctrl && !evt.meta) { + setApiKey((prev) => prev + evt.name); + evt.preventDefault(); + return; + } + } + }); + + const getStatusColor = (): string => { + const status = app.brain().status; + const colorMap: Record<string, string> = { + connected: theme.colors.success, + connecting: theme.colors.warning, + disconnected: theme.colors.textDim, + error: theme.colors.error, + }; + return colorMap[status] ?? theme.colors.textDim; + }; + + const getStatusText = (): string => { + const status = app.brain().status; + const textMap: Record<string, string> = { + connected: "Connected", + connecting: "Connecting...", + disconnected: "Not connected", + error: "Connection error", + }; + return textMap[status] ?? "Unknown"; + }; + + return ( + <box + flexDirection="column" + borderColor={theme.colors.accent} + border={["top", "bottom", "left", "right"]} + backgroundColor={theme.colors.background} + paddingLeft={1} + paddingRight={1} + width={60} + > + {/* Header */} + <box marginBottom={1} flexDirection="row" gap={1}> + <text fg="#ff69b4" attributes={TextAttributes.BOLD}> + {BRAIN_BANNER.EMOJI_CONNECTED} + </text> + <text fg={theme.colors.accent} attributes={TextAttributes.BOLD}> + CodeTyper Brain + </text> + </box> + + {/* Status */} + <box marginBottom={1} flexDirection="row"> + <text fg={theme.colors.textDim}>Status: </text> + <text fg={getStatusColor()}>{getStatusText()}</text> + <Show when={isConnected()}> + <text fg={theme.colors.textDim}> + {" "}({app.brain().knowledgeCount}K / {app.brain().memoryCount}M) + </text> + </Show> + </box> + + <Show when={isConnected() && app.brain().user}> + <box marginBottom={1} flexDirection="row"> + <text fg={theme.colors.textDim}>User: </text> + <text fg={theme.colors.info}> + {app.brain().user?.display_name ?? app.brain().user?.email} + </text> + </box> + </Show> + + {/* Error message */} + <Show when={error()}> + <box marginBottom={1}> + <text fg={theme.colors.error}>{error()}</text> + </box> + </Show> + + {/* Main menu view */} + <Show when={view() === "main"}> + <box flexDirection="column"> + <For each={menuItems()}> + {(item, index) => { + const isSelected = () => index() === selectedIndex(); + return ( + <box flexDirection="column" marginBottom={1}> + <box flexDirection="row"> + <text + fg={isSelected() ? theme.colors.accent : undefined} + attributes={isSelected() ? TextAttributes.BOLD : TextAttributes.NONE} + > + {isSelected() ? "> " : " "} + </text> + <text + fg={isSelected() ? theme.colors.accent : undefined} + attributes={isSelected() ? TextAttributes.BOLD : TextAttributes.NONE} + > + {item.label} + </text> + </box> + <box marginLeft={4}> + <text fg={theme.colors.textDim}>{item.description}</text> + </box> + </box> + ); + }} + </For> + </box> + + <box marginTop={1} flexDirection="column"> + <text fg={theme.colors.info}>{BRAIN_BANNER.CTA}: {BRAIN_BANNER.URL}</text> + <text fg={theme.colors.textDim}> + Arrow keys navigate | Enter select | Esc close + </text> + </box> + </Show> + + {/* Login URL view - shows where to login */} + <Show when={view() === "login_url"}> + <box flexDirection="column"> + <box marginBottom={1}> + <text fg={theme.colors.text}>1. Go to this page to login:</text> + </box> + <box marginBottom={1}> + <text fg={theme.colors.accent} attributes={TextAttributes.BOLD}> + {BRAIN_BANNER.LOGIN_URL} + </text> + </box> + <box marginBottom={1}> + <text fg={theme.colors.text}>2. After logging in, copy your JWT token</text> + </box> + <box marginBottom={1}> + <text fg={theme.colors.text}>3. Press Enter to input your token</text> + </box> + </box> + + <box marginTop={1}> + <text fg={theme.colors.textDim}> + Enter continue | Esc back + </text> + </box> + </Show> + + {/* JWT token input view */} + <Show when={view() === "jwt_input"}> + <box flexDirection="column"> + <box marginBottom={1} flexDirection="column"> + <text fg={theme.colors.accent}>JWT Token:</text> + <box + borderColor={theme.colors.accent} + border={["top", "bottom", "left", "right"]} + paddingLeft={1} + paddingRight={1} + > + <text fg={theme.colors.text}> + {jwtToken() ? "*".repeat(Math.min(jwtToken().length, 40)) : " "} + </text> + </box> + </box> + + <Show when={loading()}> + <text fg={theme.colors.warning}>Saving token...</text> + </Show> + </box> + + <box marginTop={1}> + <text fg={theme.colors.textDim}>Enter save | Esc back</text> + </box> + </Show> + + {/* API key form view */} + <Show when={view() === "apikey"}> + <box flexDirection="column"> + <box marginBottom={1} flexDirection="column"> + <text fg={theme.colors.accent}>API Key:</text> + <box + borderColor={theme.colors.accent} + border={["top", "bottom", "left", "right"]} + paddingLeft={1} + paddingRight={1} + > + <text fg={theme.colors.text}> + {apiKey() ? "*".repeat(Math.min(apiKey().length, 40)) : " "} + </text> + </box> + </box> + + <Show when={loading()}> + <text fg={theme.colors.warning}>Setting API key...</text> + </Show> + </box> + + <box marginTop={1}> + <text fg={theme.colors.textDim}>Enter save | Esc back</text> + </box> + </Show> + </box> + ); +} diff --git a/src/tui-solid/components/command-menu.tsx b/src/tui-solid/components/command-menu.tsx index a60a9a1..157ca92 100644 --- a/src/tui-solid/components/command-menu.tsx +++ b/src/tui-solid/components/command-menu.tsx @@ -3,6 +3,7 @@ import { useKeyboard } from "@opentui/solid"; import { TextAttributes } from "@opentui/core"; import { useTheme } from "@tui-solid/context/theme"; import { useAppStore } from "@tui-solid/context/app"; +import { BRAIN_DISABLED } from "@constants/brain"; import type { SlashCommand, CommandCategory } from "@/types/tui"; import { SLASH_COMMANDS, COMMAND_CATEGORIES } from "@constants/tui-components"; @@ -22,9 +23,14 @@ const filterCommands = ( commands: readonly SlashCommand[], filter: string, ): SlashCommand[] => { - if (!filter) return [...commands]; + // Filter out brain command when Brain is disabled + let availableCommands = BRAIN_DISABLED + ? commands.filter((cmd) => cmd.name !== "brain") + : [...commands]; + + if (!filter) return availableCommands; const query = filter.toLowerCase(); - return commands.filter( + return availableCommands.filter( (cmd) => cmd.name.toLowerCase().includes(query) || cmd.description.toLowerCase().includes(query), diff --git a/src/tui-solid/components/debug-log-panel.tsx b/src/tui-solid/components/debug-log-panel.tsx index 7f2a590..fb1a2bf 100644 --- a/src/tui-solid/components/debug-log-panel.tsx +++ b/src/tui-solid/components/debug-log-panel.tsx @@ -156,11 +156,11 @@ export function DebugLogPanel() { paddingRight={1} borderColor={theme.colors.border} border={["bottom"]} + flexDirection="row" > <text fg={theme.colors.accent} attributes={TextAttributes.BOLD}> - Debug Logs + Debug Logs ({entries().length}) </text> - <text fg={theme.colors.textDim}> ({entries().length})</text> </box> <scrollbox diff --git a/src/tui-solid/components/header.tsx b/src/tui-solid/components/header.tsx index 498c143..6bb2734 100644 --- a/src/tui-solid/components/header.tsx +++ b/src/tui-solid/components/header.tsx @@ -2,6 +2,11 @@ import { Show, createMemo } from "solid-js"; import { TextAttributes } from "@opentui/core"; import { useTheme } from "@tui-solid/context/theme"; import { useAppStore } from "@tui-solid/context/app"; +import { BRAIN_BANNER, BRAIN_DISABLED } from "@constants/brain"; +import { + TOKEN_WARNING_THRESHOLD, + TOKEN_CRITICAL_THRESHOLD, +} from "@constants/token"; interface HeaderProps { showBanner?: boolean; @@ -25,6 +30,30 @@ const MODE_COLORS = { "code-review": "success", } as const; +const BRAIN_STATUS_COLORS = { + connected: "success", + connecting: "warning", + disconnected: "textDim", + error: "error", +} as const; + +const TOKEN_STATUS_COLORS = { + normal: "textDim", + warning: "warning", + critical: "error", + compacting: "info", +} as const; + +/** + * Format token count for display (e.g., 45.2K) + */ +const formatTokenCount = (tokens: number): string => { + if (tokens >= 1000) { + return `${(tokens / 1000).toFixed(1)}K`; + } + return tokens.toString(); +}; + export function Header(props: HeaderProps) { const theme = useTheme(); const app = useAppStore(); @@ -35,57 +64,162 @@ export function Header(props: HeaderProps) { return theme.colors[colorKey]; }); - return ( - <box - flexDirection="row" - justifyContent="space-between" - paddingLeft={1} - paddingRight={1} - borderColor={theme.colors.border} - border={["bottom"]} - > - <box flexDirection="row" gap={1}> - <Show when={showBanner()}> - <text fg={theme.colors.primary} attributes={TextAttributes.BOLD}> - CodeTyper - </text> - </Show> - <text fg={theme.colors.textDim}>v{app.version()}</text> - <text fg={theme.colors.textDim}>|</text> - <box flexDirection="row"> - <text fg={modeColor()} attributes={TextAttributes.BOLD}> - [{MODE_LABELS[app.interactionMode()]}] - </text> - <Show when={app.currentAgent() !== "default"}> - <text fg={theme.colors.secondary} attributes={TextAttributes.BOLD}> - {" "} - @{app.currentAgent()} - </text> - </Show> - <text fg={theme.colors.textDim}> - {" "} - - {MODE_DESCRIPTIONS[app.interactionMode()]} - </text> - </box> - </box> + const brainColor = createMemo(() => { + const brain = app.brain(); + const colorKey = BRAIN_STATUS_COLORS[brain.status]; + return theme.colors[colorKey]; + }); - <box flexDirection="row" gap={2}> - <box flexDirection="row"> - <text fg={theme.colors.textDim}>Provider: </text> - <text fg={theme.colors.secondary}>{app.provider()}</text> - </box> - <box flexDirection="row"> - <text fg={theme.colors.textDim}>Model: </text> - <text fg={theme.colors.accent}>{app.model() || "auto"}</text> - </box> - <Show when={app.sessionId()}> - <box flexDirection="row"> - <text fg={theme.colors.textDim}>Session: </text> - <text fg={theme.colors.info}> - {app.sessionId()?.replace("session-", "").slice(-5)} + const shouldShowBrainBanner = createMemo(() => { + if (BRAIN_DISABLED) return false; + const brain = app.brain(); + return brain.showBanner && brain.status === "disconnected"; + }); + + // Context window usage calculation + const contextUsage = createMemo(() => { + const stats = app.sessionStats(); + const totalTokens = stats.inputTokens + stats.outputTokens; + const maxTokens = stats.contextMaxTokens; + const usagePercent = maxTokens > 0 ? (totalTokens / maxTokens) * 100 : 0; + + let status: "normal" | "warning" | "critical" | "compacting" = "normal"; + if (app.isCompacting()) { + status = "compacting"; + } else if (usagePercent >= TOKEN_CRITICAL_THRESHOLD * 100) { + status = "critical"; + } else if (usagePercent >= TOKEN_WARNING_THRESHOLD * 100) { + status = "warning"; + } + + return { + current: totalTokens, + max: maxTokens, + percent: usagePercent, + status, + }; + }); + + const tokenColor = createMemo(() => { + const colorKey = TOKEN_STATUS_COLORS[contextUsage().status]; + return theme.colors[colorKey]; + }); + + return ( + <box flexDirection="column"> + {/* Brain Banner - shown when not connected */} + <Show when={shouldShowBrainBanner()}> + <box + flexDirection="row" + justifyContent="space-between" + paddingLeft={1} + paddingRight={1} + backgroundColor="#1a1a2e" + > + <box flexDirection="row" gap={1}> + <text fg="#ff69b4" attributes={TextAttributes.BOLD}> + {BRAIN_BANNER.EMOJI_CONNECTED} + </text> + <text fg="#ffffff" attributes={TextAttributes.BOLD}> + {BRAIN_BANNER.TITLE} + </text> + <text fg={theme.colors.textDim}>-</text> + <text fg={theme.colors.textDim}>{BRAIN_BANNER.CTA}:</text> + <text fg={theme.colors.info} attributes={TextAttributes.UNDERLINE}> + {BRAIN_BANNER.URL} </text> </box> - </Show> + <text fg={theme.colors.textDim}>[Ctrl+B dismiss]</text> + </box> + </Show> + + {/* Main Header */} + <box + flexDirection="row" + justifyContent="space-between" + paddingLeft={1} + paddingRight={1} + borderColor={theme.colors.border} + border={["bottom"]} + > + <box flexDirection="row" gap={1}> + <Show when={showBanner()}> + <text fg={theme.colors.primary} attributes={TextAttributes.BOLD}> + CodeTyper + </text> + </Show> + <text fg={theme.colors.textDim}>v{app.version()}</text> + <text fg={theme.colors.textDim}>|</text> + <box flexDirection="row"> + <text fg={modeColor()} attributes={TextAttributes.BOLD}> + [{MODE_LABELS[app.interactionMode()]}] + </text> + <Show when={app.currentAgent() !== "default"}> + <text fg={theme.colors.secondary} attributes={TextAttributes.BOLD}> + {" "} + @{app.currentAgent()} + </text> + </Show> + <text fg={theme.colors.textDim}> + {" "} + - {MODE_DESCRIPTIONS[app.interactionMode()]} + </text> + </box> + </box> + + <box flexDirection="row" gap={2}> + {/* Context Window Usage */} + <Show when={contextUsage().max > 0}> + <box flexDirection="row"> + <text fg={tokenColor()}> + {formatTokenCount(contextUsage().current)} + </text> + <text fg={theme.colors.textDim}>/</text> + <text fg={theme.colors.textDim}> + {formatTokenCount(contextUsage().max)} + </text> + <Show when={contextUsage().status === "compacting"}> + <text fg={theme.colors.info}> [compacting]</text> + </Show> + </box> + </Show> + + {/* Brain Status Indicator - hidden when BRAIN_DISABLED */} + <Show when={!BRAIN_DISABLED}> + <box flexDirection="row"> + <text fg={brainColor()}> + {app.brain().status === "connected" + ? BRAIN_BANNER.EMOJI_CONNECTED + : app.brain().status === "connecting" + ? "..." + : BRAIN_BANNER.EMOJI_DISCONNECTED} + </text> + <Show when={app.brain().status === "connected"}> + <text fg={theme.colors.textDim}> + {" "} + {app.brain().knowledgeCount}K/{app.brain().memoryCount}M + </text> + </Show> + </box> + </Show> + + <box flexDirection="row"> + <text fg={theme.colors.textDim}>Provider: </text> + <text fg={theme.colors.secondary}>{app.provider()}</text> + </box> + <box flexDirection="row"> + <text fg={theme.colors.textDim}>Model: </text> + <text fg={theme.colors.accent}>{app.model() || "auto"}</text> + </box> + <Show when={app.sessionId()}> + <box flexDirection="row"> + <text fg={theme.colors.textDim}>Session: </text> + <text fg={theme.colors.info}> + {app.sessionId()?.replace("session-", "").slice(-5)} + </text> + </box> + </Show> + </box> </box> </box> ); diff --git a/src/tui-solid/components/input-area.tsx b/src/tui-solid/components/input-area.tsx index eb84e57..7ca4150 100644 --- a/src/tui-solid/components/input-area.tsx +++ b/src/tui-solid/components/input-area.tsx @@ -94,7 +94,11 @@ export function InputArea(props: InputAreaProps) { mode === "permission_prompt" || mode === "learning_prompt" || mode === "help_menu" || - mode === "help_detail" + mode === "help_detail" || + mode === "brain_menu" || + mode === "brain_login" || + mode === "provider_select" || + mode === "mcp_browse" ); }); const placeholder = () => @@ -108,10 +112,10 @@ export function InputArea(props: InputAreaProps) { // Handle "/" to open command menu when input is empty // Handle Enter to submit (backup in case onSubmit doesn't fire) - // Handle Ctrl+Tab to toggle interaction mode + // Handle Ctrl+M to toggle interaction mode (Ctrl+Tab doesn't work in most terminals) useKeyboard((evt) => { - // Ctrl+Tab works even when locked or menus are open - if (evt.ctrl && evt.name === "tab") { + // Ctrl+M works even when locked or menus are open + if (evt.ctrl && evt.name === "m") { app.toggleInteractionMode(); evt.preventDefault(); evt.stopPropagation(); diff --git a/src/tui-solid/components/log-panel.tsx b/src/tui-solid/components/log-panel.tsx index b2bd5bf..209c76a 100644 --- a/src/tui-solid/components/log-panel.tsx +++ b/src/tui-solid/components/log-panel.tsx @@ -4,6 +4,7 @@ import type { ScrollBoxRenderable } from "@opentui/core"; import { useTheme } from "@tui-solid/context/theme"; import { useAppStore } from "@tui-solid/context/app"; import { LogEntryDisplay } from "@tui-solid/components/log-entry"; +import { ASCII_LOGO, ASCII_LOGO_GRADIENT, HOME_VARS } from "@constants/home"; const SCROLL_LINES = 3; const MOUSE_ENABLE = "\x1b[?1000h\x1b[?1006h"; @@ -141,10 +142,22 @@ export function LogPanel() { <Show when={hasContent()} fallback={ - <box flexGrow={1} alignItems="center" justifyContent="center"> - <text fg={theme.colors.textDim}> - No messages yet. Type your prompt below. - </text> + <box + flexGrow={1} + alignItems="center" + justifyContent="center" + flexDirection="column" + > + <For each={ASCII_LOGO}> + {(line, index) => ( + <text fg={ASCII_LOGO_GRADIENT[index()] ?? theme.colors.primary}> + {line} + </text> + )} + </For> + <box marginTop={2}> + <text fg={theme.colors.textDim}>{HOME_VARS.subTitle}</text> + </box> </box> } > diff --git a/src/tui-solid/components/status-bar.tsx b/src/tui-solid/components/status-bar.tsx index 9b0d527..fc8af7d 100644 --- a/src/tui-solid/components/status-bar.tsx +++ b/src/tui-solid/components/status-bar.tsx @@ -155,11 +155,6 @@ export function StatusBar() { const hints = createMemo(() => { const result: string[] = []; - // Show mode toggle hint when idle - if (!isProcessing()) { - result.push("^Tab toggle mode"); - } - if (isProcessing()) { result.push( app.interruptPending() @@ -168,10 +163,6 @@ export function StatusBar() { ); } - if (app.todosVisible()) { - result.push(STATUS_HINTS.TOGGLE_TODOS); - } - result.push(formatDuration(elapsed())); if (totalTokens() > 0) { diff --git a/src/tui-solid/context/app.tsx b/src/tui-solid/context/app.tsx index 3feea83..db1f0e7 100644 --- a/src/tui-solid/context/app.tsx +++ b/src/tui-solid/context/app.tsx @@ -16,6 +16,7 @@ import type { SuggestionState, } from "@/types/tui"; import type { ProviderModel } from "@/types/providers"; +import type { BrainConnectionStatus, BrainUser } from "@/types/brain"; interface AppStore { mode: AppMode; @@ -44,6 +45,13 @@ interface AppStore { streamingLog: StreamingLogState; suggestions: SuggestionState; cascadeEnabled: boolean; + brain: { + status: BrainConnectionStatus; + user: BrainUser | null; + knowledgeCount: number; + memoryCount: number; + showBanner: boolean; + }; } interface AppContextValue { @@ -81,6 +89,13 @@ interface AppContextValue { streamingLogIsActive: Accessor<boolean>; suggestions: Accessor<SuggestionState>; cascadeEnabled: Accessor<boolean>; + brain: Accessor<{ + status: BrainConnectionStatus; + user: BrainUser | null; + knowledgeCount: number; + memoryCount: number; + showBanner: boolean; + }>; // Mode actions setMode: (mode: AppMode) => void; @@ -138,6 +153,7 @@ interface AppContextValue { stopThinking: () => void; addTokens: (input: number, output: number) => void; resetSessionStats: () => void; + setContextMaxTokens: (maxTokens: number) => void; // UI state actions toggleTodos: () => void; @@ -161,6 +177,13 @@ interface AppContextValue { hideSuggestions: () => void; showSuggestions: () => void; + // Brain actions + setBrainStatus: (status: BrainConnectionStatus) => void; + setBrainUser: (user: BrainUser | null) => void; + setBrainCounts: (knowledge: number, memory: number) => void; + setBrainShowBanner: (show: boolean) => void; + dismissBrainBanner: () => void; + // Computed isInputLocked: () => boolean; } @@ -174,6 +197,7 @@ const createInitialSessionStats = (): SessionStats => ({ outputTokens: 0, thinkingStartTime: null, lastThinkingDuration: 0, + contextMaxTokens: 128000, // Default, updated when model is selected }); const createInitialStreamingState = (): StreamingLogState => ({ @@ -225,6 +249,13 @@ export const { provider: AppStoreProvider, use: useAppStore } = streamingLog: createInitialStreamingState(), suggestions: createInitialSuggestionState(), cascadeEnabled: true, + brain: { + status: "disconnected" as BrainConnectionStatus, + user: null, + knowledgeCount: 0, + memoryCount: 0, + showBanner: true, + }, }); // Input insert function (set by InputArea) @@ -272,6 +303,7 @@ export const { provider: AppStoreProvider, use: useAppStore } = const streamingLogIsActive = (): boolean => store.streamingLog.isStreaming; const suggestions = (): SuggestionState => store.suggestions; const cascadeEnabled = (): boolean => store.cascadeEnabled; + const brain = () => store.brain; // Mode actions const setMode = (newMode: AppMode): void => { @@ -469,6 +501,27 @@ export const { provider: AppStoreProvider, use: useAppStore } = setStore("cascadeEnabled", !store.cascadeEnabled); }; + // Brain actions + const setBrainStatus = (status: BrainConnectionStatus): void => { + setStore("brain", { ...store.brain, status }); + }; + + const setBrainUser = (user: BrainUser | null): void => { + setStore("brain", { ...store.brain, user }); + }; + + const setBrainCounts = (knowledgeCount: number, memoryCount: number): void => { + setStore("brain", { ...store.brain, knowledgeCount, memoryCount }); + }; + + const setBrainShowBanner = (showBanner: boolean): void => { + setStore("brain", { ...store.brain, showBanner }); + }; + + const dismissBrainBanner = (): void => { + setStore("brain", { ...store.brain, showBanner: false }); + }; + // Session stats actions const startThinking = (): void => { setStore("sessionStats", { @@ -502,6 +555,13 @@ export const { provider: AppStoreProvider, use: useAppStore } = setStore("sessionStats", createInitialSessionStats()); }; + const setContextMaxTokens = (maxTokens: number): void => { + setStore("sessionStats", { + ...store.sessionStats, + contextMaxTokens: maxTokens, + }); + }; + // UI state actions const toggleTodos = (): void => { setStore("todosVisible", !store.todosVisible); @@ -698,6 +758,7 @@ export const { provider: AppStoreProvider, use: useAppStore } = streamingLogIsActive, suggestions, cascadeEnabled, + brain, // Mode actions setMode, @@ -752,11 +813,19 @@ export const { provider: AppStoreProvider, use: useAppStore } = setCascadeEnabled, toggleCascadeEnabled, + // Brain actions + setBrainStatus, + setBrainUser, + setBrainCounts, + setBrainShowBanner, + dismissBrainBanner, + // Session stats actions startThinking, stopThinking, addTokens, resetSessionStats, + setContextMaxTokens, // UI state actions toggleTodos, @@ -818,6 +887,13 @@ const defaultAppState = { isCompacting: false, streamingLog: createInitialStreamingState(), suggestions: createInitialSuggestionState(), + brain: { + status: "disconnected" as BrainConnectionStatus, + user: null, + knowledgeCount: 0, + memoryCount: 0, + showBanner: true, + }, }; export const appStore = { @@ -850,6 +926,7 @@ export const appStore = { isCompacting: storeRef.isCompacting(), streamingLog: storeRef.streamingLog(), suggestions: storeRef.suggestions(), + brain: storeRef.brain(), }; }, @@ -958,6 +1035,11 @@ export const appStore = { storeRef.resetSessionStats(); }, + setContextMaxTokens: (maxTokens: number): void => { + if (!storeRef) return; + storeRef.setContextMaxTokens(maxTokens); + }, + toggleTodos: (): void => { if (!storeRef) return; storeRef.toggleTodos(); @@ -1027,4 +1109,29 @@ export const appStore = { if (!storeRef) return; storeRef.toggleCascadeEnabled(); }, + + setBrainStatus: (status: BrainConnectionStatus): void => { + if (!storeRef) return; + storeRef.setBrainStatus(status); + }, + + setBrainUser: (user: BrainUser | null): void => { + if (!storeRef) return; + storeRef.setBrainUser(user); + }, + + setBrainCounts: (knowledge: number, memory: number): void => { + if (!storeRef) return; + storeRef.setBrainCounts(knowledge, memory); + }, + + setBrainShowBanner: (show: boolean): void => { + if (!storeRef) return; + storeRef.setBrainShowBanner(show); + }, + + dismissBrainBanner: (): void => { + if (!storeRef) return; + storeRef.dismissBrainBanner(); + }, }; diff --git a/src/tui-solid/routes/home.tsx b/src/tui-solid/routes/home.tsx index b07764c..8a8af27 100644 --- a/src/tui-solid/routes/home.tsx +++ b/src/tui-solid/routes/home.tsx @@ -53,8 +53,7 @@ export function Home(props: HomeProps) { > <Logo /> - <box marginTop={2} flexDirection="column" alignItems="center"> - <text fg={theme.colors.textDim}>{HOME_VARS.title}</text> + <box marginTop={2}> <text fg={theme.colors.textDim}>{HOME_VARS.subTitle}</text> </box> </box> diff --git a/src/tui-solid/routes/session.tsx b/src/tui-solid/routes/session.tsx index a3cd3ee..b1f229f 100644 --- a/src/tui-solid/routes/session.tsx +++ b/src/tui-solid/routes/session.tsx @@ -21,6 +21,8 @@ import { HelpDetail } from "@tui-solid/components/help-detail"; import { TodoPanel } from "@tui-solid/components/todo-panel"; import { CenteredModal } from "@tui-solid/components/centered-modal"; import { DebugLogPanel } from "@tui-solid/components/debug-log-panel"; +import { BrainMenu } from "@tui-solid/components/brain-menu"; +import { BRAIN_DISABLED } from "@constants/brain"; import type { PermissionScope, LearningScope, InteractionMode } from "@/types/tui"; import type { MCPAddFormData } from "@/types/mcp"; @@ -60,6 +62,9 @@ interface SessionProps { scope?: LearningScope, editedContent?: string, ) => void; + onBrainSetJwtToken?: (jwtToken: string) => Promise<void>; + onBrainSetApiKey?: (apiKey: string) => Promise<void>; + onBrainLogout?: () => Promise<void>; plan?: { id: string; title: string; @@ -113,6 +118,10 @@ export function Session(props: SessionProps) { app.transitionFromCommandMenu("help_menu"); return; } + if (lowerCommand === "brain" && !BRAIN_DISABLED) { + app.transitionFromCommandMenu("brain_menu"); + return; + } // For other commands, close menu and process through handler app.closeCommandMenu(); props.onCommand(command); @@ -192,6 +201,22 @@ export function Session(props: SessionProps) { app.setMode("idle"); }; + const handleBrainMenuClose = (): void => { + app.setMode("idle"); + }; + + const handleBrainSetJwtToken = async (jwtToken: string): Promise<void> => { + await props.onBrainSetJwtToken?.(jwtToken); + }; + + const handleBrainSetApiKey = async (apiKey: string): Promise<void> => { + await props.onBrainSetApiKey?.(apiKey); + }; + + const handleBrainLogout = async (): Promise<void> => { + await props.onBrainLogout?.(); + }; + return ( <box flexDirection="column" @@ -362,6 +387,18 @@ export function Session(props: SessionProps) { /> </CenteredModal> </Match> + + <Match when={app.mode() === "brain_menu" && !BRAIN_DISABLED}> + <CenteredModal> + <BrainMenu + onSetJwtToken={handleBrainSetJwtToken} + onSetApiKey={handleBrainSetApiKey} + onLogout={handleBrainLogout} + onClose={handleBrainMenuClose} + isActive={app.mode() === "brain_menu"} + /> + </CenteredModal> + </Match> </Switch> </box> ); diff --git a/src/tui/App.tsx b/src/tui/App.tsx index 679fe9a..139215c 100644 --- a/src/tui/App.tsx +++ b/src/tui/App.tsx @@ -24,7 +24,6 @@ import { MCPBrowser, TodoPanel, FilePicker, - HomeContent, SessionHeader, } from "@tui/components/index"; import { InputLine, calculateLineStartPos } from "@tui/components/input-line"; @@ -88,6 +87,8 @@ export function App({ const exitPending = useAppStore((state) => state.exitPending); const setExitPending = useAppStore((state) => state.setExitPending); const toggleTodos = useAppStore((state) => state.toggleTodos); + const toggleInteractionMode = useAppStore((state) => state.toggleInteractionMode); + const interactionMode = useAppStore((state) => state.interactionMode); const startThinking = useAppStore((state) => state.startThinking); const stopThinking = useAppStore((state) => state.stopThinking); const scrollUp = useAppStore((state) => state.scrollUp); @@ -98,6 +99,8 @@ export function App({ const setScreenMode = useAppStore((state) => state.setScreenMode); const logs = useAppStore((state) => state.logs); const sessionStats = useAppStore((state) => state.sessionStats); + const brain = useAppStore((state) => state.brain); + const dismissBrainBanner = useAppStore((state) => state.dismissBrainBanner); // Local input state const [inputBuffer, setInputBuffer] = useState(""); @@ -356,9 +359,24 @@ export function App({ // Global input handler for Ctrl+C, Ctrl+D, Ctrl+T, scroll (always active) useInput((input, key) => { - // Handle Ctrl+T to toggle todos visibility + // Handle Ctrl+M to toggle interaction mode (Ctrl+Tab doesn't work in most terminals) + if (key.ctrl && input === "m") { + toggleInteractionMode(); + // Note: The log will show the new mode after toggle + const newMode = useAppStore.getState().interactionMode; + addLog({ + type: "system", + content: `Switched to ${newMode} mode (Ctrl+M)`, + }); + return; + } + + // Handle Ctrl+T to toggle todos visibility (only in agent/code-review modes) if (key.ctrl && input === "t") { - toggleTodos(); + const currentMode = useAppStore.getState().interactionMode; + if (currentMode === "agent" || currentMode === "code-review") { + toggleTodos(); + } return; } @@ -548,8 +566,8 @@ export function App({ pastedBlocks: updatedBlocks, })); } - } else if (input === "v") { - // Handle Ctrl+V for image paste + } else if (input === "v" || input === "\x16") { + // Handle Ctrl+V for image paste (v or raw control character) readClipboardImage().then((image) => { if (image) { setPasteState((prev) => ({ @@ -562,12 +580,31 @@ export function App({ }); } }); + } else if (input === "i") { + // Handle Ctrl+I as alternative for image paste + readClipboardImage().then((image) => { + if (image) { + setPasteState((prev) => ({ + ...prev, + pastedImages: [...prev.pastedImages, image], + })); + addLog({ + type: "system", + content: `Image attached (${image.mediaType})`, + }); + } else { + addLog({ + type: "system", + content: "No image found in clipboard", + }); + } + }); } return; } // Handle Cmd+V (macOS) for image paste - if (key.meta && input === "v") { + if (key.meta && (input === "v" || input === "\x16")) { readClipboardImage().then((image) => { if (image) { setPasteState((prev) => ({ @@ -662,36 +699,31 @@ export function App({ // Calculate token count for session header const totalTokens = sessionStats.inputTokens + sessionStats.outputTokens; - const isHomeMode = screenMode === "home" && logs.length === 0; return ( <Box flexDirection="column" height="100%"> - {/* Show session header only when in session mode */} - {!isHomeMode && ( - <> - <SessionHeader - title={sessionId ?? "New session"} - tokenCount={totalTokens} - contextPercentage={15} - cost={0} - version={version} - /> - <StatusBar /> - </> - )} + {/* Always show session header and status bar */} + <SessionHeader + title={sessionId ?? "New session"} + tokenCount={totalTokens} + contextPercentage={15} + cost={0} + version={version} + interactionMode={interactionMode} + brain={brain} + onDismissBrainBanner={dismissBrainBanner} + /> + <StatusBar /> <Box flexDirection="column" flexGrow={1}> - {/* Show home content or session content */} - {isHomeMode ? ( - <HomeContent provider={provider} model={model} version={version} /> - ) : ( - <Box flexDirection="row" flexGrow={1}> - <Box flexDirection="column" flexGrow={1}> - <LogPanel /> - </Box> - <TodoPanel /> + {/* Main content area with all panes */} + <Box flexDirection="row" flexGrow={1}> + <Box flexDirection="column" flexGrow={1}> + {/* LogPanel shows logo when empty, logs otherwise */} + <LogPanel /> </Box> - )} + <TodoPanel /> + </Box> <PermissionModal /> <LearningModal /> @@ -736,6 +768,7 @@ export function App({ {isMCPSelectOpen && ( <MCPSelect onClose={handleMCPSelectClose} + onBrowse={() => setMode("mcp_browse")} isActive={isMCPSelectOpen} /> )} @@ -808,7 +841,7 @@ export function App({ <Box marginTop={1}> <Text dimColor> - Enter to send • Alt+Enter for newline • @ to add files • Ctrl+V to paste image + Enter • @ files • Ctrl+M mode • Ctrl+I image </Text> </Box> </Box> diff --git a/src/tui/components/CommandMenu.tsx b/src/tui/components/CommandMenu.tsx index db93f02..0adbaa8 100644 --- a/src/tui/components/CommandMenu.tsx +++ b/src/tui/components/CommandMenu.tsx @@ -2,9 +2,10 @@ * CommandMenu Component - Slash command selection menu * * Shows when user types '/' and provides filterable command list + * Supports scrolling for small terminal windows */ -import React, { useMemo } from "react"; +import React, { useMemo, useState, useEffect } from "react"; import { Box, Text, useInput } from "ink"; import { useAppStore } from "@tui/store"; import type { @@ -17,6 +18,9 @@ import { SLASH_COMMANDS, COMMAND_CATEGORIES } from "@constants/tui-components"; // Re-export for backwards compatibility export { SLASH_COMMANDS } from "@constants/tui-components"; +// Maximum visible items before scrolling +const MAX_VISIBLE = 12; + interface CommandWithIndex extends SlashCommand { flatIndex: number; } @@ -58,12 +62,29 @@ export function CommandMenu({ (state) => state.setCommandSelectedIndex, ); + // Scroll offset for viewport + const [scrollOffset, setScrollOffset] = useState(0); + // Filter commands based on input const filteredCommands = useMemo( () => filterCommands(SLASH_COMMANDS, commandMenu.filter), [commandMenu.filter], ); + // Reset scroll when filter changes + useEffect(() => { + setScrollOffset(0); + }, [commandMenu.filter]); + + // Ensure selected index is visible + useEffect(() => { + if (commandMenu.selectedIndex < scrollOffset) { + setScrollOffset(commandMenu.selectedIndex); + } else if (commandMenu.selectedIndex >= scrollOffset + MAX_VISIBLE) { + setScrollOffset(commandMenu.selectedIndex - MAX_VISIBLE + 1); + } + }, [commandMenu.selectedIndex, scrollOffset]); + // Handle keyboard input useInput( (input, key) => { @@ -81,7 +102,6 @@ export function CommandMenu({ if (filteredCommands.length > 0) { const selected = filteredCommands[commandMenu.selectedIndex]; if (selected) { - // handleCommandSelect will close the menu onSelect(selected.name); } } @@ -113,7 +133,6 @@ export function CommandMenu({ if (filteredCommands.length > 0) { const selected = filteredCommands[commandMenu.selectedIndex]; if (selected) { - // handleCommandSelect will close the menu onSelect(selected.name); } } @@ -154,6 +173,32 @@ export function CommandMenu({ })), ); + // Calculate visible items with scroll + const totalItems = filteredCommands.length; + const hasScrollUp = scrollOffset > 0; + const hasScrollDown = scrollOffset + MAX_VISIBLE < totalItems; + + // Get visible commands + const visibleCommands = commandsWithIndex.slice( + scrollOffset, + scrollOffset + MAX_VISIBLE, + ); + + // Group visible commands by category for display + const visibleGrouped: Array<{ + category: CommandCategory; + commands: CommandWithIndex[]; + }> = []; + + for (const cmd of visibleCommands) { + const existingGroup = visibleGrouped.find((g) => g.category === cmd.category); + if (existingGroup) { + existingGroup.commands.push(cmd); + } else { + visibleGrouped.push({ category: cmd.category, commands: [cmd] }); + } + } + return ( <Box flexDirection="column" @@ -168,23 +213,26 @@ export function CommandMenu({ </Text> {commandMenu.filter && <Text dimColor> - filtering: </Text>} {commandMenu.filter && <Text color="yellow">{commandMenu.filter}</Text>} + <Text dimColor> ({totalItems})</Text> </Box> + {hasScrollUp && ( + <Box justifyContent="center"> + <Text color="gray">↑ more ({scrollOffset} above)</Text> + </Box> + )} + {filteredCommands.length === 0 ? ( <Text dimColor>No commands match "{commandMenu.filter}"</Text> ) : ( <Box flexDirection="column"> - {groupedCommands.map((group) => ( + {visibleGrouped.map((group) => ( <Box key={group.category} flexDirection="column" marginBottom={1}> <Text dimColor bold> {capitalizeCategory(group.category)} </Text> {group.commands.map((cmd) => { - const cmdWithIndex = commandsWithIndex.find( - (c) => c.name === cmd.name, - ); - const isSelected = - cmdWithIndex?.flatIndex === commandMenu.selectedIndex; + const isSelected = cmd.flatIndex === commandMenu.selectedIndex; return ( <Box key={cmd.name}> <Text @@ -205,6 +253,12 @@ export function CommandMenu({ </Box> )} + {hasScrollDown && ( + <Box justifyContent="center"> + <Text color="gray">↓ more ({totalItems - scrollOffset - MAX_VISIBLE} below)</Text> + </Box> + )} + <Box marginTop={1}> <Text dimColor> Esc to close | Enter/Tab to select | Type to filter diff --git a/src/tui/components/MCPSelect.tsx b/src/tui/components/MCPSelect.tsx index 79c593b..d5fdc7b 100644 --- a/src/tui/components/MCPSelect.tsx +++ b/src/tui/components/MCPSelect.tsx @@ -18,15 +18,19 @@ import type { MCPServerInstance, MCPServerConfig } from "@/types/mcp"; interface MCPSelectProps { onClose: () => void; + onBrowse?: () => void; isActive?: boolean; } type MenuMode = "list" | "add_name" | "add_command" | "add_args"; +type ActionType = "add" | "browse" | "search" | "popular"; + interface MenuItem { id: string; name: string; type: "server" | "action"; + actionType?: ActionType; server?: MCPServerInstance; config?: MCPServerConfig; } @@ -42,6 +46,7 @@ const STATE_COLORS: Record<string, string> = { export function MCPSelect({ onClose, + onBrowse, isActive = true, }: MCPSelectProps): React.ReactElement { const [servers, setServers] = useState<Map<string, MCPServerInstance>>( @@ -76,11 +81,26 @@ export function MCPSelect({ const menuItems = useMemo((): MenuItem[] => { const items: MenuItem[] = []; - // Add "Add new server" action + // Add action items first + items.push({ + id: "__browse__", + name: "🔍 Browse & Search servers", + type: "action", + actionType: "browse", + }); + + items.push({ + id: "__popular__", + name: "⭐ Popular servers", + type: "action", + actionType: "popular", + }); + items.push({ id: "__add__", - name: "+ Add new MCP server", + name: "+ Add server manually", type: "action", + actionType: "add", }); // Add servers @@ -231,9 +251,36 @@ export function MCPSelect({ if (filteredItems.length > 0) { const selected = filteredItems[selectedIndex]; if (selected) { - if (selected.type === "action" && selected.id === "__add__") { - setMode("add_name"); - setMessage(null); + if (selected.type === "action") { + const actionHandlers: Record<ActionType, () => void> = { + add: () => { + setMode("add_name"); + setMessage(null); + }, + browse: () => { + if (onBrowse) { + onBrowse(); + } else { + onClose(); + } + }, + popular: () => { + if (onBrowse) { + onBrowse(); + } else { + onClose(); + } + }, + search: () => { + if (onBrowse) { + onBrowse(); + } else { + onClose(); + } + }, + }; + const handler = actionHandlers[selected.actionType || "add"]; + handler(); } else if (selected.type === "server") { toggleServer(selected); } diff --git a/src/tui/components/home/SessionHeader.tsx b/src/tui/components/home/SessionHeader.tsx index 9381932..d7e2eca 100644 --- a/src/tui/components/home/SessionHeader.tsx +++ b/src/tui/components/home/SessionHeader.tsx @@ -1,12 +1,13 @@ /** * SessionHeader Component - * Header showing session title, token count, cost, and version + * Header showing session title, token count, cost, version, and brain status */ import React from "react"; import { Box, Text } from "ink"; import { useThemeColors } from "@tui/hooks/useThemeStore"; import type { SessionHeaderProps } from "@types/home-screen"; +import { BRAIN_BANNER } from "@constants/brain"; const formatCost = (cost: number): string => { return new Intl.NumberFormat("en-US", { @@ -22,12 +23,41 @@ const formatTokenCount = (count: number): string => { return count.toLocaleString(); }; +const MODE_COLORS: Record<string, string> = { + agent: "cyan", + ask: "green", + "code-review": "yellow", +}; + +const MODE_LABELS: Record<string, string> = { + agent: "AGENT", + ask: "ASK", + "code-review": "REVIEW", +}; + +const BRAIN_STATUS_COLORS: Record<string, string> = { + connected: "green", + connecting: "yellow", + disconnected: "gray", + error: "red", +}; + +const BRAIN_STATUS_ICONS: Record<string, string> = { + connected: BRAIN_BANNER.EMOJI_CONNECTED, + connecting: "...", + disconnected: BRAIN_BANNER.EMOJI_DISCONNECTED, + error: "!", +}; + export const SessionHeader: React.FC<SessionHeaderProps> = ({ title, tokenCount, contextPercentage, cost, version, + interactionMode = "agent", + brain, + onDismissBrainBanner, }) => { const colors = useThemeColors(); @@ -36,33 +66,94 @@ export const SessionHeader: React.FC<SessionHeaderProps> = ({ ? `${formatTokenCount(tokenCount)} ${contextPercentage}%` : formatTokenCount(tokenCount); - return ( - <Box - flexShrink={0} - borderStyle="single" - borderLeft={true} - borderRight={false} - borderTop={false} - borderBottom={false} - borderColor={colors.border} - paddingTop={1} - paddingBottom={1} - paddingLeft={2} - paddingRight={1} - backgroundColor={colors.backgroundPanel} - > - <Box flexDirection="row" justifyContent="space-between" width="100%"> - {/* Title */} - <Text color={colors.text}> - <Text bold>#</Text> <Text bold>{title}</Text> - </Text> + const modeColor = MODE_COLORS[interactionMode] || "cyan"; + const modeLabel = MODE_LABELS[interactionMode] || interactionMode.toUpperCase(); - {/* Context info and version */} - <Box flexDirection="row" gap={1} flexShrink={0}> - <Text color={colors.textDim}> - {contextInfo} ({formatCost(cost)}) - </Text> - <Text color={colors.textDim}>v{version}</Text> + const brainStatus = brain?.status ?? "disconnected"; + const brainColor = BRAIN_STATUS_COLORS[brainStatus] || "gray"; + const brainIcon = BRAIN_STATUS_ICONS[brainStatus] || BRAIN_BANNER.EMOJI_DISCONNECTED; + const showBrainBanner = brain?.showBanner && brainStatus === "disconnected"; + + return ( + <Box flexDirection="column" flexShrink={0}> + {/* Brain Banner - shown when not connected */} + {showBrainBanner && ( + <Box + paddingLeft={2} + paddingRight={2} + paddingTop={0} + paddingBottom={0} + backgroundColor="#1a1a2e" + > + <Box flexDirection="row" justifyContent="space-between" width="100%"> + <Box flexDirection="row" gap={1}> + <Text color="magenta" bold> + {BRAIN_BANNER.EMOJI_CONNECTED} + </Text> + <Text color="white" bold> + {BRAIN_BANNER.TITLE} + </Text> + <Text color="gray"> + {" "} + - {BRAIN_BANNER.CTA}:{" "} + </Text> + <Text color="cyan" underline> + {BRAIN_BANNER.URL} + </Text> + </Box> + <Text color="gray" dimColor> + [press q to dismiss] + </Text> + </Box> + </Box> + )} + + {/* Main Header */} + <Box + borderStyle="single" + borderLeft={true} + borderRight={false} + borderTop={false} + borderBottom={false} + borderColor={colors.border} + paddingTop={1} + paddingBottom={1} + paddingLeft={2} + paddingRight={1} + backgroundColor={colors.backgroundPanel} + > + <Box flexDirection="row" justifyContent="space-between" width="100%"> + {/* Title and Mode */} + <Box flexDirection="row" gap={2}> + <Text color={colors.text}> + <Text bold>#</Text> <Text bold>{title}</Text> + </Text> + <Text color={modeColor} bold> + [{modeLabel}] + </Text> + </Box> + + {/* Brain status, Context info and version */} + <Box flexDirection="row" gap={1} flexShrink={0}> + {/* Brain status indicator */} + {brain && ( + <Box flexDirection="row" gap={0}> + <Text color={brainColor}> + {brainIcon} + </Text> + {brainStatus === "connected" && ( + <Text color={colors.textDim}> + {" "} + {brain.knowledgeCount}K/{brain.memoryCount}M + </Text> + )} + </Box> + )} + <Text color={colors.textDim}> + {contextInfo} ({formatCost(cost)}) + </Text> + <Text color={colors.textDim}>v{version}</Text> + </Box> </Box> </Box> </Box> diff --git a/src/tui/components/log-panel/index.tsx b/src/tui/components/log-panel/index.tsx index ce1efda..71b4e75 100644 --- a/src/tui/components/log-panel/index.tsx +++ b/src/tui/components/log-panel/index.tsx @@ -7,6 +7,7 @@ * - User scroll detection (scrolling up pauses auto-scroll) * - Resume auto-scroll when user scrolls back to bottom * - Virtual scrolling for performance + * - Shows logo and welcome screen when no logs */ import React, { useEffect, useRef, useMemo } from "react"; @@ -20,6 +21,7 @@ import { import { LogEntryDisplay } from "@tui/components/log-panel/entry-renderers"; import { ThinkingIndicator } from "@tui/components/log-panel/thinking-indicator"; import { estimateEntryLines } from "@tui/components/log-panel/utils"; +import { Logo } from "@tui/components/home/Logo"; export function LogPanel(): React.ReactElement { const allLogs = useAppStore((state) => state.logs); @@ -149,7 +151,15 @@ export function LogPanel(): React.ReactElement { )} {logs.length === 0 && !thinkingMessage ? ( - <Text dimColor>No messages yet. Type your prompt below.</Text> + <Box flexDirection="column" flexGrow={1} alignItems="center" justifyContent="center"> + <Logo /> + <Box marginTop={1}> + <Text dimColor>AI Coding Assistant</Text> + </Box> + <Box marginTop={1}> + <Text dimColor>Type your prompt below • Ctrl+M to switch modes</Text> + </Box> + </Box> ) : ( <Box flexDirection="column" flexGrow={1}> {visibleEntries.map((entry) => ( diff --git a/src/tui/store.ts b/src/tui/store.ts index 8709328..424f9a4 100644 --- a/src/tui/store.ts +++ b/src/tui/store.ts @@ -7,6 +7,7 @@ import type { AppState, AppMode, ScreenMode, + InteractionMode, LogEntry, ToolCall, PermissionRequest, @@ -17,6 +18,15 @@ import type { SuggestionPrompt, } from "@/types/tui"; import type { ProviderModel } from "@/types/providers"; +import type { BrainConnectionStatus, BrainUser } from "@/types/brain"; + +const createInitialBrainState = () => ({ + status: "disconnected" as BrainConnectionStatus, + user: null as BrainUser | null, + knowledgeCount: 0, + memoryCount: 0, + showBanner: true, +}); const createInitialSessionStats = (): SessionStats => ({ startTime: Date.now(), @@ -44,10 +54,13 @@ const generateLogId = (): string => { return `log-${++logIdCounter}-${Date.now()}`; }; +const INTERACTION_MODES: InteractionMode[] = ["agent", "ask", "code-review"]; + export const useAppStore = create<AppState>((set, get) => ({ // Initial state mode: "idle", screenMode: "home", + interactionMode: "agent" as InteractionMode, inputBuffer: "", inputCursorPosition: 0, logs: [], @@ -78,10 +91,17 @@ export const useAppStore = create<AppState>((set, get) => ({ visibleHeight: 20, streamingLog: createInitialStreamingState(), suggestions: createInitialSuggestionState(), + brain: createInitialBrainState(), // Mode actions setMode: (mode: AppMode) => set({ mode }), setScreenMode: (screenMode: ScreenMode) => set({ screenMode }), + setInteractionMode: (interactionMode: InteractionMode) => set({ interactionMode }), + toggleInteractionMode: () => set((state) => { + const currentIndex = INTERACTION_MODES.indexOf(state.interactionMode); + const nextIndex = (currentIndex + 1) % INTERACTION_MODES.length; + return { interactionMode: INTERACTION_MODES[nextIndex] }; + }), // Input actions setInputBuffer: (buffer: string) => set({ inputBuffer: buffer }), @@ -451,6 +471,32 @@ export const useAppStore = create<AppState>((set, get) => ({ }, })), + // Brain actions + setBrainStatus: (status: BrainConnectionStatus) => + set((state) => ({ + brain: { ...state.brain, status }, + })), + + setBrainUser: (user: BrainUser | null) => + set((state) => ({ + brain: { ...state.brain, user }, + })), + + setBrainCounts: (knowledgeCount: number, memoryCount: number) => + set((state) => ({ + brain: { ...state.brain, knowledgeCount, memoryCount }, + })), + + setBrainShowBanner: (showBanner: boolean) => + set((state) => ({ + brain: { ...state.brain, showBanner }, + })), + + dismissBrainBanner: () => + set((state) => ({ + brain: { ...state.brain, showBanner: false }, + })), + // Computed - check if input should be locked isInputLocked: () => { const { mode } = get(); @@ -544,6 +590,14 @@ export const appStore = { useAppStore.getState().toggleTodos(); }, + toggleInteractionMode: () => { + useAppStore.getState().toggleInteractionMode(); + }, + + setInteractionMode: (mode: InteractionMode) => { + useAppStore.getState().setInteractionMode(mode); + }, + setInterruptPending: (pending: boolean) => { useAppStore.getState().setInterruptPending(pending); }, @@ -606,4 +660,25 @@ export const appStore = { resumeAutoScroll: () => { useAppStore.getState().resumeAutoScroll(); }, + + // Brain + setBrainStatus: (status: BrainConnectionStatus) => { + useAppStore.getState().setBrainStatus(status); + }, + + setBrainUser: (user: BrainUser | null) => { + useAppStore.getState().setBrainUser(user); + }, + + setBrainCounts: (knowledge: number, memory: number) => { + useAppStore.getState().setBrainCounts(knowledge, memory); + }, + + setBrainShowBanner: (show: boolean) => { + useAppStore.getState().setBrainShowBanner(show); + }, + + dismissBrainBanner: () => { + useAppStore.getState().dismissBrainBanner(); + }, }; diff --git a/src/types/agent-definition.ts b/src/types/agent-definition.ts new file mode 100644 index 0000000..ebc08b0 --- /dev/null +++ b/src/types/agent-definition.ts @@ -0,0 +1,91 @@ +/** + * Agent markdown definition types + * Agents are defined in markdown files with YAML frontmatter + * Location: .codetyper/agents/*.md + */ + +export type AgentTier = "fast" | "balanced" | "thorough"; + +export type AgentColor = + | "red" + | "green" + | "blue" + | "yellow" + | "cyan" + | "magenta" + | "white" + | "gray"; + +export interface AgentDefinition { + readonly name: string; + readonly description: string; + readonly tools: ReadonlyArray<string>; + readonly tier: AgentTier; + readonly color: AgentColor; + readonly maxTurns?: number; + readonly systemPrompt?: string; + readonly triggerPhrases?: ReadonlyArray<string>; + readonly capabilities?: ReadonlyArray<string>; + readonly permissions?: AgentPermissions; +} + +export interface AgentPermissions { + readonly allowedPaths?: ReadonlyArray<string>; + readonly deniedPaths?: ReadonlyArray<string>; + readonly allowedTools?: ReadonlyArray<string>; + readonly deniedTools?: ReadonlyArray<string>; + readonly requireApproval?: ReadonlyArray<string>; +} + +export interface AgentDefinitionFile { + readonly filePath: string; + readonly frontmatter: AgentFrontmatter; + readonly content: string; + readonly parsed: AgentDefinition; +} + +export interface AgentFrontmatter { + readonly name: string; + readonly description: string; + readonly tools: ReadonlyArray<string>; + readonly tier?: AgentTier; + readonly color?: AgentColor; + readonly maxTurns?: number; + readonly triggerPhrases?: ReadonlyArray<string>; + readonly capabilities?: ReadonlyArray<string>; + readonly allowedPaths?: ReadonlyArray<string>; + readonly deniedPaths?: ReadonlyArray<string>; +} + +export interface AgentRegistry { + readonly agents: ReadonlyMap<string, AgentDefinition>; + readonly byTrigger: ReadonlyMap<string, string>; + readonly byCapability: ReadonlyMap<string, ReadonlyArray<string>>; +} + +export interface AgentLoadResult { + readonly success: boolean; + readonly agent?: AgentDefinition; + readonly error?: string; + readonly filePath: string; +} + +export const DEFAULT_AGENT_DEFINITION: Partial<AgentDefinition> = { + tier: "balanced", + color: "cyan", + maxTurns: 10, + tools: ["read", "glob", "grep"], + capabilities: [], + triggerPhrases: [], +}; + +export const AGENT_TIER_MODELS: Record<AgentTier, string> = { + fast: "gpt-4o-mini", + balanced: "gpt-4o", + thorough: "o1", +}; + +export const AGENT_DEFINITION_SCHEMA = { + required: ["name", "description", "tools"], + optional: ["tier", "color", "maxTurns", "triggerPhrases", "capabilities", "allowedPaths", "deniedPaths"], +}; diff --git a/src/types/apply-patch.ts b/src/types/apply-patch.ts new file mode 100644 index 0000000..97fdb84 --- /dev/null +++ b/src/types/apply-patch.ts @@ -0,0 +1,145 @@ +/** + * Apply Patch Types + * + * Types for unified diff parsing and application. + * Supports fuzzy matching and rollback on failure. + */ + +/** + * Patch line type + */ +export type PatchLineType = + | "context" // Unchanged line (starts with space) + | "addition" // Added line (starts with +) + | "deletion" // Removed line (starts with -) + | "header"; // Hunk header + +/** + * Single line in a patch + */ +export interface PatchLine { + type: PatchLineType; + content: string; + originalLineNumber?: number; + newLineNumber?: number; +} + +/** + * Patch hunk (a contiguous block of changes) + */ +export interface PatchHunk { + oldStart: number; + oldLines: number; + newStart: number; + newLines: number; + lines: PatchLine[]; + header: string; +} + +/** + * Parsed patch file for a single file + */ +export interface ParsedFilePatch { + oldPath: string; + newPath: string; + hunks: PatchHunk[]; + isBinary: boolean; + isNew: boolean; + isDeleted: boolean; + isRenamed: boolean; +} + +/** + * Complete parsed patch (may contain multiple files) + */ +export interface ParsedPatch { + files: ParsedFilePatch[]; + rawPatch: string; +} + +/** + * Fuzzy match result + */ +export interface FuzzyMatchResult { + found: boolean; + lineNumber: number; + offset: number; + confidence: number; +} + +/** + * Hunk application result + */ +export interface HunkApplicationResult { + success: boolean; + hunkIndex: number; + appliedAt?: number; + error?: string; + fuzzyOffset?: number; +} + +/** + * File patch result + */ +export interface FilePatchResult { + success: boolean; + filePath: string; + hunksApplied: number; + hunksFailed: number; + hunkResults: HunkApplicationResult[]; + newContent?: string; + error?: string; +} + +/** + * Overall patch application result + */ +export interface ApplyPatchResult { + success: boolean; + filesPatched: number; + filesFailed: number; + fileResults: FilePatchResult[]; + rollbackAvailable: boolean; + error?: string; +} + +/** + * Apply patch parameters + */ +export interface ApplyPatchParams { + patch: string; + targetFile?: string; + dryRun?: boolean; + fuzz?: number; + reverse?: boolean; +} + +/** + * Rollback information + */ +export interface PatchRollback { + filePath: string; + originalContent: string; + patchedContent: string; + timestamp: number; +} + +/** + * Patch validation result + */ +export interface PatchValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; + fileCount: number; + hunkCount: number; +} + +/** + * Context line match options + */ +export interface ContextMatchOptions { + fuzz: number; + ignoreWhitespace: boolean; + ignoreCase: boolean; +} diff --git a/src/types/background-task.ts b/src/types/background-task.ts new file mode 100644 index 0000000..e0fb611 --- /dev/null +++ b/src/types/background-task.ts @@ -0,0 +1,113 @@ +/** + * Background task types for async operations + * Allows tasks to run in background while user continues working + * Triggered with Ctrl+B or /background command + */ + +export type BackgroundTaskStatus = + | "pending" + | "running" + | "paused" + | "completed" + | "failed" + | "cancelled"; + +export type BackgroundTaskPriority = "low" | "normal" | "high"; + +export interface BackgroundTask { + readonly id: string; + readonly name: string; + readonly description: string; + readonly status: BackgroundTaskStatus; + readonly priority: BackgroundTaskPriority; + readonly createdAt: number; + readonly startedAt?: number; + readonly completedAt?: number; + readonly progress: TaskProgress; + readonly result?: TaskResult; + readonly error?: TaskError; + readonly metadata: TaskMetadata; +} + +export interface TaskProgress { + readonly current: number; + readonly total: number; + readonly percentage: number; + readonly message: string; + readonly steps: ReadonlyArray<TaskStep>; +} + +export interface TaskStep { + readonly name: string; + readonly status: BackgroundTaskStatus; + readonly startedAt?: number; + readonly completedAt?: number; + readonly output?: string; +} + +export interface TaskResult { + readonly success: boolean; + readonly output: string; + readonly artifacts: ReadonlyArray<TaskArtifact>; + readonly summary: string; +} + +export interface TaskArtifact { + readonly type: "file" | "diff" | "report" | "data"; + readonly name: string; + readonly path?: string; + readonly content?: string; +} + +export interface TaskError { + readonly code: string; + readonly message: string; + readonly stack?: string; + readonly recoverable: boolean; +} + +export interface TaskMetadata { + readonly sessionId: string; + readonly agentId?: string; + readonly prompt?: string; + readonly tools: ReadonlyArray<string>; + readonly startedByUser: boolean; +} + +export interface BackgroundTaskConfig { + readonly maxConcurrent: number; + readonly defaultTimeout: number; + readonly retryOnFailure: boolean; + readonly maxRetries: number; + readonly notifyOnComplete: boolean; + readonly persistTasks: boolean; +} + +export interface TaskNotification { + readonly taskId: string; + readonly type: "started" | "progress" | "completed" | "failed"; + readonly message: string; + readonly timestamp: number; +} + +export interface BackgroundTaskStore { + readonly tasks: ReadonlyMap<string, BackgroundTask>; + readonly queue: ReadonlyArray<string>; + readonly running: ReadonlyArray<string>; + readonly completed: ReadonlyArray<string>; +} + +export const DEFAULT_BACKGROUND_TASK_CONFIG: BackgroundTaskConfig = { + maxConcurrent: 3, + defaultTimeout: 300000, // 5 minutes + retryOnFailure: false, + maxRetries: 1, + notifyOnComplete: true, + persistTasks: true, +}; + +export const BACKGROUND_TASK_PRIORITIES: Record<BackgroundTaskPriority, number> = { + low: 1, + normal: 5, + high: 10, +}; diff --git a/src/types/brain-cloud.ts b/src/types/brain-cloud.ts new file mode 100644 index 0000000..b0dbc17 --- /dev/null +++ b/src/types/brain-cloud.ts @@ -0,0 +1,194 @@ +/** + * Brain Cloud Sync Types + * + * Types for cloud synchronization of brain data. + */ + +/** + * Sync status + */ +export type BrainSyncStatus = + | "synced" + | "pending" + | "syncing" + | "conflict" + | "offline" + | "error"; + +/** + * Conflict resolution strategy + */ +export type ConflictStrategy = + | "local-wins" + | "remote-wins" + | "manual" + | "merge"; + +/** + * Sync direction + */ +export type SyncDirection = "push" | "pull" | "both"; + +/** + * Sync operation type + */ +export type SyncOperationType = + | "create" + | "update" + | "delete" + | "conflict"; + +/** + * Brain sync state + */ +export interface BrainSyncState { + status: BrainSyncStatus; + lastSyncAt: number | null; + lastPushAt: number | null; + lastPullAt: number | null; + pendingChanges: number; + conflictCount: number; + syncErrors: string[]; +} + +/** + * Cloud brain configuration + */ +export interface CloudBrainConfig { + enabled: boolean; + endpoint: string; + syncOnSessionEnd: boolean; + syncInterval: number; + conflictStrategy: ConflictStrategy; + retryAttempts: number; + retryDelay: number; +} + +/** + * Sync item representing a change + */ +export interface SyncItem { + id: string; + type: "concept" | "memory" | "relation"; + operation: SyncOperationType; + localVersion: number; + remoteVersion?: number; + data: unknown; + timestamp: number; + synced: boolean; +} + +/** + * Sync conflict + */ +export interface SyncConflict { + id: string; + itemId: string; + itemType: "concept" | "memory" | "relation"; + localData: unknown; + remoteData: unknown; + localVersion: number; + remoteVersion: number; + localTimestamp: number; + remoteTimestamp: number; + resolved: boolean; + resolution?: ConflictStrategy; + resolvedData?: unknown; +} + +/** + * Sync result + */ +export interface SyncResult { + success: boolean; + direction: SyncDirection; + itemsSynced: number; + itemsFailed: number; + conflicts: SyncConflict[]; + errors: string[]; + duration: number; + timestamp: number; +} + +/** + * Push request + */ +export interface PushRequest { + items: SyncItem[]; + projectId: number; + clientVersion: string; +} + +/** + * Push response + */ +export interface PushResponse { + success: boolean; + accepted: number; + rejected: number; + conflicts: SyncConflict[]; + serverVersion: number; + errors?: string[]; +} + +/** + * Pull request + */ +export interface PullRequest { + projectId: number; + sinceVersion: number; + sinceTimestamp: number; + limit?: number; +} + +/** + * Pull response + */ +export interface PullResponse { + success: boolean; + items: SyncItem[]; + serverVersion: number; + hasMore: boolean; + errors?: string[]; +} + +/** + * Offline queue item + */ +export interface OfflineQueueItem { + id: string; + item: SyncItem; + retryCount: number; + lastAttempt: number; + error?: string; +} + +/** + * Offline queue state + */ +export interface OfflineQueueState { + items: OfflineQueueItem[]; + totalSize: number; + oldestItem: number | null; +} + +/** + * Sync progress event + */ +export interface SyncProgressEvent { + phase: "preparing" | "pushing" | "pulling" | "resolving" | "completing"; + current: number; + total: number; + message: string; +} + +/** + * Sync options + */ +export interface SyncOptions { + direction?: SyncDirection; + force?: boolean; + conflictStrategy?: ConflictStrategy; + onProgress?: (event: SyncProgressEvent) => void; + abortSignal?: AbortSignal; +} diff --git a/src/types/brain-mcp.ts b/src/types/brain-mcp.ts new file mode 100644 index 0000000..f7a0f07 --- /dev/null +++ b/src/types/brain-mcp.ts @@ -0,0 +1,228 @@ +/** + * Brain MCP Server types + * Exposes Brain as an MCP server for external tools + */ + +export type BrainMcpToolName = + | "brain_recall" + | "brain_learn" + | "brain_search" + | "brain_relate" + | "brain_context" + | "brain_stats" + | "brain_projects"; + +export interface BrainMcpServerConfig { + readonly port: number; + readonly host: string; + readonly enableAuth: boolean; + readonly apiKeyHeader: string; + readonly allowedOrigins: ReadonlyArray<string>; + readonly rateLimit: RateLimitConfig; + readonly logging: LoggingConfig; +} + +export interface RateLimitConfig { + readonly enabled: boolean; + readonly maxRequests: number; + readonly windowMs: number; +} + +export interface LoggingConfig { + readonly enabled: boolean; + readonly level: "debug" | "info" | "warn" | "error"; + readonly logRequests: boolean; + readonly logResponses: boolean; +} + +export interface BrainMcpTool { + readonly name: BrainMcpToolName; + readonly description: string; + readonly inputSchema: McpInputSchema; + readonly handler: string; // Function name in brain service +} + +export interface McpInputSchema { + readonly type: "object"; + readonly properties: Record<string, McpPropertySchema>; + readonly required: ReadonlyArray<string>; +} + +export interface McpPropertySchema { + readonly type: "string" | "number" | "boolean" | "array" | "object"; + readonly description: string; + readonly default?: unknown; + readonly enum?: ReadonlyArray<string>; + readonly items?: McpPropertySchema; +} + +export interface BrainMcpRequest { + readonly method: string; + readonly params: { + readonly name: BrainMcpToolName; + readonly arguments: Record<string, unknown>; + }; + readonly id: string | number; +} + +export interface BrainMcpResponse { + readonly id: string | number; + readonly result?: { + readonly content: ReadonlyArray<McpContent>; + readonly isError?: boolean; + }; + readonly error?: McpError; +} + +export interface McpContent { + readonly type: "text" | "resource"; + readonly text?: string; + readonly resource?: McpResource; +} + +export interface McpResource { + readonly uri: string; + readonly mimeType: string; + readonly text?: string; +} + +export interface McpError { + readonly code: number; + readonly message: string; + readonly data?: unknown; +} + +export interface BrainMcpServerStatus { + readonly running: boolean; + readonly port: number; + readonly host: string; + readonly connectedClients: number; + readonly uptime: number; + readonly requestsServed: number; + readonly lastRequestAt?: number; +} + +export const DEFAULT_BRAIN_MCP_SERVER_CONFIG: BrainMcpServerConfig = { + port: 5002, + host: "localhost", + enableAuth: true, + apiKeyHeader: "X-Brain-API-Key", + allowedOrigins: ["*"], + rateLimit: { + enabled: true, + maxRequests: 100, + windowMs: 60000, // 1 minute + }, + logging: { + enabled: true, + level: "info", + logRequests: true, + logResponses: false, + }, +}; + +export const BRAIN_MCP_TOOLS: ReadonlyArray<BrainMcpTool> = [ + { + name: "brain_recall", + description: "Retrieve relevant concepts from the knowledge graph based on a query", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "The search query to find relevant concepts" }, + limit: { type: "number", description: "Maximum number of concepts to return", default: 5 }, + }, + required: ["query"], + }, + handler: "recall", + }, + { + name: "brain_learn", + description: "Store a new concept in the knowledge graph", + inputSchema: { + type: "object", + properties: { + name: { type: "string", description: "The name of the concept" }, + whatItDoes: { type: "string", description: "Description of what the concept does" }, + keywords: { type: "array", items: { type: "string" }, description: "Keywords for the concept" }, + patterns: { type: "array", items: { type: "string" }, description: "Code patterns related to the concept" }, + files: { type: "array", items: { type: "string" }, description: "Files related to the concept" }, + }, + required: ["name", "whatItDoes"], + }, + handler: "learn", + }, + { + name: "brain_search", + description: "Search memories using semantic similarity", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "The search query" }, + limit: { type: "number", description: "Maximum number of memories to return", default: 10 }, + type: { type: "string", description: "Memory type filter", enum: ["fact", "pattern", "correction", "preference", "context"] }, + }, + required: ["query"], + }, + handler: "searchMemories", + }, + { + name: "brain_relate", + description: "Create a relationship between two concepts", + inputSchema: { + type: "object", + properties: { + sourceConcept: { type: "string", description: "Name of the source concept" }, + targetConcept: { type: "string", description: "Name of the target concept" }, + relationType: { type: "string", description: "Type of relationship", enum: ["depends_on", "uses", "extends", "similar_to", "part_of", "implements", "contradicts"] }, + weight: { type: "number", description: "Strength of the relationship (0-1)", default: 0.5 }, + }, + required: ["sourceConcept", "targetConcept", "relationType"], + }, + handler: "relate", + }, + { + name: "brain_context", + description: "Build a context string from relevant knowledge for prompt injection", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "The context query" }, + maxConcepts: { type: "number", description: "Maximum concepts to include", default: 5 }, + }, + required: ["query"], + }, + handler: "getContext", + }, + { + name: "brain_stats", + description: "Get statistics about the knowledge graph", + inputSchema: { + type: "object", + properties: {}, + required: [], + }, + handler: "getStats", + }, + { + name: "brain_projects", + description: "List all Brain projects", + inputSchema: { + type: "object", + properties: {}, + required: [], + }, + handler: "listProjects", + }, +]; + +export const MCP_ERROR_CODES = { + PARSE_ERROR: -32700, + INVALID_REQUEST: -32600, + METHOD_NOT_FOUND: -32601, + INVALID_PARAMS: -32602, + INTERNAL_ERROR: -32603, + TOOL_NOT_FOUND: -32001, + UNAUTHORIZED: -32002, + RATE_LIMITED: -32003, + BRAIN_UNAVAILABLE: -32004, +}; diff --git a/src/types/brain-project.ts b/src/types/brain-project.ts new file mode 100644 index 0000000..abb8d49 --- /dev/null +++ b/src/types/brain-project.ts @@ -0,0 +1,113 @@ +/** + * Multi-project Brain support types + * Switch between different Brain projects/knowledge bases + */ + +export interface BrainProject { + readonly id: number; + readonly name: string; + readonly description: string; + readonly rootPath: string; + readonly createdAt: number; + readonly updatedAt: number; + readonly stats: BrainProjectStats; + readonly settings: BrainProjectSettings; + readonly isActive: boolean; +} + +export interface BrainProjectStats { + readonly conceptCount: number; + readonly memoryCount: number; + readonly relationshipCount: number; + readonly lastSyncAt?: number; + readonly totalTokensUsed: number; +} + +export interface BrainProjectSettings { + readonly autoLearn: boolean; + readonly autoRecall: boolean; + readonly recallLimit: number; + readonly contextInjection: boolean; + readonly syncEnabled: boolean; + readonly syncInterval: number; // minutes +} + +export interface BrainProjectCreateInput { + readonly name: string; + readonly description?: string; + readonly rootPath: string; + readonly settings?: Partial<BrainProjectSettings>; +} + +export interface BrainProjectUpdateInput { + readonly name?: string; + readonly description?: string; + readonly settings?: Partial<BrainProjectSettings>; +} + +export interface BrainProjectSwitchResult { + readonly success: boolean; + readonly previousProject?: BrainProject; + readonly currentProject: BrainProject; + readonly message: string; +} + +export interface BrainProjectListResult { + readonly projects: ReadonlyArray<BrainProject>; + readonly activeProjectId?: number; + readonly total: number; +} + +export interface BrainProjectExport { + readonly project: BrainProject; + readonly concepts: ReadonlyArray<ExportedConcept>; + readonly memories: ReadonlyArray<ExportedMemory>; + readonly relationships: ReadonlyArray<ExportedRelationship>; + readonly exportedAt: number; + readonly version: string; +} + +export interface ExportedConcept { + readonly name: string; + readonly whatItDoes: string; + readonly keywords: ReadonlyArray<string>; + readonly patterns: ReadonlyArray<string>; + readonly files: ReadonlyArray<string>; + readonly importance: number; +} + +export interface ExportedMemory { + readonly content: string; + readonly type: string; + readonly tags: ReadonlyArray<string>; + readonly createdAt: number; +} + +export interface ExportedRelationship { + readonly sourceConcept: string; + readonly targetConcept: string; + readonly relationType: string; + readonly weight: number; +} + +export interface BrainProjectImportResult { + readonly success: boolean; + readonly project: BrainProject; + readonly imported: { + readonly concepts: number; + readonly memories: number; + readonly relationships: number; + }; + readonly errors: ReadonlyArray<string>; +} + +export const DEFAULT_BRAIN_PROJECT_SETTINGS: BrainProjectSettings = { + autoLearn: true, + autoRecall: true, + recallLimit: 5, + contextInjection: true, + syncEnabled: false, + syncInterval: 30, +}; + +export const BRAIN_PROJECT_EXPORT_VERSION = "1.0.0"; diff --git a/src/types/brain.ts b/src/types/brain.ts new file mode 100644 index 0000000..686d371 --- /dev/null +++ b/src/types/brain.ts @@ -0,0 +1,232 @@ +/** + * Brain API Type Definitions + * + * Types for the CodeTyper Brain service - a knowledge graph API + * that provides long-term memory and context for the CLI + */ + +// ============================================================================ +// Authentication Types +// ============================================================================ + +export interface BrainUser { + id: number; + email: string; + display_name: string; +} + +export interface BrainLoginResponse { + success: boolean; + data: { + user: BrainUser; + access_token: string; + refresh_token: string; + expires_at: string; + }; +} + +export interface BrainRegisterResponse { + success: boolean; + data: { + user: BrainUser; + access_token: string; + refresh_token: string; + expires_at: string; + }; +} + +export interface BrainCredentials { + apiKey?: string; + accessToken?: string; + refreshToken?: string; + expiresAt?: string; + user?: BrainUser; +} + +// ============================================================================ +// Knowledge Graph Types +// ============================================================================ + +export interface BrainConcept { + id: number; + name: string; + what_it_does: string; + how_it_works?: string; + patterns?: string[]; + files?: string[]; + key_functions?: string[]; + aliases?: string[]; + importance: number; + similarity?: number; + related_concepts?: BrainRelatedConcept[]; +} + +export interface BrainRelatedConcept { + id: number; + name: string; + relation_type: BrainRelationType; + strength: number; +} + +export type BrainRelationType = + | "depends_on" + | "uses" + | "extends" + | "similar_to" + | "part_of" + | "implements" + | "contradicts"; + +export interface BrainLearnConceptRequest { + project_id: number; + name: string; + what_it_does: string; + how_it_works?: string; + patterns?: string[]; + files?: string[]; + key_functions?: string[]; + aliases?: string[]; + metadata?: Record<string, unknown>; +} + +export interface BrainRecallRequest { + query: string; + project_id: number; + limit?: number; +} + +export interface BrainRecallResponse { + success: boolean; + data: { + concepts: BrainConcept[]; + keywords_matched: string[]; + suggested_context: string; + }; +} + +export interface BrainContextRequest { + query: string; + project_id: number; + max_concepts?: number; +} + +export interface BrainContextResponse { + success: boolean; + data: { + context: string; + has_knowledge: boolean; + }; +} + +export interface BrainExtractRequest { + content: string; + project_id: number; + source?: string; +} + +export interface BrainExtractResponse { + success: boolean; + data: { + extracted: Array<{ + name: string; + what_it_does: string; + confidence: number; + }>; + stored: number; + updated: number; + skipped: number; + }; +} + +// ============================================================================ +// Memory Types +// ============================================================================ + +export type BrainMemoryType = + | "fact" + | "pattern" + | "correction" + | "preference" + | "context"; + +export interface BrainMemory { + id: number; + content: string; + similarity?: number; + node_type: BrainMemoryType; + importance: number; +} + +export interface BrainMemorySearchRequest { + query: string; + limit?: number; + threshold?: number; + project_id?: number; +} + +export interface BrainMemorySearchResponse { + memories: BrainMemory[]; + count: number; +} + +export interface BrainStoreMemoryRequest { + content: string; + type?: BrainMemoryType; + project_id?: number; +} + +// ============================================================================ +// Stats Types +// ============================================================================ + +export interface BrainKnowledgeStats { + total_concepts: number; + total_relations: number; + total_keywords: number; + by_pattern: Record<string, number>; + top_concepts: Array<{ + name: string; + access_count: number; + importance: number; + }>; +} + +export interface BrainMemoryStats { + totalNodes: number; + totalEdges: number; + byType: Record<BrainMemoryType, number>; +} + +// ============================================================================ +// Connection Status Types +// ============================================================================ + +export type BrainConnectionStatus = + | "disconnected" + | "connecting" + | "connected" + | "error"; + +export interface BrainState { + status: BrainConnectionStatus; + user: BrainUser | null; + projectId: number | null; + knowledgeCount: number; + memoryCount: number; + lastError: string | null; +} + +// ============================================================================ +// API Response Types +// ============================================================================ + +export interface BrainApiResponse<T> { + success: boolean; + data?: T; + error?: string; +} + +export interface BrainHealthResponse { + status: string; + version: string; +} diff --git a/src/types/confidence-filter.ts b/src/types/confidence-filter.ts new file mode 100644 index 0000000..2b3cc0c --- /dev/null +++ b/src/types/confidence-filter.ts @@ -0,0 +1,61 @@ +/** + * Confidence-based filtering types for PR review and agent outputs + * Filters issues/findings by confidence score threshold + */ + +export type ConfidenceLevel = "low" | "medium" | "high" | "critical"; + +export interface ConfidenceScore { + readonly value: number; // 0-100 + readonly level: ConfidenceLevel; + readonly factors: ReadonlyArray<ConfidenceFactor>; +} + +export interface ConfidenceFactor { + readonly name: string; + readonly weight: number; // 0-1 + readonly score: number; // 0-100 + readonly reason: string; +} + +export interface ConfidenceFilterConfig { + readonly minThreshold: number; // Minimum confidence to include (default: 80) + readonly includeValidation: boolean; // Run validation subagent + readonly groupByLevel: boolean; // Group results by confidence level + readonly showFactors: boolean; // Show contributing factors +} + +export interface FilteredResult<T> { + readonly item: T; + readonly confidence: ConfidenceScore; + readonly passed: boolean; + readonly validationResult?: ValidationResult; +} + +export interface ValidationResult { + readonly validated: boolean; + readonly adjustedConfidence: number; + readonly validatorNotes: string; +} + +export interface ConfidenceFilterStats { + readonly total: number; + readonly passed: number; + readonly filtered: number; + readonly byLevel: Record<ConfidenceLevel, number>; + readonly averageConfidence: number; +} + +export const CONFIDENCE_LEVELS: Record<ConfidenceLevel, { min: number; max: number; color: string }> = { + low: { min: 0, max: 49, color: "gray" }, + medium: { min: 50, max: 74, color: "yellow" }, + high: { min: 75, max: 89, color: "green" }, + critical: { min: 90, max: 100, color: "red" }, +}; + +export const DEFAULT_CONFIDENCE_FILTER_CONFIG: ConfidenceFilterConfig = { + minThreshold: 80, + includeValidation: true, + groupByLevel: true, + showFactors: false, +}; diff --git a/src/types/feature-dev.ts b/src/types/feature-dev.ts new file mode 100644 index 0000000..bb9d9c7 --- /dev/null +++ b/src/types/feature-dev.ts @@ -0,0 +1,236 @@ +/** + * Feature-Dev Workflow Types + * + * Types for the 7-phase guided development workflow. + */ + +/** + * Feature development phases + */ +export type FeatureDevPhase = + | "understand" // Clarify requirements + | "explore" // Find relevant code (parallel agents) + | "plan" // Design implementation + | "implement" // Write code + | "verify" // Run tests + | "review" // Self-review changes + | "finalize"; // Commit and cleanup + +/** + * Phase status + */ +export type PhaseStatus = + | "pending" + | "in_progress" + | "awaiting_approval" + | "approved" + | "completed" + | "skipped" + | "failed"; + +/** + * User checkpoint decision + */ +export type CheckpointDecision = + | "approve" + | "reject" + | "modify" + | "skip" + | "abort"; + +/** + * Exploration result from codebase analysis + */ +export interface ExplorationResult { + query: string; + findings: ExplorationFinding[]; + relevantFiles: string[]; + patterns: string[]; + timestamp: number; +} + +/** + * Single finding from exploration + */ +export interface ExplorationFinding { + type: "file" | "function" | "pattern" | "dependency"; + path: string; + line?: number; + description: string; + relevance: number; +} + +/** + * Implementation plan created during plan phase + */ +export interface ImplementationPlan { + summary: string; + steps: ImplementationStep[]; + risks: string[]; + dependencies: string[]; + testStrategy: string; + estimatedComplexity: "low" | "medium" | "high"; +} + +/** + * Single step in implementation plan + */ +export interface ImplementationStep { + id: string; + order: number; + description: string; + file: string; + changeType: "create" | "modify" | "delete"; + details: string; + dependencies: string[]; +} + +/** + * File change made during implementation + */ +export interface FileChange { + path: string; + changeType: "created" | "modified" | "deleted"; + additions: number; + deletions: number; + diff?: string; +} + +/** + * Test result from verification phase + */ +export interface TestResult { + passed: boolean; + totalTests: number; + passedTests: number; + failedTests: number; + skippedTests: number; + coverage?: number; + failures: TestFailure[]; + duration: number; +} + +/** + * Single test failure + */ +export interface TestFailure { + testName: string; + file: string; + error: string; + stack?: string; +} + +/** + * Review finding from self-review phase + */ +export interface ReviewFinding { + type: "issue" | "suggestion" | "question"; + severity: "critical" | "warning" | "info"; + file: string; + line?: number; + message: string; + suggestion?: string; +} + +/** + * User checkpoint for approval + */ +export interface Checkpoint { + phase: FeatureDevPhase; + title: string; + summary: string; + details: string[]; + requiresApproval: boolean; + suggestedAction: CheckpointDecision; +} + +/** + * Feature development workflow state + */ +export interface FeatureDevState { + id: string; + phase: FeatureDevPhase; + phaseStatus: PhaseStatus; + startedAt: number; + updatedAt: number; + + // Requirements from understand phase + requirements: string[]; + clarifications: Array<{ question: string; answer: string }>; + + // Results from explore phase + explorationResults: ExplorationResult[]; + relevantFiles: string[]; + + // Plan from plan phase + plan?: ImplementationPlan; + + // Changes from implement phase + changes: FileChange[]; + + // Results from verify phase + testResults?: TestResult; + + // Findings from review phase + reviewFindings: ReviewFinding[]; + + // Final status + commitHash?: string; + abortReason?: string; + + // Checkpoints history + checkpoints: Array<{ + checkpoint: Checkpoint; + decision: CheckpointDecision; + feedback?: string; + timestamp: number; + }>; +} + +/** + * Phase transition request + */ +export interface PhaseTransitionRequest { + fromPhase: FeatureDevPhase; + toPhase: FeatureDevPhase; + reason?: string; + skipValidation?: boolean; +} + +/** + * Phase execution context + */ +export interface PhaseExecutionContext { + state: FeatureDevState; + workingDir: string; + sessionId: string; + abortSignal?: AbortSignal; + onProgress?: (message: string) => void; + onCheckpoint?: (checkpoint: Checkpoint) => Promise<{ + decision: CheckpointDecision; + feedback?: string; + }>; +} + +/** + * Phase execution result + */ +export interface PhaseExecutionResult { + success: boolean; + phase: FeatureDevPhase; + nextPhase?: FeatureDevPhase; + checkpoint?: Checkpoint; + error?: string; + stateUpdates: Partial<FeatureDevState>; +} + +/** + * Workflow configuration + */ +export interface FeatureDevConfig { + requireCheckpoints: boolean; + autoRunTests: boolean; + autoCommit: boolean; + maxExplorationDepth: number; + parallelExplorations: number; +} diff --git a/src/types/home-screen.ts b/src/types/home-screen.ts index 92aeb5d..112e20e 100644 --- a/src/types/home-screen.ts +++ b/src/types/home-screen.ts @@ -3,6 +3,8 @@ * Type definitions for the welcome/home screen TUI */ +import type { BrainConnectionStatus, BrainUser } from "@/types/brain"; + /** Screen mode for determining which view to show */ export type ScreenMode = "home" | "session"; @@ -43,4 +45,13 @@ export interface SessionHeaderProps { contextPercentage?: number; cost: number; version: string; + interactionMode?: string; + brain?: { + status: BrainConnectionStatus; + user: BrainUser | null; + knowledgeCount: number; + memoryCount: number; + showBanner: boolean; + }; + onDismissBrainBanner?: () => void; } diff --git a/src/types/hooks.ts b/src/types/hooks.ts index 3fd0a71..a28f528 100644 --- a/src/types/hooks.ts +++ b/src/types/hooks.ts @@ -13,6 +13,7 @@ export type HookEventType = | "SessionStart" | "SessionEnd" | "UserPromptSubmit" + | "PreCompact" | "Stop"; /** @@ -129,6 +130,17 @@ export interface StopHookInput { reason: "interrupt" | "complete" | "error"; } +/** + * Input passed to PreCompact hooks via stdin + */ +export interface PreCompactHookInput { + sessionId: string; + workingDir: string; + currentTokens: number; + maxTokens: number; + messageCount: number; +} + /** * Union of all hook input types */ @@ -138,6 +150,7 @@ export type HookInput = | SessionStartHookInput | SessionEndHookInput | UserPromptSubmitHookInput + | PreCompactHookInput | StopHookInput; /** diff --git a/src/types/index.ts b/src/types/index.ts index d6253a7..e6e8066 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -21,6 +21,140 @@ export { createImageContent, } from "@/types/image"; +// Re-export brain types +export type { + BrainUser, + BrainCredentials, + BrainConnectionStatus, + BrainState, + BrainConcept, + BrainMemory, + BrainMemoryType, + BrainRelationType, + BrainRecallResponse, + BrainExtractResponse, + BrainKnowledgeStats, + BrainMemoryStats, +} from "@/types/brain"; + +// Re-export brain cloud sync types +export type { + BrainSyncStatus, + ConflictStrategy, + SyncDirection, + SyncOperationType, + BrainSyncState, + CloudBrainConfig, + SyncItem, + SyncConflict, + SyncResult, + PushRequest, + PushResponse, + PullRequest, + PullResponse, + OfflineQueueItem, + OfflineQueueState, + SyncProgressEvent, + SyncOptions, +} from "@/types/brain-cloud"; + +// Re-export skills types +export type { + SkillLoadLevel, + SkillTriggerType, + SkillExample, + SkillMetadata, + SkillBody, + SkillDefinition, + SkillMatch, + SkillContext, + SkillExecutionResult, + SkillRegistryState, + SkillFrontmatter, + ParsedSkillFile, +} from "@/types/skills"; + +// Re-export parallel execution types +export type { + ParallelTaskType, + TaskPriority, + ParallelTaskStatus, + ParallelAgentConfig, + ParallelTask, + ParallelExecutionResult, + ConflictCheckResult, + ConflictResolution, + ResourceLimits, + ResourceState, + AggregatedResults, + ParallelExecutorOptions, + BatchExecutionRequest, + SemaphoreState, + DeduplicationKey, + DeduplicationResult, +} from "@/types/parallel"; + +// Re-export apply-patch types +export type { + PatchLineType, + PatchLine, + PatchHunk, + ParsedFilePatch, + ParsedPatch, + FuzzyMatchResult, + HunkApplicationResult, + FilePatchResult, + ApplyPatchResult, + ApplyPatchParams, + PatchRollback, + PatchValidationResult, + ContextMatchOptions, +} from "@/types/apply-patch"; + +// Re-export feature-dev types +export type { + FeatureDevPhase, + PhaseStatus, + CheckpointDecision, + ExplorationResult, + ExplorationFinding, + ImplementationPlan, + ImplementationStep, + FileChange, + TestResult, + TestFailure, + ReviewFinding, + Checkpoint, + FeatureDevState, + PhaseTransitionRequest, + PhaseExecutionContext, + PhaseExecutionResult, + FeatureDevConfig, +} from "@/types/feature-dev"; + +// Re-export pr-review types +export type { + ReviewFindingType, + ReviewSeverity, + ConfidenceLevel, + PRReviewFinding, + DiffHunk, + ParsedFileDiff, + ParsedDiff, + ReviewerConfig, + SecurityReviewCriteria, + PerformanceReviewCriteria, + StyleReviewCriteria, + LogicReviewCriteria, + PRReviewConfig, + ReviewerResult, + ReviewRating, + ReviewRecommendation, + PRReviewReport, + PRReviewRequest, + ReviewFileContext, +} from "@/types/pr-review"; + export type IntentType = | "ask" | "code" @@ -134,3 +268,102 @@ export interface TerminalSpinner { fail(text: string): void; stop(): void; } + +// Re-export confidence filter types +export type { + ConfidenceLevel as ConfidenceFilterLevel, + ConfidenceScore, + ConfidenceFactor, + ConfidenceFilterConfig, + FilteredResult, + ValidationResult as ConfidenceValidationResult, + ConfidenceFilterStats, +} from "@/types/confidence-filter"; + +export { + CONFIDENCE_LEVELS, + DEFAULT_CONFIDENCE_FILTER_CONFIG, +} from "@/types/confidence-filter"; + +// Re-export agent definition types +export type { + AgentTier, + AgentColor, + AgentDefinition, + AgentPermissions, + AgentDefinitionFile, + AgentFrontmatter, + AgentRegistry, + AgentLoadResult, +} from "@/types/agent-definition"; + +export { + DEFAULT_AGENT_DEFINITION, + AGENT_TIER_MODELS, + AGENT_DEFINITION_SCHEMA, +} from "@/types/agent-definition"; + +// Re-export background task types +export type { + BackgroundTaskStatus, + BackgroundTaskPriority, + BackgroundTask, + TaskProgress, + TaskResult, + TaskError, + TaskMetadata, + TaskNotification, + TaskStep, + TaskArtifact, + BackgroundTaskConfig, + BackgroundTaskStore, +} from "@/types/background-task"; + +export { + DEFAULT_BACKGROUND_TASK_CONFIG, + BACKGROUND_TASK_PRIORITIES, +} from "@/types/background-task"; + +// Re-export brain project types +export type { + BrainProject, + BrainProjectStats, + BrainProjectSettings, + BrainProjectCreateInput, + BrainProjectUpdateInput, + BrainProjectSwitchResult, + BrainProjectListResult, + BrainProjectExport, + BrainProjectImportResult, + ExportedConcept, + ExportedMemory, + ExportedRelationship, +} from "@/types/brain-project"; + +export { + DEFAULT_BRAIN_PROJECT_SETTINGS, + BRAIN_PROJECT_EXPORT_VERSION, +} from "@/types/brain-project"; + +// Re-export brain MCP types +export type { + BrainMcpToolName, + BrainMcpServerConfig, + RateLimitConfig, + LoggingConfig, + BrainMcpTool, + McpInputSchema, + McpPropertySchema, + BrainMcpRequest, + BrainMcpResponse, + McpContent, + McpResource, + McpError, + BrainMcpServerStatus, +} from "@/types/brain-mcp"; + +export { + DEFAULT_BRAIN_MCP_SERVER_CONFIG, + BRAIN_MCP_TOOLS, + MCP_ERROR_CODES, +} from "@/types/brain-mcp"; diff --git a/src/types/ollama.ts b/src/types/ollama.ts index 2cd8d0a..e730b58 100644 --- a/src/types/ollama.ts +++ b/src/types/ollama.ts @@ -28,6 +28,7 @@ export interface OllamaChatRequest { export interface OllamaMessage { role: string; content: string; + tool_calls?: OllamaToolCall[]; } export interface OllamaChatOptions { diff --git a/src/types/parallel.ts b/src/types/parallel.ts new file mode 100644 index 0000000..0e21d0a --- /dev/null +++ b/src/types/parallel.ts @@ -0,0 +1,173 @@ +/** + * Parallel Agent Execution Types + * + * Types for concurrent agent execution with conflict detection, + * resource management, and result aggregation. + */ + +/** + * Task type for parallel execution + */ +export type ParallelTaskType = + | "explore" // Read-only exploration + | "analyze" // Analysis without modification + | "execute" // May modify files + | "search"; // Search operations + +/** + * Task priority levels + */ +export type TaskPriority = "low" | "normal" | "high" | "critical"; + +/** + * Task execution status + */ +export type ParallelTaskStatus = + | "pending" + | "queued" + | "running" + | "completed" + | "error" + | "conflict" + | "cancelled" + | "timeout"; + +/** + * Agent configuration for parallel task + */ +export interface ParallelAgentConfig { + name: string; + systemPrompt?: string; + maxTokens?: number; + temperature?: number; +} + +/** + * Parallel task definition + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface ParallelTask<TInput = unknown, TOutput = unknown> { + id: string; + type: ParallelTaskType; + agent: ParallelAgentConfig; + input: TInput; + priority: TaskPriority; + conflictPaths?: string[]; + timeout?: number; + retryCount?: number; + metadata?: Record<string, unknown>; +} + +/** + * Task execution result + */ +export interface ParallelExecutionResult<TOutput = unknown> { + taskId: string; + status: ParallelTaskStatus; + result?: TOutput; + error?: string; + duration: number; + startedAt: number; + completedAt: number; + retryAttempt?: number; +} + +/** + * Conflict detection result + */ +export interface ConflictCheckResult { + hasConflict: boolean; + conflictingTaskIds: string[]; + conflictingPaths: string[]; + resolution?: ConflictResolution; +} + +/** + * Conflict resolution strategy + */ +export type ConflictResolution = + | "wait" // Wait for conflicting task to complete + | "cancel" // Cancel conflicting task + | "merge" // Attempt to merge results + | "abort"; // Abort this task + +/** + * Resource limits for parallel execution + */ +export interface ResourceLimits { + maxConcurrentTasks: number; + maxQueueSize: number; + defaultTimeout: number; + maxRetries: number; +} + +/** + * Resource usage state + */ +export interface ResourceState { + activeTasks: number; + queuedTasks: number; + completedTasks: number; + failedTasks: number; + totalDuration: number; +} + +/** + * Aggregated results from parallel execution + */ +export interface AggregatedResults<TOutput = unknown> { + results: ParallelExecutionResult<TOutput>[]; + successful: number; + failed: number; + cancelled: number; + totalDuration: number; + aggregatedOutput?: TOutput; +} + +/** + * Parallel executor options + */ +export interface ParallelExecutorOptions { + limits: ResourceLimits; + onTaskStart?: (task: ParallelTask) => void; + onTaskComplete?: (result: ParallelExecutionResult) => void; + onTaskError?: (task: ParallelTask, error: Error) => void; + onConflict?: (task: ParallelTask, conflict: ConflictCheckResult) => ConflictResolution; + abortSignal?: AbortSignal; +} + +/** + * Batch execution request + */ +export interface BatchExecutionRequest<TInput = unknown> { + tasks: ParallelTask<TInput>[]; + options?: Partial<ParallelExecutorOptions>; + aggregateResults?: boolean; +} + +/** + * Semaphore state for resource management + */ +export interface SemaphoreState { + permits: number; + maxPermits: number; + waiting: number; +} + +/** + * Deduplication key for result merging + */ +export interface DeduplicationKey { + type: string; + path?: string; + content?: string; +} + +/** + * Deduplication result + */ +export interface DeduplicationResult<T> { + unique: T[]; + duplicateCount: number; + mergedCount: number; +} diff --git a/src/types/pr-review.ts b/src/types/pr-review.ts new file mode 100644 index 0000000..48d6583 --- /dev/null +++ b/src/types/pr-review.ts @@ -0,0 +1,230 @@ +/** + * PR Review Toolkit Types + * + * Types for multi-agent code review with specialized reviewers. + */ + +/** + * Review finding types + */ +export type ReviewFindingType = + | "security" + | "performance" + | "style" + | "logic" + | "documentation" + | "testing"; + +/** + * Review finding severity + */ +export type ReviewSeverity = + | "critical" + | "warning" + | "suggestion" + | "nitpick"; + +/** + * Confidence level for findings + */ +export type ConfidenceLevel = "high" | "medium" | "low"; + +/** + * Single review finding + */ +export interface PRReviewFinding { + id: string; + type: ReviewFindingType; + severity: ReviewSeverity; + file: string; + line?: number; + endLine?: number; + message: string; + details?: string; + suggestion?: string; + confidence: number; + reviewer: string; +} + +/** + * Git diff hunk + */ +export interface DiffHunk { + oldStart: number; + oldLines: number; + newStart: number; + newLines: number; + content: string; + additions: string[]; + deletions: string[]; + context: string[]; +} + +/** + * Parsed diff for a single file + */ +export interface ParsedFileDiff { + oldPath: string; + newPath: string; + hunks: DiffHunk[]; + additions: number; + deletions: number; + isBinary: boolean; + isNew: boolean; + isDeleted: boolean; + isRenamed: boolean; +} + +/** + * Complete parsed diff + */ +export interface ParsedDiff { + files: ParsedFileDiff[]; + totalAdditions: number; + totalDeletions: number; + totalFiles: number; +} + +/** + * Reviewer configuration + */ +export interface ReviewerConfig { + name: string; + type: ReviewFindingType; + enabled: boolean; + minConfidence: number; + prompt?: string; +} + +/** + * Security review criteria + */ +export interface SecurityReviewCriteria { + checkInjection: boolean; + checkXSS: boolean; + checkAuth: boolean; + checkSecrets: boolean; + checkDependencies: boolean; +} + +/** + * Performance review criteria + */ +export interface PerformanceReviewCriteria { + checkComplexity: boolean; + checkMemory: boolean; + checkQueries: boolean; + checkCaching: boolean; + checkRenders: boolean; +} + +/** + * Style review criteria + */ +export interface StyleReviewCriteria { + checkNaming: boolean; + checkFormatting: boolean; + checkConsistency: boolean; + checkComments: boolean; +} + +/** + * Logic review criteria + */ +export interface LogicReviewCriteria { + checkEdgeCases: boolean; + checkNullHandling: boolean; + checkErrorHandling: boolean; + checkConcurrency: boolean; + checkTypes: boolean; +} + +/** + * Review configuration + */ +export interface PRReviewConfig { + minConfidence: number; + reviewers: ReviewerConfig[]; + security: SecurityReviewCriteria; + performance: PerformanceReviewCriteria; + style: StyleReviewCriteria; + logic: LogicReviewCriteria; + excludePatterns: string[]; + maxFindings: number; +} + +/** + * Reviewer result + */ +export interface ReviewerResult { + reviewer: string; + findings: PRReviewFinding[]; + duration: number; + error?: string; +} + +/** + * Overall review rating + */ +export type ReviewRating = 1 | 2 | 3 | 4 | 5; + +/** + * Review recommendation + */ +export type ReviewRecommendation = + | "approve" + | "approve_with_suggestions" + | "request_changes" + | "needs_discussion"; + +/** + * Complete review report + */ +export interface PRReviewReport { + id: string; + timestamp: number; + duration: number; + + // Source information + baseBranch: string; + headBranch: string; + commitRange: string; + + // Diff summary + filesChanged: number; + additions: number; + deletions: number; + + // Findings + findings: PRReviewFinding[]; + findingsBySeverity: Record<ReviewSeverity, number>; + findingsByType: Record<ReviewFindingType, number>; + + // Individual reviewer results + reviewerResults: ReviewerResult[]; + + // Overall assessment + rating: ReviewRating; + recommendation: ReviewRecommendation; + summary: string; +} + +/** + * Review request parameters + */ +export interface PRReviewRequest { + baseBranch?: string; + headBranch?: string; + files?: string[]; + config?: Partial<PRReviewConfig>; +} + +/** + * File context for review + */ +export interface ReviewFileContext { + path: string; + diff: ParsedFileDiff; + fullContent?: string; + language?: string; +} diff --git a/src/types/skills.ts b/src/types/skills.ts new file mode 100644 index 0000000..e71b4a7 --- /dev/null +++ b/src/types/skills.ts @@ -0,0 +1,127 @@ +/** + * Skill System Types + * + * Types for the progressive disclosure skill system. + * Skills are loaded in 3 levels: metadata → body → external references. + */ + +/** + * Skill loading level for progressive disclosure + */ +export type SkillLoadLevel = "metadata" | "body" | "full"; + +/** + * Skill trigger types + */ +export type SkillTriggerType = + | "command" // /commit, /review + | "pattern" // "commit changes", "review PR" + | "auto" // Automatically triggered based on context + | "explicit"; // Only when explicitly invoked + +/** + * Example for a skill + */ +export interface SkillExample { + input: string; + output: string; + description?: string; +} + +/** + * Skill metadata (Level 1 - always loaded) + */ +export interface SkillMetadata { + id: string; + name: string; + description: string; + version: string; + triggers: string[]; + triggerType: SkillTriggerType; + autoTrigger: boolean; + requiredTools: string[]; + tags?: string[]; +} + +/** + * Skill body content (Level 2 - loaded on match) + */ +export interface SkillBody { + systemPrompt: string; + instructions: string; + templates?: Record<string, string>; +} + +/** + * Full skill definition (Level 3 - loaded on execution) + */ +export interface SkillDefinition extends SkillMetadata, SkillBody { + examples?: SkillExample[]; + externalRefs?: string[]; + loadedAt?: number; +} + +/** + * Skill match result from registry + */ +export interface SkillMatch { + skill: SkillDefinition; + confidence: number; + matchedTrigger: string; + matchType: SkillTriggerType; +} + +/** + * Skill execution context + */ +export interface SkillContext { + sessionId: string; + workingDir: string; + gitBranch?: string; + userInput: string; + additionalContext?: Record<string, unknown>; +} + +/** + * Skill execution result + */ +export interface SkillExecutionResult { + success: boolean; + skillId: string; + prompt: string; + error?: string; +} + +/** + * Skill registry state + */ +export interface SkillRegistryState { + skills: Map<string, SkillDefinition>; + lastLoadedAt: number | null; + loadErrors: string[]; +} + +/** + * SKILL.md frontmatter parsed structure + */ +export interface SkillFrontmatter { + id: string; + name: string; + description: string; + version?: string; + triggers: string[]; + triggerType?: SkillTriggerType; + autoTrigger?: boolean; + requiredTools?: string[]; + tags?: string[]; +} + +/** + * Parsed SKILL.md file + */ +export interface ParsedSkillFile { + frontmatter: SkillFrontmatter; + body: string; + examples?: SkillExample[]; + filePath: string; +} diff --git a/src/types/tui.ts b/src/types/tui.ts index 425c3d4..ab0b6de 100644 --- a/src/types/tui.ts +++ b/src/types/tui.ts @@ -5,6 +5,7 @@ */ import type { ProviderModel } from "@/types/providers"; +import type { BrainConnectionStatus, BrainUser } from "@/types/brain"; // ============================================================================ // App Mode Types @@ -28,7 +29,9 @@ export type AppMode = | "provider_select" | "learning_prompt" | "help_menu" - | "help_detail"; + | "help_detail" + | "brain_menu" + | "brain_login"; /** Screen mode for determining which view to show */ export type ScreenMode = "home" | "session"; @@ -230,6 +233,7 @@ export interface SessionStats { outputTokens: number; thinkingStartTime: number | null; lastThinkingDuration: number; + contextMaxTokens: number; } // ============================================================================ @@ -270,6 +274,9 @@ export interface AppState { // Screen mode (home vs session) screenMode: ScreenMode; + // Interaction mode (agent, ask, code-review) + interactionMode: InteractionMode; + // Input state inputBuffer: string; inputCursorPosition: number; @@ -318,9 +325,20 @@ export interface AppState { // Suggestion prompts state suggestions: SuggestionState; + // Brain state + brain: { + status: BrainConnectionStatus; + user: BrainUser | null; + knowledgeCount: number; + memoryCount: number; + showBanner: boolean; + }; + // Actions setMode: (mode: AppMode) => void; setScreenMode: (screenMode: ScreenMode) => void; + setInteractionMode: (mode: InteractionMode) => void; + toggleInteractionMode: () => void; setInputBuffer: (buffer: string) => void; setInputCursorPosition: (position: number) => void; appendToInput: (text: string) => void; @@ -389,6 +407,13 @@ export interface AppState { hideSuggestions: () => void; showSuggestions: () => void; + // Brain actions + setBrainStatus: (status: BrainConnectionStatus) => void; + setBrainUser: (user: BrainUser | null) => void; + setBrainCounts: (knowledge: number, memory: number) => void; + setBrainShowBanner: (show: boolean) => void; + dismissBrainBanner: () => void; + // Computed isInputLocked: () => boolean; } diff --git a/src/types/usage.ts b/src/types/usage.ts index 6ddbc65..f57e4b7 100644 --- a/src/types/usage.ts +++ b/src/types/usage.ts @@ -17,3 +17,44 @@ export interface UsageEntry { timestamp: number; model?: string; } + +/** + * Context window tracking for display and compaction + */ +export interface ContextWindowState { + currentTokens: number; + maxTokens: number; + usagePercent: number; + status: ContextWindowStatus; + model: string; +} + +export type ContextWindowStatus = "normal" | "warning" | "critical" | "compacting"; + +/** + * Calculate context window state from usage stats + */ +export const calculateContextState = ( + totalTokens: number, + maxTokens: number, + isCompacting: boolean, +): ContextWindowState => { + const usagePercent = maxTokens > 0 ? (totalTokens / maxTokens) * 100 : 0; + + let status: ContextWindowStatus = "normal"; + if (isCompacting) { + status = "compacting"; + } else if (usagePercent >= 90) { + status = "critical"; + } else if (usagePercent >= 75) { + status = "warning"; + } + + return { + currentTokens: totalTokens, + maxTokens, + usagePercent, + status, + model: "", + }; +};