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.
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/app/api/agent/execute/route.ts +157 -0
- package/app/api/agent/status/route.ts +38 -0
- package/app/api/check-api-key/route.ts +12 -0
- package/app/api/conversations/[id]/route.ts +35 -0
- package/app/api/conversations/route.ts +24 -0
- package/app/api/members/[id]/route.ts +37 -0
- package/app/api/members/route.ts +12 -0
- package/app/api/pm/breakdown/route.ts +142 -0
- package/app/api/pm/tickets/route.ts +147 -0
- package/app/api/projects/[id]/route.ts +56 -0
- package/app/api/projects/active/route.ts +15 -0
- package/app/api/projects/route.ts +53 -0
- package/app/api/tickets/[id]/logs/route.ts +16 -0
- package/app/api/tickets/[id]/route.ts +60 -0
- package/app/api/tickets/[id]/work-logs/route.ts +16 -0
- package/app/api/tickets/route.ts +37 -0
- package/app/design-system/page.tsx +242 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +318 -0
- package/app/layout.tsx +37 -0
- package/app/page.tsx +331 -0
- package/bin/cli.js +66 -0
- package/components/ThemeProvider.tsx +56 -0
- package/components/ThemeToggle.tsx +31 -0
- package/components/activity/ActivityLog.tsx +96 -0
- package/components/activity/index.ts +1 -0
- package/components/kanban/ConversationList.tsx +75 -0
- package/components/kanban/ConversationView.tsx +132 -0
- package/components/kanban/KanbanBoard.tsx +179 -0
- package/components/kanban/KanbanColumn.tsx +80 -0
- package/components/kanban/SortableTicket.tsx +58 -0
- package/components/kanban/TicketCard.tsx +98 -0
- package/components/kanban/TicketModal.tsx +510 -0
- package/components/kanban/TicketSidebar.tsx +448 -0
- package/components/kanban/index.ts +8 -0
- package/components/pm/PMRequestModal.tsx +196 -0
- package/components/pm/index.ts +1 -0
- package/components/project/ProjectSelector.tsx +211 -0
- package/components/project/index.ts +1 -0
- package/components/team/MemberCard.tsx +147 -0
- package/components/team/TeamPanel.tsx +57 -0
- package/components/team/index.ts +2 -0
- package/components/ui/ApiKeyModal.tsx +101 -0
- package/components/ui/Avatar.tsx +95 -0
- package/components/ui/Badge.tsx +59 -0
- package/components/ui/Button.tsx +60 -0
- package/components/ui/Card.tsx +64 -0
- package/components/ui/Input.tsx +41 -0
- package/components/ui/Modal.tsx +76 -0
- package/components/ui/ResizablePane.tsx +97 -0
- package/components/ui/Select.tsx +45 -0
- package/components/ui/Textarea.tsx +41 -0
- package/components/ui/index.ts +8 -0
- package/db/dev.sqlite +0 -0
- package/db/dev.sqlite-shm +0 -0
- package/db/dev.sqlite-wal +0 -0
- package/db/schema-conversations.sql +26 -0
- package/db/schema-projects.sql +29 -0
- package/db/schema.sql +94 -0
- package/lib/agent-jobs.ts +232 -0
- package/lib/db.ts +564 -0
- package/next.config.ts +10 -0
- package/package.json +80 -0
- package/postcss.config.mjs +7 -0
- package/public/app-icon.png +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/profiles/designer.png +0 -0
- package/public/profiles/dev-backend.png +0 -0
- package/public/profiles/dev-frontend.png +0 -0
- package/public/profiles/pm.png +0 -0
- package/public/profiles/qa.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- 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
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
|
+
}
|
|
Binary file
|
package/public/file.svg
ADDED
|
@@ -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>
|
package/public/globe.svg
ADDED
|
@@ -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>
|
package/public/next.svg
ADDED
|
@@ -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>
|