devmentorai-server 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -58,6 +58,8 @@ var SESSION_TYPE_CONFIGS = {
|
|
|
58
58
|
- Error diagnosis, log analysis, and troubleshooting
|
|
59
59
|
- Architecture design and scalability patterns
|
|
60
60
|
|
|
61
|
+
IMPORTANT TOOL CAPABILITY: You have a tool called "fetch_url" available. Whenever a user provides a public URL in their request, you SHOULD PROACTIVELY use the fetch_url tool to read its contents and provide a more accurate, contextual response based on the actual content of that page. Do not assume you cannot read URLs - you have a tool specifically for this purpose.
|
|
62
|
+
|
|
61
63
|
When analyzing configurations or errors:
|
|
62
64
|
1. Identify the issue or configuration clearly
|
|
63
65
|
2. Explain WHY something is problematic or recommended
|
|
@@ -65,7 +67,7 @@ When analyzing configurations or errors:
|
|
|
65
67
|
4. Reference official documentation when helpful
|
|
66
68
|
5. Warn about security implications when relevant
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
Provide a high-quality, detailed, and explanatory response that helps the user completely understand the concepts, steps, and 'why' behind the suggestions. Use code blocks for configurations and commands.`
|
|
69
71
|
}
|
|
70
72
|
},
|
|
71
73
|
writing: {
|
|
@@ -92,7 +94,7 @@ Guidelines:
|
|
|
92
94
|
3. Preserve formatting when rewriting
|
|
93
95
|
4. For translations, keep cultural nuances in mind
|
|
94
96
|
5. Provide alternatives when helpful
|
|
95
|
-
6.
|
|
97
|
+
6. Provide rich, detailed explanations and fully expand on ideas when discussing or explaining writing choices.
|
|
96
98
|
|
|
97
99
|
When the user provides text to modify, respond with ONLY the modified text unless they ask for explanation.`
|
|
98
100
|
}
|
|
@@ -115,8 +117,10 @@ When the user provides text to modify, respond with ONLY the modified text unles
|
|
|
115
117
|
- Documentation and code comments
|
|
116
118
|
- Refactoring and code cleanup
|
|
117
119
|
|
|
120
|
+
IMPORTANT TOOL CAPABILITY: You have tools available to assist you. If you are in a devops session type, you have a tool called "fetch_url". Whenever a user provides a public URL in their request and you have the tool available, you SHOULD PROACTIVELY use the fetch_url tool to read its contents and provide a more accurate, contextual response. Do not assume you cannot read URLs.
|
|
121
|
+
|
|
118
122
|
Guidelines:
|
|
119
|
-
1.
|
|
123
|
+
1. Provide actionable and detailed explanatory responses that help the user completely understand the concepts, steps, and 'why' behind the suggestions.
|
|
120
124
|
2. Explain the "why" behind suggestions
|
|
121
125
|
3. Provide code examples when helpful
|
|
122
126
|
4. Consider edge cases and error handling
|
|
@@ -289,7 +293,7 @@ ensureDir(IMAGES_DIR);
|
|
|
289
293
|
ensureDir(LOG_DIR);
|
|
290
294
|
|
|
291
295
|
// src/version.ts
|
|
292
|
-
var BACKEND_VERSION = "1.
|
|
296
|
+
var BACKEND_VERSION = "1.3.0";
|
|
293
297
|
|
|
294
298
|
export {
|
|
295
299
|
DATA_DIR,
|
|
@@ -316,4 +320,4 @@ export {
|
|
|
316
320
|
checkForUpdate,
|
|
317
321
|
BACKEND_VERSION
|
|
318
322
|
};
|
|
319
|
-
//# sourceMappingURL=chunk-
|
|
323
|
+
//# sourceMappingURL=chunk-X4NL4LWR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../packages/shared/src/contracts/api.contracts.ts","../../../packages/shared/src/types/message.ts","../../../packages/shared/src/types/context.ts","../../../packages/shared/src/contracts/session-types.ts","../../../packages/shared/src/utils/helpers.ts","../../../packages/shared/src/utils/update-checker.ts","../src/lib/paths.ts","../src/version.ts"],"sourcesContent":["/**\n * API contract definitions for DevMentorAI\n * These define the endpoints and their request/response types\n */\n\nimport type {\n Session,\n CreateSessionRequest,\n UpdateSessionRequest,\n Message,\n SendMessageRequest,\n ApiResponse,\n PaginatedResponse,\n HealthResponse,\n ModelInfo,\n CopilotAuthStatus,\n CopilotQuotaStatus,\n} from '../types/index.js';\n\n/**\n * Backend API endpoints contract\n */\nexport const API_ENDPOINTS = {\n // Health\n HEALTH: '/api/health',\n \n // Sessions\n SESSIONS: '/api/sessions',\n SESSION: (id: string) => `/api/sessions/${id}`,\n SESSION_RESUME: (id: string) => `/api/sessions/${id}/resume`,\n SESSION_ABORT: (id: string) => `/api/sessions/${id}/abort`,\n SESSION_MESSAGES: (id: string) => `/api/sessions/${id}/messages`,\n \n // Chat\n CHAT: (sessionId: string) => `/api/sessions/${sessionId}/chat`,\n CHAT_STREAM: (sessionId: string) => `/api/sessions/${sessionId}/chat/stream`,\n \n // Image upload (pre-upload before sending chat)\n IMAGE_UPLOAD: (sessionId: string) => `/api/sessions/${sessionId}/images/upload`,\n\n // Models\n MODELS: '/api/models',\n\n // Account\n ACCOUNT_AUTH: '/api/account/auth',\n ACCOUNT_QUOTA: '/api/account/quota',\n} as const;\n\n/**\n * API endpoint type definitions\n */\nexport interface ApiEndpoints {\n // GET /api/health\n 'GET /api/health': {\n response: ApiResponse<HealthResponse>;\n };\n \n // GET /api/sessions\n 'GET /api/sessions': {\n response: ApiResponse<PaginatedResponse<Session>>;\n };\n \n // POST /api/sessions\n 'POST /api/sessions': {\n body: CreateSessionRequest;\n response: ApiResponse<Session>;\n };\n \n // GET /api/sessions/:id\n 'GET /api/sessions/:id': {\n params: { id: string };\n response: ApiResponse<Session>;\n };\n \n // PATCH /api/sessions/:id\n 'PATCH /api/sessions/:id': {\n params: { id: string };\n body: UpdateSessionRequest;\n response: ApiResponse<Session>;\n };\n \n // DELETE /api/sessions/:id\n 'DELETE /api/sessions/:id': {\n params: { id: string };\n response: ApiResponse<void>;\n };\n \n // POST /api/sessions/:id/resume\n 'POST /api/sessions/:id/resume': {\n params: { id: string };\n response: ApiResponse<Session>;\n };\n \n // POST /api/sessions/:id/abort\n 'POST /api/sessions/:id/abort': {\n params: { id: string };\n response: ApiResponse<void>;\n };\n \n // GET /api/sessions/:id/messages\n 'GET /api/sessions/:id/messages': {\n params: { id: string };\n response: ApiResponse<PaginatedResponse<Message>>;\n };\n \n // POST /api/sessions/:id/chat\n 'POST /api/sessions/:id/chat': {\n params: { id: string };\n body: SendMessageRequest;\n response: ApiResponse<Message>;\n };\n \n // POST /api/sessions/:id/chat/stream (SSE)\n 'POST /api/sessions/:id/chat/stream': {\n params: { id: string };\n body: SendMessageRequest;\n response: ReadableStream; // SSE stream\n };\n\n // POST /api/sessions/:id/images/upload\n 'POST /api/sessions/:id/images/upload': {\n params: { id: string };\n body: { images: Array<{ id: string; dataUrl: string; mimeType: string; source: string }> };\n response: ApiResponse<{ images: Array<{ id: string; thumbnailUrl: string; fullImageUrl: string; fullImagePath: string }> }>;\n };\n \n // GET /api/models\n 'GET /api/models': {\n response: ApiResponse<{ models: ModelInfo[]; default: string }>;\n };\n\n // GET /api/account/auth\n 'GET /api/account/auth': {\n response: ApiResponse<CopilotAuthStatus>;\n };\n\n // GET /api/account/quota\n 'GET /api/account/quota': {\n response: ApiResponse<CopilotQuotaStatus>;\n };\n}\n\n/**\n * Default configuration values\n */\nexport const DEFAULT_CONFIG = {\n DEFAULT_MODEL: 'gpt-4.1',\n DEFAULT_PORT: 3847,\n DEFAULT_HOST: 'localhost',\n REQUEST_TIMEOUT_MS: 60000,\n STREAM_TIMEOUT_MS: 300000,\n} as const;\n","/**\n * Message type definitions for DevMentorAI\n */\n\nexport type MessageRole = 'user' | 'assistant' | 'system';\n\nexport interface Message {\n id: string;\n sessionId: string;\n role: MessageRole;\n content: string;\n timestamp: string; // ISO date string\n metadata?: MessageMetadata;\n}\n\n// ============================================================================\n// Image Attachment Types\n// ============================================================================\n\nexport type ImageSource = 'screenshot' | 'paste' | 'drop';\nexport type ImageMimeType = 'image/png' | 'image/jpeg' | 'image/webp';\n\nexport interface ImageAttachment {\n id: string;\n source: ImageSource;\n mimeType: ImageMimeType;\n dimensions: { width: number; height: number };\n fileSize: number; // bytes\n timestamp: string; // ISO date string\n /** Original base64 data URL - only present in draft/request, not persisted */\n dataUrl?: string;\n /** Backend-provided thumbnail URL for display in chat history */\n thumbnailUrl?: string;\n /** Backend-provided full image URL for lightbox view */\n fullImageUrl?: string;\n}\n\n/** Image data sent in message request (before processing) */\nexport interface ImagePayload {\n id: string;\n dataUrl: string;\n mimeType: ImageMimeType;\n source: ImageSource;\n}\n\n/** Constants for image handling */\nexport const IMAGE_CONSTANTS = {\n MAX_IMAGES_PER_MESSAGE: 5,\n MAX_IMAGE_SIZE_BYTES: 5 * 1024 * 1024, // 5MB\n SUPPORTED_MIME_TYPES: ['image/png', 'image/jpeg', 'image/webp'] as const,\n THUMBNAIL_MAX_DIMENSION: 200,\n THUMBNAIL_QUALITY: 60,\n} as const;\n\n// ============================================================================\n// Message Metadata\n// ============================================================================\n\nexport interface MessageMetadata {\n pageUrl?: string;\n selectedText?: string;\n action?: QuickAction;\n toolCalls?: ToolCall[];\n streamComplete?: boolean;\n /** Attached images for this message */\n images?: ImageAttachment[];\n /** Whether context-aware mode was used */\n contextAware?: boolean;\n /** Stream or API error encountered during response */\n error?: string;\n}\n\nexport type QuickAction =\n | 'explain'\n | 'translate'\n | 'rewrite'\n | 'fix_grammar'\n | 'summarize'\n | 'expand'\n | 'analyze_config'\n | 'diagnose_error';\n\nexport interface ToolCall {\n toolName: string;\n toolCallId: string;\n status: 'pending' | 'running' | 'completed' | 'error';\n result?: unknown;\n}\n\nexport interface SendMessageRequest {\n prompt: string;\n context?: MessageContext;\n /** Full extracted context payload for context-aware mode */\n fullContext?: unknown; // ContextPayload - imported separately to avoid circular deps\n /** Whether to use context-aware mode */\n useContextAwareMode?: boolean;\n /** Images to attach to this message */\n images?: ImagePayload[];\n}\n\nexport interface MessageContext {\n pageUrl?: string;\n pageTitle?: string;\n selectedText?: string;\n action?: QuickAction;\n}\n\nexport interface StreamEvent {\n type: StreamEventType;\n data: StreamEventData;\n}\n\nexport type StreamEventType =\n | 'message_start'\n | 'message_delta'\n | 'message_complete'\n | 'tool_start'\n | 'tool_complete'\n | 'error'\n | 'done';\n\nexport interface StreamEventData {\n content?: string;\n deltaContent?: string;\n toolName?: string;\n toolCallId?: string;\n error?: string;\n messageId?: string;\n reason?: string; // Reason for completion (e.g., 'completed', 'timeout', 'idle_timeout')\n}\n","/**\n * Context-Aware Mentor Mode Types\n * \n * These types define the structured context payload that is extracted from\n * the browser and sent to the backend for context-aware AI responses.\n */\n\n// ============================================================================\n// Platform Detection Types\n// ============================================================================\n\nexport type PlatformType =\n | 'azure'\n | 'aws'\n | 'gcp'\n | 'github'\n | 'gitlab'\n | 'datadog'\n | 'newrelic'\n | 'grafana'\n | 'jenkins'\n | 'kubernetes'\n | 'docker'\n | 'generic';\n\nexport interface PlatformDetection {\n type: PlatformType;\n confidence: number; // 0-1\n indicators: string[];\n specificProduct?: string; // e.g., \"Azure DevOps\", \"AWS EC2\"\n specificContext?: Record<string, unknown>; // Phase 2: Platform-specific context\n}\n\n// ============================================================================\n// Error Detection Types\n// ============================================================================\n\nexport type ErrorType = 'error' | 'warning' | 'info';\nexport type ErrorSeverity = 'critical' | 'high' | 'medium' | 'low';\nexport type ErrorSource = 'console' | 'ui' | 'network' | 'dom';\n\nexport interface ExtractedError {\n type: ErrorType;\n message: string;\n source?: ErrorSource;\n severity: ErrorSeverity;\n context?: string; // Surrounding text\n element?: HTMLElementSnapshot;\n stackTrace?: string;\n}\n\nexport interface HTMLElementSnapshot {\n tagName: string;\n id?: string;\n className?: string;\n textContent?: string;\n attributes: Record<string, string>;\n}\n\n// ============================================================================\n// HTML Structure Types\n// ============================================================================\n\nexport type SectionPurpose =\n | 'error-container'\n | 'alert'\n | 'panel'\n | 'table'\n | 'form'\n | 'code-block'\n | 'modal'\n | 'generic';\n\nexport interface HTMLSection {\n purpose: SectionPurpose;\n outerHTML: string; // Truncated to ~500 chars\n textContent: string;\n attributes: Record<string, string>;\n xpath?: string;\n}\n\n// ============================================================================\n// Text Extraction Types\n// ============================================================================\n\nexport interface Heading {\n level: 1 | 2 | 3;\n text: string;\n xpath?: string;\n hierarchy?: string; // Parent > Child hierarchy\n}\n\nexport interface ConsoleLogs {\n errors: string[];\n warnings: string[];\n included: boolean;\n truncated: boolean;\n}\n\n// Phase 2: Console log capture type\nexport interface CapturedConsoleLog {\n level: 'log' | 'warn' | 'error' | 'info' | 'debug';\n message: string;\n timestamp: string;\n stackTrace?: string;\n}\n\n// Phase 2: Network error capture type\nexport interface CapturedNetworkError {\n url: string;\n method: string;\n status?: number;\n statusText?: string;\n errorMessage?: string;\n timestamp: string;\n}\n\nexport interface TextExtractionMetadata {\n totalLength: number;\n truncated: boolean;\n truncationReason?: string;\n}\n\n// ============================================================================\n// User Intent Types (DEPRECATED in Phase 3 - kept for backward compatibility)\n// ============================================================================\n\n/**\n * @deprecated Phase 3: The agent (LLM) now determines intent autonomously.\n * This type is kept for backward compatibility but fields are optional.\n */\nexport type IntentType = 'debug' | 'understand' | 'mentor' | 'help' | 'explain' | 'guide';\n\n/**\n * @deprecated Phase 3: Intent is no longer computed by the extension.\n * The agent interprets user intent directly from their message.\n */\nexport interface UserIntent {\n primary: IntentType;\n keywords: string[];\n explicitGoal?: string;\n implicitSignals: string[];\n}\n\n// ============================================================================\n// Session Context Types\n// ============================================================================\n\n/** Session type specifically for context-aware mode (extends base SessionType) */\nexport type ContextSessionType = 'devops' | 'debugging' | 'frontend' | 'general' | 'writing';\n\nexport interface ContextMessage {\n role: 'user' | 'assistant';\n content: string;\n timestamp: string;\n}\n\n// ============================================================================\n// Visual Context Types (Phase 3)\n// ============================================================================\n\nexport interface ScreenshotData {\n dataUrl: string; // base64 encoded image\n format: 'png' | 'jpeg';\n dimensions: { width: number; height: number };\n fileSize: number; // bytes\n quality?: number; // 0-100 for JPEG\n}\n\nexport interface VisualContext {\n screenshot?: ScreenshotData;\n supported: boolean; // Model supports vision\n included: boolean; // Screenshot actually captured\n reason?: string; // Why included/excluded\n}\n\n// ============================================================================\n// Privacy Types\n// ============================================================================\n\nexport interface PrivacyInfo {\n redactedFields: string[];\n sensitiveDataDetected: boolean;\n consentGiven: boolean;\n dataRetention: 'session' | 'none';\n privacyMaskingApplied?: boolean; // Whether privacy masking was applied\n sensitiveDataTypes?: string[]; // Types of data that were masked (e.g., 'email', 'token')\n}\n\n// ============================================================================\n// Main Context Payload\n// ============================================================================\n\nexport interface ContextPayload {\n // Core Metadata\n metadata: {\n captureTimestamp: string; // ISO 8601\n captureMode: 'auto' | 'manual';\n browserInfo: {\n userAgent: string;\n viewport: { width: number; height: number };\n language: string;\n };\n // Phase 2: Extended metadata\n phase2Features?: {\n consoleLogs: number;\n networkErrors: number;\n codeBlocks: number;\n tables: number;\n forms: number;\n hasModal: boolean;\n };\n // Extended features from context-extractor\n extendedFeatures?: {\n consoleLogs: number;\n networkErrors: number;\n runtimeErrors: number;\n codeBlocks: number;\n tables: number;\n forms: number;\n hasModal: boolean;\n uiState: string;\n privacyMaskingApplied: boolean;\n sensitiveDataTypesFound: string[];\n };\n };\n\n // Page Identity\n page: {\n url: string;\n urlParsed: {\n protocol: string;\n hostname: string;\n pathname: string;\n search: string;\n hash: string;\n };\n title: string;\n favicon?: string;\n platform: PlatformDetection;\n uiState?: Record<string, unknown>;\n };\n\n // Textual Context\n text: {\n selectedText?: string;\n visibleText: string;\n headings: Heading[];\n errors: ExtractedError[];\n logs?: ConsoleLogs;\n // Phase 2: Console and network error capture\n consoleLogs?: CapturedConsoleLog[];\n networkErrors?: CapturedNetworkError[];\n runtimeErrors?: Array<{\n message: string;\n source?: string;\n lineno?: number;\n colno?: number;\n stack?: string;\n timestamp: string;\n type?: string;\n }>;\n metadata: TextExtractionMetadata;\n };\n\n // Structural HTML Context\n structure: {\n relevantSections: HTMLSection[];\n errorContainers: HTMLSection[];\n // Phase 2: Extended structure extraction\n codeBlocks?: HTMLSection[];\n tables?: HTMLSection[];\n forms?: HTMLSection[];\n modal?: HTMLSection;\n activeElements: {\n focusedElement?: HTMLElementSnapshot;\n activeModals?: HTMLSection[];\n activePanels?: HTMLSection[];\n };\n metadata: {\n totalNodes: number;\n extractedNodes: number;\n relevanceScore: number; // 0-1 confidence\n };\n };\n\n // Visual Context (Optional - Phase 3)\n visual?: VisualContext;\n\n // Session Context\n session: {\n sessionId: string;\n sessionType: ContextSessionType;\n intent: UserIntent;\n previousMessages: {\n count: number;\n lastN: ContextMessage[];\n };\n userGoal?: string;\n };\n\n // Privacy & Safety\n privacy: PrivacyInfo;\n}\n\n// ============================================================================\n// Extraction Request/Response Types\n// ============================================================================\n\nexport interface ContextExtractionRequest {\n includeScreenshot?: boolean;\n maxTextLength?: number;\n maxHTMLSections?: number;\n selectedTextOnly?: boolean;\n}\n\nexport interface ContextExtractionResponse {\n success: boolean;\n context?: ContextPayload;\n error?: string;\n extractionTimeMs: number;\n}\n\n// ============================================================================\n// Size Limits Configuration\n// ============================================================================\n\nexport const CONTEXT_SIZE_LIMITS = {\n visibleText: 10000, // chars\n htmlSection: 500, // chars per section\n totalHTML: 5000, // chars total HTML\n headings: 50, // max headings\n errors: 20, // max errors\n consoleLogs: 100, // lines\n screenshot: 1024 * 1024, // 1MB\n selectedText: 5000, // chars\n} as const;\n","/**\n * Session type configurations with pre-defined agents\n */\n\nimport type { SessionType } from '../types/session.js';\n\nexport interface AgentConfig {\n name: string;\n displayName: string;\n description: string;\n prompt: string;\n}\n\nexport interface SessionTypeConfig {\n name: string;\n description: string;\n icon: string;\n agent: AgentConfig | null;\n defaultModel: string;\n}\n\nexport const SESSION_TYPE_CONFIGS: Record<SessionType, SessionTypeConfig> = {\n devops: {\n name: 'DevOps Mentor',\n description: 'Expert in DevOps, cloud infrastructure, and best practices',\n icon: '🛠️',\n defaultModel: 'gpt-4.1',\n agent: {\n name: 'devops-mentor',\n displayName: 'DevOps Mentor',\n description: 'Expert in DevOps, cloud infrastructure, and best practices',\n prompt: `You are a DevOps mentor and expert. You help users with:\n- AWS, Azure, GCP cloud services and best practices\n- Kubernetes, Docker, and container orchestration\n- CI/CD pipelines (GitHub Actions, GitLab CI, Jenkins, CircleCI)\n- Infrastructure as Code (Terraform, Pulumi, CloudFormation, Ansible)\n- Security best practices and compliance\n- Cost optimization and resource management\n- Error diagnosis, log analysis, and troubleshooting\n- Architecture design and scalability patterns\n\nIMPORTANT TOOL CAPABILITY: You have a tool called \"fetch_url\" available. Whenever a user provides a public URL in their request, you SHOULD PROACTIVELY use the fetch_url tool to read its contents and provide a more accurate, contextual response based on the actual content of that page. Do not assume you cannot read URLs - you have a tool specifically for this purpose.\n\nWhen analyzing configurations or errors:\n1. Identify the issue or configuration clearly\n2. Explain WHY something is problematic or recommended\n3. Provide actionable steps to fix or improve\n4. Reference official documentation when helpful\n5. Warn about security implications when relevant\n\nProvide a high-quality, detailed, and explanatory response that helps the user completely understand the concepts, steps, and 'why' behind the suggestions. Use code blocks for configurations and commands.`,\n },\n },\n \n writing: {\n name: 'Writing Assistant',\n description: 'Helps with writing, rewriting, and translation',\n icon: '✍️',\n defaultModel: 'gpt-4.1',\n agent: {\n name: 'writing-assistant',\n displayName: 'Writing Assistant',\n description: 'Helps with writing, rewriting, and translation',\n prompt: `You are a professional writing assistant. You help users with:\n- Writing and composing emails (formal, casual, technical)\n- Rewriting text with different tones and styles\n- Grammar, spelling, and clarity improvements\n- Translation between languages (preserving tone and meaning)\n- Summarization and expansion of content\n- Technical documentation writing\n- Business communication and proposals\n\nGuidelines:\n1. Maintain the original meaning and intent\n2. Match the requested tone (formal, casual, friendly, technical)\n3. Preserve formatting when rewriting\n4. For translations, keep cultural nuances in mind\n5. Provide alternatives when helpful\n6. Provide rich, detailed explanations and fully expand on ideas when discussing or explaining writing choices.\n\nWhen the user provides text to modify, respond with ONLY the modified text unless they ask for explanation.`,\n },\n },\n \n development: {\n name: 'Development Helper',\n description: 'Assists with code review, debugging, and best practices',\n icon: '💻',\n defaultModel: 'gpt-4.1',\n agent: {\n name: 'dev-helper',\n displayName: 'Development Helper',\n description: 'Assists with code review, debugging, and best practices',\n prompt: `You are a senior software development assistant. You help users with:\n- Code review and improvement suggestions\n- Bug diagnosis and debugging strategies\n- Architecture decisions and design patterns\n- Performance optimization\n- Testing strategies and test writing\n- Documentation and code comments\n- Refactoring and code cleanup\n\nIMPORTANT TOOL CAPABILITY: You have tools available to assist you. If you are in a devops session type, you have a tool called \"fetch_url\". Whenever a user provides a public URL in their request and you have the tool available, you SHOULD PROACTIVELY use the fetch_url tool to read its contents and provide a more accurate, contextual response. Do not assume you cannot read URLs.\n\nGuidelines:\n1. Provide actionable and detailed explanatory responses that help the user completely understand the concepts, steps, and 'why' behind the suggestions.\n2. Explain the \"why\" behind suggestions\n3. Provide code examples when helpful\n4. Consider edge cases and error handling\n5. Suggest tests for critical changes\n6. Reference best practices and patterns\n\nWhen reviewing code, focus on:\n- Correctness and logic errors\n- Security vulnerabilities\n- Performance issues\n- Maintainability and readability\n- Missing error handling`,\n },\n },\n \n general: {\n name: 'General Assistant',\n description: 'General-purpose AI assistant',\n icon: '💬',\n defaultModel: 'gpt-4.1',\n agent: null, // Uses default Copilot behavior\n },\n};\n\n/**\n * Get the agent config for a session type\n */\nexport function getAgentConfig(type: SessionType): AgentConfig | null {\n return SESSION_TYPE_CONFIGS[type]?.agent ?? null;\n}\n\n/**\n * Get the default model for a session type\n */\nexport function getDefaultModel(type: SessionType): string {\n return SESSION_TYPE_CONFIGS[type]?.defaultModel ?? 'gpt-4.1';\n}\n","/**\n * Utility functions for DevMentorAI\n */\n\n/**\n * Generate a unique ID\n */\nexport function generateId(prefix?: string): string {\n const timestamp = Date.now().toString(36);\n const random = Math.random().toString(36).substring(2, 9);\n return prefix ? `${prefix}_${timestamp}${random}` : `${timestamp}${random}`;\n}\n\n/**\n * Generate a session ID\n */\nexport function generateSessionId(): string {\n return generateId('session');\n}\n\n/**\n * Generate a message ID\n */\nexport function generateMessageId(): string {\n return generateId('msg');\n}\n\n/**\n * Format a date as ISO string\n */\nexport function formatDate(date: Date = new Date()): string {\n return date.toISOString();\n}\n\n/**\n * Parse an ISO date string to Date\n */\nexport function parseDate(dateString: string): Date {\n return new Date(dateString);\n}\n\n/**\n * Truncate text to a maximum length\n */\nexport function truncate(text: string, maxLength: number, suffix = '...'): string {\n if (text.length <= maxLength) return text;\n return text.slice(0, maxLength - suffix.length) + suffix;\n}\n\n/**\n * Sleep for a given number of milliseconds\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * Retry a function with exponential backoff\n */\nexport async function retry<T>(\n fn: () => Promise<T>,\n options: {\n maxAttempts?: number;\n initialDelay?: number;\n maxDelay?: number;\n backoffFactor?: number;\n } = {}\n): Promise<T> {\n const {\n maxAttempts = 3,\n initialDelay = 1000,\n maxDelay = 30000,\n backoffFactor = 2,\n } = options;\n \n let lastError: Error | undefined;\n let delay = initialDelay;\n \n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n \n if (attempt === maxAttempts) break;\n \n await sleep(delay);\n delay = Math.min(delay * backoffFactor, maxDelay);\n }\n }\n \n throw lastError;\n}\n\n/**\n * Create an AbortController with timeout\n */\nexport function createTimeoutController(timeoutMs: number): AbortController {\n const controller = new AbortController();\n setTimeout(() => controller.abort(), timeoutMs);\n return controller;\n}\n","/**\n * Update checker for DevMentorAI components.\n * Queries GitHub Releases API to detect new versions.\n */\n\nexport type ComponentType = 'backend' | 'extension';\n\nexport interface UpdateInfo {\n hasUpdate: boolean;\n currentVersion: string;\n latestVersion: string;\n releaseUrl: string;\n downloadUrls: { chrome?: string; firefox?: string; npm?: string };\n publishedAt: string;\n}\n\ninterface GitHubRelease {\n tag_name: string;\n html_url: string;\n published_at: string;\n draft: boolean;\n prerelease: boolean;\n assets: Array<{\n name: string;\n browser_download_url: string;\n }>;\n}\n\nconst GITHUB_OWNER = 'BOTOOM';\nconst GITHUB_REPO = 'devmentorai';\nconst TAG_PREFIXES: Record<ComponentType, string> = {\n backend: 'backend-v',\n extension: 'ext-v',\n};\n\nconst CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour\n\ninterface CacheEntry {\n data: UpdateInfo;\n timestamp: number;\n}\n\nconst cache = new Map<string, CacheEntry>();\n\n/**\n * Compare two semver strings. Returns:\n * 1 if a > b, -1 if a < b, 0 if equal\n */\nexport function compareSemver(a: string, b: string): number {\n const pa = a.split('.').map(Number);\n const pb = b.split('.').map(Number);\n for (let i = 0; i < 3; i++) {\n const na = pa[i] ?? 0;\n const nb = pb[i] ?? 0;\n if (na > nb) return 1;\n if (na < nb) return -1;\n }\n return 0;\n}\n\n/**\n * Check if an update is available for a given component.\n * Uses GitHub Releases API with a 1-hour cache.\n *\n * @param component - 'backend' or 'extension'\n * @param currentVersion - Current version string (e.g. '1.0.0')\n * @param fetchFn - Optional fetch function (for environments where global fetch differs)\n */\nexport async function checkForUpdate(\n component: ComponentType,\n currentVersion: string,\n fetchFn: typeof fetch = globalThis.fetch,\n): Promise<UpdateInfo> {\n const cacheKey = `${component}:${currentVersion}`;\n const cached = cache.get(cacheKey);\n if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {\n return cached.data;\n }\n\n const noUpdate: UpdateInfo = {\n hasUpdate: false,\n currentVersion,\n latestVersion: currentVersion,\n releaseUrl: `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases`,\n downloadUrls: {},\n publishedAt: '',\n };\n\n try {\n const prefix = TAG_PREFIXES[component];\n const response = await fetchFn(\n `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases?per_page=10`,\n {\n headers: { Accept: 'application/vnd.github.v3+json' },\n signal: AbortSignal.timeout(5000),\n },\n );\n\n if (!response.ok) {\n cache.set(cacheKey, { data: noUpdate, timestamp: Date.now() });\n return noUpdate;\n }\n\n const releases = (await response.json()) as GitHubRelease[];\n\n // Find the latest non-draft, non-prerelease release matching our tag prefix\n const matching = releases.find(\n (r) => r.tag_name.startsWith(prefix) && !r.draft && !r.prerelease,\n );\n\n if (!matching) {\n cache.set(cacheKey, { data: noUpdate, timestamp: Date.now() });\n return noUpdate;\n }\n\n const latestVersion = matching.tag_name.replace(prefix, '');\n const hasUpdate = compareSemver(latestVersion, currentVersion) > 0;\n\n const downloadUrls: UpdateInfo['downloadUrls'] = {};\n for (const asset of matching.assets) {\n if (asset.name.includes('chrome')) {\n downloadUrls.chrome = asset.browser_download_url;\n } else if (asset.name.includes('firefox')) {\n downloadUrls.firefox = asset.browser_download_url;\n }\n }\n\n if (component === 'backend') {\n downloadUrls.npm = `https://www.npmjs.com/package/devmentorai-server`;\n }\n\n const result: UpdateInfo = {\n hasUpdate,\n currentVersion,\n latestVersion,\n releaseUrl: matching.html_url,\n downloadUrls,\n publishedAt: matching.published_at,\n };\n\n cache.set(cacheKey, { data: result, timestamp: Date.now() });\n return result;\n } catch {\n cache.set(cacheKey, { data: noUpdate, timestamp: Date.now() });\n return noUpdate;\n }\n}\n\n/**\n * Clear the update check cache (useful for testing or forced refresh).\n */\nexport function clearUpdateCache(): void {\n cache.clear();\n}\n","/**\n * Cross-Platform Path Utilities\n * \n * Provides consistent path handling across Windows, macOS, and Linux.\n * Uses the same pattern as db/index.ts for data directory location.\n */\n\nimport path from 'node:path';\nimport os from 'node:os';\nimport fs from 'node:fs';\n\n/** Base data directory: ~/.devmentorai */\nexport const DATA_DIR = path.join(os.homedir(), '.devmentorai');\n\n/** Images directory: ~/.devmentorai/images */\nexport const IMAGES_DIR = path.join(DATA_DIR, 'images');\n\n/** Logs directory: ~/.devmentorai/logs */\nexport const LOG_DIR = path.join(DATA_DIR, 'logs');\n\n/** Server log file: ~/.devmentorai/logs/server.log */\nexport const LOG_FILE = path.join(LOG_DIR, 'server.log');\n\n/** PID file: ~/.devmentorai/server.pid */\nexport const PID_FILE = path.join(DATA_DIR, 'server.pid');\n\n/** Config file: ~/.devmentorai/config.json */\nexport const CONFIG_FILE = path.join(DATA_DIR, 'config.json');\n\n/**\n * Ensure a directory exists, creating it if necessary\n */\nexport function ensureDir(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n fs.mkdirSync(dirPath, { recursive: true });\n }\n}\n\n/**\n * Get the directory path for a session's images\n * @param sessionId - The session ID\n * @returns Absolute path to the session's images directory\n */\nexport function getSessionImagesDir(sessionId: string): string {\n return path.join(IMAGES_DIR, sessionId);\n}\n\n/**\n * Get the directory path for a message's images\n * @param sessionId - The session ID\n * @param messageId - The message ID\n * @returns Absolute path to the message's images directory\n */\nexport function getMessageImagesDir(sessionId: string, messageId: string): string {\n return path.join(IMAGES_DIR, sessionId, messageId);\n}\n\n/**\n * Get the file path for a thumbnail image\n * @param sessionId - The session ID\n * @param messageId - The message ID\n * @param index - The image index (0-based)\n * @returns Absolute path to the thumbnail file\n */\nexport function getThumbnailPath(sessionId: string, messageId: string, index: number): string {\n return path.join(getMessageImagesDir(sessionId, messageId), `thumb_${index}.jpg`);\n}\n\n/**\n * Convert an absolute path to a relative path from DATA_DIR\n * This is used for storing portable paths in the database\n * @param absolutePath - The absolute file path\n * @returns Relative path from DATA_DIR\n */\nexport function toRelativePath(absolutePath: string): string {\n return path.relative(DATA_DIR, absolutePath);\n}\n\n/**\n * Convert an absolute image path to a relative path from IMAGES_DIR\n * This is used for constructing image URLs\n * @param absolutePath - The absolute file path\n * @returns Relative path from IMAGES_DIR (e.g., \"session_xxx/msg_xxx/thumb_0.jpg\")\n */\nexport function toImageRelativePath(absolutePath: string): string {\n return path.relative(IMAGES_DIR, absolutePath);\n}\n\n/**\n * Convert a relative path (from DB) to an absolute path\n * @param relativePath - The relative path stored in DB\n * @returns Absolute file path\n */\nexport function toAbsolutePath(relativePath: string): string {\n return path.join(DATA_DIR, relativePath);\n}\n\n/**\n * Convert a file system path to a URL-safe path (always forward slashes)\n * @param fsPath - The file system path\n * @returns URL-safe path with forward slashes\n */\nexport function toUrlPath(fsPath: string): string {\n return fsPath.split(path.sep).join('/');\n}\n\n/**\n * Delete a directory and all its contents\n * @param dirPath - The directory to delete\n */\nexport function deleteDir(dirPath: string): void {\n if (fs.existsSync(dirPath)) {\n fs.rmSync(dirPath, { recursive: true, force: true });\n }\n}\n\n/**\n * Check if a file exists\n * @param filePath - The file path to check\n * @returns True if the file exists\n */\nexport function fileExists(filePath: string): boolean {\n return fs.existsSync(filePath);\n}\n\n/**\n * Get file stats if the file exists\n * @param filePath - The file path\n * @returns File stats or null if file doesn't exist\n */\nexport function getFileStats(filePath: string): fs.Stats | null {\n try {\n return fs.statSync(filePath);\n } catch {\n return null;\n }\n}\n\n// Initialize directories on module load\nensureDir(IMAGES_DIR);\nensureDir(LOG_DIR);\n","/**\n * Backend version - Single source of truth\n * This file is auto-updated by semantic-release\n */\nexport const BACKEND_VERSION = '1.3.0';\n"],"mappings":"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","names":[]}
|
package/dist/cli.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
toImageRelativePath,
|
|
19
19
|
toRelativePath,
|
|
20
20
|
toUrlPath
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-X4NL4LWR.js";
|
|
22
22
|
|
|
23
23
|
// src/server.ts
|
|
24
24
|
import Fastify from "fastify";
|
|
@@ -66,6 +66,11 @@ var THUMBNAIL_CONFIG = {
|
|
|
66
66
|
quality: 60,
|
|
67
67
|
format: "jpeg"
|
|
68
68
|
};
|
|
69
|
+
var FULL_IMAGE_CONFIG = {
|
|
70
|
+
maxDimension: 2e3,
|
|
71
|
+
quality: 80,
|
|
72
|
+
format: "jpeg"
|
|
73
|
+
};
|
|
69
74
|
function parseDataUrl(dataUrl) {
|
|
70
75
|
const match = dataUrl.match(/^data:(image\/[^;]+);base64,(.+)$/);
|
|
71
76
|
if (!match) return null;
|
|
@@ -86,14 +91,11 @@ async function generateThumbnail(buffer) {
|
|
|
86
91
|
withoutEnlargement: true
|
|
87
92
|
}).jpeg({ quality: THUMBNAIL_CONFIG.quality }).toBuffer();
|
|
88
93
|
}
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"image/gif": "gif"
|
|
95
|
-
};
|
|
96
|
-
return extensions[mimeType] || "jpg";
|
|
94
|
+
async function generateFullImage(buffer) {
|
|
95
|
+
return sharp(buffer).resize(FULL_IMAGE_CONFIG.maxDimension, FULL_IMAGE_CONFIG.maxDimension, {
|
|
96
|
+
fit: "inside",
|
|
97
|
+
withoutEnlargement: true
|
|
98
|
+
}).jpeg({ quality: FULL_IMAGE_CONFIG.quality }).toBuffer();
|
|
97
99
|
}
|
|
98
100
|
function getFullImagePath(sessionId, messageId, index, extension) {
|
|
99
101
|
const messageDir = getMessageImagesDir(sessionId, messageId);
|
|
@@ -117,10 +119,11 @@ async function processMessageImages(sessionId, messageId, images, backendUrl) {
|
|
|
117
119
|
const thumbnailBuffer = await generateThumbnail(buffer);
|
|
118
120
|
const thumbnailAbsPath = getThumbnailPath(sessionId, messageId, index);
|
|
119
121
|
fs.writeFileSync(thumbnailAbsPath, thumbnailBuffer);
|
|
120
|
-
const
|
|
122
|
+
const fullImageBuffer = await generateFullImage(buffer);
|
|
123
|
+
const extension = "jpg";
|
|
121
124
|
const fullImageAbsPath = getFullImagePath(sessionId, messageId, index, extension);
|
|
122
|
-
fs.writeFileSync(fullImageAbsPath,
|
|
123
|
-
console.log(`[ThumbnailService] Saved full image to ${fullImageAbsPath}`);
|
|
125
|
+
fs.writeFileSync(fullImageAbsPath, fullImageBuffer);
|
|
126
|
+
console.log(`[ThumbnailService] Saved compressed full image to ${fullImageAbsPath}`);
|
|
124
127
|
const thumbnailRelativePath = toRelativePath(thumbnailAbsPath);
|
|
125
128
|
const thumbnailUrlPath = toUrlPath(toImageRelativePath(thumbnailAbsPath));
|
|
126
129
|
const fullImageUrlPath = toUrlPath(toImageRelativePath(fullImageAbsPath));
|
|
@@ -852,12 +855,23 @@ var imagePayloadSchema = z2.object({
|
|
|
852
855
|
mimeType: z2.enum(["image/png", "image/jpeg", "image/webp"]),
|
|
853
856
|
source: z2.enum(["screenshot", "paste", "drop"])
|
|
854
857
|
});
|
|
858
|
+
var preUploadedImageSchema = z2.object({
|
|
859
|
+
id: z2.string(),
|
|
860
|
+
fullImagePath: z2.string(),
|
|
861
|
+
mimeType: z2.string(),
|
|
862
|
+
thumbnailUrl: z2.string().optional(),
|
|
863
|
+
fullImageUrl: z2.string().optional(),
|
|
864
|
+
dimensions: z2.object({ width: z2.number(), height: z2.number() }).optional(),
|
|
865
|
+
fileSize: z2.number().optional()
|
|
866
|
+
});
|
|
855
867
|
var sendMessageSchema = z2.object({
|
|
856
868
|
prompt: z2.string().min(1),
|
|
857
869
|
context: simpleContextSchema.optional(),
|
|
858
870
|
fullContext: fullContextSchema.optional(),
|
|
859
871
|
useContextAwareMode: z2.boolean().optional(),
|
|
860
|
-
images: z2.array(imagePayloadSchema).max(5).optional()
|
|
872
|
+
images: z2.array(imagePayloadSchema).max(5).optional(),
|
|
873
|
+
/** Pre-uploaded image references (already processed on disk) */
|
|
874
|
+
preUploadedImages: z2.array(preUploadedImageSchema).max(5).optional()
|
|
861
875
|
});
|
|
862
876
|
async function chatRoutes(fastify) {
|
|
863
877
|
const buildPrompt = (body) => {
|
|
@@ -964,6 +978,7 @@ async function chatRoutes(fastify) {
|
|
|
964
978
|
console.log(`[ChatRoute] Using ${promptType} prompt for streaming`);
|
|
965
979
|
let processedImagesRaw = [];
|
|
966
980
|
let processedImages = [];
|
|
981
|
+
let copilotAttachments = [];
|
|
967
982
|
const host = request.headers.host || `${request.hostname}:3847`;
|
|
968
983
|
const backendUrl = `http://${host}`;
|
|
969
984
|
const userMessage = fastify.sessionService.addMessage(
|
|
@@ -977,9 +992,30 @@ async function chatRoutes(fastify) {
|
|
|
977
992
|
contextAware: body.useContextAwareMode
|
|
978
993
|
} : { contextAware: body.useContextAwareMode }
|
|
979
994
|
);
|
|
980
|
-
if (body.
|
|
995
|
+
if (body.preUploadedImages && body.preUploadedImages.length > 0) {
|
|
996
|
+
console.log(`[ChatRoute] Using ${body.preUploadedImages.length} pre-uploaded images`);
|
|
997
|
+
copilotAttachments = body.preUploadedImages.map((img, index) => ({
|
|
998
|
+
type: "file",
|
|
999
|
+
path: img.fullImagePath,
|
|
1000
|
+
displayName: `image_${index + 1}.${(img.mimeType || "image/jpeg").split("/")[1] || "jpg"}`
|
|
1001
|
+
}));
|
|
1002
|
+
processedImages = body.preUploadedImages.map((img) => ({
|
|
1003
|
+
id: img.id,
|
|
1004
|
+
source: "screenshot",
|
|
1005
|
+
mimeType: img.mimeType || "image/jpeg",
|
|
1006
|
+
dimensions: img.dimensions || { width: 0, height: 0 },
|
|
1007
|
+
fileSize: img.fileSize || 0,
|
|
1008
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1009
|
+
thumbnailUrl: img.thumbnailUrl,
|
|
1010
|
+
fullImageUrl: img.fullImageUrl
|
|
1011
|
+
}));
|
|
1012
|
+
fastify.sessionService.updateMessageMetadata(userMessage.id, {
|
|
1013
|
+
...userMessage.metadata,
|
|
1014
|
+
images: processedImages
|
|
1015
|
+
});
|
|
1016
|
+
} else if (body.images && body.images.length > 0) {
|
|
981
1017
|
try {
|
|
982
|
-
console.log(`[ChatRoute] Processing ${body.images.length} images for message ${userMessage.id}`);
|
|
1018
|
+
console.log(`[ChatRoute] Processing ${body.images.length} inline images for message ${userMessage.id}`);
|
|
983
1019
|
processedImagesRaw = await processMessageImages(
|
|
984
1020
|
sessionId,
|
|
985
1021
|
userMessage.id,
|
|
@@ -991,16 +1027,16 @@ async function chatRoutes(fastify) {
|
|
|
991
1027
|
...userMessage.metadata,
|
|
992
1028
|
images: processedImages
|
|
993
1029
|
});
|
|
1030
|
+
copilotAttachments = processedImagesRaw.map((img, index) => ({
|
|
1031
|
+
type: "file",
|
|
1032
|
+
path: img.fullImagePath,
|
|
1033
|
+
displayName: `image_${index + 1}.${img.mimeType.split("/")[1] || "jpg"}`
|
|
1034
|
+
}));
|
|
994
1035
|
console.log(`[ChatRoute] Processed ${processedImages.length} images successfully`);
|
|
995
1036
|
} catch (err) {
|
|
996
1037
|
console.error("[ChatRoute] Failed to process images:", err);
|
|
997
1038
|
}
|
|
998
1039
|
}
|
|
999
|
-
const copilotAttachments = processedImagesRaw.map((img, index) => ({
|
|
1000
|
-
type: "file",
|
|
1001
|
-
path: img.fullImagePath,
|
|
1002
|
-
displayName: `image_${index + 1}.${img.mimeType.split("/")[1] || "jpg"}`
|
|
1003
|
-
}));
|
|
1004
1040
|
if (copilotAttachments.length > 0) {
|
|
1005
1041
|
console.log(`[ChatRoute] Built ${copilotAttachments.length} attachments for Copilot:`, copilotAttachments);
|
|
1006
1042
|
}
|
|
@@ -1129,6 +1165,12 @@ async function chatRoutes(fastify) {
|
|
|
1129
1165
|
console.error("[ChatRoute] Failed to start Copilot stream:", streamError);
|
|
1130
1166
|
cleanupTimers();
|
|
1131
1167
|
if (!streamEnded) {
|
|
1168
|
+
if (!reply.raw.headersSent) {
|
|
1169
|
+
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
1170
|
+
reply.raw.setHeader("Cache-Control", "no-cache");
|
|
1171
|
+
reply.raw.setHeader("Connection", "keep-alive");
|
|
1172
|
+
reply.raw.setHeader("Access-Control-Allow-Origin", "*");
|
|
1173
|
+
}
|
|
1132
1174
|
sendSSE({
|
|
1133
1175
|
type: "error",
|
|
1134
1176
|
data: {
|
|
@@ -1162,6 +1204,12 @@ async function chatRoutes(fastify) {
|
|
|
1162
1204
|
}
|
|
1163
1205
|
});
|
|
1164
1206
|
}
|
|
1207
|
+
if (!reply.raw.headersSent) {
|
|
1208
|
+
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
1209
|
+
reply.raw.setHeader("Cache-Control", "no-cache");
|
|
1210
|
+
reply.raw.setHeader("Connection", "keep-alive");
|
|
1211
|
+
reply.raw.setHeader("Access-Control-Allow-Origin", "*");
|
|
1212
|
+
}
|
|
1165
1213
|
const errorEvent = {
|
|
1166
1214
|
type: "error",
|
|
1167
1215
|
data: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
@@ -1338,6 +1386,7 @@ async function accountRoutes(fastify) {
|
|
|
1338
1386
|
}
|
|
1339
1387
|
|
|
1340
1388
|
// src/routes/images.ts
|
|
1389
|
+
import { z as z3 } from "zod";
|
|
1341
1390
|
import fs2 from "fs";
|
|
1342
1391
|
import path2 from "path";
|
|
1343
1392
|
var MIME_TYPES = {
|
|
@@ -1347,7 +1396,68 @@ var MIME_TYPES = {
|
|
|
1347
1396
|
"webp": "image/webp",
|
|
1348
1397
|
"gif": "image/gif"
|
|
1349
1398
|
};
|
|
1399
|
+
var uploadImageSchema = z3.object({
|
|
1400
|
+
id: z3.string(),
|
|
1401
|
+
dataUrl: z3.string(),
|
|
1402
|
+
mimeType: z3.enum(["image/png", "image/jpeg", "image/webp"]),
|
|
1403
|
+
source: z3.enum(["screenshot", "paste", "drop"])
|
|
1404
|
+
});
|
|
1405
|
+
var uploadBodySchema = z3.object({
|
|
1406
|
+
images: z3.array(uploadImageSchema).min(1).max(5)
|
|
1407
|
+
});
|
|
1350
1408
|
async function imagesRoutes(fastify) {
|
|
1409
|
+
fastify.post(
|
|
1410
|
+
"/upload/:sessionId/:messageId",
|
|
1411
|
+
async (request, reply) => {
|
|
1412
|
+
try {
|
|
1413
|
+
const { sessionId, messageId } = request.params;
|
|
1414
|
+
const body = uploadBodySchema.parse(request.body);
|
|
1415
|
+
const host = request.headers.host || `${request.hostname}:3847`;
|
|
1416
|
+
const backendUrl = `http://${host}`;
|
|
1417
|
+
console.log(`[ImagesRoute] Pre-uploading ${body.images.length} images for ${sessionId}/${messageId}`);
|
|
1418
|
+
const processedImagesRaw = await processMessageImages(
|
|
1419
|
+
sessionId,
|
|
1420
|
+
messageId,
|
|
1421
|
+
body.images,
|
|
1422
|
+
backendUrl
|
|
1423
|
+
);
|
|
1424
|
+
const processedImages = toImageAttachments(processedImagesRaw);
|
|
1425
|
+
const responseImages = processedImagesRaw.map((img, i) => ({
|
|
1426
|
+
id: img.id,
|
|
1427
|
+
thumbnailUrl: img.thumbnailUrl,
|
|
1428
|
+
fullImageUrl: img.fullImageUrl,
|
|
1429
|
+
fullImagePath: img.fullImagePath,
|
|
1430
|
+
mimeType: img.mimeType,
|
|
1431
|
+
dimensions: img.dimensions,
|
|
1432
|
+
fileSize: img.fileSize
|
|
1433
|
+
}));
|
|
1434
|
+
console.log(`[ImagesRoute] Pre-upload complete: ${responseImages.length} images processed`);
|
|
1435
|
+
return reply.send({
|
|
1436
|
+
success: true,
|
|
1437
|
+
data: { images: responseImages }
|
|
1438
|
+
});
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
if (error instanceof z3.ZodError) {
|
|
1441
|
+
return reply.code(400).send({
|
|
1442
|
+
success: false,
|
|
1443
|
+
error: {
|
|
1444
|
+
code: "VALIDATION_ERROR",
|
|
1445
|
+
message: "Invalid upload body",
|
|
1446
|
+
details: error.errors
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
console.error("[ImagesRoute] Upload failed:", error);
|
|
1451
|
+
return reply.code(500).send({
|
|
1452
|
+
success: false,
|
|
1453
|
+
error: {
|
|
1454
|
+
code: "UPLOAD_ERROR",
|
|
1455
|
+
message: error instanceof Error ? error.message : "Failed to process images"
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
);
|
|
1351
1461
|
fastify.get(
|
|
1352
1462
|
"/:sessionId/:messageId/:filename",
|
|
1353
1463
|
async (request, reply) => {
|
|
@@ -1473,7 +1583,7 @@ function registerToolsRoutes(app, copilotService) {
|
|
|
1473
1583
|
}
|
|
1474
1584
|
|
|
1475
1585
|
// src/services/copilot.service.ts
|
|
1476
|
-
import { CopilotClient } from "@github/copilot-sdk";
|
|
1586
|
+
import { CopilotClient, approveAll } from "@github/copilot-sdk";
|
|
1477
1587
|
|
|
1478
1588
|
// src/tools/devops-tools.ts
|
|
1479
1589
|
import * as fs3 from "fs/promises";
|
|
@@ -1871,11 +1981,61 @@ var analyzeErrorTool = {
|
|
|
1871
1981
|
return result;
|
|
1872
1982
|
}
|
|
1873
1983
|
};
|
|
1984
|
+
var fetchUrlTool = {
|
|
1985
|
+
name: "fetch_url",
|
|
1986
|
+
description: "Fetch the text content of a public URL. Use this to read documentation, github issues, or other public web pages when the user shares a URL.",
|
|
1987
|
+
parameters: {
|
|
1988
|
+
type: "object",
|
|
1989
|
+
properties: {
|
|
1990
|
+
url: {
|
|
1991
|
+
type: "string",
|
|
1992
|
+
description: "The HTTP or HTTPS URL to fetch"
|
|
1993
|
+
}
|
|
1994
|
+
},
|
|
1995
|
+
required: ["url"]
|
|
1996
|
+
},
|
|
1997
|
+
handler: async (params) => {
|
|
1998
|
+
const targetUrl = params.url;
|
|
1999
|
+
if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) {
|
|
2000
|
+
return `Error: Only HTTP/HTTPS URLs are supported, got "${targetUrl}"`;
|
|
2001
|
+
}
|
|
2002
|
+
try {
|
|
2003
|
+
const controller = new AbortController();
|
|
2004
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
2005
|
+
const response = await fetch(targetUrl, {
|
|
2006
|
+
signal: controller.signal,
|
|
2007
|
+
headers: {
|
|
2008
|
+
"User-Agent": "DevMentorAI/1.0",
|
|
2009
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8"
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
clearTimeout(timeoutId);
|
|
2013
|
+
if (!response.ok) {
|
|
2014
|
+
return `Error: Server responded with status ${response.status} ${response.statusText}`;
|
|
2015
|
+
}
|
|
2016
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2017
|
+
const text = await response.text();
|
|
2018
|
+
const MAX_LENGTH = 15e3;
|
|
2019
|
+
if (text.length > MAX_LENGTH) {
|
|
2020
|
+
return text.substring(0, MAX_LENGTH) + `
|
|
2021
|
+
|
|
2022
|
+
[Content truncated at ${MAX_LENGTH} characters]`;
|
|
2023
|
+
}
|
|
2024
|
+
return text;
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
if (error.name === "AbortError") {
|
|
2027
|
+
return `Error: Request to ${targetUrl} timed out after 10 seconds.`;
|
|
2028
|
+
}
|
|
2029
|
+
return `Error fetching URL: ${error instanceof Error ? error.message : String(error)}`;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
1874
2033
|
var devopsTools = [
|
|
1875
2034
|
readFileTool,
|
|
1876
2035
|
listDirectoryTool,
|
|
1877
2036
|
analyzeConfigTool,
|
|
1878
|
-
analyzeErrorTool
|
|
2037
|
+
analyzeErrorTool,
|
|
2038
|
+
fetchUrlTool
|
|
1879
2039
|
];
|
|
1880
2040
|
function getToolByName(name) {
|
|
1881
2041
|
return devopsTools.find((tool) => tool.name === name);
|
|
@@ -2121,7 +2281,8 @@ var CopilotService = class {
|
|
|
2121
2281
|
customAgents: agentConfig ? [agentConfig] : void 0,
|
|
2122
2282
|
systemMessage: systemPrompt ? { content: systemPrompt } : void 0,
|
|
2123
2283
|
tools,
|
|
2124
|
-
mcpServers
|
|
2284
|
+
mcpServers,
|
|
2285
|
+
onPermissionRequest: approveAll
|
|
2125
2286
|
});
|
|
2126
2287
|
this.sessions.set(sessionId, { sessionId, session, type });
|
|
2127
2288
|
}
|
|
@@ -2151,7 +2312,7 @@ var CopilotService = class {
|
|
|
2151
2312
|
return true;
|
|
2152
2313
|
}
|
|
2153
2314
|
try {
|
|
2154
|
-
const session = await this.client.resumeSession(sessionId);
|
|
2315
|
+
const session = await this.client.resumeSession(sessionId, { onPermissionRequest: approveAll });
|
|
2155
2316
|
const dbSession = this.sessionService.getSession(sessionId);
|
|
2156
2317
|
this.sessions.set(sessionId, { sessionId, session, type: dbSession?.type || "general" });
|
|
2157
2318
|
console.log(`[CopilotService] Session ${sessionId} resumed from disk`);
|
|
@@ -2781,6 +2942,9 @@ function truncate(str, maxLen = 500) {
|
|
|
2781
2942
|
}
|
|
2782
2943
|
async function createServer() {
|
|
2783
2944
|
const fastify = Fastify({
|
|
2945
|
+
// Allow large payloads for image uploads (data URLs can be 10-30MB for full-page screenshots)
|
|
2946
|
+
bodyLimit: 50 * 1024 * 1024,
|
|
2947
|
+
// 50MB
|
|
2784
2948
|
logger: {
|
|
2785
2949
|
level: DEBUG_MODE ? "debug" : "info",
|
|
2786
2950
|
transport: {
|