niahere 0.2.40 → 0.2.42

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.40",
3
+ "version": "0.2.42",
4
4
  "description": "A personal AI assistant daemon — scheduled jobs, chat across Telegram and Slack, persona system, and visual identity.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -4,7 +4,7 @@ import { log } from "../utils/log";
4
4
  import { createTelegramChannel } from "./telegram";
5
5
  import { createSlackChannel } from "./slack";
6
6
 
7
- export { getChannel } from "./registry";
7
+ export { getChannel, getStarted } from "./registry";
8
8
 
9
9
  /** Register all built-in channel factories. Call once at startup. */
10
10
  export function registerAllChannels(): void {
@@ -19,6 +19,10 @@ export function getChannel(name: string): Channel | undefined {
19
19
  return started.get(name);
20
20
  }
21
21
 
22
+ export function getStarted(): Channel[] {
23
+ return [...started.values()];
24
+ }
25
+
22
26
  export function clearStarted(): void {
23
27
  started.clear();
24
28
  }
package/src/chat/repl.ts CHANGED
@@ -2,7 +2,7 @@ import * as readline from "readline";
2
2
  import { createChatEngine } from "./engine";
3
3
  import { runMigrations } from "../db/migrate";
4
4
  import { closeDb } from "../db/connection";
5
- import { getMcpServers, setMcpServers } from "../mcp";
5
+ import { getMcpServers, setMcpFactory } from "../mcp";
6
6
  import { createNiaMcpServer } from "../mcp/server";
7
7
  import { Session } from "../db/models";
8
8
  import { relativeTime } from "../utils/format";
@@ -113,12 +113,9 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
113
113
  process.exit(1);
114
114
  }
115
115
 
116
- // Initialize MCP server if not already set (standalone chat mode)
116
+ // Initialize MCP server factory if not already set (standalone chat mode)
117
117
  if (!getMcpServers()) {
118
- try {
119
- const mcpConfig = createNiaMcpServer();
120
- setMcpServers({ nia: mcpConfig });
121
- } catch {}
118
+ setMcpFactory(() => ({ nia: createNiaMcpServer() }));
122
119
  }
123
120
 
124
121
  // Determine session to use
package/src/cli/index.ts CHANGED
@@ -381,12 +381,16 @@ switch (command) {
381
381
  case "channels": {
382
382
  const sub = process.argv[3];
383
383
  const { updateRawConfig } = await import("../utils/config");
384
- if (sub === "on") {
385
- updateRawConfig({ channels: { enabled: true } });
386
- console.log("channels enabled restart to apply");
387
- } else if (sub === "off") {
388
- updateRawConfig({ channels: { enabled: false } });
389
- console.log("channels disabled — restart to apply");
384
+ if (sub === "on" || sub === "off") {
385
+ const enabled = sub === "on";
386
+ updateRawConfig({ channels: { enabled } });
387
+ const pid = readPid();
388
+ if (pid && isRunning()) {
389
+ process.kill(pid, "SIGHUP");
390
+ console.log(`channels ${enabled ? "enabled" : "disabled"}`);
391
+ } else {
392
+ console.log(`channels ${enabled ? "enabled" : "disabled"} — start nia to apply`);
393
+ }
390
394
  } else {
391
395
  console.log(`channels: ${getConfig().channels.enabled ? "on" : "off"}`);
392
396
  }
@@ -1,17 +1,17 @@
1
1
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2
2
  import { dirname } from "path";
3
3
  import { getPaths } from "../utils/paths";
4
- import { getConfig } from "../utils/config";
4
+ import { getConfig, resetConfig } from "../utils/config";
5
5
  import { log } from "../utils/log";
6
6
  import { ActiveEngine } from "../db/models";
7
7
  import { runMigrations } from "../db/migrate";
8
8
  import { closeDb, getSql } from "../db/connection";
9
- import { registerAllChannels, startChannels, stopChannels } from "../channels";
9
+ import { registerAllChannels, startChannels, stopChannels, getStarted } from "../channels";
10
10
  import type { Channel } from "../types";
11
11
  import { startScheduler, stopScheduler, recomputeAllNextRuns } from "./scheduler";
12
12
  import { startAlive, stopAlive } from "./alive";
13
13
  import { createNiaMcpServer } from "../mcp/server";
14
- import { setMcpServers } from "../mcp";
14
+ import { setMcpFactory } from "../mcp";
15
15
 
16
16
  export function writePid(pid: number): void {
17
17
  const { pid: pidPath } = getPaths();
@@ -218,14 +218,9 @@ export async function runDaemon(): Promise<void> {
218
218
  log.info({ recovered }, "recovered stale running jobs");
219
219
  }
220
220
 
221
- // Initialize MCP server (in-process, no HTTP needed)
222
- try {
223
- const mcpConfig = createNiaMcpServer();
224
- setMcpServers({ nia: mcpConfig });
225
- log.info("MCP server initialized");
226
- } catch (err) {
227
- log.error({ err }, "failed to initialize MCP server");
228
- }
221
+ // Initialize MCP server factory (each query gets its own Protocol instance)
222
+ setMcpFactory(() => ({ nia: createNiaMcpServer() }));
223
+ log.info("MCP server factory initialized");
229
224
 
230
225
  // Register and start channels
231
226
  registerAllChannels();
@@ -265,9 +260,26 @@ export async function runDaemon(): Promise<void> {
265
260
  log.warn({ err }, "could not subscribe to nia_jobs, falling back to SIGHUP only");
266
261
  }
267
262
 
268
- // SIGHUP as manual fallback
263
+ // SIGHUP: reload config, reconcile channels, recompute jobs
269
264
  process.on("SIGHUP", async () => {
270
- log.info("received SIGHUP, recomputing job schedules");
265
+ log.info("received SIGHUP, reloading config");
266
+ resetConfig();
267
+ const fresh = getConfig();
268
+
269
+ const running = getStarted();
270
+ const wantChannels = fresh.channels.enabled;
271
+ const haveChannels = running.length > 0;
272
+
273
+ if (wantChannels && !haveChannels) {
274
+ log.info("SIGHUP: starting channels");
275
+ const result = await startChannels();
276
+ channels = result.started;
277
+ } else if (!wantChannels && haveChannels) {
278
+ log.info("SIGHUP: stopping channels");
279
+ await stopChannels(running);
280
+ channels = [];
281
+ }
282
+
271
283
  await recomputeAllNextRuns().catch(() => {});
272
284
  });
273
285
 
@@ -1,5 +1,5 @@
1
1
  import { getSql } from "../connection";
2
- import type { SaveMessageParams, RoomStats, RecentMessage } from "../../types";
2
+ import type { SaveMessageParams, RoomStats, RecentMessage, SearchResult, SessionMessage } from "../../types";
3
3
 
4
4
  export async function save(params: SaveMessageParams): Promise<void> {
5
5
  const sql = getSql();
@@ -29,6 +29,45 @@ export async function getRecent(limit = 20, room?: string): Promise<RecentMessag
29
29
  }));
30
30
  }
31
31
 
32
+ export async function search(query: string, limit = 20, room?: string): Promise<SearchResult[]> {
33
+ const sql = getSql();
34
+ const pattern = `%${query}%`;
35
+ const rows = room
36
+ ? await sql`
37
+ SELECT session_id, room, sender, content, created_at
38
+ FROM messages WHERE content ILIKE ${pattern} AND room = ${room}
39
+ ORDER BY created_at DESC LIMIT ${limit}
40
+ `
41
+ : await sql`
42
+ SELECT session_id, room, sender, content, created_at
43
+ FROM messages WHERE content ILIKE ${pattern}
44
+ ORDER BY created_at DESC LIMIT ${limit}
45
+ `;
46
+ return rows.map((r) => ({
47
+ sessionId: r.session_id,
48
+ room: r.room,
49
+ sender: r.sender,
50
+ content: r.content,
51
+ createdAt: String(r.created_at),
52
+ }));
53
+ }
54
+
55
+ export async function getBySession(sessionId: string): Promise<SessionMessage[]> {
56
+ const sql = getSql();
57
+ const rows = await sql`
58
+ SELECT room, sender, content, is_from_agent, created_at
59
+ FROM messages WHERE session_id = ${sessionId}
60
+ ORDER BY created_at ASC
61
+ `;
62
+ return rows.map((r) => ({
63
+ room: r.room,
64
+ sender: r.sender,
65
+ content: r.content,
66
+ isFromAgent: r.is_from_agent,
67
+ createdAt: String(r.created_at),
68
+ }));
69
+ }
70
+
32
71
  export async function getRoomStats(): Promise<RoomStats[]> {
33
72
  const sql = getSql();
34
73
  const rows = await sql`
@@ -49,6 +49,35 @@ export async function getRecent(room: string, limit = 10): Promise<SessionSummar
49
49
  }));
50
50
  }
51
51
 
52
+ export async function listRecent(limit = 10, room?: string): Promise<SessionSummary[]> {
53
+ if (room) return getRecent(room, limit);
54
+ const sql = getSql();
55
+ const rows = await sql`
56
+ SELECT
57
+ s.id,
58
+ s.room,
59
+ s.created_at,
60
+ s.updated_at,
61
+ (
62
+ SELECT content FROM messages m
63
+ WHERE m.session_id = s.id AND m.sender = 'user'
64
+ ORDER BY m.created_at ASC LIMIT 1
65
+ ) AS preview,
66
+ (SELECT COUNT(*)::int FROM messages m WHERE m.session_id = s.id) AS message_count
67
+ FROM sessions s
68
+ ORDER BY s.updated_at DESC
69
+ LIMIT ${limit}
70
+ `;
71
+ return rows.map((r) => ({
72
+ id: r.id,
73
+ room: r.room,
74
+ createdAt: String(r.created_at),
75
+ updatedAt: String(r.updated_at),
76
+ preview: r.preview ? String(r.preview) : null,
77
+ messageCount: r.message_count,
78
+ }));
79
+ }
80
+
52
81
  export async function create(id: string, room: string): Promise<void> {
53
82
  const sql = getSql();
54
83
  await sql`INSERT INTO sessions (id, room) VALUES (${id}, ${room})`;
package/src/mcp/index.ts CHANGED
@@ -1,10 +1,10 @@
1
- /** Shared MCP server config set by daemon, read by chat engine creators. */
2
- let _mcpServers: Record<string, unknown> | null = null;
1
+ /** Factory for per-query MCP serverseach query gets its own Protocol instance. */
2
+ let _mcpFactory: (() => Record<string, unknown>) | null = null;
3
3
 
4
- export function setMcpServers(servers: Record<string, unknown>): void {
5
- _mcpServers = servers;
4
+ export function setMcpFactory(factory: () => Record<string, unknown>): void {
5
+ _mcpFactory = factory;
6
6
  }
7
7
 
8
8
  export function getMcpServers(): Record<string, unknown> | undefined {
9
- return _mcpServers ?? undefined;
9
+ return _mcpFactory?.() ?? undefined;
10
10
  }
package/src/mcp/server.ts CHANGED
@@ -99,6 +99,39 @@ export function createNiaMcpServer() {
99
99
  content: [{ type: "text" as const, text: await handlers.listMessages(args.limit, args.room) }],
100
100
  }),
101
101
  ),
102
+ tool(
103
+ "list_sessions",
104
+ "Browse past conversation sessions with previews. Returns session IDs you can pass to read_session.",
105
+ {
106
+ room: z.string().optional().describe("Filter by room name"),
107
+ limit: z.number().default(10).describe("Number of sessions to return"),
108
+ },
109
+ async (args) => ({
110
+ content: [{ type: "text" as const, text: await handlers.listSessions(args.limit, args.room) }],
111
+ }),
112
+ ),
113
+ tool(
114
+ "search_messages",
115
+ "Search across all past messages by keyword. Returns matching messages with session IDs for deeper reading.",
116
+ {
117
+ query: z.string().describe("Text to search for in message content"),
118
+ room: z.string().optional().describe("Filter by room name"),
119
+ limit: z.number().default(20).describe("Max results to return"),
120
+ },
121
+ async (args) => ({
122
+ content: [{ type: "text" as const, text: await handlers.searchMessages(args.query, args.limit, args.room) }],
123
+ }),
124
+ ),
125
+ tool(
126
+ "read_session",
127
+ "Load the full transcript of a specific conversation session. Use list_sessions or search_messages to find session IDs.",
128
+ {
129
+ session_id: z.string().describe("Session ID to read"),
130
+ },
131
+ async (args) => ({
132
+ content: [{ type: "text" as const, text: await handlers.readSession(args.session_id) }],
133
+ }),
134
+ ),
102
135
  tool(
103
136
  "add_watch_channel",
104
137
  "Add or update a Slack watch channel. Watch channels receive ALL messages (not just @mentions) and act based on the behavior prompt. Takes effect on next message (hot-reloads).",
package/src/mcp/tools.ts CHANGED
@@ -244,6 +244,24 @@ export async function listMessages(limit = 20, room?: string): Promise<string> {
244
244
  return JSON.stringify(messages, null, 2);
245
245
  }
246
246
 
247
+ export async function listSessions(limit = 10, room?: string): Promise<string> {
248
+ const sessions = await Session.listRecent(limit, room);
249
+ if (sessions.length === 0) return "No sessions found.";
250
+ return JSON.stringify(sessions, null, 2);
251
+ }
252
+
253
+ export async function searchMessages(query: string, limit = 20, room?: string): Promise<string> {
254
+ const results = await Message.search(query, limit, room);
255
+ if (results.length === 0) return "No matching messages found.";
256
+ return JSON.stringify(results, null, 2);
257
+ }
258
+
259
+ export async function readSession(sessionId: string): Promise<string> {
260
+ const messages = await Message.getBySession(sessionId);
261
+ if (messages.length === 0) return "Session not found or has no messages.";
262
+ return JSON.stringify(messages, null, 2);
263
+ }
264
+
247
265
  export function addRule(rule: string): string {
248
266
  const { selfDir } = getPaths();
249
267
  const rulesPath = join(selfDir, "rules.md");
@@ -31,6 +31,9 @@ You have MCP tools for managing jobs directly (preferred over CLI for speed):
31
31
  - **run_job** — trigger a job to run immediately
32
32
  - **send_message** — send a message to the user (via telegram, slack, or default channel). Supports `media_path` to send images/files.
33
33
  - **list_messages** — read recent chat history
34
+ - **list_sessions** — browse past conversation sessions with previews and message counts. Returns session IDs.
35
+ - **search_messages** — keyword search across all past messages. Find when something was discussed.
36
+ - **read_session** — load the full transcript of a specific session by ID.
34
37
  - **add_watch_channel** — add a Slack channel for proactive monitoring. Specify channel key (`channel_id#name`) and behavior prompt. Hot-reloads.
35
38
  - **remove_watch_channel** — stop watching a Slack channel. Hot-reloads.
36
39
  - **enable_watch_channel** / **disable_watch_channel** — toggle a watch channel on/off without removing it. Hot-reloads.
@@ -73,6 +76,16 @@ Config reference:
73
76
  - `channels.slack.watch` — per-channel proactive monitoring. Keys are `channel_id#channel_name` format.
74
77
  {{slackWatch}}
75
78
 
79
+ ## Conversation History
80
+
81
+ You have access to all prior conversations stored in the database:
82
+
83
+ - **list_sessions** — browse past sessions (with previews and message counts). Use to find a conversation.
84
+ - **search_messages** — search across all past messages by keyword. Returns session IDs for deeper reading.
85
+ - **read_session** — load the full transcript of a session by ID.
86
+
87
+ Use these when the user asks "did we talk about...", "what did I say about...", or when you need context from a prior conversation. Combine with `read_memory` for a complete picture.
88
+
76
89
  ## Persona & Memory
77
90
 
78
91
  Your persona files live in {{selfDir}}/:
@@ -7,5 +7,5 @@ export type { Channel, ChannelFactory } from "./channel";
7
7
  export type { ChatState } from "./chat-state";
8
8
  export type { Config, ChannelsConfig, TelegramConfig, SlackConfig } from "./config";
9
9
  export type { Paths } from "./paths";
10
- export type { SaveMessageParams, RoomStats, RecentMessage } from "./message";
10
+ export type { SaveMessageParams, RoomStats, RecentMessage, SearchResult, SessionMessage } from "./message";
11
11
  export type { AgentInfo } from "./agent";
@@ -19,3 +19,19 @@ export interface RecentMessage {
19
19
  content: string;
20
20
  createdAt: string;
21
21
  }
22
+
23
+ export interface SearchResult {
24
+ sessionId: string;
25
+ room: string;
26
+ sender: string;
27
+ content: string;
28
+ createdAt: string;
29
+ }
30
+
31
+ export interface SessionMessage {
32
+ room: string;
33
+ sender: string;
34
+ content: string;
35
+ isFromAgent: boolean;
36
+ createdAt: string;
37
+ }