@iletai/nzb 1.7.0 → 1.7.3
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/dist/api/server.js +14 -5
- package/dist/cli.js +1 -0
- package/dist/config.js +12 -3
- package/dist/copilot/client.js +17 -16
- package/dist/copilot/mcp-config.js +2 -0
- package/dist/copilot/orchestrator.js +181 -51
- package/dist/copilot/skills.js +4 -2
- package/dist/copilot/tools.js +33 -5
- package/dist/copilot/types.js +2 -0
- package/dist/daemon.js +11 -10
- package/dist/setup.js +3 -2
- package/dist/store/conversation.js +96 -0
- package/dist/store/db.js +6 -206
- package/dist/store/memory.js +90 -0
- package/dist/store/team-store.js +51 -0
- package/dist/telegram/bot.js +77 -8
- package/dist/telegram/handlers/commands.js +1 -1
- package/dist/telegram/handlers/media.js +63 -6
- package/dist/telegram/handlers/streaming.js +223 -188
- package/dist/telegram/handlers/suggestions.js +22 -1
- package/dist/telegram/log-channel.js +2 -2
- package/dist/telegram/menus.js +243 -99
- package/dist/tui/ansi.js +19 -0
- package/dist/tui/api-client.js +158 -0
- package/dist/tui/debug.js +27 -0
- package/dist/tui/renderer.js +59 -0
- package/dist/tui/stream.js +163 -0
- package/dist/update.js +2 -0
- package/dist/utils.js +102 -0
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -26,6 +26,7 @@ function isProcessAlive(pid) {
|
|
|
26
26
|
return true;
|
|
27
27
|
}
|
|
28
28
|
catch {
|
|
29
|
+
// Expected: process.kill(0) throws when process doesn't exist
|
|
29
30
|
return false;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -64,8 +65,8 @@ function releasePidLock() {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
|
-
catch {
|
|
68
|
-
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.error("[nzb] PID lock cleanup:", err instanceof Error ? err.message : err);
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
async function main() {
|
|
@@ -197,8 +198,8 @@ async function shutdown() {
|
|
|
197
198
|
try {
|
|
198
199
|
await stopBot();
|
|
199
200
|
}
|
|
200
|
-
catch {
|
|
201
|
-
|
|
201
|
+
catch (err) {
|
|
202
|
+
console.error("[nzb] stopBot during shutdown:", err instanceof Error ? err.message : err);
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
205
|
// Destroy all active worker sessions to free memory
|
|
@@ -207,8 +208,8 @@ async function shutdown() {
|
|
|
207
208
|
try {
|
|
208
209
|
await stopClient();
|
|
209
210
|
}
|
|
210
|
-
catch {
|
|
211
|
-
|
|
211
|
+
catch (err) {
|
|
212
|
+
console.error("[nzb] stopClient during shutdown:", err instanceof Error ? err.message : err);
|
|
212
213
|
}
|
|
213
214
|
closeDb();
|
|
214
215
|
releasePidLock();
|
|
@@ -229,8 +230,8 @@ export async function restartDaemon() {
|
|
|
229
230
|
try {
|
|
230
231
|
await stopBot();
|
|
231
232
|
}
|
|
232
|
-
catch {
|
|
233
|
-
|
|
233
|
+
catch (err) {
|
|
234
|
+
console.error("[nzb] stopBot during restart:", err instanceof Error ? err.message : err);
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
// Destroy all active worker sessions to free memory
|
|
@@ -239,8 +240,8 @@ export async function restartDaemon() {
|
|
|
239
240
|
try {
|
|
240
241
|
await stopClient();
|
|
241
242
|
}
|
|
242
|
-
catch {
|
|
243
|
-
|
|
243
|
+
catch (err) {
|
|
244
|
+
console.error("[nzb] stopClient during restart:", err instanceof Error ? err.message : err);
|
|
244
245
|
}
|
|
245
246
|
closeDb();
|
|
246
247
|
releasePidLock();
|
package/dist/setup.js
CHANGED
|
@@ -28,14 +28,15 @@ async function fetchModels() {
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
catch {
|
|
31
|
+
// Expected: Copilot CLI may not be authenticated yet
|
|
31
32
|
return [];
|
|
32
33
|
}
|
|
33
34
|
finally {
|
|
34
35
|
try {
|
|
35
36
|
await client?.stop();
|
|
36
37
|
}
|
|
37
|
-
catch {
|
|
38
|
-
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error("[nzb] CopilotClient stop:", err instanceof Error ? err.message : err);
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { getDb } from "./db.js";
|
|
2
|
+
// Lazy per-connection prepared statement cache
|
|
3
|
+
let cachedDb;
|
|
4
|
+
let stmtCache;
|
|
5
|
+
function ensureStmtCache() {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
if (db !== cachedDb) {
|
|
8
|
+
cachedDb = db;
|
|
9
|
+
stmtCache = {
|
|
10
|
+
logConversation: db.prepare(`INSERT INTO conversation_log (role, content, source, telegram_msg_id) VALUES (?, ?, ?, ?)`),
|
|
11
|
+
pruneConversation: db.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 200)`),
|
|
12
|
+
getConversationByMsgId: db.prepare(`SELECT id FROM conversation_log WHERE telegram_msg_id = ? LIMIT 1`),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return stmtCache;
|
|
16
|
+
}
|
|
17
|
+
/** Log a conversation turn (user, assistant, or system) with optional Telegram message ID. Returns the row ID. */
|
|
18
|
+
export function logConversation(role, content, source, telegramMsgId) {
|
|
19
|
+
const cache = ensureStmtCache();
|
|
20
|
+
const result = cache.logConversation.run(role, content, source, telegramMsgId ?? null);
|
|
21
|
+
// Prune every ~50 inserts using rowid (crash-safe, no in-memory counter)
|
|
22
|
+
const rowId = result.lastInsertRowid;
|
|
23
|
+
if (rowId % 50 === 0) {
|
|
24
|
+
try {
|
|
25
|
+
cache.pruneConversation.run();
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.error("[nzb] Conversation prune failed:", err instanceof Error ? err.message : err);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return rowId;
|
|
32
|
+
}
|
|
33
|
+
/** Get conversation context around a Telegram message ID (±4 rows using proper subquery). */
|
|
34
|
+
export function getConversationContext(telegramMsgId) {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
const cache = ensureStmtCache();
|
|
37
|
+
const row = cache.getConversationByMsgId.get(telegramMsgId);
|
|
38
|
+
if (!row)
|
|
39
|
+
return undefined;
|
|
40
|
+
// Fetch 4 rows before + the target + 4 rows after (handles ID gaps from pruning)
|
|
41
|
+
const rows = db
|
|
42
|
+
.prepare(`
|
|
43
|
+
SELECT role, content, source, ts FROM (
|
|
44
|
+
SELECT * FROM conversation_log WHERE id < ? ORDER BY id DESC LIMIT 4
|
|
45
|
+
)
|
|
46
|
+
UNION ALL
|
|
47
|
+
SELECT role, content, source, ts FROM conversation_log WHERE id = ?
|
|
48
|
+
UNION ALL
|
|
49
|
+
SELECT role, content, source, ts FROM (
|
|
50
|
+
SELECT * FROM conversation_log WHERE id > ? ORDER BY id ASC LIMIT 4
|
|
51
|
+
)
|
|
52
|
+
`)
|
|
53
|
+
.all(row.id, row.id, row.id);
|
|
54
|
+
if (rows.length === 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
return rows
|
|
57
|
+
.map((r) => {
|
|
58
|
+
const tag = r.role === "user" ? "You" : r.role === "assistant" ? "NZB" : "System";
|
|
59
|
+
const content = r.content.length > 400 ? r.content.slice(0, 400) + "…" : r.content;
|
|
60
|
+
return `${tag}: ${content}`;
|
|
61
|
+
})
|
|
62
|
+
.join("\n");
|
|
63
|
+
}
|
|
64
|
+
/** Set Telegram message ID on a specific conversation_log row (race-free). */
|
|
65
|
+
export function setConversationTelegramMsgId(rowId, telegramMsgId) {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
db.prepare(`UPDATE conversation_log SET telegram_msg_id = ? WHERE id = ?`).run(telegramMsgId, rowId);
|
|
68
|
+
}
|
|
69
|
+
/** Look up conversation content by Telegram message ID. Returns the message content or undefined. */
|
|
70
|
+
export function getConversationByTelegramMsgId(telegramMsgId) {
|
|
71
|
+
const db = getDb();
|
|
72
|
+
const row = db
|
|
73
|
+
.prepare(`SELECT content FROM conversation_log WHERE telegram_msg_id = ? LIMIT 1`)
|
|
74
|
+
.get(telegramMsgId);
|
|
75
|
+
return row?.content;
|
|
76
|
+
}
|
|
77
|
+
/** Get recent conversation history formatted for injection into system message. */
|
|
78
|
+
export function getRecentConversation(limit = 20) {
|
|
79
|
+
const db = getDb();
|
|
80
|
+
const rows = db
|
|
81
|
+
.prepare(`SELECT role, content, source, ts FROM conversation_log ORDER BY id DESC LIMIT ?`)
|
|
82
|
+
.all(limit);
|
|
83
|
+
if (rows.length === 0)
|
|
84
|
+
return "";
|
|
85
|
+
// Reverse so oldest is first (chronological order)
|
|
86
|
+
rows.reverse();
|
|
87
|
+
return rows
|
|
88
|
+
.map((r) => {
|
|
89
|
+
const tag = r.role === "user" ? `[${r.source}] User` : r.role === "system" ? `[${r.source}] System` : "NZB";
|
|
90
|
+
// Truncate long messages to keep context manageable
|
|
91
|
+
const content = r.content.length > 500 ? r.content.slice(0, 500) + "…" : r.content;
|
|
92
|
+
return `${tag}: ${content}`;
|
|
93
|
+
})
|
|
94
|
+
.join("\n\n");
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=conversation.js.map
|
package/dist/store/db.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
import { DB_PATH, ensureNZBHome } from "../paths.js";
|
|
3
3
|
let db;
|
|
4
|
-
|
|
5
|
-
// Cached prepared statements for hot-path queries (created lazily after DB init)
|
|
4
|
+
// Cached prepared statements for state operations (created lazily after DB init)
|
|
6
5
|
let stmtCache;
|
|
7
6
|
export function getDb() {
|
|
8
7
|
if (!db) {
|
|
@@ -115,17 +114,11 @@ export function getDb() {
|
|
|
115
114
|
// FTS5 may not be available — will fall back to LIKE
|
|
116
115
|
console.log("[nzb] FTS5 not available, using LIKE fallback for memory search");
|
|
117
116
|
}
|
|
118
|
-
// Initialize cached prepared statements for
|
|
117
|
+
// Initialize cached prepared statements for state operations
|
|
119
118
|
stmtCache = {
|
|
120
119
|
getState: db.prepare(`SELECT value FROM nzb_state WHERE key = ?`),
|
|
121
120
|
setState: db.prepare(`INSERT OR REPLACE INTO nzb_state (key, value) VALUES (?, ?)`),
|
|
122
121
|
deleteState: db.prepare(`DELETE FROM nzb_state WHERE key = ?`),
|
|
123
|
-
logConversation: db.prepare(`INSERT INTO conversation_log (role, content, source, telegram_msg_id) VALUES (?, ?, ?, ?)`),
|
|
124
|
-
pruneConversation: db.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 200)`),
|
|
125
|
-
addMemory: db.prepare(`INSERT INTO memories (category, content, source) VALUES (?, ?, ?)`),
|
|
126
|
-
removeMemory: db.prepare(`DELETE FROM memories WHERE id = ?`),
|
|
127
|
-
memorySummary: db.prepare(`SELECT id, category, content FROM memories ORDER BY category, last_accessed DESC`),
|
|
128
|
-
getConversationByMsgId: db.prepare(`SELECT id FROM conversation_log WHERE telegram_msg_id = ? LIMIT 1`),
|
|
129
122
|
};
|
|
130
123
|
}
|
|
131
124
|
return db;
|
|
@@ -144,203 +137,6 @@ export function deleteState(key) {
|
|
|
144
137
|
getDb(); // ensure init
|
|
145
138
|
stmtCache.deleteState.run(key);
|
|
146
139
|
}
|
|
147
|
-
/** Log a conversation turn (user, assistant, or system) with optional Telegram message ID. Returns the row ID. */
|
|
148
|
-
export function logConversation(role, content, source, telegramMsgId) {
|
|
149
|
-
getDb(); // ensure init
|
|
150
|
-
const result = stmtCache.logConversation.run(role, content, source, telegramMsgId ?? null);
|
|
151
|
-
// Keep last 200 entries to support context recovery after session loss
|
|
152
|
-
logInsertCount++;
|
|
153
|
-
if (logInsertCount % 50 === 0) {
|
|
154
|
-
stmtCache.pruneConversation.run();
|
|
155
|
-
}
|
|
156
|
-
return result.lastInsertRowid;
|
|
157
|
-
}
|
|
158
|
-
/** Get conversation context around a Telegram message ID (±4 rows using proper subquery). */
|
|
159
|
-
export function getConversationContext(telegramMsgId) {
|
|
160
|
-
const db = getDb();
|
|
161
|
-
const row = stmtCache.getConversationByMsgId.get(telegramMsgId);
|
|
162
|
-
if (!row)
|
|
163
|
-
return undefined;
|
|
164
|
-
// Fetch 4 rows before + the target + 4 rows after (handles ID gaps from pruning)
|
|
165
|
-
const rows = db
|
|
166
|
-
.prepare(`
|
|
167
|
-
SELECT role, content, source, ts FROM (
|
|
168
|
-
SELECT * FROM conversation_log WHERE id < ? ORDER BY id DESC LIMIT 4
|
|
169
|
-
)
|
|
170
|
-
UNION ALL
|
|
171
|
-
SELECT role, content, source, ts FROM conversation_log WHERE id = ?
|
|
172
|
-
UNION ALL
|
|
173
|
-
SELECT role, content, source, ts FROM (
|
|
174
|
-
SELECT * FROM conversation_log WHERE id > ? ORDER BY id ASC LIMIT 4
|
|
175
|
-
)
|
|
176
|
-
`)
|
|
177
|
-
.all(row.id, row.id, row.id);
|
|
178
|
-
if (rows.length === 0)
|
|
179
|
-
return undefined;
|
|
180
|
-
return rows
|
|
181
|
-
.map((r) => {
|
|
182
|
-
const tag = r.role === "user" ? "You" : r.role === "assistant" ? "NZB" : "System";
|
|
183
|
-
const content = r.content.length > 400 ? r.content.slice(0, 400) + "…" : r.content;
|
|
184
|
-
return `${tag}: ${content}`;
|
|
185
|
-
})
|
|
186
|
-
.join("\n");
|
|
187
|
-
}
|
|
188
|
-
/** Set Telegram message ID on a specific conversation_log row (race-free). */
|
|
189
|
-
export function setConversationTelegramMsgId(rowId, telegramMsgId) {
|
|
190
|
-
const db = getDb();
|
|
191
|
-
db.prepare(`UPDATE conversation_log SET telegram_msg_id = ? WHERE id = ?`).run(telegramMsgId, rowId);
|
|
192
|
-
}
|
|
193
|
-
/** Look up conversation content by Telegram message ID. Returns the message content or undefined. */
|
|
194
|
-
export function getConversationByTelegramMsgId(telegramMsgId) {
|
|
195
|
-
const db = getDb();
|
|
196
|
-
const row = db
|
|
197
|
-
.prepare(`SELECT content FROM conversation_log WHERE telegram_msg_id = ? LIMIT 1`)
|
|
198
|
-
.get(telegramMsgId);
|
|
199
|
-
return row?.content;
|
|
200
|
-
}
|
|
201
|
-
/** Get recent conversation history formatted for injection into system message. */
|
|
202
|
-
export function getRecentConversation(limit = 20) {
|
|
203
|
-
const db = getDb();
|
|
204
|
-
const rows = db
|
|
205
|
-
.prepare(`SELECT role, content, source, ts FROM conversation_log ORDER BY id DESC LIMIT ?`)
|
|
206
|
-
.all(limit);
|
|
207
|
-
if (rows.length === 0)
|
|
208
|
-
return "";
|
|
209
|
-
// Reverse so oldest is first (chronological order)
|
|
210
|
-
rows.reverse();
|
|
211
|
-
return rows
|
|
212
|
-
.map((r) => {
|
|
213
|
-
const tag = r.role === "user" ? `[${r.source}] User` : r.role === "system" ? `[${r.source}] System` : "NZB";
|
|
214
|
-
// Truncate long messages to keep context manageable
|
|
215
|
-
const content = r.content.length > 500 ? r.content.slice(0, 500) + "…" : r.content;
|
|
216
|
-
return `${tag}: ${content}`;
|
|
217
|
-
})
|
|
218
|
-
.join("\n\n");
|
|
219
|
-
}
|
|
220
|
-
/** Add a memory to long-term storage. */
|
|
221
|
-
export function addMemory(category, content, source = "user") {
|
|
222
|
-
getDb(); // ensure init
|
|
223
|
-
const result = stmtCache.addMemory.run(category, content, source);
|
|
224
|
-
return result.lastInsertRowid;
|
|
225
|
-
}
|
|
226
|
-
/** Search memories by keyword and/or category. Uses FTS5 when available, falls back to LIKE. */
|
|
227
|
-
export function searchMemories(keyword, category, limit = 20) {
|
|
228
|
-
const db = getDb();
|
|
229
|
-
// Try FTS5 first for keyword search (much faster than LIKE)
|
|
230
|
-
if (keyword) {
|
|
231
|
-
try {
|
|
232
|
-
const catFilter = category ? `AND m.category = ?` : "";
|
|
233
|
-
const params = [keyword + "*"];
|
|
234
|
-
if (category)
|
|
235
|
-
params.push(category);
|
|
236
|
-
params.push(limit);
|
|
237
|
-
const rows = db
|
|
238
|
-
.prepare(`SELECT m.id, m.category, m.content, m.source, m.created_at
|
|
239
|
-
FROM memories_fts f
|
|
240
|
-
JOIN memories m ON f.rowid = m.id
|
|
241
|
-
WHERE memories_fts MATCH ? ${catFilter}
|
|
242
|
-
ORDER BY rank LIMIT ?`)
|
|
243
|
-
.all(...params);
|
|
244
|
-
return rows;
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
// FTS5 not available — fall through to LIKE
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// Fallback: LIKE-based search
|
|
251
|
-
const conditions = [];
|
|
252
|
-
const params = [];
|
|
253
|
-
if (keyword) {
|
|
254
|
-
conditions.push(`content LIKE ?`);
|
|
255
|
-
params.push(`%${keyword}%`);
|
|
256
|
-
}
|
|
257
|
-
if (category) {
|
|
258
|
-
conditions.push(`category = ?`);
|
|
259
|
-
params.push(category);
|
|
260
|
-
}
|
|
261
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
262
|
-
params.push(limit);
|
|
263
|
-
const rows = db
|
|
264
|
-
.prepare(`SELECT id, category, content, source, created_at FROM memories ${where} ORDER BY last_accessed DESC LIMIT ?`)
|
|
265
|
-
.all(...params);
|
|
266
|
-
// Update last_accessed only when explicitly requested, not on every search
|
|
267
|
-
// (removed automatic last_accessed update to avoid write side effects on reads)
|
|
268
|
-
return rows;
|
|
269
|
-
}
|
|
270
|
-
/** Remove a memory by ID. */
|
|
271
|
-
export function removeMemory(id) {
|
|
272
|
-
getDb(); // ensure init
|
|
273
|
-
const result = stmtCache.removeMemory.run(id);
|
|
274
|
-
return result.changes > 0;
|
|
275
|
-
}
|
|
276
|
-
/** Get a compact summary of all memories for injection into system message. */
|
|
277
|
-
export function getMemorySummary() {
|
|
278
|
-
getDb(); // ensure init
|
|
279
|
-
const rows = stmtCache.memorySummary.all();
|
|
280
|
-
if (rows.length === 0)
|
|
281
|
-
return "";
|
|
282
|
-
// Group by category
|
|
283
|
-
const grouped = {};
|
|
284
|
-
for (const r of rows) {
|
|
285
|
-
if (!grouped[r.category])
|
|
286
|
-
grouped[r.category] = [];
|
|
287
|
-
grouped[r.category].push({ id: r.id, content: r.content });
|
|
288
|
-
}
|
|
289
|
-
const sections = Object.entries(grouped).map(([cat, items]) => {
|
|
290
|
-
const lines = items.map((i) => ` - [#${i.id}] ${i.content}`).join("\n");
|
|
291
|
-
return `**${cat}**:\n${lines}`;
|
|
292
|
-
});
|
|
293
|
-
return sections.join("\n");
|
|
294
|
-
}
|
|
295
|
-
// ── Agent Teams CRUD ──────────────────────────────────────────
|
|
296
|
-
export function createTeam(id, taskDescription, originChannel) {
|
|
297
|
-
const db = getDb();
|
|
298
|
-
db.prepare(`INSERT INTO agent_teams (id, task_description, origin_channel) VALUES (?, ?, ?)`).run(id, taskDescription, originChannel ?? null);
|
|
299
|
-
}
|
|
300
|
-
export function addTeamMember(teamId, workerName, role) {
|
|
301
|
-
const db = getDb();
|
|
302
|
-
db.prepare(`INSERT INTO team_members (team_id, worker_name, role, status) VALUES (?, ?, ?, 'pending')`).run(teamId, workerName, role);
|
|
303
|
-
db.prepare(`UPDATE agent_teams SET member_count = member_count + 1 WHERE id = ?`).run(teamId);
|
|
304
|
-
}
|
|
305
|
-
export function updateTeamMemberResult(teamId, workerName, result, status) {
|
|
306
|
-
const db = getDb();
|
|
307
|
-
db.prepare(`UPDATE team_members SET result = ?, status = ?, completed_at = CURRENT_TIMESTAMP WHERE team_id = ? AND worker_name = ?`).run(result, status, teamId, workerName);
|
|
308
|
-
if (status === "completed" || status === "error") {
|
|
309
|
-
db.prepare(`UPDATE agent_teams SET completed_count = completed_count + 1 WHERE id = ?`).run(teamId);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
export function getTeam(id) {
|
|
313
|
-
const db = getDb();
|
|
314
|
-
return db.prepare(`SELECT * FROM agent_teams WHERE id = ?`).get(id);
|
|
315
|
-
}
|
|
316
|
-
export function getTeamMembers(teamId) {
|
|
317
|
-
const db = getDb();
|
|
318
|
-
return db
|
|
319
|
-
.prepare(`SELECT worker_name, role, status, result FROM team_members WHERE team_id = ? ORDER BY id`)
|
|
320
|
-
.all(teamId);
|
|
321
|
-
}
|
|
322
|
-
export function completeTeam(teamId, aggregatedResult, status = "completed") {
|
|
323
|
-
const db = getDb();
|
|
324
|
-
db.prepare(`UPDATE agent_teams SET status = ?, aggregated_result = ?, completed_at = CURRENT_TIMESTAMP WHERE id = ?`).run(status, aggregatedResult, teamId);
|
|
325
|
-
}
|
|
326
|
-
export function getActiveTeams() {
|
|
327
|
-
const db = getDb();
|
|
328
|
-
return db
|
|
329
|
-
.prepare(`SELECT id, status, task_description, member_count, completed_count, created_at FROM agent_teams WHERE status = 'active' ORDER BY created_at DESC`)
|
|
330
|
-
.all();
|
|
331
|
-
}
|
|
332
|
-
export function getTeamByWorkerName(workerName) {
|
|
333
|
-
const db = getDb();
|
|
334
|
-
const row = db
|
|
335
|
-
.prepare(`SELECT team_id FROM team_members WHERE worker_name = ? AND status IN ('pending', 'running') LIMIT 1`)
|
|
336
|
-
.get(workerName);
|
|
337
|
-
return row?.team_id;
|
|
338
|
-
}
|
|
339
|
-
export function cleanupTeam(teamId) {
|
|
340
|
-
const db = getDb();
|
|
341
|
-
db.prepare(`DELETE FROM team_members WHERE team_id = ?`).run(teamId);
|
|
342
|
-
db.prepare(`DELETE FROM agent_teams WHERE id = ?`).run(teamId);
|
|
343
|
-
}
|
|
344
140
|
export function closeDb() {
|
|
345
141
|
if (db) {
|
|
346
142
|
stmtCache = undefined;
|
|
@@ -348,4 +144,8 @@ export function closeDb() {
|
|
|
348
144
|
db = undefined;
|
|
349
145
|
}
|
|
350
146
|
}
|
|
147
|
+
// Re-export for backward compatibility
|
|
148
|
+
export { logConversation, getConversationContext, setConversationTelegramMsgId, getConversationByTelegramMsgId, getRecentConversation, } from "./conversation.js";
|
|
149
|
+
export { addMemory, searchMemories, removeMemory, getMemorySummary } from "./memory.js";
|
|
150
|
+
export { createTeam, addTeamMember, updateTeamMemberResult, getTeam, getTeamMembers, completeTeam, getActiveTeams, getTeamByWorkerName, cleanupTeam, } from "./team-store.js";
|
|
351
151
|
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getDb } from "./db.js";
|
|
2
|
+
// Lazy per-connection prepared statement cache
|
|
3
|
+
let cachedDb;
|
|
4
|
+
let stmtCache;
|
|
5
|
+
function ensureStmtCache() {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
if (db !== cachedDb) {
|
|
8
|
+
cachedDb = db;
|
|
9
|
+
stmtCache = {
|
|
10
|
+
addMemory: db.prepare(`INSERT INTO memories (category, content, source) VALUES (?, ?, ?)`),
|
|
11
|
+
removeMemory: db.prepare(`DELETE FROM memories WHERE id = ?`),
|
|
12
|
+
memorySummary: db.prepare(`SELECT id, category, content FROM memories ORDER BY category, last_accessed DESC`),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return stmtCache;
|
|
16
|
+
}
|
|
17
|
+
/** Add a memory to long-term storage. */
|
|
18
|
+
export function addMemory(category, content, source = "user") {
|
|
19
|
+
const cache = ensureStmtCache();
|
|
20
|
+
const result = cache.addMemory.run(category, content, source);
|
|
21
|
+
return result.lastInsertRowid;
|
|
22
|
+
}
|
|
23
|
+
/** Search memories by keyword and/or category. Uses FTS5 when available, falls back to LIKE. */
|
|
24
|
+
export function searchMemories(keyword, category, limit = 20) {
|
|
25
|
+
const db = getDb();
|
|
26
|
+
// Try FTS5 first for keyword search (much faster than LIKE)
|
|
27
|
+
if (keyword) {
|
|
28
|
+
try {
|
|
29
|
+
const catFilter = category ? `AND m.category = ?` : "";
|
|
30
|
+
const params = [keyword + "*"];
|
|
31
|
+
if (category)
|
|
32
|
+
params.push(category);
|
|
33
|
+
params.push(limit);
|
|
34
|
+
const rows = db
|
|
35
|
+
.prepare(`SELECT m.id, m.category, m.content, m.source, m.created_at
|
|
36
|
+
FROM memories_fts f
|
|
37
|
+
JOIN memories m ON f.rowid = m.id
|
|
38
|
+
WHERE memories_fts MATCH ? ${catFilter}
|
|
39
|
+
ORDER BY rank LIMIT ?`)
|
|
40
|
+
.all(...params);
|
|
41
|
+
return rows;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// FTS5 not available — fall through to LIKE
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Fallback: LIKE-based search
|
|
48
|
+
const conditions = [];
|
|
49
|
+
const params = [];
|
|
50
|
+
if (keyword) {
|
|
51
|
+
conditions.push(`content LIKE ?`);
|
|
52
|
+
params.push(`%${keyword}%`);
|
|
53
|
+
}
|
|
54
|
+
if (category) {
|
|
55
|
+
conditions.push(`category = ?`);
|
|
56
|
+
params.push(category);
|
|
57
|
+
}
|
|
58
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
59
|
+
params.push(limit);
|
|
60
|
+
const rows = db
|
|
61
|
+
.prepare(`SELECT id, category, content, source, created_at FROM memories ${where} ORDER BY last_accessed DESC LIMIT ?`)
|
|
62
|
+
.all(...params);
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
65
|
+
/** Remove a memory by ID. */
|
|
66
|
+
export function removeMemory(id) {
|
|
67
|
+
const cache = ensureStmtCache();
|
|
68
|
+
const result = cache.removeMemory.run(id);
|
|
69
|
+
return result.changes > 0;
|
|
70
|
+
}
|
|
71
|
+
/** Get a compact summary of all memories for injection into system message. */
|
|
72
|
+
export function getMemorySummary() {
|
|
73
|
+
const cache = ensureStmtCache();
|
|
74
|
+
const rows = cache.memorySummary.all();
|
|
75
|
+
if (rows.length === 0)
|
|
76
|
+
return "";
|
|
77
|
+
// Group by category
|
|
78
|
+
const grouped = {};
|
|
79
|
+
for (const r of rows) {
|
|
80
|
+
if (!grouped[r.category])
|
|
81
|
+
grouped[r.category] = [];
|
|
82
|
+
grouped[r.category].push({ id: r.id, content: r.content });
|
|
83
|
+
}
|
|
84
|
+
const sections = Object.entries(grouped).map(([cat, items]) => {
|
|
85
|
+
const lines = items.map((i) => ` - [#${i.id}] ${i.content}`).join("\n");
|
|
86
|
+
return `**${cat}**:\n${lines}`;
|
|
87
|
+
});
|
|
88
|
+
return sections.join("\n");
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getDb } from "./db.js";
|
|
2
|
+
// ── Agent Teams CRUD ──────────────────────────────────────────
|
|
3
|
+
export function createTeam(id, taskDescription, originChannel) {
|
|
4
|
+
const db = getDb();
|
|
5
|
+
db.prepare(`INSERT INTO agent_teams (id, task_description, origin_channel) VALUES (?, ?, ?)`).run(id, taskDescription, originChannel ?? null);
|
|
6
|
+
}
|
|
7
|
+
export function addTeamMember(teamId, workerName, role) {
|
|
8
|
+
const db = getDb();
|
|
9
|
+
db.prepare(`INSERT INTO team_members (team_id, worker_name, role, status) VALUES (?, ?, ?, 'pending')`).run(teamId, workerName, role);
|
|
10
|
+
db.prepare(`UPDATE agent_teams SET member_count = member_count + 1 WHERE id = ?`).run(teamId);
|
|
11
|
+
}
|
|
12
|
+
export function updateTeamMemberResult(teamId, workerName, result, status) {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
db.prepare(`UPDATE team_members SET result = ?, status = ?, completed_at = CURRENT_TIMESTAMP WHERE team_id = ? AND worker_name = ?`).run(result, status, teamId, workerName);
|
|
15
|
+
if (status === "completed" || status === "error") {
|
|
16
|
+
db.prepare(`UPDATE agent_teams SET completed_count = completed_count + 1 WHERE id = ?`).run(teamId);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function getTeam(id) {
|
|
20
|
+
const db = getDb();
|
|
21
|
+
return db.prepare(`SELECT * FROM agent_teams WHERE id = ?`).get(id);
|
|
22
|
+
}
|
|
23
|
+
export function getTeamMembers(teamId) {
|
|
24
|
+
const db = getDb();
|
|
25
|
+
return db
|
|
26
|
+
.prepare(`SELECT worker_name, role, status, result FROM team_members WHERE team_id = ? ORDER BY id`)
|
|
27
|
+
.all(teamId);
|
|
28
|
+
}
|
|
29
|
+
export function completeTeam(teamId, aggregatedResult, status = "completed") {
|
|
30
|
+
const db = getDb();
|
|
31
|
+
db.prepare(`UPDATE agent_teams SET status = ?, aggregated_result = ?, completed_at = CURRENT_TIMESTAMP WHERE id = ?`).run(status, aggregatedResult, teamId);
|
|
32
|
+
}
|
|
33
|
+
export function getActiveTeams() {
|
|
34
|
+
const db = getDb();
|
|
35
|
+
return db
|
|
36
|
+
.prepare(`SELECT id, status, task_description, member_count, completed_count, created_at FROM agent_teams WHERE status = 'active' ORDER BY created_at DESC`)
|
|
37
|
+
.all();
|
|
38
|
+
}
|
|
39
|
+
export function getTeamByWorkerName(workerName) {
|
|
40
|
+
const db = getDb();
|
|
41
|
+
const row = db
|
|
42
|
+
.prepare(`SELECT team_id FROM team_members WHERE worker_name = ? AND status IN ('pending', 'running') LIMIT 1`)
|
|
43
|
+
.get(workerName);
|
|
44
|
+
return row?.team_id;
|
|
45
|
+
}
|
|
46
|
+
export function cleanupTeam(teamId) {
|
|
47
|
+
const db = getDb();
|
|
48
|
+
db.prepare(`DELETE FROM team_members WHERE team_id = ?`).run(teamId);
|
|
49
|
+
db.prepare(`DELETE FROM agent_teams WHERE id = ?`).run(teamId);
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=team-store.js.map
|