From df4f9e30c260baa62d9338d0372dc75d440e70a9 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Fri, 16 Jan 2026 13:08:34 +0530 Subject: [PATCH 01/16] MCP working further test and improvements needed --- package-lock.json | 118 ++++++++-- package.json | 1 + src/components/Header.tsx | 44 +++- src/components/MCPToggle.tsx | 265 ++++++++++++++++++++++ src/components/SettingsPanel.tsx | 368 ++++++++++++------------------- src/core/llm/agent.ts | 48 +++- src/core/mcp/mcp-client.ts | 139 ++++++++++++ 7 files changed, 734 insertions(+), 249 deletions(-) create mode 100644 src/components/MCPToggle.tsx create mode 100644 src/core/mcp/mcp-client.ts diff --git a/package-lock.json b/package-lock.json index f146351bc7..3f413e27ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@langchain/core": "^0.3.0", "@langchain/google-genai": "^0.1.0", "@langchain/langgraph": "^0.2.74", + "@langchain/ollama": "^1.2.0", "@langchain/openai": "^0.3.0", "@sigma/edge-curve": "^3.1.0", "@tailwindcss/vite": "^4.1.18", @@ -564,6 +565,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -580,6 +582,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -596,6 +599,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -612,6 +616,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -628,6 +633,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -644,6 +650,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -660,6 +667,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -676,6 +684,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -692,6 +701,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -708,6 +718,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -724,6 +735,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -740,6 +752,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -756,6 +769,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -772,6 +786,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -788,6 +803,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -804,6 +820,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -820,6 +837,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -836,6 +854,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -852,6 +871,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -868,6 +888,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -884,6 +905,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -900,6 +922,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -916,6 +939,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1722,6 +1746,35 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@langchain/ollama": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@langchain/ollama/-/ollama-1.2.0.tgz", + "integrity": "sha512-OinxIhssKXdDQKnQoBF4TQTMBuMMV5OcNPk4Zze8UjcaSOGngn3CAI1FVbBxl0bTG5ov61w4AoWWsUwOwiSJFw==", + "license": "MIT", + "dependencies": { + "ollama": "^0.6.3", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.0.0" + } + }, + "node_modules/@langchain/ollama/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@langchain/openai": { "version": "0.3.17", "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.3.17.tgz", @@ -2013,6 +2066,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2026,6 +2080,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2039,6 +2094,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2052,6 +2108,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2065,6 +2122,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2078,6 +2136,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2091,6 +2150,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2104,6 +2164,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2117,6 +2178,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2130,6 +2192,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2143,6 +2206,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2156,6 +2220,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2169,6 +2234,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2182,6 +2248,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2195,6 +2262,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2208,6 +2276,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2221,6 +2290,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2234,6 +2304,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2247,6 +2318,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2260,6 +2332,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2273,6 +2346,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2286,6 +2360,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2505,17 +2580,6 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "license": "Apache-2.0" }, - "node_modules/@swc/helpers": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", - "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/@swc/types": { "version": "0.1.25", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", @@ -3239,12 +3303,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4225,6 +4291,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/cytoscape": { @@ -5037,6 +5104,7 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -5678,6 +5746,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5886,13 +5955,6 @@ "graphology-types": ">=0.19.0" } }, - "node_modules/graphology-types": { - "version": "0.24.8", - "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz", - "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==", - "license": "MIT", - "peer": true - }, "node_modules/graphology-utils": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz", @@ -8190,6 +8252,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -8304,6 +8367,15 @@ "integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==", "license": "MIT" }, + "node_modules/ollama": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", + "integrity": "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==", + "license": "MIT", + "dependencies": { + "whatwg-fetch": "^3.6.20" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8527,6 +8599,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -8597,6 +8670,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -8991,6 +9065,7 @@ "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -9984,6 +10059,7 @@ "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -10165,6 +10241,12 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/package.json b/package.json index 54574349e1..55fdc81892 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@langchain/core": "^0.3.0", "@langchain/google-genai": "^0.1.0", "@langchain/langgraph": "^0.2.74", + "@langchain/ollama": "^1.2.0", "@langchain/openai": "^0.3.0", "@sigma/edge-curve": "^3.1.0", "@tailwindcss/vite": "^4.1.18", diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 1fe99cacd9..44c90fb4e4 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,8 +1,9 @@ import { Search, Settings, HelpCircle, Sparkles, Github, Star } from 'lucide-react'; import { useAppState } from '../hooks/useAppState'; -import { useState, useMemo, useRef, useEffect } from 'react'; +import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { GraphNode } from '../core/graph/types'; import { EmbeddingStatus } from './EmbeddingStatus'; +import { MCPToggle } from './MCPToggle'; // Color mapping for node types in search results const NODE_TYPE_COLORS: Record = { @@ -22,7 +23,17 @@ interface HeaderProps { } export const Header = ({ onFocusNode }: HeaderProps) => { - const { projectName, graph, openChatPanel, isRightPanelOpen, rightPanelTab, setSettingsPanelOpen } = useAppState(); + const { + projectName, + graph, + openChatPanel, + isRightPanelOpen, + rightPanelTab, + setSettingsPanelOpen, + runQuery, + semanticSearch, + setHighlightedNodeIds, + } = useAppState(); const [searchQuery, setSearchQuery] = useState(''); const [isSearchOpen, setIsSearchOpen] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); @@ -206,6 +217,35 @@ export const Header = ({ onFocusNode }: HeaderProps) => { {/* Embedding Status */} + {/* MCP Toggle for external AI agents */} + { + // Use semantic search from the app + const results = await semanticSearch(query, limit); + return results; + }} + onCypher={async (query) => { + // Execute Cypher query + const results = await runQuery(query); + return results; + }} + onBlastRadius={async (nodeId, hops = 2) => { + // Run blast radius query + const query = ` + MATCH (start)-[*1..${hops}]-(connected) + WHERE start.id = '${nodeId}' OR start.name = '${nodeId}' + RETURN DISTINCT connected.id AS id, connected.name AS name, labels(connected) AS labels + `; + const results = await runQuery(query); + return results; + }} + onHighlight={(nodeIds) => { + // Highlight nodes in the graph + setHighlightedNodeIds(new Set(nodeIds)); + }} + /> + {/* Icon buttons */} + +
+

+ Run this command to enable external AI agents: +

+
+ npx gitnexus-mcp setup + +
+ +
+ + ); + } + + // Toggle switch UI + return ( +
+ MCP + + {isConnected && ( + Connected + )} + + {/* Onboarding Tip Popup */} + {showTip && !isConnected && ( +
+ + +
+
+ +
+
+

+ Connect your AI coding tool +

+

+ Let Cursor, Claude Code, or Antigravity use GitNexus for code intelligence. +

+
+
+ +
+

Run this in your terminal:

+
+ npx gitnexus-mcp setup + +
+
+ + +
+ )} +
+ ); +} diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index 25e8e266c9..ac06cb7ba0 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -1,10 +1,9 @@ import { useState, useEffect, useCallback } from 'react'; -import { X, Key, Server, Brain, Check, AlertCircle, Eye, EyeOff, RefreshCw, Loader2 } from 'lucide-react'; +import { X, Key, Server, Brain, Check, AlertCircle, Eye, EyeOff, RefreshCw } from 'lucide-react'; import { loadSettings, saveSettings, getProviderDisplayName, - getAvailableModels, } from '../core/llm/settings-service'; import type { LLMSettings, LLMProvider } from '../core/llm/types'; @@ -15,45 +14,28 @@ interface SettingsPanelProps { } /** - * Fetch available Gemini models from the API + * Check connection to local Ollama instance */ -const fetchGeminiModels = async (apiKey: string): Promise => { +const checkOllamaStatus = async (baseUrl: string): Promise<{ ok: boolean; error: string | null }> => { try { - const response = await fetch( - `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}` - ); + const response = await fetch(`${baseUrl}/api/tags`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); if (!response.ok) { - throw new Error(`API error: ${response.status}`); + if (response.status === 0 || response.status === 404) { + return { ok: false, error: 'Cannot connect to Ollama. Make sure it\'s running with `ollama serve`' }; + } + return { ok: false, error: `Ollama API error: ${response.status}` }; } - const data = await response.json(); - - // Filter for chat-capable models and extract model names - const models = (data.models || []) - .filter((model: any) => { - const methods = model.supportedGenerationMethods || []; - return methods.includes('generateContent'); - }) - .map((model: any) => { - const name = model.name || ''; - return name.replace('models/', ''); - }) - .filter((name: string) => name.length > 0) - .sort((a: string, b: string) => { - const score = (n: string) => { - if (n.includes('gemini-2')) return 0; - if (n.includes('gemini-1.5')) return 1; - if (n.includes('gemini-1')) return 2; - return 3; - }; - return score(a) - score(b) || a.localeCompare(b); - }); - - return models; + return { ok: true, error: null }; } catch (error) { - console.warn('Failed to fetch Gemini models:', error); - return []; + return { + ok: false, + error: 'Cannot connect to Ollama. Make sure it\'s running with `ollama serve`' + }; } }; @@ -61,59 +43,38 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane const [settings, setSettings] = useState(loadSettings); const [showApiKey, setShowApiKey] = useState>({}); const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle'); - - // Gemini model fetching state - const [geminiModels, setGeminiModels] = useState([]); - const [isLoadingModels, setIsLoadingModels] = useState(false); - const [modelFetchError, setModelFetchError] = useState(null); - const [useCustomModel, setUseCustomModel] = useState(false); - const [useCustomAnthropicModel, setUseCustomAnthropicModel] = useState(false); + // Ollama connection state + const [ollamaError, setOllamaError] = useState(null); + const [isCheckingOllama, setIsCheckingOllama] = useState(false); // Load settings when panel opens useEffect(() => { if (isOpen) { setSettings(loadSettings()); setSaveStatus('idle'); - setGeminiModels([]); - setModelFetchError(null); - setUseCustomModel(false); - setUseCustomAnthropicModel(false); + setOllamaError(null); } }, [isOpen]); - // Auto-fetch models when Gemini API key changes - const fetchModels = useCallback(async (apiKey: string) => { - if (!apiKey || apiKey.length < 10) { - setGeminiModels([]); - return; - } - - setIsLoadingModels(true); - setModelFetchError(null); + // Check Ollama connection when provider is selected or base URL changes + const checkOllamaConnection = useCallback(async (baseUrl: string) => { + setIsCheckingOllama(true); + setOllamaError(null); - const models = await fetchGeminiModels(apiKey); - - setIsLoadingModels(false); - - if (models.length > 0) { - setGeminiModels(models); - setModelFetchError(null); - } else { - setGeminiModels([]); - setModelFetchError('Could not fetch models. Check your API key or enter model manually.'); - } + const { error } = await checkOllamaStatus(baseUrl); + setIsCheckingOllama(false); + setOllamaError(error); }, []); - // Fetch models when API key is entered (debounced) useEffect(() => { - if (settings.activeProvider === 'gemini' && settings.gemini?.apiKey) { - const apiKey = settings.gemini.apiKey ?? ''; + if (settings.activeProvider === 'ollama') { + const baseUrl = settings.ollama?.baseUrl ?? 'http://localhost:11434'; const timer = setTimeout(() => { - fetchModels(apiKey); - }, 500); + checkOllamaConnection(baseUrl); + }, 300); return () => clearTimeout(timer); } - }, [settings.gemini?.apiKey, settings.activeProvider, fetchModels]); + }, [settings.ollama?.baseUrl, settings.activeProvider, checkOllamaConnection]); const handleProviderChange = (provider: LLMProvider) => { setSettings(prev => ({ ...prev, activeProvider: provider })); @@ -136,17 +97,8 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane if (!isOpen) return null; - const providers: LLMProvider[] = ['openai', 'gemini', 'anthropic', 'azure-openai']; + const providers: LLMProvider[] = ['openai', 'gemini', 'anthropic', 'azure-openai', 'ollama']; - const availableGeminiModels = geminiModels.length > 0 - ? geminiModels - : getAvailableModels('gemini'); - const currentGeminiModel = settings.gemini?.model ?? 'gemini-2.0-flash'; - const isCustomModelSelected = !availableGeminiModels.includes(currentGeminiModel); - - const availableAnthropicModels = getAvailableModels('anthropic'); - const currentAnthropicModel = settings.anthropic?.model ?? 'claude-sonnet-4-20250514'; - const isCustomAnthropicModelSelected = !availableAnthropicModels.includes(currentAnthropicModel); return (
@@ -201,7 +153,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane w-8 h-8 rounded-lg flex items-center justify-center text-lg ${settings.activeProvider === provider ? 'bg-accent/20' : 'bg-surface'} `}> - {provider === 'openai' ? '🤖' : provider === 'gemini' ? '💎' : provider === 'anthropic' ? '🧠' : '☁️'} + {provider === 'openai' ? '🤖' : provider === 'gemini' ? '💎' : provider === 'anthropic' ? '🧠' : provider === 'ollama' ? '🦙' : '☁️'}
{getProviderDisplayName(provider)} @@ -251,18 +203,16 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
- + placeholder="e.g., gpt-4o, gpt-4-turbo, gpt-3.5-turbo" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" + />
@@ -328,92 +278,17 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
-
- -
- {isLoadingModels && ( - - - Fetching models... - - )} - {geminiModels.length > 0 && ( - - {geminiModels.length} models available - - )} - {settings.gemini?.apiKey && !isLoadingModels && ( - - )} -
-
- - {/* Model selector or manual input */} - {(useCustomModel || isCustomModelSelected) ? ( -
- setSettings(prev => ({ - ...prev, - gemini: { ...prev.gemini!, model: e.target.value } - }))} - placeholder="e.g., gemini-2.0-flash-exp" - className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" - /> - -
- ) : ( -
- - -
- )} - - {modelFetchError && !useCustomModel && ( -

- - {modelFetchError} -

- )} + + setSettings(prev => ({ + ...prev, + gemini: { ...prev.gemini!, model: e.target.value } + }))} + placeholder="e.g., gemini-2.0-flash, gemini-1.5-pro" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" + />
)} @@ -460,58 +335,16 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
- - {(useCustomAnthropicModel || isCustomAnthropicModelSelected) ? ( -
- setSettings(prev => ({ - ...prev, - anthropic: { ...prev.anthropic!, model: e.target.value } - }))} - placeholder="e.g., claude-sonnet-4-20250514" - className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" - /> - -
- ) : ( -
- - -
- )} + setSettings(prev => ({ + ...prev, + anthropic: { ...prev.anthropic!, model: e.target.value } + }))} + placeholder="e.g., claude-sonnet-4-20250514, claude-3-opus" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" + />
)} @@ -620,6 +453,87 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane )} + {/* Ollama Settings */} + {settings.activeProvider === 'ollama' && ( +
+ {/* How to run Ollama */} +
+

+ 📋 Quick Start: Install Ollama from{' '} + + ollama.ai + , then run: +

+ + ollama serve + +
+ +
+ +
+ setSettings(prev => ({ + ...prev, + ollama: { ...prev.ollama!, baseUrl: e.target.value } + }))} + placeholder="http://localhost:11434" + className="flex-1 px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" + /> + +
+

+ Default port is 11434. +

+
+ +
+ + + {ollamaError && !isCheckingOllama && ( +
+

+ + {ollamaError} +

+
+ )} + + setSettings(prev => ({ + ...prev, + ollama: { ...prev.ollama!, model: e.target.value } + }))} + placeholder="e.g., llama3.2, mistral, codellama" + className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm" + /> +

+ Pull a model with ollama pull llama3.2 +

+
+
+ )} + {/* Privacy Note */}
diff --git a/src/core/llm/agent.ts b/src/core/llm/agent.ts index e26d69a284..27b4032676 100644 --- a/src/core/llm/agent.ts +++ b/src/core/llm/agent.ts @@ -10,6 +10,7 @@ import { SystemMessage } from '@langchain/core/messages'; import { ChatOpenAI, AzureChatOpenAI } from '@langchain/openai'; import { ChatGoogleGenerativeAI } from '@langchain/google-genai'; import { ChatAnthropic } from '@langchain/anthropic'; +import { ChatOllama } from '@langchain/ollama'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { createGraphRAGTools } from './tools'; import type { @@ -18,6 +19,7 @@ import type { AzureOpenAIConfig, GeminiConfig, AnthropicConfig, + OllamaConfig, AgentStreamChunk, } from './types'; import { @@ -160,6 +162,21 @@ export const createChatModel = (config: ProviderConfig): BaseChatModel => { }); } + case 'ollama': { + const ollamaConfig = config as OllamaConfig; + return new ChatOllama({ + baseUrl: ollamaConfig.baseUrl ?? 'http://localhost:11434', + model: ollamaConfig.model, + temperature: ollamaConfig.temperature ?? 0.1, + streaming: true, + // Allow longer responses (Ollama default is often 128-2048) + numPredict: 30000, + // Increase context window (Ollama default is only 2048!) + // This is critical for agentic workflows with tool calls + numCtx: 32768, + }); + } + default: throw new Error(`Unsupported provider: ${(config as any).provider}`); } @@ -289,6 +306,13 @@ export async function* streamAgentResponse( data = event; } + // DEBUG: Enhanced logging + if (import.meta.env.DEV) { + const msgType = mode === 'messages' && data?.[0]?._getType?.() || 'n/a'; + const hasContent = mode === 'messages' && data?.[0]?.content; + const hasToolCalls = mode === 'messages' && data?.[0]?.tool_calls?.length > 0; + console.log(`🔄 [${mode}] type:${msgType} content:${!!hasContent} tools:${hasToolCalls}`); + } // Handle 'messages' mode - token-by-token streaming if (mode === 'messages') { const [msg] = Array.isArray(data) ? data : [data]; @@ -298,11 +322,23 @@ export async function* streamAgentResponse( // AIMessageChunk - streaming text tokens if (msgType === 'ai' || msgType === 'AIMessage' || msgType === 'AIMessageChunk') { - const content = msg.content; + const rawContent = msg.content; const toolCalls = msg.tool_calls || []; + // Handle content that can be string or array of content blocks + let content: string = ''; + if (typeof rawContent === 'string') { + content = rawContent; + } else if (Array.isArray(rawContent)) { + // Content blocks format: [{type: 'text', text: '...'}, ...] + content = rawContent + .filter((block: any) => block.type === 'text' || typeof block === 'string') + .map((block: any) => typeof block === 'string' ? block : block.text || '') + .join(''); + } + // If chunk has content, stream it - if (content && typeof content === 'string' && content.length > 0) { + if (content && content.length > 0) { // Determine if this is reasoning/narration vs final answer content. // - Before the first tool call: treat as reasoning (narration) // - Between tool calls/results: treat as reasoning @@ -416,9 +452,17 @@ export async function* streamAgentResponse( } } + // DEBUG: Stream completed normally + if (import.meta.env.DEV) { + console.log('✅ Stream completed normally, yielding done'); + } yield { type: 'done' }; } catch (error) { const message = error instanceof Error ? error.message : String(error); + // DEBUG: Stream error + if (import.meta.env.DEV) { + console.error('❌ Stream error:', message, error); + } yield { type: 'error', error: message, diff --git a/src/core/mcp/mcp-client.ts b/src/core/mcp/mcp-client.ts new file mode 100644 index 0000000000..7a4dc0cacb --- /dev/null +++ b/src/core/mcp/mcp-client.ts @@ -0,0 +1,139 @@ +/** + * MCP Browser Client + * + * WebSocket client that connects to the gitnexus-mcp bridge. + * Receives tool calls from external AI agents and executes them. + */ + +export interface MCPMessage { + id: string; + method?: string; + params?: Record; + result?: any; + error?: { message: string }; +} + +type ToolHandler = (params: Record) => Promise; + +export class MCPBrowserClient { + private ws: WebSocket | null = null; + private handlers: Map = new Map(); + private connectionListeners: Set<(connected: boolean) => void> = new Set(); + + constructor(private port = 54319) {} + + /** + * Connect to the MCP bridge + */ + async connect(): Promise { + return new Promise((resolve, reject) => { + try { + this.ws = new WebSocket(`ws://localhost:${this.port}`); + + this.ws.onopen = () => { + this.notifyConnectionListeners(true); + resolve(); + }; + + this.ws.onerror = (event) => { + this.notifyConnectionListeners(false); + reject(new Error('Failed to connect to MCP bridge')); + }; + + this.ws.onmessage = (event) => { + try { + const msg: MCPMessage = JSON.parse(event.data); + this.handleMessage(msg); + } catch (error) { + console.error('[MCP] Failed to parse message:', error); + } + }; + + this.ws.onclose = () => { + this.ws = null; + this.notifyConnectionListeners(false); + }; + } catch (error) { + reject(error); + } + }); + } + + /** + * Handle incoming messages from bridge + */ + private async handleMessage(msg: MCPMessage) { + // This is a tool call request from an external agent + if (msg.method && msg.id) { + const handler = this.handlers.get(msg.method); + + if (handler) { + try { + const result = await handler(msg.params || {}); + this.send({ id: msg.id, result }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + this.send({ id: msg.id, error: { message } }); + } + } else { + this.send({ + id: msg.id, + error: { message: `Unknown tool: ${msg.method}` } + }); + } + } + } + + /** + * Send a message to the bridge + */ + private send(msg: MCPMessage) { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(msg)); + } + } + + /** + * Register a handler for a tool + */ + registerHandler(method: string, handler: ToolHandler) { + this.handlers.set(method, handler); + } + + /** + * Listen for connection state changes + */ + onConnectionChange(listener: (connected: boolean) => void) { + this.connectionListeners.add(listener); + return () => this.connectionListeners.delete(listener); + } + + private notifyConnectionListeners(connected: boolean) { + this.connectionListeners.forEach(listener => listener(connected)); + } + + /** + * Check if connected + */ + get isConnected(): boolean { + return this.ws?.readyState === WebSocket.OPEN; + } + + /** + * Disconnect from bridge + */ + disconnect() { + this.ws?.close(); + this.ws = null; + } +} + +// Singleton instance +let mcpClientInstance: MCPBrowserClient | null = null; + +export function getMCPClient(): MCPBrowserClient { + if (!mcpClientInstance) { + mcpClientInstance = new MCPBrowserClient(); + } + return mcpClientInstance; +} From e153d30c8f7a86a583bb871ad883fe014cb1c31e Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Sat, 17 Jan 2026 23:08:48 +0530 Subject: [PATCH 02/16] MCP bridge added --- gitnexus-mcp/package-lock.json | 1994 +++++++++++++++++ gitnexus-mcp/package.json | 33 + gitnexus-mcp/src/bridge/daemon-client.ts | 173 ++ gitnexus-mcp/src/bridge/protocol.ts | 36 + gitnexus-mcp/src/bridge/websocket-server.ts | 163 ++ gitnexus-mcp/src/cli.ts | 47 + gitnexus-mcp/src/commands/daemon.ts | 194 ++ .../src/commands/generate-guidance.ts | 153 ++ gitnexus-mcp/src/commands/serve.ts | 33 + gitnexus-mcp/src/commands/setup.ts | 55 + gitnexus-mcp/src/config/detect-ides.ts | 103 + gitnexus-mcp/src/config/inject-config.ts | 97 + gitnexus-mcp/src/config/paths.ts | 37 + gitnexus-mcp/src/mcp/server.ts | 220 ++ gitnexus-mcp/src/mcp/tools.ts | 174 ++ gitnexus-mcp/test-output/.cursorrules | 69 + gitnexus-mcp/test-output/AGENTS.md | 98 + gitnexus-mcp/tsconfig.json | 27 + README.md => gitnexus/README.md | 0 {api => gitnexus/api}/proxy.ts | 0 index.html => gitnexus/index.html | 0 .../package-lock.json | 0 package.json => gitnexus/package.json | 0 .../javascript/tree-sitter-javascript.wasm | Bin .../public}/wasm/kuzu-wasm.wasm | Bin .../wasm/python/tree-sitter-python.wasm | Bin .../public}/wasm/tree-sitter.wasm | Bin .../wasm/typescript/tree-sitter-tsx.wasm | Bin .../typescript/tree-sitter-typescript.wasm | Bin {src => gitnexus/src}/App.tsx | 0 gitnexus/src/components/ActivityFeed.tsx | 202 ++ .../src}/components/CodeReferencesPanel.tsx | 0 {src => gitnexus/src}/components/DropZone.tsx | 0 .../src}/components/EmbeddingStatus.tsx | 0 .../src}/components/FileTreePanel.tsx | 130 +- .../src}/components/GraphCanvas.tsx | 10 + {src => gitnexus/src}/components/Header.tsx | 98 + .../src}/components/LoadingOverlay.tsx | 0 .../src}/components/MCPToggle.tsx | 47 +- gitnexus/src/components/MarkdownRenderer.tsx | 172 ++ .../src}/components/MermaidDiagram.tsx | 0 {src => gitnexus/src}/components/QueryFAB.tsx | 0 gitnexus/src/components/RightPanel.tsx | 435 ++++ .../src}/components/SettingsPanel.tsx | 2 +- .../src}/components/StatusBar.tsx | 0 .../src}/components/ToolCallCard.tsx | 0 .../src}/components/WebGPUFallbackDialog.tsx | 0 .../src}/config/ignore-service.ts | 0 .../src}/config/supported-languages.ts | 0 .../src}/core/embeddings/embedder.ts | 0 .../core/embeddings/embedding-pipeline.ts | 0 .../src}/core/embeddings/index.ts | 0 .../src}/core/embeddings/text-generator.ts | 0 .../src}/core/embeddings/types.ts | 0 {src => gitnexus/src}/core/graph/graph.ts | 0 {src => gitnexus/src}/core/graph/types.ts | 0 .../src}/core/ingestion/ast-cache.ts | 0 .../src}/core/ingestion/call-processor.ts | 0 .../src}/core/ingestion/heritage-processor.ts | 0 .../src}/core/ingestion/import-processor.ts | 0 .../src}/core/ingestion/parsing-processor.ts | 0 .../src}/core/ingestion/pipeline.ts | 0 .../core/ingestion/structure-processor.ts | 0 .../src}/core/ingestion/symbol-table.ts | 0 .../core/ingestion/tree-sitter-queries.ts | 0 {src => gitnexus/src}/core/ingestion/utils.ts | 0 .../src}/core/kuzu/csv-generator.ts | 0 .../src}/core/kuzu/kuzu-adapter.ts | 0 {src => gitnexus/src}/core/kuzu/schema.ts | 0 {src => gitnexus/src}/core/llm/agent.ts | 0 .../src}/core/llm/context-builder.ts | 79 +- {src => gitnexus/src}/core/llm/index.ts | 0 .../src}/core/llm/settings-service.ts | 0 {src => gitnexus/src}/core/llm/tools.ts | 0 {src => gitnexus/src}/core/llm/types.ts | 0 gitnexus/src/core/mcp/mcp-client.ts | 341 +++ .../src}/core/search/bm25-index.ts | 0 .../src}/core/search/hybrid-search.ts | 0 {src => gitnexus/src}/core/search/index.ts | 0 .../src}/core/tree-sitter/parser-loader.ts | 0 {src => gitnexus/src}/hooks/useAppState.tsx | 73 + {src => gitnexus/src}/hooks/useSigma.ts | 83 +- {src => gitnexus/src}/index.css | 0 {src => gitnexus/src}/lib/constants.ts | 31 + {src => gitnexus/src}/lib/graph-adapter.ts | 6 - {src => gitnexus/src}/lib/utils.ts | 0 {src => gitnexus/src}/main.tsx | 0 {src => gitnexus/src}/services/git-clone.ts | 0 {src => gitnexus/src}/services/zip.ts | 0 {src => gitnexus/src}/types/kuzu-wasm.d.ts | 0 {src => gitnexus/src}/types/pipeline.ts | 0 {src => gitnexus/src}/vite-env.d.ts | 0 .../src}/workers/ingestion.worker.ts | 0 .../tsconfig.app.json | 0 tsconfig.json => gitnexus/tsconfig.json | 0 .../tsconfig.node.json | 0 gitnexus/vercel.json | 17 + vite.config.ts => gitnexus/vite.config.ts | 0 src/components/RightPanel.tsx | 795 ------- src/core/mcp/mcp-client.ts | 139 -- vercel.json | 18 +- 101 files changed, 5374 insertions(+), 1010 deletions(-) create mode 100644 gitnexus-mcp/package-lock.json create mode 100644 gitnexus-mcp/package.json create mode 100644 gitnexus-mcp/src/bridge/daemon-client.ts create mode 100644 gitnexus-mcp/src/bridge/protocol.ts create mode 100644 gitnexus-mcp/src/bridge/websocket-server.ts create mode 100644 gitnexus-mcp/src/cli.ts create mode 100644 gitnexus-mcp/src/commands/daemon.ts create mode 100644 gitnexus-mcp/src/commands/generate-guidance.ts create mode 100644 gitnexus-mcp/src/commands/serve.ts create mode 100644 gitnexus-mcp/src/commands/setup.ts create mode 100644 gitnexus-mcp/src/config/detect-ides.ts create mode 100644 gitnexus-mcp/src/config/inject-config.ts create mode 100644 gitnexus-mcp/src/config/paths.ts create mode 100644 gitnexus-mcp/src/mcp/server.ts create mode 100644 gitnexus-mcp/src/mcp/tools.ts create mode 100644 gitnexus-mcp/test-output/.cursorrules create mode 100644 gitnexus-mcp/test-output/AGENTS.md create mode 100644 gitnexus-mcp/tsconfig.json rename README.md => gitnexus/README.md (100%) rename {api => gitnexus/api}/proxy.ts (100%) rename index.html => gitnexus/index.html (100%) rename package-lock.json => gitnexus/package-lock.json (100%) rename package.json => gitnexus/package.json (100%) rename {public => gitnexus/public}/wasm/javascript/tree-sitter-javascript.wasm (100%) rename {public => gitnexus/public}/wasm/kuzu-wasm.wasm (100%) rename {public => gitnexus/public}/wasm/python/tree-sitter-python.wasm (100%) rename {public => gitnexus/public}/wasm/tree-sitter.wasm (100%) rename {public => gitnexus/public}/wasm/typescript/tree-sitter-tsx.wasm (100%) rename {public => gitnexus/public}/wasm/typescript/tree-sitter-typescript.wasm (100%) rename {src => gitnexus/src}/App.tsx (100%) create mode 100644 gitnexus/src/components/ActivityFeed.tsx rename {src => gitnexus/src}/components/CodeReferencesPanel.tsx (100%) rename {src => gitnexus/src}/components/DropZone.tsx (100%) rename {src => gitnexus/src}/components/EmbeddingStatus.tsx (100%) rename {src => gitnexus/src}/components/FileTreePanel.tsx (85%) rename {src => gitnexus/src}/components/GraphCanvas.tsx (97%) rename {src => gitnexus/src}/components/Header.tsx (73%) rename {src => gitnexus/src}/components/LoadingOverlay.tsx (100%) rename {src => gitnexus/src}/components/MCPToggle.tsx (85%) create mode 100644 gitnexus/src/components/MarkdownRenderer.tsx rename {src => gitnexus/src}/components/MermaidDiagram.tsx (100%) rename {src => gitnexus/src}/components/QueryFAB.tsx (100%) create mode 100644 gitnexus/src/components/RightPanel.tsx rename {src => gitnexus/src}/components/SettingsPanel.tsx (99%) rename {src => gitnexus/src}/components/StatusBar.tsx (100%) rename {src => gitnexus/src}/components/ToolCallCard.tsx (100%) rename {src => gitnexus/src}/components/WebGPUFallbackDialog.tsx (100%) rename {src => gitnexus/src}/config/ignore-service.ts (100%) rename {src => gitnexus/src}/config/supported-languages.ts (100%) rename {src => gitnexus/src}/core/embeddings/embedder.ts (100%) rename {src => gitnexus/src}/core/embeddings/embedding-pipeline.ts (100%) rename {src => gitnexus/src}/core/embeddings/index.ts (100%) rename {src => gitnexus/src}/core/embeddings/text-generator.ts (100%) rename {src => gitnexus/src}/core/embeddings/types.ts (100%) rename {src => gitnexus/src}/core/graph/graph.ts (100%) rename {src => gitnexus/src}/core/graph/types.ts (100%) rename {src => gitnexus/src}/core/ingestion/ast-cache.ts (100%) rename {src => gitnexus/src}/core/ingestion/call-processor.ts (100%) rename {src => gitnexus/src}/core/ingestion/heritage-processor.ts (100%) rename {src => gitnexus/src}/core/ingestion/import-processor.ts (100%) rename {src => gitnexus/src}/core/ingestion/parsing-processor.ts (100%) rename {src => gitnexus/src}/core/ingestion/pipeline.ts (100%) rename {src => gitnexus/src}/core/ingestion/structure-processor.ts (100%) rename {src => gitnexus/src}/core/ingestion/symbol-table.ts (100%) rename {src => gitnexus/src}/core/ingestion/tree-sitter-queries.ts (100%) rename {src => gitnexus/src}/core/ingestion/utils.ts (100%) rename {src => gitnexus/src}/core/kuzu/csv-generator.ts (100%) rename {src => gitnexus/src}/core/kuzu/kuzu-adapter.ts (100%) rename {src => gitnexus/src}/core/kuzu/schema.ts (100%) rename {src => gitnexus/src}/core/llm/agent.ts (100%) rename {src => gitnexus/src}/core/llm/context-builder.ts (79%) rename {src => gitnexus/src}/core/llm/index.ts (100%) rename {src => gitnexus/src}/core/llm/settings-service.ts (100%) rename {src => gitnexus/src}/core/llm/tools.ts (100%) rename {src => gitnexus/src}/core/llm/types.ts (100%) create mode 100644 gitnexus/src/core/mcp/mcp-client.ts rename {src => gitnexus/src}/core/search/bm25-index.ts (100%) rename {src => gitnexus/src}/core/search/hybrid-search.ts (100%) rename {src => gitnexus/src}/core/search/index.ts (100%) rename {src => gitnexus/src}/core/tree-sitter/parser-loader.ts (100%) rename {src => gitnexus/src}/hooks/useAppState.tsx (94%) rename {src => gitnexus/src}/hooks/useSigma.ts (84%) rename {src => gitnexus/src}/index.css (100%) rename {src => gitnexus/src}/lib/constants.ts (71%) rename {src => gitnexus/src}/lib/graph-adapter.ts (96%) rename {src => gitnexus/src}/lib/utils.ts (100%) rename {src => gitnexus/src}/main.tsx (100%) rename {src => gitnexus/src}/services/git-clone.ts (100%) rename {src => gitnexus/src}/services/zip.ts (100%) rename {src => gitnexus/src}/types/kuzu-wasm.d.ts (100%) rename {src => gitnexus/src}/types/pipeline.ts (100%) rename {src => gitnexus/src}/vite-env.d.ts (100%) rename {src => gitnexus/src}/workers/ingestion.worker.ts (100%) rename tsconfig.app.json => gitnexus/tsconfig.app.json (100%) rename tsconfig.json => gitnexus/tsconfig.json (100%) rename tsconfig.node.json => gitnexus/tsconfig.node.json (100%) create mode 100644 gitnexus/vercel.json rename vite.config.ts => gitnexus/vite.config.ts (100%) delete mode 100644 src/components/RightPanel.tsx delete mode 100644 src/core/mcp/mcp-client.ts diff --git a/gitnexus-mcp/package-lock.json b/gitnexus-mcp/package-lock.json new file mode 100644 index 0000000000..41fd58235f --- /dev/null +++ b/gitnexus-mcp/package-lock.json @@ -0,0 +1,1994 @@ +{ + "name": "gitnexus-mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gitnexus-mcp", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "chalk": "^5.3.0", + "commander": "^12.0.0", + "ora": "^8.0.0", + "ws": "^8.16.0" + }, + "bin": { + "gitnexus-mcp": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/ws": "^8.5.10", + "tsx": "^4.0.0", + "typescript": "^5.4.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.25.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", + "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.7", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", + "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/gitnexus-mcp/package.json b/gitnexus-mcp/package.json new file mode 100644 index 0000000000..e6d0ffa070 --- /dev/null +++ b/gitnexus-mcp/package.json @@ -0,0 +1,33 @@ +{ + "name": "gitnexus-mcp", + "version": "0.1.0", + "description": "MCP bridge for GitNexus code intelligence", + "type": "module", + "bin": { + "gitnexus-mcp": "./dist/cli.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "dev": "tsx watch src/cli.ts", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "commander": "^12.0.0", + "ws": "^8.16.0", + "chalk": "^5.3.0", + "ora": "^8.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/ws": "^8.5.10", + "typescript": "^5.4.0", + "tsx": "^4.0.0" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/gitnexus-mcp/src/bridge/daemon-client.ts b/gitnexus-mcp/src/bridge/daemon-client.ts new file mode 100644 index 0000000000..dbd36fb896 --- /dev/null +++ b/gitnexus-mcp/src/bridge/daemon-client.ts @@ -0,0 +1,173 @@ +/** + * Daemon Client + * + * WebSocket client that connects to the GitNexus daemon. + * Used by `serve` command to route tool calls through the daemon to the browser. + * Also receives codebase context for MCP resource exposure. + */ + +import WebSocket from 'ws'; + +export interface BridgeMessage { + id: string; + method?: string; + params?: any; + result?: any; + error?: { message: string }; + type?: string; + context?: CodebaseContext; +} + +/** + * Codebase context from browser + */ +export interface CodebaseContext { + projectName: string; + stats: { + fileCount: number; + functionCount: number; + classCount: number; + interfaceCount: number; + methodCount: number; + }; + hotspots: Array<{ + name: string; + type: string; + filePath: string; + connections: number; + }>; + folderTree: string; +} + +type RequestResolver = { + resolve: (result: any) => void; + reject: (error: Error) => void; +}; + +export class DaemonClient { + private ws: WebSocket | null = null; + private pendingRequests: Map = new Map(); + private requestId = 0; + private _context: CodebaseContext | null = null; + private contextListeners: Set<(context: CodebaseContext | null) => void> = new Set(); + + constructor(private port: number = 54319) {} + + /** + * Connect to the daemon + */ + async connect(): Promise { + return new Promise((resolve, reject) => { + // Connect to /mcp path so daemon knows this is an MCP client + this.ws = new WebSocket(`ws://localhost:${this.port}/mcp`); + + this.ws.on('open', () => { + resolve(); + }); + + this.ws.on('error', (error) => { + reject(error); + }); + + this.ws.on('message', (data) => { + try { + const msg: BridgeMessage = JSON.parse(data.toString()); + this.handleMessage(msg); + } catch (error) { + console.error('Failed to parse message:', error); + } + }); + + this.ws.on('close', () => { + this.ws = null; + this._context = null; + // Reject all pending requests + for (const { reject } of this.pendingRequests.values()) { + reject(new Error('Connection closed')); + } + this.pendingRequests.clear(); + }); + }); + } + + private handleMessage(msg: BridgeMessage) { + // Handle context update from daemon + if (msg.type === 'context_update' && msg.context) { + this._context = msg.context; + this.notifyContextListeners(); + return; + } + + // Response from daemon (originally from browser) + if (msg.id && this.pendingRequests.has(msg.id)) { + const { resolve, reject } = this.pendingRequests.get(msg.id)!; + this.pendingRequests.delete(msg.id); + + if (msg.error) { + reject(new Error(msg.error.message)); + } else { + resolve(msg.result); + } + } + } + + /** + * Get current codebase context + */ + get context(): CodebaseContext | null { + return this._context; + } + + /** + * Listen for context changes + */ + onContextChange(listener: (context: CodebaseContext | null) => void) { + this.contextListeners.add(listener); + return () => this.contextListeners.delete(listener); + } + + private notifyContextListeners() { + this.contextListeners.forEach(listener => listener(this._context)); + } + + /** + * Check if connected to daemon + */ + get isConnected(): boolean { + return this.ws !== null && this.ws.readyState === WebSocket.OPEN; + } + + /** + * Call a tool (routed through daemon to browser) + */ + async callTool(method: string, params: any): Promise { + if (!this.isConnected) { + throw new Error('Not connected to daemon'); + } + + const id = `req_${++this.requestId}`; + + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { resolve, reject }); + + const msg: BridgeMessage = { id, method, params }; + this.ws!.send(JSON.stringify(msg)); + + // Timeout after 30 seconds + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(new Error('Request timeout')); + } + }, 30000); + }); + } + + /** + * Disconnect from daemon + */ + disconnect() { + this.ws?.close(); + this.ws = null; + } +} diff --git a/gitnexus-mcp/src/bridge/protocol.ts b/gitnexus-mcp/src/bridge/protocol.ts new file mode 100644 index 0000000000..1578158ac6 --- /dev/null +++ b/gitnexus-mcp/src/bridge/protocol.ts @@ -0,0 +1,36 @@ +/** + * Bridge Protocol Types + * + * JSON-RPC-like protocol for communication between bridge and browser. + */ + +export interface ToolCallRequest { + id: string; + method: string; + params: Record; +} + +export interface ToolCallResponse { + id: string; + result?: any; + error?: { + code?: number; + message: string; + }; +} + +export type BridgeMessage = ToolCallRequest | ToolCallResponse; + +/** + * Check if message is a request (has method) + */ +export function isRequest(msg: BridgeMessage): msg is ToolCallRequest { + return 'method' in msg; +} + +/** + * Check if message is a response (has result or error) + */ +export function isResponse(msg: BridgeMessage): msg is ToolCallResponse { + return 'result' in msg || 'error' in msg; +} diff --git a/gitnexus-mcp/src/bridge/websocket-server.ts b/gitnexus-mcp/src/bridge/websocket-server.ts new file mode 100644 index 0000000000..0d43fb7d58 --- /dev/null +++ b/gitnexus-mcp/src/bridge/websocket-server.ts @@ -0,0 +1,163 @@ +/** + * WebSocket Bridge + * + * WebSocket server that connects to the GitNexus browser tab. + * Relays tool calls from MCP server to browser and returns results. + */ + +import { WebSocketServer, WebSocket } from 'ws'; +import { createServer as createNetServer } from 'net'; + +export interface BridgeMessage { + id: string; + method?: string; + params?: any; + result?: any; + error?: { message: string }; +} + +type RequestResolver = { + resolve: (result: any) => void; + reject: (error: Error) => void; +}; + +/** + * Check if a port is available + */ +async function isPortAvailable(port: number): Promise { + return new Promise((resolve) => { + const server = createNetServer(); + server.once('error', () => resolve(false)); + server.once('listening', () => { + server.close(); + resolve(true); + }); + server.listen(port); + }); +} + +export class WebSocketBridge { + private wss: WebSocketServer | null = null; + private client: WebSocket | null = null; + private pendingRequests: Map = new Map(); + private requestId = 0; + private started = false; + + constructor(private port: number = 54319) {} + + /** + * Start the WebSocket server (handles port-in-use gracefully) + */ + async start(): Promise { + const available = await isPortAvailable(this.port); + + if (!available) { + // Port already in use - another instance is handling browser connections + // This is OK - we can still run MCP server for stdio communication + console.error(`Port ${this.port} in use. Browser bridge running elsewhere.`); + return false; + } + + return new Promise((resolve) => { + this.wss = new WebSocketServer({ port: this.port }); + + this.wss.on('connection', (ws) => { + // Only allow one browser connection at a time + if (this.client) { + this.client.close(); + } + this.client = ws; + + ws.on('message', (data) => { + try { + const msg: BridgeMessage = JSON.parse(data.toString()); + this.handleMessage(msg); + } catch (error) { + console.error('Failed to parse message:', error); + } + }); + + ws.on('close', () => { + if (this.client === ws) { + this.client = null; + } + }); + + ws.on('error', (error) => { + console.error('WebSocket error:', error); + }); + }); + + this.wss.on('listening', () => { + this.started = true; + resolve(true); + }); + + this.wss.on('error', (error) => { + console.error('WebSocket server error:', error); + resolve(false); + }); + }); + } + + private handleMessage(msg: BridgeMessage) { + // This is a response to a pending request + if (msg.id && this.pendingRequests.has(msg.id)) { + const { resolve, reject } = this.pendingRequests.get(msg.id)!; + this.pendingRequests.delete(msg.id); + + if (msg.error) { + reject(new Error(msg.error.message)); + } else { + resolve(msg.result); + } + } + } + + /** + * Check if browser is connected + */ + get isConnected(): boolean { + return this.client !== null && this.client.readyState === WebSocket.OPEN; + } + + /** + * Check if server started successfully + */ + get isStarted(): boolean { + return this.started; + } + + /** + * Call a tool in the browser + */ + async callTool(method: string, params: any): Promise { + if (!this.isConnected) { + throw new Error('GitNexus browser not connected. Open GitNexus and enable MCP toggle.'); + } + + const id = `req_${++this.requestId}`; + + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { resolve, reject }); + + const msg: BridgeMessage = { id, method, params }; + this.client!.send(JSON.stringify(msg)); + + // Timeout after 30 seconds + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(new Error('Request timeout')); + } + }, 30000); + }); + } + + /** + * Close the WebSocket server + */ + close() { + this.wss?.close(); + } +} diff --git a/gitnexus-mcp/src/cli.ts b/gitnexus-mcp/src/cli.ts new file mode 100644 index 0000000000..726b44ed10 --- /dev/null +++ b/gitnexus-mcp/src/cli.ts @@ -0,0 +1,47 @@ +#!/usr/bin/env node +/** + * GitNexus MCP CLI + * + * Bridge between external AI agents (Cursor, Claude Code, Windsurf) + * and GitNexus code intelligence running in the browser. + */ + +import { Command } from 'commander'; +import { setupCommand } from './commands/setup.js'; +import { serveCommand } from './commands/serve.js'; +import { daemonCommand } from './commands/daemon.js'; +import { generateGuidanceCommand } from './commands/generate-guidance.js'; + +const program = new Command(); + +program + .name('gitnexus-mcp') + .description('MCP bridge for GitNexus code intelligence') + .version('0.1.0'); + +program + .command('setup') + .description('Detect AI tools and configure MCP settings') + .action(setupCommand); + +program + .command('daemon') + .description('Start the MCP daemon (run this once in background)') + .option('-p, --port ', 'WebSocket port', '54319') + .action(daemonCommand); + +program + .command('serve') + .description('Start MCP server for an AI tool (connects to daemon)') + .option('-p, --port ', 'Daemon port to connect to', '54319') + .action(serveCommand); + +program + .command('generate-guidance') + .description('Generate AI assistant guidance files (.cursorrules, AGENTS.md)') + .option('-o, --output ', 'Output directory', '.') + .action(generateGuidanceCommand); + +program.parse(); + + diff --git a/gitnexus-mcp/src/commands/daemon.ts b/gitnexus-mcp/src/commands/daemon.ts new file mode 100644 index 0000000000..42abe5541e --- /dev/null +++ b/gitnexus-mcp/src/commands/daemon.ts @@ -0,0 +1,194 @@ +/** + * Daemon Command + * + * Runs a persistent daemon that acts as the central hub for: + * - Browser connections (GitNexus web app) + * - MCP server connections (from AI tools like Cursor, Antigravity) + * + * This allows multiple AI tools to share the same browser bridge. + * Also stores codebase context sent by the browser for MCP resource exposure. + */ + +import { WebSocketServer, WebSocket } from 'ws'; + +interface BridgeMessage { + id: string; + method?: string; + params?: any; + result?: any; + error?: { message: string }; + source?: 'browser' | 'mcp'; + type?: 'context' | 'tool_call' | 'tool_result'; +} + +/** + * Codebase context sent from browser + */ +interface CodebaseContext { + projectName: string; + stats: { + fileCount: number; + functionCount: number; + classCount: number; + interfaceCount: number; + methodCount: number; + }; + hotspots: Array<{ + name: string; + type: string; + filePath: string; + connections: number; + }>; + folderTree: string; +} + +interface DaemonOptions { + port: string; +} + +// Module-level state for context (so MCP servers can access it) +let currentContext: CodebaseContext | null = null; + +export function getCodebaseContext(): CodebaseContext | null { + return currentContext; +} + +export async function daemonCommand(options: DaemonOptions) { + const port = parseInt(options.port, 10); + + let browserClient: WebSocket | null = null; + const mcpClients: Set = new Set(); + const pendingRequests: Map = new Map(); // request ID → which MCP client sent it + + const wss = new WebSocketServer({ port }); + + console.log(`🔌 GitNexus MCP Daemon running on port ${port}`); + console.log(' Waiting for connections...\n'); + + wss.on('connection', (ws, req) => { + // Determine client type from URL path + const url = new URL(req.url || '/', `http://localhost:${port}`); + const clientType = url.pathname === '/mcp' ? 'mcp' : 'browser'; + + if (clientType === 'browser') { + // Browser connection + if (browserClient) { + browserClient.close(); + } + browserClient = ws; + console.log('✅ Browser connected'); + + ws.on('message', (data) => { + try { + const msg: BridgeMessage = JSON.parse(data.toString()); + + // Handle context update from browser + if (msg.type === 'context' && msg.params) { + currentContext = msg.params as CodebaseContext; + console.log(`📊 Context received: ${currentContext.projectName} (${currentContext.stats.fileCount} files)`); + + // Broadcast context update to all MCP clients + const contextNotification = JSON.stringify({ + type: 'context_update', + context: currentContext, + }); + mcpClients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(contextNotification); + } + }); + return; + } + + // Response from browser - route back to the MCP client that sent the request + if (msg.id && pendingRequests.has(msg.id)) { + const mcpClient = pendingRequests.get(msg.id)!; + pendingRequests.delete(msg.id); + if (mcpClient.readyState === WebSocket.OPEN) { + mcpClient.send(JSON.stringify(msg)); + } + } + } catch (error) { + console.error('Failed to parse browser message:', error); + } + }); + + ws.on('close', () => { + if (browserClient === ws) { + browserClient = null; + currentContext = null; // Clear context when browser disconnects + console.log('❌ Browser disconnected'); + } + }); + + } else { + // MCP server connection + mcpClients.add(ws); + console.log(`✅ MCP client connected (total: ${mcpClients.size})`); + + // Send current context to new MCP client if available + if (currentContext) { + ws.send(JSON.stringify({ + type: 'context_update', + context: currentContext, + })); + } + + ws.on('message', (data) => { + try { + const msg: BridgeMessage = JSON.parse(data.toString()); + + // Handle context request + if (msg.method === 'getContext') { + ws.send(JSON.stringify({ + id: msg.id, + result: currentContext, + })); + return; + } + + // Tool call request from MCP server - forward to browser + if (msg.method && msg.id) { + if (browserClient && browserClient.readyState === WebSocket.OPEN) { + pendingRequests.set(msg.id, ws); + browserClient.send(JSON.stringify(msg)); + } else { + // No browser connected + ws.send(JSON.stringify({ + id: msg.id, + error: { message: 'GitNexus browser not connected. Open GitNexus and enable MCP toggle.' } + })); + } + } + } catch (error) { + console.error('Failed to parse MCP message:', error); + } + }); + + ws.on('close', () => { + mcpClients.delete(ws); + console.log(`❌ MCP client disconnected (remaining: ${mcpClients.size})`); + }); + } + + ws.on('error', (error) => { + console.error('WebSocket error:', error.message); + }); + }); + + wss.on('error', (error: NodeJS.ErrnoException) => { + if (error.code === 'EADDRINUSE') { + console.error(`❌ Port ${port} is already in use.`); + console.error(' A daemon may already be running. Check with: lsof -i :54319'); + process.exit(1); + } + console.error('Server error:', error); + }); + + // Keep the daemon running + process.on('SIGINT', () => { + console.log('\n👋 Shutting down daemon...'); + wss.close(); + process.exit(0); + }); +} diff --git a/gitnexus-mcp/src/commands/generate-guidance.ts b/gitnexus-mcp/src/commands/generate-guidance.ts new file mode 100644 index 0000000000..fe6f278a02 --- /dev/null +++ b/gitnexus-mcp/src/commands/generate-guidance.ts @@ -0,0 +1,153 @@ +/** + * Generate Guidance Command + * + * Creates AI assistant guidance files for a project: + * - .cursor/rules/gitnexus.mdc - Cursor skills file + * - AGENTS.md - Generic tool usage patterns for Claude Code etc. + */ + +import { writeFile, mkdir } from 'fs/promises'; +import { existsSync } from 'fs'; +import { join } from 'path'; +import chalk from 'chalk'; + +interface GenerateOptions { + output: string; +} + +// Cursor uses MDC format (Markdown with frontmatter) +const CURSOR_RULES_CONTENT = `--- +description: GitNexus MCP code intelligence tools +globs: +alwaysApply: true +--- + +# GitNexus Code Intelligence + +This project uses GitNexus MCP for code understanding. You have access to these tools: + +## Available MCP Tools + +### search +Semantic search across code. Returns functions, classes, files matching meaning. +- Use for: Finding code by description, not exact text +- Example: "authentication logic", "database connection handling" + +### cypher +Direct KuzuDB graph queries for relationship traversal. +- Pattern: \`MATCH (f:Function)-[:CodeRelation {type:'CALLS'}]->(g:Function) RETURN f.name, g.name\` +- Node types: File, Folder, Function, Class, Interface, Method, CodeElement +- Relation types: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS + +### blastRadius +Find all code affected by changing a node. Returns N-hop connections. +- Use for: Before refactoring, understanding impact of changes + +### highlight +Highlight nodes in the graph visualization. + +### context +Get project overview: stats, hotspots, folder tree. +- Use for: Understanding project structure + +### grep +Regex pattern search across file contents. +- Use for: Finding exact patterns, TODOs + +### read +Read file content by path. Supports line ranges. + +## Best Practices + +1. **Start with context** - Understand project structure first +2. **Use search for discovery** - Find by meaning, not keywords +3. **Use cypher for relationships** - Call chains, imports, inheritance +4. **Check blast radius before refactoring** - Understand impact + +## Cypher Examples + +\`\`\`cypher +# Find functions that call a specific function +MATCH (caller:Function)-[:CodeRelation {type:'CALLS'}]->(target:Function {name: 'authenticate'}) +RETURN caller.name, caller.filePath + +# Find class inheritance +MATCH (child:Class)-[:CodeRelation {type:'EXTENDS'}]->(parent:Class) +RETURN child.name, parent.name +\`\`\` +`; + +const AGENTS_MD_CONTENT = `# GitNexus Agent Guidelines + +This document describes how to use GitNexus MCP tools for code understanding. + +## Quick Reference + +| Tool | Use When | Example | +|------|----------|---------| +| \`search\` | Finding code by meaning | "user authentication flow" | +| \`cypher\` | Traversing relationships | Finding what calls a function | +| \`blastRadius\` | Before refactoring | What depends on this class? | +| \`context\` | Understanding project | Get overview, hotspots | +| \`grep\` | Exact pattern match | Find all TODO comments | +| \`read\` | Reading file content | View actual code | +| \`highlight\` | Visual feedback | Show user what you found | + +## Node Types +File, Folder, Function, Class, Interface, Method, CodeElement + +## Relation Types +CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS + +## Cypher Query Examples + +\`\`\`cypher +-- Find functions called by a specific function +MATCH (f:Function {name: 'handleRequest'})-[:CodeRelation {type:'CALLS'}]->(called) +RETURN called.name, called.filePath + +-- Find class hierarchy +MATCH (c:Class)-[:CodeRelation {type:'EXTENDS'}*1..3]->(parent) +RETURN c.name, collect(parent.name) as ancestors +\`\`\` + +## Workflow + +1. Run \`context\` to understand project structure +2. Use \`search\` to find relevant code +3. Use \`blastRadius\` before making changes +4. Use \`read\` to examine actual code +`; + +export async function generateGuidanceCommand(options: GenerateOptions): Promise { + const outputDir = options.output || '.'; + + console.log(chalk.cyan('\n🔧 Generating AI agent guidance files...\n')); + + try { + // Create .cursor/rules directory + const cursorRulesDir = join(outputDir, '.cursor', 'rules'); + if (!existsSync(cursorRulesDir)) { + await mkdir(cursorRulesDir, { recursive: true }); + } + + // Write .cursor/rules/gitnexus.mdc + const cursorRulePath = join(cursorRulesDir, 'gitnexus.mdc'); + await writeFile(cursorRulePath, CURSOR_RULES_CONTENT, 'utf-8'); + console.log(chalk.green('✓'), 'Created', chalk.bold('.cursor/rules/gitnexus.mdc')); + + // Write AGENTS.md + const agentsPath = join(outputDir, 'AGENTS.md'); + await writeFile(agentsPath, AGENTS_MD_CONTENT, 'utf-8'); + console.log(chalk.green('✓'), 'Created', chalk.bold('AGENTS.md')); + + console.log(chalk.green('\n✅ Guidance files generated successfully!')); + console.log(chalk.dim('\nThese files help AI assistants understand how to use GitNexus MCP tools.')); + console.log(chalk.dim('Commit them to your repo so all team members benefit.\n')); + + } catch (error) { + console.error(chalk.red('\n❌ Error generating guidance files:'), error); + process.exit(1); + } +} + diff --git a/gitnexus-mcp/src/commands/serve.ts b/gitnexus-mcp/src/commands/serve.ts new file mode 100644 index 0000000000..ed6039ef01 --- /dev/null +++ b/gitnexus-mcp/src/commands/serve.ts @@ -0,0 +1,33 @@ +/** + * Serve Command + * + * Starts the MCP server that bridges external AI agents to GitNexus. + * - Listens on stdio for MCP protocol (from AI tools) + * - Connects to daemon via WebSocket (client mode) + */ + +import { startMCPServer } from '../mcp/server.js'; +import { DaemonClient } from '../bridge/daemon-client.js'; + +interface ServeOptions { + port: string; +} + +export async function serveCommand(options: ServeOptions) { + const port = parseInt(options.port, 10); + + // Connect to daemon as a WebSocket client + const client = new DaemonClient(port); + + try { + await client.connect(); + } catch (error) { + // Daemon not running - provide helpful error + console.error('Failed to connect to GitNexus daemon.'); + console.error('Make sure the daemon is running: npx gitnexus-mcp daemon'); + process.exit(1); + } + + // Start MCP server on stdio (AI tools connect here) + await startMCPServer(client); +} diff --git a/gitnexus-mcp/src/commands/setup.ts b/gitnexus-mcp/src/commands/setup.ts new file mode 100644 index 0000000000..90470d2fda --- /dev/null +++ b/gitnexus-mcp/src/commands/setup.ts @@ -0,0 +1,55 @@ +/** + * Setup Command + * + * Detects installed AI tools (Cursor, Claude Code, Windsurf) and + * configures their MCP settings to use GitNexus. + */ + +import chalk from 'chalk'; +import ora from 'ora'; +import { detectIDEs, type IDE } from '../config/detect-ides.js'; +import { injectMCPConfig } from '../config/inject-config.js'; +import { getBridgePath } from '../config/paths.js'; + +export async function setupCommand() { + console.log(chalk.blue('\n🔧 GitNexus MCP Setup\n')); + + // Step 1: Get bridge path (this CLI itself is the bridge) + const spinner = ora('Locating bridge...').start(); + const bridgePath = getBridgePath(); + spinner.succeed(`Bridge located at ${chalk.dim(bridgePath)}`); + + // Step 2: Detect AI tools + spinner.start('Detecting AI tools...'); + const ides = await detectIDEs(); + + if (ides.length === 0) { + spinner.warn('No supported AI tools detected'); + console.log(chalk.yellow('\nSupported tools: Cursor, Claude Code, Windsurf, VS Code')); + console.log('Install one of these tools to use GitNexus MCP.\n'); + return; + } + + spinner.succeed(`Found: ${ides.map(i => chalk.cyan(i.name)).join(', ')}`); + + // Step 3: Inject MCP config into each IDE + for (const ide of ides) { + spinner.start(`Configuring ${ide.name}...`); + try { + await injectMCPConfig(ide, bridgePath); + spinner.succeed(`Configured ${chalk.cyan(ide.name)}`); + } catch (error) { + spinner.fail(`Failed to configure ${ide.name}: ${error}`); + } + } + + // Success message + console.log(chalk.green('\n✅ Setup complete!\n')); + console.log('Next steps:'); + console.log(' 1. Open GitNexus in your browser'); + console.log(' 2. Load a codebase'); + console.log(' 3. Click the MCP toggle to connect'); + console.log(' 4. Your AI tools can now use GitNexus!\n'); + + console.log(chalk.dim('Available tools: search, cypher, blastRadius, highlight\n')); +} diff --git a/gitnexus-mcp/src/config/detect-ides.ts b/gitnexus-mcp/src/config/detect-ides.ts new file mode 100644 index 0000000000..807645de39 --- /dev/null +++ b/gitnexus-mcp/src/config/detect-ides.ts @@ -0,0 +1,103 @@ +/** + * IDE Detection + * + * Detects installed AI coding tools by checking for their config directories. + * Supports Windows, macOS, and Linux paths. + */ + +import { existsSync } from 'fs'; +import { homedir, platform } from 'os'; +import { join } from 'path'; + +export interface IDE { + name: string; + configPath: string; +} + +interface IDECheck { + name: string; + // Paths relative to home directory (first found is used) + paths: { + win32: string[]; + darwin: string[]; + linux: string[]; + }; +} + +const IDE_CHECKS: IDECheck[] = [ + { + name: 'Cursor', + paths: { + win32: ['AppData/Roaming/Cursor/User/globalStorage/mcp.json', '.cursor/mcp.json'], + darwin: ['.cursor/mcp.json', 'Library/Application Support/Cursor/User/globalStorage/mcp.json'], + linux: ['.cursor/mcp.json', '.config/Cursor/User/globalStorage/mcp.json'], + }, + }, + { + name: 'Claude Code', + paths: { + win32: ['.claude/mcp.json', 'AppData/Roaming/Claude/mcp.json'], + darwin: ['.claude/mcp.json', 'Library/Application Support/Claude/mcp.json'], + linux: ['.claude/mcp.json', '.config/Claude/mcp.json'], + }, + }, + { + name: 'Windsurf', + paths: { + win32: ['.windsurf/mcp.json', '.codeium/windsurf/mcp.json'], + darwin: ['.windsurf/mcp.json', '.codeium/windsurf/mcp.json'], + linux: ['.windsurf/mcp.json', '.codeium/windsurf/mcp.json'], + }, + }, + { + name: 'VS Code', + paths: { + win32: ['AppData/Roaming/Code/User/globalStorage/mcp.json'], + darwin: ['Library/Application Support/Code/User/globalStorage/mcp.json'], + linux: ['.config/Code/User/globalStorage/mcp.json'], + }, + }, + { + name: 'Antigravity', + paths: { + win32: ['.gemini/antigravity/mcp_config.json'], + darwin: ['.gemini/antigravity/mcp_config.json'], + linux: ['.gemini/antigravity/mcp_config.json'], + }, + }, + { + name: 'OpenCode', + paths: { + win32: ['.config/opencode/opencode.json', 'opencode.json'], + darwin: ['.config/opencode/opencode.json', 'opencode.json'], + linux: ['.config/opencode/opencode.json', 'opencode.json'], + }, + }, +]; + +export async function detectIDEs(): Promise { + const home = homedir(); + const os = platform() as 'win32' | 'darwin' | 'linux'; + const ides: IDE[] = []; + + for (const check of IDE_CHECKS) { + const paths = check.paths[os] || check.paths.linux; + + for (const relPath of paths) { + const configPath = join(home, relPath); + // Check if the parent directory exists (IDE is installed) + const dirPath = configPath.replace(/[/\\]mcp\.json$/, ''); + const parentDir = dirPath.split(/[/\\]/).slice(0, -1).join('/'); + + // Look for IDE installation markers + const ideDirExists = existsSync(dirPath) || existsSync(parentDir); + + if (ideDirExists) { + ides.push({ name: check.name, configPath }); + break; // Found this IDE, move to next + } + } + } + + return ides; +} diff --git a/gitnexus-mcp/src/config/inject-config.ts b/gitnexus-mcp/src/config/inject-config.ts new file mode 100644 index 0000000000..8a7ba5de49 --- /dev/null +++ b/gitnexus-mcp/src/config/inject-config.ts @@ -0,0 +1,97 @@ +/** + * MCP Config Injection + * + * Adds GitNexus MCP server to an IDE's configuration. + * Handles different config formats for different tools. + */ + +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; +import { dirname } from 'path'; +import { type IDE } from './detect-ides.js'; + +interface MCPServerEntry { + command: string; + args?: string[]; + env?: Record; +} + +interface StandardMCPConfig { + mcpServers?: Record; +} + +// OpenCode uses a different format with 'mcp' key +interface OpenCodeConfig { + mcp?: Record; + [key: string]: any; +} + +export async function injectMCPConfig(ide: IDE, bridgePath: string): Promise { + // Create directory structure if needed + if (!existsSync(ide.configPath)) { + const dir = dirname(ide.configPath); + mkdirSync(dir, { recursive: true }); + } + + // Handle OpenCode's different format + if (ide.name === 'OpenCode') { + await injectOpenCodeConfig(ide.configPath, bridgePath); + return; + } + + // Standard MCP format (Cursor, Claude Code, Windsurf, VS Code, Antigravity) + let config: StandardMCPConfig = { mcpServers: {} }; + + // Read existing config if present + if (existsSync(ide.configPath)) { + try { + const content = readFileSync(ide.configPath, 'utf-8'); + config = JSON.parse(content); + } catch { + // Ignore parse errors, use empty config + } + } + + // Ensure mcpServers object exists + config.mcpServers = config.mcpServers || {}; + + // Add GitNexus MCP server configuration + config.mcpServers.gitnexus = { + command: bridgePath, + args: ['serve'], + }; + + // Write updated config + writeFileSync(ide.configPath, JSON.stringify(config, null, 2)); +} + +async function injectOpenCodeConfig(configPath: string, bridgePath: string): Promise { + let config: OpenCodeConfig = {}; + + // Read existing config if present + if (existsSync(configPath)) { + try { + const content = readFileSync(configPath, 'utf-8'); + config = JSON.parse(content); + } catch { + // Ignore parse errors + } + } + + // Ensure mcp object exists + config.mcp = config.mcp || {}; + + // Add GitNexus MCP server (OpenCode format) + config.mcp.gitnexus = { + type: 'local', + command: bridgePath, + args: ['serve'], + }; + + // Write updated config + writeFileSync(configPath, JSON.stringify(config, null, 2)); +} + diff --git a/gitnexus-mcp/src/config/paths.ts b/gitnexus-mcp/src/config/paths.ts new file mode 100644 index 0000000000..bfc9890820 --- /dev/null +++ b/gitnexus-mcp/src/config/paths.ts @@ -0,0 +1,37 @@ +/** + * Path Utilities + * + * Resolves paths for the MCP bridge executable. + */ + +import { fileURLToPath } from 'url'; +import { dirname, resolve } from 'path'; + +/** + * Get the path to this CLI executable. + * When installed via npm, this will be in node_modules/.bin/ + * The MCP config needs to point to the actual executable. + */ +export function getBridgePath(): string { + // When running as ES module, we need to resolve from import.meta.url + // But for the installed package, we want the bin path + + // Get the path to this module + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + + // Go up from dist/config/ to the package root, then to the bin + const packageRoot = resolve(__dirname, '..', '..'); + const binPath = resolve(packageRoot, 'dist', 'cli.js'); + + // Return as node command since it's a JS file + return `node ${binPath}`; +} + +/** + * Get user's GitNexus config directory + */ +export function getGitNexusConfigDir(): string { + const home = process.env.HOME || process.env.USERPROFILE || ''; + return resolve(home, '.gitnexus'); +} diff --git a/gitnexus-mcp/src/mcp/server.ts b/gitnexus-mcp/src/mcp/server.ts new file mode 100644 index 0000000000..2a32a93915 --- /dev/null +++ b/gitnexus-mcp/src/mcp/server.ts @@ -0,0 +1,220 @@ +/** + * MCP Server + * + * Model Context Protocol server that runs on stdio. + * External AI tools (Cursor, Claude Code) spawn this process and + * communicate via stdin/stdout using the MCP protocol. + * + * Exposes: + * - Tools: search, cypher, blastRadius, highlight + * - Resources: codebase context (stats, hotspots, folder tree) + */ + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { GITNEXUS_TOOLS } from './tools.js'; +import type { DaemonClient, CodebaseContext } from '../bridge/daemon-client.js'; + +// Interface for anything that can call tools (DaemonClient or WebSocketBridge) +interface ToolCaller { + callTool(method: string, params: any): Promise; + disconnect?(): void; + context?: CodebaseContext | null; + onContextChange?: (listener: (context: CodebaseContext | null) => void) => () => void; +} + +/** + * Format context as markdown for the resource + */ +function formatContextAsMarkdown(context: CodebaseContext): string { + const { projectName, stats, hotspots, folderTree } = context; + + const lines: string[] = []; + + lines.push(`# GitNexus: ${projectName}`); + lines.push(''); + lines.push('This codebase is currently loaded in GitNexus. Use the tools below to explore it.'); + lines.push(''); + + // Stats + lines.push('## 📊 Statistics'); + lines.push(`- **Files**: ${stats.fileCount}`); + lines.push(`- **Functions**: ${stats.functionCount}`); + if (stats.classCount > 0) lines.push(`- **Classes**: ${stats.classCount}`); + if (stats.interfaceCount > 0) lines.push(`- **Interfaces**: ${stats.interfaceCount}`); + if (stats.methodCount > 0) lines.push(`- **Methods**: ${stats.methodCount}`); + lines.push(''); + + // Hotspots + if (hotspots.length > 0) { + lines.push('## 🔥 Hotspots (Most Connected Nodes)'); + lines.push(''); + hotspots.forEach(h => { + lines.push(`- \`${h.name}\` (${h.type}) — ${h.connections} connections — ${h.filePath}`); + }); + lines.push(''); + } + + // Folder tree + if (folderTree) { + lines.push('## 📁 Project Structure'); + lines.push('```'); + lines.push(projectName + '/'); + lines.push(folderTree); + lines.push('```'); + lines.push(''); + } + + // Usage hints + lines.push('## 🛠️ Available Tools'); + lines.push(''); + lines.push('- **search**: Semantic search across the codebase'); + lines.push('- **cypher**: Execute Cypher queries on the knowledge graph'); + lines.push('- **blastRadius**: Analyze impact of changes to a node'); + lines.push('- **highlight**: Visualize nodes in the graph'); + lines.push(''); + lines.push('## 📝 Graph Schema'); + lines.push(''); + lines.push('**Node Types**: File, Folder, Function, Class, Interface, Method'); + lines.push(''); + lines.push('**Relation**: `CodeRelation` with `type` property:'); + lines.push('- CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS'); + lines.push(''); + lines.push('**Example Cypher Queries**:'); + lines.push('```cypher'); + lines.push('MATCH (f:Function) RETURN f.name LIMIT 10'); + lines.push("MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f.name, g.name"); + lines.push('```'); + + return lines.join('\n'); +} + +export async function startMCPServer(client: ToolCaller): Promise { + const server = new Server( + { + name: 'gitnexus', + version: '0.1.0', + }, + { + capabilities: { + tools: {}, + resources: {}, + }, + } + ); + + // Handle list resources request + server.setRequestHandler(ListResourcesRequestSchema, async () => { + const context = client.context; + + if (!context) { + return { resources: [] }; + } + + return { + resources: [ + { + uri: 'gitnexus://codebase/context', + name: `GitNexus: ${context.projectName}`, + description: `Codebase context for ${context.projectName} (${context.stats.fileCount} files, ${context.stats.functionCount} functions)`, + mimeType: 'text/markdown', + }, + ], + }; + }); + + // Handle read resource request + server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const { uri } = request.params; + + if (uri === 'gitnexus://codebase/context') { + const context = client.context; + + if (!context) { + return { + contents: [ + { + uri, + mimeType: 'text/plain', + text: 'No codebase loaded. Open GitNexus in your browser and load a repository.', + }, + ], + }; + } + + return { + contents: [ + { + uri, + mimeType: 'text/markdown', + text: formatContextAsMarkdown(context), + }, + ], + }; + } + + throw new Error(`Unknown resource: ${uri}`); + }); + + // Handle list tools request + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: GITNEXUS_TOOLS.map((tool) => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + })), + })); + + // Handle tool calls + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + // Forward the tool call to the browser via daemon + const result = await client.callTool(name, args); + + return { + content: [ + { + type: 'text', + text: typeof result === 'string' ? result : JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + return { + content: [ + { + type: 'text', + text: `Error: ${message}`, + }, + ], + isError: true, + }; + } + }); + + // Connect to stdio transport + const transport = new StdioServerTransport(); + await server.connect(transport); + + // Handle graceful shutdown + process.on('SIGINT', async () => { + client.disconnect?.(); + await server.close(); + process.exit(0); + }); + + process.on('SIGTERM', async () => { + client.disconnect?.(); + await server.close(); + process.exit(0); + }); +} diff --git a/gitnexus-mcp/src/mcp/tools.ts b/gitnexus-mcp/src/mcp/tools.ts new file mode 100644 index 0000000000..a822b1abb9 --- /dev/null +++ b/gitnexus-mcp/src/mcp/tools.ts @@ -0,0 +1,174 @@ +/** + * MCP Tool Definitions + * + * Defines the tools that GitNexus exposes to external AI agents. + * Each tool has a rich description with examples to help agents use them correctly. + */ + +export interface ToolDefinition { + name: string; + description: string; + inputSchema: { + type: 'object'; + properties: Record; + required: string[]; + }; +} + +export const GITNEXUS_TOOLS: ToolDefinition[] = [ + { + name: 'context', + description: `Get GitNexus codebase context. CALL THIS FIRST before using other tools. + +Returns: +- Project name and stats (files, functions, classes) +- Hotspots (most connected/important nodes) +- Directory structure (TOON format for token efficiency) +- Tool usage guidance + +ALWAYS call this first to understand the codebase before searching or querying.`, + inputSchema: { + type: 'object', + properties: {}, + required: [], + }, + }, + { + name: 'search', + description: `Hybrid search (keyword + semantic) across the codebase. +Returns code nodes with their graph connections. + +WHEN TO USE: +- Finding implementations ("where is auth handled?") +- Understanding code flow ("what calls UserService?") +- Locating patterns ("find all API endpoints") + +RETURNS: Array of {name, type, filePath, code, connections[]}`, + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Natural language or keyword search query' }, + limit: { type: 'number', description: 'Max results to return', default: 10 }, + }, + required: ['query'], + }, + }, + { + name: 'cypher', + description: `Execute Cypher query against the code knowledge graph. + +SCHEMA: +- Nodes: File, Function, Class, Interface, Method +- Edges: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, CONTAINS + +EXAMPLES: +• Find callers of a function: + MATCH (a)-[:CALLS]->(b:Function {name: "validateUser"}) RETURN a.name, a.filePath + +• Find class hierarchy: + MATCH (c:Class)-[:EXTENDS*]->(base) WHERE c.name = "AdminUser" RETURN base.name + +• Impact analysis (what depends on X): + MATCH (target:Function {name: $name})<-[:CALLS*1..3]-(caller) RETURN DISTINCT caller + +TIPS: +- Relationship types are UPPERCASE: CALLS, IMPORTS, EXTENDS +- Node labels are PascalCase: Function, Class, Interface +- Properties: name, filePath, code, startLine, endLine`, + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Cypher query to execute' }, + }, + required: ['query'], + }, + }, + { + name: 'grep', + description: `Regex search for exact patterns in file contents. + +WHEN TO USE: +- Finding exact strings: error codes, TODOs, specific API keys +- Pattern matching: all console.log, all fetch calls +- Finding imports of specific modules + +BETTER THAN search for: exact matches, regex patterns, case-sensitive + +RETURNS: Array of {filePath, line, lineNumber, match}`, + inputSchema: { + type: 'object', + properties: { + pattern: { type: 'string', description: 'Regex pattern to search for' }, + caseSensitive: { type: 'boolean', description: 'Case-sensitive search', default: false }, + maxResults: { type: 'number', description: 'Max results to return', default: 50 }, + }, + required: ['pattern'], + }, + }, + { + name: 'read', + description: `Read file content from the codebase. + +WHEN TO USE: +- After search/grep to see full context +- To understand implementation details +- Before making changes + +ALWAYS read before concluding - don't guess from names alone. + +RETURNS: {filePath, content, language, lines}`, + inputSchema: { + type: 'object', + properties: { + filePath: { type: 'string', description: 'Path to file to read' }, + startLine: { type: 'number', description: 'Start line (optional)' }, + endLine: { type: 'number', description: 'End line (optional)' }, + }, + required: ['filePath'], + }, + }, + { + name: 'blastRadius', + description: `Analyze the impact of changing a code element. +Returns all nodes that would be affected by modifying the target. + +USE BEFORE making changes to understand ripple effects. +Shows direct and indirect dependencies up to N hops. + +RETURNS: { + target: {...}, + affected: [{name, type, filePath, distance}...], + totalAffected: number, + riskLevel: "low" | "medium" | "high" +}`, + inputSchema: { + type: 'object', + properties: { + nodeId: { type: 'string', description: 'Node ID or name to analyze' }, + hops: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 }, + }, + required: ['nodeId'], + }, + }, + { + name: 'highlight', + description: `Highlight nodes in the GitNexus graph visualization. +Use after search/analysis to show the user what you found. + +The user will see the nodes glow in the graph view. +Great for visual confirmation of your findings.`, + inputSchema: { + type: 'object', + properties: { + nodeIds: { type: 'array', items: { type: 'string' }, description: 'Array of node IDs to highlight' }, + color: { type: 'string', description: 'Highlight color (optional, default: cyan)' }, + }, + required: ['nodeIds'], + }, + }, +]; diff --git a/gitnexus-mcp/test-output/.cursorrules b/gitnexus-mcp/test-output/.cursorrules new file mode 100644 index 0000000000..db2a1e2b8e --- /dev/null +++ b/gitnexus-mcp/test-output/.cursorrules @@ -0,0 +1,69 @@ +# GitNexus Code Intelligence + +This project uses GitNexus MCP for code understanding. When working with this codebase, you have access to powerful code intelligence tools. + +## Available MCP Tools + +### search +Semantic search across code. Returns functions, classes, files matching meaning. +- Use for: Finding code by description, not exact text +- Example queries: "authentication logic", "database connection handling", "error middleware" + +### cypher +Direct KuzuDB graph queries for precise relationship traversal. +- Use for: Finding call chains, imports, inheritance +- Pattern: `MATCH (f:Function)-[:CodeRelation {type:'CALLS'}]->(g:Function) RETURN f.name, g.name` +- Node types: File, Folder, Function, Class, Interface, Method, CodeElement +- Relation types: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS + +### blastRadius +Find all code affected by changing a node. Returns N-hop connections. +- Use for: Before refactoring, understanding impact of changes +- Returns: List of connected nodes with their types + +### highlight +Highlight nodes in the graph visualization. +- Use for: Showing the user what you're looking at + +### context +Get project overview: stats, hotspots, folder tree. +- Use for: Understanding project structure, finding important files +- Returns: File counts, hotspots (most connected files), directory tree + +### grep +Regex pattern search across file contents. +- Use for: Finding exact patterns, TODOs, specific strings +- Faster than search for literal text + +### read +Read file content by path. Supports line ranges. +- Use for: Reading actual code after finding via search/grep + +## Best Practices + +1. **Start with context** - Run `context` first to understand project structure +2. **Use search for discovery** - Find relevant code by meaning, not keywords +3. **Use cypher for relationships** - When you need call chains, imports, inheritance +4. **Check blast radius before refactoring** - Understand impact before making changes +5. **Read to confirm** - After finding code via search, read it to see full context + +## Cypher Query Examples + +```cypher +# Find all functions that call a specific function +MATCH (caller:Function)-[:CodeRelation {type:'CALLS'}]->(target:Function {name: 'authenticate'}) +RETURN caller.name, caller.filePath + +# Find class inheritance chain +MATCH (child:Class)-[:CodeRelation {type:'EXTENDS'}]->(parent:Class) +RETURN child.name, parent.name + +# Find all imports in a file +MATCH (f:File {name: 'api.ts'})-[:CodeRelation {type:'IMPORTS'}]->(imported:File) +RETURN imported.name, imported.filePath + +# Find hotspots (most connected nodes) +MATCH (n)-[r:CodeRelation]-() +RETURN n.name, labels(n), count(r) as connections +ORDER BY connections DESC LIMIT 10 +``` diff --git a/gitnexus-mcp/test-output/AGENTS.md b/gitnexus-mcp/test-output/AGENTS.md new file mode 100644 index 0000000000..3bb2217467 --- /dev/null +++ b/gitnexus-mcp/test-output/AGENTS.md @@ -0,0 +1,98 @@ +# GitNexus Agent Guidelines + +This document describes how to use GitNexus MCP tools effectively for code understanding. + +## Quick Reference + +| Tool | Use When | Example | +|------|----------|---------| +| `search` | Finding code by meaning | "user authentication flow" | +| `cypher` | Traversing relationships | Finding what calls a function | +| `blastRadius` | Before refactoring | What depends on this class? | +| `context` | Understanding project | Get overview, hotspots | +| `grep` | Exact pattern match | Find all TODO comments | +| `read` | Reading file content | View actual code | +| `highlight` | Visual feedback | Show user what you found | + +## Tool Details + +### search(query, limit?) +Semantic search using embeddings. Understands meaning, not just keywords. + +**Good for:** +- "error handling middleware" +- "database connection pooling" +- "form validation logic" + +**Returns:** Array of matching nodes with id, name, type, filePath, score + +--- + +### cypher(query) +Direct graph queries using Cypher syntax. Use for relationship traversal. + +**Node Tables:** File, Folder, Function, Class, Interface, Method, CodeElement + +**Relation Types:** CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS + +**Example Queries:** +```cypher +-- Find functions called by a specific function +MATCH (f:Function {name: 'handleRequest'})-[:CodeRelation {type:'CALLS'}]->(called) +RETURN called.name, called.filePath + +-- Find class hierarchy +MATCH (c:Class)-[:CodeRelation {type:'EXTENDS'}*1..3]->(parent) +RETURN c.name, collect(parent.name) as ancestors +``` + +--- + +### blastRadius(nodeId, hops?) +Find all nodes within N hops of a starting node. + +**Use before:** Renaming, refactoring, deleting code + +--- + +### context() +Get project overview including: +- File/function/class counts +- Hotspots (most connected nodes) +- Folder tree in TOON format + +**Use first** when starting work on unfamiliar project. + +--- + +### grep(pattern, caseSensitive?, maxResults?) +Regex search across file contents. + +**Faster than search** for exact patterns like: +- `TODO`, `FIXME`, `HACK` +- Specific function calls +- Import statements + +--- + +### read(filePath, startLine?, endLine?) +Read file content. Use after finding files via search/grep. + +--- + +## Workflow Recommendations + +### New to a Project +1. Run `context` to see structure and hotspots +2. Use `search` to find relevant areas +3. Use `read` to examine actual code + +### Before Refactoring +1. Find the node: `search("function to refactor")` +2. Check impact: `blastRadius(nodeId, 2)` +3. Review callers via `cypher` + +### Debugging +1. Use `grep` to find error patterns +2. Use `cypher` to trace call chain to error +3. Use `read` to examine suspicious code diff --git a/gitnexus-mcp/tsconfig.json b/gitnexus-mcp/tsconfig.json new file mode 100644 index 0000000000..6f81521615 --- /dev/null +++ b/gitnexus-mcp/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/README.md b/gitnexus/README.md similarity index 100% rename from README.md rename to gitnexus/README.md diff --git a/api/proxy.ts b/gitnexus/api/proxy.ts similarity index 100% rename from api/proxy.ts rename to gitnexus/api/proxy.ts diff --git a/index.html b/gitnexus/index.html similarity index 100% rename from index.html rename to gitnexus/index.html diff --git a/package-lock.json b/gitnexus/package-lock.json similarity index 100% rename from package-lock.json rename to gitnexus/package-lock.json diff --git a/package.json b/gitnexus/package.json similarity index 100% rename from package.json rename to gitnexus/package.json diff --git a/public/wasm/javascript/tree-sitter-javascript.wasm b/gitnexus/public/wasm/javascript/tree-sitter-javascript.wasm similarity index 100% rename from public/wasm/javascript/tree-sitter-javascript.wasm rename to gitnexus/public/wasm/javascript/tree-sitter-javascript.wasm diff --git a/public/wasm/kuzu-wasm.wasm b/gitnexus/public/wasm/kuzu-wasm.wasm similarity index 100% rename from public/wasm/kuzu-wasm.wasm rename to gitnexus/public/wasm/kuzu-wasm.wasm diff --git a/public/wasm/python/tree-sitter-python.wasm b/gitnexus/public/wasm/python/tree-sitter-python.wasm similarity index 100% rename from public/wasm/python/tree-sitter-python.wasm rename to gitnexus/public/wasm/python/tree-sitter-python.wasm diff --git a/public/wasm/tree-sitter.wasm b/gitnexus/public/wasm/tree-sitter.wasm similarity index 100% rename from public/wasm/tree-sitter.wasm rename to gitnexus/public/wasm/tree-sitter.wasm diff --git a/public/wasm/typescript/tree-sitter-tsx.wasm b/gitnexus/public/wasm/typescript/tree-sitter-tsx.wasm similarity index 100% rename from public/wasm/typescript/tree-sitter-tsx.wasm rename to gitnexus/public/wasm/typescript/tree-sitter-tsx.wasm diff --git a/public/wasm/typescript/tree-sitter-typescript.wasm b/gitnexus/public/wasm/typescript/tree-sitter-typescript.wasm similarity index 100% rename from public/wasm/typescript/tree-sitter-typescript.wasm rename to gitnexus/public/wasm/typescript/tree-sitter-typescript.wasm diff --git a/src/App.tsx b/gitnexus/src/App.tsx similarity index 100% rename from src/App.tsx rename to gitnexus/src/App.tsx diff --git a/gitnexus/src/components/ActivityFeed.tsx b/gitnexus/src/components/ActivityFeed.tsx new file mode 100644 index 0000000000..0f4444e0df --- /dev/null +++ b/gitnexus/src/components/ActivityFeed.tsx @@ -0,0 +1,202 @@ +/** + * Activity Feed Component + * + * Shows real-time log of external AI agent tool calls. + * Used in RightPanel as an alternative to the Chat tab. + */ + +import { useState, useEffect, useRef } from 'react'; +import { Activity, Search, Database, Terminal, Eye, Loader2, CheckCircle, XCircle, Clock, FileText, Zap } from 'lucide-react'; +import { getMCPClient, type ActivityEvent } from '../core/mcp/mcp-client'; + +// Tool icons +const TOOL_ICONS: Record = { + context: Zap, + search: Search, + cypher: Database, + grep: Terminal, + read: FileText, + blastRadius: Activity, + highlight: Eye, +}; + +// Tool colors +const TOOL_COLORS: Record = { + context: 'text-amber-400', + search: 'text-cyan-400', + cypher: 'text-purple-400', + grep: 'text-green-400', + read: 'text-blue-400', + blastRadius: 'text-rose-400', + highlight: 'text-teal-400', +}; + +export function ActivityFeed() { + const [events, setEvents] = useState([]); + const containerRef = useRef(null); + + useEffect(() => { + const client = getMCPClient(); + + // Subscribe to activity events + const unsubscribe = client.onActivity((event) => { + setEvents(prev => { + // Keep max 100 events + const next = [...prev, event]; + if (next.length > 100) { + next.shift(); + } + return next; + }); + }); + + // Get existing events + setEvents(client.getActivityLog()); + + return () => { + unsubscribe(); + }; + }, []); + + // Auto-scroll to bottom + useEffect(() => { + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }, [events]); + + const formatTime = (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + }; + + const formatParams = (params: any): string => { + if (!params) return ''; + // Show first key-value pairs, truncated + const entries = Object.entries(params).slice(0, 2); + return entries.map(([k, v]) => { + const val = typeof v === 'string' ? v.slice(0, 30) : JSON.stringify(v).slice(0, 30); + return `${k}: ${val}${val.length >= 30 ? '...' : ''}`; + }).join(', '); + }; + + const formatResult = (event: ActivityEvent): string => { + if (event.status === 'running') return 'Running...'; + if (event.status === 'error') return `Error: ${event.error?.slice(0, 50) || 'Unknown'}`; + + // Format result based on type + if (Array.isArray(event.result)) { + return `${event.result.length} results`; + } + if (typeof event.result === 'object' && event.result) { + const keys = Object.keys(event.result); + if (keys.includes('content')) return `${event.result.content?.length || 0} chars`; + if (keys.includes('projectName')) return event.result.projectName; + return `{${keys.slice(0, 3).join(', ')}${keys.length > 3 ? '...' : ''}}`; + } + return String(event.result || 'Done'); + }; + + if (events.length === 0) { + return ( +
+
+ 📡 +
+

+ No Agent Activity +

+

+ When external AI agents (Cursor, Claude Code) call GitNexus tools, + their activity will appear here in real-time. +

+

+ Make sure MCP toggle is enabled in the header +

+
+ ); + } + + return ( +
+
+ {events.map((event) => { + const Icon = TOOL_ICONS[event.tool] || Activity; + const color = TOOL_COLORS[event.tool] || 'text-text-muted'; + + return ( +
+ {/* Header row */} +
+ {/* Agent color indicator */} + {event.agentColor && ( +
+ )} + + {event.tool} + {event.agentName && event.agentName !== 'Unknown' && ( + + {event.agentName} + + )} + + + {formatTime(event.timestamp)} + +
+ + {/* Params preview */} + {event.params && Object.keys(event.params).length > 0 && ( +
+ {formatParams(event.params)} +
+ )} + + {/* Status/Result */} +
+ {event.status === 'running' && ( + <> + + Running... + + )} + {event.status === 'complete' && ( + <> + + {formatResult(event)} + {event.duration && ( + {event.duration}ms + )} + + )} + {event.status === 'error' && ( + <> + + {formatResult(event)} + + )} +
+
+ ); + })} +
+
+ ); +} diff --git a/src/components/CodeReferencesPanel.tsx b/gitnexus/src/components/CodeReferencesPanel.tsx similarity index 100% rename from src/components/CodeReferencesPanel.tsx rename to gitnexus/src/components/CodeReferencesPanel.tsx diff --git a/src/components/DropZone.tsx b/gitnexus/src/components/DropZone.tsx similarity index 100% rename from src/components/DropZone.tsx rename to gitnexus/src/components/DropZone.tsx diff --git a/src/components/EmbeddingStatus.tsx b/gitnexus/src/components/EmbeddingStatus.tsx similarity index 100% rename from src/components/EmbeddingStatus.tsx rename to gitnexus/src/components/EmbeddingStatus.tsx diff --git a/src/components/FileTreePanel.tsx b/gitnexus/src/components/FileTreePanel.tsx similarity index 85% rename from src/components/FileTreePanel.tsx rename to gitnexus/src/components/FileTreePanel.tsx index 936c856c6d..6daca37090 100644 --- a/src/components/FileTreePanel.tsx +++ b/gitnexus/src/components/FileTreePanel.tsx @@ -1,8 +1,8 @@ import { useState, useMemo, useCallback, useEffect } from 'react'; -import { - ChevronRight, - ChevronDown, - Folder, +import { + ChevronRight, + ChevronDown, + Folder, FolderOpen, FileCode, Search, @@ -16,7 +16,7 @@ import { Target, } from 'lucide-react'; import { useAppState } from '../hooks/useAppState'; -import { FILTERABLE_LABELS, NODE_COLORS } from '../lib/constants'; +import { FILTERABLE_LABELS, NODE_COLORS, ALL_EDGE_TYPES, EDGE_INFO, type EdgeType } from '../lib/constants'; import { GraphNode, NodeLabel } from '../core/graph/types'; // Tree node structure @@ -36,7 +36,7 @@ const buildFileTree = (nodes: GraphNode[]): TreeNode[] => { // Filter to only folders and files const fileNodes = nodes.filter(n => n.label === 'Folder' || n.label === 'File'); - + // Sort by path to ensure parents come before children fileNodes.sort((a, b) => a.properties.filePath.localeCompare(b.properties.filePath)); @@ -47,13 +47,13 @@ const buildFileTree = (nodes: GraphNode[]): TreeNode[] => { parts.forEach((part, index) => { currentPath = currentPath ? `${currentPath}/${part}` : part; - + let existing = pathMap.get(currentPath); - + if (!existing) { const isLastPart = index === parts.length - 1; const isFile = isLastPart && node.label === 'File'; - + existing = { id: isLastPart ? node.id : currentPath, name: part, @@ -62,11 +62,11 @@ const buildFileTree = (nodes: GraphNode[]): TreeNode[] => { children: [], graphNode: isLastPart ? node : undefined, }; - + pathMap.set(currentPath, existing); currentLevel.push(existing); } - + currentLevel = existing.children; }); }); @@ -85,23 +85,23 @@ interface TreeItemProps { selectedPath: string | null; } -const TreeItem = ({ - node, - depth, - searchQuery, - onNodeClick, - expandedPaths, +const TreeItem = ({ + node, + depth, + searchQuery, + onNodeClick, + expandedPaths, toggleExpanded, selectedPath, }: TreeItemProps) => { const isExpanded = expandedPaths.has(node.path); const isSelected = selectedPath === node.path; const hasChildren = node.children.length > 0; - + // Filter children based on search const filteredChildren = useMemo(() => { if (!searchQuery) return node.children; - return node.children.filter(child => + return node.children.filter(child => child.name.toLowerCase().includes(searchQuery.toLowerCase()) || child.children.some(c => c.name.toLowerCase().includes(searchQuery.toLowerCase())) ); @@ -139,7 +139,7 @@ const TreeItem = ({ ) : ( )} - + {/* Node icon */} {node.type === 'folder' ? ( isExpanded ? ( @@ -150,11 +150,11 @@ const TreeItem = ({ ) : ( )} - + {/* Name */} {node.name} - + {/* Children */} {isExpanded && filteredChildren.length > 0 && (
@@ -195,8 +195,8 @@ interface FileTreePanelProps { } export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => { - const { graph, visibleLabels, toggleLabelVisibility, selectedNode, setSelectedNode, openCodePanel, depthFilter, setDepthFilter } = useAppState(); - + const { graph, visibleLabels, toggleLabelVisibility, visibleEdgeTypes, toggleEdgeVisibility, selectedNode, setSelectedNode, openCodePanel, depthFilter, setDepthFilter } = useAppState(); + const [isCollapsed, setIsCollapsed] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [expandedPaths, setExpandedPaths] = useState>(new Set()); @@ -220,18 +220,18 @@ export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => { useEffect(() => { const path = selectedNode?.properties?.filePath; if (!path) return; - + // Expand all parent folders leading to this file const parts = path.split('/').filter(Boolean); const pathsToExpand: string[] = []; let currentPath = ''; - + // Build all parent paths (exclude the last part if it's a file) for (let i = 0; i < parts.length - 1; i++) { currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]; pathsToExpand.push(currentPath); } - + if (pathsToExpand.length > 0) { setExpandedPaths(prev => { const next = new Set(prev); @@ -303,21 +303,19 @@ export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => {
@@ -381,32 +379,32 @@ export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => { Toggle visibility of node types in the graph

- +
{FILTERABLE_LABELS.map((label) => { const Icon = getNodeTypeIcon(label); const isVisible = visibleLabels.includes(label); - + return ( @@ -414,6 +412,46 @@ export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => { })}
+ {/* Edge Type Toggles */} +
+

+ Edge Types +

+

+ Toggle visibility of relationship types +

+ +
+ {ALL_EDGE_TYPES.map((edgeType) => { + const info = EDGE_INFO[edgeType]; + const isVisible = visibleEdgeTypes.includes(edgeType); + + return ( + + ); + })} +
+
+ {/* Depth Filter */}

@@ -423,7 +461,7 @@ export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => {

Show nodes within N hops of selection

- +
{[ { value: null, label: 'All' }, @@ -447,7 +485,7 @@ export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => { ))}
- + {depthFilter !== null && !selectedNode && (

Select a node to apply depth filter @@ -463,7 +501,7 @@ export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => {

{(['Folder', 'File', 'Class', 'Function', 'Interface', 'Method'] as NodeLabel[]).map(label => (
-
diff --git a/src/components/GraphCanvas.tsx b/gitnexus/src/components/GraphCanvas.tsx similarity index 97% rename from src/components/GraphCanvas.tsx rename to gitnexus/src/components/GraphCanvas.tsx index 67e873b8a1..2bf4f1e688 100644 --- a/src/components/GraphCanvas.tsx +++ b/gitnexus/src/components/GraphCanvas.tsx @@ -16,6 +16,7 @@ export const GraphCanvas = forwardRef((_, ref) => { setSelectedNode, selectedNode: appSelectedNode, visibleLabels, + visibleEdgeTypes, openCodePanel, depthFilter, highlightedNodeIds, @@ -24,6 +25,7 @@ export const GraphCanvas = forwardRef((_, ref) => { blastRadiusNodeIds, isAIHighlightsEnabled, toggleAIHighlights, + animatedNodes, } = useAppState(); const [hoveredNodeName, setHoveredNodeName] = useState(null); @@ -42,6 +44,12 @@ export const GraphCanvas = forwardRef((_, ref) => { return blastRadiusNodeIds; }, [blastRadiusNodeIds, isAIHighlightsEnabled]); + // Animated nodes (only when AI highlights enabled) + const effectiveAnimatedNodes = useMemo(() => { + if (!isAIHighlightsEnabled) return new Map(); + return animatedNodes; + }, [animatedNodes, isAIHighlightsEnabled]); + const handleNodeClick = useCallback((nodeId: string) => { if (!graph) return; const node = graph.nodes.find(n => n.id === nodeId); @@ -85,6 +93,8 @@ export const GraphCanvas = forwardRef((_, ref) => { onStageClick: handleStageClick, highlightedNodeIds: effectiveHighlightedNodeIds, blastRadiusNodeIds: effectiveBlastRadiusNodeIds, + animatedNodes: effectiveAnimatedNodes, + visibleEdgeTypes, }); // Expose focusNode to parent via ref diff --git a/src/components/Header.tsx b/gitnexus/src/components/Header.tsx similarity index 73% rename from src/components/Header.tsx rename to gitnexus/src/components/Header.tsx index 44c90fb4e4..9e4936223e 100644 --- a/src/components/Header.tsx +++ b/gitnexus/src/components/Header.tsx @@ -4,6 +4,7 @@ import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { GraphNode } from '../core/graph/types'; import { EmbeddingStatus } from './EmbeddingStatus'; import { MCPToggle } from './MCPToggle'; +import { buildCodebaseContext } from '../core/llm/context-builder'; // Color mapping for node types in search results const NODE_TYPE_COLORS: Record = { @@ -33,6 +34,8 @@ export const Header = ({ onFocusNode }: HeaderProps) => { runQuery, semanticSearch, setHighlightedNodeIds, + fileContents, + triggerNodeAnimation, } = useAppState(); const [searchQuery, setSearchQuery] = useState(''); const [isSearchOpen, setIsSearchOpen] = useState(false); @@ -223,6 +226,11 @@ export const Header = ({ onFocusNode }: HeaderProps) => { onSearch={async (query, limit = 10) => { // Use semantic search from the app const results = await semanticSearch(query, limit); + // Trigger pulse animation on search results + const nodeIds = results.map((r: any) => r.id).filter(Boolean); + if (nodeIds.length > 0) { + triggerNodeAnimation(nodeIds, 'pulse'); + } return results; }} onCypher={async (query) => { @@ -238,11 +246,101 @@ export const Header = ({ onFocusNode }: HeaderProps) => { RETURN DISTINCT connected.id AS id, connected.name AS name, labels(connected) AS labels `; const results = await runQuery(query); + // Trigger ripple animation on blast radius results + const nodeIds = results.map((r: any) => r.id).filter(Boolean); + if (nodeIds.length > 0) { + triggerNodeAnimation(nodeIds, 'ripple'); + } return results; }} onHighlight={(nodeIds) => { // Highlight nodes in the graph setHighlightedNodeIds(new Set(nodeIds)); + // Trigger glow animation on highlighted nodes + if (nodeIds.length > 0) { + triggerNodeAnimation(nodeIds, 'glow'); + } + }} + getContext={async () => { + // Build codebase context for external AI agents + if (!projectName) return null; + const context = await buildCodebaseContext(runQuery, projectName); + // Reshape to match MCP CodebaseContext format + return { + projectName: context.stats.projectName, + stats: { + fileCount: context.stats.fileCount, + functionCount: context.stats.functionCount, + classCount: context.stats.classCount, + interfaceCount: context.stats.interfaceCount, + methodCount: context.stats.methodCount, + }, + hotspots: context.hotspots, + folderTree: context.folderTree, + }; + }} + onGrep={async (pattern, caseSensitive = false, maxResults = 50) => { + // Grep across file contents + const results: Array<{ filePath: string; line: string; lineNumber: number; match: string }> = []; + const regex = new RegExp(pattern, caseSensitive ? 'g' : 'gi'); + + for (const [filePath, content] of fileContents.entries()) { + const lines = content.split('\n'); + for (let i = 0; i < lines.length && results.length < maxResults; i++) { + const line = lines[i]; + const match = line.match(regex); + if (match) { + results.push({ + filePath, + line: line.trim(), + lineNumber: i + 1, + match: match[0], + }); + } + } + if (results.length >= maxResults) break; + } + return results; + }} + onRead={async (filePath, startLine, endLine) => { + // Read file content + let content = fileContents.get(filePath); + + // Try normalized path if not found + if (!content) { + const normalizedPath = filePath.replace(/\\/g, '/'); + for (const [path, c] of fileContents.entries()) { + if (path.endsWith(normalizedPath) || normalizedPath.endsWith(path)) { + content = c; + break; + } + } + } + + if (!content) { + return { error: `File not found: ${filePath}` }; + } + + const lines = content.split('\n'); + const language = filePath.split('.').pop() || 'text'; + + // If line range specified, return only those lines + if (startLine !== undefined && endLine !== undefined) { + const slice = lines.slice(startLine - 1, endLine); + return { + filePath, + content: slice.join('\n'), + language, + lines: slice.length, + }; + } + + return { + filePath, + content, + language, + lines: lines.length, + }; }} /> diff --git a/src/components/LoadingOverlay.tsx b/gitnexus/src/components/LoadingOverlay.tsx similarity index 100% rename from src/components/LoadingOverlay.tsx rename to gitnexus/src/components/LoadingOverlay.tsx diff --git a/src/components/MCPToggle.tsx b/gitnexus/src/components/MCPToggle.tsx similarity index 85% rename from src/components/MCPToggle.tsx rename to gitnexus/src/components/MCPToggle.tsx index 8a1c6b2401..14b40d057f 100644 --- a/src/components/MCPToggle.tsx +++ b/gitnexus/src/components/MCPToggle.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useCallback } from 'react'; import { Copy, Check, AlertCircle, X, Sparkles } from 'lucide-react'; -import { getMCPClient } from '../core/mcp/mcp-client'; +import { getMCPClient, type CodebaseContext } from '../core/mcp/mcp-client'; type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error'; @@ -20,8 +20,14 @@ interface MCPToggleProps { onBlastRadius?: (nodeId: string, hops?: number) => Promise; /** Handler for highlight tool calls from external agents */ onHighlight?: (nodeIds: string[], color?: string) => void; + /** Handler for grep tool calls from external agents */ + onGrep?: (pattern: string, caseSensitive?: boolean, maxResults?: number) => Promise; + /** Handler for read tool calls from external agents */ + onRead?: (filePath: string, startLine?: number, endLine?: number) => Promise; /** Whether to show the onboarding tip */ showOnboardingTip?: boolean; + /** Callback to get codebase context for external agents */ + getContext?: () => Promise; } const MCP_TIP_DISMISSED_KEY = 'gitnexus-mcp-tip-dismissed'; @@ -31,7 +37,10 @@ export function MCPToggle({ onCypher, onBlastRadius, onHighlight, + onGrep, + onRead, showOnboardingTip = false, + getContext, }: MCPToggleProps = {}) { const [status, setStatus] = useState('disconnected'); const [copied, setCopied] = useState(false); @@ -92,14 +101,48 @@ export function MCPToggle({ }); } + // Register context tool handler + if (getContext) { + client.registerHandler('context', async () => { + const context = await getContext(); + return context; + }); + } + + // Register grep tool handler + if (onGrep) { + client.registerHandler('grep', async (params) => { + return await onGrep(params.pattern, params.caseSensitive, params.maxResults); + }); + } + + // Register read tool handler + if (onRead) { + client.registerHandler('read', async (params) => { + return await onRead(params.filePath, params.startLine, params.endLine); + }); + } + setStatus('connected'); setShowSetup(false); localStorage.setItem(MCP_TIP_DISMISSED_KEY, 'true'); + + // Send codebase context after connecting + if (getContext) { + try { + const context = await getContext(); + if (context) { + client.sendContext(context); + } + } catch (e) { + console.error('[MCP] Failed to send context:', e); + } + } } catch { setStatus('error'); setShowSetup(true); } - }, [onSearch, onCypher, onBlastRadius, onHighlight]); + }, [onSearch, onCypher, onBlastRadius, onHighlight, onGrep, onRead, getContext]); const disconnect = useCallback(() => { const client = getMCPClient(); diff --git a/gitnexus/src/components/MarkdownRenderer.tsx b/gitnexus/src/components/MarkdownRenderer.tsx new file mode 100644 index 0000000000..3d1e7cdfc2 --- /dev/null +++ b/gitnexus/src/components/MarkdownRenderer.tsx @@ -0,0 +1,172 @@ +import React from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { MermaidDiagram } from './MermaidDiagram'; +import { ToolCallCard } from './ToolCallCard'; + +// Custom syntax theme +const customTheme = { + ...vscDarkPlus, + 'pre[class*="language-"]': { + ...vscDarkPlus['pre[class*="language-"]'], + background: '#0a0a10', + margin: 0, + padding: '16px 0', + fontSize: '13px', + lineHeight: '1.6', + }, + 'code[class*="language-"]': { + ...vscDarkPlus['code[class*="language-"]'], + background: 'transparent', + fontFamily: '"JetBrains Mono", "Fira Code", monospace', + }, +}; + +interface MarkdownRendererProps { + content: string; + onLinkClick?: (href: string) => void; + toolCalls?: any[]; // Keep flexible for now +} + +export const MarkdownRenderer: React.FC = ({ + content, + onLinkClick, + toolCalls +}) => { + + // Helper to format text for display (convert [[links]] to markdown links) + const formatMarkdownForDisplay = (md: string) => { + // Avoid rewriting inside fenced code blocks. + const parts = md.split('```'); + for (let i = 0; i < parts.length; i += 2) { + // Pattern 1: File grounding - [[file.ext]] + parts[i] = parts[i].replace( + /\[\[([a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+(?::\d+(?:[-–]\d+)?)?)\]\]/g, + (_m, inner: string) => { + const trimmed = inner.trim(); + const href = `code-ref:${encodeURIComponent(trimmed)}`; + return `[${trimmed}](${href})`; + } + ); + + // Pattern 2: Node grounding - [[Type:Name]] + parts[i] = parts[i].replace( + /\[\[(?:graph:)?(Class|Function|Method|Interface|File|Folder|Variable|Enum|Type|CodeElement):([^\]]+)\]\]/g, + (_m, nodeType: string, nodeName: string) => { + const trimmed = `${nodeType}:${nodeName.trim()}`; + const href = `node-ref:${encodeURIComponent(trimmed)}`; + return `[${trimmed}](${href})`; + } + ); + } + return parts.join('```'); + }; + + const handleLinkClick = (e: React.MouseEvent, href: string) => { + if (href.startsWith('code-ref:') || href.startsWith('node-ref:')) { + e.preventDefault(); + onLinkClick?.(href); + } + // External links open in new tab (default behavior) + }; + + const formattedContent = formatMarkdownForDisplay(content); + + return ( +
+ { + const hrefStr = href || ''; + + // Grounding links (Code refs & Node refs) + if (hrefStr.startsWith('code-ref:') || hrefStr.startsWith('node-ref:')) { + const isNodeRef = hrefStr.startsWith('node-ref:'); + const inner = decodeURIComponent(hrefStr.slice(isNodeRef ? 9 : 9)); // length is same? wait.. code-ref: (9), node-ref: (9). Yes. + + // Styles + const baseParams = "code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md font-mono text-[12px] !no-underline hover:!no-underline transition-colors"; + const colorParams = isNodeRef + ? "border border-amber-300/55 bg-amber-400/10 !text-amber-200 visited:!text-amber-200 hover:bg-amber-400/15 hover:border-amber-200/70" + : "border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 hover:bg-cyan-400/15 hover:border-cyan-200/70"; + + return ( + handleLinkClick(e, hrefStr)} + className={`${baseParams} ${colorParams}`} + title={isNodeRef ? `View ${inner} in Code panel` : `Open in Code panel • ${inner}`} + {...props} + > + {children} + + ); + } + + // External links + return ( + + {children} + + ); + }, + code: ({ className, children, ...props }) => { + const match = /language-(\w+)/.exec(className || ''); + const isInline = !className && !match; + const codeContent = String(children).replace(/\n$/, ''); + + if (isInline) { + return {children}; + } + + const language = match ? match[1] : 'text'; + + // Render Mermaid diagrams + if (language === 'mermaid') { + return ; + } + + return ( + + {codeContent} + + ); + }, + pre: ({ children }) => <>{children}, + }} + > + {formattedContent} + + + {/* Tool Call Cards appended at the bottom if provided */} + {toolCalls && toolCalls.length > 0 && ( +
+ {toolCalls.map(tc => ( + + ))} +
+ )} +
+ ); +}; diff --git a/src/components/MermaidDiagram.tsx b/gitnexus/src/components/MermaidDiagram.tsx similarity index 100% rename from src/components/MermaidDiagram.tsx rename to gitnexus/src/components/MermaidDiagram.tsx diff --git a/src/components/QueryFAB.tsx b/gitnexus/src/components/QueryFAB.tsx similarity index 100% rename from src/components/QueryFAB.tsx rename to gitnexus/src/components/QueryFAB.tsx diff --git a/gitnexus/src/components/RightPanel.tsx b/gitnexus/src/components/RightPanel.tsx new file mode 100644 index 0000000000..73022cd74d --- /dev/null +++ b/gitnexus/src/components/RightPanel.tsx @@ -0,0 +1,435 @@ +import { useState, useRef, useEffect, useCallback } from 'react'; +import { + Send, Sparkles, User, + PanelRightClose, Loader2, AlertTriangle, Activity +} from 'lucide-react'; +import { useAppState } from '../hooks/useAppState'; +import { ToolCallCard } from './ToolCallCard'; +import { isProviderConfigured } from '../core/llm/settings-service'; +import { ActivityFeed } from './ActivityFeed'; +import { MarkdownRenderer } from './MarkdownRenderer'; +export const RightPanel = () => { + const { + isRightPanelOpen, + setRightPanelOpen, + fileContents, + graph, + addCodeReference, + // LLM / chat state + chatMessages, + isChatLoading, + currentToolCalls, + agentError, + isAgentReady, + isAgentInitializing, + sendChatMessage, + clearChat, + } = useAppState(); + + const [chatInput, setChatInput] = useState(''); + const [activeTab, setActiveTab] = useState<'chat' | 'activity'>('chat'); + const textareaRef = useRef(null); + const messagesEndRef = useRef(null); + + // Auto-scroll to bottom when messages update or while streaming + useEffect(() => { + if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [chatMessages, isChatLoading]); + + const resolveFilePathForUI = useCallback((requestedPath: string): string | null => { + const req = requestedPath.replace(/\\/g, '/').replace(/^\.?\//, '').toLowerCase(); + if (!req) return null; + + // Exact match first (case-insensitive) + for (const key of fileContents.keys()) { + const norm = key.replace(/\\/g, '/').replace(/^\.?\//, '').toLowerCase(); + if (norm === req) return key; + } + + // Ends-with match (best for partial paths) + let best: { path: string; score: number } | null = null; + for (const key of fileContents.keys()) { + const norm = key.replace(/\\/g, '/').replace(/^\.?\//, '').toLowerCase(); + if (norm.endsWith(req)) { + const score = 1000 - norm.length; + if (!best || score > best.score) best = { path: key, score }; + } + } + return best?.path ?? null; + }, [fileContents]); + + const findFileNodeIdForUI = useCallback((filePath: string): string | undefined => { + if (!graph) return undefined; + const target = filePath.replace(/\\/g, '/').replace(/^\.?\//, ''); + const node = graph.nodes.find( + (n) => n.label === 'File' && n.properties.filePath.replace(/\\/g, '/').replace(/^\.?\//, '') === target + ); + return node?.id; + }, [graph]); + + const handleGroundingClick = useCallback((inner: string) => { + const raw = inner.trim(); + if (!raw) return; + + let rawPath = raw; + let startLine1: number | undefined; + let endLine1: number | undefined; + + // Match line:num or line:num-num (supports both hyphen - and en dash –) + const lineMatch = raw.match(/^(.*):(\d+)(?:[-–](\d+))?$/); + if (lineMatch) { + rawPath = lineMatch[1].trim(); + startLine1 = parseInt(lineMatch[2], 10); + endLine1 = parseInt(lineMatch[3] || lineMatch[2], 10); + } + + const resolvedPath = resolveFilePathForUI(rawPath); + if (!resolvedPath) return; + + const nodeId = findFileNodeIdForUI(resolvedPath); + + addCodeReference({ + filePath: resolvedPath, + startLine: startLine1 ? Math.max(0, startLine1 - 1) : undefined, + endLine: endLine1 ? Math.max(0, endLine1 - 1) : (startLine1 ? Math.max(0, startLine1 - 1) : undefined), + nodeId, + label: 'File', + name: resolvedPath.split('/').pop() ?? resolvedPath, + source: 'ai', + }); + }, [addCodeReference, findFileNodeIdForUI, resolveFilePathForUI]); + + // Handler for node grounding: [[Class:View]], [[Function:trigger]], etc. + const handleNodeGroundingClick = useCallback((nodeTypeAndName: string) => { + const raw = nodeTypeAndName.trim(); + if (!raw || !graph) return; + + // Parse Type:Name format + const match = raw.match(/^(Class|Function|Method|Interface|File|Folder|Variable|Enum|Type|CodeElement):(.+)$/); + if (!match) return; + + const [, nodeType, nodeName] = match; + const trimmedName = nodeName.trim(); + + // Find node in graph by type + name + const node = graph.nodes.find(n => + n.label === nodeType && + n.properties.name === trimmedName + ); + + if (!node) { + console.warn(`Node not found: ${nodeType}:${trimmedName}`); + return; + } + + // 1. Highlight in graph (add to AI citation highlights) + // Note: This requires accessing the state setter from parent context + // For now, we'll add to code references which triggers the highlight + + // 2. Add to Code Panel (if node has file/line info) + if (node.properties.filePath) { + const resolvedPath = resolveFilePathForUI(node.properties.filePath); + if (resolvedPath) { + addCodeReference({ + filePath: resolvedPath, + startLine: node.properties.startLine ? node.properties.startLine - 1 : undefined, + endLine: node.properties.endLine ? node.properties.endLine - 1 : undefined, + nodeId: node.id, + label: node.label, + name: node.properties.name, + source: 'ai', + }); + } + } + }, [graph, resolveFilePathForUI, addCodeReference]); + + const handleLinkClick = useCallback((href: string) => { + if (href.startsWith('code-ref:')) { + const inner = decodeURIComponent(href.slice('code-ref:'.length)); + handleGroundingClick(inner); + } else if (href.startsWith('node-ref:')) { + const inner = decodeURIComponent(href.slice('node-ref:'.length)); + handleNodeGroundingClick(inner); + } + }, [handleGroundingClick, handleNodeGroundingClick]); + + + + // Auto-resize textarea as user types + const adjustTextareaHeight = useCallback(() => { + const textarea = textareaRef.current; + if (!textarea) return; + + // Reset height to get accurate scrollHeight + textarea.style.height = 'auto'; + // Set to scrollHeight, capped at max + const maxHeight = 160; // ~6 lines + const newHeight = Math.min(textarea.scrollHeight, maxHeight); + textarea.style.height = `${newHeight}px`; + // Show scrollbar if content exceeds max + textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden'; + }, []); + + // Adjust height when input changes + useEffect(() => { + adjustTextareaHeight(); + }, [chatInput, adjustTextareaHeight]); + + // Chat handlers + const handleSendMessage = async () => { + if (!chatInput.trim()) return; + const text = chatInput.trim(); + setChatInput(''); + // Reset textarea height after sending + if (textareaRef.current) { + textareaRef.current.style.height = '36px'; + textareaRef.current.style.overflowY = 'hidden'; + } + await sendChatMessage(text); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + const chatSuggestions = [ + 'Explain the project architecture', + 'What does this project do?', + 'Show me the most important files', + 'Find all API handlers', + ]; + + if (!isRightPanelOpen) return null; + + return ( +