niahere 0.2.60 → 0.2.61
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/channels/slack.ts +89 -38
- package/src/chat/engine.ts +20 -53
- package/src/chat/repl.ts +4 -3
- package/src/cli/watch.ts +15 -10
- package/src/commands/init.ts +50 -23
- package/src/core/daemon.ts +49 -4
- package/src/core/finalizer.ts +125 -0
- package/src/db/migrations/014_finalization_requests.ts +21 -0
- package/src/mcp/server.ts +30 -101
- package/src/mcp/tools.ts +23 -69
- package/src/types/config.ts +8 -1
- package/src/types/paths.ts +1 -0
- package/src/utils/config.ts +48 -44
- package/src/utils/paths.ts +1 -0
- package/src/utils/watches.ts +68 -0
package/src/core/daemon.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { startScheduler, stopScheduler, recomputeAllNextRuns } from "./scheduler
|
|
|
12
12
|
import { startAlive, stopAlive } from "./alive";
|
|
13
13
|
import { createNiaMcpServer } from "../mcp/server";
|
|
14
14
|
import { setMcpFactory } from "../mcp";
|
|
15
|
+
import { processPending, cleanupOldRequests } from "./finalizer";
|
|
15
16
|
|
|
16
17
|
export function writePid(pid: number): void {
|
|
17
18
|
const { pid: pidPath } = getPaths();
|
|
@@ -107,7 +108,9 @@ function waitForExit(timeoutMs: number): void {
|
|
|
107
108
|
// Still alive — escalate to SIGKILL
|
|
108
109
|
const remaining = findDaemonPids();
|
|
109
110
|
for (const pid of remaining) {
|
|
110
|
-
try {
|
|
111
|
+
try {
|
|
112
|
+
process.kill(pid, "SIGKILL");
|
|
113
|
+
} catch {}
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
// Brief wait for SIGKILL to take effect
|
|
@@ -124,7 +127,8 @@ export function findDaemonPids(): number[] {
|
|
|
124
127
|
const result = Bun.spawnSync(["pgrep", "-f", "src/cli\\.ts run$"]);
|
|
125
128
|
const stdout = new TextDecoder().decode(result.stdout).trim();
|
|
126
129
|
if (!stdout) return [];
|
|
127
|
-
return stdout
|
|
130
|
+
return stdout
|
|
131
|
+
.split("\n")
|
|
128
132
|
.map((l) => parseInt(l, 10))
|
|
129
133
|
.filter((pid) => !isNaN(pid) && pid !== process.pid);
|
|
130
134
|
} catch {
|
|
@@ -138,7 +142,9 @@ function killAllDaemons(knownPid?: number | null): number {
|
|
|
138
142
|
if (knownPid && knownPid !== process.pid) toKill.add(knownPid);
|
|
139
143
|
|
|
140
144
|
for (const pid of toKill) {
|
|
141
|
-
try {
|
|
145
|
+
try {
|
|
146
|
+
process.kill(pid, "SIGTERM");
|
|
147
|
+
} catch {}
|
|
142
148
|
}
|
|
143
149
|
return toKill.size;
|
|
144
150
|
}
|
|
@@ -189,10 +195,20 @@ export async function runDaemon(): Promise<void> {
|
|
|
189
195
|
const { version } = await import("../../package.json");
|
|
190
196
|
const update = await checkForUpdate(version);
|
|
191
197
|
if (update) {
|
|
192
|
-
log.warn(
|
|
198
|
+
log.warn(
|
|
199
|
+
{ current: update.current, latest: update.latest },
|
|
200
|
+
"update available — run `npm i -g niahere` to update",
|
|
201
|
+
);
|
|
193
202
|
}
|
|
194
203
|
} catch {}
|
|
195
204
|
|
|
205
|
+
// Ensure watches dir exists — used for file-backed watch behaviors and
|
|
206
|
+
// per-watch working memory. Safe to call on every startup.
|
|
207
|
+
try {
|
|
208
|
+
mkdirSync(getPaths().watchesDir, { recursive: true });
|
|
209
|
+
} catch (err) {
|
|
210
|
+
log.warn({ err }, "failed to ensure watches dir");
|
|
211
|
+
}
|
|
196
212
|
|
|
197
213
|
// Startup recovery
|
|
198
214
|
try {
|
|
@@ -260,6 +276,35 @@ export async function runDaemon(): Promise<void> {
|
|
|
260
276
|
log.warn({ err }, "could not subscribe to nia_jobs, falling back to SIGHUP only");
|
|
261
277
|
}
|
|
262
278
|
|
|
279
|
+
// Listen for session finalization requests from CLI processes
|
|
280
|
+
try {
|
|
281
|
+
const sql = getSql();
|
|
282
|
+
await sql.listen("nia_finalize", async () => {
|
|
283
|
+
log.info("finalization request received via NOTIFY, processing pending");
|
|
284
|
+
await processPending().catch((err) => {
|
|
285
|
+
log.warn({ err }, "failed to process pending finalizations on notify");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
log.info("listening for finalization requests on nia_finalize channel");
|
|
289
|
+
} catch (err) {
|
|
290
|
+
log.warn({ err }, "could not subscribe to nia_finalize");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Drain any finalization requests that arrived while daemon was down
|
|
294
|
+
processPending().catch((err) => {
|
|
295
|
+
log.warn({ err }, "startup: failed to drain pending finalizations");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Clean up old finalization requests every 24h
|
|
299
|
+
setInterval(
|
|
300
|
+
() => {
|
|
301
|
+
cleanupOldRequests().catch((err) => {
|
|
302
|
+
log.warn({ err }, "failed to cleanup old finalization requests");
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
24 * 60 * 60 * 1000,
|
|
306
|
+
);
|
|
307
|
+
|
|
263
308
|
// SIGHUP: reload config, reconcile channels, recompute jobs
|
|
264
309
|
process.on("SIGHUP", async () => {
|
|
265
310
|
log.info("received SIGHUP, reloading config");
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified session finalizer — durable queue for post-session work.
|
|
3
|
+
*
|
|
4
|
+
* All callers use finalizeSession() instead of calling consolidator/summarizer
|
|
5
|
+
* directly. The function writes a row to finalization_requests and fires
|
|
6
|
+
* pg_notify('nia_finalize') to wake the daemon. That's it — all processing
|
|
7
|
+
* happens in the daemon via processPending().
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getSql } from "../db/connection";
|
|
11
|
+
import { consolidateSession } from "./consolidator";
|
|
12
|
+
import { summarizeSession } from "./summarizer";
|
|
13
|
+
import { log } from "../utils/log";
|
|
14
|
+
|
|
15
|
+
/** Enqueue a session for finalization. Always returns immediately. */
|
|
16
|
+
export async function finalizeSession(sessionId: string, room: string): Promise<void> {
|
|
17
|
+
const sql = getSql();
|
|
18
|
+
|
|
19
|
+
// Get current message count for idempotency
|
|
20
|
+
const countRows = await sql`
|
|
21
|
+
SELECT COUNT(*)::int AS count FROM messages WHERE session_id = ${sessionId}
|
|
22
|
+
`;
|
|
23
|
+
const messageCount = countRows[0]?.count ?? 0;
|
|
24
|
+
if (messageCount < 2) return;
|
|
25
|
+
|
|
26
|
+
// Cancel any pending request for this session (session resumed or new close)
|
|
27
|
+
await sql`
|
|
28
|
+
DELETE FROM finalization_requests
|
|
29
|
+
WHERE session_id = ${sessionId} AND status = 'pending'
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
// Skip if already done/processing for this exact message count
|
|
33
|
+
const existing = await sql`
|
|
34
|
+
SELECT id FROM finalization_requests
|
|
35
|
+
WHERE session_id = ${sessionId}
|
|
36
|
+
AND message_count = ${messageCount}
|
|
37
|
+
AND status IN ('done', 'processing')
|
|
38
|
+
LIMIT 1
|
|
39
|
+
`;
|
|
40
|
+
if (existing.length > 0) return;
|
|
41
|
+
|
|
42
|
+
// Insert new request
|
|
43
|
+
await sql`
|
|
44
|
+
INSERT INTO finalization_requests (session_id, room, message_count, status)
|
|
45
|
+
VALUES (${sessionId}, ${room}, ${messageCount}, 'pending')
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
// Wake the daemon
|
|
49
|
+
await sql.notify("nia_finalize", sessionId).catch((err) => {
|
|
50
|
+
log.warn({ err, sessionId }, "finalizer: pg_notify failed (daemon may not be running)");
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Cancel pending finalization for a session (e.g. session resumed). */
|
|
55
|
+
export async function cancelPending(sessionId: string): Promise<void> {
|
|
56
|
+
const sql = getSql();
|
|
57
|
+
await sql`
|
|
58
|
+
DELETE FROM finalization_requests
|
|
59
|
+
WHERE session_id = ${sessionId} AND status = 'pending'
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Process a single finalization request. */
|
|
64
|
+
async function processOne(sessionId: string, room: string, messageCount: number): Promise<void> {
|
|
65
|
+
const sql = getSql();
|
|
66
|
+
|
|
67
|
+
// Claim the request (pending -> processing)
|
|
68
|
+
const claimed = await sql`
|
|
69
|
+
UPDATE finalization_requests
|
|
70
|
+
SET status = 'processing', updated_at = NOW()
|
|
71
|
+
WHERE session_id = ${sessionId}
|
|
72
|
+
AND message_count = ${messageCount}
|
|
73
|
+
AND status = 'pending'
|
|
74
|
+
RETURNING id
|
|
75
|
+
`;
|
|
76
|
+
if (claimed.length === 0) return; // Already claimed or cancelled
|
|
77
|
+
|
|
78
|
+
const requestId = claimed[0].id;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await Promise.allSettled([consolidateSession(sessionId, room), summarizeSession(sessionId, room)]);
|
|
82
|
+
|
|
83
|
+
await sql`
|
|
84
|
+
UPDATE finalization_requests
|
|
85
|
+
SET status = 'done', updated_at = NOW()
|
|
86
|
+
WHERE id = ${requestId}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
log.info({ sessionId, room, messageCount }, "finalizer: completed");
|
|
90
|
+
} catch (err) {
|
|
91
|
+
await sql`
|
|
92
|
+
UPDATE finalization_requests
|
|
93
|
+
SET status = 'failed', updated_at = NOW()
|
|
94
|
+
WHERE id = ${requestId}
|
|
95
|
+
`.catch(() => {});
|
|
96
|
+
|
|
97
|
+
log.error({ err, sessionId, room }, "finalizer: processing failed");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Drain all pending finalization requests. Called by daemon on startup and on NOTIFY. */
|
|
102
|
+
export async function processPending(): Promise<void> {
|
|
103
|
+
const sql = getSql();
|
|
104
|
+
|
|
105
|
+
const pending = await sql`
|
|
106
|
+
SELECT session_id, room, message_count
|
|
107
|
+
FROM finalization_requests
|
|
108
|
+
WHERE status = 'pending'
|
|
109
|
+
ORDER BY created_at ASC
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
for (const row of pending) {
|
|
113
|
+
await processOne(row.session_id, row.room, row.message_count);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Clean up old completed/failed requests (> 7 days). Called periodically by daemon. */
|
|
118
|
+
export async function cleanupOldRequests(): Promise<void> {
|
|
119
|
+
const sql = getSql();
|
|
120
|
+
await sql`
|
|
121
|
+
DELETE FROM finalization_requests
|
|
122
|
+
WHERE status IN ('done', 'failed')
|
|
123
|
+
AND updated_at < NOW() - INTERVAL '7 days'
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type postgres from "postgres";
|
|
2
|
+
|
|
3
|
+
export const name = "014_finalization_requests";
|
|
4
|
+
|
|
5
|
+
export async function up(sql: postgres.Sql): Promise<void> {
|
|
6
|
+
await sql`
|
|
7
|
+
CREATE TABLE IF NOT EXISTS finalization_requests (
|
|
8
|
+
id SERIAL PRIMARY KEY,
|
|
9
|
+
session_id TEXT NOT NULL,
|
|
10
|
+
room TEXT NOT NULL,
|
|
11
|
+
message_count INT NOT NULL,
|
|
12
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
13
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
14
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
15
|
+
)
|
|
16
|
+
`;
|
|
17
|
+
await sql`
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_finalization_session
|
|
19
|
+
ON finalization_requests (session_id, status)
|
|
20
|
+
`;
|
|
21
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -7,54 +7,33 @@ export function createNiaMcpServer() {
|
|
|
7
7
|
name: "nia",
|
|
8
8
|
version: "0.1.0",
|
|
9
9
|
tools: [
|
|
10
|
-
tool(
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
{},
|
|
14
|
-
async () => ({
|
|
15
|
-
content: [{ type: "text" as const, text: await handlers.listJobs() }],
|
|
16
|
-
}),
|
|
17
|
-
),
|
|
10
|
+
tool("list_jobs", "List all scheduled jobs with status and next run time", {}, async () => ({
|
|
11
|
+
content: [{ type: "text" as const, text: await handlers.listJobs() }],
|
|
12
|
+
})),
|
|
18
13
|
tool(
|
|
19
14
|
"add_job",
|
|
20
15
|
"Create a new scheduled job. Supports cron expressions (0 9 * * *), interval durations (5m, 2h, 1d), or one-time ISO timestamps.",
|
|
21
16
|
{
|
|
22
17
|
name: z.string().describe("Unique job name"),
|
|
23
|
-
schedule: z
|
|
24
|
-
.string()
|
|
25
|
-
.describe("Cron expression, duration string, or ISO timestamp"),
|
|
18
|
+
schedule: z.string().describe("Cron expression, duration string, or ISO timestamp"),
|
|
26
19
|
prompt: z.string().describe("What the job should do"),
|
|
27
|
-
schedule_type: z
|
|
28
|
-
|
|
29
|
-
.default("cron")
|
|
30
|
-
.describe("Schedule type"),
|
|
31
|
-
always: z
|
|
32
|
-
.boolean()
|
|
33
|
-
.default(false)
|
|
34
|
-
.describe("If true, runs 24/7 ignoring active hours"),
|
|
20
|
+
schedule_type: z.enum(["cron", "interval", "once"]).default("cron").describe("Schedule type"),
|
|
21
|
+
always: z.boolean().default(false).describe("If true, runs 24/7 ignoring active hours"),
|
|
35
22
|
agent: z
|
|
36
23
|
.string()
|
|
37
24
|
.optional()
|
|
38
|
-
.describe(
|
|
39
|
-
"Agent name to use for this job (loads agent's AGENT.md as system prompt)",
|
|
40
|
-
),
|
|
25
|
+
.describe("Agent name to use for this job (loads agent's AGENT.md as system prompt)"),
|
|
41
26
|
stateless: z
|
|
42
27
|
.boolean()
|
|
43
28
|
.default(false)
|
|
44
|
-
.describe(
|
|
45
|
-
"If true, disables working memory (no state.md injection or workspace)",
|
|
46
|
-
),
|
|
29
|
+
.describe("If true, disables working memory (no state.md injection or workspace)"),
|
|
47
30
|
model: z
|
|
48
31
|
.string()
|
|
49
32
|
.optional()
|
|
50
|
-
.describe(
|
|
51
|
-
"Model override for this job (e.g. haiku, sonnet, opus). Overrides agent and global model.",
|
|
52
|
-
),
|
|
33
|
+
.describe("Model override for this job (e.g. haiku, sonnet, opus). Overrides agent and global model."),
|
|
53
34
|
},
|
|
54
35
|
async (args) => ({
|
|
55
|
-
content: [
|
|
56
|
-
{ type: "text" as const, text: await handlers.addJob(args) },
|
|
57
|
-
],
|
|
36
|
+
content: [{ type: "text" as const, text: await handlers.addJob(args) }],
|
|
58
37
|
}),
|
|
59
38
|
),
|
|
60
39
|
tool(
|
|
@@ -65,39 +44,22 @@ export function createNiaMcpServer() {
|
|
|
65
44
|
schedule: z
|
|
66
45
|
.string()
|
|
67
46
|
.optional()
|
|
68
|
-
.describe(
|
|
69
|
-
"New schedule (cron expression, interval duration, or ISO timestamp)",
|
|
70
|
-
),
|
|
47
|
+
.describe("New schedule (cron expression, interval duration, or ISO timestamp)"),
|
|
71
48
|
prompt: z.string().optional().describe("New prompt"),
|
|
72
|
-
always: z
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.describe("If true, runs 24/7 ignoring active hours"),
|
|
76
|
-
agent: z
|
|
77
|
-
.string()
|
|
78
|
-
.nullable()
|
|
79
|
-
.optional()
|
|
80
|
-
.describe("Agent name (set null to remove agent)"),
|
|
81
|
-
model: z
|
|
82
|
-
.string()
|
|
83
|
-
.nullable()
|
|
84
|
-
.optional()
|
|
85
|
-
.describe("Model override (set null to remove and use default)"),
|
|
49
|
+
always: z.boolean().optional().describe("If true, runs 24/7 ignoring active hours"),
|
|
50
|
+
agent: z.string().nullable().optional().describe("Agent name (set null to remove agent)"),
|
|
51
|
+
model: z.string().nullable().optional().describe("Model override (set null to remove and use default)"),
|
|
86
52
|
stateless: z
|
|
87
53
|
.boolean()
|
|
88
54
|
.optional()
|
|
89
|
-
.describe(
|
|
90
|
-
"If true, disables working memory (no state.md injection or workspace)",
|
|
91
|
-
),
|
|
55
|
+
.describe("If true, disables working memory (no state.md injection or workspace)"),
|
|
92
56
|
schedule_type: z
|
|
93
57
|
.enum(["cron", "interval", "once"])
|
|
94
58
|
.optional()
|
|
95
59
|
.describe("Schedule type (must match the schedule format)"),
|
|
96
60
|
},
|
|
97
61
|
async (args) => ({
|
|
98
|
-
content: [
|
|
99
|
-
{ type: "text" as const, text: await handlers.updateJob(args) },
|
|
100
|
-
],
|
|
62
|
+
content: [{ type: "text" as const, text: await handlers.updateJob(args) }],
|
|
101
63
|
}),
|
|
102
64
|
),
|
|
103
65
|
tool(
|
|
@@ -157,26 +119,17 @@ export function createNiaMcpServer() {
|
|
|
157
119
|
"Send a message to the user via configured channel (telegram, slack). Uses default_channel from config if not specified. Can also send a file/image by providing media_path.",
|
|
158
120
|
{
|
|
159
121
|
text: z.string().describe("Message text to send"),
|
|
160
|
-
channel: z
|
|
161
|
-
.string()
|
|
162
|
-
.optional()
|
|
163
|
-
.describe("Channel name (telegram, slack). Omit to use default."),
|
|
122
|
+
channel: z.string().optional().describe("Channel name (telegram, slack). Omit to use default."),
|
|
164
123
|
media_path: z
|
|
165
124
|
.string()
|
|
166
125
|
.optional()
|
|
167
|
-
.describe(
|
|
168
|
-
"Absolute path to a file to send as an attachment (image, document)",
|
|
169
|
-
),
|
|
126
|
+
.describe("Absolute path to a file to send as an attachment (image, document)"),
|
|
170
127
|
},
|
|
171
128
|
async (args) => ({
|
|
172
129
|
content: [
|
|
173
130
|
{
|
|
174
131
|
type: "text" as const,
|
|
175
|
-
text: await handlers.sendMessage(
|
|
176
|
-
args.text,
|
|
177
|
-
args.channel,
|
|
178
|
-
args.media_path,
|
|
179
|
-
),
|
|
132
|
+
text: await handlers.sendMessage(args.text, args.channel, args.media_path),
|
|
180
133
|
},
|
|
181
134
|
],
|
|
182
135
|
}),
|
|
@@ -185,10 +138,7 @@ export function createNiaMcpServer() {
|
|
|
185
138
|
"list_messages",
|
|
186
139
|
"Read recent chat history",
|
|
187
140
|
{
|
|
188
|
-
limit: z
|
|
189
|
-
.number()
|
|
190
|
-
.default(20)
|
|
191
|
-
.describe("Number of messages to return"),
|
|
141
|
+
limit: z.number().default(20).describe("Number of messages to return"),
|
|
192
142
|
room: z.string().optional().describe("Filter by room name"),
|
|
193
143
|
},
|
|
194
144
|
async (args) => ({
|
|
@@ -205,10 +155,7 @@ export function createNiaMcpServer() {
|
|
|
205
155
|
"Browse past conversation sessions with previews. Returns session IDs you can pass to read_session.",
|
|
206
156
|
{
|
|
207
157
|
room: z.string().optional().describe("Filter by room name"),
|
|
208
|
-
limit: z
|
|
209
|
-
.number()
|
|
210
|
-
.default(10)
|
|
211
|
-
.describe("Number of sessions to return"),
|
|
158
|
+
limit: z.number().default(10).describe("Number of sessions to return"),
|
|
212
159
|
},
|
|
213
160
|
async (args) => ({
|
|
214
161
|
content: [
|
|
@@ -231,11 +178,7 @@ export function createNiaMcpServer() {
|
|
|
231
178
|
content: [
|
|
232
179
|
{
|
|
233
180
|
type: "text" as const,
|
|
234
|
-
text: await handlers.searchMessages(
|
|
235
|
-
args.query,
|
|
236
|
-
args.limit,
|
|
237
|
-
args.room,
|
|
238
|
-
),
|
|
181
|
+
text: await handlers.searchMessages(args.query, args.limit, args.room),
|
|
239
182
|
},
|
|
240
183
|
],
|
|
241
184
|
}),
|
|
@@ -257,7 +200,7 @@ export function createNiaMcpServer() {
|
|
|
257
200
|
),
|
|
258
201
|
tool(
|
|
259
202
|
"add_watch_channel",
|
|
260
|
-
"Add or update a Slack watch channel. Watch channels receive ALL messages (not just @mentions)
|
|
203
|
+
"Add or update a Slack watch channel. Watch channels receive ALL messages (not just @mentions). Behavior is optional — if omitted, loads watches/<channel_name>/behavior.md at runtime. If a single word, it names a different watch dir. If prose (with spaces), treated as inline behavior. Takes effect on next message (hot-reloads).",
|
|
261
204
|
{
|
|
262
205
|
name: z
|
|
263
206
|
.string()
|
|
@@ -266,8 +209,9 @@ export function createNiaMcpServer() {
|
|
|
266
209
|
),
|
|
267
210
|
behavior: z
|
|
268
211
|
.string()
|
|
212
|
+
.optional()
|
|
269
213
|
.describe(
|
|
270
|
-
"
|
|
214
|
+
"Optional. Omit to load watches/<channel_name>/behavior.md. A single word names a different watch dir. Prose (with spaces) is inline behavior.",
|
|
271
215
|
),
|
|
272
216
|
},
|
|
273
217
|
async (args) => ({
|
|
@@ -285,9 +229,7 @@ export function createNiaMcpServer() {
|
|
|
285
229
|
{
|
|
286
230
|
name: z
|
|
287
231
|
.string()
|
|
288
|
-
.describe(
|
|
289
|
-
"Slack channel key to stop watching (e.g. 'C1234567890#ask-kay-thread-notifications')",
|
|
290
|
-
),
|
|
232
|
+
.describe("Slack channel key to stop watching (e.g. 'C1234567890#ask-kay-thread-notifications')"),
|
|
291
233
|
},
|
|
292
234
|
async (args) => ({
|
|
293
235
|
content: [
|
|
@@ -332,16 +274,10 @@ export function createNiaMcpServer() {
|
|
|
332
274
|
"add_rule",
|
|
333
275
|
"Add a behavioral rule. Rules are loaded into every session and take effect without restart. Use for 'from now on' / 'always' / 'never' type instructions.",
|
|
334
276
|
{
|
|
335
|
-
rule: z
|
|
336
|
-
.string()
|
|
337
|
-
.describe(
|
|
338
|
-
"The rule to add (e.g. 'stamp updates: 1-2 lines max, no preamble')",
|
|
339
|
-
),
|
|
277
|
+
rule: z.string().describe("The rule to add (e.g. 'stamp updates: 1-2 lines max, no preamble')"),
|
|
340
278
|
},
|
|
341
279
|
async (args) => ({
|
|
342
|
-
content: [
|
|
343
|
-
{ type: "text" as const, text: handlers.addRule(args.rule) },
|
|
344
|
-
],
|
|
280
|
+
content: [{ type: "text" as const, text: handlers.addRule(args.rule) }],
|
|
345
281
|
}),
|
|
346
282
|
),
|
|
347
283
|
tool(
|
|
@@ -356,17 +292,10 @@ export function createNiaMcpServer() {
|
|
|
356
292
|
"add_memory",
|
|
357
293
|
"Save a concise factual memory for future reference. Proactively save personal facts (travel, schedule), work context (decisions, deadlines), and corrections — don't wait to be asked. RULES: Max 300 chars. One insight per entry. NO raw logs, NO transcripts, NO status dumps.",
|
|
358
294
|
{
|
|
359
|
-
entry: z
|
|
360
|
-
.string()
|
|
361
|
-
.max(300)
|
|
362
|
-
.describe(
|
|
363
|
-
"A single concise insight (max 300 chars, no raw logs or transcripts)",
|
|
364
|
-
),
|
|
295
|
+
entry: z.string().max(300).describe("A single concise insight (max 300 chars, no raw logs or transcripts)"),
|
|
365
296
|
},
|
|
366
297
|
async (args) => ({
|
|
367
|
-
content: [
|
|
368
|
-
{ type: "text" as const, text: handlers.addMemory(args.entry) },
|
|
369
|
-
],
|
|
298
|
+
content: [{ type: "text" as const, text: handlers.addMemory(args.entry) }],
|
|
370
299
|
}),
|
|
371
300
|
),
|
|
372
301
|
tool(
|