niahere 0.2.22 → 0.2.24
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 +37 -28
- package/src/chat/repl.ts +1 -10
- package/src/cli/channels.ts +4 -4
- package/src/cli/index.ts +14 -7
- package/src/cli/job.ts +3 -3
- package/src/cli/status.ts +3 -2
- package/src/commands/health.ts +4 -7
- package/src/commands/validate.ts +150 -0
- package/src/mcp/server.ts +2 -2
- package/src/prompts/channel-slack.md +1 -0
- package/src/prompts/environment.md +1 -1
- package/src/utils/cli.ts +19 -0
package/package.json
CHANGED
package/src/channels/slack.ts
CHANGED
|
@@ -508,36 +508,45 @@ class SlackChannel implements Channel {
|
|
|
508
508
|
log.warn({ err }, "could not get slack bot user ID");
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
-
//
|
|
512
|
-
const
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
511
|
+
// Parse watch channels — keys are "channel_id#channel_name" or just "channel_name"
|
|
512
|
+
const rawWatchConfig = config.channels.slack.watch;
|
|
513
|
+
if (rawWatchConfig) {
|
|
514
|
+
for (const [key, cfg] of Object.entries(rawWatchConfig)) {
|
|
515
|
+
const hashIdx = key.indexOf("#");
|
|
516
|
+
if (hashIdx !== -1) {
|
|
517
|
+
// channel_id#channel_name format — use ID directly, no API call needed
|
|
518
|
+
const id = key.slice(0, hashIdx);
|
|
519
|
+
const name = key.slice(hashIdx + 1);
|
|
520
|
+
watchChannels.set(id, { name, behavior: cfg.behavior });
|
|
521
|
+
log.info({ channel: name, id }, "slack: watching channel");
|
|
522
|
+
} else {
|
|
523
|
+
// Legacy: plain channel name — resolve via API
|
|
524
|
+
try {
|
|
525
|
+
const channelList: { id: string; name: string }[] = [];
|
|
526
|
+
let cursor: string | undefined;
|
|
527
|
+
do {
|
|
528
|
+
const resp = await app.client.conversations.list({
|
|
529
|
+
types: "public_channel,private_channel",
|
|
530
|
+
exclude_archived: true,
|
|
531
|
+
limit: 200,
|
|
532
|
+
cursor,
|
|
533
|
+
});
|
|
534
|
+
for (const ch of resp.channels || []) {
|
|
535
|
+
if (ch.id && ch.name) channelList.push({ id: ch.id, name: ch.name });
|
|
536
|
+
}
|
|
537
|
+
cursor = resp.response_metadata?.next_cursor || undefined;
|
|
538
|
+
} while (cursor);
|
|
539
|
+
const match = channelList.find((c) => c.name === key);
|
|
540
|
+
if (match) {
|
|
541
|
+
watchChannels.set(match.id, { name: key, behavior: cfg.behavior });
|
|
542
|
+
log.info({ channel: key, id: match.id }, "slack: watching channel (resolved by name)");
|
|
543
|
+
} else {
|
|
544
|
+
log.warn({ channel: key }, "slack: watch channel not found");
|
|
545
|
+
}
|
|
546
|
+
} catch (err) {
|
|
547
|
+
log.warn({ err, channel: key }, "slack: failed to resolve watch channel");
|
|
537
548
|
}
|
|
538
549
|
}
|
|
539
|
-
} catch (err) {
|
|
540
|
-
log.warn({ err }, "slack: failed to resolve watch channels");
|
|
541
550
|
}
|
|
542
551
|
}
|
|
543
552
|
|
package/src/chat/repl.ts
CHANGED
|
@@ -6,16 +6,7 @@ import { getMcpServers, setMcpServers } from "../mcp";
|
|
|
6
6
|
import { createNiaMcpServer } from "../mcp/server";
|
|
7
7
|
import { Session } from "../db/models";
|
|
8
8
|
import { relativeTime } from "../utils/format";
|
|
9
|
-
|
|
10
|
-
// ANSI helpers
|
|
11
|
-
const DIM = "\x1b[2m";
|
|
12
|
-
const BOLD = "\x1b[1m";
|
|
13
|
-
const CYAN = "\x1b[36m";
|
|
14
|
-
const RESET = "\x1b[0m";
|
|
15
|
-
const CLEAR_LINE = "\x1b[2K\r";
|
|
16
|
-
|
|
17
|
-
// Braille spinner frames
|
|
18
|
-
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
9
|
+
import { DIM, BOLD, CYAN, RESET, CLEAR_LINE, SPINNER } from "../utils/cli";
|
|
19
10
|
|
|
20
11
|
class StatusLine {
|
|
21
12
|
private frame = 0;
|
package/src/cli/channels.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getConfig, updateRawConfig } from "../utils/config";
|
|
2
2
|
import { getPaths } from "../utils/paths";
|
|
3
3
|
import { errMsg } from "../utils/errors";
|
|
4
|
-
import { fail } from "../utils/cli";
|
|
4
|
+
import { fail, ICON_PASS, ICON_FAIL } from "../utils/cli";
|
|
5
5
|
import { log } from "../utils/log";
|
|
6
6
|
|
|
7
7
|
export async function sendCommand(): Promise<void> {
|
|
@@ -98,7 +98,7 @@ export async function slackCommand(): Promise<void> {
|
|
|
98
98
|
});
|
|
99
99
|
const data = (await resp.json()) as Record<string, unknown>;
|
|
100
100
|
if (data.ok) {
|
|
101
|
-
console.log(` Auth:
|
|
101
|
+
console.log(` Auth: ${ICON_PASS} valid`);
|
|
102
102
|
// Backfill workspace info if missing
|
|
103
103
|
if (!config.channels.slack.workspace) {
|
|
104
104
|
const enriched = await enrichSlackConfig(config.channels.slack.bot_token);
|
|
@@ -108,10 +108,10 @@ export async function slackCommand(): Promise<void> {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
} else {
|
|
111
|
-
console.log(` Auth:
|
|
111
|
+
console.log(` Auth: ${ICON_FAIL} ${data.error}`);
|
|
112
112
|
}
|
|
113
113
|
} catch (err) {
|
|
114
|
-
console.log(` Auth:
|
|
114
|
+
console.log(` Auth: ${ICON_FAIL} could not reach Slack API`);
|
|
115
115
|
}
|
|
116
116
|
} else {
|
|
117
117
|
console.log("Slack: not configured");
|
package/src/cli/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { Message } from "../db/models";
|
|
|
8
8
|
import { withDb } from "../db/connection";
|
|
9
9
|
import { getNiaHome, getPaths } from "../utils/paths";
|
|
10
10
|
import { errMsg } from "../utils/errors";
|
|
11
|
-
import { fail } from "../utils/cli";
|
|
11
|
+
import { fail, ICON_PASS, ICON_WARN } from "../utils/cli";
|
|
12
12
|
import { jobCommand } from "./job";
|
|
13
13
|
import { statusCommand } from "./status";
|
|
14
14
|
import { sendCommand, telegramCommand, slackCommand } from "./channels";
|
|
@@ -64,14 +64,14 @@ async function awaitStartup(timeout = 60_000): Promise<void> {
|
|
|
64
64
|
if (ready.has(name)) continue;
|
|
65
65
|
if (content.includes(STARTUP_MARKERS[name])) {
|
|
66
66
|
ready.add(name);
|
|
67
|
-
console.log(`
|
|
67
|
+
console.log(` ${ICON_PASS} ${name}`);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const pending = [...expecting].filter((e) => !ready.has(e));
|
|
73
73
|
if (pending.length > 0) {
|
|
74
|
-
console.log(`
|
|
74
|
+
console.log(` ${ICON_WARN} timed out waiting for: ${pending.join(", ")}`);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -143,9 +143,7 @@ switch (command) {
|
|
|
143
143
|
if (prompt) {
|
|
144
144
|
const { createChatEngine } = await import("../chat/engine");
|
|
145
145
|
const { getMcpServers } = await import("../mcp");
|
|
146
|
-
const DIM = "
|
|
147
|
-
const RST = "\x1b[0m";
|
|
148
|
-
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
146
|
+
const { DIM, RESET: RST, CLEAR_LINE, SPINNER: FRAMES } = await import("../utils/cli");
|
|
149
147
|
let frame = 0;
|
|
150
148
|
let statusText = "thinking";
|
|
151
149
|
let spinTimer: ReturnType<typeof setInterval> | null = null;
|
|
@@ -153,7 +151,7 @@ switch (command) {
|
|
|
153
151
|
let streaming = false;
|
|
154
152
|
|
|
155
153
|
const renderSpinner = () => {
|
|
156
|
-
process.stderr.write(
|
|
154
|
+
process.stderr.write(`${CLEAR_LINE}${DIM} ${FRAMES[frame]} ${statusText}${RST}`);
|
|
157
155
|
frame = (frame + 1) % FRAMES.length;
|
|
158
156
|
};
|
|
159
157
|
|
|
@@ -412,6 +410,14 @@ switch (command) {
|
|
|
412
410
|
process.exit(exitCode);
|
|
413
411
|
}
|
|
414
412
|
|
|
413
|
+
case "validate": {
|
|
414
|
+
const { validateConfig } = await import("../commands/validate");
|
|
415
|
+
const result = validateConfig();
|
|
416
|
+
for (const msg of result.messages) console.log(` ${msg}`);
|
|
417
|
+
console.log(result.ok ? "\nConfig is valid." : "\nConfig has errors.");
|
|
418
|
+
process.exit(result.ok ? 0 : 1);
|
|
419
|
+
}
|
|
420
|
+
|
|
415
421
|
case "init": {
|
|
416
422
|
const { runInit } = await import("../commands/init");
|
|
417
423
|
await runInit();
|
|
@@ -434,6 +440,7 @@ switch (command) {
|
|
|
434
440
|
console.log(" memory [show|reset] — view or reset memory.md");
|
|
435
441
|
console.log(" db <sub> — database setup/status/migrate");
|
|
436
442
|
console.log(" skills — list available skills");
|
|
443
|
+
console.log(" validate — validate config.yaml");
|
|
437
444
|
console.log(" config <sub> — get/set/list config values");
|
|
438
445
|
console.log(" send [-c ch] <msg> — send a message via channel");
|
|
439
446
|
console.log(" telegram <token> — configure telegram");
|
package/src/cli/job.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { Job } from "../db/models";
|
|
|
8
8
|
import { withDb } from "../db/connection";
|
|
9
9
|
import type { ScheduleType } from "../types";
|
|
10
10
|
import { errMsg } from "../utils/errors";
|
|
11
|
-
import { fail, pickFromList } from "../utils/cli";
|
|
11
|
+
import { fail, pickFromList, ICON_PASS, ICON_FAIL } from "../utils/cli";
|
|
12
12
|
import { computeInitialNextRun } from "../core/scheduler";
|
|
13
13
|
|
|
14
14
|
async function pickJob(prompt = "Pick a job"): Promise<string> {
|
|
@@ -164,7 +164,7 @@ export async function jobCommand(): Promise<void> {
|
|
|
164
164
|
for (const e of entries) {
|
|
165
165
|
const time = localTime(new Date(e.timestamp));
|
|
166
166
|
const dur = `${e.duration_ms}ms`;
|
|
167
|
-
const icon = e.status === "ok" ?
|
|
167
|
+
const icon = e.status === "ok" ? ICON_PASS : ICON_FAIL;
|
|
168
168
|
const summary = e.error || e.result.slice(0, 60).replace(/\n/g, " ") || "-";
|
|
169
169
|
console.log(` ${icon} ${time} ${dur.padStart(8)} ${summary}`);
|
|
170
170
|
}
|
|
@@ -230,7 +230,7 @@ export async function jobCommand(): Promise<void> {
|
|
|
230
230
|
for (const e of entries) {
|
|
231
231
|
const time = localTime(new Date(e.timestamp));
|
|
232
232
|
const dur = `${e.duration_ms}ms`;
|
|
233
|
-
const status = e.status === "ok" ?
|
|
233
|
+
const status = e.status === "ok" ? ICON_PASS : ICON_FAIL;
|
|
234
234
|
const summary = e.error || e.result.slice(0, 80).replace(/\n/g, " ") || "-";
|
|
235
235
|
console.log(` ${status} ${time} ${dur.padStart(8)} ${e.job} ${summary}`);
|
|
236
236
|
}
|
package/src/cli/status.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ScheduleType, JobStateStatus, RoomStats } from "../types";
|
|
|
8
8
|
import { withDb } from "../db/connection";
|
|
9
9
|
import { errMsg } from "../utils/errors";
|
|
10
10
|
import { checkForUpdate } from "../utils/update";
|
|
11
|
+
import { ICON_PASS, ICON_FAIL, ICON_RUNNING } from "../utils/cli";
|
|
11
12
|
|
|
12
13
|
type StatusOptions = {
|
|
13
14
|
json: boolean;
|
|
@@ -245,7 +246,7 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
245
246
|
safeDate(nextRun)!.getTime() <= now.getTime() &&
|
|
246
247
|
!stateInfo;
|
|
247
248
|
|
|
248
|
-
const statusIcon = status === "ok" ?
|
|
249
|
+
const statusIcon = status === "ok" ? ICON_PASS : status === "error" ? ICON_FAIL : status === "running" ? ICON_RUNNING : "\u2217";
|
|
249
250
|
const durationText = stateInfo?.duration_ms === undefined ? "n/a" : `${stateInfo.duration_ms}ms`;
|
|
250
251
|
const nextText = nextRun ? formatTimeLine(nextRun, now) : "unknown";
|
|
251
252
|
const lastText = lastRun ? formatTimeLine(lastRun, now) : "never";
|
|
@@ -292,7 +293,7 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
292
293
|
console.log("\nJobs (from state file):");
|
|
293
294
|
for (const [name, info] of fallbackEntries) {
|
|
294
295
|
const last = formatTimeLine(info.lastRun, now);
|
|
295
|
-
const icon = info.status === "ok" ?
|
|
296
|
+
const icon = info.status === "ok" ? ICON_PASS : info.status === "error" ? ICON_FAIL : "\u2217";
|
|
296
297
|
console.log(` ${icon} ${name}: ${info.status} (last: ${last}, ${info.duration_ms}ms)`);
|
|
297
298
|
}
|
|
298
299
|
} else if (dbError) {
|
package/src/commands/health.ts
CHANGED
|
@@ -98,14 +98,11 @@ export async function healthCommand(): Promise<void> {
|
|
|
98
98
|
push(checks, "bun", "ok", "v" + bunVersion);
|
|
99
99
|
|
|
100
100
|
// Output
|
|
101
|
-
const GREEN = "
|
|
102
|
-
const YELLOW = "\x1b[33m";
|
|
103
|
-
const RED = "\x1b[31m";
|
|
104
|
-
const RST = "\x1b[0m";
|
|
101
|
+
const { GREEN, YELLOW, RED, RESET, ICON_PASS, ICON_FAIL, ICON_WARN } = await import("../utils/cli");
|
|
105
102
|
const icons: Record<string, string> = {
|
|
106
|
-
ok: GREEN +
|
|
107
|
-
warn: YELLOW +
|
|
108
|
-
fail: RED +
|
|
103
|
+
ok: GREEN + ICON_PASS + RESET,
|
|
104
|
+
warn: YELLOW + ICON_WARN + RESET,
|
|
105
|
+
fail: RED + ICON_FAIL + RESET,
|
|
109
106
|
};
|
|
110
107
|
|
|
111
108
|
console.log();
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
import { getPaths } from "../utils/paths";
|
|
4
|
+
import { ICON_PASS as PASS, ICON_FAIL as FAIL, ICON_WARN as WARN } from "../utils/cli";
|
|
5
|
+
|
|
6
|
+
interface Result {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
messages: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function check(label: string, fn: () => string | null): { icon: string; label: string; detail?: string } {
|
|
12
|
+
const err = fn();
|
|
13
|
+
if (err) return { icon: FAIL, label, detail: err };
|
|
14
|
+
return { icon: PASS, label };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function warn(label: string, detail: string): { icon: string; label: string; detail?: string } {
|
|
18
|
+
return { icon: WARN, label, detail };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateConfig(): Result {
|
|
22
|
+
const { config: configPath } = getPaths();
|
|
23
|
+
const messages: string[] = [];
|
|
24
|
+
let ok = true;
|
|
25
|
+
|
|
26
|
+
// File exists
|
|
27
|
+
if (!existsSync(configPath)) {
|
|
28
|
+
return { ok: false, messages: [`${FAIL} config.yaml not found at ${configPath}`] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Valid YAML
|
|
32
|
+
let raw: Record<string, unknown>;
|
|
33
|
+
try {
|
|
34
|
+
const parsed = yaml.load(readFileSync(configPath, "utf8"));
|
|
35
|
+
if (!parsed || typeof parsed !== "object") {
|
|
36
|
+
return { ok: false, messages: [`${FAIL} config.yaml is empty or not an object`] };
|
|
37
|
+
}
|
|
38
|
+
raw = parsed as Record<string, unknown>;
|
|
39
|
+
messages.push(`${PASS} valid YAML`);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return { ok: false, messages: [`${FAIL} invalid YAML: ${(err as Error).message}`] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Timezone
|
|
45
|
+
if (raw.timezone) {
|
|
46
|
+
try {
|
|
47
|
+
Intl.DateTimeFormat(undefined, { timeZone: raw.timezone as string });
|
|
48
|
+
messages.push(`${PASS} timezone: ${raw.timezone}`);
|
|
49
|
+
} catch {
|
|
50
|
+
messages.push(`${FAIL} invalid timezone: ${raw.timezone}`);
|
|
51
|
+
ok = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Active hours
|
|
56
|
+
const ah = raw.active_hours as Record<string, string> | undefined;
|
|
57
|
+
if (ah) {
|
|
58
|
+
const timeRe = /^\d{2}:\d{2}$/;
|
|
59
|
+
if (ah.start && !timeRe.test(ah.start)) {
|
|
60
|
+
messages.push(`${FAIL} active_hours.start invalid: "${ah.start}" (expected HH:MM)`);
|
|
61
|
+
ok = false;
|
|
62
|
+
} else if (ah.start) {
|
|
63
|
+
messages.push(`${PASS} active_hours: ${ah.start}–${ah.end || "?"}`);
|
|
64
|
+
}
|
|
65
|
+
if (ah.end && !timeRe.test(ah.end)) {
|
|
66
|
+
messages.push(`${FAIL} active_hours.end invalid: "${ah.end}" (expected HH:MM)`);
|
|
67
|
+
ok = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Database URL
|
|
72
|
+
const dbUrl = (process.env.DATABASE_URL || raw.database_url) as string | undefined;
|
|
73
|
+
if (dbUrl && dbUrl.startsWith("postgres")) {
|
|
74
|
+
messages.push(`${PASS} database_url set`);
|
|
75
|
+
} else if (!dbUrl) {
|
|
76
|
+
messages.push(`${WARN} database_url not set (will use default)`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Runner
|
|
80
|
+
const runner = raw.runner as string | undefined;
|
|
81
|
+
if (runner && runner !== "claude" && runner !== "codex") {
|
|
82
|
+
messages.push(`${FAIL} runner must be "claude" or "codex", got "${runner}"`);
|
|
83
|
+
ok = false;
|
|
84
|
+
} else if (runner) {
|
|
85
|
+
messages.push(`${PASS} runner: ${runner}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Channels
|
|
89
|
+
const ch = raw.channels as Record<string, unknown> | undefined;
|
|
90
|
+
if (ch) {
|
|
91
|
+
// Telegram
|
|
92
|
+
const tg = ch.telegram as Record<string, unknown> | undefined;
|
|
93
|
+
if (tg) {
|
|
94
|
+
if (tg.bot_token) {
|
|
95
|
+
messages.push(`${PASS} telegram.bot_token set`);
|
|
96
|
+
} else {
|
|
97
|
+
messages.push(`${WARN} telegram.bot_token missing — telegram won't start`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Slack
|
|
102
|
+
const sl = ch.slack as Record<string, unknown> | undefined;
|
|
103
|
+
if (sl) {
|
|
104
|
+
if (!sl.bot_token) {
|
|
105
|
+
messages.push(`${WARN} slack.bot_token missing — slack won't start`);
|
|
106
|
+
} else {
|
|
107
|
+
messages.push(`${PASS} slack.bot_token set`);
|
|
108
|
+
}
|
|
109
|
+
if (!sl.app_token) {
|
|
110
|
+
messages.push(`${WARN} slack.app_token missing — slack won't start (Socket Mode requires app_token)`);
|
|
111
|
+
} else {
|
|
112
|
+
messages.push(`${PASS} slack.app_token set`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Watch channels
|
|
116
|
+
const watch = sl.watch as Record<string, unknown> | undefined;
|
|
117
|
+
if (watch) {
|
|
118
|
+
for (const [key, val] of Object.entries(watch)) {
|
|
119
|
+
if (!val || typeof val !== "object") {
|
|
120
|
+
messages.push(`${FAIL} slack.watch.${key}: must be an object with "behavior" field`);
|
|
121
|
+
ok = false;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const behavior = (val as Record<string, unknown>).behavior;
|
|
125
|
+
if (typeof behavior !== "string" || !behavior.trim()) {
|
|
126
|
+
messages.push(`${FAIL} slack.watch.${key}: missing "behavior" string`);
|
|
127
|
+
ok = false;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const hasId = key.includes("#");
|
|
131
|
+
if (hasId) {
|
|
132
|
+
messages.push(`${PASS} slack.watch: ${key}`);
|
|
133
|
+
} else {
|
|
134
|
+
messages.push(`${WARN} slack.watch.${key}: using channel name — prefer "channel_id#${key}" format for reliability`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Unknown channel keys
|
|
141
|
+
const knownChannelKeys = new Set(["enabled", "default", "telegram", "slack"]);
|
|
142
|
+
for (const key of Object.keys(ch)) {
|
|
143
|
+
if (!knownChannelKeys.has(key)) {
|
|
144
|
+
messages.push(`${WARN} unknown channel key: "channels.${key}" — did you mean "channels.slack"?`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { ok, messages };
|
|
150
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -88,7 +88,7 @@ export function createNiaMcpServer() {
|
|
|
88
88
|
"add_watch_channel",
|
|
89
89
|
"Add or update a Slack watch channel. Watch channels receive ALL messages (not just @mentions) and act based on the behavior prompt. Requires daemon restart to take effect.",
|
|
90
90
|
{
|
|
91
|
-
name: z.string().describe("Slack channel
|
|
91
|
+
name: z.string().describe("Slack channel key as 'channel_id#channel_name', e.g. 'C1234567890#ask-kay-thread-notifications'"),
|
|
92
92
|
behavior: z.string().describe("What to monitor and how to respond, e.g. 'Monitor thread notifications. Flag failures to #tech.'"),
|
|
93
93
|
},
|
|
94
94
|
async (args) => ({
|
|
@@ -99,7 +99,7 @@ export function createNiaMcpServer() {
|
|
|
99
99
|
"remove_watch_channel",
|
|
100
100
|
"Remove a Slack watch channel. Requires daemon restart to take effect.",
|
|
101
101
|
{
|
|
102
|
-
name: z.string().describe("Slack channel
|
|
102
|
+
name: z.string().describe("Slack channel key to stop watching (e.g. 'C1234567890#ask-kay-thread-notifications')"),
|
|
103
103
|
},
|
|
104
104
|
async (args) => ({
|
|
105
105
|
content: [{ type: "text" as const, text: handlers.removeWatchChannel(args.name) }],
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
### Watch mode
|
|
30
30
|
- Some channels are configured for proactive monitoring via `channels.slack.watch` in config.
|
|
31
|
+
- Watch channel keys use the format `channel_id#channel_name` (e.g. `C1234567890#ask-kay-thread-notifications`). The ID is used for matching; the name is for readability.
|
|
31
32
|
- In watch channels, you receive ALL messages — not just @mentions. Messages are prefixed with `[Watch mode — #channel-name]` and a behavior prompt.
|
|
32
33
|
- Follow the behavior prompt to decide what to do: flag issues, escalate, or stay quiet.
|
|
33
34
|
- Use `[NO_REPLY]` for messages that don't need action. Most watch messages will be `[NO_REPLY]`.
|
|
@@ -59,7 +59,7 @@ Config reference:
|
|
|
59
59
|
- `channels.slack.app_token` — Slack app token (xapp-...)
|
|
60
60
|
- `channels.slack.channel_id` — default Slack channel for outbound
|
|
61
61
|
- `channels.slack.dm_user_id` — auto-registered DM user
|
|
62
|
-
- `channels.slack.watch` — per-channel proactive monitoring
|
|
62
|
+
- `channels.slack.watch` — per-channel proactive monitoring. Keys are `channel_id#channel_name` format.
|
|
63
63
|
{{slackWatch}}
|
|
64
64
|
|
|
65
65
|
## Persona & Memory
|
package/src/utils/cli.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import * as readline from "readline";
|
|
2
2
|
|
|
3
|
+
// ANSI colors
|
|
4
|
+
export const DIM = "\x1b[2m";
|
|
5
|
+
export const BOLD = "\x1b[1m";
|
|
6
|
+
export const RESET = "\x1b[0m";
|
|
7
|
+
export const RED = "\x1b[31m";
|
|
8
|
+
export const GREEN = "\x1b[32m";
|
|
9
|
+
export const YELLOW = "\x1b[33m";
|
|
10
|
+
export const CYAN = "\x1b[36m";
|
|
11
|
+
export const CLEAR_LINE = "\x1b[2K\r";
|
|
12
|
+
|
|
13
|
+
// Icons
|
|
14
|
+
export const ICON_PASS = "\u2713";
|
|
15
|
+
export const ICON_FAIL = "\u2717";
|
|
16
|
+
export const ICON_WARN = "\u26A0";
|
|
17
|
+
export const ICON_RUNNING = "\u21bb";
|
|
18
|
+
|
|
19
|
+
// Spinner frames
|
|
20
|
+
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
21
|
+
|
|
3
22
|
export function fail(msg: string): never {
|
|
4
23
|
console.log(msg);
|
|
5
24
|
process.exit(1);
|