olly-molly 0.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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +182 -0
  3. package/app/api/agent/execute/route.ts +157 -0
  4. package/app/api/agent/status/route.ts +38 -0
  5. package/app/api/check-api-key/route.ts +12 -0
  6. package/app/api/conversations/[id]/route.ts +35 -0
  7. package/app/api/conversations/route.ts +24 -0
  8. package/app/api/members/[id]/route.ts +37 -0
  9. package/app/api/members/route.ts +12 -0
  10. package/app/api/pm/breakdown/route.ts +142 -0
  11. package/app/api/pm/tickets/route.ts +147 -0
  12. package/app/api/projects/[id]/route.ts +56 -0
  13. package/app/api/projects/active/route.ts +15 -0
  14. package/app/api/projects/route.ts +53 -0
  15. package/app/api/tickets/[id]/logs/route.ts +16 -0
  16. package/app/api/tickets/[id]/route.ts +60 -0
  17. package/app/api/tickets/[id]/work-logs/route.ts +16 -0
  18. package/app/api/tickets/route.ts +37 -0
  19. package/app/design-system/page.tsx +242 -0
  20. package/app/favicon.ico +0 -0
  21. package/app/globals.css +318 -0
  22. package/app/layout.tsx +37 -0
  23. package/app/page.tsx +331 -0
  24. package/bin/cli.js +66 -0
  25. package/components/ThemeProvider.tsx +56 -0
  26. package/components/ThemeToggle.tsx +31 -0
  27. package/components/activity/ActivityLog.tsx +96 -0
  28. package/components/activity/index.ts +1 -0
  29. package/components/kanban/ConversationList.tsx +75 -0
  30. package/components/kanban/ConversationView.tsx +132 -0
  31. package/components/kanban/KanbanBoard.tsx +179 -0
  32. package/components/kanban/KanbanColumn.tsx +80 -0
  33. package/components/kanban/SortableTicket.tsx +58 -0
  34. package/components/kanban/TicketCard.tsx +98 -0
  35. package/components/kanban/TicketModal.tsx +510 -0
  36. package/components/kanban/TicketSidebar.tsx +448 -0
  37. package/components/kanban/index.ts +8 -0
  38. package/components/pm/PMRequestModal.tsx +196 -0
  39. package/components/pm/index.ts +1 -0
  40. package/components/project/ProjectSelector.tsx +211 -0
  41. package/components/project/index.ts +1 -0
  42. package/components/team/MemberCard.tsx +147 -0
  43. package/components/team/TeamPanel.tsx +57 -0
  44. package/components/team/index.ts +2 -0
  45. package/components/ui/ApiKeyModal.tsx +101 -0
  46. package/components/ui/Avatar.tsx +95 -0
  47. package/components/ui/Badge.tsx +59 -0
  48. package/components/ui/Button.tsx +60 -0
  49. package/components/ui/Card.tsx +64 -0
  50. package/components/ui/Input.tsx +41 -0
  51. package/components/ui/Modal.tsx +76 -0
  52. package/components/ui/ResizablePane.tsx +97 -0
  53. package/components/ui/Select.tsx +45 -0
  54. package/components/ui/Textarea.tsx +41 -0
  55. package/components/ui/index.ts +8 -0
  56. package/db/dev.sqlite +0 -0
  57. package/db/dev.sqlite-shm +0 -0
  58. package/db/dev.sqlite-wal +0 -0
  59. package/db/schema-conversations.sql +26 -0
  60. package/db/schema-projects.sql +29 -0
  61. package/db/schema.sql +94 -0
  62. package/lib/agent-jobs.ts +232 -0
  63. package/lib/db.ts +564 -0
  64. package/next.config.ts +10 -0
  65. package/package.json +80 -0
  66. package/postcss.config.mjs +7 -0
  67. package/public/app-icon.png +0 -0
  68. package/public/file.svg +1 -0
  69. package/public/globe.svg +1 -0
  70. package/public/next.svg +1 -0
  71. package/public/profiles/designer.png +0 -0
  72. package/public/profiles/dev-backend.png +0 -0
  73. package/public/profiles/dev-frontend.png +0 -0
  74. package/public/profiles/pm.png +0 -0
  75. package/public/profiles/qa.png +0 -0
  76. package/public/vercel.svg +1 -0
  77. package/public/window.svg +1 -0
  78. package/tsconfig.json +34 -0
package/lib/db.ts ADDED
@@ -0,0 +1,564 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+
6
+ // Database file path
7
+ const DB_PATH = path.join(process.cwd(), 'db', 'dev.sqlite');
8
+
9
+ // Ensure db directory exists
10
+ const dbDir = path.dirname(DB_PATH);
11
+ if (!fs.existsSync(dbDir)) {
12
+ fs.mkdirSync(dbDir, { recursive: true });
13
+ }
14
+
15
+ // Create database connection
16
+ const db = new Database(DB_PATH);
17
+ db.pragma('journal_mode = WAL');
18
+
19
+ // Initialize database schema
20
+ function initializeDatabase() {
21
+ const schemaPath = path.join(process.cwd(), 'db', 'schema.sql');
22
+ const schema = fs.readFileSync(schemaPath, 'utf-8');
23
+ db.exec(schema);
24
+
25
+ // Load projects schema if exists
26
+ const projectsSchemaPath = path.join(process.cwd(), 'db', 'schema-projects.sql');
27
+ if (fs.existsSync(projectsSchemaPath)) {
28
+ const projectsSchema = fs.readFileSync(projectsSchemaPath, 'utf-8');
29
+ db.exec(projectsSchema);
30
+ }
31
+
32
+ // Load conversations schema if exists
33
+ const conversationsSchemaPath = path.join(process.cwd(), 'db', 'schema-conversations.sql');
34
+ if (fs.existsSync(conversationsSchemaPath)) {
35
+ const conversationsSchema = fs.readFileSync(conversationsSchemaPath, 'utf-8');
36
+ db.exec(conversationsSchema);
37
+ }
38
+ }
39
+
40
+ // Initialize on first load
41
+ initializeDatabase();
42
+
43
+ // Types
44
+ export interface Member {
45
+ id: string;
46
+ role: 'PM' | 'FE_DEV' | 'BACKEND_DEV' | 'QA' | 'DEVOPS';
47
+ name: string;
48
+ avatar: string | null;
49
+ system_prompt: string;
50
+ created_at: string;
51
+ updated_at: string;
52
+ }
53
+
54
+ export interface Ticket {
55
+ id: string;
56
+ title: string;
57
+ description: string | null;
58
+ status: 'TODO' | 'IN_PROGRESS' | 'IN_REVIEW' | 'NEED_FIX' | 'COMPLETE' | 'ON_HOLD';
59
+ priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
60
+ assignee_id: string | null;
61
+ project_id: string | null;
62
+ created_by: string | null;
63
+ created_at: string;
64
+ updated_at: string;
65
+ assignee?: Member;
66
+ }
67
+
68
+ export interface ActivityLog {
69
+ id: string;
70
+ ticket_id: string;
71
+ member_id: string | null;
72
+ action: string;
73
+ old_value: string | null;
74
+ new_value: string | null;
75
+ details: string | null;
76
+ created_at: string;
77
+ member?: Member;
78
+ }
79
+
80
+ export interface Project {
81
+ id: string;
82
+ name: string;
83
+ path: string;
84
+ description: string | null;
85
+ is_active: number;
86
+ created_at: string;
87
+ updated_at: string;
88
+ }
89
+
90
+ export interface AgentWorkLog {
91
+ id: string;
92
+ ticket_id: string;
93
+ agent_id: string;
94
+ project_id: string;
95
+ command: string;
96
+ prompt: string | null;
97
+ output: string | null;
98
+ status: 'RUNNING' | 'SUCCESS' | 'FAILED' | 'CANCELLED';
99
+ git_commit_hash: string | null;
100
+ started_at: string;
101
+ completed_at: string | null;
102
+ duration_ms: number | null;
103
+ }
104
+
105
+ export interface Conversation {
106
+ id: string;
107
+ ticket_id: string;
108
+ agent_id: string;
109
+ provider: 'claude' | 'opencode';
110
+ prompt: string | null;
111
+ feedback: string | null;
112
+ status: 'running' | 'completed' | 'failed' | 'cancelled';
113
+ git_commit_hash: string | null;
114
+ started_at: string;
115
+ completed_at: string | null;
116
+ created_at: string;
117
+ agent?: Member;
118
+ }
119
+
120
+ export interface ConversationMessage {
121
+ id: string;
122
+ conversation_id: string;
123
+ content: string;
124
+ message_type: 'log' | 'error' | 'success' | 'system';
125
+ created_at: string;
126
+ }
127
+
128
+ // Member operations
129
+ export const memberService = {
130
+ getAll(): Member[] {
131
+ return db.prepare('SELECT * FROM members ORDER BY role').all() as Member[];
132
+ },
133
+
134
+ getById(id: string): Member | undefined {
135
+ return db.prepare('SELECT * FROM members WHERE id = ?').get(id) as Member | undefined;
136
+ },
137
+
138
+ getByRole(role: string): Member | undefined {
139
+ return db.prepare('SELECT * FROM members WHERE role = ?').get(role) as Member | undefined;
140
+ },
141
+
142
+ updateSystemPrompt(id: string, systemPrompt: string): Member | undefined {
143
+ db.prepare(`
144
+ UPDATE members
145
+ SET system_prompt = ?, updated_at = CURRENT_TIMESTAMP
146
+ WHERE id = ?
147
+ `).run(systemPrompt, id);
148
+ return this.getById(id);
149
+ },
150
+
151
+ update(id: string, data: Partial<Pick<Member, 'name' | 'avatar' | 'system_prompt'>>): Member | undefined {
152
+ const updates: string[] = [];
153
+ const values: (string | null)[] = [];
154
+
155
+ if (data.name !== undefined) {
156
+ updates.push('name = ?');
157
+ values.push(data.name);
158
+ }
159
+ if (data.avatar !== undefined) {
160
+ updates.push('avatar = ?');
161
+ values.push(data.avatar);
162
+ }
163
+ if (data.system_prompt !== undefined) {
164
+ updates.push('system_prompt = ?');
165
+ values.push(data.system_prompt);
166
+ }
167
+
168
+ if (updates.length > 0) {
169
+ updates.push('updated_at = CURRENT_TIMESTAMP');
170
+ values.push(id);
171
+ db.prepare(`UPDATE members SET ${updates.join(', ')} WHERE id = ?`).run(...values);
172
+ }
173
+
174
+ return this.getById(id);
175
+ }
176
+ };
177
+
178
+ // Ticket operations
179
+ export const ticketService = {
180
+ getAll(status?: string, projectId?: string): Ticket[] {
181
+ let query = `
182
+ SELECT t.*, m.name as assignee_name, m.avatar as assignee_avatar, m.role as assignee_role
183
+ FROM tickets t
184
+ LEFT JOIN members m ON t.assignee_id = m.id
185
+ `;
186
+ const conditions: string[] = [];
187
+ const params: (string | null)[] = [];
188
+
189
+ if (status) {
190
+ conditions.push('t.status = ?');
191
+ params.push(status);
192
+ }
193
+
194
+ if (projectId) {
195
+ conditions.push('t.project_id = ?');
196
+ params.push(projectId);
197
+ }
198
+
199
+ if (conditions.length > 0) {
200
+ query += ' WHERE ' + conditions.join(' AND ');
201
+ }
202
+
203
+ query += ' ORDER BY t.created_at DESC';
204
+ const tickets = db.prepare(query).all(...params) as (Ticket & { assignee_name?: string; assignee_avatar?: string; assignee_role?: string })[];
205
+ return tickets.map(t => ({
206
+ ...t,
207
+ assignee: t.assignee_id ? {
208
+ id: t.assignee_id,
209
+ name: t.assignee_name!,
210
+ avatar: t.assignee_avatar!,
211
+ role: t.assignee_role as Member['role']
212
+ } as Member : undefined
213
+ }));
214
+ },
215
+
216
+ getById(id: string): Ticket | undefined {
217
+ const ticket = db.prepare(`
218
+ SELECT t.*, m.name as assignee_name, m.avatar as assignee_avatar, m.role as assignee_role
219
+ FROM tickets t
220
+ LEFT JOIN members m ON t.assignee_id = m.id
221
+ WHERE t.id = ?
222
+ `).get(id) as (Ticket & { assignee_name?: string; assignee_avatar?: string; assignee_role?: string }) | undefined;
223
+
224
+ if (!ticket) return undefined;
225
+
226
+ return {
227
+ ...ticket,
228
+ assignee: ticket.assignee_id ? {
229
+ id: ticket.assignee_id,
230
+ name: ticket.assignee_name!,
231
+ avatar: ticket.assignee_avatar!,
232
+ role: ticket.assignee_role as Member['role']
233
+ } as Member : undefined
234
+ };
235
+ },
236
+
237
+ create(data: { title: string; description?: string; priority?: Ticket['priority']; assignee_id?: string; project_id?: string; created_by?: string }): Ticket {
238
+ const id = uuidv4();
239
+ db.prepare(`
240
+ INSERT INTO tickets (id, title, description, priority, assignee_id, project_id, created_by)
241
+ VALUES (?, ?, ?, ?, ?, ?, ?)
242
+ `).run(id, data.title, data.description || null, data.priority || 'MEDIUM', data.assignee_id || null, data.project_id || null, data.created_by || null);
243
+
244
+ // Log creation
245
+ activityService.log({
246
+ ticket_id: id,
247
+ member_id: data.created_by || null,
248
+ action: 'CREATED',
249
+ new_value: data.title,
250
+ details: `Ticket "${data.title}" was created`
251
+ });
252
+
253
+ return this.getById(id)!;
254
+ },
255
+
256
+ update(id: string, data: Partial<Pick<Ticket, 'title' | 'description' | 'status' | 'priority' | 'assignee_id'>>, updatedBy?: string): Ticket | undefined {
257
+ const current = this.getById(id);
258
+ if (!current) return undefined;
259
+
260
+ const updates: string[] = [];
261
+ const values: (string | null)[] = [];
262
+
263
+ if (data.title !== undefined) {
264
+ updates.push('title = ?');
265
+ values.push(data.title);
266
+ }
267
+ if (data.description !== undefined) {
268
+ updates.push('description = ?');
269
+ values.push(data.description);
270
+ }
271
+ if (data.status !== undefined && data.status !== current.status) {
272
+ updates.push('status = ?');
273
+ values.push(data.status);
274
+ activityService.log({
275
+ ticket_id: id,
276
+ member_id: updatedBy || null,
277
+ action: 'STATUS_CHANGED',
278
+ old_value: current.status,
279
+ new_value: data.status,
280
+ details: `Status changed from ${current.status} to ${data.status}`
281
+ });
282
+ }
283
+ if (data.priority !== undefined && data.priority !== current.priority) {
284
+ updates.push('priority = ?');
285
+ values.push(data.priority);
286
+ activityService.log({
287
+ ticket_id: id,
288
+ member_id: updatedBy || null,
289
+ action: 'PRIORITY_CHANGED',
290
+ old_value: current.priority,
291
+ new_value: data.priority,
292
+ details: `Priority changed from ${current.priority} to ${data.priority}`
293
+ });
294
+ }
295
+ if (data.assignee_id !== undefined && data.assignee_id !== current.assignee_id) {
296
+ updates.push('assignee_id = ?');
297
+ values.push(data.assignee_id);
298
+ const newAssignee = data.assignee_id ? memberService.getById(data.assignee_id) : null;
299
+ const oldAssignee = current.assignee_id ? memberService.getById(current.assignee_id) : null;
300
+ activityService.log({
301
+ ticket_id: id,
302
+ member_id: updatedBy || null,
303
+ action: 'ASSIGNED',
304
+ old_value: oldAssignee?.name || null,
305
+ new_value: newAssignee?.name || null,
306
+ details: newAssignee
307
+ ? `Assigned to ${newAssignee.name}`
308
+ : 'Unassigned'
309
+ });
310
+ }
311
+
312
+ if (updates.length > 0) {
313
+ updates.push('updated_at = CURRENT_TIMESTAMP');
314
+ values.push(id);
315
+ db.prepare(`UPDATE tickets SET ${updates.join(', ')} WHERE id = ?`).run(...values);
316
+ }
317
+
318
+ return this.getById(id);
319
+ },
320
+
321
+ delete(id: string): boolean {
322
+ const result = db.prepare('DELETE FROM tickets WHERE id = ?').run(id);
323
+ return result.changes > 0;
324
+ }
325
+ };
326
+
327
+ // Activity log operations
328
+ export const activityService = {
329
+ log(data: { ticket_id: string; member_id: string | null; action: string; old_value?: string | null; new_value?: string | null; details?: string | null }): ActivityLog {
330
+ const id = uuidv4();
331
+ db.prepare(`
332
+ INSERT INTO activity_logs (id, ticket_id, member_id, action, old_value, new_value, details)
333
+ VALUES (?, ?, ?, ?, ?, ?, ?)
334
+ `).run(id, data.ticket_id, data.member_id, data.action, data.old_value || null, data.new_value || null, data.details || null);
335
+ return this.getById(id)!;
336
+ },
337
+
338
+ getById(id: string): ActivityLog | undefined {
339
+ return db.prepare(`
340
+ SELECT al.*, m.name as member_name, m.avatar as member_avatar
341
+ FROM activity_logs al
342
+ LEFT JOIN members m ON al.member_id = m.id
343
+ WHERE al.id = ?
344
+ `).get(id) as ActivityLog | undefined;
345
+ },
346
+
347
+ getByTicketId(ticketId: string): ActivityLog[] {
348
+ const logs = db.prepare(`
349
+ SELECT al.*, m.name as member_name, m.avatar as member_avatar, m.role as member_role
350
+ FROM activity_logs al
351
+ LEFT JOIN members m ON al.member_id = m.id
352
+ WHERE al.ticket_id = ?
353
+ ORDER BY al.created_at DESC
354
+ `).all(ticketId) as (ActivityLog & { member_name?: string; member_avatar?: string; member_role?: string })[];
355
+
356
+ return logs.map(log => ({
357
+ ...log,
358
+ member: log.member_id ? {
359
+ id: log.member_id,
360
+ name: log.member_name!,
361
+ avatar: log.member_avatar!,
362
+ role: log.member_role as Member['role']
363
+ } as Member : undefined
364
+ }));
365
+ }
366
+ };
367
+
368
+ // Project operations
369
+ export const projectService = {
370
+ getAll(): Project[] {
371
+ return db.prepare('SELECT * FROM projects ORDER BY is_active DESC, name ASC').all() as Project[];
372
+ },
373
+
374
+ getById(id: string): Project | undefined {
375
+ return db.prepare('SELECT * FROM projects WHERE id = ?').get(id) as Project | undefined;
376
+ },
377
+
378
+ getActive(): Project | undefined {
379
+ return db.prepare('SELECT * FROM projects WHERE is_active = 1').get() as Project | undefined;
380
+ },
381
+
382
+ create(data: { name: string; path: string; description?: string }): Project {
383
+ const id = uuidv4();
384
+ db.prepare(`
385
+ INSERT INTO projects (id, name, path, description)
386
+ VALUES (?, ?, ?, ?)
387
+ `).run(id, data.name, data.path, data.description || null);
388
+ return this.getById(id)!;
389
+ },
390
+
391
+ setActive(id: string): Project | undefined {
392
+ // Deactivate all projects first
393
+ db.prepare('UPDATE projects SET is_active = 0').run();
394
+ // Activate the selected project
395
+ db.prepare('UPDATE projects SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(id);
396
+ return this.getById(id);
397
+ },
398
+
399
+ delete(id: string): boolean {
400
+ const result = db.prepare('DELETE FROM projects WHERE id = ?').run(id);
401
+ return result.changes > 0;
402
+ }
403
+ };
404
+
405
+ // Agent work log operations
406
+ export const agentWorkLogService = {
407
+ create(data: {
408
+ ticket_id: string;
409
+ agent_id: string;
410
+ project_id: string;
411
+ command: string;
412
+ prompt?: string;
413
+ }): AgentWorkLog {
414
+ const id = uuidv4();
415
+ db.prepare(`
416
+ INSERT INTO agent_work_logs (id, ticket_id, agent_id, project_id, command, prompt)
417
+ VALUES (?, ?, ?, ?, ?, ?)
418
+ `).run(id, data.ticket_id, data.agent_id, data.project_id, data.command, data.prompt || null);
419
+ return this.getById(id)!;
420
+ },
421
+
422
+ getById(id: string): AgentWorkLog | undefined {
423
+ return db.prepare('SELECT * FROM agent_work_logs WHERE id = ?').get(id) as AgentWorkLog | undefined;
424
+ },
425
+
426
+ getByTicketId(ticketId: string): AgentWorkLog[] {
427
+ return db.prepare(`
428
+ SELECT awl.*, m.name as agent_name, m.avatar as agent_avatar, p.name as project_name
429
+ FROM agent_work_logs awl
430
+ LEFT JOIN members m ON awl.agent_id = m.id
431
+ LEFT JOIN projects p ON awl.project_id = p.id
432
+ WHERE awl.ticket_id = ?
433
+ ORDER BY awl.started_at DESC
434
+ `).all(ticketId) as AgentWorkLog[];
435
+ },
436
+
437
+ complete(id: string, data: {
438
+ status: 'SUCCESS' | 'FAILED' | 'CANCELLED';
439
+ output?: string;
440
+ git_commit_hash?: string;
441
+ }): AgentWorkLog | undefined {
442
+ const log = this.getById(id);
443
+ if (!log) return undefined;
444
+
445
+ const startTime = new Date(log.started_at).getTime();
446
+ const duration = Date.now() - startTime;
447
+
448
+ db.prepare(`
449
+ UPDATE agent_work_logs
450
+ SET status = ?, output = ?, git_commit_hash = ?, completed_at = CURRENT_TIMESTAMP, duration_ms = ?
451
+ WHERE id = ?
452
+ `).run(data.status, data.output || null, data.git_commit_hash || null, duration, id);
453
+
454
+ return this.getById(id);
455
+ }
456
+ };
457
+
458
+ // Conversation operations
459
+ export const conversationService = {
460
+ create(data: {
461
+ ticket_id: string;
462
+ agent_id: string;
463
+ provider: 'claude' | 'opencode';
464
+ prompt?: string;
465
+ feedback?: string;
466
+ }): Conversation {
467
+ const id = uuidv4();
468
+ db.prepare(`
469
+ INSERT INTO conversations (id, ticket_id, agent_id, provider, prompt, feedback)
470
+ VALUES (?, ?, ?, ?, ?, ?)
471
+ `).run(id, data.ticket_id, data.agent_id, data.provider, data.prompt || null, data.feedback || null);
472
+ return this.getById(id)!;
473
+ },
474
+
475
+ getById(id: string): Conversation | undefined {
476
+ const conversation = db.prepare(`
477
+ SELECT c.*, m.name as agent_name, m.avatar as agent_avatar, m.role as agent_role
478
+ FROM conversations c
479
+ LEFT JOIN members m ON c.agent_id = m.id
480
+ WHERE c.id = ?
481
+ `).get(id) as (Conversation & { agent_name?: string; agent_avatar?: string; agent_role?: string }) | undefined;
482
+
483
+ if (!conversation) return undefined;
484
+
485
+ return {
486
+ ...conversation,
487
+ agent: conversation.agent_id ? {
488
+ id: conversation.agent_id,
489
+ name: conversation.agent_name!,
490
+ avatar: conversation.agent_avatar!,
491
+ role: conversation.agent_role as Member['role']
492
+ } as Member : undefined
493
+ };
494
+ },
495
+
496
+ getByTicketId(ticketId: string): Conversation[] {
497
+ const conversations = db.prepare(`
498
+ SELECT c.*, m.name as agent_name, m.avatar as agent_avatar, m.role as agent_role
499
+ FROM conversations c
500
+ LEFT JOIN members m ON c.agent_id = m.id
501
+ WHERE c.ticket_id = ?
502
+ ORDER BY c.started_at DESC
503
+ `).all(ticketId) as (Conversation & { agent_name?: string; agent_avatar?: string; agent_role?: string })[];
504
+
505
+ return conversations.map(conv => ({
506
+ ...conv,
507
+ agent: conv.agent_id ? {
508
+ id: conv.agent_id,
509
+ name: conv.agent_name!,
510
+ avatar: conv.agent_avatar!,
511
+ role: conv.agent_role as Member['role']
512
+ } as Member : undefined
513
+ }));
514
+ },
515
+
516
+ updateStatus(id: string, status: Conversation['status'], commitHash?: string): void {
517
+ db.prepare(`
518
+ UPDATE conversations
519
+ SET status = ?, git_commit_hash = ?
520
+ WHERE id = ?
521
+ `).run(status, commitHash || null, id);
522
+ },
523
+
524
+ complete(id: string, data: {
525
+ status: Conversation['status'];
526
+ git_commit_hash?: string;
527
+ }): void {
528
+ db.prepare(`
529
+ UPDATE conversations
530
+ SET status = ?, git_commit_hash = ?, completed_at = CURRENT_TIMESTAMP
531
+ WHERE id = ?
532
+ `).run(data.status, data.git_commit_hash || null, id);
533
+ }
534
+ };
535
+
536
+ // Conversation message operations
537
+ export const conversationMessageService = {
538
+ create(conversationId: string, content: string, type: ConversationMessage['message_type'] = 'log'): ConversationMessage {
539
+ const id = uuidv4();
540
+ db.prepare(`
541
+ INSERT INTO conversation_messages (id, conversation_id, content, message_type)
542
+ VALUES (?, ?, ?, ?)
543
+ `).run(id, conversationId, content, type);
544
+ return this.getById(id)!;
545
+ },
546
+
547
+ getById(id: string): ConversationMessage | undefined {
548
+ return db.prepare('SELECT * FROM conversation_messages WHERE id = ?').get(id) as ConversationMessage | undefined;
549
+ },
550
+
551
+ getByConversationId(conversationId: string): ConversationMessage[] {
552
+ return db.prepare(`
553
+ SELECT * FROM conversation_messages
554
+ WHERE conversation_id = ?
555
+ ORDER BY created_at ASC
556
+ `).all(conversationId) as ConversationMessage[];
557
+ },
558
+
559
+ appendLog(conversationId: string, content: string): void {
560
+ this.create(conversationId, content, 'log');
561
+ }
562
+ };
563
+
564
+ export default db;
package/next.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ devIndicators: false,
6
+ output: "standalone",
7
+ };
8
+
9
+ export default nextConfig;
10
+
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "olly-molly",
3
+ "version": "0.1.0",
4
+ "description": "Your AI Development Team, Running Locally - Manage AI agents (PM, Frontend, Backend, QA) from a beautiful kanban board",
5
+ "keywords": [
6
+ "ai",
7
+ "development",
8
+ "team",
9
+ "kanban",
10
+ "agents",
11
+ "openai",
12
+ "local",
13
+ "cli"
14
+ ],
15
+ "author": "ruucm",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/ruucm/olly-molly.git"
20
+ },
21
+ "homepage": "https://github.com/ruucm/olly-molly#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/ruucm/olly-molly/issues"
24
+ },
25
+ "bin": {
26
+ "olly-molly": "./bin/cli.js"
27
+ },
28
+ "files": [
29
+ "bin",
30
+ "app",
31
+ "components",
32
+ "db",
33
+ "lib",
34
+ "public",
35
+ "types",
36
+ "next.config.ts",
37
+ "postcss.config.mjs",
38
+ "tsconfig.json",
39
+ "package.json"
40
+ ],
41
+ "scripts": {
42
+ "dev": "next dev --port 1234",
43
+ "build": "next build",
44
+ "start": "next start",
45
+ "lint": "eslint",
46
+ "prepublishOnly": "npm run build",
47
+ "tauri": "tauri",
48
+ "tauri:dev": "tauri dev",
49
+ "tauri:build": "tauri build",
50
+ "tauri:icon": "tauri icon ./app-icon.png"
51
+ },
52
+ "dependencies": {
53
+ "@dnd-kit/core": "^6.3.1",
54
+ "@dnd-kit/sortable": "^10.0.0",
55
+ "@dnd-kit/utilities": "^3.2.2",
56
+ "better-sqlite3": "^12.5.0",
57
+ "next": "16.1.1",
58
+ "openai": "^6.15.0",
59
+ "react": "19.2.3",
60
+ "react-dom": "19.2.3",
61
+ "swr": "^2.3.8",
62
+ "uuid": "^13.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@tailwindcss/postcss": "^4",
66
+ "@tauri-apps/cli": "^2.9.6",
67
+ "@types/better-sqlite3": "^7.6.13",
68
+ "@types/node": "^20",
69
+ "@types/react": "^19",
70
+ "@types/react-dom": "^19",
71
+ "@types/uuid": "^10.0.0",
72
+ "eslint": "^9",
73
+ "eslint-config-next": "16.1.1",
74
+ "tailwindcss": "^4",
75
+ "typescript": "^5"
76
+ },
77
+ "engines": {
78
+ "node": ">=18.0.0"
79
+ }
80
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
Binary file
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>