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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "A personal AI assistant daemon — scheduled jobs, chat across Telegram and Slack, persona system, and visual identity.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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");
@@ -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 = { ...((slack.watch || {}) as Record<string, unknown>) };
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
- updateRawConfig({ channels: { slack: { watch } } });
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
 
@@ -6,6 +6,7 @@ export interface TelegramConfig {
6
6
 
7
7
  export interface SlackWatchChannel {
8
8
  behavior: string;
9
+ enabled: boolean;
9
10
  }
10
11
 
11
12
  export interface SlackConfig {
@@ -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
- slWatch[name] = { behavior: (val as any).behavior };
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);