From 17e7b469f4b80f640f0f32e1b72d864f182c0f6f Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sun, 1 Feb 2026 17:35:03 +0200 Subject: [PATCH] Refactor: delete dead code, extract utils, rename files for clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete scheduler-mcp.ts (285 lines of dead code, unused) - Extract loadJson/saveJson to utils.ts (generic utilities) - Rename auth.ts → whatsapp-auth.ts (more specific) - Rename scheduler.ts → task-scheduler.ts (more specific) - Update all references in docs and imports Co-Authored-By: Claude Opus 4.5 --- .claude/skills/customize/SKILL.md | 2 +- CLAUDE.md | 2 +- README.md | 2 +- SPEC.md | 5 +- package.json | 2 +- src/index.ts | 19 +- src/scheduler-mcp.ts | 284 ------------------------ src/{scheduler.ts => task-scheduler.ts} | 0 src/utils.ts | 18 ++ src/{auth.ts => whatsapp-auth.ts} | 2 +- 10 files changed, 28 insertions(+), 308 deletions(-) delete mode 100644 src/scheduler-mcp.ts rename src/{scheduler.ts => task-scheduler.ts} (100%) create mode 100644 src/utils.ts rename src/{auth.ts => whatsapp-auth.ts} (98%) diff --git a/.claude/skills/customize/SKILL.md b/.claude/skills/customize/SKILL.md index c5ba96e..e01ec19 100644 --- a/.claude/skills/customize/SKILL.md +++ b/.claude/skills/customize/SKILL.md @@ -22,7 +22,7 @@ This skill helps users add capabilities or modify behavior. Use AskUserQuestion | `src/index.ts` | Message routing, WhatsApp connection, agent invocation | | `src/db.ts` | Database initialization and queries | | `src/types.ts` | TypeScript interfaces | -| `src/auth.ts` | Standalone WhatsApp authentication script | +| `src/whatsapp-auth.ts` | Standalone WhatsApp authentication script | | `.mcp.json` | MCP server configuration (reference) | | `groups/CLAUDE.md` | Global memory/persona | diff --git a/CLAUDE.md b/CLAUDE.md index 1139cae..8d68509 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ Single Node.js process that connects to WhatsApp, routes messages to Claude Agen | `src/index.ts` | Main app: WhatsApp connection, message routing, IPC | | `src/config.ts` | Trigger pattern, paths, intervals | | `src/container-runner.ts` | Spawns agent containers with mounts | -| `src/scheduler.ts` | Runs scheduled tasks | +| `src/task-scheduler.ts` | Runs scheduled tasks | | `src/db.ts` | SQLite operations | | `groups/{name}/CLAUDE.md` | Per-group memory (isolated) | diff --git a/README.md b/README.md index 984b906..5d13076 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Single Node.js process. Agents execute in isolated Linux containers with mounted Key files: - `src/index.ts` - Main app: WhatsApp connection, routing, IPC - `src/container-runner.ts` - Spawns agent containers -- `src/scheduler.ts` - Runs scheduled tasks +- `src/task-scheduler.ts` - Runs scheduled tasks - `src/db.ts` - SQLite operations - `groups/*/CLAUDE.md` - Per-group memory diff --git a/SPEC.md b/SPEC.md index b22fcda..f0ca5a8 100644 --- a/SPEC.md +++ b/SPEC.md @@ -98,9 +98,10 @@ nanoclaw/ │ ├── index.ts # Main application (WhatsApp + routing) │ ├── config.ts # Configuration constants │ ├── types.ts # TypeScript interfaces +│ ├── utils.ts # Generic utility functions │ ├── db.ts # Database initialization and queries -│ ├── auth.ts # Standalone WhatsApp authentication -│ ├── scheduler.ts # Scheduler loop (runs due tasks) +│ ├── whatsapp-auth.ts # Standalone WhatsApp authentication +│ ├── task-scheduler.ts # Runs scheduled tasks when due │ └── container-runner.ts # Spawns agents in Apple Containers │ ├── container/ diff --git a/package.json b/package.json index b862038..bb4f43f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "tsc", "start": "node dist/index.js", "dev": "tsx src/index.ts", - "auth": "tsx src/auth.ts", + "auth": "tsx src/whatsapp-auth.ts", "lint": "eslint src/", "typecheck": "tsc --noEmit" }, diff --git a/src/index.ts b/src/index.ts index 9fa25b0..8167aef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,8 +20,9 @@ import { } from './config.js'; import { RegisteredGroup, Session, NewMessage } from './types.js'; import { initDatabase, storeMessage, storeChatMetadata, getNewMessages, getMessagesSince, getAllTasks, getTaskById } from './db.js'; -import { startSchedulerLoop } from './scheduler.js'; +import { startSchedulerLoop } from './task-scheduler.js'; import { runContainerAgent, writeTasksSnapshot } from './container-runner.js'; +import { loadJson, saveJson } from './utils.js'; const logger = pino({ level: process.env.LOG_LEVEL || 'info', @@ -42,22 +43,6 @@ async function setTyping(jid: string, isTyping: boolean): Promise { } } -function loadJson(filePath: string, defaultValue: T): T { - try { - if (fs.existsSync(filePath)) { - return JSON.parse(fs.readFileSync(filePath, 'utf-8')); - } - } catch (e) { - logger.warn({ filePath, error: e }, 'Failed to load JSON file'); - } - return defaultValue; -} - -function saveJson(filePath: string, data: unknown): void { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); -} - function loadState(): void { const statePath = path.join(DATA_DIR, 'router_state.json'); const state = loadJson<{ last_timestamp?: string; last_agent_timestamp?: Record }>(statePath, {}); diff --git a/src/scheduler-mcp.ts b/src/scheduler-mcp.ts deleted file mode 100644 index 971ed8f..0000000 --- a/src/scheduler-mcp.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'; -import { z } from 'zod'; -import { CronExpressionParser } from 'cron-parser'; -import { - createTask, - getTaskById, - getTasksForGroup, - getAllTasks, - updateTask, - deleteTask, - getTaskRunLogs -} from './db.js'; -import { ScheduledTask } from './types.js'; -import { MAIN_GROUP_FOLDER } from './config.js'; - -function generateTaskId(): string { - return `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; -} - -function calculateNextRun(scheduleType: string, scheduleValue: string): string | null { - const now = new Date(); - - switch (scheduleType) { - case 'cron': { - const interval = CronExpressionParser.parse(scheduleValue); - return interval.next().toISOString(); - } - case 'interval': { - const ms = parseInt(scheduleValue, 10); - return new Date(now.getTime() + ms).toISOString(); - } - case 'once': { - const runAt = new Date(scheduleValue); - return runAt > now ? runAt.toISOString() : null; - } - default: - return null; - } -} - -function formatTask(task: ScheduledTask): string { - const lines = [ - `ID: ${task.id}`, - `Group: ${task.group_folder}`, - `Prompt: ${task.prompt}`, - `Schedule: ${task.schedule_type} (${task.schedule_value})`, - `Status: ${task.status}`, - `Next run: ${task.next_run || 'N/A'}`, - `Last run: ${task.last_run || 'Never'}`, - `Last result: ${task.last_result || 'N/A'}` - ]; - return lines.join('\n'); -} - -export interface SchedulerMcpContext { - groupFolder: string; - chatJid: string; - isMain: boolean; - sendMessage: (jid: string, text: string) => Promise; -} - -export function createSchedulerMcp(ctx: SchedulerMcpContext) { - const { groupFolder, chatJid, isMain, sendMessage } = ctx; - - return createSdkMcpServer({ - name: 'nanoclaw', - version: '1.0.0', - tools: [ - tool( - 'schedule_task', - 'Schedule a recurring or one-time task. The task will run as an agent in the current group context.', - { - prompt: z.string().describe('The prompt/instruction for the task when it runs'), - schedule_type: z.enum(['cron', 'interval', 'once']).describe('Type of schedule: cron (e.g., "0 9 * * 1" for Mondays at 9am), interval (milliseconds), or once (ISO timestamp)'), - schedule_value: z.string().describe('Schedule value: cron expression, milliseconds for interval, or ISO timestamp for once'), - target_group: z.string().optional().describe('(Main channel only) Target group folder to run the task in. Defaults to current group.') - }, - async (args) => { - const targetGroup = isMain && args.target_group ? args.target_group : groupFolder; - const targetJid = isMain && args.target_group ? '' : chatJid; // Will need to look up JID for other groups - - // Validate schedule - const nextRun = calculateNextRun(args.schedule_type, args.schedule_value); - if (nextRun === null && args.schedule_type !== 'once') { - return { content: [{ type: 'text', text: 'Error: Invalid schedule. Task would never run.' }] }; - } - - const task: Omit = { - id: generateTaskId(), - group_folder: targetGroup, - chat_jid: targetJid || chatJid, - prompt: args.prompt, - schedule_type: args.schedule_type, - schedule_value: args.schedule_value, - next_run: nextRun, - status: 'active', - created_at: new Date().toISOString() - }; - - createTask(task); - - return { - content: [{ - type: 'text', - text: `Task scheduled successfully!\n\n${formatTask(task as ScheduledTask)}` - }] - }; - } - ), - - tool( - 'list_tasks', - 'List scheduled tasks. Shows tasks for the current group, or all tasks if called from the main channel.', - {}, - async () => { - const tasks = isMain ? getAllTasks() : getTasksForGroup(groupFolder); - - if (tasks.length === 0) { - return { content: [{ type: 'text', text: 'No scheduled tasks found.' }] }; - } - - const formatted = tasks.map((t, i) => `--- Task ${i + 1} ---\n${formatTask(t)}`).join('\n\n'); - return { content: [{ type: 'text', text: `Found ${tasks.length} task(s):\n\n${formatted}` }] }; - } - ), - - tool( - 'get_task', - 'Get details about a specific task including run history.', - { - task_id: z.string().describe('The task ID') - }, - async (args) => { - const task = getTaskById(args.task_id); - if (!task) { - return { content: [{ type: 'text', text: `Task not found: ${args.task_id}` }] }; - } - - // Check permissions - if (!isMain && task.group_folder !== groupFolder) { - return { content: [{ type: 'text', text: 'Access denied: Task belongs to another group.' }] }; - } - - const logs = getTaskRunLogs(args.task_id, 5); - let output = formatTask(task); - - if (logs.length > 0) { - output += '\n\n--- Recent Runs ---\n'; - output += logs.map(l => - `${l.run_at}: ${l.status} (${l.duration_ms}ms)${l.error ? ` - ${l.error}` : ''}` - ).join('\n'); - } - - return { content: [{ type: 'text', text: output }] }; - } - ), - - tool( - 'update_task', - 'Update a scheduled task.', - { - task_id: z.string().describe('The task ID'), - prompt: z.string().optional().describe('New prompt for the task'), - schedule_type: z.enum(['cron', 'interval', 'once']).optional().describe('New schedule type'), - schedule_value: z.string().optional().describe('New schedule value') - }, - async (args) => { - const task = getTaskById(args.task_id); - if (!task) { - return { content: [{ type: 'text', text: `Task not found: ${args.task_id}` }] }; - } - - if (!isMain && task.group_folder !== groupFolder) { - return { content: [{ type: 'text', text: 'Access denied: Task belongs to another group.' }] }; - } - - const updates: Parameters[1] = {}; - if (args.prompt) updates.prompt = args.prompt; - if (args.schedule_type) updates.schedule_type = args.schedule_type; - if (args.schedule_value) updates.schedule_value = args.schedule_value; - - // Recalculate next_run if schedule changed - if (args.schedule_type || args.schedule_value) { - const schedType = args.schedule_type || task.schedule_type; - const schedValue = args.schedule_value || task.schedule_value; - updates.next_run = calculateNextRun(schedType, schedValue); - } - - updateTask(args.task_id, updates); - const updated = getTaskById(args.task_id)!; - - return { content: [{ type: 'text', text: `Task updated!\n\n${formatTask(updated)}` }] }; - } - ), - - tool( - 'pause_task', - 'Pause a scheduled task.', - { - task_id: z.string().describe('The task ID') - }, - async (args) => { - const task = getTaskById(args.task_id); - if (!task) { - return { content: [{ type: 'text', text: `Task not found: ${args.task_id}` }] }; - } - - if (!isMain && task.group_folder !== groupFolder) { - return { content: [{ type: 'text', text: 'Access denied: Task belongs to another group.' }] }; - } - - updateTask(args.task_id, { status: 'paused' }); - return { content: [{ type: 'text', text: `Task ${args.task_id} paused.` }] }; - } - ), - - tool( - 'resume_task', - 'Resume a paused task.', - { - task_id: z.string().describe('The task ID') - }, - async (args) => { - const task = getTaskById(args.task_id); - if (!task) { - return { content: [{ type: 'text', text: `Task not found: ${args.task_id}` }] }; - } - - if (!isMain && task.group_folder !== groupFolder) { - return { content: [{ type: 'text', text: 'Access denied: Task belongs to another group.' }] }; - } - - // Recalculate next_run when resuming - const nextRun = calculateNextRun(task.schedule_type, task.schedule_value); - updateTask(args.task_id, { status: 'active', next_run: nextRun }); - - return { content: [{ type: 'text', text: `Task ${args.task_id} resumed. Next run: ${nextRun}` }] }; - } - ), - - tool( - 'cancel_task', - 'Cancel and delete a scheduled task.', - { - task_id: z.string().describe('The task ID') - }, - async (args) => { - const task = getTaskById(args.task_id); - if (!task) { - return { content: [{ type: 'text', text: `Task not found: ${args.task_id}` }] }; - } - - if (!isMain && task.group_folder !== groupFolder) { - return { content: [{ type: 'text', text: 'Access denied: Task belongs to another group.' }] }; - } - - deleteTask(args.task_id); - return { content: [{ type: 'text', text: `Task ${args.task_id} cancelled and deleted.` }] }; - } - ), - - tool( - 'send_message', - 'Send a message to the WhatsApp group. Use this to notify the group about task results or updates.', - { - text: z.string().describe('The message text to send'), - target_jid: z.string().optional().describe('(Main channel only) Target group JID. Defaults to current group.') - }, - async (args) => { - const targetJid = isMain && args.target_jid ? args.target_jid : chatJid; - - try { - await sendMessage(targetJid, args.text); - return { content: [{ type: 'text', text: 'Message sent successfully.' }] }; - } catch (error) { - return { content: [{ type: 'text', text: `Failed to send message: ${error}` }] }; - } - } - ) - ] - }); -} - -export { calculateNextRun }; diff --git a/src/scheduler.ts b/src/task-scheduler.ts similarity index 100% rename from src/scheduler.ts rename to src/task-scheduler.ts diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..3cd7d0b --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,18 @@ +import fs from 'fs'; +import path from 'path'; + +export function loadJson(filePath: string, defaultValue: T): T { + try { + if (fs.existsSync(filePath)) { + return JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } + } catch { + // Return default on error + } + return defaultValue; +} + +export function saveJson(filePath: string, data: unknown): void { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); +} diff --git a/src/auth.ts b/src/whatsapp-auth.ts similarity index 98% rename from src/auth.ts rename to src/whatsapp-auth.ts index 5ca64d0..a075d2a 100644 --- a/src/auth.ts +++ b/src/whatsapp-auth.ts @@ -4,7 +4,7 @@ * Run this during setup to authenticate with WhatsApp. * Displays QR code, waits for scan, saves credentials, then exits. * - * Usage: npx tsx src/auth.ts + * Usage: npx tsx src/whatsapp-auth.ts */ import makeWASocket, {