niahere 0.2.74 → 0.2.76
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 +2 -2
- package/src/channels/slack.ts +35 -27
- package/src/chat/engine.ts +4 -2
- package/src/constants/attachment.ts +1 -1
- package/src/prompts/channel-slack.md +1 -1
- package/src/prompts/environment.md +1 -1
- package/src/utils/pid.ts +44 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "niahere",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.76",
|
|
4
4
|
"description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"private": false,
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
47
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.119",
|
|
48
48
|
"@anthropic-ai/sdk": "^0.88.0",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
50
50
|
"@slack/bolt": "^4.6.0",
|
package/src/channels/slack.ts
CHANGED
|
@@ -259,8 +259,8 @@ class SlackChannel implements Channel {
|
|
|
259
259
|
});
|
|
260
260
|
|
|
261
261
|
// Disk-backed file cache: download once, read from disk on subsequent requests
|
|
262
|
-
const
|
|
263
|
-
mkdirSync(
|
|
262
|
+
const attachRoot = join(getNiaHome(), "tmp", "attachments");
|
|
263
|
+
mkdirSync(attachRoot, { recursive: true });
|
|
264
264
|
|
|
265
265
|
interface CachedFile {
|
|
266
266
|
path: string;
|
|
@@ -292,26 +292,39 @@ class SlackChannel implements Channel {
|
|
|
292
292
|
return Buffer.from(await resp.arrayBuffer());
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
|
|
295
|
+
function cacheDirForScope(scope: string): string {
|
|
296
|
+
const safeScope = scope.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
297
|
+
const dir = join(attachRoot, safeScope);
|
|
298
|
+
mkdirSync(dir, { recursive: true });
|
|
299
|
+
return dir;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function cacheKey(scope: string, url: string): string {
|
|
303
|
+
return `${scope}:${url}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function extractSlackAttachments(files: any[], scope: string): Promise<Attachment[]> {
|
|
296
307
|
const attachments: Attachment[] = [];
|
|
297
|
-
|
|
308
|
+
const scopedAttachDir = cacheDirForScope(scope);
|
|
309
|
+
for (const file of files) {
|
|
298
310
|
const mime = file.mimetype || "application/octet-stream";
|
|
299
311
|
const attType = classifyMime(mime);
|
|
300
312
|
if (!attType) continue;
|
|
301
313
|
if (!file.url_private_download) continue;
|
|
302
314
|
|
|
303
315
|
// Check in-memory index first
|
|
304
|
-
const
|
|
316
|
+
const indexedKey = cacheKey(scope, file.url_private_download);
|
|
317
|
+
const cached = fileIndex.get(indexedKey);
|
|
305
318
|
if (cached && existsSync(cached.path)) {
|
|
306
319
|
attachments.push(loadCached(cached));
|
|
307
320
|
continue;
|
|
308
321
|
}
|
|
309
322
|
|
|
310
|
-
// Check disk (survives daemon restarts) —
|
|
323
|
+
// Check disk (survives daemon restarts) — scoped by Slack room/thread.
|
|
311
324
|
const hash = urlHash(file.url_private_download);
|
|
312
325
|
const ext = file.name?.split(".").pop() || "bin";
|
|
313
|
-
const diskPath = join(
|
|
314
|
-
const metaPath = join(
|
|
326
|
+
const diskPath = join(scopedAttachDir, `${hash}.${ext}`);
|
|
327
|
+
const metaPath = join(scopedAttachDir, `${hash}.meta.json`);
|
|
315
328
|
if (existsSync(diskPath) && existsSync(metaPath)) {
|
|
316
329
|
try {
|
|
317
330
|
const meta = JSON.parse(readFileSync(metaPath, "utf8"));
|
|
@@ -321,7 +334,7 @@ class SlackChannel implements Channel {
|
|
|
321
334
|
mimeType: meta.mimeType || mime,
|
|
322
335
|
filename: meta.filename || file.name,
|
|
323
336
|
};
|
|
324
|
-
fileIndex.set(
|
|
337
|
+
fileIndex.set(indexedKey, entry);
|
|
325
338
|
attachments.push(loadCached(entry));
|
|
326
339
|
continue;
|
|
327
340
|
} catch {
|
|
@@ -348,7 +361,7 @@ class SlackChannel implements Channel {
|
|
|
348
361
|
writeFileSync(diskPath, finalData);
|
|
349
362
|
writeFileSync(metaPath, JSON.stringify({ type: attType, mimeType: finalMime, filename: file.name }));
|
|
350
363
|
const entry: CachedFile = { path: diskPath, type: attType, mimeType: finalMime, filename: file.name };
|
|
351
|
-
fileIndex.set(
|
|
364
|
+
fileIndex.set(indexedKey, entry);
|
|
352
365
|
|
|
353
366
|
attachments.push({ type: attType, data: finalData, mimeType: finalMime, filename: file.name, sourcePath: diskPath });
|
|
354
367
|
} catch (err) {
|
|
@@ -484,7 +497,7 @@ class SlackChannel implements Channel {
|
|
|
484
497
|
// Download any file attachments
|
|
485
498
|
let attachments: Attachment[] | undefined;
|
|
486
499
|
if (hasFiles) {
|
|
487
|
-
attachments = await extractSlackAttachments(msg.files
|
|
500
|
+
attachments = await extractSlackAttachments(msg.files!, roomPrefix(key));
|
|
488
501
|
}
|
|
489
502
|
|
|
490
503
|
if (!text && (!attachments || attachments.length === 0)) return;
|
|
@@ -516,25 +529,20 @@ class SlackChannel implements Channel {
|
|
|
516
529
|
text = `[Thread context]\n${threadMessages.join("\n")}\n\n[Current message]\n${text}`;
|
|
517
530
|
}
|
|
518
531
|
|
|
519
|
-
// Download files from
|
|
532
|
+
// Download files from fetched thread messages.
|
|
520
533
|
if (!attachments) attachments = [];
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
for (const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
for (const att of extracted) {
|
|
529
|
-
if (threadFilesAdded >= threadFileBudget) break;
|
|
530
|
-
attachments.push(att);
|
|
531
|
-
threadFilesAdded++;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
if (threadFilesAdded > 0) {
|
|
535
|
-
log.info({ threadFiles: threadFilesAdded, channel: msg.channel }, "slack: downloaded thread attachments");
|
|
534
|
+
const messagesWithFiles = priorMessages.filter((m: any) => m.files?.length > 0);
|
|
535
|
+
let threadFilesAdded = 0;
|
|
536
|
+
for (const m of messagesWithFiles) {
|
|
537
|
+
const extracted = await extractSlackAttachments(m.files || [], roomPrefix(key));
|
|
538
|
+
for (const att of extracted) {
|
|
539
|
+
attachments.push(att);
|
|
540
|
+
threadFilesAdded++;
|
|
536
541
|
}
|
|
537
542
|
}
|
|
543
|
+
if (threadFilesAdded > 0) {
|
|
544
|
+
log.info({ threadFiles: threadFilesAdded, channel: msg.channel }, "slack: downloaded thread attachments");
|
|
545
|
+
}
|
|
538
546
|
} catch (err) {
|
|
539
547
|
log.warn({ err, channel: msg.channel, thread_ts: msg.thread_ts }, "failed to fetch thread context");
|
|
540
548
|
}
|
package/src/chat/engine.ts
CHANGED
|
@@ -52,7 +52,7 @@ export function buildContentBlocks(text: string, attachments?: Attachment[]): Me
|
|
|
52
52
|
.map((att, idx) => {
|
|
53
53
|
if (!att.sourcePath) return "";
|
|
54
54
|
const label = att.filename || `${att.type}-${idx + 1}`;
|
|
55
|
-
return `- ${idx + 1}. ${label} -> ${att.sourcePath}`;
|
|
55
|
+
return `- ${idx + 1}. ${label} (${att.type}, ${att.mimeType}) -> ${att.sourcePath}`;
|
|
56
56
|
})
|
|
57
57
|
.filter(Boolean);
|
|
58
58
|
|
|
@@ -61,12 +61,14 @@ export function buildContentBlocks(text: string, attachments?: Attachment[]): Me
|
|
|
61
61
|
type: "text",
|
|
62
62
|
text:
|
|
63
63
|
"[Attachment local paths]\n" +
|
|
64
|
-
"
|
|
64
|
+
"Use these absolute paths to inspect attachments. To resend/forward one, call send_message with media_path set to its path.\n" +
|
|
65
65
|
pathHints.join("\n"),
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
for (const att of attachments) {
|
|
70
|
+
if (att.sourcePath) continue;
|
|
71
|
+
|
|
70
72
|
if (att.type === "image") {
|
|
71
73
|
blocks.push({
|
|
72
74
|
type: "image",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
### Reply routing
|
|
25
25
|
- Always reply in the same thread you received the message in. Don't DM someone unless the conversation is already in DMs.
|
|
26
26
|
- `send_message` defaults to your current context (thread if in one, DM if in DM). For escalations, mention the owner in-thread rather than DMing — keeps context where the conversation is.
|
|
27
|
-
- If the user wants
|
|
27
|
+
- If the user wants you to inspect or send a file/image, use the absolute paths in the `[Attachment local paths]` block. Use `send_message` with `media_path` when forwarding one.
|
|
28
28
|
|
|
29
29
|
### Who's talking
|
|
30
30
|
- Multiple users may message you. Slack messages are prefixed with [user:ID] so you know who's talking (in channels and DMs).
|
|
@@ -43,7 +43,7 @@ You have MCP tools for managing jobs directly (preferred over CLI for speed):
|
|
|
43
43
|
- `auto` (default) — replies in the current Slack thread if you're in one, otherwise DMs the owner. This means watch sessions and thread chats reply in-thread by default.
|
|
44
44
|
- `dm` — always DMs the owner, regardless of current context. Use sparingly — prefer @mentioning the owner in-thread to keep context visible.
|
|
45
45
|
- `thread` — explicitly reply in the current thread (same as auto when in a thread, falls back to DM otherwise).
|
|
46
|
-
For inbound channel files, check the message context for an `[Attachment local paths]` block and
|
|
46
|
+
For inbound channel files, check the message context for an `[Attachment local paths]` block and use those absolute paths for inspection or forwarding.
|
|
47
47
|
- **list_messages** — read recent chat history
|
|
48
48
|
- **list_sessions** — browse past conversation sessions with previews and message counts. Returns session IDs.
|
|
49
49
|
- **search_messages** — keyword search across all past messages. Find when something was discussed.
|
package/src/utils/pid.ts
CHANGED
|
@@ -3,23 +3,50 @@ import { dirname } from "path";
|
|
|
3
3
|
import { getPaths } from "./paths";
|
|
4
4
|
import { log } from "./log";
|
|
5
5
|
|
|
6
|
+
type PidEntry = { pid: number; lstart: string };
|
|
7
|
+
|
|
8
|
+
function getLstart(pid: number): string {
|
|
9
|
+
try {
|
|
10
|
+
const result = Bun.spawnSync(["ps", "-o", "lstart=", "-p", String(pid)]);
|
|
11
|
+
return new TextDecoder().decode(result.stdout).trim();
|
|
12
|
+
} catch {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
6
17
|
export function writePid(pid: number): void {
|
|
7
18
|
const { pid: pidPath } = getPaths();
|
|
19
|
+
const lstart = getLstart(pid);
|
|
20
|
+
if (!lstart) {
|
|
21
|
+
log.warn({ pid }, "could not capture pid identity (ps returned nothing)");
|
|
22
|
+
}
|
|
8
23
|
mkdirSync(dirname(pidPath), { recursive: true });
|
|
9
|
-
writeFileSync(pidPath,
|
|
24
|
+
writeFileSync(pidPath, JSON.stringify({ pid, lstart }));
|
|
10
25
|
}
|
|
11
26
|
|
|
12
|
-
|
|
27
|
+
function readEntry(): PidEntry | null {
|
|
13
28
|
const { pid: pidPath } = getPaths();
|
|
14
29
|
if (!existsSync(pidPath)) return null;
|
|
15
30
|
|
|
16
31
|
try {
|
|
17
|
-
|
|
32
|
+
const raw = readFileSync(pidPath, "utf8").trim();
|
|
33
|
+
if (/^\d+$/.test(raw)) {
|
|
34
|
+
return { pid: parseInt(raw, 10), lstart: "" };
|
|
35
|
+
}
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (typeof parsed?.pid === "number" && typeof parsed?.lstart === "string") {
|
|
38
|
+
return parsed as PidEntry;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
18
41
|
} catch {
|
|
19
42
|
return null;
|
|
20
43
|
}
|
|
21
44
|
}
|
|
22
45
|
|
|
46
|
+
export function readPid(): number | null {
|
|
47
|
+
return readEntry()?.pid ?? null;
|
|
48
|
+
}
|
|
49
|
+
|
|
23
50
|
export function removePid(): void {
|
|
24
51
|
const { pid: pidPath } = getPaths();
|
|
25
52
|
try {
|
|
@@ -30,15 +57,22 @@ export function removePid(): void {
|
|
|
30
57
|
}
|
|
31
58
|
|
|
32
59
|
export function isRunning(): boolean {
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
60
|
+
const entry = readEntry();
|
|
61
|
+
if (entry === null) return false;
|
|
35
62
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
const currentLstart = getLstart(entry.pid);
|
|
64
|
+
if (!currentLstart) {
|
|
65
|
+
log.warn({ stalePid: entry.pid }, "removing stale pid file (process not running)");
|
|
66
|
+
removePid();
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (entry.lstart && currentLstart !== entry.lstart) {
|
|
70
|
+
log.warn(
|
|
71
|
+
{ stalePid: entry.pid, recorded: entry.lstart, current: currentLstart },
|
|
72
|
+
"removing stale pid file (process identity mismatch)",
|
|
73
|
+
);
|
|
41
74
|
removePid();
|
|
42
75
|
return false;
|
|
43
76
|
}
|
|
77
|
+
return true;
|
|
44
78
|
}
|