import fs from 'node:fs'; import path from 'node:path'; import { getErrorMessage } from './error.js'; type LogInfoFn = (message: string, details?: unknown) => void; export function ensureMemoryFile(memoryFilePath: string, logInfo: LogInfoFn) { fs.mkdirSync(path.dirname(memoryFilePath), { recursive: true }); if (!fs.existsSync(memoryFilePath)) { const template = [ '# Clawless Memory', '', 'This file stores durable memory notes for Clawless.', '', '## Notes', '', ].join('\n'); fs.writeFileSync(memoryFilePath, `${template}\n`, 'utf8'); logInfo('Created memory file', { memoryFilePath }); } } export function readMemoryContext(memoryFilePath: string, memoryMaxChars: number, logInfo: LogInfoFn) { try { const content = fs.readFileSync(memoryFilePath, 'utf8'); const trimmed = content.trim(); if (!trimmed) { return ''; } if (trimmed.length <= memoryMaxChars) { return trimmed; } return trimmed.slice(-memoryMaxChars); } catch (error: any) { logInfo('Unable to read memory file; continuing without memory context', { memoryFilePath, error: getErrorMessage(error), }); return ''; } } export function buildPromptWithMemory(params: { userPrompt: string; memoryFilePath: string; callbackHost: string; callbackPort: number; callbackChatStateFilePath: string; callbackAuthToken: string; memoryContext: string; conversationContext?: string; contextQueueContent?: string; messagingPlatform: string; includeSchedulerApi?: boolean; }) { const { userPrompt, memoryFilePath, callbackHost, callbackPort, callbackChatStateFilePath, callbackAuthToken, memoryContext, conversationContext = '', contextQueueContent = '', messagingPlatform, includeSchedulerApi = true, } = params; const callbackEndpoint = `http://${callbackHost}:${callbackPort}/callback/${messagingPlatform}`; const scheduleEndpoint = `http://${callbackHost}:${callbackPort}/api/schedule`; const semanticRecallEndpoint = `http://${callbackHost}:${callbackPort}/api/memory/semantic-recall`; const parts = [ ...(includeSchedulerApi ? [ 'System instruction:', `- Persistent memory file path: ${memoryFilePath}`, '- If user asks to remember/memorize/save for later, append a concise bullet under "## Notes" in that file.', '- Do not overwrite existing memory entries; append only.', `- Callback endpoint for proactive notifications (cron/jobs): POST ${callbackEndpoint}`, '- Callback payload should include a JSON `text` field; `chatId` is optional.', `- Persisted callback chat binding file: ${callbackChatStateFilePath}`, '- If no `chatId` is provided, the bridge sends to the persisted bound chat.', `- For scheduled jobs, include callback delivery steps so results are pushed to ${messagingPlatform} when jobs complete.`, '', 'Scheduler API:', `- POST ${scheduleEndpoint} (create: {"message": "prompt", "cronExpression": "* * * * *", "oneTime": true})`, `- PATCH ${scheduleEndpoint}/:id (update), DELETE ${scheduleEndpoint}/:id (delete), GET ${scheduleEndpoint} (list)`, '- Cron: "minute hour day month weekday" (e.g., "0 9 * * *" = daily 9am)', `- Results pushed to ${messagingPlatform}. Use for scheduling tasks, reminders, recurring jobs.`, '', ] : []), '**Semantic recall API (on-demand):**', `- Endpoint: POST ${semanticRecallEndpoint}`, '- Request body: {"input": ["keyword1", "keyword2"], "chatId": "optional", "topK": 3}', '- Use individual words in `input` array (e.g., ["project", "deadline"]), not phrases.', '- Use when additional historical context is needed.', callbackAuthToken ? '- API auth is enabled (scheduler + semantic recall): include `x-callback-token` (or bearer token) header.' : '- API auth is disabled unless CALLBACK_AUTH_TOKEN is configured.', ]; if (memoryContext && memoryContext.trim().length > 0) { parts.push('', 'Current memory context:', memoryContext); } if (conversationContext && conversationContext.trim().length > 0) { parts.push('', 'Recent conversation history:', conversationContext); } if (contextQueueContent && contextQueueContent.trim().length > 0) { parts.push('', contextQueueContent); } parts.push('', 'User message:', userPrompt); return parts.join('\n'); }