@memoryrelay/mcp-server 0.1.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/logger.ts","../src/client.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * Configuration schema with security validation\n */\nexport const configSchema = z.object({\n apiKey: z.string()\n .startsWith('mem_', { message: 'API key must start with \"mem_\"' })\n .min(20, { message: 'API key appears to be invalid (too short)' }),\n apiUrl: z.string()\n .url({ message: 'API URL must be a valid URL' })\n .default('https://api.memoryrelay.net'),\n agentId: z.string()\n .optional()\n .describe('Agent identifier - auto-detected if not provided'),\n timeout: z.number()\n .positive({ message: 'Timeout must be positive' })\n .default(30000),\n logLevel: z.enum(['debug', 'info', 'warn', 'error'])\n .default('info'),\n});\n\nexport type Config = z.infer<typeof configSchema>;\n\n/**\n * Tool groups that can be enabled/disabled via MEMORYRELAY_TOOLS env var.\n * Default (when unset or 'all'): all groups enabled.\n */\nexport const TOOL_GROUPS: Record<string, string[]> = {\n core: [\n 'memory_store', 'memory_search', 'memory_list', 'memory_get',\n 'memory_update', 'memory_delete', 'entity_create', 'entity_link',\n 'entity_list', 'entity_graph', 'memory_batch_store', 'memory_context',\n 'agent_list', 'agent_create', 'agent_get', 'memory_health',\n ],\n sessions: ['session_start', 'session_end', 'session_recall', 'session_list'],\n decisions: ['decision_record', 'decision_list', 'decision_supersede', 'decision_check'],\n patterns: ['pattern_create', 'pattern_search', 'pattern_adopt', 'pattern_suggest'],\n projects: ['project_register', 'project_list', 'project_info'],\n relationships: [\n 'project_add_relationship', 'project_dependencies', 'project_dependents',\n 'project_related', 'project_impact', 'project_shared_patterns',\n ],\n context: ['project_context', 'memory_promote'],\n};\n\n/**\n * Parse MEMORYRELAY_TOOLS env var and return set of enabled tool names.\n * Returns null if all tools should be enabled (default behavior).\n */\nexport function getEnabledTools(): Set<string> | null {\n const raw = process.env.MEMORYRELAY_TOOLS;\n if (!raw || raw.trim().toLowerCase() === 'all') {\n return null; // all tools enabled\n }\n\n const groups = raw.split(',').map(g => g.trim().toLowerCase());\n const enabled = new Set<string>();\n for (const group of groups) {\n const tools = TOOL_GROUPS[group];\n if (tools) {\n for (const tool of tools) {\n enabled.add(tool);\n }\n }\n }\n return enabled;\n}\n\n/**\n * Load and validate configuration from environment variables\n */\nexport function loadConfig(): Config {\n try {\n const config = configSchema.parse({\n apiKey: process.env.MEMORYRELAY_API_KEY,\n apiUrl: process.env.MEMORYRELAY_API_URL,\n agentId: process.env.MEMORYRELAY_AGENT_ID,\n timeout: process.env.MEMORYRELAY_TIMEOUT \n ? parseInt(process.env.MEMORYRELAY_TIMEOUT, 10) \n : undefined,\n logLevel: process.env.MEMORYRELAY_LOG_LEVEL,\n });\n\n return config;\n } catch (error) {\n if (error instanceof z.ZodError) {\n const issues = error.issues.map(issue => \n ` - ${issue.path.join('.')}: ${issue.message}`\n ).join('\\n');\n \n throw new Error(\n `Configuration validation failed:\\n${issues}\\n\\n` +\n 'Please check your environment variables:\\n' +\n ' - MEMORYRELAY_API_KEY (required, starts with \"mem_\")\\n' +\n ' - MEMORYRELAY_API_URL (optional, default: https://api.memoryrelay.net)\\n' +\n ' - MEMORYRELAY_AGENT_ID (optional, auto-detected)\\n' +\n ' - MEMORYRELAY_TIMEOUT (optional, default: 30000)\\n' +\n ' - MEMORYRELAY_LOG_LEVEL (optional, default: info)',\n { cause: error }\n );\n }\n throw error;\n }\n}\n\n/**\n * Get or generate agent ID.\n *\n * Priority order:\n * 1. MEMORYRELAY_AGENT_ID (explicit configuration)\n * 2. OPENCLAW_AGENT_NAME (OpenClaw auto-detection)\n * 3. Auto-generated from username + hostname\n *\n * When no explicit ID is provided we build a human-readable identifier\n * from the current user and hostname so that memories are easier to\n * attribute in the dashboard (e.g. \"sparc-DESKTOP\" instead of \"agent-a1b2c3d4\").\n */\nexport function getAgentId(config: Config): string {\n if (config.agentId) {\n return config.agentId;\n }\n\n // OpenClaw auto-detection: use agent name from OpenClaw environment\n const openclawAgent = process.env.OPENCLAW_AGENT_NAME;\n if (openclawAgent) {\n return openclawAgent.slice(0, 32);\n }\n\n const hostname = process.env.HOSTNAME || process.env.COMPUTERNAME || 'unknown';\n const user = process.env.USER || process.env.USERNAME || '';\n return user\n ? `${user}-${hostname}`.slice(0, 32)\n : `mcp-${hostname}`.slice(0, 32);\n}\n","/**\n * Security-hardened logger for MCP server\n * \n * - All output to stderr (stdout reserved for MCP protocol)\n * - Automatic masking of API keys (anything starting with \"mem_\")\n * - No internal paths in error messages\n */\n\ntype LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nconst LOG_LEVELS: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\nexport class Logger {\n private minLevel: number;\n\n constructor(level: LogLevel = 'info') {\n this.minLevel = LOG_LEVELS[level];\n }\n\n /**\n * Mask sensitive data in log messages\n * - API keys starting with \"mem_\" are masked\n * - Internal paths are sanitized\n */\n private sanitize(message: string): string {\n let sanitized = message;\n\n // Mask API keys (mem_xxx -> mem_****)\n sanitized = sanitized.replace(/mem_[a-zA-Z0-9_-]+/g, 'mem_****');\n\n // Remove internal paths (anything that looks like a file path)\n sanitized = sanitized.replace(/\\/[a-zA-Z0-9_\\-./]+\\.(ts|js|json)/g, '<file>');\n sanitized = sanitized.replace(/at\\s+[^\\s]+\\s+\\([^)]+\\)/g, 'at <location>');\n\n return sanitized;\n }\n\n /**\n * Format log message with timestamp and level\n */\n private format(level: LogLevel, message: string, data?: unknown): string {\n const timestamp = new Date().toISOString();\n const sanitizedMessage = this.sanitize(message);\n \n let output = `[${timestamp}] [${level.toUpperCase()}] ${sanitizedMessage}`;\n \n if (data !== undefined) {\n const sanitizedData = this.sanitize(JSON.stringify(data, null, 2));\n output += `\\n${sanitizedData}`;\n }\n \n return output;\n }\n\n debug(message: string, data?: unknown): void {\n if (this.minLevel <= LOG_LEVELS.debug) {\n console.error(this.format('debug', message, data));\n }\n }\n\n info(message: string, data?: unknown): void {\n if (this.minLevel <= LOG_LEVELS.info) {\n console.error(this.format('info', message, data));\n }\n }\n\n warn(message: string, data?: unknown): void {\n if (this.minLevel <= LOG_LEVELS.warn) {\n console.error(this.format('warn', message, data));\n }\n }\n\n error(message: string, data?: unknown): void {\n if (this.minLevel <= LOG_LEVELS.error) {\n console.error(this.format('error', message, data));\n }\n }\n}\n\n// Export singleton instance\nlet logger: Logger;\n\nexport function initLogger(level: LogLevel = 'info'): Logger {\n logger = new Logger(level);\n return logger;\n}\n\nexport function getLogger(): Logger {\n if (!logger) {\n logger = new Logger();\n }\n return logger;\n}\n","/**\n * MemoryRelay API client with retry logic and error handling\n */\n\nimport type {\n Memory,\n Entity,\n Agent,\n Session,\n SessionDetail,\n Decision,\n DecisionCheckResult,\n Project,\n Pattern,\n PatternSearchResult,\n SearchResult,\n ListResponse,\n ClientConfig,\n EntityType,\n BatchMemoryItem,\n BatchStoreResponse,\n} from './types.js';\nimport { getLogger } from './logger.js';\n\n// Retry configuration\nconst MAX_RETRIES = 3;\nconst INITIAL_DELAY_MS = 1000;\nconst MAX_CONTENT_SIZE = 50 * 1024; // 50KB\n\n/**\n * Exponential backoff retry wrapper\n */\nasync function withRetry<T>(\n fn: () => Promise<T>,\n retries: number = MAX_RETRIES\n): Promise<T> {\n let lastError: Error | undefined;\n \n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n \n // Don't retry on certain errors\n if (\n lastError.message.includes('401') ||\n lastError.message.includes('403') ||\n lastError.message.includes('404') ||\n lastError.message.includes('400')\n ) {\n throw lastError;\n }\n \n // On last attempt, throw the error\n if (attempt === retries) {\n throw lastError;\n }\n \n // Exponential backoff with jitter\n const delay = INITIAL_DELAY_MS * Math.pow(2, attempt);\n const jitter = Math.random() * 0.3 * delay;\n await new Promise(resolve => setTimeout(resolve, delay + jitter));\n }\n }\n \n throw lastError || new Error('Retry failed');\n}\n\n/**\n * Mask API key in error messages for security\n */\nfunction maskApiKey(message: string, apiKey: string): string {\n if (!apiKey) return message;\n const maskedKey = apiKey.substring(0, 8) + '***';\n return message.replace(new RegExp(apiKey, 'g'), maskedKey);\n}\n\nexport class MemoryRelayClient {\n private config: ClientConfig;\n private logger = getLogger();\n\n constructor(config: ClientConfig) {\n this.config = config;\n this.logger.info('MemoryRelay client initialized', {\n apiUrl: config.apiUrl,\n agentId: config.agentId,\n });\n }\n\n /**\n * Make authenticated HTTP request to MemoryRelay API with retry logic\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> {\n return withRetry(async () => {\n const url = `${this.config.apiUrl}${path}`;\n \n this.logger.debug(`API request: ${method} ${path}`);\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.config.apiKey}`,\n 'User-Agent': '@memoryrelay/mcp-server',\n },\n body: body ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n\n if (!response.ok) {\n // Handle rate limiting with retry\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : 5000;\n this.logger.warn(`Rate limited, waiting ${waitMs}ms`);\n await new Promise(resolve => setTimeout(resolve, waitMs));\n throw new Error(`Rate limited: 429 - Retry after ${waitMs}ms`);\n }\n\n const errorData = await response.json().catch(() => ({})) as { message?: string };\n const errorMsg = `API request failed: ${response.status} ${response.statusText}` +\n (errorData.message ? ` - ${errorData.message}` : '');\n \n // Mask API key in error message\n throw new Error(maskApiKey(errorMsg, this.config.apiKey));\n }\n\n const data = await response.json();\n this.logger.debug(`API response: ${method} ${path}`, { status: response.status });\n \n return data as T;\n } catch (error) {\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new Error(`Request timeout after ${this.config.timeout}ms`, { cause: error });\n }\n // Mask API key in all error messages\n error.message = maskApiKey(error.message, this.config.apiKey);\n }\n throw error;\n } finally {\n clearTimeout(timeout);\n }\n });\n }\n\n /**\n * Validate content size\n */\n private validateContentSize(content: string): void {\n if (content.length > MAX_CONTENT_SIZE) {\n throw new Error(`Content exceeds maximum size of ${MAX_CONTENT_SIZE} bytes`);\n }\n }\n\n /**\n * Store a new memory\n */\n async storeMemory(\n content: string,\n metadata?: Record<string, string>,\n deduplicate?: boolean,\n dedupThreshold?: number,\n project?: string,\n importance?: number,\n tier?: string\n ): Promise<Memory> {\n this.validateContentSize(content);\n\n const body: Record<string, unknown> = {\n content,\n metadata,\n agent_id: this.config.agentId,\n };\n if (deduplicate) {\n body.deduplicate = true;\n }\n if (dedupThreshold !== undefined) {\n body.dedup_threshold = dedupThreshold;\n }\n if (project) {\n body.project = project;\n }\n if (importance !== undefined) {\n body.importance = importance;\n }\n if (tier) {\n body.tier = tier;\n }\n\n return this.request<Memory>('POST', '/v1/memories', body);\n }\n\n /**\n * Search memories using semantic search\n * @param agentId - Optional agent ID override. If omitted, uses config agentId. Pass null for cross-agent search.\n * @param includeConfidential - Include confidential memories in results\n * @param includeArchived - Include archived memories in results\n * @param project - Optional project slug to filter by\n */\n async searchMemories(\n query: string,\n limit: number = 10,\n threshold: number = 0.5,\n agentId?: string | null,\n includeConfidential: boolean = false,\n includeArchived: boolean = false,\n compress: boolean = false,\n maxContextTokens?: number,\n project?: string,\n tier?: string,\n minImportance?: number\n ): Promise<SearchResult[]> {\n this.validateContentSize(query);\n\n // If agentId is explicitly null, omit it (cross-agent search).\n // If agentId is undefined, use the default from config.\n const effectiveAgentId = agentId === null ? undefined : (agentId ?? this.config.agentId);\n\n const body: Record<string, unknown> = { query, limit, threshold };\n if (effectiveAgentId) {\n body.agent_id = effectiveAgentId;\n }\n if (includeConfidential) {\n body.include_confidential = true;\n }\n if (includeArchived) {\n body.include_archived = true;\n }\n if (compress) {\n body.compress = true;\n }\n if (maxContextTokens !== undefined) {\n body.max_context_tokens = maxContextTokens;\n }\n if (project) {\n body.project = project;\n }\n if (tier) {\n body.tier = tier;\n }\n if (minImportance !== undefined) {\n body.min_importance = minImportance;\n }\n\n const response = await this.request<{ data: SearchResult[] }>(\n 'POST',\n '/v1/memories/search',\n body\n );\n return response.data;\n }\n\n /**\n * List recent memories with pagination\n */\n async listMemories(limit: number = 20, offset: number = 0): Promise<ListResponse<Memory>> {\n return this.request<ListResponse<Memory>>(\n 'GET',\n `/v1/memories?limit=${limit}&offset=${offset}`\n );\n }\n\n /**\n * Get a specific memory by ID\n */\n async getMemory(id: string): Promise<Memory> {\n return this.request<Memory>('GET', `/v1/memories/${id}`);\n }\n\n /**\n * Update an existing memory\n */\n async updateMemory(\n id: string,\n content: string,\n metadata?: Record<string, string>\n ): Promise<Memory> {\n this.validateContentSize(content);\n \n return this.request<Memory>('PATCH', `/v1/memories/${id}`, {\n content,\n metadata,\n });\n }\n\n /**\n * Delete a memory\n */\n async deleteMemory(id: string): Promise<void> {\n await this.request<void>('DELETE', `/v1/memories/${id}`);\n }\n\n /**\n * Create a named entity\n */\n async createEntity(\n name: string,\n type: EntityType,\n metadata?: Record<string, string>\n ): Promise<Entity> {\n this.validateContentSize(name);\n \n return this.request<Entity>('POST', '/v1/entities', {\n name,\n type,\n metadata,\n });\n }\n\n /**\n * Link an entity to a memory\n */\n async linkEntity(\n entityId: string,\n memoryId: string,\n relationship: string = 'mentioned_in'\n ): Promise<void> {\n await this.request<void>('POST', '/v1/entities/links', {\n entity_id: entityId,\n memory_id: memoryId,\n relationship,\n });\n }\n\n /**\n * Get an entity by ID\n */\n async getEntity(id: string): Promise<Entity> {\n return this.request<Entity>('GET', `/v1/entities/${id}`);\n }\n\n /**\n * List entities with pagination\n */\n async listEntities(limit: number = 20, offset: number = 0): Promise<ListResponse<Entity>> {\n return this.request<ListResponse<Entity>>(\n 'GET',\n `/v1/entities?limit=${limit}&offset=${offset}`\n );\n }\n\n /**\n * Get entity neighborhood (ego-centric subgraph).\n * Returns the entity's 1-hop or 2-hop neighbors and relationships.\n */\n async getEntityNeighborhood(\n entityId: string,\n depth: number = 1,\n maxNeighbors: number = 50\n ): Promise<Record<string, unknown>> {\n return this.request<Record<string, unknown>>(\n 'GET',\n `/v1/entities/${entityId}/neighborhood?depth=${depth}&max_neighbors=${maxNeighbors}`\n );\n }\n\n /**\n * Delete an entity\n */\n async deleteEntity(id: string): Promise<void> {\n await this.request<void>('DELETE', `/v1/entities/${id}`);\n }\n\n /**\n * List agents with pagination\n */\n async listAgents(limit: number = 20, offset: number = 0): Promise<ListResponse<Agent>> {\n return this.request<ListResponse<Agent>>(\n 'GET',\n `/v1/agents?limit=${limit}&offset=${offset}`\n );\n }\n\n /**\n * Create a new agent\n */\n async createAgent(\n name: string,\n description?: string,\n metadata?: Record<string, string>\n ): Promise<Agent> {\n const body: Record<string, unknown> = { name };\n if (description) body.description = description;\n if (metadata) body.metadata = metadata;\n\n return this.request<Agent>('POST', '/v1/agents', body);\n }\n\n /**\n * Get an agent by ID\n */\n async getAgent(id: string): Promise<Agent> {\n return this.request<Agent>('GET', `/v1/agents/${id}`);\n }\n\n /**\n * Batch store multiple memories in a single API call.\n * Uses the /v1/memories/batch endpoint.\n */\n async batchStoreMemories(\n items: BatchMemoryItem[]\n ): Promise<BatchStoreResponse> {\n if (items.length === 0) {\n return { success: true, total: 0, succeeded: 0, failed: 0, skipped: 0, results: [] };\n }\n if (items.length > 100) {\n throw new Error('Batch size exceeds maximum of 100 memories');\n }\n\n // Attach default agent_id to items that don't have one\n const memories = items.map(item => ({\n content: item.content,\n metadata: item.metadata || {},\n agent_id: item.agent_id || this.config.agentId,\n }));\n\n return this.request<BatchStoreResponse>('POST', '/v1/memories/batch', { memories });\n }\n\n /**\n * Build a context string from search results.\n * Searches for relevant memories, formats them, and returns\n * a single string ready for prompt injection.\n */\n async buildContext(\n query: string,\n limit: number = 10,\n threshold: number = 0.5,\n maxTokens?: number\n ): Promise<{ context: string; memories_used: number; total_chars: number }> {\n const results = await this.searchMemories(\n query,\n limit,\n threshold,\n undefined, // use default agent\n false, // no confidential\n false, // no archived\n !!maxTokens, // compress if token budget provided\n maxTokens\n );\n\n if (results.length === 0) {\n return { context: '', memories_used: 0, total_chars: 0 };\n }\n\n const lines: string[] = [];\n for (const result of results) {\n const score = (result.score * 100).toFixed(0);\n lines.push(`[${score}%] ${result.memory.content}`);\n }\n\n const context = lines.join('\\n\\n');\n\n // Rough token budget enforcement (1 token ≈ 4 chars)\n let finalContext = context;\n if (maxTokens) {\n const charBudget = maxTokens * 4;\n if (finalContext.length > charBudget) {\n finalContext = finalContext.slice(0, charBudget) + '\\n\\n[...truncated]';\n }\n }\n\n return {\n context: finalContext,\n memories_used: results.length,\n total_chars: finalContext.length,\n };\n }\n\n /**\n * Start a new session\n */\n async startSession(\n title?: string,\n project?: string,\n metadata?: Record<string, string>\n ): Promise<Session> {\n const body: Record<string, unknown> = {};\n if (this.config.agentId) body.agent_id = this.config.agentId;\n if (title) body.title = title;\n if (project) body.project = project;\n if (metadata) body.metadata = metadata;\n return this.request<Session>('POST', '/v1/sessions', body);\n }\n\n /**\n * End an active session\n */\n async endSession(\n sessionId: string,\n summary?: string,\n ): Promise<Session> {\n const body: Record<string, unknown> = {};\n if (summary) body.summary = summary;\n return this.request<Session>('PUT', `/v1/sessions/${sessionId}/end`, body);\n }\n\n /**\n * Get a session by ID with its memories\n */\n async getSession(sessionId: string): Promise<SessionDetail> {\n return this.request<SessionDetail>(\n 'GET',\n `/v1/sessions/${sessionId}?include_memories=true`\n );\n }\n\n /**\n * List sessions with optional filters\n */\n async listSessions(\n limit: number = 20,\n agentId?: string,\n project?: string,\n status?: string,\n ): Promise<ListResponse<Session>> {\n const params = new URLSearchParams();\n params.set('limit', String(limit));\n const effectiveAgentId = agentId ?? this.config.agentId;\n if (effectiveAgentId) params.set('agent_id', effectiveAgentId);\n if (project) params.set('project', project);\n if (status) params.set('status', status);\n return this.request<ListResponse<Session>>(\n 'GET',\n `/v1/sessions?${params.toString()}`\n );\n }\n\n /**\n * Record a new decision\n */\n async recordDecision(\n title: string,\n rationale: string,\n alternatives?: string,\n project?: string,\n tags?: string[],\n status?: string,\n metadata?: Record<string, string>\n ): Promise<Decision> {\n const body: Record<string, unknown> = { title, rationale };\n if (this.config.agentId) body.agent_id = this.config.agentId;\n if (alternatives) body.alternatives = alternatives;\n if (project) body.project_slug = project;\n if (tags) body.tags = tags;\n if (status) body.status = status;\n if (metadata) body.metadata = metadata;\n return this.request<Decision>('POST', '/v1/decisions', body);\n }\n\n /**\n * List decisions with optional filters\n */\n async listDecisions(\n limit?: number,\n project?: string,\n status?: string,\n tags?: string,\n ): Promise<ListResponse<Decision>> {\n const params = new URLSearchParams();\n if (limit) params.set('limit', String(limit));\n if (project) params.set('project', project);\n if (status) params.set('status', status);\n if (tags) params.set('tags', tags);\n return this.request<ListResponse<Decision>>(\n 'GET',\n `/v1/decisions?${params.toString()}`\n );\n }\n\n /**\n * Supersede a decision with a new one\n */\n async supersedeDecision(\n decisionId: string,\n title: string,\n rationale: string,\n alternatives?: string,\n tags?: string[],\n ): Promise<Decision> {\n const body: Record<string, unknown> = { title, rationale };\n if (alternatives) body.alternatives = alternatives;\n if (tags) body.tags = tags;\n return this.request<Decision>(\n 'POST',\n `/v1/decisions/${decisionId}/supersede`,\n body\n );\n }\n\n /**\n * Check for existing decisions about a topic (semantic search)\n */\n async checkDecisions(\n query: string,\n project?: string,\n limit?: number,\n threshold?: number,\n includeSuperseded?: boolean,\n ): Promise<{ data: DecisionCheckResult[]; query: string; total: number }> {\n const params = new URLSearchParams();\n params.set('query', query);\n if (limit) params.set('limit', String(limit));\n if (threshold !== undefined) params.set('threshold', String(threshold));\n if (project) params.set('project', project);\n if (includeSuperseded) params.set('include_superseded', 'true');\n return this.request<{ data: DecisionCheckResult[]; query: string; total: number }>(\n 'GET',\n `/v1/decisions/check?${params.toString()}`\n );\n }\n\n /**\n * Register a new project\n */\n async createProject(\n slug: string,\n name: string,\n description?: string,\n stack?: Record<string, unknown>,\n repo_url?: string,\n metadata?: Record<string, unknown>\n ): Promise<Project> {\n const body: Record<string, unknown> = { slug, name };\n if (description) body.description = description;\n if (stack) body.stack = stack;\n if (repo_url) body.repo_url = repo_url;\n if (metadata) body.metadata = metadata;\n return this.request<Project>('POST', '/v1/projects', body);\n }\n\n /**\n * List projects with optional pagination\n */\n async listProjects(\n limit: number = 20,\n cursor?: string\n ): Promise<ListResponse<Project>> {\n const params = new URLSearchParams();\n params.set('limit', String(limit));\n if (cursor) params.set('cursor', cursor);\n return this.request<ListResponse<Project>>(\n 'GET',\n `/v1/projects?${params.toString()}`\n );\n }\n\n /**\n * Get a project by slug\n */\n async getProject(slug: string): Promise<Project> {\n return this.request<Project>('GET', `/v1/projects/${slug}`);\n }\n\n /**\n * Create a reusable pattern\n */\n async createPattern(\n title: string,\n description: string,\n category?: string,\n example_code?: string,\n scope?: string,\n tags?: string[],\n source_project?: string,\n metadata?: Record<string, unknown>\n ): Promise<Pattern> {\n const body: Record<string, unknown> = { title, description };\n if (category) body.category = category;\n if (example_code) body.example_code = example_code;\n if (scope) body.scope = scope;\n if (tags) body.tags = tags;\n if (source_project) body.source_project = source_project;\n if (metadata) body.metadata = metadata;\n return this.request<Pattern>('POST', '/v1/patterns', body);\n }\n\n /**\n * Search patterns using semantic search\n */\n async searchPatterns(\n query: string,\n category?: string,\n project?: string,\n limit?: number,\n threshold?: number,\n ): Promise<{ data: PatternSearchResult[]; query: string; total: number }> {\n const params = new URLSearchParams();\n params.set('query', query);\n if (category) params.set('category', category);\n if (project) params.set('project', project);\n if (limit) params.set('limit', String(limit));\n if (threshold !== undefined) params.set('threshold', String(threshold));\n return this.request<{ data: PatternSearchResult[]; query: string; total: number }>(\n 'GET',\n `/v1/patterns/search?${params.toString()}`\n );\n }\n\n /**\n * Adopt a pattern for a project\n */\n async adoptPattern(\n patternId: string,\n project: string,\n ): Promise<Pattern> {\n return this.request<Pattern>(\n 'POST',\n `/v1/patterns/${patternId}/adopt`,\n { project }\n );\n }\n\n /**\n * Suggest patterns for a project\n */\n async suggestPatterns(\n project: string,\n limit?: number,\n ): Promise<{ data: Pattern[]; project: string; total: number }> {\n const params = new URLSearchParams();\n params.set('project', project);\n if (limit) params.set('limit', String(limit));\n return this.request<{ data: Pattern[]; project: string; total: number }>(\n 'GET',\n `/v1/patterns/suggest?${params.toString()}`\n );\n }\n\n // ── Project Relationships (Issue #186) ──\n\n /**\n * Add a relationship between two projects\n */\n async addProjectRelationship(\n sourceSlug: string,\n targetProject: string,\n relationshipType: string,\n metadata?: Record<string, unknown>,\n ): Promise<Record<string, unknown>> {\n const body: Record<string, unknown> = {\n target_project: targetProject,\n relationship_type: relationshipType,\n };\n if (metadata) body.metadata = metadata;\n return this.request<Record<string, unknown>>(\n 'POST',\n `/v1/projects/${sourceSlug}/relationships`,\n body,\n );\n }\n\n /**\n * Get what this project depends on\n */\n async getProjectDependencies(slug: string): Promise<Record<string, unknown>> {\n return this.request<Record<string, unknown>>(\n 'GET',\n `/v1/projects/${slug}/dependencies`,\n );\n }\n\n /**\n * Get what depends on this project\n */\n async getProjectDependents(slug: string): Promise<Record<string, unknown>> {\n return this.request<Record<string, unknown>>(\n 'GET',\n `/v1/projects/${slug}/dependents`,\n );\n }\n\n /**\n * Get all related projects\n */\n async getProjectRelated(slug: string): Promise<Record<string, unknown>> {\n return this.request<Record<string, unknown>>(\n 'GET',\n `/v1/projects/${slug}/related`,\n );\n }\n\n /**\n * Run impact analysis for a project change\n */\n async projectImpactAnalysis(\n project: string,\n changeDescription: string,\n ): Promise<Record<string, unknown>> {\n return this.request<Record<string, unknown>>(\n 'POST',\n '/v1/projects/impact-analysis',\n { project, change_description: changeDescription },\n );\n }\n\n /**\n * Find patterns shared between two projects\n */\n async getSharedPatterns(\n slugA: string,\n slugB: string,\n ): Promise<Record<string, unknown>> {\n return this.request<Record<string, unknown>>(\n 'GET',\n `/v1/projects/shared-patterns?a=${encodeURIComponent(slugA)}&b=${encodeURIComponent(slugB)}`,\n );\n }\n\n /**\n * Get full project context (hot memories, decisions, patterns, formatted text)\n */\n async getProjectContext(slug: string): Promise<Record<string, unknown>> {\n return this.request<Record<string, unknown>>(\n 'GET',\n `/v1/projects/${encodeURIComponent(slug)}/context`,\n );\n }\n\n /**\n * Promote/demote a memory by updating its importance and tier\n */\n async promoteMemory(\n memoryId: string,\n importance: number,\n tier?: string\n ): Promise<Memory> {\n const body: Record<string, unknown> = { importance };\n if (tier) {\n body.tier = tier;\n }\n return this.request<Memory>(\n 'PUT',\n `/v1/memories/${encodeURIComponent(memoryId)}/importance`,\n body,\n );\n }\n\n /**\n * Health check - verify API connectivity\n */\n async healthCheck(): Promise<{ status: string; message: string }> {\n try {\n // Simple GET request to check API is reachable\n await this.request<{ status: string }>('GET', '/v1/health');\n return {\n status: 'healthy',\n message: 'API connection successful',\n };\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : 'Unknown error';\n return {\n status: 'unhealthy',\n message: `API connection failed: ${errorMsg}`,\n };\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Interactive setup wizard for MemoryRelay MCP server.
4
+ *
5
+ * Detects installed MCP clients (Claude Desktop, Claude Code, OpenClaw),
6
+ * validates the API key, and writes the correct configuration.
7
+ *
8
+ * Usage: npx memoryrelay-mcp setup
9
+ */
10
+ declare function runSetup(): Promise<void>;
11
+
12
+ export { runSetup };
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/setup.ts
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import * as os from "os";
7
+ import * as readline from "readline";
8
+ function stderr(msg) {
9
+ process.stderr.write(msg + "\n");
10
+ }
11
+ function createReadline() {
12
+ return readline.createInterface({
13
+ input: process.stdin,
14
+ output: process.stderr
15
+ });
16
+ }
17
+ function ask(rl, question) {
18
+ return new Promise((resolve) => {
19
+ rl.question(question, (answer) => resolve(answer.trim()));
20
+ });
21
+ }
22
+ function getClaudeDesktopConfigPath() {
23
+ const platform2 = os.platform();
24
+ const home = os.homedir();
25
+ if (platform2 === "darwin") {
26
+ return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
27
+ } else if (platform2 === "win32") {
28
+ return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
29
+ } else {
30
+ return path.join(home, ".config", "Claude", "claude_desktop_config.json");
31
+ }
32
+ }
33
+ function getClaudeCodeConfigPath() {
34
+ const home = os.homedir();
35
+ return path.join(home, ".claude", "settings.json");
36
+ }
37
+ function getOpenClawConfigPath() {
38
+ const home = os.homedir();
39
+ return path.join(home, ".openclaw", "config.json");
40
+ }
41
+ function detectClients() {
42
+ const clients = [
43
+ {
44
+ name: "Claude Desktop",
45
+ configPath: getClaudeDesktopConfigPath(),
46
+ exists: false
47
+ },
48
+ {
49
+ name: "Claude Code",
50
+ configPath: getClaudeCodeConfigPath(),
51
+ exists: false
52
+ },
53
+ {
54
+ name: "OpenClaw",
55
+ configPath: getOpenClawConfigPath(),
56
+ exists: false
57
+ }
58
+ ];
59
+ for (const client of clients) {
60
+ try {
61
+ const dir = path.dirname(client.configPath);
62
+ client.exists = fs.existsSync(dir);
63
+ } catch {
64
+ client.exists = false;
65
+ }
66
+ }
67
+ return clients;
68
+ }
69
+ async function validateApiKey(apiKey, apiUrl) {
70
+ try {
71
+ const controller = new AbortController();
72
+ const timeout = setTimeout(() => controller.abort(), 1e4);
73
+ const response = await fetch(`${apiUrl}/v1/health`, {
74
+ headers: {
75
+ "Authorization": `Bearer ${apiKey}`,
76
+ "User-Agent": "@memoryrelay/mcp-server setup"
77
+ },
78
+ signal: controller.signal
79
+ });
80
+ clearTimeout(timeout);
81
+ if (response.ok) {
82
+ return { valid: true, message: "API key validated successfully" };
83
+ } else if (response.status === 401 || response.status === 403) {
84
+ return { valid: false, message: "Invalid API key. Check your key at https://app.memoryrelay.ai/settings" };
85
+ } else {
86
+ return { valid: false, message: `Unexpected response: ${response.status} ${response.statusText}` };
87
+ }
88
+ } catch (error) {
89
+ if (error instanceof Error && error.name === "AbortError") {
90
+ return { valid: false, message: "Connection timed out. Check your network and API URL." };
91
+ }
92
+ return { valid: false, message: `Connection failed: ${error instanceof Error ? error.message : "Unknown error"}` };
93
+ }
94
+ }
95
+ function buildServerConfig(apiKey, apiUrl, agentId) {
96
+ const env = {
97
+ MEMORYRELAY_API_KEY: apiKey
98
+ };
99
+ if (apiUrl !== "https://api.memoryrelay.net") {
100
+ env.MEMORYRELAY_API_URL = apiUrl;
101
+ }
102
+ if (agentId) {
103
+ env.MEMORYRELAY_AGENT_ID = agentId;
104
+ }
105
+ return {
106
+ command: "npx",
107
+ args: ["-y", "@memoryrelay/mcp-server"],
108
+ env
109
+ };
110
+ }
111
+ function writeConfig(configPath, serverConfig) {
112
+ try {
113
+ let config = {};
114
+ if (fs.existsSync(configPath)) {
115
+ const raw = fs.readFileSync(configPath, "utf-8");
116
+ config = JSON.parse(raw);
117
+ }
118
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
119
+ config.mcpServers = {};
120
+ }
121
+ config.mcpServers.memoryrelay = serverConfig;
122
+ const dir = path.dirname(configPath);
123
+ if (!fs.existsSync(dir)) {
124
+ fs.mkdirSync(dir, { recursive: true });
125
+ }
126
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
127
+ return { success: true, message: `Configuration written to ${configPath}` };
128
+ } catch (error) {
129
+ return {
130
+ success: false,
131
+ message: `Failed to write config: ${error instanceof Error ? error.message : "Unknown error"}`
132
+ };
133
+ }
134
+ }
135
+ async function runSetup() {
136
+ const rl = createReadline();
137
+ stderr("");
138
+ stderr(" MemoryRelay MCP Server Setup");
139
+ stderr(" ============================");
140
+ stderr("");
141
+ stderr(" Detecting MCP clients...");
142
+ const clients = detectClients();
143
+ const detectedClients = clients.filter((c) => c.exists);
144
+ if (detectedClients.length === 0) {
145
+ stderr("");
146
+ stderr(" No MCP clients detected. Supported clients:");
147
+ stderr(" - Claude Desktop (https://claude.ai/download)");
148
+ stderr(" - Claude Code (https://claude.ai/code)");
149
+ stderr(" - OpenClaw");
150
+ stderr("");
151
+ stderr(" You can still configure manually. Continuing...");
152
+ stderr("");
153
+ } else {
154
+ stderr("");
155
+ for (const client of detectedClients) {
156
+ stderr(` [+] ${client.name} found`);
157
+ }
158
+ stderr("");
159
+ }
160
+ const apiKey = await ask(rl, " API Key (starts with mem_): ");
161
+ if (!apiKey.startsWith("mem_")) {
162
+ stderr("");
163
+ stderr(' Error: API key must start with "mem_"');
164
+ stderr(" Get your key at https://app.memoryrelay.ai/settings");
165
+ rl.close();
166
+ process.exit(1);
167
+ }
168
+ if (apiKey.length < 20) {
169
+ stderr("");
170
+ stderr(" Error: API key appears too short");
171
+ rl.close();
172
+ process.exit(1);
173
+ }
174
+ const apiUrlInput = await ask(rl, " API URL [https://api.memoryrelay.net]: ");
175
+ const apiUrl = apiUrlInput || "https://api.memoryrelay.net";
176
+ const agentId = await ask(rl, " Agent ID (optional, auto-detected if blank): ");
177
+ stderr("");
178
+ stderr(" Validating API key...");
179
+ const validation = await validateApiKey(apiKey, apiUrl);
180
+ if (!validation.valid) {
181
+ stderr(` Error: ${validation.message}`);
182
+ stderr("");
183
+ const proceed = await ask(rl, " Continue anyway? (y/N): ");
184
+ if (proceed.toLowerCase() !== "y") {
185
+ rl.close();
186
+ process.exit(1);
187
+ }
188
+ } else {
189
+ stderr(` ${validation.message}`);
190
+ }
191
+ const serverConfig = buildServerConfig(apiKey, apiUrl, agentId || void 0);
192
+ const configuredClients = [];
193
+ stderr("");
194
+ if (detectedClients.length > 0) {
195
+ for (const client of detectedClients) {
196
+ const answer = await ask(rl, ` Configure ${client.name}? (Y/n): `);
197
+ if (answer.toLowerCase() !== "n") {
198
+ const result = writeConfig(client.configPath, serverConfig);
199
+ if (result.success) {
200
+ stderr(` ${result.message}`);
201
+ configuredClients.push(client.name);
202
+ } else {
203
+ stderr(` ${result.message}`);
204
+ }
205
+ }
206
+ }
207
+ } else {
208
+ for (const client of clients) {
209
+ const answer = await ask(rl, ` Configure ${client.name}? (y/N): `);
210
+ if (answer.toLowerCase() === "y") {
211
+ const result = writeConfig(client.configPath, serverConfig);
212
+ if (result.success) {
213
+ stderr(` ${result.message}`);
214
+ configuredClients.push(client.name);
215
+ } else {
216
+ stderr(` ${result.message}`);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ stderr("");
222
+ stderr(" Setup complete!");
223
+ stderr(" ===============");
224
+ stderr("");
225
+ if (configuredClients.length > 0) {
226
+ stderr(` Configured: ${configuredClients.join(", ")}`);
227
+ stderr("");
228
+ stderr(" Restart your MCP client to activate MemoryRelay.");
229
+ } else {
230
+ stderr(" No clients were configured. To configure manually, add");
231
+ stderr(" the following to your MCP client config:");
232
+ stderr("");
233
+ stderr(JSON.stringify({ mcpServers: { memoryrelay: serverConfig } }, null, 2).split("\n").map((line) => " " + line).join("\n"));
234
+ }
235
+ stderr("");
236
+ stderr(" Documentation: https://github.com/memoryrelay/mcp-server");
237
+ stderr("");
238
+ rl.close();
239
+ }
240
+ export {
241
+ runSetup
242
+ };
243
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cli/setup.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Interactive setup wizard for MemoryRelay MCP server.\n *\n * Detects installed MCP clients (Claude Desktop, Claude Code, OpenClaw),\n * validates the API key, and writes the correct configuration.\n *\n * Usage: npx memoryrelay-mcp setup\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as os from 'node:os';\nimport * as readline from 'node:readline';\n\n// ── Helpers ──────────────────────────────────────────────────────────\n\nfunction stderr(msg: string): void {\n process.stderr.write(msg + '\\n');\n}\n\nfunction createReadline(): readline.Interface {\n return readline.createInterface({\n input: process.stdin,\n output: process.stderr,\n });\n}\n\nfunction ask(rl: readline.Interface, question: string): Promise<string> {\n return new Promise((resolve) => {\n rl.question(question, (answer) => resolve(answer.trim()));\n });\n}\n\n// ── Client Detection ─────────────────────────────────────────────────\n\ninterface MCPClient {\n name: string;\n configPath: string;\n exists: boolean;\n}\n\nfunction getClaudeDesktopConfigPath(): string {\n const platform = os.platform();\n const home = os.homedir();\n\n if (platform === 'darwin') {\n return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');\n } else if (platform === 'win32') {\n return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');\n } else {\n return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');\n }\n}\n\nfunction getClaudeCodeConfigPath(): string {\n const home = os.homedir();\n return path.join(home, '.claude', 'settings.json');\n}\n\nfunction getOpenClawConfigPath(): string {\n const home = os.homedir();\n return path.join(home, '.openclaw', 'config.json');\n}\n\nfunction detectClients(): MCPClient[] {\n const clients: MCPClient[] = [\n {\n name: 'Claude Desktop',\n configPath: getClaudeDesktopConfigPath(),\n exists: false,\n },\n {\n name: 'Claude Code',\n configPath: getClaudeCodeConfigPath(),\n exists: false,\n },\n {\n name: 'OpenClaw',\n configPath: getOpenClawConfigPath(),\n exists: false,\n },\n ];\n\n for (const client of clients) {\n try {\n // Check if config file or its parent directory exists\n const dir = path.dirname(client.configPath);\n client.exists = fs.existsSync(dir);\n } catch {\n client.exists = false;\n }\n }\n\n return clients;\n}\n\n// ── API Key Validation ───────────────────────────────────────────────\n\nasync function validateApiKey(apiKey: string, apiUrl: string): Promise<{ valid: boolean; message: string }> {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 10000);\n\n const response = await fetch(`${apiUrl}/v1/health`, {\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'User-Agent': '@memoryrelay/mcp-server setup',\n },\n signal: controller.signal,\n });\n\n clearTimeout(timeout);\n\n if (response.ok) {\n return { valid: true, message: 'API key validated successfully' };\n } else if (response.status === 401 || response.status === 403) {\n return { valid: false, message: 'Invalid API key. Check your key at https://app.memoryrelay.ai/settings' };\n } else {\n return { valid: false, message: `Unexpected response: ${response.status} ${response.statusText}` };\n }\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n return { valid: false, message: 'Connection timed out. Check your network and API URL.' };\n }\n return { valid: false, message: `Connection failed: ${error instanceof Error ? error.message : 'Unknown error'}` };\n }\n}\n\n// ── Config Writing ───────────────────────────────────────────────────\n\ninterface MCPServerConfig {\n command: string;\n args: string[];\n env: Record<string, string>;\n}\n\nfunction buildServerConfig(apiKey: string, apiUrl: string, agentId?: string): MCPServerConfig {\n const env: Record<string, string> = {\n MEMORYRELAY_API_KEY: apiKey,\n };\n\n if (apiUrl !== 'https://api.memoryrelay.net') {\n env.MEMORYRELAY_API_URL = apiUrl;\n }\n\n if (agentId) {\n env.MEMORYRELAY_AGENT_ID = agentId;\n }\n\n return {\n command: 'npx',\n args: ['-y', '@memoryrelay/mcp-server'],\n env,\n };\n}\n\nfunction writeConfig(configPath: string, serverConfig: MCPServerConfig): { success: boolean; message: string } {\n try {\n // Read existing config or create new one\n let config: Record<string, unknown> = {};\n if (fs.existsSync(configPath)) {\n const raw = fs.readFileSync(configPath, 'utf-8');\n config = JSON.parse(raw);\n }\n\n // Ensure mcpServers section exists\n if (!config.mcpServers || typeof config.mcpServers !== 'object') {\n config.mcpServers = {};\n }\n\n // Add or update memoryrelay server\n (config.mcpServers as Record<string, unknown>).memoryrelay = serverConfig;\n\n // Ensure directory exists\n const dir = path.dirname(configPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Write config\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\\n', 'utf-8');\n\n return { success: true, message: `Configuration written to ${configPath}` };\n } catch (error) {\n return {\n success: false,\n message: `Failed to write config: ${error instanceof Error ? error.message : 'Unknown error'}`,\n };\n }\n}\n\n// ── Main Setup Flow ──────────────────────────────────────────────────\n\nexport async function runSetup(): Promise<void> {\n const rl = createReadline();\n\n stderr('');\n stderr(' MemoryRelay MCP Server Setup');\n stderr(' ============================');\n stderr('');\n\n // Step 1: Detect clients\n stderr(' Detecting MCP clients...');\n const clients = detectClients();\n const detectedClients = clients.filter(c => c.exists);\n\n if (detectedClients.length === 0) {\n stderr('');\n stderr(' No MCP clients detected. Supported clients:');\n stderr(' - Claude Desktop (https://claude.ai/download)');\n stderr(' - Claude Code (https://claude.ai/code)');\n stderr(' - OpenClaw');\n stderr('');\n stderr(' You can still configure manually. Continuing...');\n stderr('');\n } else {\n stderr('');\n for (const client of detectedClients) {\n stderr(` [+] ${client.name} found`);\n }\n stderr('');\n }\n\n // Step 2: Get API key\n const apiKey = await ask(rl, ' API Key (starts with mem_): ');\n\n if (!apiKey.startsWith('mem_')) {\n stderr('');\n stderr(' Error: API key must start with \"mem_\"');\n stderr(' Get your key at https://app.memoryrelay.ai/settings');\n rl.close();\n process.exit(1);\n }\n\n if (apiKey.length < 20) {\n stderr('');\n stderr(' Error: API key appears too short');\n rl.close();\n process.exit(1);\n }\n\n // Step 3: API URL (optional)\n const apiUrlInput = await ask(rl, ' API URL [https://api.memoryrelay.net]: ');\n const apiUrl = apiUrlInput || 'https://api.memoryrelay.net';\n\n // Step 4: Agent ID (optional)\n const agentId = await ask(rl, ' Agent ID (optional, auto-detected if blank): ');\n\n // Step 5: Validate API key\n stderr('');\n stderr(' Validating API key...');\n const validation = await validateApiKey(apiKey, apiUrl);\n\n if (!validation.valid) {\n stderr(` Error: ${validation.message}`);\n stderr('');\n const proceed = await ask(rl, ' Continue anyway? (y/N): ');\n if (proceed.toLowerCase() !== 'y') {\n rl.close();\n process.exit(1);\n }\n } else {\n stderr(` ${validation.message}`);\n }\n\n // Step 6: Choose clients to configure\n const serverConfig = buildServerConfig(apiKey, apiUrl, agentId || undefined);\n const configuredClients: string[] = [];\n\n stderr('');\n\n if (detectedClients.length > 0) {\n for (const client of detectedClients) {\n const answer = await ask(rl, ` Configure ${client.name}? (Y/n): `);\n if (answer.toLowerCase() !== 'n') {\n const result = writeConfig(client.configPath, serverConfig);\n if (result.success) {\n stderr(` ${result.message}`);\n configuredClients.push(client.name);\n } else {\n stderr(` ${result.message}`);\n }\n }\n }\n } else {\n // No detected clients — offer manual config options\n for (const client of clients) {\n const answer = await ask(rl, ` Configure ${client.name}? (y/N): `);\n if (answer.toLowerCase() === 'y') {\n const result = writeConfig(client.configPath, serverConfig);\n if (result.success) {\n stderr(` ${result.message}`);\n configuredClients.push(client.name);\n } else {\n stderr(` ${result.message}`);\n }\n }\n }\n }\n\n // Step 7: Summary\n stderr('');\n stderr(' Setup complete!');\n stderr(' ===============');\n stderr('');\n\n if (configuredClients.length > 0) {\n stderr(` Configured: ${configuredClients.join(', ')}`);\n stderr('');\n stderr(' Restart your MCP client to activate MemoryRelay.');\n } else {\n stderr(' No clients were configured. To configure manually, add');\n stderr(' the following to your MCP client config:');\n stderr('');\n stderr(JSON.stringify({ mcpServers: { memoryrelay: serverConfig } }, null, 2)\n .split('\\n')\n .map(line => ' ' + line)\n .join('\\n'));\n }\n\n stderr('');\n stderr(' Documentation: https://github.com/memoryrelay/mcp-server');\n stderr('');\n\n rl.close();\n}\n"],"mappings":"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","names":["platform"]}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Connection test command for MemoryRelay MCP server.
3
+ *
4
+ * Validates configuration and tests API connectivity.
5
+ *
6
+ * Usage: npx memoryrelay-mcp test
7
+ */
8
+ declare function runTest(): Promise<void>;
9
+
10
+ export { runTest };
@@ -0,0 +1,80 @@
1
+ import {
2
+ MemoryRelayClient,
3
+ getAgentId,
4
+ loadConfig
5
+ } from "../chunk-P6TZEH6O.js";
6
+
7
+ // src/cli/test.ts
8
+ function stderr(msg) {
9
+ process.stderr.write(msg + "\n");
10
+ }
11
+ async function runTest() {
12
+ stderr("");
13
+ stderr(" MemoryRelay Connection Test");
14
+ stderr(" ===========================");
15
+ stderr("");
16
+ stderr(" 1. Checking configuration...");
17
+ let config;
18
+ try {
19
+ config = loadConfig();
20
+ stderr(" API Key: " + config.apiKey.substring(0, 8) + "***");
21
+ stderr(" API URL: " + config.apiUrl);
22
+ } catch (error) {
23
+ stderr(` FAIL: ${error instanceof Error ? error.message : "Unknown error"}`);
24
+ stderr("");
25
+ stderr(' Run "npx memoryrelay-mcp setup" to configure.');
26
+ process.exit(1);
27
+ }
28
+ stderr("");
29
+ stderr(" 2. Resolving agent ID...");
30
+ const agentId = getAgentId(config);
31
+ stderr(` Agent ID: ${agentId}`);
32
+ if (config.agentId) {
33
+ stderr(" Source: MEMORYRELAY_AGENT_ID");
34
+ } else if (process.env.OPENCLAW_AGENT_NAME) {
35
+ stderr(" Source: OPENCLAW_AGENT_NAME");
36
+ } else {
37
+ stderr(" Source: auto-generated (user@hostname)");
38
+ }
39
+ stderr("");
40
+ stderr(" 3. Testing API connectivity...");
41
+ const client = new MemoryRelayClient({
42
+ apiKey: config.apiKey,
43
+ apiUrl: config.apiUrl,
44
+ agentId,
45
+ timeout: config.timeout
46
+ });
47
+ const health = await client.healthCheck();
48
+ if (health.status === "healthy") {
49
+ stderr(` ${health.message}`);
50
+ } else {
51
+ stderr(` FAIL: ${health.message}`);
52
+ process.exit(1);
53
+ }
54
+ stderr("");
55
+ stderr(" 4. Testing memory operations...");
56
+ try {
57
+ const memory = await client.storeMemory(
58
+ "__memoryrelay_connection_test__",
59
+ { _test: "true", _cleanup: "safe_to_delete" }
60
+ );
61
+ stderr(` Store: OK (id: ${memory.id})`);
62
+ const results = await client.searchMemories("connection test", 1, 0);
63
+ stderr(` Search: OK (${results.length} result${results.length !== 1 ? "s" : ""})`);
64
+ await client.deleteMemory(memory.id);
65
+ stderr(" Delete: OK (test memory cleaned up)");
66
+ } catch (error) {
67
+ stderr(` FAIL: ${error instanceof Error ? error.message : "Unknown error"}`);
68
+ stderr("");
69
+ stderr(" API connection works but memory operations failed.");
70
+ stderr(" Check your API key scopes (needs read + write + delete).");
71
+ process.exit(1);
72
+ }
73
+ stderr("");
74
+ stderr(" All tests passed! MemoryRelay is ready.");
75
+ stderr("");
76
+ }
77
+ export {
78
+ runTest
79
+ };
80
+ //# sourceMappingURL=test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cli/test.ts"],"sourcesContent":["/**\n * Connection test command for MemoryRelay MCP server.\n *\n * Validates configuration and tests API connectivity.\n *\n * Usage: npx memoryrelay-mcp test\n */\n\nimport { loadConfig, getAgentId } from '../config.js';\nimport { MemoryRelayClient } from '../client.js';\n\nfunction stderr(msg: string): void {\n process.stderr.write(msg + '\\n');\n}\n\nexport async function runTest(): Promise<void> {\n stderr('');\n stderr(' MemoryRelay Connection Test');\n stderr(' ===========================');\n stderr('');\n\n // Step 1: Check configuration\n stderr(' 1. Checking configuration...');\n let config;\n try {\n config = loadConfig();\n stderr(' API Key: ' + config.apiKey.substring(0, 8) + '***');\n stderr(' API URL: ' + config.apiUrl);\n } catch (error) {\n stderr(` FAIL: ${error instanceof Error ? error.message : 'Unknown error'}`);\n stderr('');\n stderr(' Run \"npx memoryrelay-mcp setup\" to configure.');\n process.exit(1);\n }\n\n // Step 2: Resolve agent ID\n stderr('');\n stderr(' 2. Resolving agent ID...');\n const agentId = getAgentId(config);\n stderr(` Agent ID: ${agentId}`);\n if (config.agentId) {\n stderr(' Source: MEMORYRELAY_AGENT_ID');\n } else if (process.env.OPENCLAW_AGENT_NAME) {\n stderr(' Source: OPENCLAW_AGENT_NAME');\n } else {\n stderr(' Source: auto-generated (user@hostname)');\n }\n\n // Step 3: Test API connectivity\n stderr('');\n stderr(' 3. Testing API connectivity...');\n const client = new MemoryRelayClient({\n apiKey: config.apiKey,\n apiUrl: config.apiUrl,\n agentId,\n timeout: config.timeout,\n });\n\n const health = await client.healthCheck();\n if (health.status === 'healthy') {\n stderr(` ${health.message}`);\n } else {\n stderr(` FAIL: ${health.message}`);\n process.exit(1);\n }\n\n // Step 4: Test memory operations\n stderr('');\n stderr(' 4. Testing memory operations...');\n try {\n // Store a test memory\n const memory = await client.storeMemory(\n '__memoryrelay_connection_test__',\n { _test: 'true', _cleanup: 'safe_to_delete' }\n );\n stderr(` Store: OK (id: ${memory.id})`);\n\n // Search for it\n const results = await client.searchMemories('connection test', 1, 0.0);\n stderr(` Search: OK (${results.length} result${results.length !== 1 ? 's' : ''})`);\n\n // Clean up\n await client.deleteMemory(memory.id);\n stderr(' Delete: OK (test memory cleaned up)');\n } catch (error) {\n stderr(` FAIL: ${error instanceof Error ? error.message : 'Unknown error'}`);\n stderr('');\n stderr(' API connection works but memory operations failed.');\n stderr(' Check your API key scopes (needs read + write + delete).');\n process.exit(1);\n }\n\n // Summary\n stderr('');\n stderr(' All tests passed! MemoryRelay is ready.');\n stderr('');\n}\n"],"mappings":";;;;;;;AAWA,SAAS,OAAO,KAAmB;AACjC,UAAQ,OAAO,MAAM,MAAM,IAAI;AACjC;AAEA,eAAsB,UAAyB;AAC7C,SAAO,EAAE;AACT,SAAO,+BAA+B;AACtC,SAAO,+BAA+B;AACtC,SAAO,EAAE;AAGT,SAAO,gCAAgC;AACvC,MAAI;AACJ,MAAI;AACF,aAAS,WAAW;AACpB,WAAO,mBAAmB,OAAO,OAAO,UAAU,GAAG,CAAC,IAAI,KAAK;AAC/D,WAAO,mBAAmB,OAAO,MAAM;AAAA,EACzC,SAAS,OAAO;AACd,WAAO,cAAc,iBAAiB,QAAQ,MAAM,UAAU,eAAe,EAAE;AAC/E,WAAO,EAAE;AACT,WAAO,iDAAiD;AACxD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,SAAO,EAAE;AACT,SAAO,4BAA4B;AACnC,QAAM,UAAU,WAAW,MAAM;AACjC,SAAO,kBAAkB,OAAO,EAAE;AAClC,MAAI,OAAO,SAAS;AAClB,WAAO,mCAAmC;AAAA,EAC5C,WAAW,QAAQ,IAAI,qBAAqB;AAC1C,WAAO,kCAAkC;AAAA,EAC3C,OAAO;AACL,WAAO,6CAA6C;AAAA,EACtD;AAGA,SAAO,EAAE;AACT,SAAO,kCAAkC;AACzC,QAAM,SAAS,IAAI,kBAAkB;AAAA,IACnC,QAAQ,OAAO;AAAA,IACf,QAAQ,OAAO;AAAA,IACf;AAAA,IACA,SAAS,OAAO;AAAA,EAClB,CAAC;AAED,QAAM,SAAS,MAAM,OAAO,YAAY;AACxC,MAAI,OAAO,WAAW,WAAW;AAC/B,WAAO,QAAQ,OAAO,OAAO,EAAE;AAAA,EACjC,OAAO;AACL,WAAO,cAAc,OAAO,OAAO,EAAE;AACrC,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,SAAO,EAAE;AACT,SAAO,mCAAmC;AAC1C,MAAI;AAEF,UAAM,SAAS,MAAM,OAAO;AAAA,MAC1B;AAAA,MACA,EAAE,OAAO,QAAQ,UAAU,iBAAiB;AAAA,IAC9C;AACA,WAAO,uBAAuB,OAAO,EAAE,GAAG;AAG1C,UAAM,UAAU,MAAM,OAAO,eAAe,mBAAmB,GAAG,CAAG;AACrE,WAAO,oBAAoB,QAAQ,MAAM,UAAU,QAAQ,WAAW,IAAI,MAAM,EAAE,GAAG;AAGrF,UAAM,OAAO,aAAa,OAAO,EAAE;AACnC,WAAO,0CAA0C;AAAA,EACnD,SAAS,OAAO;AACd,WAAO,cAAc,iBAAiB,QAAQ,MAAM,UAAU,eAAe,EAAE;AAC/E,WAAO,EAAE;AACT,WAAO,sDAAsD;AAC7D,WAAO,4DAA4D;AACnE,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,SAAO,EAAE;AACT,SAAO,2CAA2C;AAClD,SAAO,EAAE;AACX;","names":[]}