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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="app-icon.png" width="80" height="80" alt="Olly Molly">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Olly Molly</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Your AI Development Team, Running Locally</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="#quick-start">Quick Start</a> •
|
|
13
|
+
<a href="#features">Features</a> •
|
|
14
|
+
<a href="#how-it-works">How It Works</a> •
|
|
15
|
+
<a href="#contributing">Contributing</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img src="https://img.shields.io/npm/v/olly-molly?style=flat-square" alt="npm version">
|
|
20
|
+
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="license">
|
|
21
|
+
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square" alt="PRs welcome">
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
**Olly Molly** is a local-first AI development team manager. Assign tasks to AI agents (PM, Frontend, Backend, QA) and watch them work on your codebase—all from a beautiful kanban board interface.
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx olly-molly
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That's it. Open `http://localhost:1234` and start managing your AI team.
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- 🎯 **Kanban Board** — Drag-and-drop task management
|
|
39
|
+
- 🤖 **AI Agents** — PM, Frontend Dev, Backend Dev, QA agents
|
|
40
|
+
- 💬 **Natural Requests** — Ask PM in plain language, get structured tickets
|
|
41
|
+
- 🔒 **Local-First** — Everything runs on your machine
|
|
42
|
+
- 🎨 **Minimal Design** — Clean, paper-like UI inspired by fontshare.com
|
|
43
|
+
- 🌙 **Dark Mode** — Easy on the eyes
|
|
44
|
+
|
|
45
|
+
## How It Works
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
┌─────────────────────────────────────────────────────────┐
|
|
49
|
+
│ Olly Molly │
|
|
50
|
+
├─────────────────────────────────────────────────────────┤
|
|
51
|
+
│ │
|
|
52
|
+
│ You ──▶ PM Agent ──▶ Creates Tickets │
|
|
53
|
+
│ │ │
|
|
54
|
+
│ ▼ │
|
|
55
|
+
│ ┌─────────────────────────────────────────────────┐ │
|
|
56
|
+
│ │ TODO │ PROGRESS │ REVIEW │ DONE │ HOLD │ │
|
|
57
|
+
│ │ 📋 │ 🔄 │ 👀 │ ✅ │ ⏸️ │ │
|
|
58
|
+
│ └─────────────────────────────────────────────────┘ │
|
|
59
|
+
│ │ │
|
|
60
|
+
│ ▼ │
|
|
61
|
+
│ Agents (FE/BE/QA) work on assigned tickets │
|
|
62
|
+
│ │ │
|
|
63
|
+
│ ▼ │
|
|
64
|
+
│ Code changes in YOUR local project │
|
|
65
|
+
│ │
|
|
66
|
+
└─────────────────────────────────────────────────────────┘
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Setup
|
|
70
|
+
|
|
71
|
+
### Prerequisites
|
|
72
|
+
|
|
73
|
+
- Node.js 18+
|
|
74
|
+
- OpenAI API key (or compatible provider)
|
|
75
|
+
|
|
76
|
+
### Run with npx (Recommended)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx olly-molly
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Or install globally
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install -g olly-molly
|
|
86
|
+
olly-molly
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/YOUR_USERNAME/olly-molly.git
|
|
93
|
+
cd olly-molly
|
|
94
|
+
npm install
|
|
95
|
+
npm run dev
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
On first launch, you'll be prompted to enter your OpenAI API key. This is stored locally in your browser's localStorage.
|
|
101
|
+
|
|
102
|
+
You can also set it via environment variable:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
export OPENAI_API_KEY=your-key-here
|
|
106
|
+
npx olly-molly
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Project Selection
|
|
110
|
+
|
|
111
|
+
1. Click "Select Project" in the header
|
|
112
|
+
2. Add your project path (e.g., `/Users/you/my-app`)
|
|
113
|
+
3. AI agents will work within that directory
|
|
114
|
+
|
|
115
|
+
## Contributing
|
|
116
|
+
|
|
117
|
+
We love contributions! Here's how you can help:
|
|
118
|
+
|
|
119
|
+
### Ways to Contribute
|
|
120
|
+
|
|
121
|
+
- 🐛 **Bug Reports** — Found a bug? Open an issue
|
|
122
|
+
- 💡 **Feature Requests** — Have an idea? Let's discuss
|
|
123
|
+
- 🔧 **Pull Requests** — Code contributions are welcome
|
|
124
|
+
- 📖 **Documentation** — Help improve our docs
|
|
125
|
+
- 🎨 **Design** — UI/UX improvements
|
|
126
|
+
|
|
127
|
+
### Development Setup
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Clone the repo
|
|
131
|
+
git clone https://github.com/YOUR_USERNAME/olly-molly.git
|
|
132
|
+
cd olly-molly
|
|
133
|
+
|
|
134
|
+
# Install dependencies
|
|
135
|
+
npm install
|
|
136
|
+
|
|
137
|
+
# Start development server
|
|
138
|
+
npm run dev
|
|
139
|
+
|
|
140
|
+
# Open http://localhost:1234
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Project Structure
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
olly-molly/
|
|
147
|
+
├── app/ # Next.js app router
|
|
148
|
+
│ ├── api/ # API routes
|
|
149
|
+
│ ├── design-system/ # Design system docs
|
|
150
|
+
│ └── page.tsx # Main dashboard
|
|
151
|
+
├── components/ # React components
|
|
152
|
+
│ ├── kanban/ # Kanban board
|
|
153
|
+
│ ├── ui/ # Reusable UI components
|
|
154
|
+
│ └── ...
|
|
155
|
+
├── db/ # SQLite schemas
|
|
156
|
+
└── lib/ # Utilities
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Code Style
|
|
160
|
+
|
|
161
|
+
- TypeScript for type safety
|
|
162
|
+
- Functional components with hooks
|
|
163
|
+
- CSS variables for theming
|
|
164
|
+
- Minimal dependencies
|
|
165
|
+
|
|
166
|
+
## Tech Stack
|
|
167
|
+
|
|
168
|
+
- **Framework**: Next.js 16
|
|
169
|
+
- **UI**: React 19, Tailwind CSS 4
|
|
170
|
+
- **Database**: SQLite (better-sqlite3)
|
|
171
|
+
- **Drag & Drop**: dnd-kit
|
|
172
|
+
- **AI**: OpenAI API
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT © [Your Name]
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
<p align="center">
|
|
181
|
+
<sub>Built with ❤️ for developers who love AI</sub>
|
|
182
|
+
</p>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ticketService, memberService, projectService, activityService, conversationService } from '@/lib/db';
|
|
3
|
+
import { startBackgroundJob, AgentProvider } from '@/lib/agent-jobs';
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
|
|
6
|
+
interface AgentExecuteRequest {
|
|
7
|
+
ticket_id: string;
|
|
8
|
+
feedback?: string;
|
|
9
|
+
provider?: AgentProvider;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildAgentPrompt(ticket: {
|
|
13
|
+
title: string;
|
|
14
|
+
description?: string | null;
|
|
15
|
+
}, agent: {
|
|
16
|
+
name: string;
|
|
17
|
+
role: string;
|
|
18
|
+
system_prompt: string;
|
|
19
|
+
}, project: {
|
|
20
|
+
name: string;
|
|
21
|
+
path: string;
|
|
22
|
+
}, feedback?: string): string {
|
|
23
|
+
// Check if role is QA to add specific port instructions
|
|
24
|
+
const isQA = agent.role === 'QA';
|
|
25
|
+
const qaInstruction = isQA
|
|
26
|
+
? `\nIMPORTANT:
|
|
27
|
+
1. PORT CONFIGURATION: When running tests or starting servers for the TARGET PROJECT, you MUST use port 3001 (or any port other than 1234) to avoid conflict with this dashboard app. Use "PORT=3001 npm run dev" or equivalent.
|
|
28
|
+
2. TOOL USAGE: You MUST use the **Playwright MCP** (https://github.com/microsoft/playwright-mcp) tools for automated testing. verify the available tools and use them for browser automation and testing. Do NOT rely solely on manual terminal commands.`
|
|
29
|
+
: '';
|
|
30
|
+
|
|
31
|
+
const feedbackSection = feedback
|
|
32
|
+
? `\n\nIMPORTANT FEEDBACK FROM USER:\n${feedback}\n\nPlease address this feedback specifically in your implementation.`
|
|
33
|
+
: '';
|
|
34
|
+
|
|
35
|
+
return `You are acting as ${agent.name} (${agent.role}) for the project "${project.name}".
|
|
36
|
+
|
|
37
|
+
${agent.system_prompt}
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
TASK TO COMPLETE:
|
|
42
|
+
Title: ${ticket.title}
|
|
43
|
+
${ticket.description ? `Description: ${ticket.description}` : ''}
|
|
44
|
+
${feedbackSection}
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
INSTRUCTIONS:
|
|
49
|
+
1. Analyze the task requirements carefully
|
|
50
|
+
2. Make the necessary code changes to complete this task
|
|
51
|
+
3. Focus only on what's needed for this specific task
|
|
52
|
+
4. Write clean, well-documented code
|
|
53
|
+
5. After completing, provide a brief summary of changes made
|
|
54
|
+
6. If you make changes, commit them with a meaningful message
|
|
55
|
+
7. CRITICAL: You are working on the external project "${project.name}". When starting its server, ALWAYS use port 3001 (e.g. "PORT=3001 npm run dev"). NEVER use port 1234.${qaInstruction}
|
|
56
|
+
|
|
57
|
+
Please complete this task now.`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function POST(request: NextRequest) {
|
|
61
|
+
try {
|
|
62
|
+
const body: AgentExecuteRequest = await request.json();
|
|
63
|
+
|
|
64
|
+
if (!body.ticket_id) {
|
|
65
|
+
return NextResponse.json({ error: 'ticket_id is required' }, { status: 400 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Get ticket
|
|
69
|
+
const ticket = ticketService.getById(body.ticket_id);
|
|
70
|
+
if (!ticket) {
|
|
71
|
+
return NextResponse.json({ error: 'Ticket not found' }, { status: 404 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get assignee (agent)
|
|
75
|
+
if (!ticket.assignee_id) {
|
|
76
|
+
return NextResponse.json({ error: 'Ticket has no assignee' }, { status: 400 });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const agent = memberService.getById(ticket.assignee_id);
|
|
80
|
+
if (!agent) {
|
|
81
|
+
return NextResponse.json({ error: 'Agent not found' }, { status: 404 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Get active project
|
|
85
|
+
const project = projectService.getActive();
|
|
86
|
+
if (!project) {
|
|
87
|
+
return NextResponse.json({ error: 'No active project selected. Please select a project first.' }, { status: 400 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Build prompt
|
|
91
|
+
const prompt = buildAgentPrompt(ticket, agent, project, body.feedback);
|
|
92
|
+
|
|
93
|
+
// Use provided provider or default to 'claude'
|
|
94
|
+
const provider: AgentProvider = body.provider || 'claude';
|
|
95
|
+
|
|
96
|
+
// Generate job ID
|
|
97
|
+
const jobId = uuidv4();
|
|
98
|
+
|
|
99
|
+
// Update ticket status to IN_PROGRESS
|
|
100
|
+
ticketService.update(body.ticket_id, { status: 'IN_PROGRESS' }, agent.id);
|
|
101
|
+
|
|
102
|
+
// Create conversation for this execution
|
|
103
|
+
const conversation = conversationService.create({
|
|
104
|
+
ticket_id: body.ticket_id,
|
|
105
|
+
agent_id: agent.id,
|
|
106
|
+
provider,
|
|
107
|
+
prompt,
|
|
108
|
+
feedback: body.feedback,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Log activity
|
|
112
|
+
activityService.log({
|
|
113
|
+
ticket_id: body.ticket_id,
|
|
114
|
+
member_id: agent.id,
|
|
115
|
+
action: 'AGENT_WORK_STARTED',
|
|
116
|
+
details: `${agent.name} started working on this task using ${provider === 'opencode' ? 'OpenCode' : 'Claude Code'}`,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Start background job (non-blocking)
|
|
120
|
+
startBackgroundJob({
|
|
121
|
+
jobId,
|
|
122
|
+
conversationId: conversation.id,
|
|
123
|
+
ticketId: body.ticket_id,
|
|
124
|
+
agentId: agent.id,
|
|
125
|
+
agentName: agent.name,
|
|
126
|
+
projectPath: project.path,
|
|
127
|
+
prompt,
|
|
128
|
+
provider,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Return immediately with job info
|
|
132
|
+
return NextResponse.json({
|
|
133
|
+
success: true,
|
|
134
|
+
job_id: jobId,
|
|
135
|
+
conversation_id: conversation.id,
|
|
136
|
+
message: `${agent.name} started working on the task. The job is running in the background.`,
|
|
137
|
+
agent: {
|
|
138
|
+
id: agent.id,
|
|
139
|
+
name: agent.name,
|
|
140
|
+
role: agent.role,
|
|
141
|
+
avatar: agent.avatar,
|
|
142
|
+
},
|
|
143
|
+
project: {
|
|
144
|
+
id: project.id,
|
|
145
|
+
name: project.name,
|
|
146
|
+
path: project.path,
|
|
147
|
+
},
|
|
148
|
+
ticket_status: 'IN_PROGRESS',
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('Error executing agent:', error);
|
|
152
|
+
return NextResponse.json({
|
|
153
|
+
error: 'Failed to execute agent',
|
|
154
|
+
details: String(error)
|
|
155
|
+
}, { status: 500 });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getRunningJobs, getJobByTicketId, getJobOutput, cancelJob } from '@/lib/agent-jobs';
|
|
3
|
+
|
|
4
|
+
// Get all running jobs
|
|
5
|
+
export async function GET(request: NextRequest) {
|
|
6
|
+
const url = new URL(request.url);
|
|
7
|
+
const ticketId = url.searchParams.get('ticket_id');
|
|
8
|
+
const jobId = url.searchParams.get('job_id');
|
|
9
|
+
|
|
10
|
+
if (jobId) {
|
|
11
|
+
// Get specific job output
|
|
12
|
+
const output = getJobOutput(jobId);
|
|
13
|
+
return NextResponse.json({ output });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (ticketId) {
|
|
17
|
+
// Get job for specific ticket
|
|
18
|
+
const job = getJobByTicketId(ticketId);
|
|
19
|
+
return NextResponse.json({ job });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Get all running jobs
|
|
23
|
+
const jobs = getRunningJobs();
|
|
24
|
+
return NextResponse.json({ jobs });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Cancel a job
|
|
28
|
+
export async function DELETE(request: NextRequest) {
|
|
29
|
+
const url = new URL(request.url);
|
|
30
|
+
const jobId = url.searchParams.get('job_id');
|
|
31
|
+
|
|
32
|
+
if (!jobId) {
|
|
33
|
+
return NextResponse.json({ error: 'job_id is required' }, { status: 400 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cancelled = cancelJob(jobId);
|
|
37
|
+
return NextResponse.json({ success: cancelled });
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
export async function GET(request: NextRequest) {
|
|
4
|
+
try {
|
|
5
|
+
// Check if OPENAI_API_KEY exists in environment
|
|
6
|
+
const hasKey = !!process.env.OPENAI_API_KEY;
|
|
7
|
+
|
|
8
|
+
return NextResponse.json({ hasKey });
|
|
9
|
+
} catch (error) {
|
|
10
|
+
return NextResponse.json({ hasKey: false }, { status: 500 });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { conversationService, conversationMessageService } from '@/lib/db';
|
|
3
|
+
|
|
4
|
+
interface RouteParams {
|
|
5
|
+
params: Promise<{
|
|
6
|
+
id: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// GET /api/conversations/[id]
|
|
11
|
+
export async function GET(request: NextRequest, { params }: RouteParams) {
|
|
12
|
+
try {
|
|
13
|
+
// Await params for Next.js 15+
|
|
14
|
+
const { id } = await params;
|
|
15
|
+
|
|
16
|
+
const conversation = conversationService.getById(id);
|
|
17
|
+
|
|
18
|
+
if (!conversation) {
|
|
19
|
+
return NextResponse.json({ error: 'Conversation not found' }, { status: 404 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const messages = conversationMessageService.getByConversationId(id);
|
|
23
|
+
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
conversation,
|
|
26
|
+
messages
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Error fetching conversation:', error);
|
|
30
|
+
return NextResponse.json({
|
|
31
|
+
error: 'Failed to fetch conversation',
|
|
32
|
+
details: String(error)
|
|
33
|
+
}, { status: 500 });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { conversationService, conversationMessageService } from '@/lib/db';
|
|
3
|
+
|
|
4
|
+
// GET /api/conversations?ticket_id=xxx
|
|
5
|
+
export async function GET(request: NextRequest) {
|
|
6
|
+
try {
|
|
7
|
+
const { searchParams } = new URL(request.url);
|
|
8
|
+
const ticketId = searchParams.get('ticket_id');
|
|
9
|
+
|
|
10
|
+
if (!ticketId) {
|
|
11
|
+
return NextResponse.json({ error: 'ticket_id is required' }, { status: 400 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const conversations = conversationService.getByTicketId(ticketId);
|
|
15
|
+
|
|
16
|
+
return NextResponse.json({ conversations });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Error fetching conversations:', error);
|
|
19
|
+
return NextResponse.json({
|
|
20
|
+
error: 'Failed to fetch conversations',
|
|
21
|
+
details: String(error)
|
|
22
|
+
}, { status: 500 });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { memberService } from '@/lib/db';
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
request: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
7
|
+
) {
|
|
8
|
+
try {
|
|
9
|
+
const { id } = await params;
|
|
10
|
+
const member = memberService.getById(id);
|
|
11
|
+
if (!member) {
|
|
12
|
+
return NextResponse.json({ error: 'Member not found' }, { status: 404 });
|
|
13
|
+
}
|
|
14
|
+
return NextResponse.json(member);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error('Error fetching member:', error);
|
|
17
|
+
return NextResponse.json({ error: 'Failed to fetch member' }, { status: 500 });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function PATCH(
|
|
22
|
+
request: NextRequest,
|
|
23
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
24
|
+
) {
|
|
25
|
+
try {
|
|
26
|
+
const { id } = await params;
|
|
27
|
+
const body = await request.json();
|
|
28
|
+
const member = memberService.update(id, body);
|
|
29
|
+
if (!member) {
|
|
30
|
+
return NextResponse.json({ error: 'Member not found' }, { status: 404 });
|
|
31
|
+
}
|
|
32
|
+
return NextResponse.json(member);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Error updating member:', error);
|
|
35
|
+
return NextResponse.json({ error: 'Failed to update member' }, { status: 500 });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { memberService } from '@/lib/db';
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
const members = memberService.getAll();
|
|
7
|
+
return NextResponse.json(members);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
console.error('Error fetching members:', error);
|
|
10
|
+
return NextResponse.json({ error: 'Failed to fetch members' }, { status: 500 });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ticketService, memberService } from '@/lib/db';
|
|
3
|
+
import OpenAI from 'openai';
|
|
4
|
+
|
|
5
|
+
const openai = new OpenAI({
|
|
6
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PM Agent - AI-powered feature breakdown
|
|
11
|
+
*
|
|
12
|
+
* Uses OpenAI to analyze feature requests and create appropriate tasks
|
|
13
|
+
* with intelligent assignment to team members.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface TaskFromAI {
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
|
20
|
+
assignee_role: 'FE_DEV' | 'BACKEND_DEV' | 'QA' | 'DEVOPS';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SYSTEM_PROMPT = `You are a PM (Project Manager) AI agent for a software development team.
|
|
24
|
+
|
|
25
|
+
Your team consists of:
|
|
26
|
+
- FE_DEV (Frontend Developer): Handles UI/UX, React, Next.js, CSS, components, user interfaces
|
|
27
|
+
- BACKEND_DEV (Backend Developer): Handles APIs, databases, server logic, authentication, business logic
|
|
28
|
+
- QA (QA Engineer): Handles testing, quality assurance, bug verification, E2E tests
|
|
29
|
+
- DEVOPS (DevOps Engineer): Handles deployment, CI/CD, infrastructure, monitoring
|
|
30
|
+
|
|
31
|
+
When given a feature request, you must:
|
|
32
|
+
1. Break it down into specific, actionable tasks
|
|
33
|
+
2. Assign each task to the appropriate team member based on their expertise
|
|
34
|
+
3. Set priorities (CRITICAL > HIGH > MEDIUM > LOW)
|
|
35
|
+
|
|
36
|
+
IMPORTANT RULES:
|
|
37
|
+
- Create focused, single-responsibility tasks
|
|
38
|
+
- Backend tasks should come before frontend integration tasks
|
|
39
|
+
- Always include a QA task for testing
|
|
40
|
+
- Be specific in task descriptions
|
|
41
|
+
- Use Korean for titles and descriptions
|
|
42
|
+
|
|
43
|
+
Respond in JSON format:
|
|
44
|
+
{
|
|
45
|
+
"tasks": [
|
|
46
|
+
{
|
|
47
|
+
"title": "Task title in Korean",
|
|
48
|
+
"description": "Detailed task description in Korean",
|
|
49
|
+
"priority": "HIGH",
|
|
50
|
+
"assignee_role": "BACKEND_DEV"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"summary": "Brief summary of the breakdown in Korean"
|
|
54
|
+
}`;
|
|
55
|
+
|
|
56
|
+
async function breakdownWithAI(request: string): Promise<{ tasks: TaskFromAI[]; summary: string }> {
|
|
57
|
+
const completion = await openai.chat.completions.create({
|
|
58
|
+
model: 'gpt-4o-mini',
|
|
59
|
+
messages: [
|
|
60
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
61
|
+
{ role: 'user', content: `Feature Request: ${request}` }
|
|
62
|
+
],
|
|
63
|
+
response_format: { type: 'json_object' },
|
|
64
|
+
temperature: 0.7,
|
|
65
|
+
max_tokens: 2000,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const content = completion.choices[0].message.content;
|
|
69
|
+
if (!content) {
|
|
70
|
+
throw new Error('No response from AI');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return JSON.parse(content);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getAssigneeByRole(role: string): string | null {
|
|
77
|
+
const member = memberService.getByRole(role);
|
|
78
|
+
return member?.id || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function POST(request: NextRequest) {
|
|
82
|
+
try {
|
|
83
|
+
const body = await request.json();
|
|
84
|
+
|
|
85
|
+
if (!body.request) {
|
|
86
|
+
return NextResponse.json(
|
|
87
|
+
{ error: 'Feature request is required' },
|
|
88
|
+
{ status: 400 }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
93
|
+
return NextResponse.json(
|
|
94
|
+
{ error: 'OpenAI API key not configured' },
|
|
95
|
+
{ status: 500 }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const pmMember = memberService.getByRole('PM');
|
|
100
|
+
|
|
101
|
+
// Use AI to break down the request
|
|
102
|
+
const aiResponse = await breakdownWithAI(body.request);
|
|
103
|
+
|
|
104
|
+
const createdTickets = [];
|
|
105
|
+
|
|
106
|
+
for (const task of aiResponse.tasks) {
|
|
107
|
+
const assigneeId = getAssigneeByRole(task.assignee_role);
|
|
108
|
+
|
|
109
|
+
const ticket = ticketService.create({
|
|
110
|
+
title: task.title,
|
|
111
|
+
description: task.description,
|
|
112
|
+
priority: task.priority,
|
|
113
|
+
assignee_id: assigneeId || undefined,
|
|
114
|
+
project_id: body.project_id,
|
|
115
|
+
created_by: pmMember?.id,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const assignee = assigneeId ? memberService.getById(assigneeId) : null;
|
|
119
|
+
createdTickets.push({
|
|
120
|
+
...ticket,
|
|
121
|
+
assignee,
|
|
122
|
+
assigned_role: task.assignee_role,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return NextResponse.json({
|
|
127
|
+
success: true,
|
|
128
|
+
original_request: body.request,
|
|
129
|
+
created_by: pmMember,
|
|
130
|
+
tickets_created: createdTickets.length,
|
|
131
|
+
tickets: createdTickets,
|
|
132
|
+
ai_summary: aiResponse.summary,
|
|
133
|
+
message: `PM이 AI를 사용해 "${body.request}" 요청을 분석하여 ${createdTickets.length}개의 태스크를 생성했습니다.`,
|
|
134
|
+
}, { status: 201 });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('Error in PM breakdown:', error);
|
|
137
|
+
return NextResponse.json(
|
|
138
|
+
{ error: 'Failed to process feature request', details: String(error) },
|
|
139
|
+
{ status: 500 }
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|