niahere 0.3.10 → 0.3.12
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 +2 -1
- package/package.json +1 -1
- package/src/channels/index.ts +19 -0
- package/src/channels/slack.ts +29 -4
- package/src/channels/telegram.ts +2 -1
- package/src/chat/engine.ts +8 -4
- package/src/cli/index.ts +21 -6
- package/src/cli/status.ts +2 -2
- package/src/commands/validate.ts +2 -0
- package/src/core/daemon.ts +13 -3
- package/src/types/config.ts +2 -0
- package/src/types/engine.ts +2 -0
- package/src/utils/config.ts +8 -2
package/README.md
CHANGED
|
@@ -86,7 +86,8 @@ nia agent show <name> — show agent details and prompt
|
|
|
86
86
|
nia skills [source] — list available skills
|
|
87
87
|
|
|
88
88
|
nia channels — show channel status (on/off)
|
|
89
|
-
nia channels on / off — enable/disable channels (applied via SIGHUP, no restart)
|
|
89
|
+
nia channels on / off — enable/disable all channels (applied via SIGHUP, no restart)
|
|
90
|
+
nia channels off telegram — disable one channel without removing its token
|
|
90
91
|
nia watch list — list Slack watch channels
|
|
91
92
|
nia watch add/remove/enable/disable — manage watch channels
|
|
92
93
|
|
package/package.json
CHANGED
package/src/channels/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Channel } from "../types";
|
|
2
2
|
import { registerChannel, getFactories, trackStarted, clearStarted } from "./registry";
|
|
3
3
|
import { log } from "../utils/log";
|
|
4
|
+
import { getConfig } from "../utils/config";
|
|
4
5
|
import { createTelegramChannel } from "./telegram";
|
|
5
6
|
import { createSlackChannel } from "./slack";
|
|
6
7
|
import { createPhoneChannel } from "./phone";
|
|
@@ -59,6 +60,24 @@ export async function startChannels(): Promise<StartResult> {
|
|
|
59
60
|
return { started, failed };
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
export function getConfiguredChannelNames(): string[] {
|
|
64
|
+
const { channels } = getConfig();
|
|
65
|
+
if (!channels.enabled) return [];
|
|
66
|
+
|
|
67
|
+
const names: string[] = [];
|
|
68
|
+
if (channels.telegram.enabled && channels.telegram.bot_token) names.push("telegram");
|
|
69
|
+
if (channels.slack.enabled && channels.slack.bot_token && channels.slack.app_token) names.push("slack");
|
|
70
|
+
if (channels.phone.enabled && channels.twilio.sid && channels.twilio.secret && channels.phone.from_number) {
|
|
71
|
+
names.push("phone");
|
|
72
|
+
}
|
|
73
|
+
const smsFromNumber = channels.sms.from_number ?? channels.phone.from_number;
|
|
74
|
+
if (channels.sms.enabled && channels.twilio.sid && channels.twilio.secret && smsFromNumber) names.push("sms");
|
|
75
|
+
if (channels.whatsapp.enabled && channels.twilio.sid && channels.twilio.secret && channels.whatsapp.from_number) {
|
|
76
|
+
names.push("whatsapp");
|
|
77
|
+
}
|
|
78
|
+
return names;
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
export async function stopChannels(channels: Channel[]): Promise<void> {
|
|
63
82
|
const results = await Promise.allSettled(
|
|
64
83
|
channels.map(async (channel) => {
|
package/src/channels/slack.ts
CHANGED
|
@@ -15,6 +15,21 @@ function cleanSentinel(text: string): string {
|
|
|
15
15
|
return text.replace(/`/g, "").trim();
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
interface SlackReactionClient {
|
|
19
|
+
reactions: {
|
|
20
|
+
add(args: { channel: string; timestamp: string; name: string }): Promise<unknown>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function reactToSlackMessage(
|
|
25
|
+
client: SlackReactionClient,
|
|
26
|
+
channel: string,
|
|
27
|
+
timestamp: string,
|
|
28
|
+
name: string,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
await client.reactions.add({ channel, timestamp, name });
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
class SlackChannel implements Channel {
|
|
19
34
|
name = "slack" as const;
|
|
20
35
|
private app: App | null = null;
|
|
@@ -415,12 +430,11 @@ class SlackChannel implements Channel {
|
|
|
415
430
|
}
|
|
416
431
|
|
|
417
432
|
// Add thinking reaction inside the lock so cleanup is guaranteed
|
|
418
|
-
await client.
|
|
419
|
-
.add({ channel: msg.channel, timestamp: msg.ts, name: "thinking_face" })
|
|
433
|
+
await reactToSlackMessage(client, msg.channel, msg.ts, "thinking_face")
|
|
420
434
|
.catch((err) => log.debug({ err, channel: msg.channel }, "slack: failed to add thinking reaction"));
|
|
421
435
|
|
|
422
436
|
try {
|
|
423
|
-
const { result, messageId } = await state.engine.send(
|
|
437
|
+
const { result, messageId, signal } = await state.engine.send(
|
|
424
438
|
text,
|
|
425
439
|
{
|
|
426
440
|
onActivity(status) {
|
|
@@ -430,6 +444,15 @@ class SlackChannel implements Channel {
|
|
|
430
444
|
attachments,
|
|
431
445
|
);
|
|
432
446
|
|
|
447
|
+
if (signal === "provider_down") {
|
|
448
|
+
await reactToSlackMessage(client, msg.channel, msg.ts, "skull").catch((err) =>
|
|
449
|
+
log.debug({ err, channel: msg.channel }, "slack: failed to add provider-down reaction"),
|
|
450
|
+
);
|
|
451
|
+
if (messageId) await Message.updateDeliveryStatus(messageId, "sent").catch(() => {});
|
|
452
|
+
log.info({ channel: msg.channel, key, reaction: "skull" }, "slack provider failure sent as reaction");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
433
456
|
const reply = result.trim();
|
|
434
457
|
const cleaned = cleanSentinel(reply);
|
|
435
458
|
|
|
@@ -517,6 +540,8 @@ class SlackChannel implements Channel {
|
|
|
517
540
|
|
|
518
541
|
export function createSlackChannel(): SlackChannel | null {
|
|
519
542
|
const config = getConfig();
|
|
520
|
-
if (!config.channels.slack.bot_token || !config.channels.slack.app_token)
|
|
543
|
+
if (!config.channels.slack.enabled || !config.channels.slack.bot_token || !config.channels.slack.app_token) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
521
546
|
return new SlackChannel();
|
|
522
547
|
}
|
package/src/channels/telegram.ts
CHANGED
|
@@ -286,6 +286,7 @@ class TelegramChannel implements Channel {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
export function createTelegramChannel(): TelegramChannel | null {
|
|
289
|
-
|
|
289
|
+
const telegram = getConfig().channels.telegram;
|
|
290
|
+
if (!telegram.enabled || !telegram.bot_token) return null;
|
|
290
291
|
return new TelegramChannel();
|
|
291
292
|
}
|
package/src/chat/engine.ts
CHANGED
|
@@ -33,8 +33,7 @@ const IDLE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
|
33
33
|
const LONG_RUNNING_WARN = 30 * 60 * 1000; // 30 minutes
|
|
34
34
|
const MAX_SEND_RETRIES = 2;
|
|
35
35
|
const SEND_RETRY_DELAYS = [3_000, 8_000];
|
|
36
|
-
const GENERIC_CHAT_ERROR =
|
|
37
|
-
"Claude/Anthropic returned an error without details. This is usually temporary; please try again shortly.";
|
|
36
|
+
const GENERIC_CHAT_ERROR = "💀";
|
|
38
37
|
|
|
39
38
|
interface SDKUserMessage {
|
|
40
39
|
type: "user";
|
|
@@ -102,7 +101,7 @@ export function buildContentBlocks(text: string, attachments?: Attachment[]): Me
|
|
|
102
101
|
/** Convert SDK error text into a channel-safe chat response. */
|
|
103
102
|
export function formatChatError(rawError: string | null | undefined): string {
|
|
104
103
|
const error = rawError?.trim();
|
|
105
|
-
if (
|
|
104
|
+
if (getChatErrorSignal(error) === "provider_down") {
|
|
106
105
|
return GENERIC_CHAT_ERROR;
|
|
107
106
|
}
|
|
108
107
|
if (error === "oauth_org_not_allowed") {
|
|
@@ -111,6 +110,11 @@ export function formatChatError(rawError: string | null | undefined): string {
|
|
|
111
110
|
return `[error] ${error}`;
|
|
112
111
|
}
|
|
113
112
|
|
|
113
|
+
export function getChatErrorSignal(rawError: string | null | undefined): SendResult["signal"] | undefined {
|
|
114
|
+
const error = rawError?.trim();
|
|
115
|
+
return !error || error.toLowerCase() === "unknown error" ? "provider_down" : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
114
118
|
export function resolveSdkModel(contextModel?: string | null): string | undefined {
|
|
115
119
|
const model = contextModel || getConfig().model;
|
|
116
120
|
return model && model !== "default" ? model : undefined;
|
|
@@ -560,7 +564,7 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
560
564
|
);
|
|
561
565
|
await ActiveEngine.unregister(room);
|
|
562
566
|
clearLongRunningTimer();
|
|
563
|
-
pending.resolve({ result: errorText, costUsd: 0, turns: 0 });
|
|
567
|
+
pending.resolve({ result: errorText, costUsd: 0, turns: 0, signal: getChatErrorSignal(rawError) });
|
|
564
568
|
pending = null;
|
|
565
569
|
retryCount = 0;
|
|
566
570
|
resetIdleTimer();
|
package/src/cli/index.ts
CHANGED
|
@@ -49,8 +49,10 @@ async function awaitStartup(timeout = 60_000): Promise<void> {
|
|
|
49
49
|
const config = getConfig();
|
|
50
50
|
const expecting = new Set<string>();
|
|
51
51
|
if (config.channels.enabled) {
|
|
52
|
-
if (config.channels.telegram.bot_token) expecting.add("telegram");
|
|
53
|
-
if (config.channels.slack.bot_token && config.channels.slack.app_token)
|
|
52
|
+
if (config.channels.telegram.enabled && config.channels.telegram.bot_token) expecting.add("telegram");
|
|
53
|
+
if (config.channels.slack.enabled && config.channels.slack.bot_token && config.channels.slack.app_token) {
|
|
54
|
+
expecting.add("slack");
|
|
55
|
+
}
|
|
54
56
|
}
|
|
55
57
|
expecting.add("scheduler");
|
|
56
58
|
|
|
@@ -445,16 +447,29 @@ switch (command) {
|
|
|
445
447
|
|
|
446
448
|
case "channels": {
|
|
447
449
|
const sub = process.argv[3];
|
|
450
|
+
const target = process.argv[4];
|
|
448
451
|
const { updateRawConfig } = await import("../utils/config");
|
|
449
452
|
if (sub === "on" || sub === "off") {
|
|
450
453
|
const enabled = sub === "on";
|
|
451
|
-
|
|
454
|
+
if (target) {
|
|
455
|
+
const supported = new Set(["telegram", "slack", "phone", "sms", "whatsapp"]);
|
|
456
|
+
if (!supported.has(target)) fail("Usage: nia channels <on|off> [telegram|slack|phone|sms|whatsapp]");
|
|
457
|
+
updateRawConfig({ channels: { ...(enabled ? { enabled: true } : {}), [target]: { enabled } } });
|
|
458
|
+
} else {
|
|
459
|
+
updateRawConfig({ channels: { enabled } });
|
|
460
|
+
}
|
|
452
461
|
const pid = readPid();
|
|
453
462
|
if (pid && isRunning()) {
|
|
454
463
|
process.kill(pid, "SIGHUP");
|
|
455
|
-
console.log(
|
|
464
|
+
console.log(
|
|
465
|
+
target ? `${target} ${enabled ? "enabled" : "disabled"}` : `channels ${enabled ? "enabled" : "disabled"}`,
|
|
466
|
+
);
|
|
456
467
|
} else {
|
|
457
|
-
console.log(
|
|
468
|
+
console.log(
|
|
469
|
+
target
|
|
470
|
+
? `${target} ${enabled ? "enabled" : "disabled"} — start nia to apply`
|
|
471
|
+
: `channels ${enabled ? "enabled" : "disabled"} — start nia to apply`,
|
|
472
|
+
);
|
|
458
473
|
}
|
|
459
474
|
} else {
|
|
460
475
|
console.log(`channels: ${getConfig().channels.enabled ? "on" : "off"}`);
|
|
@@ -605,7 +620,7 @@ Persona:
|
|
|
605
620
|
skills [source] List available skills
|
|
606
621
|
|
|
607
622
|
Channels:
|
|
608
|
-
channels [on|off]
|
|
623
|
+
channels [on|off] [name] Toggle all channels or one channel
|
|
609
624
|
watch <sub> Manage Slack watch channels
|
|
610
625
|
telegram [setup] Configure telegram
|
|
611
626
|
slack [setup] Configure slack
|
package/src/cli/status.ts
CHANGED
|
@@ -84,7 +84,7 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
84
84
|
configured: Boolean(config.channels.telegram.bot_token),
|
|
85
85
|
status: !config.channels.telegram.bot_token
|
|
86
86
|
? "not configured"
|
|
87
|
-
: !config.channels.enabled
|
|
87
|
+
: !config.channels.enabled || !config.channels.telegram.enabled
|
|
88
88
|
? "disabled"
|
|
89
89
|
: running
|
|
90
90
|
? "active"
|
|
@@ -96,7 +96,7 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
96
96
|
appTokenConfigured: Boolean(config.channels.slack.app_token),
|
|
97
97
|
status: !config.channels.slack.bot_token
|
|
98
98
|
? "not configured"
|
|
99
|
-
: !config.channels.enabled
|
|
99
|
+
: !config.channels.enabled || !config.channels.slack.enabled
|
|
100
100
|
? "disabled"
|
|
101
101
|
: running
|
|
102
102
|
? config.channels.slack.app_token
|
package/src/commands/validate.ts
CHANGED
|
@@ -114,6 +114,7 @@ export function validateConfig(): Result {
|
|
|
114
114
|
// Telegram
|
|
115
115
|
const tg = ch.telegram as Record<string, unknown> | undefined;
|
|
116
116
|
if (tg) {
|
|
117
|
+
if (tg.enabled === false) messages.push(`${WARN} telegram disabled`);
|
|
117
118
|
if (tg.bot_token) {
|
|
118
119
|
messages.push(`${PASS} telegram.bot_token set`);
|
|
119
120
|
} else {
|
|
@@ -124,6 +125,7 @@ export function validateConfig(): Result {
|
|
|
124
125
|
// Slack
|
|
125
126
|
const sl = ch.slack as Record<string, unknown> | undefined;
|
|
126
127
|
if (sl) {
|
|
128
|
+
if (sl.enabled === false) messages.push(`${WARN} slack disabled`);
|
|
127
129
|
if (!sl.bot_token) {
|
|
128
130
|
messages.push(`${WARN} slack.bot_token missing — slack won't start`);
|
|
129
131
|
} else {
|
package/src/core/daemon.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { isRunning, readPid, removePid, writePid } from "../utils/pid";
|
|
|
8
8
|
import { ActiveEngine, Job } from "../db/models";
|
|
9
9
|
import { runMigrations } from "../db/migrate";
|
|
10
10
|
import { closeDb, getSql } from "../db/connection";
|
|
11
|
-
import { registerAllChannels, startChannels, stopChannels, getStarted } from "../channels";
|
|
11
|
+
import { registerAllChannels, startChannels, stopChannels, getStarted, getConfiguredChannelNames } from "../channels";
|
|
12
12
|
import type { Channel } from "../types";
|
|
13
13
|
import { startScheduler, stopScheduler, recomputeAllNextRuns } from "./scheduler";
|
|
14
14
|
import { startAlive, stopAlive } from "./alive";
|
|
@@ -346,11 +346,16 @@ export async function runDaemon(): Promise<void> {
|
|
|
346
346
|
process.on("SIGHUP", async () => {
|
|
347
347
|
log.info("received SIGHUP, reloading config");
|
|
348
348
|
resetConfig();
|
|
349
|
-
const fresh = getConfig();
|
|
350
349
|
|
|
351
350
|
const running = getStarted();
|
|
352
|
-
const
|
|
351
|
+
const wantedNames = getConfiguredChannelNames();
|
|
352
|
+
const runningNames = running.map((channel) => channel.name).sort();
|
|
353
|
+
const wantChannels = wantedNames.length > 0;
|
|
353
354
|
const haveChannels = running.length > 0;
|
|
355
|
+
const needsReconcile =
|
|
356
|
+
wantChannels &&
|
|
357
|
+
haveChannels &&
|
|
358
|
+
(wantedNames.length !== runningNames.length || wantedNames.sort().some((name, i) => name !== runningNames[i]));
|
|
354
359
|
|
|
355
360
|
if (wantChannels && !haveChannels) {
|
|
356
361
|
log.info("SIGHUP: starting channels");
|
|
@@ -360,6 +365,11 @@ export async function runDaemon(): Promise<void> {
|
|
|
360
365
|
log.info("SIGHUP: stopping channels");
|
|
361
366
|
await stopChannels(running);
|
|
362
367
|
channels = [];
|
|
368
|
+
} else if (needsReconcile) {
|
|
369
|
+
log.info({ wantedNames, runningNames }, "SIGHUP: reconciling channels");
|
|
370
|
+
await stopChannels(running);
|
|
371
|
+
const result = await startChannels();
|
|
372
|
+
channels = result.started;
|
|
363
373
|
}
|
|
364
374
|
|
|
365
375
|
await recomputeAllNextRuns().catch(() => {});
|
package/src/types/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export interface TelegramConfig {
|
|
2
|
+
enabled: boolean;
|
|
2
3
|
bot_token: string | null;
|
|
3
4
|
chat_id: number | null;
|
|
4
5
|
open: boolean;
|
|
@@ -17,6 +18,7 @@ export interface SlackWatchChannel {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export interface SlackConfig {
|
|
21
|
+
enabled: boolean;
|
|
20
22
|
bot_token: string | null;
|
|
21
23
|
app_token: string | null;
|
|
22
24
|
dm_user_id: string | null;
|
package/src/types/engine.ts
CHANGED
|
@@ -2,6 +2,8 @@ export interface SendResult {
|
|
|
2
2
|
result: string;
|
|
3
3
|
costUsd: number;
|
|
4
4
|
turns: number;
|
|
5
|
+
/** Optional channel-level signal for transports with richer UI than plain text. */
|
|
6
|
+
signal?: "provider_down";
|
|
5
7
|
/** DB message ID for delivery status tracking (only set for agent replies) */
|
|
6
8
|
messageId?: number;
|
|
7
9
|
}
|
package/src/utils/config.ts
CHANGED
|
@@ -24,8 +24,9 @@ const DEFAULTS: Config = {
|
|
|
24
24
|
channels: {
|
|
25
25
|
enabled: true,
|
|
26
26
|
default: "telegram",
|
|
27
|
-
telegram: { bot_token: null, chat_id: null, open: false },
|
|
27
|
+
telegram: { enabled: true, bot_token: null, chat_id: null, open: false },
|
|
28
28
|
slack: {
|
|
29
|
+
enabled: true,
|
|
29
30
|
bot_token: null,
|
|
30
31
|
app_token: null,
|
|
31
32
|
dm_user_id: null,
|
|
@@ -157,6 +158,8 @@ export function loadConfig(): Config {
|
|
|
157
158
|
const defaultChannel = typeof ch.default === "string" ? ch.default : DEFAULTS.channels.default;
|
|
158
159
|
|
|
159
160
|
// Telegram — env vars override config
|
|
161
|
+
const tgEnabled = chTg.enabled !== false;
|
|
162
|
+
|
|
160
163
|
const tgBotToken = process.env.TELEGRAM_BOT_TOKEN || (typeof chTg.bot_token === "string" ? chTg.bot_token : null);
|
|
161
164
|
|
|
162
165
|
const tgChatId =
|
|
@@ -166,6 +169,8 @@ export function loadConfig(): Config {
|
|
|
166
169
|
const tgOpen = chTg.open === true;
|
|
167
170
|
|
|
168
171
|
// Slack — env vars override config
|
|
172
|
+
const slEnabled = chSl.enabled !== false;
|
|
173
|
+
|
|
169
174
|
const slBotToken = process.env.SLACK_BOT_TOKEN || (typeof chSl.bot_token === "string" ? chSl.bot_token : null);
|
|
170
175
|
|
|
171
176
|
const slAppToken = process.env.SLACK_APP_TOKEN || (typeof chSl.app_token === "string" ? chSl.app_token : null);
|
|
@@ -254,8 +259,9 @@ export function loadConfig(): Config {
|
|
|
254
259
|
channels: {
|
|
255
260
|
enabled: channelsEnabled,
|
|
256
261
|
default: defaultChannel,
|
|
257
|
-
telegram: { bot_token: tgBotToken, chat_id: tgChatId, open: tgOpen },
|
|
262
|
+
telegram: { enabled: tgEnabled, bot_token: tgBotToken, chat_id: tgChatId, open: tgOpen },
|
|
258
263
|
slack: {
|
|
264
|
+
enabled: slEnabled,
|
|
259
265
|
bot_token: slBotToken,
|
|
260
266
|
app_token: slAppToken,
|
|
261
267
|
dm_user_id: slDmUserId,
|