niahere 0.2.37 → 0.2.38
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/README.md +5 -0
- package/package.json +1 -1
- package/src/channels/slack.ts +8 -2
- package/src/chat/engine.ts +6 -0
- package/src/chat/identity.ts +4 -0
- package/src/cli/agent.ts +47 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/job.ts +14 -3
- package/src/cli/status.ts +5 -1
- package/src/core/agents.ts +94 -0
- package/src/core/daemon.ts +2 -1
- package/src/core/runner.ts +23 -4
- package/src/db/migrations/007_jobs_agent.ts +7 -0
- package/src/db/models/job.ts +12 -8
- package/src/mcp/server.ts +11 -1
- package/src/mcp/tools.ts +19 -4
- package/src/types/agent.ts +7 -0
- package/src/types/index.ts +1 -0
- package/src/types/job.ts +1 -0
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ nia start # starts daemon + registers OS service
|
|
|
35
35
|
- **Terminal chat** — REPL with session resume support
|
|
36
36
|
- **Scheduled jobs** — recurring jobs and crons that run Claude and can message you back
|
|
37
37
|
- **Persona system** — customizable identity, soul, owner profile, and on-demand memory
|
|
38
|
+
- **Agents** — domain specialists (marketer, senior-dev) via Claude Agent SDK subagents
|
|
38
39
|
- **Skills** — loads skills from multiple directories, invokable as slash commands
|
|
39
40
|
- **Cross-platform service** — launchd (macOS), systemd (Linux), service-aware restart
|
|
40
41
|
- **MCP tools** — 18 tools for job management, messaging, memory, and channel control
|
|
@@ -58,12 +59,16 @@ nia version — show version
|
|
|
58
59
|
nia job list — list all jobs
|
|
59
60
|
nia job show [name] — full details + recent runs
|
|
60
61
|
nia job add <n> <s> <p> — add a job (active hours only)
|
|
62
|
+
nia job add <n> <s> <p> --agent <name> — add a job using an agent
|
|
61
63
|
nia job add <n> <s> <p> --always — add a cron (runs 24/7)
|
|
62
64
|
nia job remove <name> — delete a job
|
|
63
65
|
nia job enable / disable <n> — toggle a job
|
|
64
66
|
nia job run <name> — run a job once
|
|
65
67
|
nia job log [name] — show recent run history
|
|
66
68
|
|
|
69
|
+
nia agent list — list available agents
|
|
70
|
+
nia agent show <name> — show agent details and prompt
|
|
71
|
+
|
|
67
72
|
nia db setup — install PostgreSQL + create database + migrate
|
|
68
73
|
nia db migrate — run database migrations
|
|
69
74
|
nia db status — check database connection
|
package/package.json
CHANGED
package/src/channels/slack.ts
CHANGED
|
@@ -120,7 +120,7 @@ class SlackChannel implements Channel {
|
|
|
120
120
|
}
|
|
121
121
|
const queued = state.lock !== Promise.resolve();
|
|
122
122
|
if (queued) log.debug({ key }, "slack: message queued behind active lock");
|
|
123
|
-
state.lock = state.lock.then(fn, fn);
|
|
123
|
+
state.lock = state.lock.then(fn, fn).catch((err) => log.error({ err, key }, "unhandled error in locked handler"));
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
const self = this;
|
|
@@ -479,7 +479,13 @@ class SlackChannel implements Channel {
|
|
|
479
479
|
|
|
480
480
|
log.info({ channel: msg.channel, key, text: text.slice(0, 100), isDm, watched: isWatched, attachments: attachments?.length || 0 }, "slack message received");
|
|
481
481
|
|
|
482
|
-
|
|
482
|
+
let state: ChatState;
|
|
483
|
+
try {
|
|
484
|
+
state = await getState(key);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
log.error({ err, key }, "slack: failed to create chat engine");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
483
489
|
|
|
484
490
|
withLock(key, async () => {
|
|
485
491
|
// Add thinking reaction inside the lock so cleanup is guaranteed
|
package/src/chat/engine.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { join } from "path";
|
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
import { buildSystemPrompt } from "./identity";
|
|
9
|
+
import { getAgentDefinitions } from "../core/agents";
|
|
9
10
|
import { Session, Message, ActiveEngine } from "../db/models";
|
|
10
11
|
import type { Attachment, SendResult, StreamCallback, ActivityCallback, SendCallbacks, ChatEngine, EngineOptions } from "../types";
|
|
11
12
|
import { truncate, formatToolUse } from "../utils/format-activity";
|
|
@@ -177,6 +178,11 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
177
178
|
options.mcpServers = mcpServers;
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
const agentDefs = getAgentDefinitions();
|
|
182
|
+
if (Object.keys(agentDefs).length > 0) {
|
|
183
|
+
options.agents = agentDefs;
|
|
184
|
+
}
|
|
185
|
+
|
|
180
186
|
queryHandle = query({
|
|
181
187
|
prompt: stream as any,
|
|
182
188
|
options: options as any,
|
package/src/chat/identity.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "path";
|
|
|
3
3
|
import { getPaths } from "../utils/paths";
|
|
4
4
|
import { getEnvironmentPrompt, getModePrompt, getChannelPrompt } from "../prompts";
|
|
5
5
|
import { getSkillsSummary } from "../core/skills";
|
|
6
|
+
import { getAgentsSummary } from "../core/agents";
|
|
6
7
|
import type { Mode } from "../types";
|
|
7
8
|
|
|
8
9
|
// Re-export for backwards compat
|
|
@@ -37,5 +38,8 @@ export function buildSystemPrompt(mode: Mode = "chat", channel: string = "termin
|
|
|
37
38
|
const skills = getSkillsSummary();
|
|
38
39
|
if (skills) parts.push(skills);
|
|
39
40
|
|
|
41
|
+
const agents = getAgentsSummary();
|
|
42
|
+
if (agents) parts.push(agents);
|
|
43
|
+
|
|
40
44
|
return parts.join("\n\n");
|
|
41
45
|
}
|
package/src/cli/agent.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { scanAgents } from "../core/agents";
|
|
2
|
+
|
|
3
|
+
export async function agentCommand(): Promise<void> {
|
|
4
|
+
const subcommand = process.argv[3];
|
|
5
|
+
|
|
6
|
+
switch (subcommand) {
|
|
7
|
+
case "list": {
|
|
8
|
+
const agents = scanAgents();
|
|
9
|
+
if (agents.length === 0) {
|
|
10
|
+
console.log("No agents found. Create agents in ~/.niahere/agents/<name>/AGENT.md");
|
|
11
|
+
} else {
|
|
12
|
+
for (const a of agents) {
|
|
13
|
+
const model = a.model ? ` (${a.model})` : "";
|
|
14
|
+
console.log(` ${a.name}${model} [${a.source}]`);
|
|
15
|
+
if (a.description) console.log(` ${a.description}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
case "show": {
|
|
22
|
+
const name = process.argv[4];
|
|
23
|
+
if (!name) {
|
|
24
|
+
console.error("Usage: nia agent show <name>");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const agents = scanAgents();
|
|
28
|
+
const agent = agents.find((a) => a.name === name);
|
|
29
|
+
if (!agent) {
|
|
30
|
+
console.error(`Agent "${name}" not found.`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
console.log(`Name: ${agent.name}`);
|
|
34
|
+
console.log(`Description: ${agent.description}`);
|
|
35
|
+
if (agent.model) console.log(`Model: ${agent.model}`);
|
|
36
|
+
console.log(`Source: ${agent.source}`);
|
|
37
|
+
console.log(`\n--- Prompt ---\n`);
|
|
38
|
+
console.log(agent.body);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
default:
|
|
43
|
+
console.log("Usage: nia agent <list|show>");
|
|
44
|
+
console.log(" list List all available agents");
|
|
45
|
+
console.log(" show <name> Show agent details and prompt");
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { statusCommand } from "./status";
|
|
|
14
14
|
import { sendCommand, telegramCommand, slackCommand } from "./channels";
|
|
15
15
|
import { rulesCommand, memoryCommand } from "./self";
|
|
16
16
|
import { watchCommand } from "./watch";
|
|
17
|
+
import { agentCommand } from "./agent";
|
|
17
18
|
|
|
18
19
|
// Set LOG_LEVEL from config before anything else logs
|
|
19
20
|
try {
|
|
@@ -286,6 +287,11 @@ switch (command) {
|
|
|
286
287
|
break;
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
case "agent": {
|
|
291
|
+
await agentCommand();
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
289
295
|
case "skills": {
|
|
290
296
|
const { scanSkills: loadSkills } = await import("../core/skills");
|
|
291
297
|
const filter = process.argv[3]; // e.g. "project", "nia", "shared", "claude"
|
|
@@ -485,6 +491,7 @@ switch (command) {
|
|
|
485
491
|
console.log(" rules [show|reset] — view or reset rules.md");
|
|
486
492
|
console.log(" memory [show|reset] — view or reset memory.md");
|
|
487
493
|
console.log(" db <sub> — database setup/status/migrate");
|
|
494
|
+
console.log(" agent <sub> — list/show agents");
|
|
488
495
|
console.log(" skills — list available skills");
|
|
489
496
|
console.log(" watch <sub> — manage Slack watch channels");
|
|
490
497
|
console.log(" validate — validate config.yaml");
|
package/src/cli/job.ts
CHANGED
|
@@ -50,7 +50,8 @@ export async function jobCommand(): Promise<void> {
|
|
|
50
50
|
for (const job of jobs) {
|
|
51
51
|
const tag = job.always ? " always" : "";
|
|
52
52
|
const type = job.scheduleType !== "cron" ? ` (${job.scheduleType})` : "";
|
|
53
|
-
|
|
53
|
+
const agentTag = job.agent ? ` [${job.agent}]` : "";
|
|
54
|
+
console.log(` ${job.enabled ? "●" : "○"} ${job.name} ${job.schedule}${type}${tag}${agentTag}`);
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
});
|
|
@@ -75,12 +76,20 @@ export async function jobCommand(): Promise<void> {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
// Parse --agent flag
|
|
80
|
+
let agent: string | undefined;
|
|
81
|
+
const agentIdx = cliArgs.indexOf("--agent");
|
|
82
|
+
if (agentIdx !== -1 && cliArgs[agentIdx + 1]) {
|
|
83
|
+
agent = cliArgs[agentIdx + 1];
|
|
84
|
+
cliArgs.splice(agentIdx, 2);
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
const name = cliArgs[0];
|
|
79
88
|
const schedule = cliArgs[1];
|
|
80
89
|
const prompt = cliArgs.slice(2).join(" ");
|
|
81
90
|
|
|
82
91
|
if (!name || !schedule || !prompt) {
|
|
83
|
-
console.log('Usage: nia job add <name> <schedule> <prompt> [--always] [--type cron|interval|once]');
|
|
92
|
+
console.log('Usage: nia job add <name> <schedule> <prompt> [--always] [--type cron|interval|once] [--agent <name>]');
|
|
84
93
|
fail('Example: nia job add heartbeat "*/10 * * * *" Check system health --always');
|
|
85
94
|
}
|
|
86
95
|
|
|
@@ -93,7 +102,7 @@ export async function jobCommand(): Promise<void> {
|
|
|
93
102
|
const config = getConfig();
|
|
94
103
|
const nextRunAt = computeInitialNextRun(scheduleType, schedule, config.timezone);
|
|
95
104
|
await withDb(async () => {
|
|
96
|
-
await Job.create(name, schedule, prompt, always, scheduleType, nextRunAt);
|
|
105
|
+
await Job.create(name, schedule, prompt, always, scheduleType, nextRunAt, agent);
|
|
97
106
|
console.log(`Job "${name}" added (${scheduleType}: ${schedule}).${always ? " (runs 24/7)" : ""}`);
|
|
98
107
|
});
|
|
99
108
|
} catch (err) {
|
|
@@ -178,6 +187,7 @@ export async function jobCommand(): Promise<void> {
|
|
|
178
187
|
console.log(` schedule: ${job.schedule}`);
|
|
179
188
|
console.log(` enabled: ${job.enabled}`);
|
|
180
189
|
console.log(` type: ${job.always ? "cron (runs 24/7)" : "job (active hours only)"}`);
|
|
190
|
+
if (job.agent) console.log(` agent: ${job.agent}`);
|
|
181
191
|
console.log(` prompt: ${job.prompt}`);
|
|
182
192
|
|
|
183
193
|
const state = readState();
|
|
@@ -307,6 +317,7 @@ export async function jobCommand(): Promise<void> {
|
|
|
307
317
|
console.log(" status [name] — quick status check");
|
|
308
318
|
console.log(" add <name> <schedule> <prompt> — add a job (active hours only)")
|
|
309
319
|
console.log(" --always — run 24/7 regardless of active hours");
|
|
320
|
+
console.log(" --agent <name> — assign an agent to the job");
|
|
310
321
|
console.log(" update <name> [--schedule s] [--prompt p] [--always] — update a job");
|
|
311
322
|
console.log(" remove <name> — delete a job");
|
|
312
323
|
console.log(" enable <name> — enable a job");
|
package/src/cli/status.ts
CHANGED
|
@@ -23,6 +23,7 @@ type JobStatusLine = {
|
|
|
23
23
|
enabled: boolean;
|
|
24
24
|
always: boolean;
|
|
25
25
|
scheduleType: ScheduleType;
|
|
26
|
+
agent: string | null;
|
|
26
27
|
status: JobStateStatus | "never";
|
|
27
28
|
lastRun: string | null;
|
|
28
29
|
nextRunAt: string | null;
|
|
@@ -121,6 +122,7 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
121
122
|
enabled: job.enabled,
|
|
122
123
|
always: job.always,
|
|
123
124
|
scheduleType: job.scheduleType,
|
|
125
|
+
agent: job.agent,
|
|
124
126
|
status: stateInfo?.status ?? (job.lastRunAt ? "ok" : "never"),
|
|
125
127
|
lastRun: safeDate(lastRun)?.toISOString() ?? null,
|
|
126
128
|
nextRunAt: safeDate(job.nextRunAt)?.toISOString() ?? null,
|
|
@@ -142,6 +144,7 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
142
144
|
enabled: false,
|
|
143
145
|
always: false,
|
|
144
146
|
scheduleType: "cron",
|
|
147
|
+
agent: null,
|
|
145
148
|
status: info.status,
|
|
146
149
|
lastRun,
|
|
147
150
|
nextRunAt: null,
|
|
@@ -253,7 +256,8 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
253
256
|
const lastText = lastRun ? formatTimeLine(lastRun, now) : "never";
|
|
254
257
|
const staleText = stale ? " ⚠ stale" : "";
|
|
255
258
|
|
|
256
|
-
|
|
259
|
+
const agentTag = job.agent ? ` [${job.agent}]` : "";
|
|
260
|
+
console.log(` ${job.enabled ? "\u25cf" : "\u25cb"} ${job.name.padEnd(20)} ${job.enabled ? "enabled" : "disabled"}${agentTag}`);
|
|
257
261
|
console.log(` ${statusIcon} ${status} last: ${lastText} next: ${nextText} duration: ${durationText}${staleText}`);
|
|
258
262
|
}
|
|
259
263
|
} else {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
2
|
+
import { join, resolve } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import { getNiaHome } from "../utils/paths";
|
|
6
|
+
import { log } from "../utils/log";
|
|
7
|
+
import type { AgentInfo } from "../types/agent";
|
|
8
|
+
|
|
9
|
+
// niahere project root (resolved from this file's location)
|
|
10
|
+
const PROJECT_ROOT = resolve(import.meta.dir, "../..");
|
|
11
|
+
|
|
12
|
+
function getAgentDirs(): { dir: string; source: string }[] {
|
|
13
|
+
const niaHome = getNiaHome();
|
|
14
|
+
const dirs: { dir: string; source: string }[] = [
|
|
15
|
+
{ dir: join(process.cwd(), "agents"), source: "cwd" },
|
|
16
|
+
{ dir: join(PROJECT_ROOT, "agents"), source: "project" },
|
|
17
|
+
{ dir: join(niaHome, "agents"), source: "nia" },
|
|
18
|
+
{ dir: join(homedir(), ".shared", "agents"), source: "shared" },
|
|
19
|
+
];
|
|
20
|
+
// Deduplicate paths (cwd, project, and nia may overlap)
|
|
21
|
+
const seen = new Set<string>();
|
|
22
|
+
return dirs.filter(({ dir }) => {
|
|
23
|
+
const resolved = resolve(dir);
|
|
24
|
+
if (seen.has(resolved)) return false;
|
|
25
|
+
seen.add(resolved);
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function scanAgents(): AgentInfo[] {
|
|
31
|
+
const agents: AgentInfo[] = [];
|
|
32
|
+
const seen = new Set<string>();
|
|
33
|
+
|
|
34
|
+
for (const { dir, source } of getAgentDirs()) {
|
|
35
|
+
if (!existsSync(dir)) continue;
|
|
36
|
+
|
|
37
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
38
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
39
|
+
|
|
40
|
+
const agentFile = join(dir, entry.name, "AGENT.md");
|
|
41
|
+
if (!existsSync(agentFile)) continue;
|
|
42
|
+
|
|
43
|
+
const content = readFileSync(agentFile, "utf8");
|
|
44
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
45
|
+
if (!fmMatch) continue;
|
|
46
|
+
|
|
47
|
+
let meta: Record<string, unknown> = {};
|
|
48
|
+
try {
|
|
49
|
+
meta = (yaml.load(fmMatch[1]) as Record<string, unknown>) || {};
|
|
50
|
+
} catch (err) {
|
|
51
|
+
log.warn({ err, agent: entry.name, path: agentFile }, "failed to parse agent metadata, skipping");
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const name = (typeof meta.name === "string" ? meta.name : "") || entry.name;
|
|
55
|
+
|
|
56
|
+
if (seen.has(name)) continue;
|
|
57
|
+
seen.add(name);
|
|
58
|
+
|
|
59
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
|
|
60
|
+
|
|
61
|
+
agents.push({
|
|
62
|
+
name,
|
|
63
|
+
description: typeof meta.description === "string" ? meta.description : "",
|
|
64
|
+
body,
|
|
65
|
+
model: typeof meta.model === "string" ? meta.model : undefined,
|
|
66
|
+
source,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return agents;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getAgentsSummary(): string {
|
|
75
|
+
const agents = scanAgents();
|
|
76
|
+
if (agents.length === 0) return "";
|
|
77
|
+
const lines = agents.map((a) => a.description ? `- @${a.name}: ${a.description}` : `- @${a.name}`);
|
|
78
|
+
return `Available agents:\n${lines.join("\n")}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getAgentDefinitions(): Record<string, { description: string; prompt: string; model?: string }> {
|
|
82
|
+
const agents = scanAgents();
|
|
83
|
+
const defs: Record<string, { description: string; prompt: string; model?: string }> = {};
|
|
84
|
+
|
|
85
|
+
for (const agent of agents) {
|
|
86
|
+
defs[agent.name] = {
|
|
87
|
+
description: agent.description,
|
|
88
|
+
prompt: agent.body,
|
|
89
|
+
...(agent.model ? { model: agent.model } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return defs;
|
|
94
|
+
}
|
package/src/core/daemon.ts
CHANGED
|
@@ -171,7 +171,8 @@ export async function runDaemon(): Promise<void> {
|
|
|
171
171
|
process.exit(1);
|
|
172
172
|
});
|
|
173
173
|
process.on("unhandledRejection", (reason) => {
|
|
174
|
-
|
|
174
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
175
|
+
log.fatal({ err }, "unhandled rejection — cleaning up");
|
|
175
176
|
removePid();
|
|
176
177
|
process.exit(1);
|
|
177
178
|
});
|
package/src/core/runner.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { appendAudit, readState, writeState } from "../utils/logger";
|
|
|
7
7
|
import type { AuditEntry, JobState } from "../types";
|
|
8
8
|
import { getConfig } from "../utils/config";
|
|
9
9
|
import { buildSystemPrompt } from "../chat/identity";
|
|
10
|
+
import { scanAgents } from "./agents";
|
|
10
11
|
import { truncate, formatToolUse } from "../utils/format-activity";
|
|
11
12
|
|
|
12
13
|
export type ActivityCallback = (line: string) => void;
|
|
@@ -195,12 +196,30 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
|
|
|
195
196
|
const cwd = homedir();
|
|
196
197
|
let output: RunnerOutput;
|
|
197
198
|
|
|
199
|
+
// Resolve system prompt: use agent body if job references an agent, else default
|
|
200
|
+
let systemPrompt: string;
|
|
201
|
+
let agentModel: string | undefined;
|
|
202
|
+
if (job.agent) {
|
|
203
|
+
const agents = scanAgents();
|
|
204
|
+
const agentDef = agents.find((a) => a.name === job.agent);
|
|
205
|
+
if (agentDef) {
|
|
206
|
+
systemPrompt = agentDef.body;
|
|
207
|
+
agentModel = agentDef.model;
|
|
208
|
+
} else {
|
|
209
|
+
systemPrompt = buildSystemPrompt("job");
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
systemPrompt = buildSystemPrompt("job");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const jobPrompt = job.prompt
|
|
216
|
+
? `Job: ${job.name} (schedule: ${job.schedule})\n\n${job.prompt}`
|
|
217
|
+
: `Job: ${job.name} (schedule: ${job.schedule})\n\nExecute your scheduled tasks.`;
|
|
218
|
+
|
|
198
219
|
if (config.runner === "codex") {
|
|
199
|
-
const fullPrompt = `${
|
|
200
|
-
output = await runJobWithCodex(fullPrompt, cwd, config.model);
|
|
220
|
+
const fullPrompt = `${systemPrompt}\n\n---\n\n${jobPrompt}`;
|
|
221
|
+
output = await runJobWithCodex(fullPrompt, cwd, agentModel || config.model);
|
|
201
222
|
} else {
|
|
202
|
-
const systemPrompt = buildSystemPrompt("job");
|
|
203
|
-
const jobPrompt = `Job: ${job.name} (schedule: ${job.schedule})\n\n${job.prompt}`;
|
|
204
223
|
output = await runJobWithClaude(systemPrompt, jobPrompt, cwd, onActivity);
|
|
205
224
|
}
|
|
206
225
|
|
package/src/db/models/job.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface Job {
|
|
|
8
8
|
enabled: boolean;
|
|
9
9
|
always: boolean;
|
|
10
10
|
scheduleType: ScheduleType;
|
|
11
|
+
agent: string | null;
|
|
11
12
|
nextRunAt: string | null;
|
|
12
13
|
lastRunAt: string | null;
|
|
13
14
|
createdAt: string;
|
|
@@ -22,6 +23,7 @@ function toJob(r: Record<string, any>): Job {
|
|
|
22
23
|
enabled: r.enabled,
|
|
23
24
|
always: r.always ?? false,
|
|
24
25
|
scheduleType: r.schedule_type || "cron",
|
|
26
|
+
agent: r.agent || null,
|
|
25
27
|
nextRunAt: r.next_run_at ? String(r.next_run_at) : null,
|
|
26
28
|
lastRunAt: r.last_run_at ? String(r.last_run_at) : null,
|
|
27
29
|
createdAt: String(r.created_at),
|
|
@@ -41,6 +43,7 @@ export async function create(
|
|
|
41
43
|
always = false,
|
|
42
44
|
scheduleType: ScheduleType = "cron",
|
|
43
45
|
nextRunAt?: Date,
|
|
46
|
+
agent?: string,
|
|
44
47
|
): Promise<void> {
|
|
45
48
|
const existing = await get(name);
|
|
46
49
|
if (existing) {
|
|
@@ -48,27 +51,27 @@ export async function create(
|
|
|
48
51
|
}
|
|
49
52
|
const sql = getSql();
|
|
50
53
|
await sql`
|
|
51
|
-
INSERT INTO jobs (name, schedule, prompt, always, schedule_type, next_run_at)
|
|
52
|
-
VALUES (${name}, ${schedule}, ${prompt}, ${always}, ${scheduleType}, ${nextRunAt ?? null})
|
|
54
|
+
INSERT INTO jobs (name, schedule, prompt, always, schedule_type, next_run_at, agent)
|
|
55
|
+
VALUES (${name}, ${schedule}, ${prompt}, ${always}, ${scheduleType}, ${nextRunAt ?? null}, ${agent ?? null})
|
|
53
56
|
`;
|
|
54
57
|
await notifyChange();
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export async function list(): Promise<Job[]> {
|
|
58
61
|
const sql = getSql();
|
|
59
|
-
const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, next_run_at, last_run_at, created_at, updated_at FROM jobs ORDER BY name`;
|
|
62
|
+
const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, next_run_at, last_run_at, created_at, updated_at FROM jobs ORDER BY name`;
|
|
60
63
|
return rows.map(toJob);
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
export async function get(name: string): Promise<Job | null> {
|
|
64
67
|
const sql = getSql();
|
|
65
|
-
const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE name = ${name}`;
|
|
68
|
+
const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE name = ${name}`;
|
|
66
69
|
return rows.length > 0 ? toJob(rows[0]) : null;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
export async function update(
|
|
70
73
|
name: string,
|
|
71
|
-
fields: Partial<{ schedule: string; prompt: string; enabled: boolean; always: boolean }>,
|
|
74
|
+
fields: Partial<{ schedule: string; prompt: string; enabled: boolean; always: boolean; agent: string | null }>,
|
|
72
75
|
): Promise<boolean> {
|
|
73
76
|
const sql = getSql();
|
|
74
77
|
const existing = await get(name);
|
|
@@ -78,10 +81,11 @@ export async function update(
|
|
|
78
81
|
const prompt = fields.prompt ?? existing.prompt;
|
|
79
82
|
const enabled = fields.enabled ?? existing.enabled;
|
|
80
83
|
const always = fields.always ?? existing.always;
|
|
84
|
+
const agent = fields.agent !== undefined ? fields.agent : existing.agent;
|
|
81
85
|
|
|
82
86
|
await sql`
|
|
83
87
|
UPDATE jobs
|
|
84
|
-
SET schedule = ${schedule}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, updated_at = NOW()
|
|
88
|
+
SET schedule = ${schedule}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, updated_at = NOW()
|
|
85
89
|
WHERE name = ${name}
|
|
86
90
|
`;
|
|
87
91
|
await notifyChange();
|
|
@@ -97,14 +101,14 @@ export async function remove(name: string): Promise<boolean> {
|
|
|
97
101
|
|
|
98
102
|
export async function listEnabled(): Promise<Job[]> {
|
|
99
103
|
const sql = getSql();
|
|
100
|
-
const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE enabled = TRUE ORDER BY name`;
|
|
104
|
+
const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE enabled = TRUE ORDER BY name`;
|
|
101
105
|
return rows.map(toJob);
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
export async function listDue(): Promise<Job[]> {
|
|
105
109
|
const sql = getSql();
|
|
106
110
|
const rows = await sql`
|
|
107
|
-
SELECT name, schedule, prompt, enabled, always, schedule_type, next_run_at, last_run_at, created_at, updated_at
|
|
111
|
+
SELECT name, schedule, prompt, enabled, always, schedule_type, agent, next_run_at, last_run_at, created_at, updated_at
|
|
108
112
|
FROM jobs
|
|
109
113
|
WHERE enabled = TRUE AND next_run_at <= NOW()
|
|
110
114
|
ORDER BY next_run_at
|
package/src/mcp/server.ts
CHANGED
|
@@ -24,6 +24,7 @@ export function createNiaMcpServer() {
|
|
|
24
24
|
prompt: z.string().describe("What the job should do"),
|
|
25
25
|
schedule_type: z.enum(["cron", "interval", "once"]).default("cron").describe("Schedule type"),
|
|
26
26
|
always: z.boolean().default(false).describe("If true, runs 24/7 ignoring active hours"),
|
|
27
|
+
agent: z.string().optional().describe("Agent name to use for this job (loads agent's AGENT.md as system prompt)"),
|
|
27
28
|
},
|
|
28
29
|
async (args) => ({
|
|
29
30
|
content: [{ type: "text" as const, text: await handlers.addJob(args) }],
|
|
@@ -31,12 +32,13 @@ export function createNiaMcpServer() {
|
|
|
31
32
|
),
|
|
32
33
|
tool(
|
|
33
34
|
"update_job",
|
|
34
|
-
"Update an existing job's schedule, prompt,
|
|
35
|
+
"Update an existing job's schedule, prompt, always flag, or agent. Only pass fields you want to change.",
|
|
35
36
|
{
|
|
36
37
|
name: z.string().describe("Job name to update"),
|
|
37
38
|
schedule: z.string().optional().describe("New schedule (cron expression, interval duration, or ISO timestamp)"),
|
|
38
39
|
prompt: z.string().optional().describe("New prompt"),
|
|
39
40
|
always: z.boolean().optional().describe("If true, runs 24/7 ignoring active hours"),
|
|
41
|
+
agent: z.string().nullable().optional().describe("Agent name (set null to remove agent)"),
|
|
40
42
|
},
|
|
41
43
|
async (args) => ({
|
|
42
44
|
content: [{ type: "text" as const, text: await handlers.updateJob(args) }],
|
|
@@ -166,6 +168,14 @@ export function createNiaMcpServer() {
|
|
|
166
168
|
content: [{ type: "text" as const, text: handlers.addMemory(args.entry) }],
|
|
167
169
|
}),
|
|
168
170
|
),
|
|
171
|
+
tool(
|
|
172
|
+
"list_agents",
|
|
173
|
+
"List all available agents. Agents are role/domain specialists that can be delegated to via the Agent tool or referenced by jobs.",
|
|
174
|
+
{},
|
|
175
|
+
async () => ({
|
|
176
|
+
content: [{ type: "text" as const, text: handlers.listAgents() }],
|
|
177
|
+
}),
|
|
178
|
+
),
|
|
169
179
|
],
|
|
170
180
|
});
|
|
171
181
|
}
|
package/src/mcp/tools.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { getPaths } from "../utils/paths";
|
|
|
8
8
|
import { getChannel } from "../channels/registry";
|
|
9
9
|
import { log } from "../utils/log";
|
|
10
10
|
import { classifyMime } from "../utils/attachment";
|
|
11
|
+
import { scanAgents } from "../core/agents";
|
|
11
12
|
|
|
12
13
|
export async function listJobs(): Promise<string> {
|
|
13
14
|
const jobs = await Job.list();
|
|
@@ -21,14 +22,16 @@ export async function addJob(args: {
|
|
|
21
22
|
prompt: string;
|
|
22
23
|
schedule_type?: ScheduleType;
|
|
23
24
|
always?: boolean;
|
|
25
|
+
agent?: string;
|
|
24
26
|
}): Promise<string> {
|
|
25
27
|
const scheduleType = args.schedule_type || "cron";
|
|
26
28
|
const always = args.always || false;
|
|
27
29
|
const config = getConfig();
|
|
28
30
|
|
|
29
31
|
const nextRunAt = computeInitialNextRun(scheduleType, args.schedule, config.timezone);
|
|
30
|
-
await Job.create(args.name, args.schedule, args.prompt, always, scheduleType, nextRunAt);
|
|
31
|
-
|
|
32
|
+
await Job.create(args.name, args.schedule, args.prompt, always, scheduleType, nextRunAt, args.agent);
|
|
33
|
+
const agentNote = args.agent ? ` [agent: ${args.agent}]` : "";
|
|
34
|
+
return `Job "${args.name}" created (${scheduleType}: ${args.schedule})${agentNote}. Next run: ${nextRunAt.toISOString()}`;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export async function updateJob(args: {
|
|
@@ -36,13 +39,15 @@ export async function updateJob(args: {
|
|
|
36
39
|
schedule?: string;
|
|
37
40
|
prompt?: string;
|
|
38
41
|
always?: boolean;
|
|
42
|
+
agent?: string | null;
|
|
39
43
|
}): Promise<string> {
|
|
40
|
-
const fields: Partial<{ schedule: string; prompt: string; always: boolean }> = {};
|
|
44
|
+
const fields: Partial<{ schedule: string; prompt: string; always: boolean; agent: string | null }> = {};
|
|
41
45
|
if (args.schedule) fields.schedule = args.schedule;
|
|
42
46
|
if (args.prompt) fields.prompt = args.prompt;
|
|
43
47
|
if (args.always !== undefined) fields.always = args.always;
|
|
48
|
+
if (args.agent !== undefined) fields.agent = args.agent;
|
|
44
49
|
|
|
45
|
-
if (Object.keys(fields).length === 0) return "Nothing to update. Pass at least one field (schedule, prompt, or
|
|
50
|
+
if (Object.keys(fields).length === 0) return "Nothing to update. Pass at least one field (schedule, prompt, always, or agent).";
|
|
46
51
|
|
|
47
52
|
const updated = await Job.update(args.name, fields);
|
|
48
53
|
if (!updated) return `Job "${args.name}" not found.`;
|
|
@@ -327,3 +332,13 @@ export function addMemory(entry: string): string {
|
|
|
327
332
|
}
|
|
328
333
|
return `Memory saved.`;
|
|
329
334
|
}
|
|
335
|
+
|
|
336
|
+
export function listAgents(): string {
|
|
337
|
+
const agents = scanAgents();
|
|
338
|
+
if (agents.length === 0) return "No agents found.";
|
|
339
|
+
return JSON.stringify(
|
|
340
|
+
agents.map((a) => ({ name: a.name, description: a.description, model: a.model, source: a.source })),
|
|
341
|
+
null,
|
|
342
|
+
2,
|
|
343
|
+
);
|
|
344
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -8,3 +8,4 @@ export type { ChatState } from "./chat-state";
|
|
|
8
8
|
export type { Config, ChannelsConfig, TelegramConfig, SlackConfig } from "./config";
|
|
9
9
|
export type { Paths } from "./paths";
|
|
10
10
|
export type { SaveMessageParams, RoomStats, RecentMessage } from "./message";
|
|
11
|
+
export type { AgentInfo } from "./agent";
|