niahere 0.2.37 → 0.2.40
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/commands/service.ts +4 -2
- package/src/core/agents.ts +94 -0
- package/src/core/alive.ts +81 -2
- package/src/core/daemon.ts +11 -7
- package/src/core/runner.ts +31 -8
- 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 {
|
package/src/commands/service.ts
CHANGED
|
@@ -48,6 +48,8 @@ function buildPlist(): string {
|
|
|
48
48
|
<key>SuccessfulExit</key>
|
|
49
49
|
<false/>
|
|
50
50
|
</dict>
|
|
51
|
+
<key>ThrottleInterval</key>
|
|
52
|
+
<integer>10</integer>
|
|
51
53
|
<key>StandardOutPath</key>
|
|
52
54
|
<string>${paths.daemonLog}</string>
|
|
53
55
|
<key>StandardErrorPath</key>
|
|
@@ -86,10 +88,10 @@ async function uninstallLaunchd(): Promise<void> {
|
|
|
86
88
|
const path = plistPath();
|
|
87
89
|
if (!existsSync(path)) return;
|
|
88
90
|
|
|
91
|
+
// Unload to stop the process and disable KeepAlive respawn.
|
|
92
|
+
// Keep the plist file so RunAtLoad starts the daemon on next login.
|
|
89
93
|
const unload = Bun.spawn(["launchctl", "unload", path], { stdout: "pipe", stderr: "pipe" });
|
|
90
94
|
await unload.exited;
|
|
91
|
-
|
|
92
|
-
try { unlinkSync(path); } catch { /* already gone */ }
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
function isLaunchdInstalled(): boolean {
|
|
@@ -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/alive.ts
CHANGED
|
@@ -4,11 +4,72 @@ import { getSql, closeDb } from "../db/connection";
|
|
|
4
4
|
import { getFailures, type Check } from "./health";
|
|
5
5
|
|
|
6
6
|
const HEARTBEAT_INTERVAL = 60_000; // 60s
|
|
7
|
+
const PG_DATA_DIRS = [
|
|
8
|
+
"/opt/homebrew/var/postgresql@18",
|
|
9
|
+
"/opt/homebrew/var/postgresql@17",
|
|
10
|
+
"/opt/homebrew/var/postgres",
|
|
11
|
+
];
|
|
7
12
|
|
|
8
13
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
9
14
|
let lastFailures: string[] = [];
|
|
10
15
|
let recoveryAttempted = false;
|
|
11
16
|
|
|
17
|
+
/** Deterministic Postgres recovery: remove stale PID file + restart service. */
|
|
18
|
+
async function recoverPostgres(): Promise<boolean> {
|
|
19
|
+
const ready = Bun.spawnSync(["pg_isready"]);
|
|
20
|
+
if (ready.exitCode === 0) return true; // already up
|
|
21
|
+
|
|
22
|
+
log.info("alive: postgres not ready, attempting deterministic recovery");
|
|
23
|
+
|
|
24
|
+
// Find and remove stale postmaster.pid
|
|
25
|
+
const { existsSync, unlinkSync, readFileSync } = await import("fs");
|
|
26
|
+
for (const dir of PG_DATA_DIRS) {
|
|
27
|
+
const pidFile = `${dir}/postmaster.pid`;
|
|
28
|
+
if (!existsSync(pidFile)) continue;
|
|
29
|
+
|
|
30
|
+
// Read the PID from line 1 and check if it's actually a postgres process
|
|
31
|
+
try {
|
|
32
|
+
const pid = parseInt(readFileSync(pidFile, "utf8").split("\n")[0], 10);
|
|
33
|
+
if (!isNaN(pid)) {
|
|
34
|
+
const check = Bun.spawnSync(["ps", "-p", String(pid), "-o", "comm="]);
|
|
35
|
+
const comm = new TextDecoder().decode(check.stdout).trim();
|
|
36
|
+
if (check.exitCode !== 0 || !comm.includes("postgres")) {
|
|
37
|
+
log.info({ pidFile, stalePid: pid, actualProcess: comm || "dead" }, "alive: removing stale postmaster.pid");
|
|
38
|
+
unlinkSync(pidFile);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
log.warn({ err, pidFile }, "alive: could not inspect postmaster.pid");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Restart the service
|
|
47
|
+
if (process.platform === "darwin") {
|
|
48
|
+
// Try common brew postgresql service names
|
|
49
|
+
for (const svc of ["postgresql@18", "postgresql@17", "postgresql"]) {
|
|
50
|
+
const result = Bun.spawnSync(["brew", "services", "start", svc]);
|
|
51
|
+
if (result.exitCode === 0) {
|
|
52
|
+
log.info({ service: svc }, "alive: brew service start issued");
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
Bun.spawnSync(["systemctl", "start", "postgresql"]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Wait briefly for postgres to come up
|
|
61
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
62
|
+
|
|
63
|
+
const check = Bun.spawnSync(["pg_isready"]);
|
|
64
|
+
if (check.exitCode === 0) {
|
|
65
|
+
log.info("alive: postgres recovered via deterministic fix");
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
log.warn("alive: deterministic postgres recovery failed");
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
12
73
|
async function attemptDbReconnect(): Promise<boolean> {
|
|
13
74
|
try {
|
|
14
75
|
await closeDb();
|
|
@@ -138,10 +199,28 @@ async function heartbeat(): Promise<void> {
|
|
|
138
199
|
}
|
|
139
200
|
}
|
|
140
201
|
|
|
141
|
-
//
|
|
202
|
+
// Deterministic postgres recovery before LLM agent
|
|
203
|
+
if (failureNames.includes("database") && !recoveryAttempted) {
|
|
204
|
+
const pgFixed = await recoverPostgres();
|
|
205
|
+
if (pgFixed) {
|
|
206
|
+
const reconnected = await attemptDbReconnect();
|
|
207
|
+
if (reconnected) {
|
|
208
|
+
const remaining = await getFailures();
|
|
209
|
+
if (remaining.length === 0) {
|
|
210
|
+
log.info("alive: postgres recovered (deterministic fix, no LLM needed)");
|
|
211
|
+
await notifyUser("Postgres was down (stale PID). Fixed automatically — no LLM agent needed.");
|
|
212
|
+
lastFailures = [];
|
|
213
|
+
recoveryAttempted = false;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Run LLM recovery agent once per outage (fallback for non-trivial issues)
|
|
142
221
|
if (!recoveryAttempted) {
|
|
143
222
|
recoveryAttempted = true;
|
|
144
|
-
log.info({ failures: failureNames }, "alive: running recovery agent");
|
|
223
|
+
log.info({ failures: failureNames }, "alive: running LLM recovery agent");
|
|
145
224
|
|
|
146
225
|
const { recovered, report } = await runRecoveryAgent(failures);
|
|
147
226
|
|
package/src/core/daemon.ts
CHANGED
|
@@ -121,7 +121,7 @@ function waitForExit(timeoutMs: number): void {
|
|
|
121
121
|
/** Return PIDs of running daemon processes (excluding ourselves). */
|
|
122
122
|
export function findDaemonPids(): number[] {
|
|
123
123
|
try {
|
|
124
|
-
const result = Bun.spawnSync(["pgrep", "-f", "
|
|
124
|
+
const result = Bun.spawnSync(["pgrep", "-f", "src/cli\\.ts run$"]);
|
|
125
125
|
const stdout = new TextDecoder().decode(result.stdout).trim();
|
|
126
126
|
if (!stdout) return [];
|
|
127
127
|
return stdout.split("\n")
|
|
@@ -150,16 +150,19 @@ export async function runDaemon(): Promise<void> {
|
|
|
150
150
|
delete process.env.CLAUDE_CODE_ENTRYPOINT;
|
|
151
151
|
delete process.env.CLAUDE_AGENT_SDK_VERSION;
|
|
152
152
|
|
|
153
|
-
// Startup guard: if another daemon is alive, exit immediately
|
|
153
|
+
// Startup guard: if another nia daemon is alive, exit immediately.
|
|
154
|
+
// Use pgrep (via findDaemonPids) instead of kill(pid,0) to verify the
|
|
155
|
+
// PID is actually a nia process — not a recycled OS PID from something else.
|
|
154
156
|
const existingPid = readPid();
|
|
155
157
|
if (existingPid !== null && existingPid !== process.pid) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
const aliveDaemons = findDaemonPids();
|
|
159
|
+
if (aliveDaemons.includes(existingPid)) {
|
|
158
160
|
log.debug({ existingPid, myPid: process.pid }, "another daemon is already running, exiting");
|
|
159
161
|
process.exit(0);
|
|
160
|
-
} catch {
|
|
161
|
-
// Dead PID in pidfile — safe to take over
|
|
162
162
|
}
|
|
163
|
+
// PID in file is stale (dead or recycled by OS) — safe to take over
|
|
164
|
+
log.warn({ stalePid: existingPid }, "taking over from stale pid");
|
|
165
|
+
removePid();
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
// Crash handlers — ensure PID cleanup and logging on unhandled errors.
|
|
@@ -171,7 +174,8 @@ export async function runDaemon(): Promise<void> {
|
|
|
171
174
|
process.exit(1);
|
|
172
175
|
});
|
|
173
176
|
process.on("unhandledRejection", (reason) => {
|
|
174
|
-
|
|
177
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
178
|
+
log.fatal({ err }, "unhandled rejection — cleaning up");
|
|
175
179
|
removePid();
|
|
176
180
|
process.exit(1);
|
|
177
181
|
});
|
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
|
|
|
@@ -228,13 +247,15 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
|
|
|
228
247
|
};
|
|
229
248
|
appendAudit(auditEntry);
|
|
230
249
|
|
|
231
|
-
state
|
|
250
|
+
// Re-read state to avoid clobbering concurrent job updates
|
|
251
|
+
const freshState = { ...readState() };
|
|
252
|
+
freshState[job.name] = {
|
|
232
253
|
lastRun: timestamp,
|
|
233
254
|
status: result.status,
|
|
234
255
|
duration_ms: result.duration_ms,
|
|
235
256
|
error: result.error,
|
|
236
257
|
};
|
|
237
|
-
writeState(
|
|
258
|
+
writeState(freshState);
|
|
238
259
|
|
|
239
260
|
return result;
|
|
240
261
|
} catch (err) {
|
|
@@ -259,13 +280,15 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
|
|
|
259
280
|
error: errorMsg,
|
|
260
281
|
});
|
|
261
282
|
|
|
262
|
-
state
|
|
283
|
+
// Re-read state to avoid clobbering concurrent job updates
|
|
284
|
+
const freshState = { ...readState() };
|
|
285
|
+
freshState[job.name] = {
|
|
263
286
|
lastRun: timestamp,
|
|
264
287
|
status: "error",
|
|
265
288
|
duration_ms,
|
|
266
289
|
error: errorMsg,
|
|
267
290
|
};
|
|
268
|
-
writeState(
|
|
291
|
+
writeState(freshState);
|
|
269
292
|
|
|
270
293
|
return result;
|
|
271
294
|
}
|
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";
|