From 87e89147c934db87f1263160c435863e4ebf033c Mon Sep 17 00:00:00 2001 From: gavrielc Date: Wed, 1 Apr 2026 21:52:45 +0300 Subject: [PATCH] style: run prettier on container/agent-runner/src/ Co-Authored-By: Claude Opus 4.6 (1M context) --- container/agent-runner/src/index.ts | 278 ++++++++++++++------ container/agent-runner/src/ipc-mcp-stdio.ts | 231 +++++++++++++--- 2 files changed, 386 insertions(+), 123 deletions(-) diff --git a/container/agent-runner/src/index.ts b/container/agent-runner/src/index.ts index 25554f9..e0d6ff6 100644 --- a/container/agent-runner/src/index.ts +++ b/container/agent-runner/src/index.ts @@ -17,7 +17,11 @@ import fs from 'fs'; import path from 'path'; import { execFile } from 'child_process'; -import { query, HookCallback, PreCompactHookInput } from '@anthropic-ai/claude-agent-sdk'; +import { + query, + HookCallback, + PreCompactHookInput, +} from '@anthropic-ai/claude-agent-sdk'; import { fileURLToPath } from 'url'; interface ContainerInput { @@ -90,7 +94,9 @@ class MessageStream { yield this.queue.shift()!; } if (this.done) return; - await new Promise(r => { this.waiting = r; }); + await new Promise((r) => { + this.waiting = r; + }); this.waiting = null; } } @@ -100,7 +106,9 @@ async function readStdin(): Promise { return new Promise((resolve, reject) => { let data = ''; process.stdin.setEncoding('utf8'); - process.stdin.on('data', chunk => { data += chunk; }); + process.stdin.on('data', (chunk) => { + data += chunk; + }); process.stdin.on('end', () => resolve(data)); process.stdin.on('error', reject); }); @@ -119,7 +127,10 @@ function log(message: string): void { console.error(`[agent-runner] ${message}`); } -function getSessionSummary(sessionId: string, transcriptPath: string): string | null { +function getSessionSummary( + sessionId: string, + transcriptPath: string, +): string | null { const projectDir = path.dirname(transcriptPath); const indexPath = path.join(projectDir, 'sessions-index.json'); @@ -129,13 +140,17 @@ function getSessionSummary(sessionId: string, transcriptPath: string): string | } try { - const index: SessionsIndex = JSON.parse(fs.readFileSync(indexPath, 'utf-8')); - const entry = index.entries.find(e => e.sessionId === sessionId); + const index: SessionsIndex = JSON.parse( + fs.readFileSync(indexPath, 'utf-8'), + ); + const entry = index.entries.find((e) => e.sessionId === sessionId); if (entry?.summary) { return entry.summary; } } catch (err) { - log(`Failed to read sessions index: ${err instanceof Error ? err.message : String(err)}`); + log( + `Failed to read sessions index: ${err instanceof Error ? err.message : String(err)}`, + ); } return null; @@ -174,12 +189,18 @@ function createPreCompactHook(assistantName?: string): HookCallback { const filename = `${date}-${name}.md`; const filePath = path.join(conversationsDir, filename); - const markdown = formatTranscriptMarkdown(messages, summary, assistantName); + const markdown = formatTranscriptMarkdown( + messages, + summary, + assistantName, + ); fs.writeFileSync(filePath, markdown); log(`Archived conversation to ${filePath}`); } catch (err) { - log(`Failed to archive transcript: ${err instanceof Error ? err.message : String(err)}`); + log( + `Failed to archive transcript: ${err instanceof Error ? err.message : String(err)}`, + ); } return {}; @@ -212,9 +233,12 @@ function parseTranscript(content: string): ParsedMessage[] { try { const entry = JSON.parse(line); if (entry.type === 'user' && entry.message?.content) { - const text = typeof entry.message.content === 'string' - ? entry.message.content - : entry.message.content.map((c: { text?: string }) => c.text || '').join(''); + const text = + typeof entry.message.content === 'string' + ? entry.message.content + : entry.message.content + .map((c: { text?: string }) => c.text || '') + .join(''); if (text) messages.push({ role: 'user', content: text }); } else if (entry.type === 'assistant' && entry.message?.content) { const textParts = entry.message.content @@ -223,22 +247,26 @@ function parseTranscript(content: string): ParsedMessage[] { const text = textParts.join(''); if (text) messages.push({ role: 'assistant', content: text }); } - } catch { - } + } catch {} } return messages; } -function formatTranscriptMarkdown(messages: ParsedMessage[], title?: string | null, assistantName?: string): string { +function formatTranscriptMarkdown( + messages: ParsedMessage[], + title?: string | null, + assistantName?: string, +): string { const now = new Date(); - const formatDateTime = (d: Date) => d.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true - }); + const formatDateTime = (d: Date) => + d.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); const lines: string[] = []; lines.push(`# ${title || 'Conversation'}`); @@ -249,10 +277,11 @@ function formatTranscriptMarkdown(messages: ParsedMessage[], title?: string | nu lines.push(''); for (const msg of messages) { - const sender = msg.role === 'user' ? 'User' : (assistantName || 'Assistant'); - const content = msg.content.length > 2000 - ? msg.content.slice(0, 2000) + '...' - : msg.content; + const sender = msg.role === 'user' ? 'User' : assistantName || 'Assistant'; + const content = + msg.content.length > 2000 + ? msg.content.slice(0, 2000) + '...' + : msg.content; lines.push(`**${sender}**: ${content}`); lines.push(''); } @@ -265,7 +294,11 @@ function formatTranscriptMarkdown(messages: ParsedMessage[], title?: string | nu */ function shouldClose(): boolean { if (fs.existsSync(IPC_INPUT_CLOSE_SENTINEL)) { - try { fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); } catch { /* ignore */ } + try { + fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); + } catch { + /* ignore */ + } return true; } return false; @@ -278,8 +311,9 @@ function shouldClose(): boolean { function drainIpcInput(): string[] { try { fs.mkdirSync(IPC_INPUT_DIR, { recursive: true }); - const files = fs.readdirSync(IPC_INPUT_DIR) - .filter(f => f.endsWith('.json')) + const files = fs + .readdirSync(IPC_INPUT_DIR) + .filter((f) => f.endsWith('.json')) .sort(); const messages: string[] = []; @@ -292,8 +326,14 @@ function drainIpcInput(): string[] { messages.push(data.text); } } catch (err) { - log(`Failed to process input file ${file}: ${err instanceof Error ? err.message : String(err)}`); - try { fs.unlinkSync(filePath); } catch { /* ignore */ } + log( + `Failed to process input file ${file}: ${err instanceof Error ? err.message : String(err)}`, + ); + try { + fs.unlinkSync(filePath); + } catch { + /* ignore */ + } } } return messages; @@ -338,7 +378,11 @@ async function runQuery( containerInput: ContainerInput, sdkEnv: Record, resumeAt?: string, -): Promise<{ newSessionId?: string; lastAssistantUuid?: string; closedDuringQuery: boolean }> { +): Promise<{ + newSessionId?: string; + lastAssistantUuid?: string; + closedDuringQuery: boolean; +}> { const stream = new MessageStream(); stream.push(prompt); @@ -399,17 +443,32 @@ async function runQuery( resume: sessionId, resumeSessionAt: resumeAt, systemPrompt: globalClaudeMd - ? { type: 'preset' as const, preset: 'claude_code' as const, append: globalClaudeMd } + ? { + type: 'preset' as const, + preset: 'claude_code' as const, + append: globalClaudeMd, + } : undefined, allowedTools: [ 'Bash', - 'Read', 'Write', 'Edit', 'Glob', 'Grep', - 'WebSearch', 'WebFetch', - 'Task', 'TaskOutput', 'TaskStop', - 'TeamCreate', 'TeamDelete', 'SendMessage', - 'TodoWrite', 'ToolSearch', 'Skill', + 'Read', + 'Write', + 'Edit', + 'Glob', + 'Grep', + 'WebSearch', + 'WebFetch', + 'Task', + 'TaskOutput', + 'TaskStop', + 'TeamCreate', + 'TeamDelete', + 'SendMessage', + 'TodoWrite', + 'ToolSearch', + 'Skill', 'NotebookEdit', - 'mcp__nanoclaw__*' + 'mcp__nanoclaw__*', ], env: sdkEnv, permissionMode: 'bypassPermissions', @@ -427,12 +486,17 @@ async function runQuery( }, }, hooks: { - PreCompact: [{ hooks: [createPreCompactHook(containerInput.assistantName)] }], + PreCompact: [ + { hooks: [createPreCompactHook(containerInput.assistantName)] }, + ], }, - } + }, })) { messageCount++; - const msgType = message.type === 'system' ? `system/${(message as { subtype?: string }).subtype}` : message.type; + const msgType = + message.type === 'system' + ? `system/${(message as { subtype?: string }).subtype}` + : message.type; log(`[msg #${messageCount}] type=${msgType}`); if (message.type === 'assistant' && 'uuid' in message) { @@ -444,25 +508,39 @@ async function runQuery( log(`Session initialized: ${newSessionId}`); } - if (message.type === 'system' && (message as { subtype?: string }).subtype === 'task_notification') { - const tn = message as { task_id: string; status: string; summary: string }; - log(`Task notification: task=${tn.task_id} status=${tn.status} summary=${tn.summary}`); + if ( + message.type === 'system' && + (message as { subtype?: string }).subtype === 'task_notification' + ) { + const tn = message as { + task_id: string; + status: string; + summary: string; + }; + log( + `Task notification: task=${tn.task_id} status=${tn.status} summary=${tn.summary}`, + ); } if (message.type === 'result') { resultCount++; - const textResult = 'result' in message ? (message as { result?: string }).result : null; - log(`Result #${resultCount}: subtype=${message.subtype}${textResult ? ` text=${textResult.slice(0, 200)}` : ''}`); + const textResult = + 'result' in message ? (message as { result?: string }).result : null; + log( + `Result #${resultCount}: subtype=${message.subtype}${textResult ? ` text=${textResult.slice(0, 200)}` : ''}`, + ); writeOutput({ status: 'success', result: textResult || null, - newSessionId + newSessionId, }); } } ipcPolling = false; - log(`Query done. Messages: ${messageCount}, results: ${resultCount}, lastAssistantUuid: ${lastAssistantUuid || 'none'}, closedDuringQuery: ${closedDuringQuery}`); + log( + `Query done. Messages: ${messageCount}, results: ${resultCount}, lastAssistantUuid: ${lastAssistantUuid || 'none'}, closedDuringQuery: ${closedDuringQuery}`, + ); return { newSessionId, lastAssistantUuid, closedDuringQuery }; } @@ -478,40 +556,47 @@ async function runScript(script: string): Promise { fs.writeFileSync(scriptPath, script, { mode: 0o755 }); return new Promise((resolve) => { - execFile('bash', [scriptPath], { - timeout: SCRIPT_TIMEOUT_MS, - maxBuffer: 1024 * 1024, - env: process.env, - }, (error, stdout, stderr) => { - if (stderr) { - log(`Script stderr: ${stderr.slice(0, 500)}`); - } + execFile( + 'bash', + [scriptPath], + { + timeout: SCRIPT_TIMEOUT_MS, + maxBuffer: 1024 * 1024, + env: process.env, + }, + (error, stdout, stderr) => { + if (stderr) { + log(`Script stderr: ${stderr.slice(0, 500)}`); + } - if (error) { - log(`Script error: ${error.message}`); - return resolve(null); - } - - // Parse last non-empty line of stdout as JSON - const lines = stdout.trim().split('\n'); - const lastLine = lines[lines.length - 1]; - if (!lastLine) { - log('Script produced no output'); - return resolve(null); - } - - try { - const result = JSON.parse(lastLine); - if (typeof result.wakeAgent !== 'boolean') { - log(`Script output missing wakeAgent boolean: ${lastLine.slice(0, 200)}`); + if (error) { + log(`Script error: ${error.message}`); return resolve(null); } - resolve(result as ScriptResult); - } catch { - log(`Script output is not valid JSON: ${lastLine.slice(0, 200)}`); - resolve(null); - } - }); + + // Parse last non-empty line of stdout as JSON + const lines = stdout.trim().split('\n'); + const lastLine = lines[lines.length - 1]; + if (!lastLine) { + log('Script produced no output'); + return resolve(null); + } + + try { + const result = JSON.parse(lastLine); + if (typeof result.wakeAgent !== 'boolean') { + log( + `Script output missing wakeAgent boolean: ${lastLine.slice(0, 200)}`, + ); + return resolve(null); + } + resolve(result as ScriptResult); + } catch { + log(`Script output is not valid JSON: ${lastLine.slice(0, 200)}`); + resolve(null); + } + }, + ); }); } @@ -521,13 +606,17 @@ async function main(): Promise { try { const stdinData = await readStdin(); containerInput = JSON.parse(stdinData); - try { fs.unlinkSync('/tmp/input.json'); } catch { /* may not exist */ } + try { + fs.unlinkSync('/tmp/input.json'); + } catch { + /* may not exist */ + } log(`Received input for group: ${containerInput.groupFolder}`); } catch (err) { writeOutput({ status: 'error', result: null, - error: `Failed to parse input: ${err instanceof Error ? err.message : String(err)}` + error: `Failed to parse input: ${err instanceof Error ? err.message : String(err)}`, }); process.exit(1); } @@ -543,7 +632,11 @@ async function main(): Promise { fs.mkdirSync(IPC_INPUT_DIR, { recursive: true }); // Clean up stale _close sentinel from previous container runs - try { fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); } catch { /* ignore */ } + try { + fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); + } catch { + /* ignore */ + } // Build initial prompt (drain any pending IPC messages too) let prompt = containerInput.prompt; @@ -562,7 +655,9 @@ async function main(): Promise { const scriptResult = await runScript(containerInput.script); if (!scriptResult || !scriptResult.wakeAgent) { - const reason = scriptResult ? 'wakeAgent=false' : 'script error/no output'; + const reason = scriptResult + ? 'wakeAgent=false' + : 'script error/no output'; log(`Script decided not to wake agent: ${reason}`); writeOutput({ status: 'success', @@ -580,9 +675,18 @@ async function main(): Promise { let resumeAt: string | undefined; try { while (true) { - log(`Starting query (session: ${sessionId || 'new'}, resumeAt: ${resumeAt || 'latest'})...`); + log( + `Starting query (session: ${sessionId || 'new'}, resumeAt: ${resumeAt || 'latest'})...`, + ); - const queryResult = await runQuery(prompt, sessionId, mcpServerPath, containerInput, sdkEnv, resumeAt); + const queryResult = await runQuery( + prompt, + sessionId, + mcpServerPath, + containerInput, + sdkEnv, + resumeAt, + ); if (queryResult.newSessionId) { sessionId = queryResult.newSessionId; } @@ -620,7 +724,7 @@ async function main(): Promise { status: 'error', result: null, newSessionId: sessionId, - error: errorMessage + error: errorMessage, }); process.exit(1); } diff --git a/container/agent-runner/src/ipc-mcp-stdio.ts b/container/agent-runner/src/ipc-mcp-stdio.ts index 5b03478..fb429ed 100644 --- a/container/agent-runner/src/ipc-mcp-stdio.ts +++ b/container/agent-runner/src/ipc-mcp-stdio.ts @@ -44,7 +44,12 @@ server.tool( "Send a message to the user or group immediately while you're still running. Use this for progress updates or to send multiple messages. You can call this multiple times.", { text: z.string().describe('The message text to send'), - sender: z.string().optional().describe('Your role/identity name (e.g. "Researcher"). When set, messages appear from a dedicated bot in Telegram.'), + sender: z + .string() + .optional() + .describe( + 'Your role/identity name (e.g. "Researcher"). When set, messages appear from a dedicated bot in Telegram.', + ), }, async (args) => { const data: Record = { @@ -86,12 +91,39 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone): \u2022 interval: Milliseconds between runs (e.g., "300000" for 5 minutes, "3600000" for 1 hour) \u2022 once: Local time WITHOUT "Z" suffix (e.g., "2026-02-01T15:30:00"). Do NOT use UTC/Z suffix.`, { - prompt: z.string().describe('What the agent should do when the task runs. For isolated mode, include all necessary context here.'), - 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: local timestamp like "2026-02-01T15:30:00" (no Z suffix!)'), - context_mode: z.enum(['group', 'isolated']).default('group').describe('group=runs with chat history and memory, isolated=fresh session (include context in prompt)'), - target_group_jid: z.string().optional().describe('(Main group only) JID of the group to schedule the task for. Defaults to the current group.'), - script: z.string().optional().describe('Optional bash script to run before waking the agent. Script must output JSON on the last line of stdout: { "wakeAgent": boolean, "data"?: any }. If wakeAgent is false, the agent is not called. Test your script with bash -c "..." before scheduling.'), + prompt: z + .string() + .describe( + 'What the agent should do when the task runs. For isolated mode, include all necessary context here.', + ), + 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: local timestamp like "2026-02-01T15:30:00" (no Z suffix!)', + ), + context_mode: z + .enum(['group', 'isolated']) + .default('group') + .describe( + 'group=runs with chat history and memory, isolated=fresh session (include context in prompt)', + ), + target_group_jid: z + .string() + .optional() + .describe( + '(Main group only) JID of the group to schedule the task for. Defaults to the current group.', + ), + script: z + .string() + .optional() + .describe( + 'Optional bash script to run before waking the agent. Script must output JSON on the last line of stdout: { "wakeAgent": boolean, "data"?: any }. If wakeAgent is false, the agent is not called. Test your script with bash -c "..." before scheduling.', + ), }, async (args) => { // Validate schedule_value before writing IPC @@ -100,7 +132,12 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone): CronExpressionParser.parse(args.schedule_value); } catch { return { - content: [{ type: 'text' as const, text: `Invalid cron: "${args.schedule_value}". Use format like "0 9 * * *" (daily 9am) or "*/5 * * * *" (every 5 min).` }], + content: [ + { + type: 'text' as const, + text: `Invalid cron: "${args.schedule_value}". Use format like "0 9 * * *" (daily 9am) or "*/5 * * * *" (every 5 min).`, + }, + ], isError: true, }; } @@ -108,28 +145,47 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone): const ms = parseInt(args.schedule_value, 10); if (isNaN(ms) || ms <= 0) { return { - content: [{ type: 'text' as const, text: `Invalid interval: "${args.schedule_value}". Must be positive milliseconds (e.g., "300000" for 5 min).` }], + content: [ + { + type: 'text' as const, + text: `Invalid interval: "${args.schedule_value}". Must be positive milliseconds (e.g., "300000" for 5 min).`, + }, + ], isError: true, }; } } else if (args.schedule_type === 'once') { - if (/[Zz]$/.test(args.schedule_value) || /[+-]\d{2}:\d{2}$/.test(args.schedule_value)) { + if ( + /[Zz]$/.test(args.schedule_value) || + /[+-]\d{2}:\d{2}$/.test(args.schedule_value) + ) { return { - content: [{ type: 'text' as const, text: `Timestamp must be local time without timezone suffix. Got "${args.schedule_value}" — use format like "2026-02-01T15:30:00".` }], + content: [ + { + type: 'text' as const, + text: `Timestamp must be local time without timezone suffix. Got "${args.schedule_value}" — use format like "2026-02-01T15:30:00".`, + }, + ], isError: true, }; } const date = new Date(args.schedule_value); if (isNaN(date.getTime())) { return { - content: [{ type: 'text' as const, text: `Invalid timestamp: "${args.schedule_value}". Use local time format like "2026-02-01T15:30:00".` }], + content: [ + { + type: 'text' as const, + text: `Invalid timestamp: "${args.schedule_value}". Use local time format like "2026-02-01T15:30:00".`, + }, + ], isError: true, }; } } // Non-main groups can only schedule for themselves - const targetJid = isMain && args.target_group_jid ? args.target_group_jid : chatJid; + const targetJid = + isMain && args.target_group_jid ? args.target_group_jid : chatJid; const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; @@ -149,7 +205,12 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone): writeIpcFile(TASKS_DIR, data); return { - content: [{ type: 'text' as const, text: `Task ${taskId} scheduled: ${args.schedule_type} - ${args.schedule_value}` }], + content: [ + { + type: 'text' as const, + text: `Task ${taskId} scheduled: ${args.schedule_type} - ${args.schedule_value}`, + }, + ], }; }, ); @@ -163,30 +224,56 @@ server.tool( try { if (!fs.existsSync(tasksFile)) { - return { content: [{ type: 'text' as const, text: 'No scheduled tasks found.' }] }; + return { + content: [ + { type: 'text' as const, text: 'No scheduled tasks found.' }, + ], + }; } const allTasks = JSON.parse(fs.readFileSync(tasksFile, 'utf-8')); const tasks = isMain ? allTasks - : allTasks.filter((t: { groupFolder: string }) => t.groupFolder === groupFolder); + : allTasks.filter( + (t: { groupFolder: string }) => t.groupFolder === groupFolder, + ); if (tasks.length === 0) { - return { content: [{ type: 'text' as const, text: 'No scheduled tasks found.' }] }; + return { + content: [ + { type: 'text' as const, text: 'No scheduled tasks found.' }, + ], + }; } const formatted = tasks .map( - (t: { id: string; prompt: string; schedule_type: string; schedule_value: string; status: string; next_run: string }) => + (t: { + id: string; + prompt: string; + schedule_type: string; + schedule_value: string; + status: string; + next_run: string; + }) => `- [${t.id}] ${t.prompt.slice(0, 50)}... (${t.schedule_type}: ${t.schedule_value}) - ${t.status}, next: ${t.next_run || 'N/A'}`, ) .join('\n'); - return { content: [{ type: 'text' as const, text: `Scheduled tasks:\n${formatted}` }] }; + return { + content: [ + { type: 'text' as const, text: `Scheduled tasks:\n${formatted}` }, + ], + }; } catch (err) { return { - content: [{ type: 'text' as const, text: `Error reading tasks: ${err instanceof Error ? err.message : String(err)}` }], + content: [ + { + type: 'text' as const, + text: `Error reading tasks: ${err instanceof Error ? err.message : String(err)}`, + }, + ], }; } }, @@ -207,7 +294,14 @@ server.tool( writeIpcFile(TASKS_DIR, data); - return { content: [{ type: 'text' as const, text: `Task ${args.task_id} pause requested.` }] }; + return { + content: [ + { + type: 'text' as const, + text: `Task ${args.task_id} pause requested.`, + }, + ], + }; }, ); @@ -226,7 +320,14 @@ server.tool( writeIpcFile(TASKS_DIR, data); - return { content: [{ type: 'text' as const, text: `Task ${args.task_id} resume requested.` }] }; + return { + content: [ + { + type: 'text' as const, + text: `Task ${args.task_id} resume requested.`, + }, + ], + }; }, ); @@ -245,7 +346,14 @@ server.tool( writeIpcFile(TASKS_DIR, data); - return { content: [{ type: 'text' as const, text: `Task ${args.task_id} cancellation requested.` }] }; + return { + content: [ + { + type: 'text' as const, + text: `Task ${args.task_id} cancellation requested.`, + }, + ], + }; }, ); @@ -255,19 +363,38 @@ server.tool( { task_id: z.string().describe('The task ID to update'), 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 (see schedule_task for format)'), - script: z.string().optional().describe('New script for the task. Set to empty string to remove the script.'), + schedule_type: z + .enum(['cron', 'interval', 'once']) + .optional() + .describe('New schedule type'), + schedule_value: z + .string() + .optional() + .describe('New schedule value (see schedule_task for format)'), + script: z + .string() + .optional() + .describe( + 'New script for the task. Set to empty string to remove the script.', + ), }, async (args) => { // Validate schedule_value if provided - if (args.schedule_type === 'cron' || (!args.schedule_type && args.schedule_value)) { + if ( + args.schedule_type === 'cron' || + (!args.schedule_type && args.schedule_value) + ) { if (args.schedule_value) { try { CronExpressionParser.parse(args.schedule_value); } catch { return { - content: [{ type: 'text' as const, text: `Invalid cron: "${args.schedule_value}".` }], + content: [ + { + type: 'text' as const, + text: `Invalid cron: "${args.schedule_value}".`, + }, + ], isError: true, }; } @@ -277,7 +404,12 @@ server.tool( const ms = parseInt(args.schedule_value, 10); if (isNaN(ms) || ms <= 0) { return { - content: [{ type: 'text' as const, text: `Invalid interval: "${args.schedule_value}".` }], + content: [ + { + type: 'text' as const, + text: `Invalid interval: "${args.schedule_value}".`, + }, + ], isError: true, }; } @@ -292,12 +424,21 @@ server.tool( }; if (args.prompt !== undefined) data.prompt = args.prompt; if (args.script !== undefined) data.script = args.script; - if (args.schedule_type !== undefined) data.schedule_type = args.schedule_type; - if (args.schedule_value !== undefined) data.schedule_value = args.schedule_value; + if (args.schedule_type !== undefined) + data.schedule_type = args.schedule_type; + if (args.schedule_value !== undefined) + data.schedule_value = args.schedule_value; writeIpcFile(TASKS_DIR, data); - return { content: [{ type: 'text' as const, text: `Task ${args.task_id} update requested.` }] }; + return { + content: [ + { + type: 'text' as const, + text: `Task ${args.task_id} update requested.`, + }, + ], + }; }, ); @@ -307,15 +448,28 @@ server.tool( Use available_groups.json to find the JID for a group. The folder name must be channel-prefixed: "{channel}_{group-name}" (e.g., "whatsapp_family-chat", "telegram_dev-team", "discord_general"). Use lowercase with hyphens for the group name part.`, { - jid: z.string().describe('The chat JID (e.g., "120363336345536173@g.us", "tg:-1001234567890", "dc:1234567890123456")'), + jid: z + .string() + .describe( + 'The chat JID (e.g., "120363336345536173@g.us", "tg:-1001234567890", "dc:1234567890123456")', + ), name: z.string().describe('Display name for the group'), - folder: z.string().describe('Channel-prefixed folder name (e.g., "whatsapp_family-chat", "telegram_dev-team")'), + folder: z + .string() + .describe( + 'Channel-prefixed folder name (e.g., "whatsapp_family-chat", "telegram_dev-team")', + ), trigger: z.string().describe('Trigger word (e.g., "@Andy")'), }, async (args) => { if (!isMain) { return { - content: [{ type: 'text' as const, text: 'Only the main group can register new groups.' }], + content: [ + { + type: 'text' as const, + text: 'Only the main group can register new groups.', + }, + ], isError: true, }; } @@ -332,7 +486,12 @@ Use available_groups.json to find the JID for a group. The folder name must be c writeIpcFile(TASKS_DIR, data); return { - content: [{ type: 'text' as const, text: `Group "${args.name}" registered. It will start receiving messages immediately.` }], + content: [ + { + type: 'text' as const, + text: `Group "${args.name}" registered. It will start receiving messages immediately.`, + }, + ], }; }, );