niahere 0.2.79 → 0.2.81
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 +16 -1
- package/src/channels/telegram.ts +8 -3
- package/src/chat/engine.ts +23 -1
- package/src/core/daemon.ts +4 -1
- package/src/mcp/tools.ts +3 -1
- package/src/types/channel.ts +2 -0
package/package.json
CHANGED
package/src/channels/slack.ts
CHANGED
|
@@ -53,6 +53,16 @@ class SlackChannel implements Channel {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
async sendMediaToThread(channelId: string, data: Buffer, mimeType: string, filename?: string, threadTs?: string): Promise<void> {
|
|
57
|
+
if (!this.app) throw new Error("Slack not started");
|
|
58
|
+
await this.app.client.filesUploadV2({
|
|
59
|
+
channel_id: channelId,
|
|
60
|
+
file: data,
|
|
61
|
+
filename: filename || `file.${mimeType.split("/")[1] || "bin"}`,
|
|
62
|
+
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
63
|
+
} as any);
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
async start(): Promise<void> {
|
|
57
67
|
const config = getConfig();
|
|
58
68
|
const botToken = config.channels.slack.bot_token!;
|
|
@@ -308,6 +318,11 @@ class SlackChannel implements Channel {
|
|
|
308
318
|
return ext && /^[a-zA-Z0-9]{1,16}$/.test(ext) ? ext : "bin";
|
|
309
319
|
}
|
|
310
320
|
|
|
321
|
+
function cacheExtension(filename: string | undefined, mime: string, attType: AttachmentType): string {
|
|
322
|
+
if (attType === "image" && mime !== "image/gif") return "jpg";
|
|
323
|
+
return safeExtension(filename);
|
|
324
|
+
}
|
|
325
|
+
|
|
311
326
|
async function extractSlackAttachments(files: any[], scope: string): Promise<Attachment[]> {
|
|
312
327
|
const attachments: Attachment[] = [];
|
|
313
328
|
const scopedAttachDir = cacheDirForScope(scope);
|
|
@@ -327,7 +342,7 @@ class SlackChannel implements Channel {
|
|
|
327
342
|
|
|
328
343
|
// Check disk (survives daemon restarts) — scoped by Slack room/thread.
|
|
329
344
|
const hash = urlHash(file.url_private_download);
|
|
330
|
-
const ext =
|
|
345
|
+
const ext = cacheExtension(file.name, mime, attType);
|
|
331
346
|
const diskPath = join(scopedAttachDir, `${hash}.${ext}`);
|
|
332
347
|
const metaPath = join(scopedAttachDir, `${hash}.meta.json`);
|
|
333
348
|
if (existsSync(diskPath) && existsSync(metaPath)) {
|
package/src/channels/telegram.ts
CHANGED
|
@@ -17,6 +17,11 @@ function safeExtension(filename?: string): string {
|
|
|
17
17
|
return ext && /^[a-zA-Z0-9]{1,16}$/.test(ext) ? ext : "bin";
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function cacheExtension(filename: string | undefined, mimeType: string): string {
|
|
21
|
+
if (mimeType === "image/jpeg") return "jpg";
|
|
22
|
+
return safeExtension(filename);
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
class TelegramChannel implements Channel {
|
|
21
26
|
name = "telegram";
|
|
22
27
|
private bot: Bot | null = null;
|
|
@@ -52,11 +57,11 @@ class TelegramChannel implements Channel {
|
|
|
52
57
|
return Buffer.from(await resp.arrayBuffer());
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
private cacheAttachment(chatId: number, roomIndex: number, data: Buffer, filename?: string): string {
|
|
60
|
+
private cacheAttachment(chatId: number, roomIndex: number, data: Buffer, mimeType: string, filename?: string): string {
|
|
56
61
|
const scope = `telegram-${chatId}-${roomIndex}`;
|
|
57
62
|
const dir = join(getNiaHome(), "tmp", "attachments", scope);
|
|
58
63
|
mkdirSync(dir, { recursive: true });
|
|
59
|
-
const ext =
|
|
64
|
+
const ext = cacheExtension(filename, mimeType);
|
|
60
65
|
const hash = createHash("sha256").update(data).digest("hex").slice(0, 16);
|
|
61
66
|
const path = join(dir, `${hash}.${ext}`);
|
|
62
67
|
writeFileSync(path, data);
|
|
@@ -257,7 +262,7 @@ class TelegramChannel implements Channel {
|
|
|
257
262
|
data = prepared.data;
|
|
258
263
|
finalMime = prepared.mimeType;
|
|
259
264
|
}
|
|
260
|
-
const sourcePath = self.cacheAttachment(ctx.chatId, state.roomIndex, data, doc.file_name);
|
|
265
|
+
const sourcePath = self.cacheAttachment(ctx.chatId, state.roomIndex, data, finalMime, doc.file_name);
|
|
261
266
|
const attachment: Attachment = { type: attType, data, mimeType: finalMime, filename: doc.file_name, sourcePath };
|
|
262
267
|
const caption = ctx.message.caption || (attType === "image" ? "What's in this image?" : "Here's a file.");
|
|
263
268
|
await processMessage(ctx, state, caption, [attachment]);
|
package/src/chat/engine.ts
CHANGED
|
@@ -29,6 +29,8 @@ const IDLE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
|
29
29
|
const LONG_RUNNING_WARN = 30 * 60 * 1000; // 30 minutes
|
|
30
30
|
const MAX_SEND_RETRIES = 2;
|
|
31
31
|
const SEND_RETRY_DELAYS = [3_000, 8_000];
|
|
32
|
+
const GENERIC_CHAT_ERROR =
|
|
33
|
+
"Claude/Anthropic returned an error without details. This is usually temporary; please try again shortly.";
|
|
32
34
|
|
|
33
35
|
interface SDKUserMessage {
|
|
34
36
|
type: "user";
|
|
@@ -93,6 +95,15 @@ export function buildContentBlocks(text: string, attachments?: Attachment[]): Me
|
|
|
93
95
|
return blocks as MessageParam["content"];
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/** Convert SDK error text into a channel-safe chat response. */
|
|
99
|
+
export function formatChatError(rawError: string | null | undefined): string {
|
|
100
|
+
const error = rawError?.trim();
|
|
101
|
+
if (!error || error.toLowerCase() === "unknown error") {
|
|
102
|
+
return GENERIC_CHAT_ERROR;
|
|
103
|
+
}
|
|
104
|
+
return `[error] ${error}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
/**
|
|
97
108
|
* Push-based async iterable for streaming user messages to the SDK.
|
|
98
109
|
* Keeps the query subprocess alive between messages.
|
|
@@ -505,7 +516,18 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
505
516
|
retryPending.onActivity?.("retrying after API error...");
|
|
506
517
|
stream!.push(retryPending.userMessage);
|
|
507
518
|
} else {
|
|
508
|
-
const errorText =
|
|
519
|
+
const errorText = formatChatError(rawError);
|
|
520
|
+
log.error(
|
|
521
|
+
{
|
|
522
|
+
room,
|
|
523
|
+
error: rawError,
|
|
524
|
+
errors,
|
|
525
|
+
subtype: msg.subtype,
|
|
526
|
+
terminal_reason: msg.terminal_reason,
|
|
527
|
+
session_id: msg.session_id,
|
|
528
|
+
},
|
|
529
|
+
"chat send failed with SDK result error",
|
|
530
|
+
);
|
|
509
531
|
await ActiveEngine.unregister(room);
|
|
510
532
|
clearLongRunningTimer();
|
|
511
533
|
pending.resolve({ result: errorText, costUsd: 0, turns: 0 });
|
package/src/core/daemon.ts
CHANGED
|
@@ -57,7 +57,10 @@ export function stopDaemon(opts: { force?: boolean } = {}): boolean {
|
|
|
57
57
|
|
|
58
58
|
// Kill all daemon processes — pidfile PID plus any orphans.
|
|
59
59
|
const killed = killAllDaemons(pidfilePid);
|
|
60
|
-
if (killed === 0 && pidfilePid === null)
|
|
60
|
+
if (killed === 0 && pidfilePid === null) {
|
|
61
|
+
if (opts.force) clearForceShutdownRequest();
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
61
64
|
|
|
62
65
|
// Wait for processes to finish (up to 5 min for active engines, then SIGKILL)
|
|
63
66
|
waitForExit(opts.force ? 30_000 : 310_000);
|
package/src/mcp/tools.ts
CHANGED
|
@@ -296,7 +296,9 @@ export async function sendMessage(text: string, channelName?: string, mediaPath?
|
|
|
296
296
|
const mimeType = guessMime(mediaPath);
|
|
297
297
|
const filename = basename(mediaPath);
|
|
298
298
|
|
|
299
|
-
if (channel?.
|
|
299
|
+
if (useThread && channel?.sendMediaToThread) {
|
|
300
|
+
await channel.sendMediaToThread(sourceCtx!.slackChannelId!, data, mimeType, filename, sourceCtx!.slackThreadTs);
|
|
301
|
+
} else if (channel?.sendMedia) {
|
|
300
302
|
await channel.sendMedia(data, mimeType, filename);
|
|
301
303
|
} else {
|
|
302
304
|
await sendMediaDirect(channelTarget, data, mimeType, filename);
|
package/src/types/channel.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface Channel {
|
|
|
4
4
|
stop(): Promise<void>;
|
|
5
5
|
sendMessage?(text: string): Promise<void>;
|
|
6
6
|
sendMedia?(data: Buffer, mimeType: string, filename?: string): Promise<void>;
|
|
7
|
+
/** Send media to a specific channel/thread when the channel supports it. */
|
|
8
|
+
sendMediaToThread?(channelId: string, data: Buffer, mimeType: string, filename?: string, threadTs?: string): Promise<void>;
|
|
7
9
|
/** Send a message to a specific channel/thread (e.g. reply back to a Slack thread). */
|
|
8
10
|
sendToThread?(channelId: string, text: string, threadTs?: string): Promise<void>;
|
|
9
11
|
}
|