diff --git a/SPEC.md b/SPEC.md index c70a6a1..b22fcda 100644 --- a/SPEC.md +++ b/SPEC.md @@ -53,7 +53,7 @@ A personal Claude assistant accessible via WhatsApp, with persistent memory per │ │ Working directory: /workspace/group (mounted from host) │ │ │ │ Volume mounts: │ │ │ │ • groups/{name}/ → /workspace/group │ │ -│ │ • groups/CLAUDE.md → /workspace/global/CLAUDE.md │ │ +│ │ • groups/global/ → /workspace/global/ (non-main only) │ │ │ │ • ~/.claude/ → /home/node/.claude/ (sessions) │ │ │ │ • Additional dirs → /workspace/extra/* │ │ │ │ │ │ diff --git a/container/agent-runner/src/index.ts b/container/agent-runner/src/index.ts index 652654b..5e6f9c3 100644 --- a/container/agent-runner/src/index.ts +++ b/container/agent-runner/src/index.ts @@ -14,6 +14,7 @@ interface ContainerInput { groupFolder: string; chatJid: string; isMain: boolean; + isScheduledTask?: boolean; } interface ContainerOutput { @@ -219,11 +220,17 @@ async function main(): Promise { let result: string | null = null; let newSessionId: string | undefined; + // Add context for scheduled tasks + let prompt = input.prompt; + if (input.isScheduledTask) { + prompt = `[SCHEDULED TASK - You are running automatically, not in response to a user message. Use mcp__nanoclaw__send_message if needed to communicate with the user.]\n\n${input.prompt}`; + } + try { log('Starting agent...'); for await (const message of query({ - prompt: input.prompt, + prompt, options: { cwd: '/workspace/group', resume: input.sessionId, diff --git a/container/agent-runner/src/ipc-mcp.ts b/container/agent-runner/src/ipc-mcp.ts index 37ffd94..9d0d712 100644 --- a/container/agent-runner/src/ipc-mcp.ts +++ b/container/agent-runner/src/ipc-mcp.ts @@ -67,11 +67,16 @@ export function createIpcMcp(ctx: IpcMcpContext) { tool( 'schedule_task', - 'Schedule a recurring or one-time task. The task will run as a full agent with access to all tools.', + `Schedule a recurring or one-time task. The task will run as a full agent with access to all tools. + +IMPORTANT - schedule_value format depends on schedule_type: +• cron: Standard cron expression (e.g., "*/5 * * * *" for every 5 minutes, "0 9 * * *" for daily at 9am) +• interval: Milliseconds between runs (e.g., "300000" for 5 minutes, "3600000" for 1 hour) +• once: ISO 8601 timestamp (e.g., "2026-02-01T15:30:00.000Z"). Calculate this from current time.`, { prompt: z.string().describe('What the agent should do when the task runs'), - schedule_type: z.enum(['cron', 'interval', 'once']).describe('Type of schedule'), - schedule_value: z.string().describe('Cron expression, interval in ms, or ISO timestamp'), + schedule_type: z.enum(['cron', 'interval', 'once']).describe('cron=recurring at specific times, interval=recurring every N ms, once=run once at specific time'), + schedule_value: z.string().describe('cron: "*/5 * * * *" | interval: milliseconds like "300000" | once: ISO timestamp like "2026-02-01T15:30:00.000Z"'), target_group: z.string().optional().describe('Target group folder (main only, defaults to current group)') }, async (args) => { diff --git a/groups/CLAUDE.md b/groups/global/CLAUDE.md similarity index 76% rename from groups/CLAUDE.md rename to groups/global/CLAUDE.md index c084ae6..0865d9f 100644 --- a/groups/CLAUDE.md +++ b/groups/global/CLAUDE.md @@ -21,6 +21,15 @@ If a request requires significant work (research, multiple steps, file operation This keeps users informed instead of waiting in silence. +## Scheduled Tasks + +When you run as a scheduled task (no direct user message), use `mcp__nanoclaw__send_message` if needed to communicate with the user. Your return value is only logged internally - it won't be sent to the user. + +Example: If your task is "Share the weather forecast", you should: +1. Get the weather data +2. Call `mcp__nanoclaw__send_message` with the formatted forecast +3. Return a brief summary for the logs + ## Your Workspace Files you create are saved in `/workspace/group/`. Use this for notes, research, or anything that should persist. diff --git a/groups/main/CLAUDE.md b/groups/main/CLAUDE.md index e7286b1..a1db749 100644 --- a/groups/main/CLAUDE.md +++ b/groups/main/CLAUDE.md @@ -147,7 +147,7 @@ Read `/workspace/project/data/registered_groups.json` and format it nicely. ## Global Memory -You can read and write to `/workspace/project/groups/CLAUDE.md` for facts that should apply to all groups. Only update global memory when explicitly asked to "remember this globally" or similar. +You can read and write to `/workspace/project/groups/global/CLAUDE.md` for facts that should apply to all groups. Only update global memory when explicitly asked to "remember this globally" or similar. --- diff --git a/src/container-runner.ts b/src/container-runner.ts index ef025e1..3c6c783 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -26,6 +26,7 @@ export interface ContainerInput { groupFolder: string; chatJid: string; isMain: boolean; + isScheduledTask?: boolean; } export interface ContainerOutput { @@ -68,12 +69,13 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount readonly: false }); - // Global CLAUDE.md (read-only for non-main) - const globalClaudeMd = path.join(GROUPS_DIR, 'CLAUDE.md'); - if (fs.existsSync(globalClaudeMd)) { + // Global memory directory (read-only for non-main) + // Apple Container only supports directory mounts, not file mounts + const globalDir = path.join(GROUPS_DIR, 'global'); + if (fs.existsSync(globalDir)) { mounts.push({ - hostPath: globalClaudeMd, - containerPath: '/workspace/global/CLAUDE.md', + hostPath: globalDir, + containerPath: '/workspace/global', readonly: true }); } diff --git a/src/scheduler.ts b/src/scheduler.ts index 0eb463a..52c9b87 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -4,7 +4,7 @@ import pino from 'pino'; import { CronExpressionParser } from 'cron-parser'; import { getDueTasks, updateTaskAfterRun, logTaskRun, getTaskById, getAllTasks } from './db.js'; import { ScheduledTask, RegisteredGroup } from './types.js'; -import { GROUPS_DIR, SCHEDULER_POLL_INTERVAL, DATA_DIR } from './config.js'; +import { GROUPS_DIR, SCHEDULER_POLL_INTERVAL, DATA_DIR, MAIN_GROUP_FOLDER } from './config.js'; import { runContainerAgent, writeTasksSnapshot } from './container-runner.js'; const logger = pino({ @@ -56,11 +56,13 @@ async function runTask(task: ScheduledTask, deps: SchedulerDependencies): Promis let error: string | null = null; try { + const isMain = task.group_folder === MAIN_GROUP_FOLDER; const output = await runContainerAgent(group, { prompt: task.prompt, groupFolder: task.group_folder, chatJid: task.chat_jid, - isMain: false // Scheduled tasks run in their group's context + isMain, + isScheduledTask: true }); if (output.status === 'error') {