niahere 0.2.49 → 0.2.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/job.ts +7 -5
- package/src/core/scheduler.ts +12 -5
- package/src/db/models/job.ts +37 -2
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +4 -2
package/package.json
CHANGED
package/src/cli/job.ts
CHANGED
|
@@ -144,26 +144,28 @@ export async function jobCommand(): Promise<void> {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
case "update": {
|
|
147
|
-
const
|
|
148
|
-
|
|
147
|
+
const hasAlways = process.argv.includes("--always");
|
|
148
|
+
const hasNoAlways = process.argv.includes("--no-always");
|
|
149
|
+
let cliArgs = process.argv.slice(4).filter((a) => a !== "--always" && a !== "--no-always");
|
|
149
150
|
|
|
150
151
|
const name = cliArgs[0];
|
|
151
152
|
if (!name) {
|
|
152
|
-
console.log('Usage: nia job update <name> [--schedule <schedule>] [--prompt <prompt>] [--always]');
|
|
153
|
+
console.log('Usage: nia job update <name> [--schedule <schedule>] [--prompt <prompt>] [--always] [--no-always]');
|
|
153
154
|
fail('Example: nia job update curator --schedule "4h" --prompt "New prompt"');
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
const scheduleIdx = cliArgs.indexOf("--schedule");
|
|
157
158
|
const schedule = scheduleIdx !== -1 ? cliArgs[scheduleIdx + 1] : undefined;
|
|
158
159
|
const promptIdx = cliArgs.indexOf("--prompt");
|
|
159
|
-
const prompt = promptIdx !== -1 ? cliArgs.slice(promptIdx + 1).filter((a) => a !== "--
|
|
160
|
+
const prompt = promptIdx !== -1 ? cliArgs.slice(promptIdx + 1).filter((a) => a !== "--schedule" && a !== schedule).join(" ") : undefined;
|
|
160
161
|
|
|
161
162
|
try {
|
|
162
163
|
await withDb(async () => {
|
|
163
164
|
const fields: Partial<{ schedule: string; prompt: string; always: boolean }> = {};
|
|
164
165
|
if (schedule) fields.schedule = schedule;
|
|
165
166
|
if (prompt) fields.prompt = prompt;
|
|
166
|
-
if (
|
|
167
|
+
if (hasAlways) fields.always = true;
|
|
168
|
+
if (hasNoAlways) fields.always = false;
|
|
167
169
|
|
|
168
170
|
const updated = await Job.update(name, fields);
|
|
169
171
|
if (!updated) fail(`Job not found: "${name}". Use \`nia job list\` to see available jobs.`);
|
package/src/core/scheduler.ts
CHANGED
|
@@ -79,10 +79,10 @@ async function tick(): Promise<void> {
|
|
|
79
79
|
|
|
80
80
|
for (const job of dueJobs) {
|
|
81
81
|
if (!job.always && !isWithinActiveHours()) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
await Job.markRun(job.name, nextRun).catch(() => {});
|
|
85
|
-
}
|
|
82
|
+
try {
|
|
83
|
+
const nextRun = computeNextRun(job.scheduleType, job.schedule, config.timezone, new Date());
|
|
84
|
+
if (nextRun) await Job.markRun(job.name, nextRun).catch(() => {});
|
|
85
|
+
} catch {}
|
|
86
86
|
log.info({ job: job.name }, "scheduler: skipping — outside active hours");
|
|
87
87
|
continue;
|
|
88
88
|
}
|
|
@@ -95,7 +95,14 @@ async function tick(): Promise<void> {
|
|
|
95
95
|
log.error({ err, job: job.name }, "scheduler: job failed");
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
let nextRun: Date | null = null;
|
|
99
|
+
try {
|
|
100
|
+
nextRun = computeNextRun(job.scheduleType, job.schedule, config.timezone, new Date());
|
|
101
|
+
} catch (err) {
|
|
102
|
+
log.error({ err, job: job.name, schedule: job.schedule }, "scheduler: invalid schedule, disabling job");
|
|
103
|
+
await Job.update(job.name, { enabled: false }).catch(() => {});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
99
106
|
await Job.markRun(job.name, nextRun).catch((err) => {
|
|
100
107
|
log.error({ err, job: job.name }, "scheduler: failed to update next_run_at");
|
|
101
108
|
});
|
package/src/db/models/job.ts
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
import { getSql } from "../connection";
|
|
2
|
+
import { CronExpressionParser } from "cron-parser";
|
|
3
|
+
import { parseDuration } from "../../utils/duration";
|
|
2
4
|
import type { ScheduleType } from "../../types";
|
|
3
5
|
|
|
6
|
+
/** Validate that a schedule string matches its declared type. Throws on mismatch. */
|
|
7
|
+
function validateSchedule(schedule: string, scheduleType: ScheduleType): void {
|
|
8
|
+
switch (scheduleType) {
|
|
9
|
+
case "cron":
|
|
10
|
+
try {
|
|
11
|
+
CronExpressionParser.parse(schedule);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
throw new Error(`Invalid cron expression "${schedule}": ${err instanceof Error ? err.message : err}`);
|
|
14
|
+
}
|
|
15
|
+
break;
|
|
16
|
+
case "interval":
|
|
17
|
+
try {
|
|
18
|
+
parseDuration(schedule);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
throw new Error(`Invalid interval "${schedule}": ${err instanceof Error ? err.message : err}`);
|
|
21
|
+
}
|
|
22
|
+
break;
|
|
23
|
+
case "once": {
|
|
24
|
+
const d = new Date(schedule);
|
|
25
|
+
if (isNaN(d.getTime())) {
|
|
26
|
+
throw new Error(`Invalid timestamp "${schedule}": expected ISO 8601 date`);
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
4
33
|
export interface Job {
|
|
5
34
|
name: string;
|
|
6
35
|
schedule: string;
|
|
@@ -45,6 +74,7 @@ export async function create(
|
|
|
45
74
|
nextRunAt?: Date,
|
|
46
75
|
agent?: string,
|
|
47
76
|
): Promise<void> {
|
|
77
|
+
validateSchedule(schedule, scheduleType);
|
|
48
78
|
const existing = await get(name);
|
|
49
79
|
if (existing) {
|
|
50
80
|
throw new Error(`Job "${name}" already exists. Use \`nia job remove ${name}\` first, or choose a different name.`);
|
|
@@ -71,21 +101,26 @@ export async function get(name: string): Promise<Job | null> {
|
|
|
71
101
|
|
|
72
102
|
export async function update(
|
|
73
103
|
name: string,
|
|
74
|
-
fields: Partial<{ schedule: string; prompt: string; enabled: boolean; always: boolean; agent: string | null }>,
|
|
104
|
+
fields: Partial<{ schedule: string; prompt: string; enabled: boolean; always: boolean; agent: string | null; scheduleType: ScheduleType }>,
|
|
75
105
|
): Promise<boolean> {
|
|
76
106
|
const sql = getSql();
|
|
77
107
|
const existing = await get(name);
|
|
78
108
|
if (!existing) return false;
|
|
79
109
|
|
|
80
110
|
const schedule = fields.schedule ?? existing.schedule;
|
|
111
|
+
const scheduleType = fields.scheduleType ?? existing.scheduleType;
|
|
81
112
|
const prompt = fields.prompt ?? existing.prompt;
|
|
82
113
|
const enabled = fields.enabled ?? existing.enabled;
|
|
83
114
|
const always = fields.always ?? existing.always;
|
|
84
115
|
const agent = fields.agent !== undefined ? fields.agent : existing.agent;
|
|
85
116
|
|
|
117
|
+
if (fields.schedule || fields.scheduleType) {
|
|
118
|
+
validateSchedule(schedule, scheduleType);
|
|
119
|
+
}
|
|
120
|
+
|
|
86
121
|
await sql`
|
|
87
122
|
UPDATE jobs
|
|
88
|
-
SET schedule = ${schedule}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, updated_at = NOW()
|
|
123
|
+
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, updated_at = NOW()
|
|
89
124
|
WHERE name = ${name}
|
|
90
125
|
`;
|
|
91
126
|
await notifyChange();
|
package/src/mcp/server.ts
CHANGED
|
@@ -32,13 +32,14 @@ export function createNiaMcpServer() {
|
|
|
32
32
|
),
|
|
33
33
|
tool(
|
|
34
34
|
"update_job",
|
|
35
|
-
"Update an existing job's schedule, prompt, always flag, or
|
|
35
|
+
"Update an existing job's schedule, prompt, always flag, agent, or schedule_type. Only pass fields you want to change.",
|
|
36
36
|
{
|
|
37
37
|
name: z.string().describe("Job name to update"),
|
|
38
38
|
schedule: z.string().optional().describe("New schedule (cron expression, interval duration, or ISO timestamp)"),
|
|
39
39
|
prompt: z.string().optional().describe("New prompt"),
|
|
40
40
|
always: z.boolean().optional().describe("If true, runs 24/7 ignoring active hours"),
|
|
41
41
|
agent: z.string().nullable().optional().describe("Agent name (set null to remove agent)"),
|
|
42
|
+
schedule_type: z.enum(["cron", "interval", "once"]).optional().describe("Schedule type (must match the schedule format)"),
|
|
42
43
|
},
|
|
43
44
|
async (args) => ({
|
|
44
45
|
content: [{ type: "text" as const, text: await handlers.updateJob(args) }],
|
package/src/mcp/tools.ts
CHANGED
|
@@ -40,14 +40,16 @@ export async function updateJob(args: {
|
|
|
40
40
|
prompt?: string;
|
|
41
41
|
always?: boolean;
|
|
42
42
|
agent?: string | null;
|
|
43
|
+
schedule_type?: "cron" | "interval" | "once";
|
|
43
44
|
}): Promise<string> {
|
|
44
|
-
const fields: Partial<{ schedule: string; prompt: string; always: boolean; agent: string | null }> = {};
|
|
45
|
+
const fields: Partial<{ schedule: string; prompt: string; always: boolean; agent: string | null; scheduleType: "cron" | "interval" | "once" }> = {};
|
|
45
46
|
if (args.schedule) fields.schedule = args.schedule;
|
|
46
47
|
if (args.prompt) fields.prompt = args.prompt;
|
|
47
48
|
if (args.always !== undefined) fields.always = args.always;
|
|
48
49
|
if (args.agent !== undefined) fields.agent = args.agent;
|
|
50
|
+
if (args.schedule_type) fields.scheduleType = args.schedule_type;
|
|
49
51
|
|
|
50
|
-
if (Object.keys(fields).length === 0) return "Nothing to update. Pass at least one field (schedule, prompt, always, or
|
|
52
|
+
if (Object.keys(fields).length === 0) return "Nothing to update. Pass at least one field (schedule, prompt, always, agent, or schedule_type).";
|
|
51
53
|
|
|
52
54
|
const updated = await Job.update(args.name, fields);
|
|
53
55
|
if (!updated) return `Job "${args.name}" not found.`;
|