life-todo

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

claude.js (2755B)


      1 const Anthropic = require('@anthropic-ai/sdk');
      2 const fs = require('fs');
      3 const path = require('path');
      4 
      5 const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
      6 
      7 const CLAUDE_MD_PATH = path.join(__dirname, '..', 'CLAUDE.md');
      8 
      9 const RESPONSE_FORMAT_INSTRUCTIONS = `
     10 ## Response Format
     11 
     12 You MUST respond with a raw JSON object — no markdown fences, no extra text before or after.
     13 The object must have exactly these four fields:
     14 
     15 {
     16   "reply": "<your conversational reply to send to the user via Telegram>",
     17   "updatedToday": "<the full updated today.md content as a plain string>",
     18   "updatedBacklog": "<the full updated backlog.md content as a plain string>",
     19   "commitMessage": "<a short git commit message, e.g. 'add: book dentist (backlog)'>"
     20 }
     21 
     22 Rules:
     23 - If a file did not change, return its original content unchanged.
     24 - If you're asking a clarifying question, return both files unchanged.
     25 - Always return valid JSON. Never wrap it in a code block.
     26 `;
     27 
     28 function getClaudeMd() {
     29   if (!fs.existsSync(CLAUDE_MD_PATH)) return '';
     30   return fs.readFileSync(CLAUDE_MD_PATH, 'utf8');
     31 }
     32 
     33 function buildSystemBlocks(todayContent, backlogContent, rotated) {
     34   const parts = [
     35     `## today.mdnn```markdownn${todayContent || '(empty)'}n````,
     36     `## backlog.mdnn```markdownn${backlogContent || '(empty)'}n````,
     37   ];
     38   if (rotated) {
     39     parts.unshift('> **New day.** today.md has been reset. Briefly suggest pulling any urgent backlog items into today — but don't do it automatically.');
     40   }
     41 
     42   return [
     43     {
     44       type: 'text',
     45       text: getClaudeMd() + 'nn' + RESPONSE_FORMAT_INSTRUCTIONS,
     46       cache_control: { type: 'ephemeral' },
     47     },
     48     {
     49       type: 'text',
     50       text: parts.join('nn'),
     51     },
     52   ];
     53 }
     54 
     55 async function chat(history, userMessage, todayContent, backlogContent, rotated = false) {
     56   const messages = [
     57     ...history,
     58     { role: 'user', content: userMessage },
     59   ];
     60 
     61   const response = await client.messages.create({
     62     model: 'claude-sonnet-4-6',
     63     max_tokens: 2048,
     64     system: buildSystemBlocks(todayContent, backlogContent, rotated),
     65     messages,
     66   });
     67 
     68   const raw = response.content[0].text.trim();
     69 
     70   try {
     71     const parsed = JSON.parse(raw);
     72     return {
     73       reply:           parsed.reply          ?? '(no reply)',
     74       updatedToday:    parsed.updatedToday   ?? todayContent,
     75       updatedBacklog:  parsed.updatedBacklog ?? backlogContent,
     76       commitMessage:   parsed.commitMessage  ?? 'update: todo',
     77     };
     78   } catch {
     79     console.error('Failed to parse Claude response as JSON:', raw);
     80     return {
     81       reply:          raw,
     82       updatedToday:   todayContent,
     83       updatedBacklog: backlogContent,
     84       commitMessage:  null,
     85     };
     86   }
     87 }
     88 
     89 module.exports = { chat };