* Copyright (C) 2026 Xiaomi Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* This file contains code derived from MimiClaw (https://github.com/memovai/mimiclaw)
* Copyright (c) 2026 Ziboyan Wang, licensed under the MIT License.
* See NOTICE file for the original MIT License terms.
*/
#include "tools/tool_cron.h"
#include "core/message_bus.h"
#include "infra/cron_service.h"
#include "agent_config.h"
#include "cJSON.h"
#include <errno.h>
#include <string.h>
#include <time.h>
static double get_numeric_value(cJSON* item, bool* ok);
static int parse_every_schedule(cJSON* root, cron_job_t* job,
char* output, size_t output_size);
static int parse_at_schedule(cJSON* root, cron_job_t* job,
char* output, size_t output_size);
* JSON number or a string containing digits. Many LLMs emit numeric
* parameters as quoted strings; this helper tolerates both forms. */
static double get_numeric_value(cJSON* item, bool* ok)
{
if (!item) {
*ok = false;
return 0.0;
}
if (cJSON_IsNumber(item)) {
*ok = true;
return item->valuedouble;
}
if (cJSON_IsString(item) && item->valuestring) {
char* end;
double v = strtod(item->valuestring, &end);
if (end != item->valuestring && *end == '\0') {
*ok = true;
return v;
}
}
*ok = false;
return 0.0;
}
static int parse_every_schedule(cJSON* root, cron_job_t* job,
char* output, size_t output_size)
{
bool ok;
double val = get_numeric_value(
cJSON_GetObjectItem(root, "interval_s"), &ok);
if (!ok || val <= 0) {
snprintf(output, output_size,
"Error: 'every' requires positive 'interval_s'");
return ERROR;
}
job->kind = CRON_KIND_EVERY;
job->interval_s = (uint32_t)val;
job->delete_after_run = false;
return OK;
}
static int parse_at_schedule(cJSON* root, cron_job_t* job,
char* output, size_t output_size)
{
bool ok;
double val = get_numeric_value(
cJSON_GetObjectItem(root, "at_epoch"), &ok);
if (!ok || val <= 0) {
snprintf(output, output_size,
"Error: 'at' requires 'at_epoch' "
"(unix timestamp as a number)");
return ERROR;
}
job->kind = CRON_KIND_AT;
job->at_epoch = (int64_t)val;
time_t now = time(NULL);
if (job->at_epoch <= now) {
snprintf(output, output_size,
"Error: at_epoch %lld is in the past (now=%lld)",
(long long)job->at_epoch, (long long)now);
return ERROR;
}
cJSON* delete_j = cJSON_GetObjectItem(root, "delete_after_run");
job->delete_after_run = delete_j ? cJSON_IsTrue(delete_j) : true;
return OK;
}
int tool_cron_add_execute(const char* input_json,
char* output, size_t output_size)
{
cJSON* root = cJSON_Parse(input_json);
if (!root) {
snprintf(output, output_size, "Error: invalid JSON input");
return ERROR;
}
const char* name = cJSON_GetStringValue(
cJSON_GetObjectItem(root, "name"));
const char* schedule_type = cJSON_GetStringValue(
cJSON_GetObjectItem(root, "schedule_type"));
const char* message = cJSON_GetStringValue(
cJSON_GetObjectItem(root, "message"));
if (!message) {
message = cJSON_GetStringValue(
cJSON_GetObjectItem(root, "description"));
}
if (!name || !schedule_type || !message) {
snprintf(output, output_size,
"Error: missing required fields. "
"You MUST provide: name, schedule_type, message. "
"Example: {\"name\":\"meeting_reminder\","
"\"schedule_type\":\"at\","
"\"at_epoch\":1773990900,"
"\"message\":\"该开会了\"}. "
"Call get_current_time first, then compute "
"at_epoch = current_epoch + delay_seconds.");
cJSON_Delete(root);
return ERROR;
}
if (strlen(message) == 0) {
snprintf(output, output_size,
"Error: message must not be empty");
cJSON_Delete(root);
return ERROR;
}
cron_job_t job;
memset(&job, 0, sizeof(job));
strncpy(job.name, name, sizeof(job.name) - 1);
strncpy(job.message, message, sizeof(job.message) - 1);
const char* channel = cJSON_GetStringValue(
cJSON_GetObjectItem(root, "channel"));
const char* chat_id = cJSON_GetStringValue(
cJSON_GetObjectItem(root, "chat_id"));
if (channel) {
strncpy(job.channel, channel, sizeof(job.channel) - 1);
}
if (chat_id) {
strncpy(job.chat_id, chat_id, sizeof(job.chat_id) - 1);
}
const char* action = cJSON_GetStringValue(
cJSON_GetObjectItem(root, "action"));
const char* action_args = cJSON_GetStringValue(
cJSON_GetObjectItem(root, "action_args"));
if (action) {
strncpy(job.action, action, sizeof(job.action) - 1);
}
if (action_args) {
strncpy(job.action_args, action_args, sizeof(job.action_args) - 1);
}
if (strcmp(job.channel, AGENT_CHAN_FEISHU) == 0
&& (job.chat_id[0] == '\0'
|| strcmp(job.chat_id, "cron") == 0)) {
snprintf(output, output_size,
"Error: channel='feishu' requires a valid chat_id");
cJSON_Delete(root);
return ERROR;
}
int ret;
if (strcmp(schedule_type, "every") == 0) {
ret = parse_every_schedule(root, &job, output, output_size);
} else if (strcmp(schedule_type, "at") == 0) {
ret = parse_at_schedule(root, &job, output, output_size);
} else {
snprintf(output, output_size,
"Error: schedule_type must be 'every' or 'at'");
cJSON_Delete(root);
return ERROR;
}
cJSON_Delete(root);
if (ret != OK) {
return ret;
}
int err = cron_add_job(&job);
if (err != OK) {
snprintf(output, output_size,
"Error: failed to add job (%s)", strerror(errno));
return err;
}
if (job.kind == CRON_KIND_EVERY) {
snprintf(output, output_size,
"OK: Added recurring job '%s' (id=%s), "
"runs every %lu seconds. Next run at epoch %lld.",
job.name, job.id,
(unsigned long)job.interval_s,
(long long)job.next_run);
} else {
snprintf(output, output_size,
"OK: Added one-shot job '%s' (id=%s), "
"fires at epoch %lld.%s",
job.name, job.id,
(long long)job.at_epoch,
job.delete_after_run
? " Will be deleted after firing."
: "");
}
syslog(LOG_INFO, "[tool_cron] cron_add: %s\n", output);
return OK;
}
int tool_cron_list_execute(const char* input_json,
char* output, size_t output_size)
{
(void)input_json;
cron_job_t jobs[AGENT_CRON_MAX_JOBS];
int count = cron_list_jobs(jobs, AGENT_CRON_MAX_JOBS);
if (count == 0) {
snprintf(output, output_size, "No cron jobs scheduled.");
return OK;
}
size_t off = 0;
off += snprintf(output + off, output_size - off,
"Scheduled jobs (%d):\n", count);
for (int i = 0; i < count && off < output_size - 1; i++) {
const cron_job_t* j = &jobs[i];
if (j->kind == CRON_KIND_EVERY) {
off += snprintf(output + off, output_size - off,
" %d. [%s] \"%s\" every %lus, %s, "
"next=%lld, last=%lld, ch=%s:%s\n",
i + 1, j->id, j->name,
(unsigned long)j->interval_s,
j->enabled ? "enabled" : "disabled",
(long long)j->next_run,
(long long)j->last_run,
j->channel, j->chat_id);
} else {
off += snprintf(output + off, output_size - off,
" %d. [%s] \"%s\" at %lld, %s, "
"last=%lld, ch=%s:%s%s\n",
i + 1, j->id, j->name,
(long long)j->at_epoch,
j->enabled ? "enabled" : "disabled",
(long long)j->last_run,
j->channel, j->chat_id,
j->delete_after_run ? " (auto-delete)" : "");
}
}
syslog(LOG_INFO, "[tool_cron] cron_list: %d jobs\n", count);
return OK;
}
int tool_cron_remove_execute(const char* input_json,
char* output, size_t output_size)
{
cJSON* root = cJSON_Parse(input_json);
if (!root) {
snprintf(output, output_size, "Error: invalid JSON input");
return ERROR;
}
const char* job_id = cJSON_GetStringValue(
cJSON_GetObjectItem(root, "job_id"));
if (!job_id || strlen(job_id) == 0) {
snprintf(output, output_size, "Error: missing 'job_id' field");
cJSON_Delete(root);
return ERROR;
}
char id_copy[AGENT_CRON_ID_LEN];
memset(id_copy, 0, sizeof(id_copy));
strncpy(id_copy, job_id, sizeof(id_copy) - 1);
cJSON_Delete(root);
int err = cron_remove_job(id_copy);
if (err == OK) {
snprintf(output, output_size,
"OK: Removed cron job %s", id_copy);
} else {
snprintf(output, output_size,
"Error: job '%s' not found", id_copy);
}
syslog(LOG_INFO, "[tool_cron] cron_remove: %s\n", id_copy);
return err;
}