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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user