lobstakit-cloud 1.0.16 → 1.1.0

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.
@@ -0,0 +1,57 @@
1
+ name: Claude Code Review
2
+ on:
3
+ pull_request:
4
+ types: [opened, synchronize, reopened]
5
+
6
+ jobs:
7
+ review:
8
+ runs-on: ubuntu-latest
9
+ permissions:
10
+ contents: read
11
+ pull-requests: write
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v6
15
+ with:
16
+ fetch-depth: 0
17
+
18
+ - uses: anthropics/claude-code-action@v1
19
+ with:
20
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
21
+ track_progress: true
22
+ prompt: |
23
+ REPO: ${{ github.repository }}
24
+ PR NUMBER: ${{ github.event.pull_request.number }}
25
+
26
+ You are a senior code reviewer for LobstaCloud (RedLobsta Cloud hosting platform).
27
+ Review this pull request thoroughly. Focus on:
28
+
29
+ **Security (HIGH PRIORITY — we run a hosting platform):**
30
+ - Auth/token handling (Bearer tokens, encryption, timing-safe comparison)
31
+ - Input validation and sanitization
32
+ - CORS, CSP, and header security
33
+ - Cloud-init / server provisioning safety
34
+ - No secrets in code, logs, or error messages
35
+
36
+ **Code Quality:**
37
+ - Logic errors and edge cases
38
+ - Error handling (fail gracefully, not silently)
39
+ - TypeScript type safety
40
+ - DRY violations and dead code
41
+
42
+ **Performance:**
43
+ - N+1 queries, unnecessary API calls
44
+ - Memory leaks, unbounded loops
45
+
46
+ **Architecture:**
47
+ - Separation of concerns
48
+ - API contract consistency
49
+
50
+ Be direct and specific. Skip praising obvious things.
51
+ Flag blockers as 🚨, suggestions as 💡, nits as 📝.
52
+
53
+ Use inline comments for specific code issues.
54
+ Use a single summary PR comment for overall assessment.
55
+
56
+ claude_args: |
57
+ --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(cat:*),Bash(find:*),Bash(grep:*)"
package/lib/mc-db.js ADDED
@@ -0,0 +1,437 @@
1
+ /**
2
+ * Mission Control — SQLite Database Module
3
+ *
4
+ * Separate database for multi-agent coordination.
5
+ * Uses better-sqlite3 for synchronous, fast access.
6
+ * Auto-creates tables on first access.
7
+ */
8
+
9
+ const Database = require('better-sqlite3');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ const DB_PATH = path.join(os.homedir(), '.lobstakit', 'mission-control.db');
14
+
15
+ let db = null;
16
+
17
+ function getDb() {
18
+ if (db) return db;
19
+
20
+ // Ensure directory exists
21
+ const fs = require('fs');
22
+ const dir = path.dirname(DB_PATH);
23
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
24
+
25
+ db = new Database(DB_PATH);
26
+ db.pragma('journal_mode = WAL');
27
+ db.pragma('foreign_keys = ON');
28
+
29
+ initSchema();
30
+ return db;
31
+ }
32
+
33
+ function initSchema() {
34
+ db.exec(`
35
+ CREATE TABLE IF NOT EXISTS agents (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ name TEXT NOT NULL,
38
+ role TEXT,
39
+ level TEXT DEFAULT 'SPC',
40
+ status TEXT DEFAULT 'idle',
41
+ session_key TEXT,
42
+ soul_summary TEXT,
43
+ avatar_emoji TEXT,
44
+ created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
45
+ updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS tasks (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ title TEXT NOT NULL,
51
+ description TEXT,
52
+ status TEXT DEFAULT 'inbox' CHECK(status IN ('inbox','assigned','in_progress','review','done','blocked')),
53
+ priority INTEGER DEFAULT 0,
54
+ tags TEXT,
55
+ created_by INTEGER REFERENCES agents(id) ON DELETE SET NULL,
56
+ created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
57
+ updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS task_assignees (
61
+ task_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE,
62
+ agent_id INTEGER REFERENCES agents(id) ON DELETE CASCADE,
63
+ PRIMARY KEY(task_id, agent_id)
64
+ );
65
+
66
+ CREATE TABLE IF NOT EXISTS messages (
67
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ task_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE,
69
+ from_agent_id INTEGER REFERENCES agents(id) ON DELETE SET NULL,
70
+ content TEXT NOT NULL,
71
+ created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
72
+ );
73
+
74
+ CREATE TABLE IF NOT EXISTS activities (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ type TEXT NOT NULL,
77
+ agent_id INTEGER REFERENCES agents(id) ON DELETE SET NULL,
78
+ task_id INTEGER REFERENCES tasks(id) ON DELETE SET NULL,
79
+ message TEXT NOT NULL,
80
+ created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
81
+ );
82
+
83
+ CREATE TABLE IF NOT EXISTS notifications (
84
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
85
+ mentioned_agent_id INTEGER REFERENCES agents(id) ON DELETE CASCADE,
86
+ from_agent_id INTEGER REFERENCES agents(id) ON DELETE SET NULL,
87
+ task_id INTEGER REFERENCES tasks(id) ON DELETE SET NULL,
88
+ content TEXT,
89
+ delivered INTEGER DEFAULT 0,
90
+ created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
91
+ );
92
+
93
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
94
+ CREATE INDEX IF NOT EXISTS idx_activities_created ON activities(created_at DESC);
95
+ CREATE INDEX IF NOT EXISTS idx_notifications_agent ON notifications(mentioned_agent_id, delivered);
96
+ CREATE INDEX IF NOT EXISTS idx_messages_task ON messages(task_id);
97
+ `);
98
+ }
99
+
100
+ // ─── Helper: parse tags on read ──────────────────────────────────────────────
101
+
102
+ function parseTags(row) {
103
+ if (!row) return row;
104
+ if (row.tags) {
105
+ try { row.tags = JSON.parse(row.tags); } catch { row.tags = []; }
106
+ }
107
+ return row;
108
+ }
109
+
110
+ function parseTagsArray(rows) {
111
+ return rows.map(parseTags);
112
+ }
113
+
114
+ // ─── Agents CRUD ─────────────────────────────────────────────────────────────
115
+
116
+ function listAgents() {
117
+ return getDb().prepare('SELECT * FROM agents ORDER BY id').all();
118
+ }
119
+
120
+ function getAgent(id) {
121
+ return getDb().prepare('SELECT * FROM agents WHERE id = ?').get(id);
122
+ }
123
+
124
+ function createAgent({ name, role, level, session_key, soul_summary, avatar_emoji }) {
125
+ const result = getDb().prepare(
126
+ `INSERT INTO agents (name, role, level, session_key, soul_summary, avatar_emoji)
127
+ VALUES (?, ?, ?, ?, ?, ?)`
128
+ ).run(name, role || null, level || 'SPC', session_key || null, soul_summary || null, avatar_emoji || null);
129
+ return getAgent(result.lastInsertRowid);
130
+ }
131
+
132
+ function updateAgent(id, fields) {
133
+ const allowed = ['name', 'role', 'level', 'status', 'session_key', 'soul_summary', 'avatar_emoji'];
134
+ const updates = [];
135
+ const values = [];
136
+ for (const key of allowed) {
137
+ if (fields[key] !== undefined) {
138
+ updates.push(`${key} = ?`);
139
+ values.push(fields[key]);
140
+ }
141
+ }
142
+ if (updates.length === 0) return getAgent(id);
143
+ updates.push(`updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')`);
144
+ values.push(id);
145
+ getDb().prepare(`UPDATE agents SET ${updates.join(', ')} WHERE id = ?`).run(...values);
146
+ return getAgent(id);
147
+ }
148
+
149
+ function deleteAgent(id) {
150
+ return getDb().prepare('DELETE FROM agents WHERE id = ?').run(id);
151
+ }
152
+
153
+ // ─── Tasks CRUD ──────────────────────────────────────────────────────────────
154
+
155
+ function listTasks(statusFilter) {
156
+ const d = getDb();
157
+ let rows;
158
+ if (statusFilter) {
159
+ rows = d.prepare('SELECT * FROM tasks WHERE status = ? ORDER BY priority DESC, id DESC').all(statusFilter);
160
+ } else {
161
+ rows = d.prepare('SELECT * FROM tasks ORDER BY priority DESC, id DESC').all();
162
+ }
163
+ // Attach assignees
164
+ const assigneeStmt = d.prepare(
165
+ `SELECT a.* FROM agents a
166
+ JOIN task_assignees ta ON ta.agent_id = a.id
167
+ WHERE ta.task_id = ?`
168
+ );
169
+ for (const row of rows) {
170
+ parseTags(row);
171
+ row.assignees = assigneeStmt.all(row.id);
172
+ }
173
+ return rows;
174
+ }
175
+
176
+ function getTask(id) {
177
+ const d = getDb();
178
+ const row = d.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
179
+ if (!row) return null;
180
+ parseTags(row);
181
+ row.assignees = d.prepare(
182
+ `SELECT a.* FROM agents a
183
+ JOIN task_assignees ta ON ta.agent_id = a.id
184
+ WHERE ta.task_id = ?`
185
+ ).all(id);
186
+ return row;
187
+ }
188
+
189
+ function createTask({ title, description, status, priority, tags, created_by, assignee_ids }) {
190
+ const d = getDb();
191
+ const tagsJson = tags ? JSON.stringify(tags) : null;
192
+ const result = d.prepare(
193
+ `INSERT INTO tasks (title, description, status, priority, tags, created_by)
194
+ VALUES (?, ?, ?, ?, ?, ?)`
195
+ ).run(title, description || null, status || 'inbox', priority || 0, tagsJson, created_by || null);
196
+
197
+ const taskId = result.lastInsertRowid;
198
+
199
+ // Assign agents
200
+ if (assignee_ids && assignee_ids.length > 0) {
201
+ const assignStmt = d.prepare('INSERT OR IGNORE INTO task_assignees (task_id, agent_id) VALUES (?, ?)');
202
+ for (const agentId of assignee_ids) {
203
+ assignStmt.run(taskId, agentId);
204
+ }
205
+ }
206
+
207
+ return getTask(taskId);
208
+ }
209
+
210
+ function updateTask(id, fields) {
211
+ const d = getDb();
212
+ const allowed = ['title', 'description', 'status', 'priority', 'tags'];
213
+ const updates = [];
214
+ const values = [];
215
+ for (const key of allowed) {
216
+ if (fields[key] !== undefined) {
217
+ if (key === 'tags') {
218
+ updates.push(`${key} = ?`);
219
+ values.push(JSON.stringify(fields[key]));
220
+ } else {
221
+ updates.push(`${key} = ?`);
222
+ values.push(fields[key]);
223
+ }
224
+ }
225
+ }
226
+ if (updates.length > 0) {
227
+ updates.push(`updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')`);
228
+ values.push(id);
229
+ d.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ?`).run(...values);
230
+ }
231
+
232
+ // Update assignees if provided
233
+ if (fields.assignee_ids !== undefined) {
234
+ d.prepare('DELETE FROM task_assignees WHERE task_id = ?').run(id);
235
+ if (fields.assignee_ids && fields.assignee_ids.length > 0) {
236
+ const assignStmt = d.prepare('INSERT OR IGNORE INTO task_assignees (task_id, agent_id) VALUES (?, ?)');
237
+ for (const agentId of fields.assignee_ids) {
238
+ assignStmt.run(id, agentId);
239
+ }
240
+ }
241
+ }
242
+
243
+ return getTask(id);
244
+ }
245
+
246
+ function deleteTask(id) {
247
+ return getDb().prepare('DELETE FROM tasks WHERE id = ?').run(id);
248
+ }
249
+
250
+ // ─── Messages ────────────────────────────────────────────────────────────────
251
+
252
+ function getMessages(taskId) {
253
+ return getDb().prepare(
254
+ `SELECT m.*, a.name as from_agent_name, a.avatar_emoji as from_agent_emoji
255
+ FROM messages m
256
+ LEFT JOIN agents a ON a.id = m.from_agent_id
257
+ WHERE m.task_id = ?
258
+ ORDER BY m.created_at ASC`
259
+ ).all(taskId);
260
+ }
261
+
262
+ function createMessage({ task_id, from_agent_id, content }) {
263
+ const result = getDb().prepare(
264
+ 'INSERT INTO messages (task_id, from_agent_id, content) VALUES (?, ?, ?)'
265
+ ).run(task_id, from_agent_id || null, content);
266
+ return getDb().prepare(
267
+ `SELECT m.*, a.name as from_agent_name, a.avatar_emoji as from_agent_emoji
268
+ FROM messages m
269
+ LEFT JOIN agents a ON a.id = m.from_agent_id
270
+ WHERE m.id = ?`
271
+ ).get(result.lastInsertRowid);
272
+ }
273
+
274
+ // ─── Activities ──────────────────────────────────────────────────────────────
275
+
276
+ function getActivities(limit = 50) {
277
+ return getDb().prepare(
278
+ `SELECT act.*, a.name as agent_name, a.avatar_emoji as agent_emoji, t.title as task_title
279
+ FROM activities act
280
+ LEFT JOIN agents a ON a.id = act.agent_id
281
+ LEFT JOIN tasks t ON t.id = act.task_id
282
+ ORDER BY act.created_at DESC
283
+ LIMIT ?`
284
+ ).all(limit);
285
+ }
286
+
287
+ function logActivity({ type, agent_id, task_id, message }) {
288
+ const result = getDb().prepare(
289
+ 'INSERT INTO activities (type, agent_id, task_id, message) VALUES (?, ?, ?, ?)'
290
+ ).run(type, agent_id || null, task_id || null, message);
291
+ return result.lastInsertRowid;
292
+ }
293
+
294
+ // ─── Notifications ───────────────────────────────────────────────────────────
295
+
296
+ function getUnreadNotifications(agentId) {
297
+ return getDb().prepare(
298
+ `SELECT n.*, a.name as from_agent_name, a.avatar_emoji as from_agent_emoji, t.title as task_title
299
+ FROM notifications n
300
+ LEFT JOIN agents a ON a.id = n.from_agent_id
301
+ LEFT JOIN tasks t ON t.id = n.task_id
302
+ WHERE n.mentioned_agent_id = ? AND n.delivered = 0
303
+ ORDER BY n.created_at DESC`
304
+ ).all(agentId);
305
+ }
306
+
307
+ function markNotificationRead(id) {
308
+ return getDb().prepare('UPDATE notifications SET delivered = 1 WHERE id = ?').run(id);
309
+ }
310
+
311
+ function createNotification({ mentioned_agent_id, from_agent_id, task_id, content }) {
312
+ const result = getDb().prepare(
313
+ 'INSERT INTO notifications (mentioned_agent_id, from_agent_id, task_id, content) VALUES (?, ?, ?, ?)'
314
+ ).run(mentioned_agent_id, from_agent_id || null, task_id || null, content);
315
+ return result.lastInsertRowid;
316
+ }
317
+
318
+ // ─── Standup ─────────────────────────────────────────────────────────────────
319
+
320
+ function getStandupData() {
321
+ const d = getDb();
322
+ const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
323
+
324
+ const completedToday = d.prepare(
325
+ `SELECT t.*, GROUP_CONCAT(a.name) as assignee_names
326
+ FROM tasks t
327
+ LEFT JOIN task_assignees ta ON ta.task_id = t.id
328
+ LEFT JOIN agents a ON a.id = ta.agent_id
329
+ WHERE t.status = 'done' AND t.updated_at >= ?
330
+ GROUP BY t.id
331
+ ORDER BY t.updated_at DESC`
332
+ ).all(today + 'T00:00:00Z');
333
+
334
+ const inProgress = d.prepare(
335
+ `SELECT t.*, GROUP_CONCAT(a.name) as assignee_names
336
+ FROM tasks t
337
+ LEFT JOIN task_assignees ta ON ta.task_id = t.id
338
+ LEFT JOIN agents a ON a.id = ta.agent_id
339
+ WHERE t.status = 'in_progress'
340
+ GROUP BY t.id
341
+ ORDER BY t.priority DESC`
342
+ ).all();
343
+
344
+ const blocked = d.prepare(
345
+ `SELECT t.*, GROUP_CONCAT(a.name) as assignee_names
346
+ FROM tasks t
347
+ LEFT JOIN task_assignees ta ON ta.task_id = t.id
348
+ LEFT JOIN agents a ON a.id = ta.agent_id
349
+ WHERE t.status = 'blocked'
350
+ GROUP BY t.id
351
+ ORDER BY t.priority DESC`
352
+ ).all();
353
+
354
+ // Recent activity from today
355
+ const recentActivity = d.prepare(
356
+ `SELECT act.*, a.name as agent_name, a.avatar_emoji as agent_emoji, t.title as task_title
357
+ FROM activities act
358
+ LEFT JOIN agents a ON a.id = act.agent_id
359
+ LEFT JOIN tasks t ON t.id = act.task_id
360
+ WHERE act.created_at >= ?
361
+ ORDER BY act.created_at DESC
362
+ LIMIT 15`
363
+ ).all(today + 'T00:00:00Z');
364
+
365
+ const agentCount = d.prepare('SELECT COUNT(*) as count FROM agents').get().count;
366
+ const activeAgents = d.prepare("SELECT COUNT(*) as count FROM agents WHERE status != 'offline'").get().count;
367
+
368
+ return {
369
+ date: today,
370
+ summary: {
371
+ completed: completedToday.length,
372
+ inProgress: inProgress.length,
373
+ blocked: blocked.length,
374
+ agents: { total: agentCount, active: activeAgents }
375
+ },
376
+ completedToday: parseTagsArray(completedToday),
377
+ inProgress: parseTagsArray(inProgress),
378
+ blocked: parseTagsArray(blocked),
379
+ recentActivity
380
+ };
381
+ }
382
+
383
+ // ─── Seed ────────────────────────────────────────────────────────────────────
384
+
385
+ function seedData() {
386
+ const d = getDb();
387
+ const count = d.prepare('SELECT COUNT(*) as count FROM agents').get().count;
388
+ if (count > 0) return { seeded: false, message: 'Agents already exist, skipping seed' };
389
+
390
+ const coordinator = createAgent({
391
+ name: 'Coordinator',
392
+ role: 'orchestrator',
393
+ level: 'LEAD',
394
+ avatar_emoji: '🦞',
395
+ soul_summary: 'The central coordinator for Mission Control. Manages task assignment and agent communication.'
396
+ });
397
+
398
+ logActivity({
399
+ type: 'system',
400
+ agent_id: coordinator.id,
401
+ message: 'Mission Control initialized. Coordinator agent created.'
402
+ });
403
+
404
+ return { seeded: true, agent: coordinator };
405
+ }
406
+
407
+ // ─── Exports ─────────────────────────────────────────────────────────────────
408
+
409
+ module.exports = {
410
+ getDb,
411
+ // Agents
412
+ listAgents,
413
+ getAgent,
414
+ createAgent,
415
+ updateAgent,
416
+ deleteAgent,
417
+ // Tasks
418
+ listTasks,
419
+ getTask,
420
+ createTask,
421
+ updateTask,
422
+ deleteTask,
423
+ // Messages
424
+ getMessages,
425
+ createMessage,
426
+ // Activities
427
+ getActivities,
428
+ logActivity,
429
+ // Notifications
430
+ getUnreadNotifications,
431
+ markNotificationRead,
432
+ createNotification,
433
+ // Standup
434
+ getStandupData,
435
+ // Seed
436
+ seedData
437
+ };