claude-ws 0.1.20 → 0.1.23

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 +174 -0
  2. package/drizzle.config.ts +6 -0
  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,143 @@ Open [http://localhost:8556](http://localhost:8556)
102
104
 
103
105
  ---
104
106
 
107
+ ## Configuration
108
+
109
+ ### API Authentication (Optional)
110
+
111
+ For secure deployments, you can enable API authentication by setting an `API_ACCESS_KEY`:
112
+
113
+ 1. Create a `.env` file in your project directory (or use the global one in `~/.claude-ws/.env`)
114
+ 2. Add your API access key:
115
+
116
+ ```bash
117
+ # .env
118
+ API_ACCESS_KEY=your-secret-key-here
119
+ ```
120
+
121
+ 3. All API requests must include the key in the `x-api-key` header:
122
+
123
+ ```bash
124
+ curl -H "x-api-key: your-secret-key-here" http://localhost:8556/api/conversations
125
+ ```
126
+
127
+ **Note:** Leave `API_ACCESS_KEY` empty to disable authentication (default for local development).
128
+
129
+ ### Claude Code CLI Path
130
+
131
+ Claude Workspace requires the Claude Code CLI to be installed. Set the `CLAUDE_PATH` environment variable to point to your Claude executable:
132
+
133
+ **Linux/Ubuntu:**
134
+ ```bash
135
+ CLAUDE_PATH=/home/$(whoami)/.local/bin/claude
136
+ ```
137
+
138
+ **macOS (Homebrew):**
139
+ ```bash
140
+ CLAUDE_PATH=/opt/homebrew/bin/claude
141
+ ```
142
+
143
+ **Windows:**
144
+ ```bash
145
+ CLAUDE_PATH=%USERPROFILE%\.local\bin\claude.exe
146
+ ```
147
+
148
+ Add this to your `.env` file in the project directory, or the global `.env` at `~/.claude-ws/.env`.
149
+
150
+ ### Environment Variables
151
+
152
+ | Variable | Description | Default |
153
+ |----------|-------------|---------|
154
+ | `CLAUDE_PATH` | Path to Claude Code CLI executable | Auto-detected |
155
+ | `PORT` | Server port | `3000` |
156
+ | `NODE_ENV` | Environment mode | `development` |
157
+ | `API_ACCESS_KEY` | API authentication key | (empty, no auth) |
158
+ | `DATABASE_URL` | SQLite database path | `./data.db` |
159
+ | `AGENT_FACTORY_DIR` | Agent Factory plugins directory | `~/.claude/agentfactory` |
160
+
161
+ ---
162
+
163
+ ## Work Everywhere with Cloudflare Tunnel
164
+
165
+ Access Claude Workspace securely from anywhere using Cloudflare Tunnel + Access.
166
+
167
+ ### 1. Install cloudflared
168
+
169
+ ```bash
170
+ # macOS
171
+ brew install cloudflared
172
+
173
+ # Linux
174
+ curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
175
+ chmod +x cloudflared && sudo mv cloudflared /usr/local/bin/
176
+
177
+ # Windows
178
+ winget install Cloudflare.cloudflared
179
+ ```
180
+
181
+ ### 2. Authenticate with Cloudflare
182
+
183
+ ```bash
184
+ cloudflared tunnel login
185
+ ```
186
+
187
+ ### 3. Create Tunnel
188
+
189
+ ```bash
190
+ cloudflared tunnel create claude-workspace
191
+ ```
192
+
193
+ ### 4. Configure Tunnel
194
+
195
+ Create `~/.cloudflared/config.yml`:
196
+
197
+ ```yaml
198
+ tunnel: claude-workspace
199
+ credentials-file: ~/.cloudflared/<TUNNEL_ID>.json
200
+
201
+ ingress:
202
+ - hostname: claude-ws.yourdomain.com
203
+ service: http://localhost:8556
204
+ - service: http_status:404
205
+ ```
206
+
207
+ ### 5. Add DNS Record
208
+
209
+ ```bash
210
+ cloudflared tunnel route dns claude-workspace claude-ws.yourdomain.com
211
+ ```
212
+
213
+ ### 6. Run Tunnel
214
+
215
+ ```bash
216
+ # Foreground
217
+ cloudflared tunnel run claude-workspace
218
+
219
+ # Or as service (recommended)
220
+ sudo cloudflared service install
221
+ sudo systemctl enable cloudflared
222
+ sudo systemctl start cloudflared
223
+ ```
224
+
225
+ ### 7. Setup Cloudflare Access (Authentication)
226
+
227
+ 1. Go to [Cloudflare Zero Trust Dashboard](https://one.dash.cloudflare.com/)
228
+ 2. Navigate to **Access** → **Applications** → **Add an application**
229
+ 3. Select **Self-hosted**
230
+ 4. Configure:
231
+ - **Application name**: Claude Workspace
232
+ - **Session duration**: 24 hours (or your preference)
233
+ - **Application domain**: `claude-ws.yourdomain.com`
234
+ 5. Add **Access Policy**:
235
+ - **Policy name**: Allowed Users
236
+ - **Action**: Allow
237
+ - **Include**: Emails ending in `@yourdomain.com` or specific email addresses
238
+ 6. Save and deploy
239
+
240
+ Now access `https://claude-ws.yourdomain.com` from anywhere with Cloudflare authentication.
241
+
242
+ ---
243
+
105
244
  ## Updating
106
245
 
107
246
  ### Check current version
@@ -142,6 +281,41 @@ npm install -g claude-ws@latest
142
281
  | `pnpm start` | Start production server |
143
282
  | `pnpm lint` | Run ESLint |
144
283
  | `pnpm db:migrate` | Run database migrations |
284
+ | `pnpm pm2:start` | Start with PM2 process manager |
285
+ | `pnpm pm2:stop` | Stop PM2 process |
286
+ | `pnpm pm2:restart` | Restart PM2 process |
287
+ | `pnpm pm2:logs` | View PM2 logs |
288
+ | `pnpm pm2:monit` | Monitor PM2 process |
289
+
290
+ ### Running with PM2
291
+
292
+ For production deployments with auto-restart and process management:
293
+
294
+ ```bash
295
+ # Install PM2 globally (if not already installed)
296
+ npm install -g pm2
297
+
298
+ # Start the server
299
+ pnpm pm2:start
300
+
301
+ # View logs
302
+ pnpm pm2:logs
303
+
304
+ # Monitor process
305
+ pnpm pm2:monit
306
+
307
+ # Restart
308
+ pnpm pm2:restart
309
+
310
+ # Stop
311
+ pnpm pm2:stop
312
+ ```
313
+
314
+ PM2 Features:
315
+ - Auto-restart on crash (max 10 attempts)
316
+ - Memory limit monitoring (500MB)
317
+ - Log rotation and management
318
+ - Process status tracking
145
319
 
146
320
  ---
147
321
 
package/drizzle.config.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import type { Config } from 'drizzle-kit';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import fs from 'fs';
4
5
 
5
6
  // Database location in user's home directory for persistence
6
7
  const DB_DIR = path.join(os.homedir(), '.claude-ws');
7
8
  const DB_PATH = path.join(DB_DIR, 'claude-ws.db');
8
9
 
10
+ // Ensure directory exists before drizzle-kit tries to connect
11
+ if (!fs.existsSync(DB_DIR)) {
12
+ fs.mkdirSync(DB_DIR, { recursive: true });
13
+ }
14
+
9
15
  export default {
10
16
  schema: './src/lib/db/schema.ts',
11
17
  out: './drizzle',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-ws",
3
- "version": "0.1.20",
3
+ "version": "0.1.23",
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
+ }