niahere 0.2.26 → 0.2.28
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 +4 -2
- package/src/cli/index.ts +7 -0
- package/src/cli/watch.ts +71 -0
- package/src/mcp/server.ts +20 -0
- package/src/mcp/tools.ts +29 -5
- package/src/prompts/channel-slack.md +4 -0
- package/src/prompts/environment.md +1 -0
- package/src/types/config.ts +1 -0
- package/src/utils/config.ts +9 -5
package/package.json
CHANGED
package/src/channels/slack.ts
CHANGED
|
@@ -512,15 +512,17 @@ class SlackChannel implements Channel {
|
|
|
512
512
|
const rawWatchConfig = config.channels.slack.watch;
|
|
513
513
|
if (rawWatchConfig) {
|
|
514
514
|
for (const [key, cfg] of Object.entries(rawWatchConfig)) {
|
|
515
|
+
if (!cfg.enabled) {
|
|
516
|
+
log.info({ channel: key }, "slack: watch channel disabled, skipping");
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
515
519
|
const hashIdx = key.indexOf("#");
|
|
516
520
|
if (hashIdx !== -1) {
|
|
517
|
-
// channel_id#channel_name format — use ID directly, no API call needed
|
|
518
521
|
const id = key.slice(0, hashIdx);
|
|
519
522
|
const name = key.slice(hashIdx + 1);
|
|
520
523
|
watchChannels.set(id, { name, behavior: cfg.behavior });
|
|
521
524
|
log.info({ channel: name, id }, "slack: watching channel");
|
|
522
525
|
} else {
|
|
523
|
-
// Legacy: plain channel name — resolve via API
|
|
524
526
|
try {
|
|
525
527
|
const channelList: { id: string; name: string }[] = [];
|
|
526
528
|
let cursor: string | undefined;
|
package/src/cli/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { jobCommand } from "./job";
|
|
|
13
13
|
import { statusCommand } from "./status";
|
|
14
14
|
import { sendCommand, telegramCommand, slackCommand } from "./channels";
|
|
15
15
|
import { rulesCommand, memoryCommand } from "./self";
|
|
16
|
+
import { watchCommand } from "./watch";
|
|
16
17
|
|
|
17
18
|
// Set LOG_LEVEL from config before anything else logs
|
|
18
19
|
try {
|
|
@@ -216,6 +217,11 @@ switch (command) {
|
|
|
216
217
|
break;
|
|
217
218
|
}
|
|
218
219
|
|
|
220
|
+
case "watch": {
|
|
221
|
+
watchCommand();
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
219
225
|
case "history": {
|
|
220
226
|
const room = process.argv[3];
|
|
221
227
|
try {
|
|
@@ -440,6 +446,7 @@ switch (command) {
|
|
|
440
446
|
console.log(" memory [show|reset] — view or reset memory.md");
|
|
441
447
|
console.log(" db <sub> — database setup/status/migrate");
|
|
442
448
|
console.log(" skills — list available skills");
|
|
449
|
+
console.log(" watch <sub> — manage Slack watch channels");
|
|
443
450
|
console.log(" validate — validate config.yaml");
|
|
444
451
|
console.log(" config <sub> — get/set/list config values");
|
|
445
452
|
console.log(" send [-c ch] <msg> — send a message via channel");
|
package/src/cli/watch.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readRawConfig } from "../utils/config";
|
|
2
|
+
import { addWatchChannel, removeWatchChannel, enableWatchChannel, disableWatchChannel } from "../mcp/tools";
|
|
3
|
+
import { fail, ICON_PASS, ICON_FAIL } from "../utils/cli";
|
|
4
|
+
|
|
5
|
+
export function watchCommand(): void {
|
|
6
|
+
const sub = process.argv[3];
|
|
7
|
+
|
|
8
|
+
switch (sub) {
|
|
9
|
+
case "list":
|
|
10
|
+
case undefined: {
|
|
11
|
+
const raw = readRawConfig();
|
|
12
|
+
const channels = (raw.channels || {}) as Record<string, unknown>;
|
|
13
|
+
const slack = (channels.slack || {}) as Record<string, unknown>;
|
|
14
|
+
const watch = (slack.watch || {}) as Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
const entries = Object.entries(watch);
|
|
17
|
+
if (entries.length === 0) {
|
|
18
|
+
console.log("No watch channels configured.");
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
for (const [key, val] of entries) {
|
|
22
|
+
const cfg = val as Record<string, unknown>;
|
|
23
|
+
const enabled = cfg.enabled !== false;
|
|
24
|
+
const icon = enabled ? ICON_PASS : ICON_FAIL;
|
|
25
|
+
const behavior = typeof cfg.behavior === "string" ? cfg.behavior.slice(0, 80).replace(/\n/g, " ") : "";
|
|
26
|
+
console.log(` ${icon} ${key} ${behavior}${behavior.length >= 80 ? "..." : ""}`);
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
case "add": {
|
|
32
|
+
const name = process.argv[4];
|
|
33
|
+
const behavior = process.argv.slice(5).join(" ");
|
|
34
|
+
if (!name || !behavior) {
|
|
35
|
+
fail('Usage: nia watch add <channel_id#name> <behavior>');
|
|
36
|
+
}
|
|
37
|
+
console.log(addWatchChannel(name, behavior));
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
case "remove": {
|
|
42
|
+
const name = process.argv[4];
|
|
43
|
+
if (!name) fail("Usage: nia watch remove <channel_id#name>");
|
|
44
|
+
console.log(removeWatchChannel(name));
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case "enable": {
|
|
49
|
+
const name = process.argv[4];
|
|
50
|
+
if (!name) fail("Usage: nia watch enable <channel_id#name>");
|
|
51
|
+
console.log(enableWatchChannel(name));
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case "disable": {
|
|
56
|
+
const name = process.argv[4];
|
|
57
|
+
if (!name) fail("Usage: nia watch disable <channel_id#name>");
|
|
58
|
+
console.log(disableWatchChannel(name));
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
default:
|
|
63
|
+
console.log("Usage: nia watch <list|add|remove|enable|disable>\n");
|
|
64
|
+
console.log(" list — list watch channels (default)");
|
|
65
|
+
console.log(" add <channel_id#name> <behavior> — add a watch channel");
|
|
66
|
+
console.log(" remove <channel_id#name> — remove a watch channel");
|
|
67
|
+
console.log(" enable <channel_id#name> — enable a watch channel");
|
|
68
|
+
console.log(" disable <channel_id#name> — disable a watch channel");
|
|
69
|
+
process.exit(sub ? 1 : 0);
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -105,6 +105,26 @@ export function createNiaMcpServer() {
|
|
|
105
105
|
content: [{ type: "text" as const, text: handlers.removeWatchChannel(args.name) }],
|
|
106
106
|
}),
|
|
107
107
|
),
|
|
108
|
+
tool(
|
|
109
|
+
"enable_watch_channel",
|
|
110
|
+
"Enable a disabled Slack watch channel. Requires daemon restart to take effect.",
|
|
111
|
+
{
|
|
112
|
+
name: z.string().describe("Slack channel key to enable"),
|
|
113
|
+
},
|
|
114
|
+
async (args) => ({
|
|
115
|
+
content: [{ type: "text" as const, text: handlers.enableWatchChannel(args.name) }],
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
tool(
|
|
119
|
+
"disable_watch_channel",
|
|
120
|
+
"Disable a Slack watch channel without removing it. Requires daemon restart to take effect.",
|
|
121
|
+
{
|
|
122
|
+
name: z.string().describe("Slack channel key to disable"),
|
|
123
|
+
},
|
|
124
|
+
async (args) => ({
|
|
125
|
+
content: [{ type: "text" as const, text: handlers.disableWatchChannel(args.name) }],
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
108
128
|
tool(
|
|
109
129
|
"add_rule",
|
|
110
130
|
"Add a behavioral rule. Rules are loaded into every session and take effect without restart. Use for 'from now on' / 'always' / 'never' type instructions.",
|
package/src/mcp/tools.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ScheduleType } from "../types";
|
|
|
3
3
|
import { basename, join } from "path";
|
|
4
4
|
import { Job, Message, Session } from "../db/models";
|
|
5
5
|
import { computeInitialNextRun } from "../core/scheduler";
|
|
6
|
-
import { getConfig, readRawConfig, updateRawConfig } from "../utils/config";
|
|
6
|
+
import { getConfig, readRawConfig, updateRawConfig, writeRawConfig } from "../utils/config";
|
|
7
7
|
import { getPaths } from "../utils/paths";
|
|
8
8
|
import { getChannel } from "../channels/registry";
|
|
9
9
|
import { log } from "../utils/log";
|
|
@@ -233,22 +233,46 @@ export function addWatchChannel(name: string, behavior: string): string {
|
|
|
233
233
|
const raw = readRawConfig();
|
|
234
234
|
const channels = (raw.channels || {}) as Record<string, unknown>;
|
|
235
235
|
const slack = (channels.slack || {}) as Record<string, unknown>;
|
|
236
|
-
const watch = { ...((slack.watch || {}) as Record<string, unknown>), [name]: { behavior } };
|
|
236
|
+
const watch = { ...((slack.watch || {}) as Record<string, unknown>), [name]: { behavior, enabled: true } };
|
|
237
237
|
updateRawConfig({ channels: { slack: { watch } } });
|
|
238
|
-
return `Watch channel "${name}" added. Restart daemon to apply.`;
|
|
238
|
+
return `Watch channel "${name}" added (enabled). Restart daemon to apply.`;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
export function removeWatchChannel(name: string): string {
|
|
242
242
|
const raw = readRawConfig();
|
|
243
243
|
const channels = (raw.channels || {}) as Record<string, unknown>;
|
|
244
244
|
const slack = (channels.slack || {}) as Record<string, unknown>;
|
|
245
|
-
const watch =
|
|
245
|
+
const watch = (slack.watch || {}) as Record<string, unknown>;
|
|
246
246
|
if (!watch[name]) return `Watch channel "${name}" not found.`;
|
|
247
247
|
delete watch[name];
|
|
248
|
-
|
|
248
|
+
writeRawConfig(raw);
|
|
249
249
|
return `Watch channel "${name}" removed. Restart daemon to apply.`;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
export function enableWatchChannel(name: string): string {
|
|
253
|
+
const raw = readRawConfig();
|
|
254
|
+
const channels = (raw.channels || {}) as Record<string, unknown>;
|
|
255
|
+
const slack = (channels.slack || {}) as Record<string, unknown>;
|
|
256
|
+
const watch = (slack.watch || {}) as Record<string, unknown>;
|
|
257
|
+
if (!watch[name]) return `Watch channel "${name}" not found.`;
|
|
258
|
+
const entry = watch[name] as Record<string, unknown>;
|
|
259
|
+
entry.enabled = true;
|
|
260
|
+
updateRawConfig({ channels: { slack: { watch } } });
|
|
261
|
+
return `Watch channel "${name}" enabled. Restart daemon to apply.`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function disableWatchChannel(name: string): string {
|
|
265
|
+
const raw = readRawConfig();
|
|
266
|
+
const channels = (raw.channels || {}) as Record<string, unknown>;
|
|
267
|
+
const slack = (channels.slack || {}) as Record<string, unknown>;
|
|
268
|
+
const watch = (slack.watch || {}) as Record<string, unknown>;
|
|
269
|
+
if (!watch[name]) return `Watch channel "${name}" not found.`;
|
|
270
|
+
const entry = watch[name] as Record<string, unknown>;
|
|
271
|
+
entry.enabled = false;
|
|
272
|
+
updateRawConfig({ channels: { slack: { watch } } });
|
|
273
|
+
return `Watch channel "${name}" disabled. Restart daemon to apply.`;
|
|
274
|
+
}
|
|
275
|
+
|
|
252
276
|
export function addMemory(entry: string): string {
|
|
253
277
|
// Guard: reject raw logs, transcripts, and overly long entries
|
|
254
278
|
const trimmed = entry.trim();
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
## Channel: Slack
|
|
2
2
|
|
|
3
|
+
### Timestamps
|
|
4
|
+
- Prefer relative times ("5 minutes ago", "~2 hours ago") over absolute timestamps. They're timezone-agnostic and easier to read.
|
|
5
|
+
- If you must use absolute times, use the configured timezone from your environment, never raw UTC.
|
|
6
|
+
|
|
3
7
|
### Formatting
|
|
4
8
|
- This is Slack, NOT markdown. Do NOT use **double asterisks** for bold — Slack renders them literally.
|
|
5
9
|
- Slack bold: *bold* (single asterisks). Italic: _italic_. Code: `code`. Links: <url|text>.
|
|
@@ -24,6 +24,7 @@ You have MCP tools for managing jobs directly — no need for shell commands:
|
|
|
24
24
|
- **list_messages** — read recent chat history
|
|
25
25
|
- **add_watch_channel** — add a Slack channel for proactive monitoring. Specify channel name and behavior prompt. Requires daemon restart.
|
|
26
26
|
- **remove_watch_channel** — stop watching a Slack channel. Requires daemon restart.
|
|
27
|
+
- **enable_watch_channel** / **disable_watch_channel** — toggle a watch channel on/off without removing it. Requires daemon restart.
|
|
27
28
|
- **add_rule** — save a behavioral rule (loaded into every session, no restart needed). Use when told "from now on", "always", "never", or "remember to always..."
|
|
28
29
|
- **add_memory** — save a factual memory (read on demand). Use when told "remember that...", or when you learn something surprising worth keeping
|
|
29
30
|
|
package/src/types/config.ts
CHANGED
package/src/utils/config.ts
CHANGED
|
@@ -144,12 +144,13 @@ export function loadConfig(): Config {
|
|
|
144
144
|
|
|
145
145
|
// Slack watch channels
|
|
146
146
|
const rawWatch = chSl.watch as Record<string, unknown> | undefined;
|
|
147
|
-
let slWatch: Record<string, { behavior: string }> | null = null;
|
|
147
|
+
let slWatch: Record<string, { behavior: string; enabled: boolean }> | null = null;
|
|
148
148
|
if (rawWatch && typeof rawWatch === "object") {
|
|
149
149
|
slWatch = {};
|
|
150
150
|
for (const [name, val] of Object.entries(rawWatch)) {
|
|
151
151
|
if (val && typeof val === "object" && typeof (val as any).behavior === "string") {
|
|
152
|
-
|
|
152
|
+
const enabled = (val as any).enabled !== false; // default true
|
|
153
|
+
slWatch[name] = { behavior: (val as any).behavior, enabled };
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
if (Object.keys(slWatch).length === 0) slWatch = null;
|
|
@@ -199,16 +200,19 @@ function deepMerge(target: Record<string, unknown>, source: Record<string, unkno
|
|
|
199
200
|
|
|
200
201
|
/** Deep-merge fields into config.yaml and write back atomically. */
|
|
201
202
|
export function updateRawConfig(fields: Record<string, unknown>): void {
|
|
202
|
-
const { config } = getPaths();
|
|
203
203
|
const raw = readRawConfig();
|
|
204
204
|
deepMerge(raw, fields);
|
|
205
|
+
writeRawConfig(raw);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Write a full config object to config.yaml atomically (backup + temp + rename). */
|
|
209
|
+
export function writeRawConfig(raw: Record<string, unknown>): void {
|
|
210
|
+
const { config } = getPaths();
|
|
205
211
|
const dir = dirname(config);
|
|
206
212
|
mkdirSync(dir, { recursive: true });
|
|
207
|
-
// Back up current config before overwriting
|
|
208
213
|
if (existsSync(config)) {
|
|
209
214
|
copyFileSync(config, join(dir, "config.yaml.bak"));
|
|
210
215
|
}
|
|
211
|
-
// Write to temp file then rename for atomic update (prevents corruption on crash)
|
|
212
216
|
const tmp = join(dir, `.config.yaml.tmp.${process.pid}`);
|
|
213
217
|
writeFileSync(tmp, yaml.dump(raw, { lineWidth: -1 }));
|
|
214
218
|
renameSync(tmp, config);
|