@stackmemoryai/stackmemory 0.3.5 → 0.3.7
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.
- package/dist/agents/verifiers/base-verifier.js.map +2 -2
- package/dist/agents/verifiers/formatter-verifier.js.map +2 -2
- package/dist/agents/verifiers/llm-judge.js.map +2 -2
- package/dist/cli/claude-sm.js +24 -13
- package/dist/cli/claude-sm.js.map +2 -2
- package/dist/cli/codex-sm.js +24 -13
- package/dist/cli/codex-sm.js.map +2 -2
- package/dist/cli/commands/agent.js.map +2 -2
- package/dist/cli/commands/chromadb.js +217 -32
- package/dist/cli/commands/chromadb.js.map +2 -2
- package/dist/cli/commands/clear.js +12 -1
- package/dist/cli/commands/clear.js.map +2 -2
- package/dist/cli/commands/context.js +13 -2
- package/dist/cli/commands/context.js.map +2 -2
- package/dist/cli/commands/dashboard.js.map +2 -2
- package/dist/cli/commands/gc.js +202 -0
- package/dist/cli/commands/gc.js.map +7 -0
- package/dist/cli/commands/handoff.js +12 -1
- package/dist/cli/commands/handoff.js.map +2 -2
- package/dist/cli/commands/infinite-storage.js +251 -0
- package/dist/cli/commands/infinite-storage.js.map +7 -0
- package/dist/cli/commands/linear-create.js +13 -2
- package/dist/cli/commands/linear-create.js.map +2 -2
- package/dist/cli/commands/linear-list.js +12 -1
- package/dist/cli/commands/linear-list.js.map +2 -2
- package/dist/cli/commands/linear-migrate.js +12 -1
- package/dist/cli/commands/linear-migrate.js.map +2 -2
- package/dist/cli/commands/linear-test.js +12 -1
- package/dist/cli/commands/linear-test.js.map +2 -2
- package/dist/cli/commands/linear-unified.js +262 -0
- package/dist/cli/commands/linear-unified.js.map +7 -0
- package/dist/cli/commands/linear.js +17 -6
- package/dist/cli/commands/linear.js.map +2 -2
- package/dist/cli/commands/monitor.js.map +2 -2
- package/dist/cli/commands/onboard.js.map +2 -2
- package/dist/cli/commands/quality.js.map +2 -2
- package/dist/cli/commands/search.js.map +2 -2
- package/dist/cli/commands/session.js.map +2 -2
- package/dist/cli/commands/skills.js +12 -1
- package/dist/cli/commands/skills.js.map +2 -2
- package/dist/cli/commands/storage.js +18 -7
- package/dist/cli/commands/storage.js.map +2 -2
- package/dist/cli/commands/tasks.js.map +2 -2
- package/dist/cli/commands/tui.js +13 -2
- package/dist/cli/commands/tui.js.map +2 -2
- package/dist/cli/commands/webhook.js +14 -3
- package/dist/cli/commands/webhook.js.map +2 -2
- package/dist/cli/commands/workflow.js +14 -3
- package/dist/cli/commands/workflow.js.map +2 -2
- package/dist/cli/commands/worktree.js.map +2 -2
- package/dist/cli/index.js +20 -5
- package/dist/cli/index.js.map +2 -2
- package/dist/core/config/config-manager.js.map +2 -2
- package/dist/core/context/auto-context.js.map +2 -2
- package/dist/core/context/compaction-handler.js.map +2 -2
- package/dist/core/context/context-bridge.js.map +2 -2
- package/dist/core/context/dual-stack-manager.js.map +2 -2
- package/dist/core/context/frame-database.js.map +2 -2
- package/dist/core/context/frame-digest.js.map +2 -2
- package/dist/core/context/frame-handoff-manager.js.map +2 -2
- package/dist/core/context/frame-manager.js +12 -1
- package/dist/core/context/frame-manager.js.map +2 -2
- package/dist/core/context/frame-stack.js.map +2 -2
- package/dist/core/context/incremental-gc.js +279 -0
- package/dist/core/context/incremental-gc.js.map +7 -0
- package/dist/core/context/permission-manager.js +12 -1
- package/dist/core/context/permission-manager.js.map +2 -2
- package/dist/core/context/refactored-frame-manager.js.map +2 -2
- package/dist/core/context/shared-context-layer.js +12 -1
- package/dist/core/context/shared-context-layer.js.map +2 -2
- package/dist/core/context/stack-merge-resolver.js.map +2 -2
- package/dist/core/context/validation.js.map +2 -2
- package/dist/core/database/batch-operations.js.map +2 -2
- package/dist/core/database/connection-pool.js.map +2 -2
- package/dist/core/database/migration-manager.js.map +2 -2
- package/dist/core/database/paradedb-adapter.js.map +2 -2
- package/dist/core/database/query-cache.js.map +2 -2
- package/dist/core/database/query-router.js.map +2 -2
- package/dist/core/database/sqlite-adapter.js.map +2 -2
- package/dist/core/digest/enhanced-hybrid-digest.js.map +2 -2
- package/dist/core/errors/recovery.js.map +2 -2
- package/dist/core/merge/resolution-engine.js.map +2 -2
- package/dist/core/monitoring/error-handler.js.map +2 -2
- package/dist/core/monitoring/logger.js +14 -3
- package/dist/core/monitoring/logger.js.map +2 -2
- package/dist/core/monitoring/metrics.js +13 -2
- package/dist/core/monitoring/metrics.js.map +2 -2
- package/dist/core/monitoring/progress-tracker.js +12 -1
- package/dist/core/monitoring/progress-tracker.js.map +2 -2
- package/dist/core/monitoring/session-monitor.js.map +2 -2
- package/dist/core/performance/context-cache.js.map +2 -2
- package/dist/core/performance/lazy-context-loader.js.map +2 -2
- package/dist/core/performance/monitor.js.map +2 -2
- package/dist/core/performance/optimized-frame-context.js.map +2 -2
- package/dist/core/performance/performance-benchmark.js.map +2 -2
- package/dist/core/performance/performance-profiler.js +12 -1
- package/dist/core/performance/performance-profiler.js.map +2 -2
- package/dist/core/performance/streaming-jsonl-parser.js.map +2 -2
- package/dist/core/persistence/postgres-adapter.js.map +2 -2
- package/dist/core/projects/project-manager.js.map +2 -2
- package/dist/core/retrieval/context-retriever.js.map +2 -2
- package/dist/core/retrieval/graph-retrieval.js.map +2 -2
- package/dist/core/retrieval/llm-context-retrieval.js.map +2 -2
- package/dist/core/retrieval/retrieval-benchmarks.js.map +2 -2
- package/dist/core/retrieval/summary-generator.js.map +2 -2
- package/dist/core/session/clear-survival.js.map +2 -2
- package/dist/core/session/handoff-generator.js.map +2 -2
- package/dist/core/session/session-manager.js +16 -5
- package/dist/core/session/session-manager.js.map +2 -2
- package/dist/core/skills/skill-storage.js +13 -2
- package/dist/core/skills/skill-storage.js.map +2 -2
- package/dist/core/storage/chromadb-adapter.js.map +2 -2
- package/dist/core/storage/chromadb-simple.js +160 -0
- package/dist/core/storage/chromadb-simple.js.map +7 -0
- package/dist/core/storage/infinite-storage.js +443 -0
- package/dist/core/storage/infinite-storage.js.map +7 -0
- package/dist/core/storage/railway-optimized-storage.js +19 -8
- package/dist/core/storage/railway-optimized-storage.js.map +2 -2
- package/dist/core/storage/remote-storage.js +12 -1
- package/dist/core/storage/remote-storage.js.map +2 -2
- package/dist/core/trace/cli-trace-wrapper.js +16 -5
- package/dist/core/trace/cli-trace-wrapper.js.map +2 -2
- package/dist/core/trace/db-trace-wrapper.js.map +2 -2
- package/dist/core/trace/debug-trace.js +21 -10
- package/dist/core/trace/debug-trace.js.map +2 -2
- package/dist/core/trace/index.js +46 -35
- package/dist/core/trace/index.js.map +2 -2
- package/dist/core/trace/trace-demo.js +12 -1
- package/dist/core/trace/trace-demo.js.map +2 -2
- package/dist/core/trace/trace-detector.js.map +2 -2
- package/dist/core/trace/trace-store.js.map +2 -2
- package/dist/core/utils/compression.js +79 -0
- package/dist/core/utils/compression.js.map +7 -0
- package/dist/core/utils/update-checker.js.map +2 -2
- package/dist/core/worktree/worktree-manager.js.map +2 -2
- package/dist/features/analytics/api/analytics-api.js.map +2 -2
- package/dist/features/analytics/core/analytics-service.js +12 -1
- package/dist/features/analytics/core/analytics-service.js.map +2 -2
- package/dist/features/analytics/queries/metrics-queries.js.map +2 -2
- package/dist/features/tasks/pebbles-task-store.js.map +2 -2
- package/dist/features/tui/components/analytics-panel.js.map +2 -2
- package/dist/features/tui/components/pr-tracker.js.map +2 -2
- package/dist/features/tui/components/session-monitor.js.map +2 -2
- package/dist/features/tui/components/subagent-fleet.js.map +2 -2
- package/dist/features/tui/components/task-board.js +650 -2
- package/dist/features/tui/components/task-board.js.map +2 -2
- package/dist/features/tui/index.js +16 -5
- package/dist/features/tui/index.js.map +2 -2
- package/dist/features/tui/services/data-service.js +35 -52
- package/dist/features/tui/services/data-service.js.map +2 -2
- package/dist/features/tui/services/linear-task-reader.js +100 -0
- package/dist/features/tui/services/linear-task-reader.js.map +7 -0
- package/dist/features/tui/services/websocket-client.js +13 -2
- package/dist/features/tui/services/websocket-client.js.map +2 -2
- package/dist/features/tui/terminal-compat.js +27 -16
- package/dist/features/tui/terminal-compat.js.map +2 -2
- package/dist/features/web/client/stores/task-store.js +22 -0
- package/dist/features/web/client/stores/task-store.js.map +7 -0
- package/dist/features/web/server/index.js +182 -0
- package/dist/features/web/server/index.js.map +7 -0
- package/dist/integrations/claude-code/enhanced-pre-clear-hooks.js.map +2 -2
- package/dist/integrations/claude-code/lifecycle-hooks.js.map +2 -2
- package/dist/integrations/claude-code/post-task-hooks.js.map +2 -2
- package/dist/integrations/linear/auth.js +17 -6
- package/dist/integrations/linear/auth.js.map +2 -2
- package/dist/integrations/linear/auto-sync.js.map +2 -2
- package/dist/integrations/linear/client.js.map +2 -2
- package/dist/integrations/linear/config.js.map +2 -2
- package/dist/integrations/linear/migration.js.map +2 -2
- package/dist/integrations/linear/oauth-server.js +13 -2
- package/dist/integrations/linear/oauth-server.js.map +2 -2
- package/dist/integrations/linear/rest-client.js.map +2 -2
- package/dist/integrations/linear/sync-enhanced.js +202 -0
- package/dist/integrations/linear/sync-enhanced.js.map +7 -0
- package/dist/integrations/linear/sync-manager.js.map +2 -2
- package/dist/integrations/linear/sync-service.js +12 -1
- package/dist/integrations/linear/sync-service.js.map +2 -2
- package/dist/integrations/linear/sync.js +34 -3
- package/dist/integrations/linear/sync.js.map +2 -2
- package/dist/integrations/linear/unified-sync.js +560 -0
- package/dist/integrations/linear/unified-sync.js.map +7 -0
- package/dist/integrations/linear/webhook-handler.js +12 -1
- package/dist/integrations/linear/webhook-handler.js.map +2 -2
- package/dist/integrations/linear/webhook-server.js +14 -3
- package/dist/integrations/linear/webhook-server.js.map +2 -2
- package/dist/integrations/linear/webhook.js +12 -1
- package/dist/integrations/linear/webhook.js.map +2 -2
- package/dist/integrations/mcp/handlers/context-handlers.js.map +2 -2
- package/dist/integrations/mcp/handlers/linear-handlers.js.map +2 -2
- package/dist/integrations/mcp/handlers/skill-handlers.js +13 -2
- package/dist/integrations/mcp/handlers/skill-handlers.js.map +2 -2
- package/dist/integrations/mcp/handlers/task-handlers.js.map +2 -2
- package/dist/integrations/mcp/handlers/trace-handlers.js.map +2 -2
- package/dist/integrations/mcp/middleware/tool-scoring.js.map +2 -2
- package/dist/integrations/mcp/refactored-server.js +15 -4
- package/dist/integrations/mcp/refactored-server.js.map +2 -2
- package/dist/integrations/mcp/server.js +12 -1
- package/dist/integrations/mcp/server.js.map +2 -2
- package/dist/integrations/mcp/tool-definitions.js.map +2 -2
- package/dist/integrations/pg-aiguide/embedding-provider.js +13 -2
- package/dist/integrations/pg-aiguide/embedding-provider.js.map +2 -2
- package/dist/integrations/pg-aiguide/semantic-search.js.map +2 -2
- package/dist/mcp/stackmemory-mcp-server.js +12 -1
- package/dist/mcp/stackmemory-mcp-server.js.map +2 -2
- package/dist/middleware/exponential-rate-limiter.js.map +2 -2
- package/dist/servers/production/auth-middleware.js +13 -2
- package/dist/servers/production/auth-middleware.js.map +2 -2
- package/dist/servers/railway/index.js +22 -11
- package/dist/servers/railway/index.js.map +2 -2
- package/dist/services/config-service.js.map +2 -2
- package/dist/services/context-service.js.map +2 -2
- package/dist/skills/claude-skills.js +150 -1
- package/dist/skills/claude-skills.js.map +2 -2
- package/dist/skills/dashboard-launcher.js +212 -0
- package/dist/skills/dashboard-launcher.js.map +7 -0
- package/dist/skills/repo-ingestion-skill.js +561 -0
- package/dist/skills/repo-ingestion-skill.js.map +7 -0
- package/dist/utils/logger.js +12 -1
- package/dist/utils/logger.js.map +2 -2
- package/package.json +7 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/integrations/linear/oauth-server.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * OAuth Callback Server for Linear Integration\n * Handles the OAuth callback redirect and completes the authentication flow\n */\n\nimport express from 'express';\nimport http from 'http';\nimport { URL } from 'url';\nimport { logger } from '../../core/monitoring/logger.js';\nimport { LinearAuthManager } from './auth.js';\nimport chalk from 'chalk';\n\nexport interface OAuthServerConfig {\n port?: number;\n host?: string;\n redirectPath?: string;\n autoShutdown?: boolean;\n shutdownDelay?: number;\n}\n\nexport class LinearOAuthServer {\n private app: express.Application;\n private server: http.Server | null = null;\n private authManager: LinearAuthManager;\n private config: OAuthServerConfig;\n private pendingCodeVerifiers: Map<string, string> = new Map();\n private authCompleteCallbacks: Map<string, (success: boolean) => void> = new Map();\n\n constructor(projectRoot: string, config?: OAuthServerConfig) {\n this.app = express();\n this.authManager = new LinearAuthManager(projectRoot);\n \n this.config = {\n port: config?.port || 3456,\n host: config?.host || 'localhost',\n redirectPath: config?.redirectPath || '/auth/linear/callback',\n autoShutdown: config?.autoShutdown !== false,\n shutdownDelay: config?.shutdownDelay || 5000,\n };\n\n this.setupRoutes();\n }\n\n private setupRoutes(): void {\n // Health check endpoint\n this.app.get('/health', (req, res) => {\n res.json({\n status: 'healthy',\n service: 'linear-oauth',\n timestamp: new Date().toISOString(),\n });\n });\n\n // OAuth callback endpoint\n this.app.get(this.config.redirectPath!, async (req, res) => {\n const { code, state, error, error_description } = req.query;\n\n // Handle OAuth errors\n if (error) {\n logger.error(`OAuth error: ${error} - ${error_description}`);\n res.send(this.generateErrorPage(\n 'Authorization Failed',\n `${error}: ${error_description || 'An error occurred during authorization'}`\n ));\n \n if (state && this.authCompleteCallbacks.has(state as string)) {\n this.authCompleteCallbacks.get(state as string)!(false);\n this.authCompleteCallbacks.delete(state as string);\n }\n \n this.scheduleShutdown();\n return;\n }\n\n // Validate required parameters\n if (!code) {\n res.send(this.generateErrorPage(\n 'Missing Authorization Code',\n 'No authorization code was provided in the callback'\n ));\n this.scheduleShutdown();\n return;\n }\n\n try {\n // Get the code verifier for this session\n const codeVerifier = state \n ? this.pendingCodeVerifiers.get(state as string)\n : process.env._LINEAR_CODE_VERIFIER;\n\n if (!codeVerifier) {\n throw new Error('Code verifier not found. Please restart the authorization process.');\n }\n\n // Exchange code for tokens\n logger.info('Exchanging authorization code for tokens...');\n await this.authManager.exchangeCodeForToken(code as string, codeVerifier);\n\n // Clean up\n if (state) {\n this.pendingCodeVerifiers.delete(state as string);\n }\n delete process.env._LINEAR_CODE_VERIFIER;\n\n // Test the connection\n const testSuccess = await this.testConnection();\n \n if (testSuccess) {\n res.send(this.generateSuccessPage());\n logger.info('Linear OAuth authentication completed successfully!');\n } else {\n throw new Error('Failed to verify Linear connection');\n }\n\n // Notify callback if registered\n if (state && this.authCompleteCallbacks.has(state as string)) {\n this.authCompleteCallbacks.get(state as string)!(true);\n this.authCompleteCallbacks.delete(state as string);\n }\n\n // Schedule server shutdown if auto-shutdown is enabled\n this.scheduleShutdown();\n } catch (error) {\n logger.error('Failed to complete OAuth flow:', error as Error);\n res.send(this.generateErrorPage(\n 'Authentication Failed',\n (error as Error).message\n ));\n \n if (state && this.authCompleteCallbacks.has(state as string)) {\n this.authCompleteCallbacks.get(state as string)!(false);\n this.authCompleteCallbacks.delete(state as string);\n }\n \n this.scheduleShutdown();\n }\n });\n\n // Start OAuth flow endpoint\n this.app.get('/auth/linear/start', (req, res) => {\n try {\n const config = this.authManager.loadConfig();\n if (!config) {\n res.status(400).send(this.generateErrorPage(\n 'Configuration Missing',\n 'Linear OAuth configuration not found. Please configure your client ID and secret.'\n ));\n return;\n }\n\n // Generate state for CSRF protection\n const state = this.generateState();\n const { url, codeVerifier } = this.authManager.generateAuthUrl(state);\n \n // Store code verifier for this session\n this.pendingCodeVerifiers.set(state, codeVerifier);\n\n // Redirect to Linear OAuth page\n res.redirect(url);\n } catch (error) {\n logger.error('Failed to start OAuth flow:', error as Error);\n res.status(500).send(this.generateErrorPage(\n 'OAuth Start Failed',\n (error as Error).message\n ));\n }\n });\n\n // 404 handler\n this.app.use((req, res) => {\n res.status(404).json({ error: 'Not found' });\n });\n }\n\n private generateState(): string {\n return Math.random().toString(36).substring(2, 15) + \n Math.random().toString(36).substring(2, 15);\n }\n\n private generateSuccessPage(): string {\n return `\n <!DOCTYPE html>\n <html>\n <head>\n <title>Linear Authorization Successful</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n margin: 0;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n }\n .container {\n background: white;\n padding: 3rem;\n border-radius: 12px;\n box-shadow: 0 20px 60px rgba(0,0,0,0.3);\n text-align: center;\n max-width: 400px;\n }\n h1 {\n color: #2d3748;\n margin-bottom: 1rem;\n }\n .success-icon {\n font-size: 4rem;\n margin-bottom: 1rem;\n }\n p {\n color: #4a5568;\n line-height: 1.6;\n margin: 1rem 0;\n }\n .close-note {\n color: #718096;\n font-size: 0.875rem;\n margin-top: 2rem;\n }\n code {\n background: #f7fafc;\n padding: 0.25rem 0.5rem;\n border-radius: 4px;\n font-family: 'Courier New', monospace;\n }\n </style>\n </head>\n <body>\n <div class=\"container\">\n <div class=\"success-icon\">\u2705</div>\n <h1>Authorization Successful!</h1>\n <p>Your Linear account has been successfully connected to StackMemory.</p>\n <p>You can now use Linear integration features:</p>\n <p><code>stackmemory linear sync</code></p>\n <p><code>stackmemory linear create</code></p>\n <p class=\"close-note\">You can safely close this window and return to your terminal.</p>\n </div>\n </body>\n </html>\n `;\n }\n\n private generateErrorPage(title: string, message: string): string {\n return `\n <!DOCTYPE html>\n <html>\n <head>\n <title>${title}</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n margin: 0;\n background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);\n }\n .container {\n background: white;\n padding: 3rem;\n border-radius: 12px;\n box-shadow: 0 20px 60px rgba(0,0,0,0.3);\n text-align: center;\n max-width: 400px;\n }\n h1 {\n color: #e53e3e;\n margin-bottom: 1rem;\n }\n .error-icon {\n font-size: 4rem;\n margin-bottom: 1rem;\n }\n p {\n color: #4a5568;\n line-height: 1.6;\n margin: 1rem 0;\n }\n .error-message {\n background: #fff5f5;\n border: 1px solid #fed7d7;\n color: #742a2a;\n padding: 1rem;\n border-radius: 6px;\n margin-top: 1rem;\n font-size: 0.875rem;\n }\n .retry-note {\n color: #718096;\n font-size: 0.875rem;\n margin-top: 2rem;\n }\n code {\n background: #f7fafc;\n padding: 0.25rem 0.5rem;\n border-radius: 4px;\n font-family: 'Courier New', monospace;\n }\n </style>\n </head>\n <body>\n <div class=\"container\">\n <div class=\"error-icon\">\u274C</div>\n <h1>${title}</h1>\n <p>Unable to complete Linear authorization.</p>\n <div class=\"error-message\">${message}</div>\n <p class=\"retry-note\">\n Please try again with:<br>\n <code>stackmemory linear auth</code>\n </p>\n </div>\n </body>\n </html>\n `;\n }\n\n private async testConnection(): Promise<boolean> {\n try {\n const token = await this.authManager.getValidToken();\n \n const response = await fetch('https://api.linear.app/graphql', {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n query: 'query { viewer { id name email } }',\n }),\n });\n\n if (response.ok) {\n const result = await response.json() as {\n data?: { viewer?: { id: string; name: string; email: string } };\n };\n if (result.data?.viewer) {\n logger.info(`Connected to Linear as: ${result.data.viewer.name} (${result.data.viewer.email})`);\n return true;\n }\n }\n\n return false;\n } catch (error) {\n logger.error('Linear connection test failed:', error as Error);\n return false;\n }\n }\n\n private scheduleShutdown(): void {\n if (this.config.autoShutdown && this.server) {\n setTimeout(() => {\n logger.info('Auto-shutting down OAuth server...');\n this.stop();\n }, this.config.shutdownDelay);\n }\n }\n\n public async start(): Promise<{ url: string; codeVerifier?: string }> {\n return new Promise((resolve, reject) => {\n try {\n // Load config and generate auth URL\n const config = this.authManager.loadConfig();\n if (!config) {\n // If no config, provide setup instructions\n const setupUrl = `http://${this.config.host}:${this.config.port}/auth/linear/start`;\n \n this.server = this.app.listen(\n this.config.port!,\n this.config.host!,\n () => {\n console.log(chalk.green('\u2713') + chalk.bold(' Linear OAuth Server Started'));\n console.log(chalk.cyan(' Authorization URL: ') + setupUrl);\n console.log(chalk.cyan(' Callback URL: ') + \n `http://${this.config.host}:${this.config.port}${this.config.redirectPath}`);\n console.log('');\n console.log(chalk.yellow(' \u26A0 Configuration Required:'));\n console.log(' 1. Create a Linear OAuth app at: https://linear.app/settings/api');\n console.log(` 2. Set redirect URI to: http://${this.config.host}:${this.config.port}${this.config.redirectPath}`);\n console.log(' 3. Set environment variables:');\n console.log(' export LINEAR_CLIENT_ID=\"your_client_id\"');\n console.log(' export LINEAR_CLIENT_SECRET=\"your_client_secret\"');\n console.log(' 4. Restart the auth process');\n \n resolve({ url: setupUrl });\n }\n );\n return;\n }\n\n // Generate state and auth URL\n const state = this.generateState();\n const { url, codeVerifier } = this.authManager.generateAuthUrl(state);\n \n // Store code verifier\n this.pendingCodeVerifiers.set(state, codeVerifier);\n\n this.server = this.app.listen(\n this.config.port!,\n this.config.host!,\n () => {\n console.log(chalk.green('\u2713') + chalk.bold(' Linear OAuth Server Started'));\n console.log(chalk.cyan(' Open this URL in your browser:'));\n console.log(' ' + chalk.underline(url));\n console.log('');\n console.log(chalk.gray(' The server will automatically shut down after authorization completes.'));\n \n resolve({ url, codeVerifier });\n }\n );\n\n // Register auth complete callback\n this.authCompleteCallbacks.set(state, (success) => {\n if (success) {\n console.log(chalk.green('\\n\u2713 Linear authorization completed successfully!'));\n } else {\n console.log(chalk.red('\\n\u2717 Linear authorization failed'));\n }\n });\n\n } catch (error) {\n reject(error);\n }\n });\n }\n\n public async stop(): Promise<void> {\n return new Promise((resolve) => {\n if (this.server) {\n this.server.close(() => {\n logger.info('OAuth server stopped');\n this.server = null;\n resolve();\n });\n } else {\n resolve();\n }\n });\n }\n\n public async waitForAuth(state: string, timeout: number = 300000): Promise<boolean> {\n return new Promise((resolve) => {\n const timeoutId = setTimeout(() => {\n this.authCompleteCallbacks.delete(state);\n resolve(false);\n }, timeout);\n\n this.authCompleteCallbacks.set(state, (success) => {\n clearTimeout(timeoutId);\n resolve(success);\n });\n });\n }\n}\n\n// Standalone execution support\nif (process.argv[1] === new URL(import.meta.url).pathname) {\n const projectRoot = process.cwd();\n const server = new LinearOAuthServer(projectRoot, {\n autoShutdown: true,\n shutdownDelay: 5000,\n });\n\n server.start()\n .then(({ url }) => {\n if (url) {\n console.log(chalk.cyan('\\nWaiting for authorization...'));\n console.log(chalk.gray('Press Ctrl+C to cancel\\n'));\n }\n })\n .catch((error) => {\n console.error(chalk.red('Failed to start OAuth server:'), error);\n process.exit(1);\n });\n\n process.on('SIGINT', async () => {\n console.log(chalk.yellow('\\n\\nShutting down OAuth server...'));\n await server.stop();\n process.exit(0);\n });\n}"],
|
|
5
|
-
"mappings": "AAKA,OAAO,aAAa;AAEpB,SAAS,WAAW;AACpB,SAAS,cAAc;AACvB,SAAS,yBAAyB;AAClC,OAAO,WAAW;
|
|
4
|
+
"sourcesContent": ["/**\n * OAuth Callback Server for Linear Integration\n * Handles the OAuth callback redirect and completes the authentication flow\n */\n\nimport express from 'express';\nimport http from 'http';\nimport { URL } from 'url';\nimport { logger } from '../../core/monitoring/logger.js';\nimport { LinearAuthManager } from './auth.js';\nimport chalk from 'chalk';\n// Type-safe environment variable access\nfunction getEnv(key: string, defaultValue?: string): string {\n const value = process.env[key];\n if (value === undefined) {\n if (defaultValue !== undefined) return defaultValue;\n throw new Error(`Environment variable ${key} is required`);\n }\n return value;\n}\n\nfunction getOptionalEnv(key: string): string | undefined {\n return process.env[key];\n}\n\n\nexport interface OAuthServerConfig {\n port?: number;\n host?: string;\n redirectPath?: string;\n autoShutdown?: boolean;\n shutdownDelay?: number;\n}\n\nexport class LinearOAuthServer {\n private app: express.Application;\n private server: http.Server | null = null;\n private authManager: LinearAuthManager;\n private config: OAuthServerConfig;\n private pendingCodeVerifiers: Map<string, string> = new Map();\n private authCompleteCallbacks: Map<string, (success: boolean) => void> = new Map();\n\n constructor(projectRoot: string, config?: OAuthServerConfig) {\n this.app = express();\n this.authManager = new LinearAuthManager(projectRoot);\n \n this.config = {\n port: config?.port || 3456,\n host: config?.host || 'localhost',\n redirectPath: config?.redirectPath || '/auth/linear/callback',\n autoShutdown: config?.autoShutdown !== false,\n shutdownDelay: config?.shutdownDelay || 5000,\n };\n\n this.setupRoutes();\n }\n\n private setupRoutes(): void {\n // Health check endpoint\n this.app.get('/health', (req, res) => {\n res.json({\n status: 'healthy',\n service: 'linear-oauth',\n timestamp: new Date().toISOString(),\n });\n });\n\n // OAuth callback endpoint\n this.app.get(this.config.redirectPath!, async (req, res) => {\n const { code, state, error, error_description } = req.query;\n\n // Handle OAuth errors\n if (error) {\n logger.error(`OAuth error: ${error} - ${error_description}`);\n res.send(this.generateErrorPage(\n 'Authorization Failed',\n `${error}: ${error_description || 'An error occurred during authorization'}`\n ));\n \n if (state && this.authCompleteCallbacks.has(state as string)) {\n this.authCompleteCallbacks.get(state as string)!(false);\n this.authCompleteCallbacks.delete(state as string);\n }\n \n this.scheduleShutdown();\n return;\n }\n\n // Validate required parameters\n if (!code) {\n res.send(this.generateErrorPage(\n 'Missing Authorization Code',\n 'No authorization code was provided in the callback'\n ));\n this.scheduleShutdown();\n return;\n }\n\n try {\n // Get the code verifier for this session\n const codeVerifier = state \n ? this.pendingCodeVerifiers.get(state as string)\n : process.env['_LINEAR_CODE_VERIFIER'];\n\n if (!codeVerifier) {\n throw new Error('Code verifier not found. Please restart the authorization process.');\n }\n\n // Exchange code for tokens\n logger.info('Exchanging authorization code for tokens...');\n await this.authManager.exchangeCodeForToken(code as string, codeVerifier);\n\n // Clean up\n if (state) {\n this.pendingCodeVerifiers.delete(state as string);\n }\n delete process.env['_LINEAR_CODE_VERIFIER'];\n\n // Test the connection\n const testSuccess = await this.testConnection();\n \n if (testSuccess) {\n res.send(this.generateSuccessPage());\n logger.info('Linear OAuth authentication completed successfully!');\n } else {\n throw new Error('Failed to verify Linear connection');\n }\n\n // Notify callback if registered\n if (state && this.authCompleteCallbacks.has(state as string)) {\n this.authCompleteCallbacks.get(state as string)!(true);\n this.authCompleteCallbacks.delete(state as string);\n }\n\n // Schedule server shutdown if auto-shutdown is enabled\n this.scheduleShutdown();\n } catch (error: unknown) {\n logger.error('Failed to complete OAuth flow:', error as Error);\n res.send(this.generateErrorPage(\n 'Authentication Failed',\n (error as Error).message\n ));\n \n if (state && this.authCompleteCallbacks.has(state as string)) {\n this.authCompleteCallbacks.get(state as string)!(false);\n this.authCompleteCallbacks.delete(state as string);\n }\n \n this.scheduleShutdown();\n }\n });\n\n // Start OAuth flow endpoint\n this.app.get('/auth/linear/start', (req, res) => {\n try {\n const config = this.authManager.loadConfig();\n if (!config) {\n res.status(400).send(this.generateErrorPage(\n 'Configuration Missing',\n 'Linear OAuth configuration not found. Please configure your client ID and secret.'\n ));\n return;\n }\n\n // Generate state for CSRF protection\n const state = this.generateState();\n const { url, codeVerifier } = this.authManager.generateAuthUrl(state);\n \n // Store code verifier for this session\n this.pendingCodeVerifiers.set(state, codeVerifier);\n\n // Redirect to Linear OAuth page\n res.redirect(url);\n } catch (error: unknown) {\n logger.error('Failed to start OAuth flow:', error as Error);\n res.status(500).send(this.generateErrorPage(\n 'OAuth Start Failed',\n (error as Error).message\n ));\n }\n });\n\n // 404 handler\n this.app.use((req, res) => {\n res.status(404).json({ error: 'Not found' });\n });\n }\n\n private generateState(): string {\n return Math.random().toString(36).substring(2, 15) + \n Math.random().toString(36).substring(2, 15);\n }\n\n private generateSuccessPage(): string {\n return `\n <!DOCTYPE html>\n <html>\n <head>\n <title>Linear Authorization Successful</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n margin: 0;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n }\n .container {\n background: white;\n padding: 3rem;\n border-radius: 12px;\n box-shadow: 0 20px 60px rgba(0,0,0,0.3);\n text-align: center;\n max-width: 400px;\n }\n h1 {\n color: #2d3748;\n margin-bottom: 1rem;\n }\n .success-icon {\n font-size: 4rem;\n margin-bottom: 1rem;\n }\n p {\n color: #4a5568;\n line-height: 1.6;\n margin: 1rem 0;\n }\n .close-note {\n color: #718096;\n font-size: 0.875rem;\n margin-top: 2rem;\n }\n code {\n background: #f7fafc;\n padding: 0.25rem 0.5rem;\n border-radius: 4px;\n font-family: 'Courier New', monospace;\n }\n </style>\n </head>\n <body>\n <div class=\"container\">\n <div class=\"success-icon\">\u2705</div>\n <h1>Authorization Successful!</h1>\n <p>Your Linear account has been successfully connected to StackMemory.</p>\n <p>You can now use Linear integration features:</p>\n <p><code>stackmemory linear sync</code></p>\n <p><code>stackmemory linear create</code></p>\n <p class=\"close-note\">You can safely close this window and return to your terminal.</p>\n </div>\n </body>\n </html>\n `;\n }\n\n private generateErrorPage(title: string, message: string): string {\n return `\n <!DOCTYPE html>\n <html>\n <head>\n <title>${title}</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n margin: 0;\n background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);\n }\n .container {\n background: white;\n padding: 3rem;\n border-radius: 12px;\n box-shadow: 0 20px 60px rgba(0,0,0,0.3);\n text-align: center;\n max-width: 400px;\n }\n h1 {\n color: #e53e3e;\n margin-bottom: 1rem;\n }\n .error-icon {\n font-size: 4rem;\n margin-bottom: 1rem;\n }\n p {\n color: #4a5568;\n line-height: 1.6;\n margin: 1rem 0;\n }\n .error-message {\n background: #fff5f5;\n border: 1px solid #fed7d7;\n color: #742a2a;\n padding: 1rem;\n border-radius: 6px;\n margin-top: 1rem;\n font-size: 0.875rem;\n }\n .retry-note {\n color: #718096;\n font-size: 0.875rem;\n margin-top: 2rem;\n }\n code {\n background: #f7fafc;\n padding: 0.25rem 0.5rem;\n border-radius: 4px;\n font-family: 'Courier New', monospace;\n }\n </style>\n </head>\n <body>\n <div class=\"container\">\n <div class=\"error-icon\">\u274C</div>\n <h1>${title}</h1>\n <p>Unable to complete Linear authorization.</p>\n <div class=\"error-message\">${message}</div>\n <p class=\"retry-note\">\n Please try again with:<br>\n <code>stackmemory linear auth</code>\n </p>\n </div>\n </body>\n </html>\n `;\n }\n\n private async testConnection(): Promise<boolean> {\n try {\n const token = await this.authManager.getValidToken();\n \n const response = await fetch('https://api.linear.app/graphql', {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n query: 'query { viewer { id name email } }',\n }),\n });\n\n if (response.ok) {\n const result = await response.json() as {\n data?: { viewer?: { id: string; name: string; email: string } };\n };\n if (result.data?.viewer) {\n logger.info(`Connected to Linear as: ${result.data.viewer.name} (${result.data.viewer.email})`);\n return true;\n }\n }\n\n return false;\n } catch (error: unknown) {\n logger.error('Linear connection test failed:', error as Error);\n return false;\n }\n }\n\n private scheduleShutdown(): void {\n if (this.config.autoShutdown && this.server) {\n setTimeout(() => {\n logger.info('Auto-shutting down OAuth server...');\n this.stop();\n }, this.config.shutdownDelay);\n }\n }\n\n public async start(): Promise<{ url: string; codeVerifier?: string }> {\n return new Promise((resolve, reject) => {\n try {\n // Load config and generate auth URL\n const config = this.authManager.loadConfig();\n if (!config) {\n // If no config, provide setup instructions\n const setupUrl = `http://${this.config.host}:${this.config.port}/auth/linear/start`;\n \n this.server = this.app.listen(\n this.config.port!,\n this.config.host!,\n () => {\n console.log(chalk.green('\u2713') + chalk.bold(' Linear OAuth Server Started'));\n console.log(chalk.cyan(' Authorization URL: ') + setupUrl);\n console.log(chalk.cyan(' Callback URL: ') + \n `http://${this.config.host}:${this.config.port}${this.config.redirectPath}`);\n console.log('');\n console.log(chalk.yellow(' \u26A0 Configuration Required:'));\n console.log(' 1. Create a Linear OAuth app at: https://linear.app/settings/api');\n console.log(` 2. Set redirect URI to: http://${this.config.host}:${this.config.port}${this.config.redirectPath}`);\n console.log(' 3. Set environment variables:');\n console.log(' export LINEAR_CLIENT_ID=\"your_client_id\"');\n console.log(' export LINEAR_CLIENT_SECRET=\"your_client_secret\"');\n console.log(' 4. Restart the auth process');\n \n resolve({ url: setupUrl });\n }\n );\n return;\n }\n\n // Generate state and auth URL\n const state = this.generateState();\n const { url, codeVerifier } = this.authManager.generateAuthUrl(state);\n \n // Store code verifier\n this.pendingCodeVerifiers.set(state, codeVerifier);\n\n this.server = this.app.listen(\n this.config.port!,\n this.config.host!,\n () => {\n console.log(chalk.green('\u2713') + chalk.bold(' Linear OAuth Server Started'));\n console.log(chalk.cyan(' Open this URL in your browser:'));\n console.log(' ' + chalk.underline(url));\n console.log('');\n console.log(chalk.gray(' The server will automatically shut down after authorization completes.'));\n \n resolve({ url, codeVerifier });\n }\n );\n\n // Register auth complete callback\n this.authCompleteCallbacks.set(state, (success) => {\n if (success) {\n console.log(chalk.green('\\n\u2713 Linear authorization completed successfully!'));\n } else {\n console.log(chalk.red('\\n\u2717 Linear authorization failed'));\n }\n });\n\n } catch (error: unknown) {\n reject(error);\n }\n });\n }\n\n public async stop(): Promise<void> {\n return new Promise((resolve) => {\n if (this.server) {\n this.server.close(() => {\n logger.info('OAuth server stopped');\n this.server = null;\n resolve();\n });\n } else {\n resolve();\n }\n });\n }\n\n public async waitForAuth(state: string, timeout: number = 300000): Promise<boolean> {\n return new Promise((resolve) => {\n const timeoutId = setTimeout(() => {\n this.authCompleteCallbacks.delete(state);\n resolve(false);\n }, timeout);\n\n this.authCompleteCallbacks.set(state, (success) => {\n clearTimeout(timeoutId);\n resolve(success);\n });\n });\n }\n}\n\n// Standalone execution support\nif (process.argv[1] === new URL(import.meta.url).pathname) {\n const projectRoot = process.cwd();\n const server = new LinearOAuthServer(projectRoot, {\n autoShutdown: true,\n shutdownDelay: 5000,\n });\n\n server.start()\n .then(({ url }) => {\n if (url) {\n console.log(chalk.cyan('\\nWaiting for authorization...'));\n console.log(chalk.gray('Press Ctrl+C to cancel\\n'));\n }\n })\n .catch((error) => {\n console.error(chalk.red('Failed to start OAuth server:'), error);\n process.exit(1);\n });\n\n process.on('SIGINT', async () => {\n console.log(chalk.yellow('\\n\\nShutting down OAuth server...'));\n await server.stop();\n process.exit(0);\n });\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["error"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/integrations/linear/rest-client.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Linear REST Client for StackMemory\n * Provides memory-based task storage using REST API instead of GraphQL\n */\n\nimport { logger } from '../../core/monitoring/logger.js';\n\nexport interface LinearTask {\n id: string;\n identifier: string;\n title: string;\n description?: string;\n state: {\n name: string;\n type: string;\n };\n priority: number;\n assignee?: {\n id: string;\n name: string;\n };\n estimate?: number;\n createdAt: string;\n updatedAt: string;\n url: string;\n}\n\nexport interface LinearTasksResponse {\n data: {\n issues: {\n nodes: LinearTask[];\n pageInfo: {\n hasNextPage: boolean;\n endCursor?: string;\n };\n };\n };\n}\n\nexport class LinearRestClient {\n private apiKey: string;\n private baseUrl = 'https://api.linear.app/graphql';\n private taskCache = new Map<string, LinearTask>();\n private lastSync = 0;\n private cacheTTL = 5 * 60 * 1000; // 5 minutes\n\n constructor(apiKey: string) {\n this.apiKey = apiKey;\n }\n\n /**\n * Get all tasks and store in memory\n */\n async getAllTasks(forceRefresh = false): Promise<LinearTask[]> {\n const now = Date.now();\n \n // Return cached data if fresh\n if (!forceRefresh && (now - this.lastSync) < this.cacheTTL && this.taskCache.size > 0) {\n return Array.from(this.taskCache.values());\n }\n\n try {\n const allTasks: LinearTask[] = [];\n let hasNextPage = true;\n let cursor: string | undefined;\n\n while (hasNextPage) {\n const query = `\n query($after: String) {\n issues(\n filter: { team: { key: { eq: \"ENG\" } } }\n first: 100\n after: $after\n ) {\n nodes {\n id\n identifier\n title\n description\n state {\n name\n type\n }\n priority\n assignee {\n id\n name\n }\n estimate\n createdAt\n updatedAt\n url\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n `;\n\n const variables = cursor ? { after: cursor } : {};\n const response = await this.makeRequest<LinearTasksResponse>(query, variables);\n\n const tasks = response.data.issues.nodes;\n allTasks.push(...tasks);\n\n // Update cache\n tasks.forEach(task => {\n this.taskCache.set(task.id, task);\n });\n\n hasNextPage = response.data.issues.pageInfo.hasNextPage;\n cursor = response.data.issues.pageInfo.endCursor;\n\n logger.info(`Fetched ${tasks.length} tasks, total: ${allTasks.length}`);\n }\n\n this.lastSync = now;\n logger.info(`Cached ${allTasks.length} Linear tasks in memory`);\n\n return allTasks;\n } catch (error) {\n logger.error('Failed to fetch Linear tasks:', error as Error);\n return Array.from(this.taskCache.values()); // Return cached data on error\n }\n }\n\n /**\n * Get tasks by status\n */\n async getTasksByStatus(status: string): Promise<LinearTask[]> {\n const tasks = await this.getAllTasks();\n return tasks.filter(task => task.state.type === status);\n }\n\n /**\n * Get tasks assigned to current user\n */\n async getMyTasks(): Promise<LinearTask[]> {\n try {\n const viewer = await this.getViewer();\n const tasks = await this.getAllTasks();\n return tasks.filter(task => task.assignee?.id === viewer.id);\n } catch (error) {\n logger.error('Failed to get assigned tasks:', error as Error);\n return [];\n }\n }\n\n /**\n * Get task count by status\n */\n async getTaskCounts(): Promise<Record<string, number>> {\n const tasks = await this.getAllTasks();\n const counts: Record<string, number> = {};\n\n tasks.forEach(task => {\n const status = task.state.type;\n counts[status] = (counts[status] || 0) + 1;\n });\n\n return counts;\n }\n\n /**\n * Search tasks by title or description\n */\n async searchTasks(query: string): Promise<LinearTask[]> {\n const tasks = await this.getAllTasks();\n const searchTerm = query.toLowerCase();\n\n return tasks.filter(task => \n task.title.toLowerCase().includes(searchTerm) ||\n task.description?.toLowerCase().includes(searchTerm) ||\n task.identifier.toLowerCase().includes(searchTerm)\n );\n }\n\n /**\n * Get current viewer info\n */\n async getViewer(): Promise<{ id: string; name: string; email: string }> {\n const query = `\n query {\n viewer {\n id\n name\n email\n }\n }\n `;\n\n const response = await this.makeRequest<{\n data: {\n viewer: { id: string; name: string; email: string };\n };\n }>(query);\n\n return response.data.viewer;\n }\n\n /**\n * Get team info\n */\n async getTeam(): Promise<{ id: string; name: string; key: string }> {\n const query = `\n query {\n teams(filter: { key: { eq: \"ENG\" } }, first: 1) {\n nodes {\n id\n name\n key\n }\n }\n }\n `;\n\n const response = await this.makeRequest<{\n data: {\n teams: {\n nodes: Array<{ id: string; name: string; key: string }>;\n };\n };\n }>(query);\n\n if (response.data.teams.nodes.length === 0) {\n throw new Error('ENG team not found');\n }\n\n return response.data.teams.nodes[0]!;\n }\n\n /**\n * Get cache stats\n */\n getCacheStats(): {\n size: number;\n lastSync: number;\n age: number;\n fresh: boolean;\n } {\n const now = Date.now();\n return {\n size: this.taskCache.size,\n lastSync: this.lastSync,\n age: now - this.lastSync,\n fresh: (now - this.lastSync) < this.cacheTTL,\n };\n }\n\n /**\n * Clear cache\n */\n clearCache(): void {\n this.taskCache.clear();\n this.lastSync = 0;\n logger.info('Linear task cache cleared');\n }\n\n /**\n * Make GraphQL request\n */\n async makeRequest<T>(\n query: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n const response = await fetch(this.baseUrl, {\n method: 'POST',\n headers: {\n Authorization: this.apiKey,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ query, variables }),\n });\n\n const result = await response.json() as {\n data?: unknown;\n errors?: Array<{ message: string }>;\n };\n\n if (!response.ok || result.errors) {\n const errorMsg = result.errors?.[0]?.message || `${response.status} ${response.statusText}`;\n throw new Error(`Linear API error: ${errorMsg}`);\n }\n\n return result as T;\n }\n}"],
|
|
5
|
-
"mappings": "AAKA,SAAS,cAAc;AAkChB,MAAM,iBAAiB;AAAA,EACpB;AAAA,EACA,UAAU;AAAA,EACV,YAAY,oBAAI,IAAwB;AAAA,EACxC,WAAW;AAAA,EACX,WAAW,IAAI,KAAK;AAAA;AAAA,EAE5B,YAAY,QAAgB;AAC1B,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,eAAe,OAA8B;AAC7D,UAAM,MAAM,KAAK,IAAI;AAGrB,QAAI,CAAC,gBAAiB,MAAM,KAAK,WAAY,KAAK,YAAY,KAAK,UAAU,OAAO,GAAG;AACrF,aAAO,MAAM,KAAK,KAAK,UAAU,OAAO,CAAC;AAAA,IAC3C;AAEA,QAAI;AACF,YAAM,WAAyB,CAAC;AAChC,UAAI,cAAc;AAClB,UAAI;AAEJ,aAAO,aAAa;AAClB,cAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkCd,cAAM,YAAY,SAAS,EAAE,OAAO,OAAO,IAAI,CAAC;AAChD,cAAM,WAAW,MAAM,KAAK,YAAiC,OAAO,SAAS;AAE7E,cAAM,QAAQ,SAAS,KAAK,OAAO;AACnC,iBAAS,KAAK,GAAG,KAAK;AAGtB,cAAM,QAAQ,UAAQ;AACpB,eAAK,UAAU,IAAI,KAAK,IAAI,IAAI;AAAA,QAClC,CAAC;AAED,sBAAc,SAAS,KAAK,OAAO,SAAS;AAC5C,iBAAS,SAAS,KAAK,OAAO,SAAS;AAEvC,eAAO,KAAK,WAAW,MAAM,MAAM,kBAAkB,SAAS,MAAM,EAAE;AAAA,MACxE;AAEA,WAAK,WAAW;AAChB,aAAO,KAAK,UAAU,SAAS,MAAM,yBAAyB;AAE9D,aAAO;AAAA,IACT,SAAS,
|
|
4
|
+
"sourcesContent": ["/**\n * Linear REST Client for StackMemory\n * Provides memory-based task storage using REST API instead of GraphQL\n */\n\nimport { logger } from '../../core/monitoring/logger.js';\n\nexport interface LinearTask {\n id: string;\n identifier: string;\n title: string;\n description?: string;\n state: {\n name: string;\n type: string;\n };\n priority: number;\n assignee?: {\n id: string;\n name: string;\n };\n estimate?: number;\n createdAt: string;\n updatedAt: string;\n url: string;\n}\n\nexport interface LinearTasksResponse {\n data: {\n issues: {\n nodes: LinearTask[];\n pageInfo: {\n hasNextPage: boolean;\n endCursor?: string;\n };\n };\n };\n}\n\nexport class LinearRestClient {\n private apiKey: string;\n private baseUrl = 'https://api.linear.app/graphql';\n private taskCache = new Map<string, LinearTask>();\n private lastSync = 0;\n private cacheTTL = 5 * 60 * 1000; // 5 minutes\n\n constructor(apiKey: string) {\n this.apiKey = apiKey;\n }\n\n /**\n * Get all tasks and store in memory\n */\n async getAllTasks(forceRefresh = false): Promise<LinearTask[]> {\n const now = Date.now();\n \n // Return cached data if fresh\n if (!forceRefresh && (now - this.lastSync) < this.cacheTTL && this.taskCache.size > 0) {\n return Array.from(this.taskCache.values());\n }\n\n try {\n const allTasks: LinearTask[] = [];\n let hasNextPage = true;\n let cursor: string | undefined;\n\n while (hasNextPage) {\n const query = `\n query($after: String) {\n issues(\n filter: { team: { key: { eq: \"ENG\" } } }\n first: 100\n after: $after\n ) {\n nodes {\n id\n identifier\n title\n description\n state {\n name\n type\n }\n priority\n assignee {\n id\n name\n }\n estimate\n createdAt\n updatedAt\n url\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n `;\n\n const variables = cursor ? { after: cursor } : {};\n const response = await this.makeRequest<LinearTasksResponse>(query, variables);\n\n const tasks = response.data.issues.nodes;\n allTasks.push(...tasks);\n\n // Update cache\n tasks.forEach(task => {\n this.taskCache.set(task.id, task);\n });\n\n hasNextPage = response.data.issues.pageInfo.hasNextPage;\n cursor = response.data.issues.pageInfo.endCursor;\n\n logger.info(`Fetched ${tasks.length} tasks, total: ${allTasks.length}`);\n }\n\n this.lastSync = now;\n logger.info(`Cached ${allTasks.length} Linear tasks in memory`);\n\n return allTasks;\n } catch (error: unknown) {\n logger.error('Failed to fetch Linear tasks:', error as Error);\n return Array.from(this.taskCache.values()); // Return cached data on error\n }\n }\n\n /**\n * Get tasks by status\n */\n async getTasksByStatus(status: string): Promise<LinearTask[]> {\n const tasks = await this.getAllTasks();\n return tasks.filter((task: any) => task.state.type === status);\n }\n\n /**\n * Get tasks assigned to current user\n */\n async getMyTasks(): Promise<LinearTask[]> {\n try {\n const viewer = await this.getViewer();\n const tasks = await this.getAllTasks();\n return tasks.filter((task: any) => task.assignee?.id === viewer.id);\n } catch (error: unknown) {\n logger.error('Failed to get assigned tasks:', error as Error);\n return [];\n }\n }\n\n /**\n * Get task count by status\n */\n async getTaskCounts(): Promise<Record<string, number>> {\n const tasks = await this.getAllTasks();\n const counts: Record<string, number> = {};\n\n tasks.forEach(task => {\n const status = task.state.type;\n counts[status] = (counts[status] || 0) + 1;\n });\n\n return counts;\n }\n\n /**\n * Search tasks by title or description\n */\n async searchTasks(query: string): Promise<LinearTask[]> {\n const tasks = await this.getAllTasks();\n const searchTerm = query.toLowerCase();\n\n return tasks.filter((task: any) => \n task.title.toLowerCase().includes(searchTerm) ||\n task.description?.toLowerCase().includes(searchTerm) ||\n task.identifier.toLowerCase().includes(searchTerm)\n );\n }\n\n /**\n * Get current viewer info\n */\n async getViewer(): Promise<{ id: string; name: string; email: string }> {\n const query = `\n query {\n viewer {\n id\n name\n email\n }\n }\n `;\n\n const response = await this.makeRequest<{\n data: {\n viewer: { id: string; name: string; email: string };\n };\n }>(query);\n\n return response.data.viewer;\n }\n\n /**\n * Get team info\n */\n async getTeam(): Promise<{ id: string; name: string; key: string }> {\n const query = `\n query {\n teams(filter: { key: { eq: \"ENG\" } }, first: 1) {\n nodes {\n id\n name\n key\n }\n }\n }\n `;\n\n const response = await this.makeRequest<{\n data: {\n teams: {\n nodes: Array<{ id: string; name: string; key: string }>;\n };\n };\n }>(query);\n\n if (response.data.teams.nodes.length === 0) {\n throw new Error('ENG team not found');\n }\n\n return response.data.teams.nodes[0]!;\n }\n\n /**\n * Get cache stats\n */\n getCacheStats(): {\n size: number;\n lastSync: number;\n age: number;\n fresh: boolean;\n } {\n const now = Date.now();\n return {\n size: this.taskCache.size,\n lastSync: this.lastSync,\n age: now - this.lastSync,\n fresh: (now - this.lastSync) < this.cacheTTL,\n };\n }\n\n /**\n * Clear cache\n */\n clearCache(): void {\n this.taskCache.clear();\n this.lastSync = 0;\n logger.info('Linear task cache cleared');\n }\n\n /**\n * Make GraphQL request\n */\n async makeRequest<T>(\n query: string,\n variables: Record<string, unknown> = {}\n ): Promise<T> {\n const response = await fetch(this.baseUrl, {\n method: 'POST',\n headers: {\n Authorization: this.apiKey,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ query, variables }),\n });\n\n const result = await response.json() as {\n data?: unknown;\n errors?: Array<{ message: string }>;\n };\n\n if (!response.ok || result.errors) {\n const errorMsg = result.errors?.[0]?.message || `${response.status} ${response.statusText}`;\n throw new Error(`Linear API error: ${errorMsg}`);\n }\n\n return result as T;\n }\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { logger } from "../../core/monitoring/logger.js";
|
|
2
|
+
class LinearDuplicateDetector {
|
|
3
|
+
linearClient;
|
|
4
|
+
titleCache = /* @__PURE__ */ new Map();
|
|
5
|
+
cacheExpiry = 5 * 60 * 1e3;
|
|
6
|
+
// 5 minutes
|
|
7
|
+
lastCacheRefresh = 0;
|
|
8
|
+
constructor(linearClient) {
|
|
9
|
+
this.linearClient = linearClient;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Search for existing Linear issues with similar titles
|
|
13
|
+
*/
|
|
14
|
+
async searchByTitle(title, teamId) {
|
|
15
|
+
const normalizedTitle = this.normalizeTitle(title);
|
|
16
|
+
if (this.isCacheValid()) {
|
|
17
|
+
const cached = this.titleCache.get(normalizedTitle);
|
|
18
|
+
if (cached) return cached;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const allIssues = await this.linearClient.getIssues({
|
|
22
|
+
teamId,
|
|
23
|
+
limit: 100
|
|
24
|
+
// Use smaller limit to avoid API errors
|
|
25
|
+
});
|
|
26
|
+
const matchingIssues = allIssues.filter((issue) => {
|
|
27
|
+
const issueNormalized = this.normalizeTitle(issue.title);
|
|
28
|
+
if (issueNormalized === normalizedTitle) return true;
|
|
29
|
+
const similarity = this.calculateSimilarity(normalizedTitle, issueNormalized);
|
|
30
|
+
return similarity > 0.85;
|
|
31
|
+
});
|
|
32
|
+
this.titleCache.set(normalizedTitle, matchingIssues);
|
|
33
|
+
this.lastCacheRefresh = Date.now();
|
|
34
|
+
return matchingIssues;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
logger.error("Failed to search Linear issues by title:", error);
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if a task title would create a duplicate in Linear
|
|
42
|
+
*/
|
|
43
|
+
async checkForDuplicate(title, teamId) {
|
|
44
|
+
const existingIssues = await this.searchByTitle(title, teamId);
|
|
45
|
+
if (existingIssues.length === 0) {
|
|
46
|
+
return { isDuplicate: false };
|
|
47
|
+
}
|
|
48
|
+
let bestMatch;
|
|
49
|
+
let bestSimilarity = 0;
|
|
50
|
+
for (const issue of existingIssues) {
|
|
51
|
+
const similarity = this.calculateSimilarity(
|
|
52
|
+
this.normalizeTitle(title),
|
|
53
|
+
this.normalizeTitle(issue.title)
|
|
54
|
+
);
|
|
55
|
+
if (similarity > bestSimilarity) {
|
|
56
|
+
bestSimilarity = similarity;
|
|
57
|
+
bestMatch = issue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
isDuplicate: true,
|
|
62
|
+
existingIssue: bestMatch,
|
|
63
|
+
similarity: bestSimilarity
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Merge task content into existing Linear issue
|
|
68
|
+
*/
|
|
69
|
+
async mergeIntoExisting(existingIssue, newTitle, newDescription, additionalContext) {
|
|
70
|
+
try {
|
|
71
|
+
let mergedDescription = existingIssue.description || "";
|
|
72
|
+
if (newDescription && !mergedDescription.includes(newDescription)) {
|
|
73
|
+
mergedDescription += `
|
|
74
|
+
|
|
75
|
+
## Additional Context (${(/* @__PURE__ */ new Date()).toISOString()})
|
|
76
|
+
`;
|
|
77
|
+
mergedDescription += newDescription;
|
|
78
|
+
}
|
|
79
|
+
if (additionalContext) {
|
|
80
|
+
mergedDescription += `
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
${additionalContext}`;
|
|
84
|
+
}
|
|
85
|
+
const updateQuery = `
|
|
86
|
+
mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
|
|
87
|
+
issueUpdate(id: $id, input: $input) {
|
|
88
|
+
issue {
|
|
89
|
+
id
|
|
90
|
+
identifier
|
|
91
|
+
title
|
|
92
|
+
description
|
|
93
|
+
updatedAt
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
98
|
+
const variables = {
|
|
99
|
+
id: existingIssue.id,
|
|
100
|
+
input: {
|
|
101
|
+
description: mergedDescription
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const response = await this.linearClient.graphql(updateQuery, variables);
|
|
105
|
+
const updatedIssue = response.issueUpdate?.issue;
|
|
106
|
+
if (updatedIssue) {
|
|
107
|
+
logger.info(
|
|
108
|
+
`Merged content into existing Linear issue ${existingIssue.identifier}: ${existingIssue.title}`
|
|
109
|
+
);
|
|
110
|
+
return updatedIssue;
|
|
111
|
+
}
|
|
112
|
+
return existingIssue;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
logger.error("Failed to merge into existing Linear issue:", error);
|
|
115
|
+
return existingIssue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Normalize title for comparison
|
|
120
|
+
*/
|
|
121
|
+
normalizeTitle(title) {
|
|
122
|
+
return title.toLowerCase().trim().replace(/\s+/g, " ").replace(/[^\w\s-]/g, "").replace(/^(sta|eng|bug|feat|task|tsk)[-\s]\d+[-\s:]*/, "").trim();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Calculate similarity between two strings (Levenshtein distance based)
|
|
126
|
+
*/
|
|
127
|
+
calculateSimilarity(str1, str2) {
|
|
128
|
+
if (str1 === str2) return 1;
|
|
129
|
+
if (str1.length === 0 || str2.length === 0) return 0;
|
|
130
|
+
const distance = this.levenshteinDistance(str1, str2);
|
|
131
|
+
const maxLength = Math.max(str1.length, str2.length);
|
|
132
|
+
return 1 - distance / maxLength;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Calculate Levenshtein distance between two strings
|
|
136
|
+
*/
|
|
137
|
+
levenshteinDistance(str1, str2) {
|
|
138
|
+
const m = str1.length;
|
|
139
|
+
const n = str2.length;
|
|
140
|
+
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
141
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
142
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
143
|
+
for (let i = 1; i <= m; i++) {
|
|
144
|
+
for (let j = 1; j <= n; j++) {
|
|
145
|
+
if (str1[i - 1] === str2[j - 1]) {
|
|
146
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
147
|
+
} else {
|
|
148
|
+
dp[i][j] = 1 + Math.min(
|
|
149
|
+
dp[i - 1][j],
|
|
150
|
+
// deletion
|
|
151
|
+
dp[i][j - 1],
|
|
152
|
+
// insertion
|
|
153
|
+
dp[i - 1][j - 1]
|
|
154
|
+
// substitution
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return dp[m][n];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Check if cache is still valid
|
|
163
|
+
*/
|
|
164
|
+
isCacheValid() {
|
|
165
|
+
return Date.now() - this.lastCacheRefresh < this.cacheExpiry;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Clear the title cache
|
|
169
|
+
*/
|
|
170
|
+
clearCache() {
|
|
171
|
+
this.titleCache.clear();
|
|
172
|
+
this.lastCacheRefresh = 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function syncToLinearWithDuplicateCheck(linearClient, task, teamId) {
|
|
176
|
+
const detector = new LinearDuplicateDetector(linearClient);
|
|
177
|
+
const duplicateCheck = await detector.checkForDuplicate(task.title, teamId);
|
|
178
|
+
if (duplicateCheck.isDuplicate && duplicateCheck.existingIssue) {
|
|
179
|
+
logger.info(
|
|
180
|
+
`Found existing Linear issue for "${task.title}": ${duplicateCheck.existingIssue.identifier} (${Math.round((duplicateCheck.similarity || 0) * 100)}% match)`
|
|
181
|
+
);
|
|
182
|
+
const mergedIssue = await detector.mergeIntoExisting(
|
|
183
|
+
duplicateCheck.existingIssue,
|
|
184
|
+
task.title,
|
|
185
|
+
task.description,
|
|
186
|
+
`StackMemory Task ID: ${task.id}`
|
|
187
|
+
);
|
|
188
|
+
return { issue: mergedIssue, wasmerged: true };
|
|
189
|
+
}
|
|
190
|
+
const newIssue = await linearClient.createIssue({
|
|
191
|
+
title: task.title,
|
|
192
|
+
description: task.description,
|
|
193
|
+
teamId
|
|
194
|
+
});
|
|
195
|
+
logger.info(`Created new Linear issue: ${newIssue.identifier}`);
|
|
196
|
+
return { issue: newIssue, wasmerged: false };
|
|
197
|
+
}
|
|
198
|
+
export {
|
|
199
|
+
LinearDuplicateDetector,
|
|
200
|
+
syncToLinearWithDuplicateCheck
|
|
201
|
+
};
|
|
202
|
+
//# sourceMappingURL=sync-enhanced.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/integrations/linear/sync-enhanced.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Enhanced Linear Sync with Duplicate Detection\n * Prevents duplicate issues by checking titles before creation\n */\n\nimport { LinearClient, LinearIssue } from './client.js';\nimport { logger } from '../../core/monitoring/logger.js';\n\nexport interface DuplicateCheckResult {\n isDuplicate: boolean;\n existingIssue?: LinearIssue;\n similarity?: number;\n}\n\nexport class LinearDuplicateDetector {\n private linearClient: LinearClient;\n private titleCache: Map<string, LinearIssue[]> = new Map();\n private cacheExpiry: number = 5 * 60 * 1000; // 5 minutes\n private lastCacheRefresh: number = 0;\n\n constructor(linearClient: LinearClient) {\n this.linearClient = linearClient;\n }\n\n /**\n * Search for existing Linear issues with similar titles\n */\n async searchByTitle(title: string, teamId?: string): Promise<LinearIssue[]> {\n const normalizedTitle = this.normalizeTitle(title);\n \n // Check cache first\n if (this.isCacheValid()) {\n const cached = this.titleCache.get(normalizedTitle);\n if (cached) return cached;\n }\n\n try {\n // Get all issues from the team (Linear API limit is 250)\n const allIssues = await this.linearClient.getIssues({\n teamId,\n limit: 100, // Use smaller limit to avoid API errors\n });\n\n // Filter for matching titles (exact and fuzzy)\n const matchingIssues = allIssues.filter(issue => {\n const issueNormalized = this.normalizeTitle(issue.title);\n \n // Exact match\n if (issueNormalized === normalizedTitle) return true;\n \n // Fuzzy match - check if titles are very similar\n const similarity = this.calculateSimilarity(normalizedTitle, issueNormalized);\n return similarity > 0.85; // 85% similarity threshold\n });\n\n // Update cache\n this.titleCache.set(normalizedTitle, matchingIssues);\n this.lastCacheRefresh = Date.now();\n\n return matchingIssues;\n } catch (error) {\n logger.error('Failed to search Linear issues by title:', error as Error);\n return [];\n }\n }\n\n /**\n * Check if a task title would create a duplicate in Linear\n */\n async checkForDuplicate(\n title: string,\n teamId?: string\n ): Promise<DuplicateCheckResult> {\n const existingIssues = await this.searchByTitle(title, teamId);\n \n if (existingIssues.length === 0) {\n return { isDuplicate: false };\n }\n\n // Find the best match\n let bestMatch: LinearIssue | undefined;\n let bestSimilarity = 0;\n\n for (const issue of existingIssues) {\n const similarity = this.calculateSimilarity(\n this.normalizeTitle(title),\n this.normalizeTitle(issue.title)\n );\n \n if (similarity > bestSimilarity) {\n bestSimilarity = similarity;\n bestMatch = issue;\n }\n }\n\n return {\n isDuplicate: true,\n existingIssue: bestMatch,\n similarity: bestSimilarity,\n };\n }\n\n /**\n * Merge task content into existing Linear issue\n */\n async mergeIntoExisting(\n existingIssue: LinearIssue,\n newTitle: string,\n newDescription?: string,\n additionalContext?: string\n ): Promise<LinearIssue> {\n try {\n // Build merged description\n let mergedDescription = existingIssue.description || '';\n \n if (newDescription && !mergedDescription.includes(newDescription)) {\n mergedDescription += `\\n\\n## Additional Context (${new Date().toISOString()})\\n`;\n mergedDescription += newDescription;\n }\n\n if (additionalContext) {\n mergedDescription += `\\n\\n---\\n${additionalContext}`;\n }\n\n // Update the existing issue\n const updateQuery = `\n mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n issueUpdate(id: $id, input: $input) {\n issue {\n id\n identifier\n title\n description\n updatedAt\n }\n }\n }\n `;\n\n const variables = {\n id: existingIssue.id,\n input: {\n description: mergedDescription,\n },\n };\n\n const response = await this.linearClient.graphql(updateQuery, variables);\n const updatedIssue = response.issueUpdate?.issue;\n\n if (updatedIssue) {\n logger.info(\n `Merged content into existing Linear issue ${existingIssue.identifier}: ${existingIssue.title}`\n );\n return updatedIssue;\n }\n\n return existingIssue;\n } catch (error) {\n logger.error('Failed to merge into existing Linear issue:', error as Error);\n return existingIssue;\n }\n }\n\n /**\n * Normalize title for comparison\n */\n private normalizeTitle(title: string): string {\n return title\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, ' ') // Normalize whitespace\n .replace(/[^\\w\\s-]/g, '') // Remove special characters except hyphens\n .replace(/^(sta|eng|bug|feat|task|tsk)[-\\s]\\d+[-\\s:]*/, '') // Remove issue prefixes\n .trim();\n }\n\n /**\n * Calculate similarity between two strings (Levenshtein distance based)\n */\n private calculateSimilarity(str1: string, str2: string): number {\n if (str1 === str2) return 1;\n if (str1.length === 0 || str2.length === 0) return 0;\n\n // Use Levenshtein distance for similarity calculation\n const distance = this.levenshteinDistance(str1, str2);\n const maxLength = Math.max(str1.length, str2.length);\n \n return 1 - (distance / maxLength);\n }\n\n /**\n * Calculate Levenshtein distance between two strings\n */\n private levenshteinDistance(str1: string, str2: string): number {\n const m = str1.length;\n const n = str2.length;\n const dp: number[][] = Array(m + 1)\n .fill(null)\n .map(() => Array(n + 1).fill(0));\n\n for (let i = 0; i <= m; i++) dp[i][0] = i;\n for (let j = 0; j <= n; j++) dp[0][j] = j;\n\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n if (str1[i - 1] === str2[j - 1]) {\n dp[i][j] = dp[i - 1][j - 1];\n } else {\n dp[i][j] = 1 + Math.min(\n dp[i - 1][j], // deletion\n dp[i][j - 1], // insertion\n dp[i - 1][j - 1] // substitution\n );\n }\n }\n }\n\n return dp[m][n];\n }\n\n /**\n * Check if cache is still valid\n */\n private isCacheValid(): boolean {\n return Date.now() - this.lastCacheRefresh < this.cacheExpiry;\n }\n\n /**\n * Clear the title cache\n */\n clearCache(): void {\n this.titleCache.clear();\n this.lastCacheRefresh = 0;\n }\n}\n\n/**\n * Enhanced sync function that prevents duplicates\n */\nexport async function syncToLinearWithDuplicateCheck(\n linearClient: LinearClient,\n task: any,\n teamId: string\n): Promise<{ issue: LinearIssue; wasmerged: boolean }> {\n const detector = new LinearDuplicateDetector(linearClient);\n \n // Check for duplicates\n const duplicateCheck = await detector.checkForDuplicate(task.title, teamId);\n \n if (duplicateCheck.isDuplicate && duplicateCheck.existingIssue) {\n // Merge into existing issue\n logger.info(\n `Found existing Linear issue for \"${task.title}\": ${duplicateCheck.existingIssue.identifier} (${Math.round((duplicateCheck.similarity || 0) * 100)}% match)`\n );\n \n const mergedIssue = await detector.mergeIntoExisting(\n duplicateCheck.existingIssue,\n task.title,\n task.description,\n `StackMemory Task ID: ${task.id}`\n );\n \n return { issue: mergedIssue, wasmerged: true };\n }\n \n // No duplicate found, create new issue\n const newIssue = await linearClient.createIssue({\n title: task.title,\n description: task.description,\n teamId,\n });\n \n logger.info(`Created new Linear issue: ${newIssue.identifier}`);\n return { issue: newIssue, wasmerged: false };\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/integrations/linear/sync-manager.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Linear Sync Manager\n * Handles periodic and event-based synchronization with Linear\n */\n\nimport { EventEmitter } from 'events';\nimport { logger } from '../../core/monitoring/logger.js';\nimport { LinearSyncEngine, SyncConfig, SyncResult } from './sync.js';\nimport { PebblesTaskStore } from '../../features/tasks/pebbles-task-store.js';\nimport { LinearAuthManager } from './auth.js';\n\nexport interface SyncManagerConfig extends SyncConfig {\n autoSyncInterval?: number; // minutes\n syncOnTaskChange?: boolean;\n syncOnSessionStart?: boolean;\n syncOnSessionEnd?: boolean;\n debounceInterval?: number; // milliseconds\n}\n\nexport class LinearSyncManager extends EventEmitter {\n private syncEngine: LinearSyncEngine;\n private syncTimer?: NodeJS.Timeout;\n private pendingSyncTimer?: NodeJS.Timeout;\n private config: SyncManagerConfig;\n private lastSyncTime: number = 0;\n private syncInProgress: boolean = false;\n private syncLockAcquired: number = 0; // Timestamp when lock was acquired\n private readonly SYNC_LOCK_TIMEOUT = 300000; // 5 minutes max sync time\n private taskStore: PebblesTaskStore;\n\n constructor(\n taskStore: PebblesTaskStore,\n authManager: LinearAuthManager,\n config: SyncManagerConfig,\n projectRoot?: string\n ) {\n super();\n this.taskStore = taskStore;\n this.config = {\n ...config,\n autoSyncInterval: config.autoSyncInterval || 15,\n syncOnTaskChange: config.syncOnTaskChange !== false,\n syncOnSessionStart: config.syncOnSessionStart !== false,\n syncOnSessionEnd: config.syncOnSessionEnd !== false,\n debounceInterval: config.debounceInterval || 5000, // 5 seconds\n };\n\n this.syncEngine = new LinearSyncEngine(\n taskStore,\n authManager,\n config,\n projectRoot\n );\n\n this.setupEventListeners();\n this.setupPeriodicSync();\n }\n\n /**\n * Setup event listeners for automatic sync triggers\n */\n private setupEventListeners(): void {\n if (this.config.syncOnTaskChange && this.taskStore) {\n // Listen for task changes to trigger sync\n this.taskStore.on('sync:needed', (changeType: string) => {\n logger.debug(`Task change detected: ${changeType}`);\n this.scheduleDebouncedSync();\n });\n\n // Listen for specific task events if needed for logging\n this.taskStore.on('task:created', (task: any) => {\n logger.debug(`Task created: ${task.title}`);\n });\n\n this.taskStore.on('task:completed', (task: any) => {\n logger.debug(`Task completed: ${task.title}`);\n });\n\n logger.info('Task change sync enabled via EventEmitter');\n }\n }\n\n /**\n * Setup periodic sync timer\n */\n private setupPeriodicSync(): void {\n if (!this.config.autoSync || !this.config.autoSyncInterval) {\n return;\n }\n\n // Clear existing timer if any\n if (this.syncTimer) {\n clearInterval(this.syncTimer);\n }\n\n // Setup new timer\n const intervalMs = this.config.autoSyncInterval * 60 * 1000;\n this.syncTimer = setInterval(() => {\n this.performSync('periodic');\n }, intervalMs);\n\n logger.info(\n `Periodic Linear sync enabled: every ${this.config.autoSyncInterval} minutes`\n );\n }\n\n /**\n * Schedule a debounced sync to avoid too frequent syncs\n */\n private scheduleDebouncedSync(): void {\n if (!this.config.enabled) return;\n\n // Clear existing pending sync\n if (this.pendingSyncTimer) {\n clearTimeout(this.pendingSyncTimer);\n }\n\n // Schedule new sync\n this.pendingSyncTimer = setTimeout(() => {\n this.performSync('task-change');\n }, this.config.debounceInterval);\n }\n\n /**\n * Perform a sync operation\n */\n async performSync(\n trigger:\n | 'manual'\n | 'periodic'\n | 'task-change'\n | 'session-start'\n | 'session-end'\n ): Promise<SyncResult> {\n if (!this.config.enabled) {\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: ['Sync is disabled'],\n };\n }\n\n if (this.syncInProgress) {\n logger.warn(`Linear sync already in progress, skipping ${trigger} sync`);\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: ['Sync already in progress'],\n };\n }\n\n // Check minimum time between syncs (avoid rapid fire)\n const now = Date.now();\n const timeSinceLastSync = now - this.lastSyncTime;\n const minInterval = 10000; // 10 seconds minimum between syncs\n\n if (trigger !== 'manual' && timeSinceLastSync < minInterval) {\n logger.debug(\n `Skipping ${trigger} sync, too soon since last sync (${timeSinceLastSync}ms ago)`\n );\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: [\n `Too soon since last sync (wait ${minInterval - timeSinceLastSync}ms)`,\n ],\n };\n }\n\n try {\n this.syncInProgress = true;\n this.emit('sync:started', { trigger });\n\n logger.info(`Starting Linear sync (trigger: ${trigger})`);\n const result = await this.syncEngine.sync();\n\n this.lastSyncTime = now;\n\n if (result.success) {\n logger.info(\n `Linear sync completed: ${result.synced.toLinear} to Linear, ${result.synced.fromLinear} from Linear, ${result.synced.updated} updated`\n );\n this.emit('sync:completed', { trigger, result });\n } else {\n logger.error(`Linear sync failed: ${result.errors.join(', ')}`);\n this.emit('sync:failed', { trigger, result });\n }\n\n return result;\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n logger.error(`Linear sync error: ${errorMessage}`);\n\n const result: SyncResult = {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: [errorMessage],\n };\n\n this.emit('sync:failed', { trigger, result, error });\n return result;\n } finally {\n this.syncInProgress = false;\n }\n }\n\n /**\n * Sync on session start\n */\n async syncOnStart(): Promise<SyncResult | null> {\n if (this.config.syncOnSessionStart) {\n return await this.performSync('session-start');\n }\n return null;\n }\n\n /**\n * Sync on session end\n */\n async syncOnEnd(): Promise<SyncResult | null> {\n if (this.config.syncOnSessionEnd) {\n return await this.performSync('session-end');\n }\n return null;\n }\n\n /**\n * Update sync configuration\n */\n updateConfig(newConfig: Partial<SyncManagerConfig>): void {\n this.config = { ...this.config, ...newConfig };\n this.syncEngine.updateConfig(newConfig);\n\n // Restart periodic sync if interval changed\n if (\n newConfig.autoSyncInterval !== undefined ||\n newConfig.autoSync !== undefined\n ) {\n this.setupPeriodicSync();\n }\n }\n\n /**\n * Get sync status\n */\n getStatus(): {\n enabled: boolean;\n syncInProgress: boolean;\n lastSyncTime: number;\n nextSyncTime: number | null;\n config: SyncManagerConfig;\n } {\n const nextSyncTime =\n this.config.autoSync && this.config.autoSyncInterval\n ? this.lastSyncTime + this.config.autoSyncInterval * 60 * 1000\n : null;\n\n return {\n enabled: this.config.enabled,\n syncInProgress: this.syncInProgress,\n lastSyncTime: this.lastSyncTime,\n nextSyncTime,\n config: this.config,\n };\n }\n\n /**\n * Stop all sync activities\n */\n stop(): void {\n if (this.syncTimer) {\n clearInterval(this.syncTimer);\n this.syncTimer = undefined;\n }\n\n if (this.pendingSyncTimer) {\n clearTimeout(this.pendingSyncTimer);\n this.pendingSyncTimer = undefined;\n }\n\n this.removeAllListeners();\n logger.info('Linear sync manager stopped');\n }\n\n /**\n * Force an immediate sync\n */\n async forceSync(): Promise<SyncResult> {\n return await this.performSync('manual');\n }\n}\n\n/**\n * Default sync manager configuration\n */\nexport const DEFAULT_SYNC_MANAGER_CONFIG: SyncManagerConfig = {\n enabled: true,\n direction: 'bidirectional',\n autoSync: true,\n autoSyncInterval: 15, // minutes\n conflictResolution: 'newest_wins',\n syncOnTaskChange: true,\n syncOnSessionStart: true,\n syncOnSessionEnd: true,\n debounceInterval: 5000, // 5 seconds\n};\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Linear Sync Manager\n * Handles periodic and event-based synchronization with Linear\n */\n\nimport { EventEmitter } from 'events';\nimport { logger } from '../../core/monitoring/logger.js';\nimport { LinearSyncEngine, SyncConfig, SyncResult } from './sync.js';\nimport { PebblesTaskStore } from '../../features/tasks/pebbles-task-store.js';\nimport { LinearAuthManager } from './auth.js';\n\nexport interface SyncManagerConfig extends SyncConfig {\n autoSyncInterval?: number; // minutes\n syncOnTaskChange?: boolean;\n syncOnSessionStart?: boolean;\n syncOnSessionEnd?: boolean;\n debounceInterval?: number; // milliseconds\n}\n\nexport class LinearSyncManager extends EventEmitter {\n private syncEngine: LinearSyncEngine;\n private syncTimer?: NodeJS.Timeout;\n private pendingSyncTimer?: NodeJS.Timeout;\n private config: SyncManagerConfig;\n private lastSyncTime: number = 0;\n private syncInProgress: boolean = false;\n private syncLockAcquired: number = 0; // Timestamp when lock was acquired\n private readonly SYNC_LOCK_TIMEOUT = 300000; // 5 minutes max sync time\n private taskStore: PebblesTaskStore;\n\n constructor(\n taskStore: PebblesTaskStore,\n authManager: LinearAuthManager,\n config: SyncManagerConfig,\n projectRoot?: string\n ) {\n super();\n this.taskStore = taskStore;\n this.config = {\n ...config,\n autoSyncInterval: config.autoSyncInterval || 15,\n syncOnTaskChange: config.syncOnTaskChange !== false,\n syncOnSessionStart: config.syncOnSessionStart !== false,\n syncOnSessionEnd: config.syncOnSessionEnd !== false,\n debounceInterval: config.debounceInterval || 5000, // 5 seconds\n };\n\n this.syncEngine = new LinearSyncEngine(\n taskStore,\n authManager,\n config,\n projectRoot\n );\n\n this.setupEventListeners();\n this.setupPeriodicSync();\n }\n\n /**\n * Setup event listeners for automatic sync triggers\n */\n private setupEventListeners(): void {\n if (this.config.syncOnTaskChange && this.taskStore) {\n // Listen for task changes to trigger sync\n this.taskStore.on('sync:needed', (changeType: string) => {\n logger.debug(`Task change detected: ${changeType}`);\n this.scheduleDebouncedSync();\n });\n\n // Listen for specific task events if needed for logging\n this.taskStore.on('task:created', (task: any) => {\n logger.debug(`Task created: ${task.title}`);\n });\n\n this.taskStore.on('task:completed', (task: any) => {\n logger.debug(`Task completed: ${task.title}`);\n });\n\n logger.info('Task change sync enabled via EventEmitter');\n }\n }\n\n /**\n * Setup periodic sync timer\n */\n private setupPeriodicSync(): void {\n if (!this.config.autoSync || !this.config.autoSyncInterval) {\n return;\n }\n\n // Clear existing timer if any\n if (this.syncTimer) {\n clearInterval(this.syncTimer);\n }\n\n // Setup new timer\n const intervalMs = this.config.autoSyncInterval * 60 * 1000;\n this.syncTimer = setInterval(() => {\n this.performSync('periodic');\n }, intervalMs);\n\n logger.info(\n `Periodic Linear sync enabled: every ${this.config.autoSyncInterval} minutes`\n );\n }\n\n /**\n * Schedule a debounced sync to avoid too frequent syncs\n */\n private scheduleDebouncedSync(): void {\n if (!this.config.enabled) return;\n\n // Clear existing pending sync\n if (this.pendingSyncTimer) {\n clearTimeout(this.pendingSyncTimer);\n }\n\n // Schedule new sync\n this.pendingSyncTimer = setTimeout(() => {\n this.performSync('task-change');\n }, this.config.debounceInterval);\n }\n\n /**\n * Perform a sync operation\n */\n async performSync(\n trigger:\n | 'manual'\n | 'periodic'\n | 'task-change'\n | 'session-start'\n | 'session-end'\n ): Promise<SyncResult> {\n if (!this.config.enabled) {\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: ['Sync is disabled'],\n };\n }\n\n if (this.syncInProgress) {\n logger.warn(`Linear sync already in progress, skipping ${trigger} sync`);\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: ['Sync already in progress'],\n };\n }\n\n // Check minimum time between syncs (avoid rapid fire)\n const now = Date.now();\n const timeSinceLastSync = now - this.lastSyncTime;\n const minInterval = 10000; // 10 seconds minimum between syncs\n\n if (trigger !== 'manual' && timeSinceLastSync < minInterval) {\n logger.debug(\n `Skipping ${trigger} sync, too soon since last sync (${timeSinceLastSync}ms ago)`\n );\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: [\n `Too soon since last sync (wait ${minInterval - timeSinceLastSync}ms)`,\n ],\n };\n }\n\n try {\n this.syncInProgress = true;\n this.emit('sync:started', { trigger });\n\n logger.info(`Starting Linear sync (trigger: ${trigger})`);\n const result = await this.syncEngine.sync();\n\n this.lastSyncTime = now;\n\n if (result.success) {\n logger.info(\n `Linear sync completed: ${result.synced.toLinear} to Linear, ${result.synced.fromLinear} from Linear, ${result.synced.updated} updated`\n );\n this.emit('sync:completed', { trigger, result });\n } else {\n logger.error(`Linear sync failed: ${result.errors.join(', ')}`);\n this.emit('sync:failed', { trigger, result });\n }\n\n return result;\n } catch (error: unknown) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n logger.error(`Linear sync error: ${errorMessage}`);\n\n const result: SyncResult = {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: [errorMessage],\n };\n\n this.emit('sync:failed', { trigger, result, error });\n return result;\n } finally {\n this.syncInProgress = false;\n }\n }\n\n /**\n * Sync on session start\n */\n async syncOnStart(): Promise<SyncResult | null> {\n if (this.config.syncOnSessionStart) {\n return await this.performSync('session-start');\n }\n return null;\n }\n\n /**\n * Sync on session end\n */\n async syncOnEnd(): Promise<SyncResult | null> {\n if (this.config.syncOnSessionEnd) {\n return await this.performSync('session-end');\n }\n return null;\n }\n\n /**\n * Update sync configuration\n */\n updateConfig(newConfig: Partial<SyncManagerConfig>): void {\n this.config = { ...this.config, ...newConfig };\n this.syncEngine.updateConfig(newConfig);\n\n // Restart periodic sync if interval changed\n if (\n newConfig.autoSyncInterval !== undefined ||\n newConfig.autoSync !== undefined\n ) {\n this.setupPeriodicSync();\n }\n }\n\n /**\n * Get sync status\n */\n getStatus(): {\n enabled: boolean;\n syncInProgress: boolean;\n lastSyncTime: number;\n nextSyncTime: number | null;\n config: SyncManagerConfig;\n } {\n const nextSyncTime =\n this.config.autoSync && this.config.autoSyncInterval\n ? this.lastSyncTime + this.config.autoSyncInterval * 60 * 1000\n : null;\n\n return {\n enabled: this.config.enabled,\n syncInProgress: this.syncInProgress,\n lastSyncTime: this.lastSyncTime,\n nextSyncTime,\n config: this.config,\n };\n }\n\n /**\n * Stop all sync activities\n */\n stop(): void {\n if (this.syncTimer) {\n clearInterval(this.syncTimer);\n this.syncTimer = undefined;\n }\n\n if (this.pendingSyncTimer) {\n clearTimeout(this.pendingSyncTimer);\n this.pendingSyncTimer = undefined;\n }\n\n this.removeAllListeners();\n logger.info('Linear sync manager stopped');\n }\n\n /**\n * Force an immediate sync\n */\n async forceSync(): Promise<SyncResult> {\n return await this.performSync('manual');\n }\n}\n\n/**\n * Default sync manager configuration\n */\nexport const DEFAULT_SYNC_MANAGER_CONFIG: SyncManagerConfig = {\n enabled: true,\n direction: 'bidirectional',\n autoSync: true,\n autoSyncInterval: 15, // minutes\n conflictResolution: 'newest_wins',\n syncOnTaskChange: true,\n syncOnSessionStart: true,\n syncOnSessionEnd: true,\n debounceInterval: 5000, // 5 seconds\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,17 @@ import { LinearClient } from "./client.js";
|
|
|
2
2
|
import { ContextService } from "../../services/context-service.js";
|
|
3
3
|
import { ConfigService } from "../../services/config-service.js";
|
|
4
4
|
import { Logger } from "../../utils/logger.js";
|
|
5
|
+
function getEnv(key, defaultValue) {
|
|
6
|
+
const value = process.env[key];
|
|
7
|
+
if (value === void 0) {
|
|
8
|
+
if (defaultValue !== void 0) return defaultValue;
|
|
9
|
+
throw new Error(`Environment variable ${key} is required`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function getOptionalEnv(key) {
|
|
14
|
+
return process.env[key];
|
|
15
|
+
}
|
|
5
16
|
class LinearSyncService {
|
|
6
17
|
linearClient;
|
|
7
18
|
contextService;
|
|
@@ -11,7 +22,7 @@ class LinearSyncService {
|
|
|
11
22
|
this.logger = new Logger("LinearSync");
|
|
12
23
|
this.configService = new ConfigService();
|
|
13
24
|
this.contextService = new ContextService();
|
|
14
|
-
const apiKey = process.env
|
|
25
|
+
const apiKey = process.env["LINEAR_API_KEY"];
|
|
15
26
|
if (!apiKey) {
|
|
16
27
|
throw new Error("LINEAR_API_KEY environment variable not set");
|
|
17
28
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/integrations/linear/sync-service.ts"],
|
|
4
|
-
"sourcesContent": ["import { LinearClient, LinearIssue, LinearCreateIssueInput } from './client.js';\nimport { ContextService } from '../../services/context-service.js';\nimport { ConfigService } from '../../services/config-service.js';\nimport { Logger } from '../../utils/logger.js';\nimport { Task, TaskStatus, TaskPriority } from '../../types/task.js';\n\n// Minimal issue data needed for sync (webhook payloads may have fewer fields)\nexport interface LinearIssueData {\n id: string;\n identifier: string;\n title: string;\n description?: string;\n state: { id?: string; name?: string; type: string };\n priority?: number;\n assignee?: { id: string; name: string };\n labels?: Array<{ name: string }>;\n url?: string;\n updatedAt: string;\n}\n\nexport interface SyncResult {\n created: number;\n updated: number;\n deleted: number;\n conflicts: number;\n errors: string[];\n}\n\nexport class LinearSyncService {\n private linearClient: LinearClient;\n private contextService: ContextService;\n private configService: ConfigService;\n private logger: Logger;\n\n constructor() {\n this.logger = new Logger('LinearSync');\n this.configService = new ConfigService();\n this.contextService = new ContextService();\n\n const apiKey = process.env
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAyD;AAClE,SAAS,sBAAsB;AAC/B,SAAS,qBAAqB;AAC9B,SAAS,cAAc;
|
|
4
|
+
"sourcesContent": ["import { LinearClient, LinearIssue, LinearCreateIssueInput } from './client.js';\nimport { ContextService } from '../../services/context-service.js';\nimport { ConfigService } from '../../services/config-service.js';\nimport { Logger } from '../../utils/logger.js';\nimport { Task, TaskStatus, TaskPriority } from '../../types/task.js';\n// Type-safe environment variable access\nfunction getEnv(key: string, defaultValue?: string): string {\n const value = process.env[key];\n if (value === undefined) {\n if (defaultValue !== undefined) return defaultValue;\n throw new Error(`Environment variable ${key} is required`);\n }\n return value;\n}\n\nfunction getOptionalEnv(key: string): string | undefined {\n return process.env[key];\n}\n\n\n// Minimal issue data needed for sync (webhook payloads may have fewer fields)\nexport interface LinearIssueData {\n id: string;\n identifier: string;\n title: string;\n description?: string;\n state: { id?: string; name?: string; type: string };\n priority?: number;\n assignee?: { id: string; name: string };\n labels?: Array<{ name: string }>;\n url?: string;\n updatedAt: string;\n}\n\nexport interface SyncResult {\n created: number;\n updated: number;\n deleted: number;\n conflicts: number;\n errors: string[];\n}\n\nexport class LinearSyncService {\n private linearClient: LinearClient;\n private contextService: ContextService;\n private configService: ConfigService;\n private logger: Logger;\n\n constructor() {\n this.logger = new Logger('LinearSync');\n this.configService = new ConfigService();\n this.contextService = new ContextService();\n\n const apiKey = process.env['LINEAR_API_KEY'];\n if (!apiKey) {\n throw new Error('LINEAR_API_KEY environment variable not set');\n }\n\n this.linearClient = new LinearClient({ apiKey });\n }\n\n public async syncAllIssues(): Promise<SyncResult> {\n const result: SyncResult = {\n created: 0,\n updated: 0,\n deleted: 0,\n conflicts: 0,\n errors: [],\n };\n\n try {\n const config = await this.configService.getConfig();\n const teamId = config.integrations?.linear?.teamId;\n\n if (!teamId) {\n throw new Error('Linear team ID not configured');\n }\n\n const issues = await this.linearClient.getIssues({ teamId });\n\n for (const issue of issues) {\n try {\n const synced = await this.syncIssueToLocal(issue);\n if (synced === 'created') result.created++;\n else if (synced === 'updated') result.updated++;\n } catch (error: unknown) {\n const message =\n error instanceof Error ? error.message : String(error);\n result.errors.push(`Failed to sync ${issue.identifier}: ${message}`);\n }\n }\n\n this.logger.info(\n `Sync complete: ${result.created} created, ${result.updated} updated`\n );\n } catch (error: unknown) {\n this.logger.error('Sync failed:', error);\n const message = error instanceof Error ? error.message : String(error);\n result.errors.push(message);\n }\n\n return result;\n }\n\n public async syncIssueToLocal(\n issue: LinearIssueData\n ): Promise<'created' | 'updated' | 'skipped'> {\n try {\n const task = this.convertIssueToTask(issue);\n const existingTask = await this.contextService.getTaskByExternalId(\n issue.id\n );\n\n if (existingTask) {\n if (this.hasChanges(existingTask, task)) {\n await this.contextService.updateTask(existingTask.id, task);\n this.logger.debug(`Updated task: ${issue.identifier}`);\n return 'updated';\n }\n return 'skipped';\n } else {\n await this.contextService.createTask(task);\n this.logger.debug(`Created task: ${issue.identifier}`);\n return 'created';\n }\n } catch (error: unknown) {\n this.logger.error(`Failed to sync issue ${issue.identifier}:`, error);\n throw error;\n }\n }\n\n public async syncLocalToLinear(taskId: string): Promise<any> {\n try {\n const task = await this.contextService.getTask(taskId);\n if (!task) {\n throw new Error(`Task ${taskId} not found`);\n }\n\n if (task.externalId) {\n const updateData = this.convertTaskToUpdateData(task);\n const updated = await this.linearClient.updateIssue(\n task.externalId,\n updateData\n );\n this.logger.debug(`Updated Linear issue: ${updated.identifier}`);\n return updated;\n } else {\n const config = await this.configService.getConfig();\n const teamId = config.integrations?.linear?.teamId;\n if (!teamId) {\n throw new Error('Linear team ID not configured');\n }\n const createData: LinearCreateIssueInput = {\n title: task.title,\n description: task.description,\n teamId,\n priority: this.mapTaskPriorityToLinearPriority(task.priority),\n };\n const created = await this.linearClient.createIssue(createData);\n await this.contextService.updateTask(taskId, {\n externalId: created.id,\n });\n this.logger.debug(`Created Linear issue: ${created.identifier}`);\n return created;\n }\n } catch (error: unknown) {\n this.logger.error(`Failed to sync task ${taskId} to Linear:`, error);\n throw error;\n }\n }\n\n public async removeLocalIssue(identifier: string): Promise<void> {\n try {\n const tasks = await this.contextService.getAllTasks();\n const task = tasks.find((t) => t.externalIdentifier === identifier);\n\n if (task) {\n await this.contextService.deleteTask(task.id);\n this.logger.debug(`Removed local task: ${identifier}`);\n }\n } catch (error: unknown) {\n this.logger.error(`Failed to remove task ${identifier}:`, error);\n throw error;\n }\n }\n\n private convertIssueToTask(issue: LinearIssueData): Partial<Task> {\n return {\n title: issue.title,\n description: issue.description || '',\n status: this.mapLinearStateToTaskStatus(issue.state.type),\n priority: this.mapLinearPriorityToTaskPriority(issue.priority),\n externalId: issue.id,\n externalIdentifier: issue.identifier,\n externalUrl: issue.url,\n tags: issue.labels?.map((l) => l.name) || [],\n metadata: {\n linear: {\n stateId: issue.state.id,\n stateName: issue.state.name,\n assigneeId: issue.assignee?.id,\n assigneeName: issue.assignee?.name,\n },\n },\n updatedAt: new Date(issue.updatedAt),\n };\n }\n\n private convertTaskToUpdateData(\n task: Task\n ): Partial<LinearCreateIssueInput> & { stateId?: string } {\n return {\n title: task.title,\n description: task.description,\n priority: this.mapTaskPriorityToLinearPriority(task.priority),\n stateId: task.metadata?.linear?.stateId as string | undefined,\n };\n }\n\n private mapLinearStateToTaskStatus(state: string): TaskStatus {\n switch (state.toLowerCase()) {\n case 'backlog':\n case 'triage':\n return 'todo';\n case 'unstarted':\n case 'todo':\n return 'todo';\n case 'started':\n case 'in_progress':\n return 'in_progress';\n case 'completed':\n case 'done':\n return 'done';\n case 'canceled':\n case 'cancelled':\n return 'cancelled';\n default:\n return 'todo';\n }\n }\n\n private mapTaskPriorityToLinearPriority(priority?: TaskPriority): number {\n switch (priority) {\n case 'urgent':\n return 1;\n case 'high':\n return 2;\n case 'medium':\n return 3;\n case 'low':\n return 4;\n default:\n return 0;\n }\n }\n\n private mapLinearPriorityToTaskPriority(\n priority?: number\n ): TaskPriority | undefined {\n switch (priority) {\n case 1:\n return 'urgent';\n case 2:\n return 'high';\n case 3:\n return 'medium';\n case 4:\n return 'low';\n default:\n return undefined;\n }\n }\n\n private hasChanges(existing: Task, updated: Partial<Task>): boolean {\n return (\n existing.title !== updated.title ||\n existing.description !== updated.description ||\n existing.status !== updated.status ||\n existing.priority !== updated.priority ||\n JSON.stringify(existing.tags) !== JSON.stringify(updated.tags)\n );\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|