From 6ddda58a17a5fbb76bcd4043fc6b307dd6e84d7d Mon Sep 17 00:00:00 2001 From: Bikash Panda Date: Sun, 13 Sep 2026 23:39:06 +0530 Subject: [PATCH 01/19] feat(ai): integrate Model Context Protocol (MCP) server, tools, and telemetry revamp --- ai/core/AIService.js | 81 +- ai/core/Agent.js | 32 +- ai/telemetry/TelemetryDB.js | 86 ++ electron/ai/aiHandlers.cjs | 287 +----- electron/lib/core/appMenu.cjs | 21 +- electron/main.cjs | 18 +- electron/mcp/McpConfig.cjs | 91 ++ electron/mcp/McpLifecycle.cjs | 176 ++++ electron/mcp/McpServer.cjs | 299 ++++++ electron/mcp/McpSessionManager.cjs | 82 ++ electron/preload.cjs | 19 +- electron/tools/ApplicationToolRegistry.cjs | 63 ++ package-lock.json | 924 +++++++++++++++++- package.json | 1 + src/App.jsx | 202 ++-- src/ai/utils/ipcProtocol.js | 19 +- src/components/AIChatPanel.jsx | 567 ----------- src/components/AIHealthPage.jsx | 21 +- src/components/AIPalette.jsx | 628 ------------ src/components/AISettings.jsx | 2 +- src/components/DashboardPanels.jsx | 5 +- src/components/DocumentDetail.jsx | 5 - src/components/MCPSettings.jsx | 513 ++++++++++ src/components/MCPStatusBar.jsx | 85 ++ src/components/MCPToolsPage.jsx | 416 ++++++++ src/components/MarkdownEditor.jsx | 235 +---- src/components/OnboardingFlow.jsx | 2 +- src/components/SettingsModal.jsx | 13 +- src/components/SlashMenuOverlay.jsx | 47 - src/components/WorkspaceIndexPage.jsx | 23 +- .../document/DocumentDetailHeader.jsx | 15 - src/components/layout/AppSubpageViews.jsx | 23 +- src/hooks/useAIAssistant.js | 608 ------------ src/services/electron/aiService.js | 104 +- src/services/electron/mcpService.js | 89 ++ src/services/electronService.js | 1 + src/styles/AIPalette.css | 744 -------------- src/utils/keyboardShortcuts.js | 10 +- tests/mcp/mcpServer.spec.js | 148 +++ 39 files changed, 3163 insertions(+), 3542 deletions(-) create mode 100644 electron/mcp/McpConfig.cjs create mode 100644 electron/mcp/McpLifecycle.cjs create mode 100644 electron/mcp/McpServer.cjs create mode 100644 electron/mcp/McpSessionManager.cjs delete mode 100644 src/components/AIChatPanel.jsx delete mode 100644 src/components/AIPalette.jsx create mode 100644 src/components/MCPSettings.jsx create mode 100644 src/components/MCPStatusBar.jsx create mode 100644 src/components/MCPToolsPage.jsx delete mode 100644 src/hooks/useAIAssistant.js create mode 100644 src/services/electron/mcpService.js delete mode 100644 src/styles/AIPalette.css create mode 100644 tests/mcp/mcpServer.spec.js diff --git a/ai/core/AIService.js b/ai/core/AIService.js index e3ec061c..7d25258e 100644 --- a/ai/core/AIService.js +++ b/ai/core/AIService.js @@ -38,17 +38,12 @@ class AIService { try { log.info('Initializing AI Service...'); - // Dynamic require of index.js bootstrap to initialize the agent const { initializeAISystem } = require('../index.js'); const result = await initializeAISystem(appDataDir, workspaceRoot, llmProvider, embeddingConfig); const { getAIAgent } = require('../index.js'); this.agent = getAIAgent(); - const AIFlow = require('./AIFlow'); - this.aiFlow = new AIFlow(this.agent); - this.agent.aiFlow = this.aiFlow; - - log.info('AI Service & AIFlow Orchestrator successfully initialized'); + log.info('AI Service successfully initialized (embeddings + graph ready)'); return result; } catch (error) { log.error('Failed to initialize AI Service:', error.message); @@ -120,14 +115,12 @@ class AIService { const { shutdownAISystem } = require('../index.js'); shutdownAISystem(); this.agent = null; - this.aiFlow = null; } shutdown() { const { shutdownAISystem } = require('../index.js'); shutdownAISystem(); this.agent = null; - this.aiFlow = null; log.info('AI Service shut down'); } @@ -227,78 +220,6 @@ class AIService { } } - async chat(message, context = {}) { - if (!this.enabled || !this.agent) { - throw new Error('AI is currently disabled or uninitialized.'); - } - if (!this.aiFlow) { - const AIFlow = require('./AIFlow'); - this.aiFlow = new AIFlow(this.agent); - this.agent.aiFlow = this.aiFlow; - } - return this.aiFlow.execute(message, context); - } - - /** - * Main chat query streaming wrapper - */ - async stream(message, context = {}, onChunk, abortSignal) { - if (!this.enabled || !this.agent) { - throw new Error('AI is currently disabled or uninitialized.'); - } - if (!this.aiFlow) { - const AIFlow = require('./AIFlow'); - this.aiFlow = new AIFlow(this.agent); - this.agent.aiFlow = this.aiFlow; - } - return this.aiFlow.stream(message, context, onChunk, abortSignal); - } - - // --- Facade API Methods for Subsystem Modules --- - - getGraphStatus() { - return this.agent?.graphDb ? this.agent.graphDb.getStatus() : null; - } - - getGraphData() { - return this.agent?.graphDb ? this.agent.graphDb.getAll() : null; - } - - clearGraphData() { - if (this.agent?.graphDb) { - this.agent.graphDb.clearAllData(); - } - } - - async buildGraph(onProgress) { - return this.agent ? this.agent.buildRelationshipGraph(onProgress) : { success: false, error: 'Agent not initialized' }; - } - - getEmbeddingStats() { - return this.agent?.embeddingDb ? this.agent.embeddingDb.getStats() : null; - } - - clearEmbeddingData() { - if (this.agent?.embeddingDb) { - this.agent.embeddingDb.clearAllData(); - } - } - - async generateEmbeddings(forceRefresh = false) { - return this.agent ? this.agent.generateEmbeddings(forceRefresh) : { success: false, error: 'Agent not initialized' }; - } - - detectPatterns() { - return this.agent ? this.agent.detectPatterns() : { success: false, error: 'Agent not initialized' }; - } - - getConversationStore() { - return this.agent?.conversationStore || null; - } - - getPersonaManager() { - return this.agent?.personaManager || null; - } } const aiServiceInstance = new AIService(); diff --git a/ai/core/Agent.js b/ai/core/Agent.js index 56018389..684033c2 100644 --- a/ai/core/Agent.js +++ b/ai/core/Agent.js @@ -117,39 +117,9 @@ class Agent { } } - /** - * Process a query via AIFlow orchestrator - */ - async query(userQuery, context = {}) { - if (!this.isInitialized) { - throw new Error('Agent not initialized'); - } - if (this.aiFlow) { - return this.aiFlow.execute(userQuery, context); - } + // query() and stream() removed — chat moved to MCP layer - const AIFlow = require('./AIFlow'); - this.aiFlow = new AIFlow(this); - return this.aiFlow.execute(userQuery, context); - } - - /** - * Process a query with streaming output via AIFlow orchestrator - */ - async stream(userQuery, context = {}, onChunk, abortSignal) { - if (!this.isInitialized) { - throw new Error('Agent not initialized'); - } - - if (this.aiFlow) { - return this.aiFlow.stream(userQuery, context, onChunk, abortSignal); - } - - const AIFlow = require('./AIFlow'); - this.aiFlow = new AIFlow(this); - return this.aiFlow.stream(userQuery, context, onChunk, abortSignal); - } /** * Generate embeddings for workspace diff --git a/ai/telemetry/TelemetryDB.js b/ai/telemetry/TelemetryDB.js index 20573b34..577ea1a8 100644 --- a/ai/telemetry/TelemetryDB.js +++ b/ai/telemetry/TelemetryDB.js @@ -96,6 +96,28 @@ class TelemetryDB { payload TEXT, created_at TEXT NOT NULL ); + + CREATE TABLE IF NOT EXISTS mcp_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT UNIQUE NOT NULL, + connected_at TEXT NOT NULL, + disconnected_at TEXT, + client_info TEXT, + tool_calls_count INTEGER DEFAULT 0, + errors_count INTEGER DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS mcp_tool_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + tool_name TEXT NOT NULL, + input_summary TEXT, + output_summary TEXT, + duration_ms INTEGER, + success INTEGER DEFAULT 1, + error TEXT, + called_at TEXT NOT NULL + ); `); // Add trace_id column if upgrading existing database @@ -116,6 +138,8 @@ class TelemetryDB { CREATE INDEX IF NOT EXISTS idx_events_type ON telemetry_events(event_type); CREATE INDEX IF NOT EXISTS idx_events_status ON telemetry_events(status); CREATE INDEX IF NOT EXISTS idx_events_severity ON telemetry_events(severity); + CREATE INDEX IF NOT EXISTS idx_mcp_tool_calls_session ON mcp_tool_calls(session_id); + CREATE INDEX IF NOT EXISTS idx_mcp_tool_calls_called_at ON mcp_tool_calls(called_at); `); this.isInitialized = true; @@ -351,6 +375,68 @@ class TelemetryDB { }; } + recordMcpSession(session) { + if (!this.db || !this.isInitialized || !session?.id) return; + try { + const stmt = this.db.prepare(` + INSERT INTO mcp_sessions (session_id, connected_at, disconnected_at, client_info, tool_calls_count, errors_count) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(session_id) DO UPDATE SET + disconnected_at = excluded.disconnected_at, + tool_calls_count = excluded.tool_calls_count, + errors_count = excluded.errors_count + `); + stmt.run( + session.id, + session.connectedAt || new Date().toISOString(), + session.disconnectedAt || null, + JSON.stringify({ remoteAddress: session.remoteAddress, userAgent: session.userAgent }), + session.toolCallsCount || 0, + session.errorsCount || 0 + ); + } catch (err) { + log.error('Failed to record MCP session in TelemetryDB:', err.message); + } + } + + recordMcpToolCall({ sessionId, toolName, inputSummary, outputSummary, durationMs, success, error }) { + if (!this.db || !this.isInitialized) return; + try { + const stmt = this.db.prepare(` + INSERT INTO mcp_tool_calls (session_id, tool_name, input_summary, output_summary, duration_ms, success, error, called_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + stmt.run( + sessionId || 'anonymous', + toolName || 'unknown', + inputSummary ? sanitizePayload(inputSummary) : null, + outputSummary ? sanitizePayload(outputSummary) : null, + durationMs || 0, + success ? 1 : 0, + error || null, + new Date().toISOString() + ); + } catch (err) { + log.error('Failed to record MCP tool call in TelemetryDB:', err.message); + } + } + + getMcpStats() { + if (!this.db || !this.isInitialized) return { totalSessions: 0, totalToolCalls: 0, totalErrors: 0 }; + try { + const sessionsRow = this.db.prepare('SELECT COUNT(*) as count FROM mcp_sessions').get(); + const callsRow = this.db.prepare('SELECT COUNT(*) as count, SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as errors FROM mcp_tool_calls').get(); + return { + totalSessions: sessionsRow?.count || 0, + totalToolCalls: callsRow?.count || 0, + totalErrors: callsRow?.errors || 0 + }; + } catch (err) { + log.error('Failed to get MCP stats:', err.message); + return { totalSessions: 0, totalToolCalls: 0, totalErrors: 0 }; + } + } + close() { if (this.db) { try { diff --git a/electron/ai/aiHandlers.cjs b/electron/ai/aiHandlers.cjs index cb8836ef..bc6cc4e4 100644 --- a/electron/ai/aiHandlers.cjs +++ b/electron/ai/aiHandlers.cjs @@ -57,11 +57,12 @@ let handlersRegistered = false; // --- Input validation & sender trust guards ------------------------------- -const MAX_QUERY_LENGTH = 8000; -const MAX_CONTEXT_BYTES = 200000; + +// Input size limits (kept for config/payload validation) const MAX_API_KEY_LENGTH = 2048; const MIN_API_KEY_LENGTH = 8; + // Derived from providerRegistry — the single source of truth for valid provider ids. const { ALLOWED_PROVIDER_IDS: ALLOWED_PROVIDERS } = require('../../ai/providers/ProviderRegistry'); @@ -112,28 +113,7 @@ function maskApiKey(apiKey) { return `${key.slice(0, 5)}...${key.slice(-5)}`; } -function sanitizeQueryPayload(payload) { - const source = payload && typeof payload === 'object' ? payload : {}; - const query = typeof source.query === 'string' ? source.query : ''; - if (!query.trim()) { - throw new Error('Query must be a non-empty string.'); - } - if (query.length > MAX_QUERY_LENGTH) { - throw new Error('Query is too long.'); - } - - let context = source.context && typeof source.context === 'object' ? source.context : {}; - try { - if (JSON.stringify(context).length > MAX_CONTEXT_BYTES) { - throw new Error('Context payload is too large.'); - } - } catch { - // Non-serializable context is dropped rather than forwarded. - context = {}; - } - return { query, context }; -} function registerHandler(channel, handler) { if (!channel || typeof channel !== 'string') { @@ -148,7 +128,7 @@ function registerHandler(channel, handler) { }); } -const activeQueryControllers = new Map(); + /** * Initialize IPC handlers @@ -203,10 +183,9 @@ function initializeAIHandlers(electronApp, agent) { // AI Initialization registerHandler(IPC_EVENTS.AI_INIT, handleInitialize); - // AI Query - registerHandler(IPC_EVENTS.AI_QUERY, handleQuery); - registerHandler(IPC_EVENTS.AI_QUERY_STREAM, handleQueryStream); - registerHandler(IPC_EVENTS.AI_QUERY_ABORT, handleQueryAbort); + + // AI Query — removed (chat moved to MCP layer) + // Status registerHandler(IPC_EVENTS.AI_STATUS, handleStatus); @@ -237,8 +216,7 @@ function initializeAIHandlers(electronApp, agent) { registerHandler(IPC_EVENTS.AI_GRAPH_MODEL_DELETE, handleDeleteGraphModel); registerHandler(IPC_EVENTS.AI_GRAPH_MODEL_STATUS, handleGetGraphModelStatus); - // Pattern detection - registerHandler(IPC_EVENTS.AI_DETECT_PATTERNS, handleDetectPatterns); + // Persistent Log Store registerHandler(IPC_EVENTS.AI_LOGS_GET, handleGetLogs); @@ -261,15 +239,7 @@ function initializeAIHandlers(electronApp, agent) { registerHandler(IPC_EVENTS.AI_DISABLE, handleDisableAI); registerHandler(IPC_EVENTS.AI_HEALTH_GET, handleGetAIHealth); - // Phase 5 — Conversations - registerHandler(IPC_EVENTS.AI_CONVERSATION_LIST, handleConversationList); - registerHandler(IPC_EVENTS.AI_CONVERSATION_GET, handleConversationGet); - registerHandler(IPC_EVENTS.AI_CONVERSATION_CREATE, handleConversationCreate); - registerHandler(IPC_EVENTS.AI_CONVERSATION_DELETE, handleConversationDelete); - registerHandler(IPC_EVENTS.AI_CONVERSATION_CLEAR, handleConversationClear); - registerHandler(IPC_EVENTS.AI_CONVERSATION_SET_PERSONA, handleConversationSetPersona); - registerHandler(IPC_EVENTS.AI_CONVERSATION_GET_MESSAGES, handleConversationGetMessages); - registerHandler(IPC_EVENTS.AI_CONVERSATION_ADD_MESSAGE, handleConversationAddMessage); + // Phase 5 — Conversations removed (chat moved to MCP layer) // Phase 5 — Personas registerHandler(IPC_EVENTS.AI_PERSONA_LIST, handlePersonaList); @@ -279,10 +249,7 @@ function initializeAIHandlers(electronApp, agent) { registerHandler(IPC_EVENTS.AI_PERSONA_IMPORT, handlePersonaImport); registerHandler(IPC_EVENTS.AI_PERSONA_EXPORT, handlePersonaExport); - // Phase 5 — Candidate Knowledge - registerHandler(IPC_EVENTS.AI_KNOWLEDGE_LIST_PENDING, handleKnowledgeListPending); - registerHandler(IPC_EVENTS.AI_KNOWLEDGE_APPROVE, handleKnowledgeApprove); - registerHandler(IPC_EVENTS.AI_KNOWLEDGE_REJECT, handleKnowledgeReject); + // Candidate Knowledge — removed (chat-only) // Shutdown registerHandler(IPC_EVENTS.AI_SHUTDOWN, handleShutdown); @@ -371,79 +338,6 @@ async function handleInitialize(event, payload) { } } -/** - * Handle AI query - */ -async function handleQuery(event, payload) { - try { - if (!aiService.isEnabled() || !aiService.agent) { - throw new Error('AI agent is disabled or not initialized'); - } - - const { query, context } = sanitizeQueryPayload(payload); - const result = await aiService.chat(query, context); - - return new AIQueryResponse(result.success, result); - } catch (error) { - console.error('[AI IPC] Query handling failed:', error); - return new AIQueryResponse(false, null, error.message); - } -} - -/** - * Handle AI query streaming - */ -async function handleQueryStream(event, payload) { - const queryId = payload?.queryId || require('crypto').randomUUID(); - try { - if (!aiService.isEnabled() || !aiService.agent) { - throw new Error('AI agent is disabled or not initialized'); - } - - const { query, context } = sanitizeQueryPayload(payload); - - const controller = new AbortController(); - activeQueryControllers.set(queryId, controller); - - const result = await aiService.stream( - query, - context, - (chunk) => { - if (!event.sender.isDestroyed()) { - event.sender.send('ai:chat:chunk', { queryId, chunk }); - } - }, - controller.signal - ); - - activeQueryControllers.delete(queryId); - return new AIQueryResponse(true, result); - } catch (error) { - activeQueryControllers.delete(queryId); - console.error('[AI IPC] Streaming query handling failed:', error); - return new AIQueryResponse(false, null, error.message); - } -} - -/** - * Handle AI query abort - */ -async function handleQueryAbort(_event, payload) { - const queryId = payload?.queryId; - if (!queryId) { - return new AIQueryResponse(false, null, 'queryId is required'); - } - - const controller = activeQueryControllers.get(queryId); - if (controller) { - controller.abort(); - activeQueryControllers.delete(queryId); - return new AIQueryResponse(true, { message: 'Query generation aborted.' }); - } - - return new AIQueryResponse(false, null, 'No active query found for this ID.'); -} - /** * Handle status request */ @@ -928,22 +822,6 @@ async function handleGetGraphStatus(_event, payload) { } } -/** - * Handle pattern detection - */ -async function handleDetectPatterns(_event, _payload) { - try { - if (!aiService.isEnabled() || !aiService.agent) { - throw new Error('AI agent is disabled or not initialized'); - } - - const result = aiService.agent.detectPatterns(); - return new AIQueryResponse(true, result); - } catch (error) { - console.error('[AI IPC] Pattern detection failed:', error); - return new AIQueryResponse(false, null, error.message); - } -} /** * Handle API key configuration @@ -1349,100 +1227,26 @@ async function handleGetAIHealth(_event, _payload) { } } -// ─── Phase 5: Context Engine Helpers ───────────────────────────────────────── +// ─── Personas (exposed via MCP; backed by PersonaDB) ────────────────────── +// ConversationStore and chat-scoped conversation handlers removed. +// Persona handlers now load PersonaDB directly. -function _getStore() { +function _getPersonaDB() { const agent = aiService.agent; - if (!agent?.conversationStore) throw new Error('ConversationStore not initialized.'); - return agent.conversationStore; -} - -// ─── Conversations ───────────────────────────────────────────────────────── - -async function handleConversationList(_event, _payload) { - try { - return new AIQueryResponse(true, _getStore().listConversations()); - } catch (err) { - return new AIQueryResponse(false, null, err.message); - } + if (agent?.personaDB) return agent.personaDB; + // Fallback: direct PersonaDB access (agent may not be running) + const { PersonaDB } = require('../../ai/memory'); + const { app } = require('electron'); + const appDataDir = require('path').join(app.getPath('appData'), 'Notely', 'notely'); + const db = new PersonaDB(appDataDir); + db.initialize(); + return db; } -async function handleConversationGet(_event, payload) { - try { - const conv = _getStore().getConversation(payload?.id); - if (!conv) return new AIQueryResponse(false, null, 'Conversation not found.'); - return new AIQueryResponse(true, conv); - } catch (err) { - return new AIQueryResponse(false, null, err.message); - } -} - -async function handleConversationCreate(_event, payload) { - try { - const conv = _getStore().createConversation(payload?.title, payload?.persona); - return new AIQueryResponse(true, conv); - } catch (err) { - return new AIQueryResponse(false, null, err.message); - } -} - -async function handleConversationDelete(_event, payload) { - try { - const convId = payload?.id; - _getStore().deleteConversation(convId); - if (convId) { - const telDb = getTelemetryDbInstance(); - if (telDb) telDb.clearTelemetry(convId); - } - return new AIQueryResponse(true, { deleted: convId }); - } catch (err) { - return new AIQueryResponse(false, null, err.message); - } -} - -async function handleConversationClear(_event, payload) { - try { - const beforeTimestamp = payload?.beforeTimestamp || null; - _getStore().clearAll(beforeTimestamp); - const telDb = getTelemetryDbInstance(); - if (telDb) telDb.clearTelemetry(null, beforeTimestamp); - return new AIQueryResponse(true, { cleared: true }); - } catch (err) { - return new AIQueryResponse(false, null, err.message); - } -} - -async function handleConversationSetPersona(_event, payload) { - try { - _getStore().setPersona(payload?.conversationId, payload?.personaId); - return new AIQueryResponse(true, { ok: true }); - } catch (err) { - return new AIQueryResponse(false, null, err.message); - } -} - -async function handleConversationGetMessages(_event, payload) { - try { - return new AIQueryResponse(true, _getStore().getMessages(payload?.conversationId)); - } catch (err) { - return new AIQueryResponse(false, null, err.message); - } -} - -async function handleConversationAddMessage(_event, payload) { - try { - const msg = _getStore().addMessage(payload?.conversationId, payload?.role, payload?.content, payload?.metadata || null); - return new AIQueryResponse(true, msg); - } catch (err) { - return new AIQueryResponse(false, null, err.message); - } -} - -// ─── Personas ───────────────────────────────────────────────────────────── async function handlePersonaList(_event, _payload) { try { - return new AIQueryResponse(true, _getStore().listPersonas()); + return new AIQueryResponse(true, _getPersonaDB().list()); } catch (err) { return new AIQueryResponse(false, null, err.message); } @@ -1450,7 +1254,7 @@ async function handlePersonaList(_event, _payload) { async function handlePersonaGet(_event, payload) { try { - const p = _getStore().getPersona(payload?.id); + const p = _getPersonaDB().get(payload?.id); if (!p) return new AIQueryResponse(false, null, 'Persona not found.'); return new AIQueryResponse(true, p); } catch (err) { @@ -1460,7 +1264,7 @@ async function handlePersonaGet(_event, payload) { async function handlePersonaSave(_event, payload) { try { - _getStore().savePersona(payload); + _getPersonaDB().save(payload); return new AIQueryResponse(true, { ok: true }); } catch (err) { return new AIQueryResponse(false, null, err.message); @@ -1469,7 +1273,7 @@ async function handlePersonaSave(_event, payload) { async function handlePersonaDelete(_event, payload) { try { - _getStore().deletePersona(payload?.id); + _getPersonaDB().delete(payload?.id); return new AIQueryResponse(true, { deleted: payload?.id }); } catch (err) { return new AIQueryResponse(false, null, err.message); @@ -1478,7 +1282,7 @@ async function handlePersonaDelete(_event, payload) { async function handlePersonaImport(_event, payload) { try { - const result = _getStore().importPersonaFromFile(payload?.filePath); + const result = _getPersonaDB().importFromFile(payload?.filePath); return new AIQueryResponse(true, result); } catch (err) { return new AIQueryResponse(false, null, err.message); @@ -1487,7 +1291,7 @@ async function handlePersonaImport(_event, payload) { async function handlePersonaExport(_event, payload) { try { - const dest = _getStore().exportPersonaToFile(payload?.id, payload?.destPath); + const dest = _getPersonaDB().exportToFile(payload?.id, payload?.destPath); try { const { getExportManager } = require("../lib/export/ExportManager.cjs"); const exportManager = getExportManager(); @@ -1504,33 +1308,8 @@ async function handlePersonaExport(_event, payload) { } } -// ─── Candidate Knowledge ────────────────────────────────────────────────── +// ─── Candidate Knowledge removed (chat-only) ───────────────────────────── -async function handleKnowledgeListPending(_event, _payload) { - try { - return new AIQueryResponse(true, _getStore().listPendingKnowledge()); - } catch (err) { - return new AIQueryResponse(false, null, err.message); - } -} - -async function handleKnowledgeApprove(_event, payload) { - try { - _getStore().approveKnowledge(payload?.id); - return new AIQueryResponse(true, { ok: true }); - } catch (err) { - return new AIQueryResponse(false, null, err.message); - } -} - -async function handleKnowledgeReject(_event, payload) { - try { - _getStore().rejectKnowledge(payload?.id); - return new AIQueryResponse(true, { ok: true }); - } catch (err) { - return new AIQueryResponse(false, null, err.message); - } -} let logDbInstance = null; function getLogDbInstance() { @@ -1621,14 +1400,6 @@ async function handleClearLogs(_event, payload) { } } - if (!subsystem) { - try { - _getStore().clearAll(beforeTimestamp); - } catch (err) { - console.warn('[AI IPC] Note: Failed clearing conversation store during clearLogs:', err.message); - } - } - const logDb = getLogDbInstance(); if (logDb) { logDb.clearLogs(subsystem, beforeTimestamp); diff --git a/electron/lib/core/appMenu.cjs b/electron/lib/core/appMenu.cjs index 23739b0a..6e771260 100644 --- a/electron/lib/core/appMenu.cjs +++ b/electron/lib/core/appMenu.cjs @@ -879,16 +879,7 @@ function buildAppMenuTemplate(win, context = {}, deps = {}) { { label: "AI", submenu: [ - ...(screen === "document" - ? [ - { - label: "Open AI Palette", - accelerator: "CmdOrCtrl+Shift+I", - click: () => sendMenuAction(win, "open-ai-palette") - }, - { type: "separator" } - ] - : []), + { label: "AI Settings", accelerator: "CmdOrCtrl+Shift+,", @@ -910,8 +901,12 @@ function buildAppMenuTemplate(win, context = {}, deps = {}) { }, { type: "separator" }, { - label: "Diagnostics", + label: "AI Health & Diagnostics", click: () => sendMenuAction(win, "open-health-page") + }, + { + label: "MCP Tools & Capabilities", + click: () => sendMenuAction(win, "open-mcp-tools") } ] }, @@ -942,6 +937,10 @@ function buildAppMenuTemplate(win, context = {}, deps = {}) { label: "System & Application Logs", click: () => sendMenuAction(win, "open-app-logs") }, + { + label: "MCP Tools & Capabilities", + click: () => sendMenuAction(win, "open-mcp-tools") + }, { type: "separator" }, { label: "Check for Updates", diff --git a/electron/main.cjs b/electron/main.cjs index ef200bf7..bbc4f975 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -41,6 +41,7 @@ const { createMainHelpers } = require("./lib/core/mainHelpers.cjs"); const { registerWorkspaceExportIpcHandlers } = require("./lib/export/workspaceExportIpc.cjs"); const { setupDiagramHandlers } = require("./diagram-handlers.cjs"); const { initializeAIHandlers } = require("./ai/aiHandlers.cjs"); +const { mcpLifecycle } = require("./mcp/McpLifecycle.cjs"); const { registerGitIpcHandlers } = require("./lib/git/gitIpc.cjs"); const gitService = require("./lib/git/gitService.cjs"); const { registerNotePackageIpc } = require("./lib/export/notePackageIpc.cjs"); @@ -889,8 +890,21 @@ if (canRunApp) { // Register AI IPC handlers in the ready phase so renderer calls never race missing handlers. initializeAIHandlers(app, aiAgent); + + // Register and initialize MCP server subsystem + mcpLifecycle.registerIpcHandlers(ipcMain); + const mcpAppDataDir = path.join(app.getPath("appData"), "Notely", "notely"); + mcpLifecycle.initialize(mcpAppDataDir); + + app.on("browser-window-created", (_event, win) => { + mcpLifecycle.trackWindow(win); + }); + windowLifecycle.applyContentSecurityPolicy(); - windowLifecycle.focusOrCreateWindow(); + const mainWin = windowLifecycle.focusOrCreateWindow(); + if (mainWin) { + mcpLifecycle.trackWindow(mainWin); + } broadcastThemeChange(); // Defer workspace init so splash window has time to paint before sync FS/git work starts. @@ -910,6 +924,8 @@ app.on("window-all-closed", () => { }); app.on("before-quit", () => { + mcpLifecycle.shutdown(); + shutdownAISystemRef(); webPreview.dispose(); diff --git a/electron/mcp/McpConfig.cjs b/electron/mcp/McpConfig.cjs new file mode 100644 index 00000000..577f6d79 --- /dev/null +++ b/electron/mcp/McpConfig.cjs @@ -0,0 +1,91 @@ +/** + * McpConfig.cjs + * Manages configuration for the Notely MCP (Model Context Protocol) Server. + */ + +const fs = require('fs'); +const path = require('path'); + +const DEFAULT_CONFIG = { + enabled: true, + port: 3700, + host: '127.0.0.1', + bearerToken: '' +}; + +class McpConfig { + constructor(appDataDir) { + this.appDataDir = appDataDir; + this.configPath = path.join(appDataDir, 'mcp-config.json'); + this.config = { ...DEFAULT_CONFIG }; + this.load(); + } + + load() { + try { + if (fs.existsSync(this.configPath)) { + const raw = fs.readFileSync(this.configPath, 'utf8'); + const parsed = JSON.parse(raw); + this.config = { + ...DEFAULT_CONFIG, + ...parsed, + port: Number(parsed.port) || DEFAULT_CONFIG.port + }; + } + } catch (err) { + console.warn('[MCP Config] Failed to load config file, using defaults:', err.message); + this.config = { ...DEFAULT_CONFIG }; + } + return this.getConfig(); + } + + save(updates = {}) { + try { + const next = { + ...this.config, + ...updates + }; + if (updates.port !== undefined) { + const p = Number(updates.port); + if (Number.isInteger(p) && p > 0 && p <= 65535) { + next.port = p; + } + } + if (updates.enabled !== undefined) { + next.enabled = Boolean(updates.enabled); + } + if (updates.host !== undefined) { + next.host = String(updates.host || '127.0.0.1').trim(); + } + if (updates.bearerToken !== undefined) { + next.bearerToken = String(updates.bearerToken || '').trim(); + } + + this.config = next; + const dir = path.dirname(this.configPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf8'); + } catch (err) { + console.error('[MCP Config] Failed to save config file:', err.message); + throw err; + } + return this.getConfig(); + } + + getConfig() { + return { + enabled: Boolean(this.config.enabled), + port: Number(this.config.port) || DEFAULT_CONFIG.port, + host: this.config.host || DEFAULT_CONFIG.host, + bearerToken: this.config.bearerToken || '', + isTokenProtected: Boolean(this.config.bearerToken && this.config.bearerToken.trim().length > 0) + }; + } +} + +module.exports = { + McpConfig, + DEFAULT_CONFIG +}; diff --git a/electron/mcp/McpLifecycle.cjs b/electron/mcp/McpLifecycle.cjs new file mode 100644 index 00000000..a24c8aeb --- /dev/null +++ b/electron/mcp/McpLifecycle.cjs @@ -0,0 +1,176 @@ +/** + * McpLifecycle.cjs + * Lifecycle management and IPC controller for Notely MCP Server. + */ + +const { McpConfig } = require('./McpConfig.cjs'); +const { McpSessionManager } = require('./McpSessionManager.cjs'); +const { McpServer } = require('./McpServer.cjs'); + +class McpLifecycle { + constructor() { + this.config = null; + this.sessionManager = new McpSessionManager(); + this.server = null; + this.initialized = false; + this.browserWindows = new Set(); + } + + initialize(appDataDir) { + if (this.initialized) return; + this.config = new McpConfig(appDataDir); + const cfg = this.config.getConfig(); + + this.server = new McpServer({ + port: cfg.port, + host: cfg.host, + bearerToken: cfg.bearerToken, + sessionManager: this.sessionManager + }); + + this.initialized = true; + + if (cfg.enabled) { + this.start().catch((err) => { + console.warn('[MCP Lifecycle] Initial start encountered error:', err.message); + }); + } + } + + trackWindow(win) { + if (!win || win.isDestroyed()) return; + this.browserWindows.add(win); + win.on('closed', () => this.browserWindows.delete(win)); + } + + broadcastStatus() { + const status = this.getStatus(); + for (const win of this.browserWindows) { + if (!win.isDestroyed()) { + win.webContents.send('mcp:status-changed', status); + } + } + } + + async start() { + if (!this.server) throw new Error('MCP server not initialized.'); + try { + await this.server.start(); + this.broadcastStatus(); + return this.getStatus(); + } catch (err) { + this.broadcastStatus(); + return this.getStatus(); + } + } + + async stop() { + if (!this.server) return this.getStatus(); + await this.server.stop(); + this.broadcastStatus(); + return this.getStatus(); + } + + async restart() { + await this.stop(); + return this.start(); + } + + async updateConfig(updates = {}) { + if (!this.config) throw new Error('MCP config not initialized.'); + const oldConfig = this.config.getConfig(); + const newConfig = this.config.save(updates); + + if (this.server) { + this.server.updateConfig({ + port: newConfig.port, + host: newConfig.host, + bearerToken: newConfig.bearerToken + }); + } + + if (!newConfig.enabled) { + if (this.server?.isRunning) { + await this.stop(); + } + } else { + // If port changed or was stopped, restart + await this.restart(); + } + + this.broadcastStatus(); + return { + config: newConfig, + status: this.getStatus() + }; + } + + getStatus() { + const cfg = this.config ? this.config.getConfig() : { enabled: false, port: 3700, host: '127.0.0.1', isTokenProtected: false }; + const isRunning = Boolean(this.server?.isRunning); + const lastError = this.server?.lastError || null; + const errorCode = this.server?.errorCode || null; + const stats = this.sessionManager.getStats(); + + return { + enabled: cfg.enabled, + running: isRunning, + port: cfg.port, + host: cfg.host, + isTokenProtected: cfg.isTokenProtected, + error: lastError, + errorCode, + activeSessions: stats.activeCount, + totalToolCalls: stats.totalToolCalls, + totalErrors: stats.totalErrors + }; + } + + registerIpcHandlers(ipcMain) { + ipcMain.handle('mcp:get-status', async () => { + return this.getStatus(); + }); + + ipcMain.handle('mcp:get-config', async () => { + return this.config ? this.config.getConfig() : null; + }); + + ipcMain.handle('mcp:set-config', async (_event, updates) => { + return this.updateConfig(updates); + }); + + ipcMain.handle('mcp:start', async () => { + return this.start(); + }); + + ipcMain.handle('mcp:stop', async () => { + return this.stop(); + }); + + ipcMain.handle('mcp:restart', async () => { + return this.restart(); + }); + + ipcMain.handle('mcp:get-sessions', async () => { + return { + active: this.sessionManager.getActiveSessions(), + stats: this.sessionManager.getStats() + }; + }); + } + + async shutdown() { + if (this.server) { + await this.server.stop(); + } + this.sessionManager.clear(); + } +} + +// Global singleton +const mcpLifecycle = new McpLifecycle(); + +module.exports = { + McpLifecycle, + mcpLifecycle +}; diff --git a/electron/mcp/McpServer.cjs b/electron/mcp/McpServer.cjs new file mode 100644 index 00000000..16df5390 --- /dev/null +++ b/electron/mcp/McpServer.cjs @@ -0,0 +1,299 @@ +/** + * McpServer.cjs + * HTTP and SSE Model Context Protocol (MCP) Server for Notely. + * Exposes Notely capabilities to external AI clients (Claude Desktop, IDE agents, custom scripts). + */ + +const http = require('http'); +const { URL } = require('url'); +const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); +const { SSEServerTransport } = require('@modelcontextprotocol/sdk/server/sse.js'); +const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js'); +const { applicationToolRegistry } = require('../tools/ApplicationToolRegistry.cjs'); + +class McpServer { + /** + * @param {object} options + * @param {number} options.port + * @param {string} options.host + * @param {string} options.bearerToken + * @param {import('./McpSessionManager.cjs').McpSessionManager} options.sessionManager + */ + constructor(options = {}) { + this.port = Number(options.port) || 3700; + this.host = options.host || '127.0.0.1'; + this.bearerToken = options.bearerToken || ''; + this.sessionManager = options.sessionManager; + + this.httpServer = null; + this.transports = new Map(); // sessionId -> { transport, server } + this.isRunning = false; + this.lastError = null; + this.errorCode = null; + } + + updateConfig({ port, host, bearerToken }) { + if (port !== undefined) this.port = Number(port); + if (host !== undefined) this.host = host; + if (bearerToken !== undefined) this.bearerToken = bearerToken; + } + + _checkAuth(req) { + if (!this.bearerToken || !this.bearerToken.trim()) { + return true; + } + const authHeader = req.headers['authorization'] || ''; + return authHeader.trim() === `Bearer ${this.bearerToken.trim()}`; + } + + _createServerInstance(sessionId) { + const server = new Server( + { name: 'notely', version: '0.1.41' }, + { capabilities: { tools: {} } } + ); + + server.setRequestHandler(ListToolsRequestSchema, async () => { + const tools = applicationToolRegistry.toMcpSchemas(); + return { tools }; + }); + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + const start = Date.now(); + try { + const result = await applicationToolRegistry.executeTool(name, args || {}, { + caller: 'mcp_client', + sessionId + }); + const duration = Date.now() - start; + if (this.sessionManager) { + this.sessionManager.recordToolCall(sessionId, name, duration, result.success, result.error?.message); + } + + let textContent = ''; + if (result.data !== null && result.data !== undefined) { + if (typeof result.data === 'string') { + textContent = result.data; + } else if (result.data.content && typeof result.data.content === 'string') { + textContent = result.data.content; + } else { + textContent = JSON.stringify(result.data, null, 2); + } + } else { + textContent = result.error?.message || 'Tool executed with no output.'; + } + + return { + content: [{ type: 'text', text: textContent }], + isError: !result.success + }; + } catch (err) { + const duration = Date.now() - start; + if (this.sessionManager) { + this.sessionManager.recordToolCall(sessionId, name, duration, false, err.message); + } + return { + content: [{ type: 'text', text: err.message || 'Execution error' }], + isError: true + }; + } + }); + + return server; + } + + start() { + return new Promise((resolve, reject) => { + if (this.isRunning) { + return resolve({ port: this.port, host: this.host }); + } + + this.lastError = null; + this.errorCode = null; + + const server = http.createServer(async (req, res) => { + // Handle CORS + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + return; + } + + let parsedUrl; + try { + parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`); + } catch { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Malformed URL' })); + return; + } + + const pathname = parsedUrl.pathname; + + // Health / Status ping + if (pathname === '/health' || pathname === '/status' || pathname === '/') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + status: 'ok', + server: 'notely-mcp', + version: '0.1.41', + port: this.port, + toolsCount: applicationToolRegistry.toMcpSchemas().length, + activeSessions: this.sessionManager ? this.sessionManager.getActiveSessions().length : 0 + })); + return; + } + + // Tools discovery list endpoint (HTTP convenience for testing) + if (pathname === '/tools' && req.method === 'GET') { + if (!this._checkAuth(req)) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Unauthorized: invalid or missing Bearer token' })); + return; + } + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ tools: applicationToolRegistry.toMcpSchemas() }, null, 2)); + return; + } + + // SSE endpoint: establish connection + if (pathname === '/sse' && req.method === 'GET') { + if (!this._checkAuth(req)) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Unauthorized: invalid or missing Bearer token' })); + return; + } + + try { + const transport = new SSEServerTransport('/messages', res); + const sessionId = transport.sessionId; + + if (this.sessionManager) { + this.sessionManager.createSession(sessionId, req); + } + + const mcpInstance = this._createServerInstance(sessionId); + this.transports.set(sessionId, { transport, server: mcpInstance }); + + req.on('close', async () => { + this.transports.delete(sessionId); + if (this.sessionManager) { + this.sessionManager.closeSession(sessionId); + } + try { + await mcpInstance.close(); + } catch {} + }); + + await mcpInstance.connect(transport); + } catch (err) { + console.error('[MCP Server] Error establishing SSE transport:', err); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: err.message })); + } + } + return; + } + + // POST /messages: incoming JSON-RPC from client + if (pathname === '/messages' && req.method === 'POST') { + if (!this._checkAuth(req)) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Unauthorized: invalid or missing Bearer token' })); + return; + } + + const sessionId = parsedUrl.searchParams.get('sessionId') || parsedUrl.searchParams.get('session_id'); + if (!sessionId) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Missing sessionId query parameter' })); + return; + } + + const session = this.transports.get(sessionId); + if (!session) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: `Session not found or expired: ${sessionId}` })); + return; + } + + try { + await session.transport.handlePostMessage(req, res); + } catch (err) { + console.error(`[MCP Server] Error handling POST message for session ${sessionId}:`, err); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: err.message })); + } + } + return; + } + + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: `Route not found: ${pathname}` })); + }); + + server.on('error', (err) => { + this.isRunning = false; + this.httpServer = null; + this.lastError = err.message; + if (err.code === 'EADDRINUSE') { + this.errorCode = 'EADDRINUSE'; + console.warn(`[MCP Server] Port ${this.port} already in use. MCP server disabled on this port.`); + } else { + this.errorCode = err.code || 'SERVER_ERROR'; + console.error('[MCP Server] Server error:', err); + } + reject(err); + }); + + server.listen(this.port, this.host, () => { + this.isRunning = true; + this.httpServer = server; + this.lastError = null; + this.errorCode = null; + console.log(`[MCP Server] Listening on http://${this.host}:${this.port} (SSE at /sse)`); + resolve({ port: this.port, host: this.host }); + }); + }); + } + + async stop() { + if (!this.httpServer) { + this.isRunning = false; + return; + } + + // Close all open client transports + for (const [sessionId, { transport, server }] of this.transports.entries()) { + try { + await transport.close(); + } catch {} + try { + await server.close(); + } catch {} + if (this.sessionManager) { + this.sessionManager.closeSession(sessionId); + } + } + this.transports.clear(); + + return new Promise((resolve) => { + this.httpServer.close(() => { + this.isRunning = false; + this.httpServer = null; + console.log('[MCP Server] Stopped.'); + resolve(); + }); + }); + } +} + +module.exports = { + McpServer +}; diff --git a/electron/mcp/McpSessionManager.cjs b/electron/mcp/McpSessionManager.cjs new file mode 100644 index 00000000..71f1914a --- /dev/null +++ b/electron/mcp/McpSessionManager.cjs @@ -0,0 +1,82 @@ +/** + * McpSessionManager.cjs + * Tracks client sessions connected to Notely MCP Server. + */ + +class McpSessionManager { + constructor() { + this.sessions = new Map(); + this.totalConnections = 0; + this.totalToolCalls = 0; + this.totalErrors = 0; + } + + createSession(sessionId, req = null) { + const session = { + id: sessionId, + connectedAt: new Date().toISOString(), + remoteAddress: req?.socket?.remoteAddress || '127.0.0.1', + userAgent: req?.headers?.['user-agent'] || 'unknown', + toolCallsCount: 0, + errorsCount: 0, + lastActivityAt: new Date().toISOString() + }; + this.sessions.set(sessionId, session); + this.totalConnections++; + return session; + } + + closeSession(sessionId) { + const session = this.sessions.get(sessionId); + if (session) { + session.disconnectedAt = new Date().toISOString(); + this.sessions.delete(sessionId); + } + return session; + } + + recordToolCall(sessionId, toolName, durationMs, success, error = null) { + this.totalToolCalls++; + if (!success) { + this.totalErrors++; + } + + const session = this.sessions.get(sessionId); + if (session) { + session.toolCallsCount++; + session.lastActivityAt = new Date().toISOString(); + if (!success) { + session.errorsCount++; + } + } + + return { + sessionId, + toolName, + durationMs, + success, + error + }; + } + + getActiveSessions() { + return Array.from(this.sessions.values()); + } + + getStats() { + return { + activeCount: this.sessions.size, + totalConnections: this.totalConnections, + totalToolCalls: this.totalToolCalls, + totalErrors: this.totalErrors + }; + } + + clear() { + this.sessions.clear(); + } +} + +module.exports = { + McpSessionManager +}; diff --git a/electron/preload.cjs b/electron/preload.cjs index e9ccf0ce..50ef25f6 100644 --- a/electron/preload.cjs +++ b/electron/preload.cjs @@ -63,14 +63,19 @@ contextBridge.exposeInMainWorld("notesApi", { ipcRenderer.on("window:menu-updated", listener); return () => ipcRenderer.removeListener("window:menu-updated", listener); }, - aiQuery: (payload) => ipcRenderer.invoke("ai:query", payload), - aiQueryStream: (payload) => ipcRenderer.invoke("ai:query:stream", payload), - aiQueryAbort: (payload) => ipcRenderer.invoke("ai:query:abort", payload), - onChatStreamChunk: (callback) => { - if (typeof callback !== 'function') return () => {}; + // MCP (Model Context Protocol) Server + mcpGetStatus: () => ipcRenderer.invoke("mcp:get-status"), + mcpGetConfig: () => ipcRenderer.invoke("mcp:get-config"), + mcpSetConfig: (updates) => ipcRenderer.invoke("mcp:set-config", updates), + mcpStart: () => ipcRenderer.invoke("mcp:start"), + mcpStop: () => ipcRenderer.invoke("mcp:stop"), + mcpRestart: () => ipcRenderer.invoke("mcp:restart"), + mcpGetSessions: () => ipcRenderer.invoke("mcp:get-sessions"), + onMcpStatusChanged: (callback) => { + if (typeof callback !== "function") return () => {}; const listener = (_event, payload) => callback(payload); - ipcRenderer.on('ai:chat:chunk', listener); - return () => ipcRenderer.removeListener('ai:chat:chunk', listener); + ipcRenderer.on("mcp:status-changed", listener); + return () => ipcRenderer.removeListener("mcp:status-changed", listener); }, aiGetApiKey: (payload) => ipcRenderer.invoke("ai:config:get-api-key", payload), aiSetApiKey: (payload) => ipcRenderer.invoke("ai:config:set-api-key", payload), diff --git a/electron/tools/ApplicationToolRegistry.cjs b/electron/tools/ApplicationToolRegistry.cjs index 13ab408e..b3135afb 100644 --- a/electron/tools/ApplicationToolRegistry.cjs +++ b/electron/tools/ApplicationToolRegistry.cjs @@ -571,6 +571,69 @@ class ApplicationToolRegistry { }, execute: async (args) => this.webService.fetchUrl(args) }); + + // 15. personas.list + this.registerTool({ + name: 'personas.list', + version: 'v1', + aliases: ['list_personas'], + sdkName: 'list_personas', + capability: 'personas:list', + serviceName: 'PersonaService', + description: 'List all available custom and system personas.', + schema: z.object({}), + jsonSchema: { + type: 'object', + properties: {} + }, + execute: async () => { + try { + const { PersonaDB } = require('../../ai/memory'); + const { app } = require('electron'); + const appDataDir = app ? require('path').join(app.getPath('appData'), 'Notely', 'notely') : null; + if (!appDataDir) return []; + const db = new PersonaDB(appDataDir); + db.initialize(); + return db.list(); + } catch { + return []; + } + } + }); + + // 16. personas.get + this.registerTool({ + name: 'personas.get', + version: 'v1', + aliases: ['get_persona'], + sdkName: 'get_persona', + capability: 'personas:get', + serviceName: 'PersonaService', + description: 'Get details of a specific persona by ID.', + schema: z.object({ + id: z.string().describe('ID of the persona to fetch.') + }), + jsonSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'ID of the persona to fetch.' } + }, + required: ['id'] + }, + execute: async (args) => { + try { + const { PersonaDB } = require('../../ai/memory'); + const { app } = require('electron'); + const appDataDir = app ? require('path').join(app.getPath('appData'), 'Notely', 'notely') : null; + if (!appDataDir) return null; + const db = new PersonaDB(appDataDir); + db.initialize(); + return db.get(args.id); + } catch { + return null; + } + } + }); } } diff --git a/package-lock.json b/package-lock.json index c7714ca2..f93e2ac6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "notely", - "version": "0.1.37", + "version": "0.1.41", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "notely", - "version": "0.1.37", + "version": "0.1.41", "hasInstallScript": true, "license": "CC-BY-NC-4.0", "dependencies": { @@ -14,6 +14,7 @@ "@ai-sdk/groq": "^4.0.12", "@ai-sdk/openai": "^4.0.16", "@huggingface/transformers": "^4.2.0", + "@modelcontextprotocol/sdk": "^1.30.0", "ai": "^7.0.31", "groq-sdk": "^1.3.0", "highlight.js": "^11.11.1", @@ -3000,6 +3001,18 @@ "@hapi/hoek": "^11.0.2" } }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@huggingface/jinja": { "version": "0.5.9", "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz", @@ -4113,6 +4126,68 @@ "@chevrotain/types": "~11.1.1" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "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.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "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.1" + }, + "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/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "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/@modelcontextprotocol/sdk/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/@napi-rs/canvas": { "version": "0.1.100", "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", @@ -6663,6 +6738,44 @@ "dev": true, "license": "MIT" }, + "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/accepts/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/accepts/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/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -6742,6 +6855,45 @@ "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/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "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/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/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -7296,6 +7448,59 @@ "bluebird": "^3.5.5" } }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "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/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -7500,6 +7705,15 @@ "node": ">= 10.0.0" } }, + "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/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -7562,7 +7776,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7576,7 +7789,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -8102,6 +8314,28 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "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/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -8109,6 +8343,24 @@ "dev": true, "license": "MIT" }, + "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/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -8133,6 +8385,23 @@ "license": "MIT", "optional": true }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cose-base": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", @@ -8184,7 +8453,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -9034,6 +9302,15 @@ "node": ">=0.4.0" } }, + "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/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -9264,7 +9541,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -9282,6 +9558,12 @@ "dev": true, "license": "MIT" }, + "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/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -9457,6 +9739,15 @@ "dev": true, "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/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -9641,7 +9932,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -9779,6 +10069,12 @@ "node": ">=6" } }, + "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/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -10072,6 +10368,27 @@ "node": ">=0.10.0" } }, + "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.1.0", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", @@ -10091,6 +10408,93 @@ "node": ">=12.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": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/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/express/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/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -10145,7 +10549,6 @@ "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==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -10179,6 +10582,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -10253,6 +10672,27 @@ "node": ">=8" } }, + "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/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -10378,6 +10818,15 @@ "node": ">= 6" } }, + "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/fractional-indexing": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fractional-indexing/-/fractional-indexing-3.2.0.tgz", @@ -10388,6 +10837,15 @@ "node": "^14.13.1 || >=16.0.0" } }, + "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/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -10462,7 +10920,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10545,7 +11002,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -10580,7 +11036,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -10911,7 +11366,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10940,7 +11394,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -10996,6 +11449,15 @@ "node": ">=12.0.0" } }, + "node_modules/hono": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", + "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", @@ -11067,6 +11529,26 @@ "dev": true, "license": "BSD-2-Clause" }, + "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/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -11244,7 +11726,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -11282,6 +11763,24 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ip-address": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", + "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -11689,6 +12188,12 @@ "dev": true, "license": "MIT" }, + "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-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -11877,7 +12382,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -11951,6 +12455,15 @@ "node": ">= 20" } }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jotai": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.11.0.tgz", @@ -12088,6 +12601,12 @@ "dev": true, "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/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -12545,7 +13064,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12855,6 +13373,19 @@ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "license": "MIT" }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -12862,6 +13393,18 @@ "dev": true, "license": "MIT" }, + "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/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -13766,6 +14309,35 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/node-addon-api": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", @@ -13864,7 +14436,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13874,7 +14445,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13967,11 +14537,22 @@ "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==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -14267,6 +14848,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "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-data-parser": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", @@ -14298,7 +14888,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14335,6 +14924,16 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", @@ -14432,6 +15031,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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/platform": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", @@ -14728,6 +15336,19 @@ "node": ">=12.0.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/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -14775,6 +15396,22 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -14809,6 +15446,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/raw-body/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "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/rcedit": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-5.0.2.tgz", @@ -15511,7 +16192,6 @@ "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==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15681,6 +16361,22 @@ "points-on-path": "^0.2.1" } }, + "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/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -15795,7 +16491,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/sanitize-filename": { @@ -15872,6 +16567,57 @@ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "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/send/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/send/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/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -15887,6 +16633,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -15936,6 +16701,12 @@ "node": ">= 0.4" } }, + "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/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -15996,7 +16767,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -16009,7 +16779,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16049,7 +16818,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -16069,7 +16837,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -16086,7 +16853,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -16105,7 +16871,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -16308,6 +17073,15 @@ "node": ">= 6" } }, + "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/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -16789,6 +17563,15 @@ "node": ">=8.0" } }, + "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/tough-cookie": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", @@ -16909,6 +17692,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/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/type-is/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/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -17212,6 +18051,15 @@ "node": ">= 4.0.0" } }, + "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/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -17328,6 +18176,15 @@ "uuid": "dist-node/bin/uuid" } }, + "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/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -19286,7 +20143,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -19462,7 +20318,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/xml-name-validator": { @@ -19590,6 +20445,15 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, "node_modules/zustand": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", diff --git a/package.json b/package.json index 884ad346..0a11bece 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@ai-sdk/groq": "^4.0.12", "@ai-sdk/openai": "^4.0.16", "@huggingface/transformers": "^4.2.0", + "@modelcontextprotocol/sdk": "^1.30.0", "ai": "^7.0.31", "groq-sdk": "^1.3.0", "highlight.js": "^11.11.1", diff --git a/src/App.jsx b/src/App.jsx index b8971dac..bbab8c08 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -32,11 +32,10 @@ const GlobalSearchOverlay = lazy(() => const KeyboardShortcutsModal = lazy(() => import("./components/KeyboardShortcutsModal").then((m) => ({ default: m.KeyboardShortcutsModal })) ); -const AIChatPanel = lazy(() => - import("./components/AIChatPanel").then((m) => ({ default: m.default || m.AIChatPanel })) -); + import { GitStatusBar } from "./components/GitStatusBar"; import { AIStatusBar } from "./components/AIStatusBar"; +import { MCPStatusBar } from "./components/MCPStatusBar"; const NoteListPanel = lazy(() => import("./components/NoteListPanel").then((m) => ({ default: m.NoteListPanel })) @@ -74,7 +73,11 @@ import { } from "./services/electronService"; import { useToast } from "./hooks/useToast"; import { useP2PSync } from "./hooks/useP2PSync"; -import { useAIAssistant } from "./hooks/useAIAssistant"; +import { + aiGenerateEmbeddings, + aiBuildGraph, + aiClearData, +} from "./services/electron/aiService"; import { useDocumentManager } from "./hooks/useDocumentManager"; import { useWorkspaceScopedStorage } from "./hooks/useWorkspaceScopedStorage"; import { useUIState } from "./contexts/UIStateContext"; @@ -933,48 +936,51 @@ export default function App() { handleResolveConflict, handleOpenNextConflict, } = useP2PSync({ notify, setError, loadDocumentsData, syncStateRef }); - const { - aiSettingsOpen, - setAiSettingsOpen, - aiQueryLoading, - aiQueryError, - aiContextSummary, - aiPaletteIntent, - aiChatMessages, - isAIConfigured, - aiPanelVisible, - setAiPanelVisible, - inlineGhostSuggestion, - aiEditorRef, - refreshAIConfiguration, - handleAIEmbeddings, - handleAIGraph, - handleAIClearCache, - handleOpenAIPalette, - handleInlineAIRequest, - handleApplyAIResult, - handleAIChatSend, - handleAIChatAbort, - handleClearAIChat, - handleRejectInlineGhost, - handleAcceptInlineGhost, - activeProvider, - activePersona, - setActivePersona, - activeQueryId, - conversations, - loadConversations, - loadConversation, - deleteConversation, - } = useAIAssistant({ - current, - activeTab, - mode, - activeProject, - landingFolderPath, - notesFolderPath, - notify, - }); + const [aiSettingsOpen, setAiSettingsOpen] = useState(false); + const [mcpToolsPageOpen, setMcpToolsPageOpen] = useState(false); + + const handleAIEmbeddings = useCallback(async () => { + notify("Generating embeddings...", "info"); + try { + const result = await aiGenerateEmbeddings(true); + if (result?.success) { + notify("Embeddings generated successfully!", "success"); + } else { + notify(result?.error || "Failed to generate embeddings", "error"); + } + } catch (err) { + notify(err?.message || "Failed to generate embeddings", "error"); + } + }, [notify]); + + const handleAIGraph = useCallback(async () => { + notify("Building knowledge graph...", "info"); + try { + const result = await aiBuildGraph(); + if (result?.success) { + notify("Knowledge graph built successfully!", "success"); + } else { + notify(result?.error || "Failed to build knowledge graph", "error"); + } + } catch (err) { + console.error("[AI] Graph build error:", err); + notify(err?.message || "Failed to build knowledge graph", "error"); + } + }, [notify]); + + const handleAIClearCache = useCallback(async () => { + notify("Clearing AI cache...", "info"); + try { + const result = await aiClearData(); + if (result?.success) { + notify("AI cache cleared successfully!", "success"); + } else { + notify(result?.error || "Failed to clear cache", "error"); + } + } catch (err) { + notify(err?.message || "Failed to clear cache", "error"); + } + }, [notify]); useEffect(() => { if (p2pStatusOpen) { @@ -2084,10 +2090,7 @@ export default function App() { return; } - if (action === "open-ai-palette") { - handleOpenAIPalette({ forceOpen: true }); - return; - } + if (action === "ai-generate-embeddings") { handleAIEmbeddings(); @@ -2110,6 +2113,11 @@ export default function App() { return; } + if (action === "open-mcp-tools") { + setMcpToolsPageOpen(true); + return; + } + if (action === "open-workspace-index" || action === "workspace-index") { setWorkspaceIndexOpen(true); return; @@ -2313,7 +2321,8 @@ export default function App() { const paletteCommandsBase = [ { id: "restart-app", label: "Restart Notely", group: "App", aliases: "restart relaunch reboot app application" }, { id: "new-note", label: "Create New Note", group: "Notes", shortcut: "Ctrl/Cmd+N", aliases: "add note new document write jot capture" }, - { id: "open-ai-palette", label: "Open AI Palette", group: "AI", shortcut: "Ctrl/Cmd+Shift+I", aliases: "assistant ask ai prompt summarize rewrite" }, + { id: "open-mcp-tools", label: "Open MCP Tools & Capabilities", group: "AI", shortcut: "Ctrl/Cmd+Shift+M", aliases: "mcp tools agent external server api" }, + { id: "open-help-center", label: "Open Help Center", group: "Help", shortcut: "F1", aliases: "help docs guide manual about" }, { id: "open-feedback", label: "Report Bug / Feedback", group: "Help", aliases: "feedback bug report issue feature request" }, { id: "open-about", label: "Open About Notely", group: "Help", aliases: "about version build" }, @@ -2631,11 +2640,13 @@ export default function App() { return; } - if (resolvedCommandId === "open-ai-palette") { - handleOpenAIPalette({ forceOpen: true }); + if (resolvedCommandId === "open-mcp-tools") { + setMcpToolsPageOpen(true); return; } + + if (resolvedCommandId === "open-help-center") { setHelpConfirmationOpen(true); return; @@ -2930,16 +2941,6 @@ export default function App() { return; } - if (action === "ai") { - if (!isAIConfigured) { - notify("Configure an AI provider key in AI Settings to use AI chat.", "warning"); - setAiSettingsOpen(true); - return; - } - setAiPanelVisible((visible) => !visible); - return; - } - if (action === "trash") { setTrashDialogOpen(true); } @@ -3029,37 +3030,7 @@ export default function App() { [favoriteNotes, recentDashboardNotes, continueDashboardNotes] ); - const aiSidebarComponent = aiPanelVisible && isAIConfigured ? ( - - Loading AI…}> - setAiPanelVisible(false)} - onClear={handleClearAIChat} - onSend={handleAIChatSend} - onAbort={handleAIChatAbort} - activeQueryId={activeQueryId} - onApply={handleApplyAIResult} - onOpenDocument={handleOpenReferencedDocumentFromUI} - onPreviewNote={handlePreviewNote} - isLoading={aiQueryLoading} - error={aiQueryError || null} - contextSummary={aiContextSummary} - intent={aiPaletteIntent} - messages={aiChatMessages} - noteTitle={current?.title || "Current Note"} - activeProvider={activeProvider} - activePersona={activePersona} - setActivePersona={setActivePersona} - workspaceStorageScope={workspaceStorageScope} - conversations={conversations} - onLoadConversations={loadConversations} - onLoadConversation={loadConversation} - onDeleteConversation={deleteConversation} - /> - - - ) : null; return (
@@ -3154,6 +3125,10 @@ export default function App() { onClick={() => setGitVCOpen(true)} /> setAiSettingsOpen(true)} /> + { + setSettingsTab("mcp"); + setSettingsOpen(true); + }} /> {current && !(graphPanelOpen || embeddingsPageOpen || personasPageOpen || healthPageOpen || appLogsOpen || gitVCOpen) ? ( <> {documentStats ? ( @@ -3188,17 +3163,7 @@ export default function App() { documents={documents} workspaceTaskDocuments={workspaceTaskDocuments} loading={loading} - aiSidebar={aiSidebarComponent} - aiPanelVisible={aiPanelVisible} - isAIConfigured={isAIConfigured} - onShowAI={() => { - if (!isAIConfigured) { - notify("Configure an AI provider key in AI Settings to use AI chat.", "warning"); - setAiSettingsOpen(true); - return; - } - setAiPanelVisible((visible) => !visible); - }} + onOpenListItem={handleOpenListItem} onOpenReferencedDocument={(task) => handleOpenReferencedDocument(task?.filePath)} onOpenAllTasks={() => { @@ -3297,25 +3262,7 @@ export default function App() { if (!didLeave) return; await handleLandingNavigateTo(targetPath); }} - onOpenAI={handleOpenAIPalette} - onOpenAIRequest={handleOpenAIPalette} - onInlineAIRequest={handleInlineAIRequest} - onRegisterAIEditor={(api) => { - aiEditorRef.current = api; - }} - inlineGhostSuggestion={inlineGhostSuggestion} - onAcceptInlineGhost={handleAcceptInlineGhost} - onRejectInlineGhost={handleRejectInlineGhost} - aiEnabled={isAIConfigured} - aiPanelVisible={aiPanelVisible} - onShowAI={() => { - if (!isAIConfigured) { - notify("Configure an AI provider key in AI Settings to use AI chat.", "warning"); - setAiSettingsOpen(true); - return; - } - setAiPanelVisible((visible) => !visible); - }} + onOpenAISettings={() => setAiSettingsOpen(true)} onOpenDocument={handleOpenReferencedDocumentFromUI} initialLine={initialLine} @@ -3335,7 +3282,6 @@ export default function App() { scrollSyncEnabled={scrollSyncEnabled} onScrollSyncEnabledChange={setScrollSyncEnabled} onReloadFromDisk={(filePath) => handleReloadCurrentFromDisk(filePath)} - aiSidebar={aiSidebarComponent} /> )} @@ -3511,9 +3457,9 @@ export default function App() { {settingsOpen ? ( { setSettingsOpen(false); - refreshAIConfiguration(); }} activeTab={settingsTab} themePreference={themePreference} @@ -3851,6 +3797,12 @@ export default function App() { setWorkspaceIndexOpen={setWorkspaceIndexOpen} diagramsMediaOpen={diagramsMediaOpen} setDiagramsMediaOpen={setDiagramsMediaOpen} + mcpToolsPageOpen={mcpToolsPageOpen} + setMcpToolsPageOpen={setMcpToolsPageOpen} + onOpenMcpSettings={() => { + setSettingsTab("mcp"); + setSettingsOpen(true); + }} onSelectHeader={(docId, line) => { handleOpenReferencedDocument(docId, line); }} diff --git a/src/ai/utils/ipcProtocol.js b/src/ai/utils/ipcProtocol.js index 7c2e744f..efbacca3 100644 --- a/src/ai/utils/ipcProtocol.js +++ b/src/ai/utils/ipcProtocol.js @@ -4,9 +4,7 @@ const IPC_EVENTS = { AI_INIT: 'ai:init', - AI_QUERY: 'ai:query', - AI_QUERY_STREAM: 'ai:query:stream', - AI_QUERY_ABORT: 'ai:query:abort', + // AI_QUERY/STREAM/ABORT removed — chat moved to MCP layer AI_STATUS: 'ai:status', AI_GENERATE_EMBEDDINGS: 'ai:embeddings:generate', AI_BUILD_GRAPH: 'ai:graph:build', @@ -28,7 +26,7 @@ const IPC_EVENTS = { AI_GRAPH_MODEL_DOWNLOAD: 'ai:graph-model:download', AI_GRAPH_MODEL_DELETE: 'ai:graph-model:delete', AI_GRAPH_MODEL_STATUS: 'ai:graph-model:status', - AI_DETECT_PATTERNS: 'ai:patterns:detect', + // AI_DETECT_PATTERNS removed — chat-only AI_LOGS_GET: 'ai:logs:get', AI_LOGS_CLEAR: 'ai:logs:clear', AI_NOTE_STATS: 'ai:note:stats', @@ -44,23 +42,14 @@ const IPC_EVENTS = { AI_ENABLE: 'ai:enable', AI_DISABLE: 'ai:disable', AI_HEALTH_GET: 'ai:health:get', - AI_CONVERSATION_LIST: 'ai:conversation:list', - AI_CONVERSATION_GET: 'ai:conversation:get', - AI_CONVERSATION_CREATE: 'ai:conversation:create', - AI_CONVERSATION_DELETE: 'ai:conversation:delete', - AI_CONVERSATION_CLEAR: 'ai:conversation:clear', - AI_CONVERSATION_SET_PERSONA: 'ai:conversation:set-persona', - AI_CONVERSATION_GET_MESSAGES: 'ai:conversation:get-messages', - AI_CONVERSATION_ADD_MESSAGE: 'ai:conversation:add-message', + // AI_CONVERSATION_* removed — chat moved to MCP layer AI_PERSONA_LIST: 'ai:persona:list', AI_PERSONA_GET: 'ai:persona:get', AI_PERSONA_SAVE: 'ai:persona:save', AI_PERSONA_DELETE: 'ai:persona:delete', AI_PERSONA_IMPORT: 'ai:persona:import', AI_PERSONA_EXPORT: 'ai:persona:export', - AI_KNOWLEDGE_LIST_PENDING: 'ai:knowledge:list-pending', - AI_KNOWLEDGE_APPROVE: 'ai:knowledge:approve', - AI_KNOWLEDGE_REJECT: 'ai:knowledge:reject', + // AI_KNOWLEDGE_* removed — chat-only AI_SHUTDOWN: 'ai:shutdown', TOOL_EXECUTE: 'tool:execute', TOOL_LIST: 'tool:list' diff --git a/src/components/AIChatPanel.jsx b/src/components/AIChatPanel.jsx deleted file mode 100644 index ea3ac68a..00000000 --- a/src/components/AIChatPanel.jsx +++ /dev/null @@ -1,567 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from "react"; -import { Send, X, Trash2, Pencil, Check, History, RotateCcw } from "lucide-react"; -import AppButton from "./AppButton"; -import AppTextarea from "./AppTextarea"; -import NotePreviewModal from "./NotePreviewModal"; -import { renderMarkdown } from "../utils/renderUtils"; -import { aiListPersonas } from "../services/electronService"; -import { useWorkspaceScopedStorage } from "../hooks/useWorkspaceScopedStorage"; -import { useConfirm } from "../hooks/useConfirm"; - -const SCOPE_OPTIONS = [ - { id: "auto", label: "Auto" }, - { id: "selection", label: "Selection" }, - { id: "block", label: "Block" }, - { id: "document", label: "Note" }, - { id: "workspace", label: "Workspace" }, -]; - -function buildStarterPrompts(contextSummary) { - if (!contextSummary?.hasActiveDocument) { - return [ - "Summarize key tasks across my workspace.", - "List all active projects in this workspace.", - "Find links or references related to design plans.", - ]; - } - if (contextSummary?.hasSelection) { - return [ - "Make this clearer without changing the meaning.", - "Turn this into concise action items.", - "Challenge the assumptions in this selection.", - ]; - } - if (contextSummary?.hasCurrentBlock) { - return [ - "Continue this section in the same tone.", - "Summarize this block more cleanly.", - "Extract next steps from this block.", - ]; - } - return [ - "Summarize this note in 3 key bullet points.", - "Extract all action items & TODOs.", - "Explain the core technical concepts mentioned.", - ]; -} - -function getScopeHelp(scope, contextSummary) { - if (!contextSummary?.hasActiveDocument) return ""; - if (scope === "workspace") { - return contextSummary?.hasSelection - ? "Uses the selected text as the focal point, then widens to the whole workspace." - : "Uses this note as the focal point, then widens to the whole workspace."; - } - if (scope === "selection") { - return contextSummary?.hasSelection - ? "Uses only the selected text." - : "No selection is active, so this will fall back to the full note."; - } - if (scope === "block") { - return contextSummary?.hasCurrentBlock - ? "Uses the current paragraph or block around the cursor." - : "No current block is available, so this will fall back to the full note."; - } - if (scope === "document") return "Uses the full current note."; - return contextSummary?.hasSelection - ? "Auto uses the current selection first, otherwise the full note." - : "Auto uses the full note unless a selection is active."; -} - -export default function AIChatPanel({ - onHide, - onClear, - onSend, - onAbort, - activeQueryId, - onApply, - onOpenDocument, - onPreviewNote, - isLoading = false, - error = null, - contextSummary = null, - intent = null, - messages = [], - _noteTitle = "Current Note", - _activeProvider = "", - activePersona = null, - setActivePersona, - workspaceStorageScope = "default", - conversations = [], - onLoadConversations, - onLoadConversation, - onDeleteConversation, -}) { - const [previewTarget, setPreviewTarget] = useState(null); - const [draft, setDraft] = useState(""); - const [scope, setScope] = useState("auto"); - const [personas, setPersonas] = useState([]); - const [isEditingPersona, setIsEditingPersona] = useState(false); - const [selectedPersonaId, setSelectedPersonaId] = useState("default"); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const inputRef = useRef(null); - const lastAutoRunRequestIdRef = useRef(""); - const messagesEndRef = useRef(null); - - const handlePreviewLink = (rawPath, lineNum = null) => { - if (onPreviewNote) { - onPreviewNote(rawPath, lineNum); - } else { - setPreviewTarget({ path: rawPath, lineNum }); - } - }; - - const { confirm } = useConfirm(); - - const [persistedPersonaId, setPersistedPersonaId] = useWorkspaceScopedStorage({ - workspaceScope: workspaceStorageScope, - key: "activePersonaId", - defaultValue: "default", - }); - - const starterPrompts = useMemo(() => buildStarterPrompts(contextSummary), [contextSummary]); - - const lastUserMessage = useMemo(() => { - for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i].role === "user") return messages[i]; - } - return null; - }, [messages]); - - useEffect(() => { - setDraft(intent?.query || ""); - setScope(intent?.target || (contextSummary?.hasActiveDocument ? "auto" : "workspace")); - requestAnimationFrame(() => inputRef.current?.focus()); - }, [intent, contextSummary]); - - useEffect(() => { - if (!contextSummary?.hasActiveDocument) { - setScope("workspace"); - } - }, [contextSummary]); - - useEffect(() => { - async function load() { - const res = await aiListPersonas(); - if (res?.success && res.data) { - setPersonas(res.data); - const targetId = persistedPersonaId || "default"; - const matched = res.data.find(p => p.id === targetId) || res.data.find(p => p.id === "default") || res.data[0]; - if (matched && (!activePersona || activePersona.id !== matched.id)) { - setActivePersona(matched); - } - } - } - load(); - }, [persistedPersonaId, activePersona, setActivePersona]); - - useEffect(() => { - if (!intent?.autoRun || !intent?.query) return; - if (lastAutoRunRequestIdRef.current === intent.requestId) return; - lastAutoRunRequestIdRef.current = intent.requestId; - onSend?.({ message: intent.query, target: intent.target || scope }); - setDraft(""); - }, [intent, onSend, scope]); - - useEffect(() => { - if (isDrawerOpen) { - onLoadConversations?.(); - } - }, [isDrawerOpen, onLoadConversations]); - - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages]); - - const handleClearWithConfirm = async () => { - const confirmed = await confirm({ - title: "Clear Chat History", - message: "Are you sure you want to clear all messages in this conversation? This cannot be undone.", - confirmLabel: "Clear History", - cancelLabel: "Cancel", - variant: "danger", - }); - if (confirmed) { - onClear?.(); - } - }; - - return ( - - ); -} diff --git a/src/components/AIHealthPage.jsx b/src/components/AIHealthPage.jsx index 5885d682..4c4ac7a3 100644 --- a/src/components/AIHealthPage.jsx +++ b/src/components/AIHealthPage.jsx @@ -25,9 +25,10 @@ import { Brain, FileText, Bot, - Filter + Filter, + Server } from 'lucide-react'; -import { aiGetHealth, aiListConversations, aiGetMessages, aiGetLogs, aiClearLogs, aiClearConversations, onTelemetryEvent } from '../services/electronService'; +import { aiGetHealth, aiListConversations, aiGetMessages, aiGetLogs, aiClearLogs, aiClearConversations, onTelemetryEvent, mcpGetStatus } from '../services/electronService'; import { useConfirm } from '../hooks/useConfirm'; import { renderMarkdown } from '../utils/renderUtils'; import '../styles/KnowledgeGraph.css'; @@ -589,7 +590,7 @@ function FlowTelemetryPane({ conv, flowLogs }) { {filteredLogs.length === 0 && (
{flowLogs.length === 0 - ? 'No flow telemetry recorded yet for this conversation thread. Send a message in chat to generate execution events.' + ? 'No flow telemetry recorded yet. Trigger MCP tools or background AI tasks to generate execution events.' : 'No execution events match your search.'}
)} @@ -821,6 +822,7 @@ function ConversationPane({ conv, onBack }) { export default function AIHealthPage({ onBack }) { const { confirm } = useConfirm(); const [health, setHealth] = useState(null); + const [mcpStatus, setMcpStatus] = useState(null); const [conversations, setConversations] = useState([]); const [selectedConv, setSelectedConv] = useState(null); const [convSearch, setConvSearch] = useState(''); @@ -831,13 +833,15 @@ export default function AIHealthPage({ onBack }) { try { setLoading(true); setError(''); - const [healthRes, convRes] = await Promise.all([ + const [healthRes, convRes, mcpRes] = await Promise.all([ aiGetHealth(), - aiListConversations().catch(() => ({ success: true, data: [] })) + aiListConversations().catch(() => ({ success: true, data: [] })), + mcpGetStatus().catch(() => null) ]); if (healthRes?.success) setHealth(healthRes.data); else setError(healthRes?.error || 'Failed to fetch diagnostics.'); if (convRes?.success) setConversations(convRes.data || []); + if (mcpRes) setMcpStatus(mcpRes); } catch (err) { setError(err.message); } finally { @@ -866,7 +870,7 @@ export default function AIHealthPage({ onBack }) { - AI Health & Diagnostics + AI & MCP Diagnostics
@@ -915,7 +919,7 @@ export default function AIHealthPage({ onBack }) {
- +
@@ -925,6 +929,7 @@ export default function AIHealthPage({ onBack }) {
+ @@ -1017,7 +1022,7 @@ export default function AIHealthPage({ onBack }) { {filteredConversations.length === 0 ? (
{conversations.length === 0 - ? 'No conversations yet. Start chatting to see history here.' + ? 'No conversations yet. External AI clients connect and invoke capabilities via MCP.' : 'No matches for your search.'}
) : ( diff --git a/src/components/AIPalette.jsx b/src/components/AIPalette.jsx deleted file mode 100644 index bd0530d1..00000000 --- a/src/components/AIPalette.jsx +++ /dev/null @@ -1,628 +0,0 @@ -import React, { useEffect, useMemo, useRef, useState } from "react"; -import "../styles/AIPalette.css"; -import AppButton from "./AppButton"; -import AppChipButton from "./AppChipButton"; -import OverlayDialog from "./OverlayDialog"; - -const TARGET_OPTIONS = [ - { id: "selection", label: "Selection" }, - { id: "block", label: "Current Block" }, - { id: "document", label: "Whole Note" }, -]; - -const APPLY_OPTIONS = [ - { id: "insert", label: "Insert at Cursor" }, - { id: "replace-selection", label: "Replace Selection" }, - { id: "replace-block", label: "Replace Block" }, -]; - -const NOTE_PRESETS = [ - { id: "meeting", label: "Meeting Notes" }, - { id: "research", label: "Research Notes" }, - { id: "action-plan", label: "Action Plan" }, -]; - -function getPresetStorageKey(scope, value) { - if (!value) return ""; - return `ai:preset:${scope}:${value}`; -} - -function buildPreviewRows(currentText, nextText) { - const currentLines = String(currentText || "").split(/\r?\n/); - const nextLines = String(nextText || "").split(/\r?\n/); - const max = Math.max(currentLines.length, nextLines.length); - const rows = []; - - for (let index = 0; index < max; index += 1) { - const previous = currentLines[index] ?? ""; - const latest = nextLines[index] ?? ""; - let status = "same"; - if (index >= currentLines.length) status = "added"; - else if (index >= nextLines.length) status = "removed"; - else if (previous !== latest) status = "changed"; - rows.push({ - id: `${index}-${status}`, - line: index + 1, - previous, - latest, - status, - }); - } - - return rows; -} - -function mergePreviewRows(rows, selectedRows) { - const merged = []; - - rows.forEach((row) => { - const chosen = selectedRows[row.id] !== false; - if (row.status === "same") { - merged.push(row.previous); - return; - } - - if (row.status === "added") { - if (chosen) merged.push(row.latest); - return; - } - - if (row.status === "removed") { - if (!chosen) merged.push(row.previous); - return; - } - - merged.push(chosen ? row.latest : row.previous); - }); - - return merged.join("\n"); -} - -const AI_COMMANDS = [ - { id: "summarize", label: "Summarize Document", description: "Generate a concise summary of the current note", icon: "Sum" }, - { id: "analyze", label: "Analyze Content", description: "Analyze the note and surface key insights", icon: "Ana" }, - { id: "format", label: "Format Markdown", description: "Improve markdown structure and consistency", icon: "Fmt" }, - { id: "search", label: "Search Workspace", description: "Find related notes and concepts", icon: "Sea" }, - { id: "generate", label: "Generate Content", description: "Draft new markdown to add into the note", icon: "Gen" }, - { id: "refactor", label: "Organize Content", description: "Rewrite or reorganize the current section", icon: "Org" }, - { id: "find-related", label: "Find Related Docs", description: "Find semantically similar documents", icon: "Rel" }, -]; - -function buildQuickActions(contextSummary, noteTitle, preset) { - if (preset === "meeting") { - return [ - { - id: "meeting-decisions", - label: "Extract Decisions", - description: "Pull out decisions and owners from this note.", - query: "Extract decisions, owners, and follow-up actions from this meeting note as markdown bullets.", - target: contextSummary?.hasSelection ? "selection" : contextSummary?.hasCurrentBlock ? "block" : "document", - }, - { - id: "meeting-minutes", - label: "Polish Minutes", - description: "Turn rough notes into cleaner meeting minutes.", - query: "Rewrite these meeting notes into crisp meeting minutes with sections for discussion, decisions, and next steps.", - target: contextSummary?.hasSelection ? "selection" : contextSummary?.hasCurrentBlock ? "block" : "document", - }, - { - id: "meeting-followup", - label: "Create Follow-Ups", - description: "Generate a follow-up checklist after the meeting.", - query: "Create a follow-up checklist from this meeting note with owners and due-date placeholders.", - target: contextSummary?.hasSelection ? "selection" : contextSummary?.hasCurrentBlock ? "block" : "document", - }, - ]; - } - - if (preset === "action-plan") { - return [ - { - id: "plan-steps", - label: "Structure Plan", - description: "Turn ideas into sequenced implementation steps.", - query: "Turn this into a structured action plan with phases, tasks, and dependencies.", - target: contextSummary?.hasSelection ? "selection" : contextSummary?.hasCurrentBlock ? "block" : "document", - }, - { - id: "plan-risks", - label: "Find Risks", - description: "Surface blockers, risks, and missing prerequisites.", - query: "Identify risks, blockers, and missing prerequisites in this action plan.", - target: contextSummary?.hasSelection ? "selection" : contextSummary?.hasCurrentBlock ? "block" : "document", - }, - { - id: "plan-checklist", - label: "Make Execution Checklist", - description: "Convert this plan into a tighter execution checklist.", - query: "Convert this into a practical markdown execution checklist with milestones.", - target: contextSummary?.hasSelection ? "selection" : contextSummary?.hasCurrentBlock ? "block" : "document", - }, - ]; - } - - if (contextSummary?.hasSelection) { - return [ - { - id: "rewrite-selection", - label: "Rewrite Cleanly", - description: "Polish the selected text while preserving the point.", - query: "Rewrite this selection to be clearer, tighter, and more polished while preserving meaning.", - target: "selection", - }, - { - id: "expand-selection", - label: "Add Detail", - description: "Expand the selection with more useful specifics.", - query: "Expand this selection with more concrete detail and helpful context in markdown.", - target: "selection", - }, - { - id: "checklist-selection", - label: "Make Checklist", - description: "Turn the selection into an actionable markdown checklist.", - query: "Convert this selection into an actionable markdown checklist.", - target: "selection", - }, - ]; - } - - if (contextSummary?.hasCurrentBlock) { - return [ - { - id: "continue-block", - label: "Continue Section", - description: "Keep writing from the current block in the same tone.", - query: "Continue this section in the same tone and structure with useful next details.", - target: "block", - }, - { - id: "compress-block", - label: "Tighten Block", - description: "Make the current block shorter and easier to scan.", - query: "Rewrite the current block to be shorter, clearer, and easier to scan.", - target: "block", - }, - { - id: "extract-actions", - label: "Extract Next Steps", - description: "Pull out action items and decisions from the current block.", - query: "Extract the action items, decisions, and follow-ups from this block as markdown bullets.", - target: "block", - }, - ]; - } - - return [ - { - id: "summarize-note", - label: "Summarize Note", - description: `Create a concise executive summary of ${noteTitle}.`, - query: "Summarize this note into a concise executive overview with key takeaways.", - target: "document", - }, - { - id: "find-gaps", - label: "Find Gaps", - description: "Identify what is missing or unclear in the note.", - query: "Review this note and identify missing details, unclear sections, and suggested improvements.", - target: "document", - }, - { - id: "create-plan", - label: "Create Plan", - description: "Turn the note into a clearer next-step plan.", - query: "Turn this note into a structured next-step plan with markdown headings and bullets.", - target: "document", - }, - ]; -} - -export default function AIPalette({ - isOpen, - onClose, - onQuery, - onApply, - isLoading = false, - error = null, - contextSummary = null, - intent = null, - noteTitle = "Current Note", - noteKey = "", - workspaceKey = "", -}) { - const [searchInput, setSearchInput] = useState(""); - const [suggestions, setSuggestions] = useState([]); - const [selectedIndex, setSelectedIndex] = useState(0); - const [recentQueries, setRecentQueries] = useState([]); - const [responseText, setResponseText] = useState(""); - const [target, setTarget] = useState("selection"); - const [notePreset, setNotePreset] = useState("research"); - const [pendingApply, setPendingApply] = useState(null); - const [selectedDiffRows, setSelectedDiffRows] = useState({}); - const inputRef = useRef(null); - const lastAutoRunRequestIdRef = useRef(""); - - const availableApplyOptions = useMemo( - () => APPLY_OPTIONS.map((option) => ({ - ...option, - disabled: - option.id === "replace-selection" - ? !contextSummary?.hasSelection - : option.id === "replace-block" - ? !contextSummary?.hasCurrentBlock - : false, - })), - [contextSummary] - ); - - const quickActions = useMemo( - () => buildQuickActions(contextSummary, noteTitle, notePreset), - [contextSummary, noteTitle, notePreset] - ); - - const diffPreview = useMemo(() => { - if (!pendingApply?.rows) return []; - return pendingApply.rows.filter((row) => row.status !== "same"); - }, [pendingApply]); - - useEffect(() => { - const recent = localStorage.getItem("ai-recent-queries"); - if (recent) { - setRecentQueries(JSON.parse(recent)); - } - }, []); - - useEffect(() => { - if (!isOpen) return; - inputRef.current?.focus(); - setSearchInput(intent?.query || ""); - setResponseText(""); - setPendingApply(null); - setSelectedDiffRows({}); - setSelectedIndex(0); - setTarget(intent?.target || (contextSummary?.hasSelection ? "selection" : contextSummary?.hasCurrentBlock ? "block" : "document")); - const notePresetKey = getPresetStorageKey("note", noteKey); - const workspacePresetKey = getPresetStorageKey("workspace", workspaceKey); - const savedPreset = - (notePresetKey ? window.localStorage.getItem(notePresetKey) : "") - || (workspacePresetKey ? window.localStorage.getItem(workspacePresetKey) : ""); - setNotePreset(savedPreset || contextSummary?.suggestedPreset || "research"); - setSuggestions(AI_COMMANDS); - }, [contextSummary, intent, isOpen, noteKey, workspaceKey]); - - useEffect(() => { - if (!isOpen || !notePreset) return; - const notePresetKey = getPresetStorageKey("note", noteKey); - const workspacePresetKey = getPresetStorageKey("workspace", workspaceKey); - if (notePresetKey) { - window.localStorage.setItem(notePresetKey, notePreset); - } - if (workspacePresetKey) { - window.localStorage.setItem(workspacePresetKey, notePreset); - } - }, [isOpen, noteKey, notePreset, workspaceKey]); - - useEffect(() => { - if (!isOpen || !intent?.autoRun || !intent?.query) return; - if (lastAutoRunRequestIdRef.current === intent.requestId) return; - lastAutoRunRequestIdRef.current = intent.requestId; - handleCustomQuery(intent.query, intent.target || target); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen, intent, target]); - - const updateSuggestions = (query) => { - if (!query.trim()) { - setSuggestions(AI_COMMANDS); - return; - } - - const lowered = query.toLowerCase(); - const filtered = AI_COMMANDS.filter( - (command) => command.label.toLowerCase().includes(lowered) - || command.description.toLowerCase().includes(lowered) - ); - setSuggestions(filtered); - setSelectedIndex(0); - }; - - const handleInputChange = (event) => { - const value = event.target.value; - setSearchInput(value); - updateSuggestions(value); - }; - - const handleKeyDown = (event) => { - switch (event.key) { - case "ArrowDown": - event.preventDefault(); - if (suggestions.length) { - setSelectedIndex((prev) => (prev + 1) % suggestions.length); - } - break; - case "ArrowUp": - event.preventDefault(); - if (suggestions.length) { - setSelectedIndex((prev) => (prev - 1 + suggestions.length) % suggestions.length); - } - break; - case "Enter": - event.preventDefault(); - if (suggestions.length > 0) { - handleSelectCommand(suggestions[selectedIndex]); - } else if (searchInput.trim()) { - handleCustomQuery(searchInput); - } - break; - case "Escape": - event.preventDefault(); - onClose(); - break; - default: - break; - } - }; - - const handleSelectCommand = (command) => { - setSearchInput(command.label); - handleCustomQuery(command.label); - }; - - const handleCustomQuery = async (query, overrideTarget = null) => { - if (!query.trim() || isLoading) return; - const effectiveTarget = overrideTarget || target; - - const updated = [query, ...recentQueries.filter((item) => item !== query)].slice(0, 10); - setRecentQueries(updated); - localStorage.setItem("ai-recent-queries", JSON.stringify(updated)); - - const result = await onQuery({ query, target: effectiveTarget }); - setResponseText(result?.text || ""); - }; - - const handleApply = async (mode) => { - if (!responseText || typeof onApply !== "function") return; - const preview = await onApply({ text: responseText, mode, previewOnly: mode !== "insert" }); - if (mode === "insert") { - return; - } - if (!preview?.applied && preview?.currentText && preview?.nextText) { - const rows = buildPreviewRows(preview.currentText, preview.nextText); - setPendingApply({ - mode, - text: responseText, - currentText: preview.currentText, - nextText: preview.nextText, - rows, - }); - setSelectedDiffRows( - rows.reduce((acc, row) => { - if (row.status !== "same") acc[row.id] = true; - return acc; - }, {}) - ); - } - }; - - const handleConfirmApply = async () => { - if (!pendingApply) return; - const nextText = pendingApply.mode === "replace-selection" - ? mergePreviewRows(pendingApply.rows, selectedDiffRows) - : pendingApply.text; - await onApply({ text: nextText, mode: pendingApply.mode, previewOnly: false }); - setPendingApply(null); - setSelectedDiffRows({}); - }; - - return ( - -
-
-
AI Assistant
-
Working inside {noteTitle}
-
- -
- -
-
- {NOTE_PRESETS.map((preset) => ( - setNotePreset(preset.id)} - > - {preset.label} - - ))} -
-
- {TARGET_OPTIONS.map((option) => ( - setTarget(option.id)} - > - {option.label} - - ))} -
-
- {contextSummary?.label || "Using the current note as context."} -
-
- -
- - {isLoading ?
: null} -
- - {quickActions.length ? ( -
-
Quick actions
-
- {quickActions.map((action) => ( - - ))} -
-
- ) : null} - - {error ? ( -
- ! - {error} -
- ) : null} - -
-
Commands
- {suggestions.length > 0 ? ( - suggestions.map((command, index) => ( - - )) - ) : searchInput.trim() ? ( -
-
Ask custom question:
- -
- ) : ( -
No suggestions
- )} -
- - {recentQueries.length > 0 && !searchInput ? ( -
-
Recent:
-
- {recentQueries.slice(0, 3).map((query, index) => ( - - ))} -
-
- ) : null} - - {responseText ? ( -
-
- AI Draft - {responseText.length} chars -
-
{responseText}
-
- {availableApplyOptions.map((option) => ( - - ))} -
- {pendingApply ? ( -
-
- Preview replacement - setPendingApply(null)}>Cancel -
-
- {diffPreview.map((row) => ( -
- {pendingApply.mode === "replace-selection" ? ( - - ) : null} - {row.line} -
{row.previous}
-
{row.latest}
-
- ))} -
-
- Apply Replacement -
-
- ) : null} -
- ) : null} - -
- Ctrl/Cmd+Shift+I opens the AI palette. Run a prompt, then insert or replace directly in the editor. -
- - ); -} diff --git a/src/components/AISettings.jsx b/src/components/AISettings.jsx index 863dd281..034baa78 100644 --- a/src/components/AISettings.jsx +++ b/src/components/AISettings.jsx @@ -356,7 +356,7 @@ export const AISettingsContent = ({ _onClose }) => { Enable AI Subsystem - Toggle the global switch to enable or disable all background AI services, embeddings, and chat. + Toggle the global switch to enable or disable all background AI services, embeddings, and graph extraction.