@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.
@@ -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,
@@ -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
- this.notesDir = this.expandPath(
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 notePath = this.getNotePath(title);
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
- return note;
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 notePath = this.getNotePath(title);
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: titleMatch?.[1]?.trim() ?? title,
113
- content: body,
114
- createdAt: createdMatch ? new Date(createdMatch[1]!) : new Date(),
115
- updatedAt: updatedMatch ? new Date(updatedMatch[1]!) : new Date(),
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
- if (!fs.existsSync(this.notesDir)) {
121
- return [];
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 notePath = this.getNotePath(title);
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
- const results: Note[] = [];
152
- const lowerQuery = query.toLowerCase();
153
-
154
- for (const { title } of this.list()) {
155
- const note = this.read(title);
156
- if (note && (
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();