niahere 0.2.79 → 0.2.80

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.79",
3
+ "version": "0.2.80",
4
4
  "description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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 = safeExtension(file.name);
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)) {
@@ -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 = safeExtension(filename);
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]);
@@ -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) return false;
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?.sendMedia) {
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);
@@ -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
  }