style: run prettier on container/agent-runner/src/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-01 21:52:45 +03:00
parent 7b0d79a6f3
commit 87e89147c9
2 changed files with 386 additions and 123 deletions

View File

@@ -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<void>(r => { this.waiting = r; });
await new Promise<void>((r) => {
this.waiting = r;
});
this.waiting = null;
}
}
@@ -100,7 +106,9 @@ async function readStdin(): Promise<string> {
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<string, string | undefined>,
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<ScriptResult | null> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
status: 'error',
result: null,
newSessionId: sessionId,
error: errorMessage
error: errorMessage,
});
process.exit(1);
}

View File

@@ -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<string, string | undefined> = {
@@ -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.`,
},
],
};
},
);