@johpaz/hive-core 1.0.6 → 1.0.7
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 +5 -2
- package/src/agent/index.ts +145 -17
- package/src/agent/providers/index.ts +20 -1
- package/src/agent/workspace.ts +111 -0
- package/src/channels/base.ts +28 -2
- package/src/channels/discord.ts +58 -10
- package/src/channels/manager.ts +114 -3
- package/src/channels/slack.ts +38 -4
- package/src/channels/telegram.ts +263 -43
- package/src/channels/webchat.ts +22 -0
- package/src/channels/whatsapp.ts +51 -3
- package/src/config/loader.ts +47 -8
- package/src/gateway/server.ts +612 -240
- package/src/gateway/session.ts +2 -1
- package/src/gateway/slash-commands.ts +7 -14
- package/src/memory/notes.ts +28 -130
- package/src/multi-agent/manager.ts +28 -0
- package/src/storage/sqlite.ts +230 -0
- package/src/tools/workspace.ts +171 -0
package/src/gateway/session.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface SessionId {
|
|
|
5
5
|
channel: string;
|
|
6
6
|
kind: "main" | "dm" | "group";
|
|
7
7
|
identifier: string;
|
|
8
|
+
userId?: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function parseSessionId(sessionId: string): SessionId | null {
|
|
@@ -38,7 +39,7 @@ export function parseSessionId(sessionId: string): SessionId | null {
|
|
|
38
39
|
return null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
return { agentId: agentId!, channel, kind, identifier };
|
|
42
|
+
return { agentId: agentId!, channel, kind, identifier, userId: identifier };
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
return null;
|
|
@@ -4,7 +4,7 @@ import { laneQueue } from "./lane-queue.ts";
|
|
|
4
4
|
import { logger } from "../utils/logger.ts";
|
|
5
5
|
|
|
6
6
|
export interface InboundMessage {
|
|
7
|
-
type: "message" | "command" | "ping";
|
|
7
|
+
type: "message" | "command" | "ping" | "join";
|
|
8
8
|
sessionId: string;
|
|
9
9
|
content?: string;
|
|
10
10
|
command?: string;
|
|
@@ -13,11 +13,12 @@ export interface InboundMessage {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface OutboundMessage {
|
|
16
|
-
type: "message" | "stream" | "status" | "error" | "pong" | "command_result";
|
|
16
|
+
type: "message" | "stream" | "status" | "error" | "pong" | "command_result" | "joined" | "typing";
|
|
17
17
|
sessionId: string;
|
|
18
18
|
content?: string;
|
|
19
19
|
chunk?: string;
|
|
20
20
|
isLast?: boolean;
|
|
21
|
+
isTyping?: boolean;
|
|
21
22
|
status?: {
|
|
22
23
|
state: string;
|
|
23
24
|
model?: string;
|
|
@@ -60,23 +61,15 @@ export async function executeSlashCommand(
|
|
|
60
61
|
sessionId: string,
|
|
61
62
|
content: string,
|
|
62
63
|
ws: ServerWebSocket<unknown>
|
|
63
|
-
): Promise<OutboundMessage> {
|
|
64
|
+
): Promise<OutboundMessage | null> {
|
|
64
65
|
const parsed = parseSlashCommand(content);
|
|
65
66
|
if (!parsed) {
|
|
66
|
-
return
|
|
67
|
-
type: "error",
|
|
68
|
-
sessionId,
|
|
69
|
-
error: "Invalid slash command format",
|
|
70
|
-
};
|
|
67
|
+
return null;
|
|
71
68
|
}
|
|
72
69
|
|
|
73
70
|
const command = slashCommands.get(parsed.name);
|
|
74
71
|
if (!command) {
|
|
75
|
-
return
|
|
76
|
-
type: "error",
|
|
77
|
-
sessionId,
|
|
78
|
-
error: `Unknown command: /${parsed.name}`,
|
|
79
|
-
};
|
|
72
|
+
return null;
|
|
80
73
|
}
|
|
81
74
|
|
|
82
75
|
logger.info(`Executing slash command: /${parsed.name}`, { sessionId, args: parsed.args });
|
|
@@ -116,7 +109,7 @@ registerSlashCommand({
|
|
|
116
109
|
handler: async (sessionId) => {
|
|
117
110
|
const session = sessionManager.get(sessionId);
|
|
118
111
|
const queueStatus = laneQueue.getStatus(sessionId);
|
|
119
|
-
|
|
112
|
+
|
|
120
113
|
return {
|
|
121
114
|
sessionId,
|
|
122
115
|
createdAt: session?.createdAt,
|
package/src/memory/notes.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
1
|
import type { Config } from "../config/loader.ts";
|
|
4
2
|
import { logger } from "../utils/logger.ts";
|
|
3
|
+
import { dbService } from "../storage/sqlite.ts";
|
|
5
4
|
|
|
6
5
|
export interface Note {
|
|
7
6
|
title: string;
|
|
@@ -11,157 +10,56 @@ export interface Note {
|
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
export class MemoryStore {
|
|
14
|
-
private notesDir: string;
|
|
15
13
|
private log = logger.child("memory");
|
|
16
14
|
|
|
17
15
|
constructor(config: Config) {
|
|
18
|
-
|
|
19
|
-
config.memory?.notesDir ?? "~/.hive/agents/main/workspace/memory"
|
|
20
|
-
);
|
|
21
|
-
this.ensureDir();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private expandPath(p: string): string {
|
|
25
|
-
if (p.startsWith("~")) {
|
|
26
|
-
return path.join(process.env.HOME ?? "", p.slice(1));
|
|
27
|
-
}
|
|
28
|
-
return p;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
private ensureDir(): void {
|
|
32
|
-
if (!fs.existsSync(this.notesDir)) {
|
|
33
|
-
fs.mkdirSync(this.notesDir, { recursive: true });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private getNotePath(title: string): string {
|
|
38
|
-
const safeTitle = title.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
|
|
39
|
-
return path.join(this.notesDir, `${safeTitle}.md`);
|
|
16
|
+
// Configuration can remain unused as sqlite db handles path resolution
|
|
40
17
|
}
|
|
41
18
|
|
|
42
19
|
write(title: string, content: string): Note {
|
|
43
|
-
const
|
|
44
|
-
const now = new Date();
|
|
45
|
-
|
|
46
|
-
let note: Note;
|
|
47
|
-
|
|
48
|
-
if (fs.existsSync(notePath)) {
|
|
49
|
-
const existing = fs.readFileSync(notePath, "utf-8");
|
|
50
|
-
const lines = existing.split("\n");
|
|
51
|
-
const createdAtLine = lines.find((l) => l.startsWith("created:"));
|
|
52
|
-
const createdAt = createdAtLine
|
|
53
|
-
? new Date(createdAtLine.replace("created:", "").trim())
|
|
54
|
-
: now;
|
|
55
|
-
|
|
56
|
-
note = {
|
|
57
|
-
title,
|
|
58
|
-
content,
|
|
59
|
-
createdAt,
|
|
60
|
-
updatedAt: now,
|
|
61
|
-
};
|
|
62
|
-
} else {
|
|
63
|
-
note = {
|
|
64
|
-
title,
|
|
65
|
-
content,
|
|
66
|
-
createdAt: now,
|
|
67
|
-
updatedAt: now,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const frontmatter = `---
|
|
72
|
-
title: ${title}
|
|
73
|
-
created: ${note.createdAt.toISOString()}
|
|
74
|
-
updated: ${note.updatedAt.toISOString()}
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
${content}`;
|
|
78
|
-
|
|
79
|
-
fs.writeFileSync(notePath, frontmatter, "utf-8");
|
|
20
|
+
const row = dbService.writeNote(title, content);
|
|
80
21
|
this.log.debug(`Wrote note: ${title}`);
|
|
81
|
-
|
|
82
|
-
|
|
22
|
+
return {
|
|
23
|
+
title: row.title,
|
|
24
|
+
content: row.content,
|
|
25
|
+
createdAt: new Date(row.createdAt),
|
|
26
|
+
updatedAt: new Date(row.updatedAt)
|
|
27
|
+
};
|
|
83
28
|
}
|
|
84
29
|
|
|
85
30
|
read(title: string): Note | null {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
if (!fs.existsSync(notePath)) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const content = fs.readFileSync(notePath, "utf-8");
|
|
93
|
-
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
94
|
-
|
|
95
|
-
if (!match) {
|
|
96
|
-
return {
|
|
97
|
-
title,
|
|
98
|
-
content,
|
|
99
|
-
createdAt: new Date(),
|
|
100
|
-
updatedAt: new Date(),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const frontmatter = match[1]!;
|
|
105
|
-
const body = match[2]!.trim();
|
|
106
|
-
|
|
107
|
-
const titleMatch = frontmatter.match(/title:\s*(.+)/);
|
|
108
|
-
const createdMatch = frontmatter.match(/created:\s*(.+)/);
|
|
109
|
-
const updatedMatch = frontmatter.match(/updated:\s*(.+)/);
|
|
110
|
-
|
|
31
|
+
const row = dbService.readNote(title);
|
|
32
|
+
if (!row) return null;
|
|
111
33
|
return {
|
|
112
|
-
title:
|
|
113
|
-
content:
|
|
114
|
-
createdAt:
|
|
115
|
-
updatedAt:
|
|
34
|
+
title: row.title,
|
|
35
|
+
content: row.content,
|
|
36
|
+
createdAt: new Date(row.createdAt),
|
|
37
|
+
updatedAt: new Date(row.updatedAt)
|
|
116
38
|
};
|
|
117
39
|
}
|
|
118
40
|
|
|
119
41
|
list(): Array<{ title: string; path: string }> {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const files = fs.readdirSync(this.notesDir).filter((f) => f.endsWith(".md"));
|
|
125
|
-
|
|
126
|
-
return files.map((file) => {
|
|
127
|
-
const filePath = path.join(this.notesDir, file);
|
|
128
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
129
|
-
const titleMatch = content.match(/title:\s*(.+)/);
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
title: titleMatch?.[1]?.trim() ?? file.replace(".md", ""),
|
|
133
|
-
path: filePath,
|
|
134
|
-
};
|
|
135
|
-
});
|
|
42
|
+
return dbService.listNotes().map(row => ({
|
|
43
|
+
title: row.title,
|
|
44
|
+
path: `sqlite://notes/${row.id}`
|
|
45
|
+
}));
|
|
136
46
|
}
|
|
137
47
|
|
|
138
48
|
delete(title: string): boolean {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
if (fs.existsSync(notePath)) {
|
|
142
|
-
fs.unlinkSync(notePath);
|
|
49
|
+
const deleted = dbService.deleteNote(title);
|
|
50
|
+
if (deleted) {
|
|
143
51
|
this.log.debug(`Deleted note: ${title}`);
|
|
144
|
-
return true;
|
|
145
52
|
}
|
|
146
|
-
|
|
147
|
-
return false;
|
|
53
|
+
return deleted;
|
|
148
54
|
}
|
|
149
55
|
|
|
150
56
|
search(query: string): Note[] {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
note.title.toLowerCase().includes(lowerQuery) ||
|
|
158
|
-
note.content.toLowerCase().includes(lowerQuery)
|
|
159
|
-
)) {
|
|
160
|
-
results.push(note);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return results;
|
|
57
|
+
return dbService.searchNotes(query).map(row => ({
|
|
58
|
+
title: row.title,
|
|
59
|
+
content: row.content,
|
|
60
|
+
createdAt: new Date(row.createdAt),
|
|
61
|
+
updatedAt: new Date(row.updatedAt)
|
|
62
|
+
}));
|
|
165
63
|
}
|
|
166
64
|
}
|
|
167
65
|
|
|
@@ -3,6 +3,12 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { Config, AgentEntry } from "../config/loader.ts";
|
|
4
4
|
import { Agent } from "../agent/index.ts";
|
|
5
5
|
import { ToolRegistry } from "../tools/registry.ts";
|
|
6
|
+
import { readTool, writeTool, editTool } from "../tools/read.ts";
|
|
7
|
+
import { createExecTool } from "../tools/exec.ts";
|
|
8
|
+
import { createWebSearchTool, createWebFetchTool } from "../tools/web.ts";
|
|
9
|
+
import { createNotifyTool } from "../tools/notify.ts";
|
|
10
|
+
import { createWorkspaceTools } from "../tools/workspace.ts";
|
|
11
|
+
import { WorkspaceLoader } from "../agent/workspace.ts";
|
|
6
12
|
import { logger } from "../utils/logger.ts";
|
|
7
13
|
|
|
8
14
|
export interface AgentInstance {
|
|
@@ -66,6 +72,28 @@ export class AgentManager {
|
|
|
66
72
|
|
|
67
73
|
const toolRegistry = new ToolRegistry(this.config);
|
|
68
74
|
|
|
75
|
+
const workspaceLoader = new WorkspaceLoader(workspacePath);
|
|
76
|
+
const workspaceTools = createWorkspaceTools(workspaceLoader);
|
|
77
|
+
for (const tool of workspaceTools) {
|
|
78
|
+
toolRegistry.register(tool);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
toolRegistry.register(readTool);
|
|
82
|
+
toolRegistry.register(writeTool);
|
|
83
|
+
toolRegistry.register(editTool);
|
|
84
|
+
|
|
85
|
+
const execTool = createExecTool(this.config);
|
|
86
|
+
if (execTool) toolRegistry.register(execTool);
|
|
87
|
+
|
|
88
|
+
const webSearchTool = createWebSearchTool(this.config);
|
|
89
|
+
toolRegistry.register(webSearchTool);
|
|
90
|
+
|
|
91
|
+
const webFetchTool = createWebFetchTool(this.config);
|
|
92
|
+
toolRegistry.register(webFetchTool);
|
|
93
|
+
|
|
94
|
+
const notifyTool = createNotifyTool();
|
|
95
|
+
toolRegistry.register(notifyTool);
|
|
96
|
+
|
|
69
97
|
const instance: AgentInstance = {
|
|
70
98
|
id: entry.id,
|
|
71
99
|
agent,
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { logger } from "../utils/logger.ts";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
|
|
6
|
+
export interface ChatMessageRow {
|
|
7
|
+
id: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
userId?: string;
|
|
10
|
+
role: string;
|
|
11
|
+
content: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NoteRow {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
content: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class DatabaseService {
|
|
24
|
+
private db: Database;
|
|
25
|
+
private log = logger.child("sqlite");
|
|
26
|
+
|
|
27
|
+
constructor(dbPath?: string) {
|
|
28
|
+
const defaultPath = path.join(process.env.HOME || "", ".hive", "hive.db");
|
|
29
|
+
const targetPath = dbPath ?? defaultPath;
|
|
30
|
+
|
|
31
|
+
// Ensure directory exists
|
|
32
|
+
const dir = path.dirname(targetPath);
|
|
33
|
+
if (!fs.existsSync(dir)) {
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.db = new Database(targetPath, { create: true });
|
|
38
|
+
this.initSchema();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private initSchema() {
|
|
42
|
+
try {
|
|
43
|
+
this.db.exec(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
45
|
+
id TEXT PRIMARY KEY,
|
|
46
|
+
sessionId TEXT NOT NULL,
|
|
47
|
+
role TEXT NOT NULL,
|
|
48
|
+
content TEXT NOT NULL,
|
|
49
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
50
|
+
);
|
|
51
|
+
`);
|
|
52
|
+
|
|
53
|
+
this.db.exec(`
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_messages_sessionId ON messages(sessionId);
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
this.db.exec(`
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
this.migrateUserId();
|
|
62
|
+
this.initNotesTable();
|
|
63
|
+
this.log.debug("Database schema initialized");
|
|
64
|
+
} catch (e: any) {
|
|
65
|
+
this.log.error(`Failed to initialize database schema: ${e.message}`);
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private migrateUserId(): void {
|
|
71
|
+
try {
|
|
72
|
+
const result = this.db.query("PRAGMA table_info(messages)").all() as Array<{ name: string }>;
|
|
73
|
+
const hasUserId = result.some(col => col.name === "userId");
|
|
74
|
+
if (!hasUserId) {
|
|
75
|
+
this.db.exec("ALTER TABLE messages ADD COLUMN userId TEXT");
|
|
76
|
+
this.log.info("Added userId column to messages table");
|
|
77
|
+
}
|
|
78
|
+
} catch (e: any) {
|
|
79
|
+
this.log.warn(`Migration userId: ${e.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private initNotesTable(): void {
|
|
84
|
+
this.db.exec(`
|
|
85
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
86
|
+
id TEXT PRIMARY KEY,
|
|
87
|
+
title TEXT UNIQUE NOT NULL,
|
|
88
|
+
content TEXT NOT NULL,
|
|
89
|
+
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
90
|
+
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
91
|
+
);
|
|
92
|
+
`);
|
|
93
|
+
|
|
94
|
+
this.db.exec(`
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_notes_title ON notes(title);
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public addMessage(sessionId: string, role: string, content: string, userId?: string): void {
|
|
100
|
+
const stmt = this.db.query(`
|
|
101
|
+
INSERT INTO messages (id, sessionId, userId, role, content)
|
|
102
|
+
VALUES ($id, $sessionId, $userId, $role, $content)
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const id = crypto.randomUUID();
|
|
107
|
+
stmt.run({
|
|
108
|
+
$id: id,
|
|
109
|
+
$sessionId: sessionId,
|
|
110
|
+
$userId: userId ?? null,
|
|
111
|
+
$role: role,
|
|
112
|
+
$content: content
|
|
113
|
+
});
|
|
114
|
+
this.log.debug(`Message added for session ${sessionId}, user ${userId ?? 'unknown'}`);
|
|
115
|
+
} catch (error: any) {
|
|
116
|
+
this.log.error(`Failed to add message: ${error.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public getMessagesByUser(userId: string, limit: number = 100): ChatMessageRow[] {
|
|
121
|
+
const stmt = this.db.query(`
|
|
122
|
+
SELECT * FROM messages
|
|
123
|
+
WHERE userId = $userId
|
|
124
|
+
ORDER BY timestamp DESC
|
|
125
|
+
LIMIT $limit
|
|
126
|
+
`);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const rows = stmt.all({
|
|
130
|
+
$userId: userId,
|
|
131
|
+
$limit: limit
|
|
132
|
+
}) as ChatMessageRow[];
|
|
133
|
+
return rows.reverse();
|
|
134
|
+
} catch (error: any) {
|
|
135
|
+
this.log.error(`Failed to get messages by user: ${error.message}`);
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public getAllUserMessages(userId: string, limit: number = 100): ChatMessageRow[] {
|
|
141
|
+
const stmt = this.db.query(`
|
|
142
|
+
SELECT * FROM messages
|
|
143
|
+
WHERE userId = $userId OR sessionId LIKE $sessionPattern
|
|
144
|
+
ORDER BY timestamp DESC
|
|
145
|
+
LIMIT $limit
|
|
146
|
+
`);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const rows = stmt.all({
|
|
150
|
+
$userId: userId,
|
|
151
|
+
$sessionPattern: `%${userId}`,
|
|
152
|
+
$limit: limit
|
|
153
|
+
}) as ChatMessageRow[];
|
|
154
|
+
return rows.reverse();
|
|
155
|
+
} catch (error: any) {
|
|
156
|
+
this.log.error(`Failed to get all user messages: ${error.message}`);
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public getMessages(sessionId: string, limit: number = 100): ChatMessageRow[] {
|
|
162
|
+
const stmt = this.db.query(`
|
|
163
|
+
SELECT * FROM messages
|
|
164
|
+
WHERE sessionId = $sessionId
|
|
165
|
+
ORDER BY timestamp DESC
|
|
166
|
+
LIMIT $limit
|
|
167
|
+
`);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const rows = stmt.all({
|
|
171
|
+
$sessionId: sessionId,
|
|
172
|
+
$limit: limit
|
|
173
|
+
}) as ChatMessageRow[];
|
|
174
|
+
|
|
175
|
+
// Return in chronological order
|
|
176
|
+
return rows.reverse();
|
|
177
|
+
} catch (error: any) {
|
|
178
|
+
this.log.error(`Failed to get messages: ${error.message}`);
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public close(): void {
|
|
184
|
+
this.db.close();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public writeNote(title: string, content: string): NoteRow {
|
|
188
|
+
const stmt = this.db.query(`
|
|
189
|
+
INSERT INTO notes (id, title, content)
|
|
190
|
+
VALUES ($id, $title, $content)
|
|
191
|
+
ON CONFLICT(title) DO UPDATE SET
|
|
192
|
+
content = excluded.content,
|
|
193
|
+
updatedAt = CURRENT_TIMESTAMP
|
|
194
|
+
RETURNING *
|
|
195
|
+
`);
|
|
196
|
+
return stmt.get({
|
|
197
|
+
$id: crypto.randomUUID(),
|
|
198
|
+
$title: title,
|
|
199
|
+
$content: content
|
|
200
|
+
}) as NoteRow;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public readNote(title: string): NoteRow | null {
|
|
204
|
+
const stmt = this.db.query(`SELECT * FROM notes WHERE title = $title`);
|
|
205
|
+
return stmt.get({ $title: title }) as NoteRow | null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public listNotes(): NoteRow[] {
|
|
209
|
+
const stmt = this.db.query(`SELECT * FROM notes ORDER BY updatedAt DESC`);
|
|
210
|
+
return stmt.all() as NoteRow[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public searchNotes(queryText: string): NoteRow[] {
|
|
214
|
+
const stmt = this.db.query(`
|
|
215
|
+
SELECT * FROM notes
|
|
216
|
+
WHERE title LIKE $query OR content LIKE $query
|
|
217
|
+
ORDER BY updatedAt DESC
|
|
218
|
+
`);
|
|
219
|
+
return stmt.all({ $query: `%${queryText}%` }) as NoteRow[];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public deleteNote(title: string): boolean {
|
|
223
|
+
const stmt = this.db.query(`DELETE FROM notes WHERE title = $title`);
|
|
224
|
+
const result = stmt.run({ $title: title });
|
|
225
|
+
return result.changes > 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Singleton instance
|
|
230
|
+
export const dbService = new DatabaseService();
|