claude-ws 0.1.19 → 0.1.22
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/README.md +118 -0
- package/bin/claudekanban.js +53 -3
- package/package.json +9 -2
- package/public/desktop-review-0.jpeg +0 -0
- package/public/desktop-review-1.jpeg +0 -0
- package/public/mobile-review-0.jpg +0 -0
- package/server.ts +155 -4
- package/src/app/api/attempts/route.ts +217 -14
- package/src/app/api/git/branch/route.ts +123 -0
- package/src/app/api/git/branches/route.ts +104 -0
- package/src/app/api/git/checkout/route.ts +87 -0
- package/src/app/api/git/show-file-diff/route.ts +107 -0
- package/src/app/api/tasks/route.ts +2 -31
- package/src/app/page.tsx +11 -10
- package/src/components/editor/code-editor-with-inline-edit.tsx +64 -0
- package/src/components/editor/selection-mention-popup.tsx +151 -0
- package/src/components/sidebar/file-browser/file-tab-content.tsx +295 -67
- package/src/components/sidebar/file-browser/file-tabs-panel.tsx +5 -0
- package/src/components/sidebar/file-browser/unified-search.tsx +11 -2
- package/src/components/sidebar/git-changes/branch-checkout-modal.tsx +196 -0
- package/src/components/sidebar/git-changes/commit-details-modal.tsx +346 -76
- package/src/components/sidebar/git-changes/commit-file-diff-viewer.tsx +257 -0
- package/src/components/sidebar/git-changes/diff-tabs-panel.tsx +144 -0
- package/src/components/sidebar/git-changes/diff-viewer.tsx +36 -3
- package/src/components/sidebar/git-changes/git-panel.tsx +46 -5
- package/src/components/sidebar/git-changes/index.ts +1 -0
- package/src/components/sidebar/index.ts +1 -0
- package/src/components/sidebar/sidebar-panel.tsx +18 -2
- package/src/components/task/prompt-input.tsx +6 -6
- package/src/stores/context-mention-store.ts +3 -3
- package/src/stores/project-store.ts +13 -0
- package/src/stores/sidebar-store.ts +66 -2
- package/src/stores/task-store.ts +1 -7
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Claude Workspace
|
|
2
2
|
|
|
3
|
+
> ⚠️ **DISCLAIMER:** This software is provided "AS IS" without warranty. The owners and contributors accept **no liability** for any damages or claims arising from its use. [Read full disclaimer](./DISCLAIMER.md).
|
|
4
|
+
|
|
3
5
|
**Visual workspace for Claude Code with Kanban board, code editor, and Git integration.**
|
|
4
6
|
|
|
5
7
|
Local-first SQLite database. Real-time streaming. Plugin system for custom agents and skills.
|
|
@@ -102,6 +104,87 @@ Open [http://localhost:8556](http://localhost:8556)
|
|
|
102
104
|
|
|
103
105
|
---
|
|
104
106
|
|
|
107
|
+
## Work Everywhere with Cloudflare Tunnel
|
|
108
|
+
|
|
109
|
+
Access Claude Workspace securely from anywhere using Cloudflare Tunnel + Access.
|
|
110
|
+
|
|
111
|
+
### 1. Install cloudflared
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# macOS
|
|
115
|
+
brew install cloudflared
|
|
116
|
+
|
|
117
|
+
# Linux
|
|
118
|
+
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
|
|
119
|
+
chmod +x cloudflared && sudo mv cloudflared /usr/local/bin/
|
|
120
|
+
|
|
121
|
+
# Windows
|
|
122
|
+
winget install Cloudflare.cloudflared
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 2. Authenticate with Cloudflare
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
cloudflared tunnel login
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 3. Create Tunnel
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
cloudflared tunnel create claude-workspace
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 4. Configure Tunnel
|
|
138
|
+
|
|
139
|
+
Create `~/.cloudflared/config.yml`:
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
tunnel: claude-workspace
|
|
143
|
+
credentials-file: ~/.cloudflared/<TUNNEL_ID>.json
|
|
144
|
+
|
|
145
|
+
ingress:
|
|
146
|
+
- hostname: claude-ws.yourdomain.com
|
|
147
|
+
service: http://localhost:8556
|
|
148
|
+
- service: http_status:404
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 5. Add DNS Record
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
cloudflared tunnel route dns claude-workspace claude-ws.yourdomain.com
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 6. Run Tunnel
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Foreground
|
|
161
|
+
cloudflared tunnel run claude-workspace
|
|
162
|
+
|
|
163
|
+
# Or as service (recommended)
|
|
164
|
+
sudo cloudflared service install
|
|
165
|
+
sudo systemctl enable cloudflared
|
|
166
|
+
sudo systemctl start cloudflared
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 7. Setup Cloudflare Access (Authentication)
|
|
170
|
+
|
|
171
|
+
1. Go to [Cloudflare Zero Trust Dashboard](https://one.dash.cloudflare.com/)
|
|
172
|
+
2. Navigate to **Access** → **Applications** → **Add an application**
|
|
173
|
+
3. Select **Self-hosted**
|
|
174
|
+
4. Configure:
|
|
175
|
+
- **Application name**: Claude Workspace
|
|
176
|
+
- **Session duration**: 24 hours (or your preference)
|
|
177
|
+
- **Application domain**: `claude-ws.yourdomain.com`
|
|
178
|
+
5. Add **Access Policy**:
|
|
179
|
+
- **Policy name**: Allowed Users
|
|
180
|
+
- **Action**: Allow
|
|
181
|
+
- **Include**: Emails ending in `@yourdomain.com` or specific email addresses
|
|
182
|
+
6. Save and deploy
|
|
183
|
+
|
|
184
|
+
Now access `https://claude-ws.yourdomain.com` from anywhere with Cloudflare authentication.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
105
188
|
## Updating
|
|
106
189
|
|
|
107
190
|
### Check current version
|
|
@@ -142,6 +225,41 @@ npm install -g claude-ws@latest
|
|
|
142
225
|
| `pnpm start` | Start production server |
|
|
143
226
|
| `pnpm lint` | Run ESLint |
|
|
144
227
|
| `pnpm db:migrate` | Run database migrations |
|
|
228
|
+
| `pnpm pm2:start` | Start with PM2 process manager |
|
|
229
|
+
| `pnpm pm2:stop` | Stop PM2 process |
|
|
230
|
+
| `pnpm pm2:restart` | Restart PM2 process |
|
|
231
|
+
| `pnpm pm2:logs` | View PM2 logs |
|
|
232
|
+
| `pnpm pm2:monit` | Monitor PM2 process |
|
|
233
|
+
|
|
234
|
+
### Running with PM2
|
|
235
|
+
|
|
236
|
+
For production deployments with auto-restart and process management:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Install PM2 globally (if not already installed)
|
|
240
|
+
npm install -g pm2
|
|
241
|
+
|
|
242
|
+
# Start the server
|
|
243
|
+
pnpm pm2:start
|
|
244
|
+
|
|
245
|
+
# View logs
|
|
246
|
+
pnpm pm2:logs
|
|
247
|
+
|
|
248
|
+
# Monitor process
|
|
249
|
+
pnpm pm2:monit
|
|
250
|
+
|
|
251
|
+
# Restart
|
|
252
|
+
pnpm pm2:restart
|
|
253
|
+
|
|
254
|
+
# Stop
|
|
255
|
+
pnpm pm2:stop
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
PM2 Features:
|
|
259
|
+
- Auto-restart on crash (max 10 attempts)
|
|
260
|
+
- Memory limit monitoring (500MB)
|
|
261
|
+
- Log rotation and management
|
|
262
|
+
- Process status tracking
|
|
145
263
|
|
|
146
264
|
---
|
|
147
265
|
|
package/bin/claudekanban.js
CHANGED
|
@@ -53,6 +53,52 @@ if (!fs.existsSync(DB_DIR)) {
|
|
|
53
53
|
fs.mkdirSync(DB_DIR, { recursive: true });
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
async function runMigrations() {
|
|
57
|
+
console.log('[Claude Workspace] Initializing database...');
|
|
58
|
+
|
|
59
|
+
// Simple approach: just require the db module which auto-runs initDb()
|
|
60
|
+
const dbPath = path.join(packageRoot, 'src', 'lib', 'db', 'index.ts');
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Find tsx binary (should already be installed by this point)
|
|
64
|
+
let tsxCmd;
|
|
65
|
+
const possiblePaths = [
|
|
66
|
+
path.join(packageRoot, 'node_modules', '.bin', 'tsx'),
|
|
67
|
+
path.join(packageRoot, '..', '.bin', 'tsx'),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
for (const tsxPath of possiblePaths) {
|
|
71
|
+
if (fs.existsSync(tsxPath)) {
|
|
72
|
+
tsxCmd = tsxPath;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!tsxCmd) {
|
|
78
|
+
// Try global tsx
|
|
79
|
+
try {
|
|
80
|
+
const { execSync } = require('child_process');
|
|
81
|
+
execSync('which tsx', { stdio: 'ignore' });
|
|
82
|
+
tsxCmd = 'tsx';
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error('tsx not found - this should not happen after dependency installation');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { execSync } = require('child_process');
|
|
89
|
+
execSync(`"${tsxCmd}" -e "require('${dbPath}'); console.log('[Claude Workspace] ✓ Database ready');"`, {
|
|
90
|
+
cwd: packageRoot,
|
|
91
|
+
stdio: 'inherit',
|
|
92
|
+
env: { ...process.env }
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log('');
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('[Claude Workspace] Database initialization failed:', error.message);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
56
102
|
async function startServer() {
|
|
57
103
|
console.log('[Claude Workspace] Starting server...');
|
|
58
104
|
console.log('[Claude Workspace] Database location:', DB_PATH);
|
|
@@ -86,6 +132,12 @@ async function startServer() {
|
|
|
86
132
|
console.error('[Claude Workspace] Failed to install dependencies:', error.message);
|
|
87
133
|
process.exit(1);
|
|
88
134
|
}
|
|
135
|
+
|
|
136
|
+
// Run migrations after dependencies are installed
|
|
137
|
+
await runMigrations();
|
|
138
|
+
} else {
|
|
139
|
+
// Dependencies already installed, run migrations
|
|
140
|
+
await runMigrations();
|
|
89
141
|
}
|
|
90
142
|
|
|
91
143
|
// Check if .next directory has valid build (check BUILD_ID file)
|
|
@@ -221,9 +273,7 @@ async function main() {
|
|
|
221
273
|
console.log('='.repeat(60));
|
|
222
274
|
console.log('');
|
|
223
275
|
|
|
224
|
-
//
|
|
225
|
-
// No need to run separate migration - initDb() handles it all
|
|
226
|
-
|
|
276
|
+
// Migrations will be run inside startServer() after dependencies are installed
|
|
227
277
|
await startServer();
|
|
228
278
|
|
|
229
279
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-ws",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A beautifully crafted workspace interface for Claude Code with real-time streaming and local SQLite database",
|
|
6
6
|
"keywords": [
|
|
@@ -62,6 +62,12 @@
|
|
|
62
62
|
"lint": "eslint",
|
|
63
63
|
"db:generate": "drizzle-kit generate",
|
|
64
64
|
"db:migrate": "drizzle-kit migrate",
|
|
65
|
+
"pm2:start": "pm2 start ecosystem.config.cjs",
|
|
66
|
+
"pm2:stop": "pm2 stop claude-ws",
|
|
67
|
+
"pm2:restart": "pm2 restart claude-ws",
|
|
68
|
+
"pm2:delete": "pm2 delete claude-ws",
|
|
69
|
+
"pm2:logs": "pm2 logs claude-ws",
|
|
70
|
+
"pm2:monit": "pm2 monit",
|
|
65
71
|
"version:patch": "npm version patch --no-git-tag-version",
|
|
66
72
|
"version:minor": "npm version minor --no-git-tag-version",
|
|
67
73
|
"version:major": "npm version major --no-git-tag-version",
|
|
@@ -143,5 +149,6 @@
|
|
|
143
149
|
"@types/react-dom": "^19",
|
|
144
150
|
"eslint": "^9",
|
|
145
151
|
"eslint-config-next": "^16.1.3"
|
|
146
|
-
}
|
|
152
|
+
},
|
|
153
|
+
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48"
|
|
147
154
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/server.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createServer } from 'http';
|
|
|
7
7
|
import { parse } from 'url';
|
|
8
8
|
import next from 'next';
|
|
9
9
|
import { Server as SocketIOServer } from 'socket.io';
|
|
10
|
+
import { homedir } from 'os';
|
|
10
11
|
import { agentManager } from './src/lib/agent-manager';
|
|
11
12
|
import { sessionManager } from './src/lib/session-manager';
|
|
12
13
|
import { checkpointManager } from './src/lib/checkpoint-manager';
|
|
@@ -65,20 +66,170 @@ app.prepare().then(async () => {
|
|
|
65
66
|
// Start new attempt
|
|
66
67
|
socket.on(
|
|
67
68
|
'attempt:start',
|
|
68
|
-
async (data: {
|
|
69
|
-
|
|
69
|
+
async (data: {
|
|
70
|
+
taskId: string;
|
|
71
|
+
prompt: string;
|
|
72
|
+
displayPrompt?: string;
|
|
73
|
+
fileIds?: string[];
|
|
74
|
+
force_create?: boolean;
|
|
75
|
+
projectId?: string;
|
|
76
|
+
projectName?: string;
|
|
77
|
+
taskTitle?: string;
|
|
78
|
+
projectRootPath?: string;
|
|
79
|
+
}) => {
|
|
80
|
+
const {
|
|
81
|
+
taskId,
|
|
82
|
+
prompt,
|
|
83
|
+
displayPrompt,
|
|
84
|
+
fileIds = [],
|
|
85
|
+
force_create,
|
|
86
|
+
projectId,
|
|
87
|
+
projectName,
|
|
88
|
+
taskTitle,
|
|
89
|
+
projectRootPath
|
|
90
|
+
} = data;
|
|
91
|
+
|
|
92
|
+
console.log('[Socket] attempt:start received:', {
|
|
93
|
+
taskId,
|
|
94
|
+
prompt,
|
|
95
|
+
force_create,
|
|
96
|
+
projectId,
|
|
97
|
+
projectName,
|
|
98
|
+
taskTitle,
|
|
99
|
+
projectRootPath
|
|
100
|
+
});
|
|
70
101
|
|
|
71
102
|
try {
|
|
72
|
-
|
|
73
|
-
const task = await db.query.tasks.findFirst({
|
|
103
|
+
let task = await db.query.tasks.findFirst({
|
|
74
104
|
where: eq(schema.tasks.id, taskId),
|
|
75
105
|
});
|
|
76
106
|
|
|
107
|
+
// Handle force_create logic
|
|
108
|
+
if (force_create && !task) {
|
|
109
|
+
console.log('[Socket] Task not found, force_create=true');
|
|
110
|
+
|
|
111
|
+
if (!projectId) {
|
|
112
|
+
socket.emit('error', { message: 'projectId required' });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check if project exists
|
|
117
|
+
let project = await db.query.projects.findFirst({
|
|
118
|
+
where: eq(schema.projects.id, projectId),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
console.log('[Socket] Project exists?', !!project);
|
|
122
|
+
|
|
123
|
+
// Create project if it doesn't exist
|
|
124
|
+
if (!project) {
|
|
125
|
+
console.log('[Socket] Project does not exist, checking projectName...');
|
|
126
|
+
console.log('[Socket] projectName value:', projectName);
|
|
127
|
+
|
|
128
|
+
if (!projectName || projectName.trim() === '') {
|
|
129
|
+
console.log('[Socket] Project name required but not provided');
|
|
130
|
+
socket.emit('error', { message: 'projectName required' });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create project directory and record
|
|
135
|
+
const { mkdir } = await import('fs/promises');
|
|
136
|
+
const { join } = await import('path');
|
|
137
|
+
|
|
138
|
+
const projectDirName = `${projectId}-${projectName}`;
|
|
139
|
+
const projectPath = projectRootPath
|
|
140
|
+
? join(projectRootPath, projectDirName)
|
|
141
|
+
: join(homedir(), '.claude-ws', 'projects', projectDirName);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await mkdir(projectPath, { recursive: true });
|
|
145
|
+
console.log('[Socket] Created project directory:', projectPath);
|
|
146
|
+
} catch (mkdirError: any) {
|
|
147
|
+
if (mkdirError?.code !== 'EEXIST') {
|
|
148
|
+
console.error('[Socket] Failed to create project folder:', mkdirError);
|
|
149
|
+
socket.emit('error', { message: 'Failed to create project folder: ' + mkdirError.message });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await db.insert(schema.projects).values({
|
|
156
|
+
id: projectId,
|
|
157
|
+
name: projectName,
|
|
158
|
+
path: projectPath,
|
|
159
|
+
createdAt: Date.now(),
|
|
160
|
+
});
|
|
161
|
+
console.log('[Socket] Created project:', projectId);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('[Socket] Failed to create project:', error);
|
|
164
|
+
socket.emit('error', { message: 'Failed to create project' });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Project created, fetch it
|
|
169
|
+
project = await db.query.projects.findFirst({
|
|
170
|
+
where: eq(schema.projects.id, projectId),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check taskTitle
|
|
175
|
+
if (!taskTitle || taskTitle.trim() === '') {
|
|
176
|
+
console.log('[Socket] Task title required but not provided');
|
|
177
|
+
socket.emit('error', { message: 'taskTitle required' });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Create task
|
|
182
|
+
const { and, desc } = await import('drizzle-orm');
|
|
183
|
+
|
|
184
|
+
// Get next position for todo status
|
|
185
|
+
const tasksInStatus = await db
|
|
186
|
+
.select()
|
|
187
|
+
.from(schema.tasks)
|
|
188
|
+
.where(
|
|
189
|
+
and(
|
|
190
|
+
eq(schema.tasks.projectId, projectId),
|
|
191
|
+
eq(schema.tasks.status, 'todo')
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
.orderBy(desc(schema.tasks.position))
|
|
195
|
+
.limit(1);
|
|
196
|
+
|
|
197
|
+
const position = tasksInStatus.length > 0 ? tasksInStatus[0].position + 1 : 0;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await db.insert(schema.tasks).values({
|
|
201
|
+
id: taskId,
|
|
202
|
+
projectId,
|
|
203
|
+
title: taskTitle,
|
|
204
|
+
description: null,
|
|
205
|
+
status: 'todo',
|
|
206
|
+
position,
|
|
207
|
+
chatInit: false,
|
|
208
|
+
rewindSessionId: null,
|
|
209
|
+
rewindMessageUuid: null,
|
|
210
|
+
createdAt: Date.now(),
|
|
211
|
+
updatedAt: Date.now(),
|
|
212
|
+
});
|
|
213
|
+
console.log('[Socket] Created task:', taskId);
|
|
214
|
+
|
|
215
|
+
// Fetch the created task
|
|
216
|
+
task = await db.query.tasks.findFirst({
|
|
217
|
+
where: eq(schema.tasks.id, taskId),
|
|
218
|
+
});
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error('[Socket] Failed to create task:', error);
|
|
221
|
+
socket.emit('error', { message: 'Failed to create task' });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Validate task exists
|
|
77
227
|
if (!task) {
|
|
78
228
|
socket.emit('error', { message: 'Task not found' });
|
|
79
229
|
return;
|
|
80
230
|
}
|
|
81
231
|
|
|
232
|
+
// Get project info
|
|
82
233
|
const project = await db.query.projects.findFirst({
|
|
83
234
|
where: eq(schema.projects.id, task.projectId),
|
|
84
235
|
});
|
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { db, schema } from '@/lib/db';
|
|
3
3
|
import { nanoid } from 'nanoid';
|
|
4
|
+
import { eq, and, desc } from 'drizzle-orm';
|
|
5
|
+
import { mkdir } from 'fs/promises';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
4
8
|
|
|
5
9
|
// POST /api/attempts - Create a new attempt (only creates record)
|
|
6
10
|
// Actual execution happens via WebSocket
|
|
7
11
|
export async function POST(request: NextRequest) {
|
|
8
12
|
try {
|
|
9
13
|
const body = await request.json();
|
|
10
|
-
const {
|
|
14
|
+
const {
|
|
15
|
+
taskId,
|
|
16
|
+
prompt,
|
|
17
|
+
force_create,
|
|
18
|
+
projectId,
|
|
19
|
+
projectName,
|
|
20
|
+
taskTitle,
|
|
21
|
+
projectRootPath
|
|
22
|
+
} = body;
|
|
23
|
+
|
|
24
|
+
console.log('POST /api/attempts received:', {
|
|
25
|
+
taskId,
|
|
26
|
+
prompt,
|
|
27
|
+
force_create,
|
|
28
|
+
projectId,
|
|
29
|
+
projectName,
|
|
30
|
+
taskTitle,
|
|
31
|
+
projectRootPath
|
|
32
|
+
});
|
|
11
33
|
|
|
12
34
|
if (!taskId || !prompt) {
|
|
13
35
|
return NextResponse.json(
|
|
@@ -16,26 +38,149 @@ export async function POST(request: NextRequest) {
|
|
|
16
38
|
);
|
|
17
39
|
}
|
|
18
40
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
// Step 1: If force_create is not true, skip validation and create attempt directly
|
|
42
|
+
if (!force_create) {
|
|
43
|
+
// Validate task exists (current behavior)
|
|
44
|
+
const task = await db.query.tasks.findFirst({
|
|
45
|
+
where: eq(schema.tasks.id, taskId)
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!task) {
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ error: 'Task not found' },
|
|
51
|
+
{ status: 404 }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Create attempt with existing task
|
|
56
|
+
return await createAttempt(task, prompt);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Step 2: Check if taskId exists
|
|
60
|
+
const existingTask = await db.query.tasks.findFirst({
|
|
61
|
+
where: eq(schema.tasks.id, taskId)
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (existingTask) {
|
|
65
|
+
// Task exists, create attempt directly
|
|
66
|
+
return await createAttempt(existingTask, prompt);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Step 3: Task doesn't exist, need to validate and create
|
|
70
|
+
// First, get the projectId from the request body
|
|
71
|
+
if (!projectId) {
|
|
72
|
+
return NextResponse.json(
|
|
73
|
+
{ error: 'projectId required' },
|
|
74
|
+
{ status: 400 }
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 4: Check if projectId exists
|
|
79
|
+
let existingProject;
|
|
80
|
+
try {
|
|
81
|
+
existingProject = await db.query.projects.findFirst({
|
|
82
|
+
where: eq(schema.projects.id, projectId)
|
|
83
|
+
});
|
|
84
|
+
} catch (dbError) {
|
|
85
|
+
console.error('Database error checking project:', dbError);
|
|
86
|
+
return NextResponse.json(
|
|
87
|
+
{ error: 'Database error checking project' },
|
|
88
|
+
{ status: 500 }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log('Project exists?', !!existingProject);
|
|
93
|
+
|
|
94
|
+
let finalProjectId = projectId;
|
|
95
|
+
|
|
96
|
+
if (!existingProject) {
|
|
97
|
+
console.log('Project does not exist, checking projectName...');
|
|
98
|
+
console.log('projectName value:', projectName);
|
|
99
|
+
console.log('projectName type:', typeof projectName);
|
|
100
|
+
console.log('projectName === undefined:', projectName === undefined);
|
|
101
|
+
console.log('projectName === null:', projectName === null);
|
|
102
|
+
console.log('projectName === "":', projectName === "");
|
|
103
|
+
|
|
104
|
+
// Project doesn't exist, check if projectName provided
|
|
105
|
+
if (!projectName || projectName.trim() === '') {
|
|
106
|
+
console.log('Project name required but not provided');
|
|
107
|
+
return NextResponse.json(
|
|
108
|
+
{ error: 'projectName required' },
|
|
109
|
+
{ status: 400 }
|
|
110
|
+
);
|
|
111
|
+
}
|
|
30
112
|
|
|
31
|
-
|
|
113
|
+
// Determine project path based on projectRootPath
|
|
114
|
+
const projectDirName = `${projectId}-${projectName}`;
|
|
115
|
+
const projectPath = projectRootPath
|
|
116
|
+
? join(projectRootPath, projectDirName)
|
|
117
|
+
: join(homedir(), '.claude-ws', 'projects', projectDirName);
|
|
118
|
+
|
|
119
|
+
// Create the project folder
|
|
120
|
+
try {
|
|
121
|
+
await mkdir(projectPath, { recursive: true });
|
|
122
|
+
} catch (mkdirError: any) {
|
|
123
|
+
// If folder already exists, that's okay
|
|
124
|
+
if (mkdirError?.code !== 'EEXIST') {
|
|
125
|
+
console.error('Failed to create project folder:', mkdirError);
|
|
126
|
+
return NextResponse.json(
|
|
127
|
+
{ error: 'Failed to create project folder: ' + mkdirError.message },
|
|
128
|
+
{ status: 500 }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Create new project
|
|
134
|
+
const newProject = {
|
|
135
|
+
id: projectId,
|
|
136
|
+
name: projectName,
|
|
137
|
+
path: projectPath,
|
|
138
|
+
createdAt: Date.now(),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await db.insert(schema.projects).values(newProject);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Failed to create project:', error);
|
|
145
|
+
return NextResponse.json(
|
|
146
|
+
{ error: 'Failed to create project' },
|
|
147
|
+
{ status: 500 }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Step 5: At this point, project exists (either was existing or was just created)
|
|
153
|
+
// Check if taskTitle is provided
|
|
154
|
+
if (!taskTitle || taskTitle.trim() === '') {
|
|
155
|
+
console.log('Task title required but not provided');
|
|
156
|
+
return NextResponse.json(
|
|
157
|
+
{ error: 'taskTitle required' },
|
|
158
|
+
{ status: 400 }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Create new task
|
|
163
|
+
const newTask = await createNewTask(taskId, finalProjectId, taskTitle);
|
|
164
|
+
|
|
165
|
+
if (!newTask) {
|
|
166
|
+
return NextResponse.json(
|
|
167
|
+
{ error: 'Failed to create task' },
|
|
168
|
+
{ status: 500 }
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Step 6: Create attempt with the newly created task
|
|
173
|
+
return await createAttempt(newTask, prompt);
|
|
32
174
|
|
|
33
|
-
return NextResponse.json(newAttempt, { status: 201 });
|
|
34
175
|
} catch (error: any) {
|
|
35
176
|
console.error('Failed to create attempt:', error);
|
|
177
|
+
console.error('Error code:', error?.code);
|
|
178
|
+
console.error('Error message:', error?.message);
|
|
179
|
+
console.error('Error stack:', error?.stack);
|
|
36
180
|
|
|
37
181
|
// Handle foreign key constraint (invalid taskId)
|
|
38
182
|
if (error?.code === 'SQLITE_CONSTRAINT_FOREIGNKEY') {
|
|
183
|
+
console.error('Foreign key constraint violation');
|
|
39
184
|
return NextResponse.json(
|
|
40
185
|
{ error: 'Task not found' },
|
|
41
186
|
{ status: 404 }
|
|
@@ -48,3 +193,61 @@ export async function POST(request: NextRequest) {
|
|
|
48
193
|
);
|
|
49
194
|
}
|
|
50
195
|
}
|
|
196
|
+
|
|
197
|
+
// Helper function to create attempt
|
|
198
|
+
async function createAttempt(task: any, prompt: string) {
|
|
199
|
+
const newAttempt = {
|
|
200
|
+
id: nanoid(),
|
|
201
|
+
taskId: task.id,
|
|
202
|
+
prompt,
|
|
203
|
+
status: 'running' as const,
|
|
204
|
+
branch: null,
|
|
205
|
+
diffAdditions: 0,
|
|
206
|
+
diffDeletions: 0,
|
|
207
|
+
createdAt: Date.now(),
|
|
208
|
+
completedAt: null,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
await db.insert(schema.attempts).values(newAttempt);
|
|
212
|
+
return NextResponse.json(newAttempt, { status: 201 });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Helper function to create new task
|
|
216
|
+
async function createNewTask(taskId: string, projectId: string, taskTitle: string) {
|
|
217
|
+
// Get next position for todo status
|
|
218
|
+
const tasksInStatus = await db
|
|
219
|
+
.select()
|
|
220
|
+
.from(schema.tasks)
|
|
221
|
+
.where(
|
|
222
|
+
and(
|
|
223
|
+
eq(schema.tasks.projectId, projectId),
|
|
224
|
+
eq(schema.tasks.status, 'todo')
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
.orderBy(desc(schema.tasks.position))
|
|
228
|
+
.limit(1);
|
|
229
|
+
|
|
230
|
+
const position = tasksInStatus.length > 0 ? tasksInStatus[0].position + 1 : 0;
|
|
231
|
+
|
|
232
|
+
const newTask = {
|
|
233
|
+
id: taskId,
|
|
234
|
+
projectId,
|
|
235
|
+
title: taskTitle,
|
|
236
|
+
description: null,
|
|
237
|
+
status: 'todo' as const,
|
|
238
|
+
position,
|
|
239
|
+
chatInit: false,
|
|
240
|
+
rewindSessionId: null,
|
|
241
|
+
rewindMessageUuid: null,
|
|
242
|
+
createdAt: Date.now(),
|
|
243
|
+
updatedAt: Date.now(),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
await db.insert(schema.tasks).values(newTask);
|
|
248
|
+
return newTask;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error('Failed to create task:', error);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|