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.
Files changed (33) hide show
  1. package/README.md +118 -0
  2. package/bin/claudekanban.js +53 -3
  3. package/package.json +9 -2
  4. package/public/desktop-review-0.jpeg +0 -0
  5. package/public/desktop-review-1.jpeg +0 -0
  6. package/public/mobile-review-0.jpg +0 -0
  7. package/server.ts +155 -4
  8. package/src/app/api/attempts/route.ts +217 -14
  9. package/src/app/api/git/branch/route.ts +123 -0
  10. package/src/app/api/git/branches/route.ts +104 -0
  11. package/src/app/api/git/checkout/route.ts +87 -0
  12. package/src/app/api/git/show-file-diff/route.ts +107 -0
  13. package/src/app/api/tasks/route.ts +2 -31
  14. package/src/app/page.tsx +11 -10
  15. package/src/components/editor/code-editor-with-inline-edit.tsx +64 -0
  16. package/src/components/editor/selection-mention-popup.tsx +151 -0
  17. package/src/components/sidebar/file-browser/file-tab-content.tsx +295 -67
  18. package/src/components/sidebar/file-browser/file-tabs-panel.tsx +5 -0
  19. package/src/components/sidebar/file-browser/unified-search.tsx +11 -2
  20. package/src/components/sidebar/git-changes/branch-checkout-modal.tsx +196 -0
  21. package/src/components/sidebar/git-changes/commit-details-modal.tsx +346 -76
  22. package/src/components/sidebar/git-changes/commit-file-diff-viewer.tsx +257 -0
  23. package/src/components/sidebar/git-changes/diff-tabs-panel.tsx +144 -0
  24. package/src/components/sidebar/git-changes/diff-viewer.tsx +36 -3
  25. package/src/components/sidebar/git-changes/git-panel.tsx +46 -5
  26. package/src/components/sidebar/git-changes/index.ts +1 -0
  27. package/src/components/sidebar/index.ts +1 -0
  28. package/src/components/sidebar/sidebar-panel.tsx +18 -2
  29. package/src/components/task/prompt-input.tsx +6 -6
  30. package/src/stores/context-mention-store.ts +3 -3
  31. package/src/stores/project-store.ts +13 -0
  32. package/src/stores/sidebar-store.ts +66 -2
  33. 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
 
@@ -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
- // Database will be auto-initialized by src/lib/db/index.ts on first import
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.19",
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: { taskId: string; prompt: string; displayPrompt?: string; fileIds?: string[] }) => {
69
- const { taskId, prompt, displayPrompt, fileIds = [] } = data;
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
- // Get task and project info
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 { taskId, prompt } = body;
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
- const newAttempt = {
20
- id: nanoid(),
21
- taskId,
22
- prompt,
23
- status: 'running' as const,
24
- branch: null,
25
- diffAdditions: 0,
26
- diffDeletions: 0,
27
- createdAt: Date.now(),
28
- completedAt: null,
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
- await db.insert(schema.attempts).values(newAttempt);
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
+ }