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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.2.49",
3
+ "version": "0.2.51",
4
4
  "description": "A personal AI assistant daemon — scheduled jobs, chat across Telegram and Slack, persona system, and visual identity.",
5
5
  "type": "module",
6
6
  "scripts": {
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 always = process.argv.includes("--always");
148
- let cliArgs = process.argv.slice(4).filter((a) => a !== "--always");
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 !== "--always" && a !== "--schedule" && a !== schedule).join(" ") : undefined;
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 (always) fields.always = always;
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.`);
@@ -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
- const nextRun = computeNextRun(job.scheduleType, job.schedule, config.timezone, new Date());
83
- if (nextRun) {
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
- const nextRun = computeNextRun(job.scheduleType, job.schedule, config.timezone, new Date());
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
  });
@@ -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 agent. Only pass fields you want to change.",
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 agent).";
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.`;