diff --git a/src/index.ts b/src/index.ts index ee63844..08679e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,40 @@ import type { MemoryType } from "./types/index.js"; import { getLanguageName } from "./services/language-detector.js"; import type { MemoryScope } from "./services/client.js"; +const helpResponseCache = new Map(); + +function getHelpResponse(langName: string): string { + let cached = helpResponseCache.get(langName); + if (cached) return cached; + + cached = JSON.stringify({ + success: true, + message: "Memory System Usage Guide", + commands: [ + { + command: "add", + description: `Store new memory (MATCH USER LANGUAGE: ${langName})`, + args: ["content", "type?", "tags?"], + }, + { + command: "search", + description: `Search memories via keywords (MATCH USER LANGUAGE: ${langName})`, + args: ["query"], + }, + { + command: "profile", + description: "View user profile or save an explicit preference (provide content to write)", + args: ["content?"], + }, + { command: "list", description: "List recent memories", args: ["limit?"] }, + { command: "forget", description: "Remove memory", args: ["memoryId"] }, + ], + tagGuidance: "Use technical keywords for search. Tags rank highest.", + }); + helpResponseCache.set(langName, cached); + return cached; +} + export const OpenCodeMemPlugin: Plugin = async (ctx: PluginInput) => { const { directory } = ctx; initConfig(directory); @@ -327,31 +361,7 @@ export const OpenCodeMemPlugin: Plugin = async (ctx: PluginInput) => { try { switch (mode) { case "help": - return JSON.stringify({ - success: true, - message: "Memory System Usage Guide", - commands: [ - { - command: "add", - description: `Store new memory (MATCH USER LANGUAGE: ${langName})`, - args: ["content", "type?", "tags?"], - }, - { - command: "search", - description: `Search memories via keywords (MATCH USER LANGUAGE: ${langName})`, - args: ["query"], - }, - { - command: "profile", - description: - "View user profile or save an explicit preference (provide content to write)", - args: ["content?"], - }, - { command: "list", description: "List recent memories", args: ["limit?"] }, - { command: "forget", description: "Remove memory", args: ["memoryId"] }, - ], - tagGuidance: "Use technical keywords for search. Tags rank highest.", - }); + return getHelpResponse(langName); case "add": if (!args.content) @@ -622,15 +632,17 @@ function formatSearchResults(query: string, results: any, limit?: number): strin } function formatMemoriesForCompaction(memories: any[]): string { - let output = `## Restored Session Memory\n\n`; + const sections: string[] = ["## Restored Session Memory\n"]; - memories.forEach((m, i) => { - output += `### Memory ${i + 1}\n`; - output += `${m.memory}\n\n`; + for (let i = 0; i < memories.length; i++) { + const m = memories[i]; + sections.push(`### Memory ${i + 1}`); + sections.push(m.memory); if (m.tags && m.tags.length > 0) { - output += `Tags: ${m.tags.join(", ")}\n\n`; + sections.push(`Tags: ${m.tags.join(", ")}`); } - }); + sections.push(""); + } - return output; + return sections.join("\n"); } diff --git a/src/services/auto-capture.ts b/src/services/auto-capture.ts index 63db9f2..7aa0478 100644 --- a/src/services/auto-capture.ts +++ b/src/services/auto-capture.ts @@ -12,6 +12,32 @@ interface ToolCallInfo { const MAX_TOOL_INPUT_LENGTH = 100; +const AUTO_CAPTURE_SYSTEM_PROMPT_TEMPLATE = ( + langName: string +) => `You are a technical memory recorder for a software development project. + +RULES: +1. ONLY capture technical work (code, bugs, features, architecture, config) +2. SKIP non-technical by returning type="skip" +3. NO meta-commentary or behavior analysis +4. Include specific file names, functions, technical details +5. Generate 2-4 technical tags (e.g., "react", "auth", "bug-fix") +6. You MUST write the summary in ${langName}. + +FORMAT: +## Request +[1-2 sentences: what was requested, in ${langName}] + +## Outcome +[1-2 sentences: what was done, include files/functions, in ${langName}] + +SKIP if: greetings, casual chat, no code/decisions made +CAPTURE if: code changed, bug fixed, feature added, decision made`; + +const AUTO_CAPTURE_ANALYSIS_PROMPT = (context: string) => `${context} + +Analyze this conversation. If it contains technical work (code, bugs, features, decisions), create a concise summary and relevant tags. If it's non-technical (greetings, casual chat, incomplete requests), return type="skip" with empty summary.`; + let isCaptureRunning = false; export async function performAutoCapture( @@ -260,30 +286,6 @@ async function generateSummary( : CONFIG.autoCaptureLanguage; const langName = getLanguageName(targetLang); - const systemPrompt = `You are a technical memory recorder for a software development project. - -RULES: -1. ONLY capture technical work (code, bugs, features, architecture, config) -2. SKIP non-technical by returning type="skip" -3. NO meta-commentary or behavior analysis -4. Include specific file names, functions, technical details -5. Generate 2-4 technical tags (e.g., "react", "auth", "bug-fix") -6. You MUST write the summary in ${langName}. - -FORMAT: -## Request -[1-2 sentences: what was requested, in ${langName}] - -## Outcome -[1-2 sentences: what was done, include files/functions, in ${langName}] - -SKIP if: greetings, casual chat, no code/decisions made -CAPTURE if: code changed, bug fixed, feature added, decision made`; - - const aiPrompt = `${context} - -Analyze this conversation. If it contains technical work (code, bugs, features, decisions), create a concise summary and relevant tags. If it's non-technical (greetings, casual chat, incomplete requests), return type="skip" with empty summary.`; - const { z } = await import("zod"); const schema = z.object({ summary: z.string(), @@ -295,8 +297,8 @@ Analyze this conversation. If it contains technical work (code, bugs, features, providerName: CONFIG.opencodeProvider, modelId: CONFIG.opencodeModel, statePath: getStatePath(), - systemPrompt, - userPrompt: aiPrompt, + systemPrompt: AUTO_CAPTURE_SYSTEM_PROMPT_TEMPLATE(langName), + userPrompt: AUTO_CAPTURE_ANALYSIS_PROMPT(context), schema, temperature: CONFIG.memoryTemperature === false ? undefined : (CONFIG.memoryTemperature ?? 0.3), @@ -329,30 +331,6 @@ Analyze this conversation. If it contains technical work (code, bugs, features, const langName = getLanguageName(targetLang); - const systemPrompt = `You are a technical memory recorder for a software development project. - -RULES: -1. ONLY capture technical work (code, bugs, features, architecture, config) -2. SKIP non-technical by returning type="skip" -3. NO meta-commentary or behavior analysis -4. Include specific file names, functions, technical details -5. Generate 2-4 technical tags (e.g., "react", "auth", "bug-fix") -6. You MUST write the summary in ${langName}. - -FORMAT: -## Request -[1-2 sentences: what was requested, in ${langName}] - -## Outcome -[1-2 sentences: what was done, include files/functions, in ${langName}] - -SKIP if: greetings, casual chat, no code/decisions made -CAPTURE if: code changed, bug fixed, feature added, decision made`; - - const aiPrompt = `${context} - -Analyze this conversation. If it contains technical work (code, bugs, features, decisions), create a concise summary and relevant tags. If it's non-technical (greetings, casual chat, incomplete requests), return type="skip" with empty summary.`; - const toolSchema = { type: "function" as const, function: { @@ -381,7 +359,12 @@ Analyze this conversation. If it contains technical work (code, bugs, features, }, }; - const result = await provider.executeToolCall(systemPrompt, aiPrompt, toolSchema, sessionID); + const result = await provider.executeToolCall( + AUTO_CAPTURE_SYSTEM_PROMPT_TEMPLATE(langName), + AUTO_CAPTURE_ANALYSIS_PROMPT(context), + toolSchema, + sessionID + ); if (!result.success || !result.data) { throw new Error(result.error || "Failed to generate summary"); diff --git a/src/services/user-memory-learning.ts b/src/services/user-memory-learning.ts index 4f41a4a..404ad7c 100644 --- a/src/services/user-memory-learning.ts +++ b/src/services/user-memory-learning.ts @@ -8,6 +8,16 @@ import type { UserPrompt } from "./user-prompt/user-prompt-manager.js"; import { userProfileManager } from "./user-profile/user-profile-manager.js"; import type { UserProfile, UserProfileData } from "./user-profile/types.js"; +const USER_PROFILE_SYSTEM_PROMPT = ( + existingProfile: boolean +) => `You are a user behavior analyst for a coding assistant. + +Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile. + +CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts. + +Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`; + let isLearningRunning = false; export async function performUserProfileLearning( @@ -159,14 +169,6 @@ async function analyzeUserProfile( ); } - const systemPrompt = `You are a user behavior analyst for a coding assistant. - -Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile. - -CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts. - -Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`; - const { z } = await import("zod"); const schema = z.object({ preferences: z.array( @@ -195,7 +197,7 @@ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "ne providerName: CONFIG.opencodeProvider, modelId: CONFIG.opencodeModel, statePath: getStatePath(), - systemPrompt, + systemPrompt: USER_PROFILE_SYSTEM_PROMPT(!!existingProfile), userPrompt: context, schema, temperature: @@ -228,14 +230,6 @@ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "ne const provider = AIProviderFactory.createProvider(CONFIG.memoryProvider, providerConfig); - const systemPrompt = `You are a user behavior analyst for a coding assistant. - -Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile. - -CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts. - -Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`; - const toolSchema = { type: "function" as const, function: { @@ -288,7 +282,7 @@ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "ne }; const result = await provider.executeToolCall( - systemPrompt, + USER_PROFILE_SYSTEM_PROMPT(!!existingProfile), context, toolSchema, `user-profile-${Date.now()}` diff --git a/src/services/user-prompt/user-prompt-manager.ts b/src/services/user-prompt/user-prompt-manager.ts index 038d973..160dcfc 100644 --- a/src/services/user-prompt/user-prompt-manager.ts +++ b/src/services/user-prompt/user-prompt-manager.ts @@ -8,6 +8,10 @@ type DatabaseType = Database; const USER_PROMPTS_DB_NAME = "user-prompts.db"; +function escapeLikePattern(value: string): string { + return value.replace(/[\\%_]/g, (char) => `\\${char}`); +} + export interface UserPrompt { id: string; sessionId: string; @@ -23,11 +27,100 @@ export interface UserPrompt { export class UserPromptManager { private db: DatabaseType; private readonly dbPath: string; + private readonly stmts: { + savePrompt: ReturnType; + getLastUncaptured: ReturnType; + deletePrompt: ReturnType; + markCaptured: ReturnType; + claimPrompt: ReturnType; + resetClaim: ReturnType; + countUncaptured: ReturnType; + getUncaptured: ReturnType; + markMultipleCaptured: ReturnType; + countUnanalyzed: ReturnType; + getForUserLearning: ReturnType; + markUserLearningCaptured: ReturnType; + markMultipleUserLearning: ReturnType; + getLinkedMemoryIds: ReturnType; + deleteOldPrompts: ReturnType; + linkMemory: ReturnType; + getById: ReturnType; + getCaptured: ReturnType; + getCapturedByProject: ReturnType; + searchPrompts: ReturnType; + searchPromptsByProject: ReturnType; + getByIds: ReturnType; + }; constructor() { this.dbPath = join(CONFIG.storagePath, USER_PROMPTS_DB_NAME); this.db = connectionManager.getConnection(this.dbPath); this.initDatabase(); + this.stmts = { + savePrompt: this.db.prepare(` + INSERT INTO user_prompts (id, session_id, message_id, project_path, content, created_at, captured) + VALUES (?, ?, ?, ?, ?, ?, 0) + `), + getLastUncaptured: this.db.prepare(` + SELECT * FROM user_prompts + WHERE session_id = ? AND captured = 0 + ORDER BY created_at DESC + LIMIT 1 + `), + deletePrompt: this.db.prepare(`DELETE FROM user_prompts WHERE id = ?`), + markCaptured: this.db.prepare(`UPDATE user_prompts SET captured = 1 WHERE id = ?`), + claimPrompt: this.db.prepare( + `UPDATE user_prompts SET captured = 2 WHERE id = ? AND captured = 0` + ), + resetClaim: this.db.prepare( + `UPDATE user_prompts SET captured = 0 WHERE id = ? AND captured = 2` + ), + countUncaptured: this.db.prepare( + `SELECT COUNT(*) as count FROM user_prompts WHERE captured = 0` + ), + getUncaptured: this.db.prepare(` + SELECT * FROM user_prompts + WHERE captured = 0 + ORDER BY created_at ASC + LIMIT ? + `), + markMultipleCaptured: this.db.prepare(`UPDATE user_prompts SET captured = 1 WHERE id = ?`), + countUnanalyzed: this.db.prepare( + `SELECT COUNT(*) as count FROM user_prompts WHERE user_learning_captured = 0` + ), + getForUserLearning: this.db.prepare(` + SELECT * FROM user_prompts + WHERE user_learning_captured = 0 + ORDER BY created_at ASC + LIMIT ? + `), + markUserLearningCaptured: this.db.prepare( + `UPDATE user_prompts SET user_learning_captured = 1 WHERE id = ?` + ), + markMultipleUserLearning: this.db.prepare( + `UPDATE user_prompts SET user_learning_captured = 1 WHERE id = ?` + ), + getLinkedMemoryIds: this.db.prepare(` + SELECT linked_memory_id FROM user_prompts + WHERE created_at < ? AND linked_memory_id IS NOT NULL + `), + deleteOldPrompts: this.db.prepare(`DELETE FROM user_prompts WHERE created_at < ?`), + linkMemory: this.db.prepare(`UPDATE user_prompts SET linked_memory_id = ? WHERE id = ?`), + getById: this.db.prepare(`SELECT * FROM user_prompts WHERE id = ?`), + getCaptured: this.db.prepare( + `SELECT * FROM user_prompts WHERE captured = 1 ORDER BY created_at DESC` + ), + getCapturedByProject: this.db.prepare( + `SELECT * FROM user_prompts WHERE captured = 1 AND project_path = ? ORDER BY created_at DESC` + ), + searchPrompts: this.db.prepare( + `SELECT * FROM user_prompts WHERE content LIKE ? ESCAPE '\\' AND captured = 1 ORDER BY created_at DESC LIMIT ?` + ), + searchPromptsByProject: this.db.prepare( + `SELECT * FROM user_prompts WHERE content LIKE ? ESCAPE '\\' AND captured = 1 AND project_path = ? ORDER BY created_at DESC LIMIT ?` + ), + getByIds: this.db.prepare(`SELECT * FROM user_prompts WHERE id = ?`), + }; } private initDatabase(): void { @@ -66,76 +159,49 @@ export class UserPromptManager { savePrompt(sessionId: string, messageId: string, projectPath: string, content: string): string { const id = `prompt_${Date.now()}_${randomBytes(4).toString("hex")}`; const now = Date.now(); - - const stmt = this.db.prepare(` - INSERT INTO user_prompts (id, session_id, message_id, project_path, content, created_at, captured) - VALUES (?, ?, ?, ?, ?, ?, 0) - `); - - stmt.run(id, sessionId, messageId, projectPath, content, now); + this.stmts.savePrompt.run(id, sessionId, messageId, projectPath, content, now); return id; } getLastUncapturedPrompt(sessionId: string): UserPrompt | null { - const stmt = this.db.prepare(` - SELECT * FROM user_prompts - WHERE session_id = ? AND captured = 0 - ORDER BY created_at DESC - LIMIT 1 - `); - - const row = stmt.get(sessionId) as any; + const row = this.stmts.getLastUncaptured.get(sessionId) as any; if (!row) return null; - return this.rowToPrompt(row); } deletePrompt(promptId: string): void { - const stmt = this.db.prepare(`DELETE FROM user_prompts WHERE id = ?`); - stmt.run(promptId); + this.stmts.deletePrompt.run(promptId); } markAsCaptured(promptId: string): void { - const stmt = this.db.prepare(`UPDATE user_prompts SET captured = 1 WHERE id = ?`); - stmt.run(promptId); + this.stmts.markCaptured.run(promptId); } claimPrompt(promptId: string): boolean { - const stmt = this.db.prepare( - `UPDATE user_prompts SET captured = 2 WHERE id = ? AND captured = 0` - ); - const result = stmt.run(promptId); + const result = this.stmts.claimPrompt.run(promptId); return result.changes > 0; } resetPromptClaim(promptId: string): void { - const stmt = this.db.prepare( - `UPDATE user_prompts SET captured = 0 WHERE id = ? AND captured = 2` - ); - stmt.run(promptId); + this.stmts.resetClaim.run(promptId); } countUncapturedPrompts(): number { - const stmt = this.db.prepare(`SELECT COUNT(*) as count FROM user_prompts WHERE captured = 0`); - const row = stmt.get() as any; + const row = this.stmts.countUncaptured.get() as any; return row?.count || 0; } getUncapturedPrompts(limit: number): UserPrompt[] { - const stmt = this.db.prepare(` - SELECT * FROM user_prompts - WHERE captured = 0 - ORDER BY created_at ASC - LIMIT ? - `); - - const rows = stmt.all(limit) as any[]; + const rows = this.stmts.getUncaptured.all(limit) as any[]; return rows.map((row) => this.rowToPrompt(row)); } markMultipleAsCaptured(promptIds: string[]): void { if (promptIds.length === 0) return; - + if (promptIds.length === 1) { + this.stmts.markMultipleCaptured.run(promptIds[0]); + return; + } const placeholders = promptIds.map(() => "?").join(","); const stmt = this.db.prepare( `UPDATE user_prompts SET captured = 1 WHERE id IN (${placeholders})` @@ -144,33 +210,25 @@ export class UserPromptManager { } countUnanalyzedForUserLearning(): number { - const stmt = this.db.prepare( - `SELECT COUNT(*) as count FROM user_prompts WHERE user_learning_captured = 0` - ); - const row = stmt.get() as any; + const row = this.stmts.countUnanalyzed.get() as any; return row?.count || 0; } getPromptsForUserLearning(limit: number): UserPrompt[] { - const stmt = this.db.prepare(` - SELECT * FROM user_prompts - WHERE user_learning_captured = 0 - ORDER BY created_at ASC - LIMIT ? - `); - - const rows = stmt.all(limit) as any[]; + const rows = this.stmts.getForUserLearning.all(limit) as any[]; return rows.map((row) => this.rowToPrompt(row)); } markAsUserLearningCaptured(promptId: string): void { - const stmt = this.db.prepare(`UPDATE user_prompts SET user_learning_captured = 1 WHERE id = ?`); - stmt.run(promptId); + this.stmts.markUserLearningCaptured.run(promptId); } markMultipleAsUserLearningCaptured(promptIds: string[]): void { if (promptIds.length === 0) return; - + if (promptIds.length === 1) { + this.stmts.markMultipleUserLearning.run(promptIds[0]); + return; + } const placeholders = promptIds.map(() => "?").join(","); const stmt = this.db.prepare( `UPDATE user_prompts SET user_learning_captured = 1 WHERE id IN (${placeholders})` @@ -179,16 +237,9 @@ export class UserPromptManager { } deleteOldPrompts(cutoffTime: number): { deleted: number; linkedMemoryIds: string[] } { - const getLinkedStmt = this.db.prepare(` - SELECT linked_memory_id FROM user_prompts - WHERE created_at < ? AND linked_memory_id IS NOT NULL - `); - const linkedRows = getLinkedStmt.all(cutoffTime) as any[]; + const linkedRows = this.stmts.getLinkedMemoryIds.all(cutoffTime) as any[]; const linkedMemoryIds = linkedRows.map((row) => row.linked_memory_id).filter((id) => id); - - const deleteStmt = this.db.prepare(`DELETE FROM user_prompts WHERE created_at < ?`); - const result = deleteStmt.run(cutoffTime); - + const result = this.stmts.deleteOldPrompts.run(cutoffTime); return { deleted: result.changes, linkedMemoryIds, @@ -196,52 +247,40 @@ export class UserPromptManager { } linkMemoryToPrompt(promptId: string, memoryId: string): void { - const stmt = this.db.prepare(`UPDATE user_prompts SET linked_memory_id = ? WHERE id = ?`); - stmt.run(memoryId, promptId); + this.stmts.linkMemory.run(memoryId, promptId); } getPromptById(promptId: string): UserPrompt | null { - const stmt = this.db.prepare(`SELECT * FROM user_prompts WHERE id = ?`); - const row = stmt.get(promptId) as any; + const row = this.stmts.getById.get(promptId) as any; if (!row) return null; return this.rowToPrompt(row); } getCapturedPrompts(projectPath?: string): UserPrompt[] { - let query = `SELECT * FROM user_prompts WHERE captured = 1`; - const params: any[] = []; - if (projectPath) { - query += ` AND project_path = ?`; - params.push(projectPath); + const rows = this.stmts.getCapturedByProject.all(projectPath) as any[]; + return rows.map((row) => this.rowToPrompt(row)); } - - query += ` ORDER BY created_at DESC`; - - const stmt = this.db.prepare(query); - const rows = stmt.all(...params) as any[]; + const rows = this.stmts.getCaptured.all() as any[]; return rows.map((row) => this.rowToPrompt(row)); } searchPrompts(query: string, projectPath?: string, limit: number = 20): UserPrompt[] { - let sql = `SELECT * FROM user_prompts WHERE content LIKE ? AND captured = 1`; - const params: any[] = [`%${query}%`]; - + const likePattern = `%${escapeLikePattern(query)}%`; if (projectPath) { - sql += ` AND project_path = ?`; - params.push(projectPath); + const rows = this.stmts.searchPromptsByProject.all(likePattern, projectPath, limit) as any[]; + return rows.map((row) => this.rowToPrompt(row)); } - - sql += ` ORDER BY created_at DESC LIMIT ?`; - params.push(limit); - - const stmt = this.db.prepare(sql); - const rows = stmt.all(...params) as any[]; + const rows = this.stmts.searchPrompts.all(likePattern, limit) as any[]; return rows.map((row) => this.rowToPrompt(row)); } getPromptsByIds(ids: string[]): UserPrompt[] { if (ids.length === 0) return []; + if (ids.length === 1) { + const row = this.stmts.getByIds.get(ids[0]) as any; + return row ? [this.rowToPrompt(row)] : []; + } const placeholders = ids.map(() => "?").join(","); const stmt = this.db.prepare(`SELECT * FROM user_prompts WHERE id IN (${placeholders})`); const rows = stmt.all(...ids) as any[]; diff --git a/src/services/web-server.ts b/src/services/web-server.ts index 34e0478..d395f5c 100644 --- a/src/services/web-server.ts +++ b/src/services/web-server.ts @@ -35,6 +35,17 @@ import { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +const LOCAL_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]); +const SENSITIVE_KEYS = new Set([ + "userEmail", + "displayName", + "userName", + "projectPath", + "projectName", + "gitRepoUrl", + "userId", +]); + interface WebServerConfig { port: number; host: string; @@ -197,22 +208,8 @@ export class WebServer { const newObj: any = {}; for (const [key, value] of Object.entries(obj)) { - if ( - [ - "userEmail", - "displayName", - "userName", - "projectPath", - "projectName", - "gitRepoUrl", - "userId", - ].includes(key) - ) { - if (value !== undefined && value !== null && value !== "") { - newObj[key] = "[REDACTED]"; - } else { - newObj[key] = value; - } + if (SENSITIVE_KEYS.has(key)) { + newObj[key] = value !== undefined && value !== null && value !== "" ? "[REDACTED]" : value; } else if (typeof value === "object") { newObj[key] = this.redactPII(value); } else { @@ -229,12 +226,11 @@ export class WebServer { try { // Optional API key auth: enforce only when binding to non-loopback - const localHosts = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]); - const requiresAuth = !localHosts.has(this.config.host) && !!this.config.enabled; + const requiresAuth = !LOCAL_HOSTS.has(this.config.host) && !!this.config.enabled; const remoteIp = this.server?.requestIP(req)?.address; const isLocal = - localHosts.has(this.config.host) || (remoteIp ? localHosts.has(remoteIp) : false); + LOCAL_HOSTS.has(this.config.host) || (remoteIp ? LOCAL_HOSTS.has(remoteIp) : false); if (this.config.apiKey && requiresAuth) { const apiKey = req.headers.get("x-opencode-mem-key");