claude-queue 1.0.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.
@@ -0,0 +1,1306 @@
1
+ // src/index.ts
2
+ import express from "express";
3
+ import cors from "cors";
4
+ import { existsSync as existsSync4 } from "fs";
5
+ import { join as join5, dirname as dirname2 } from "path";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
7
+ import { createProxyMiddleware } from "http-proxy-middleware";
8
+ import { customAlphabet as customAlphabet2 } from "nanoid";
9
+
10
+ // src/api/projects.ts
11
+ import { Router } from "express";
12
+ import { customAlphabet, nanoid } from "nanoid";
13
+
14
+ // src/db/index.ts
15
+ import Database from "better-sqlite3";
16
+ import { existsSync, mkdirSync } from "fs";
17
+ import { homedir } from "os";
18
+ import { join } from "path";
19
+
20
+ // src/db/schema.ts
21
+ function initSchema(db2) {
22
+ db2.exec(`
23
+ CREATE TABLE IF NOT EXISTS projects (
24
+ id TEXT PRIMARY KEY,
25
+ path TEXT UNIQUE NOT NULL,
26
+ name TEXT NOT NULL,
27
+ paused INTEGER DEFAULT 0,
28
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
29
+ );
30
+ `);
31
+ const projectColumns = db2.prepare("PRAGMA table_info(projects)").all();
32
+ const hasPausedColumn = projectColumns.some((col) => col.name === "paused");
33
+ if (!hasPausedColumn) {
34
+ db2.exec("ALTER TABLE projects ADD COLUMN paused INTEGER DEFAULT 0");
35
+ }
36
+ db2.exec(`
37
+
38
+ CREATE TABLE IF NOT EXISTS tasks (
39
+ id TEXT PRIMARY KEY,
40
+ project_id TEXT NOT NULL,
41
+ title TEXT NOT NULL,
42
+ description TEXT,
43
+ status TEXT NOT NULL DEFAULT 'backlog',
44
+ blocked INTEGER DEFAULT 0,
45
+ current_activity TEXT,
46
+ position INTEGER NOT NULL,
47
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
48
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
49
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
50
+ );
51
+
52
+ CREATE TABLE IF NOT EXISTS comments (
53
+ id TEXT PRIMARY KEY,
54
+ task_id TEXT NOT NULL,
55
+ author TEXT NOT NULL,
56
+ content TEXT NOT NULL,
57
+ seen INTEGER DEFAULT 0,
58
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
59
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
60
+ );
61
+
62
+ CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
63
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
64
+ CREATE INDEX IF NOT EXISTS idx_comments_task_id ON comments(task_id);
65
+
66
+ CREATE TABLE IF NOT EXISTS templates (
67
+ id TEXT PRIMARY KEY,
68
+ project_id TEXT NOT NULL,
69
+ title TEXT NOT NULL,
70
+ description TEXT,
71
+ position INTEGER NOT NULL,
72
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
73
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
74
+ );
75
+
76
+ CREATE INDEX IF NOT EXISTS idx_templates_project_id ON templates(project_id);
77
+
78
+ CREATE TABLE IF NOT EXISTS seeded_templates (
79
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
80
+ project_id TEXT NOT NULL,
81
+ template_key TEXT NOT NULL,
82
+ seeded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
83
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
84
+ UNIQUE(project_id, template_key)
85
+ );
86
+
87
+ CREATE INDEX IF NOT EXISTS idx_seeded_templates_project_id ON seeded_templates(project_id);
88
+
89
+ CREATE TABLE IF NOT EXISTS task_activity (
90
+ id TEXT PRIMARY KEY,
91
+ task_id TEXT NOT NULL,
92
+ type TEXT NOT NULL,
93
+ old_value TEXT,
94
+ new_value TEXT,
95
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
96
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
97
+ );
98
+
99
+ CREATE INDEX IF NOT EXISTS idx_task_activity_task_id ON task_activity(task_id);
100
+
101
+ CREATE TABLE IF NOT EXISTS attachments (
102
+ id TEXT PRIMARY KEY,
103
+ task_id TEXT,
104
+ template_id TEXT,
105
+ filename TEXT NOT NULL,
106
+ original_name TEXT NOT NULL,
107
+ mime_type TEXT NOT NULL,
108
+ size INTEGER NOT NULL,
109
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
110
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
111
+ FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE
112
+ );
113
+
114
+ CREATE INDEX IF NOT EXISTS idx_attachments_task_id ON attachments(task_id);
115
+
116
+ CREATE TABLE IF NOT EXISTS prompts (
117
+ id TEXT PRIMARY KEY,
118
+ project_id TEXT,
119
+ type TEXT NOT NULL CHECK(type IN ('master', 'project')),
120
+ content TEXT NOT NULL,
121
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
122
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
123
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
124
+ );
125
+
126
+ CREATE INDEX IF NOT EXISTS idx_prompts_project_id ON prompts(project_id);
127
+ CREATE INDEX IF NOT EXISTS idx_prompts_type ON prompts(type);
128
+ `);
129
+ const commentColumns = db2.prepare("PRAGMA table_info(comments)").all();
130
+ const hasSeenColumn = commentColumns.some((col) => col.name === "seen");
131
+ if (!hasSeenColumn) {
132
+ db2.exec("ALTER TABLE comments ADD COLUMN seen INTEGER DEFAULT 0");
133
+ }
134
+ const taskColumns = db2.prepare("PRAGMA table_info(tasks)").all();
135
+ const hasStartingCommit = taskColumns.some((col) => col.name === "starting_commit");
136
+ if (!hasStartingCommit) {
137
+ db2.exec("ALTER TABLE tasks ADD COLUMN starting_commit TEXT");
138
+ }
139
+ const hasClaudeLastSeen = projectColumns.some((col) => col.name === "claude_last_seen");
140
+ if (!hasClaudeLastSeen) {
141
+ db2.exec("ALTER TABLE projects ADD COLUMN claude_last_seen DATETIME");
142
+ }
143
+ const hasStartedAt = taskColumns.some((col) => col.name === "started_at");
144
+ if (!hasStartedAt) {
145
+ db2.exec("ALTER TABLE tasks ADD COLUMN started_at DATETIME");
146
+ }
147
+ const hasCompletedAt = taskColumns.some((col) => col.name === "completed_at");
148
+ if (!hasCompletedAt) {
149
+ db2.exec("ALTER TABLE tasks ADD COLUMN completed_at DATETIME");
150
+ }
151
+ const attachmentColumns = db2.prepare("PRAGMA table_info(attachments)").all();
152
+ const hasTemplateId = attachmentColumns.some((col) => col.name === "template_id");
153
+ if (!hasTemplateId) {
154
+ db2.exec("ALTER TABLE attachments ADD COLUMN template_id TEXT REFERENCES templates(id) ON DELETE CASCADE");
155
+ }
156
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_attachments_template_id ON attachments(template_id)");
157
+ const existingProjects = db2.prepare("SELECT id FROM projects").all();
158
+ const DEFAULT_TEMPLATE_KEYS = ["bug-fix", "add-tests", "refactor", "code-review", "documentation"];
159
+ for (const project of existingProjects) {
160
+ const seededCount = db2.prepare("SELECT COUNT(*) as count FROM seeded_templates WHERE project_id = ?").get(project.id);
161
+ if (seededCount.count === 0) {
162
+ const templateCount = db2.prepare("SELECT COUNT(*) as count FROM templates WHERE project_id = ?").get(project.id);
163
+ if (templateCount.count > 0) {
164
+ const insertStmt = db2.prepare("INSERT OR IGNORE INTO seeded_templates (project_id, template_key) VALUES (?, ?)");
165
+ for (const key of DEFAULT_TEMPLATE_KEYS) {
166
+ insertStmt.run(project.id, key);
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // src/db/index.ts
174
+ var DATA_DIR = join(homedir(), ".claude-queue");
175
+ var DB_PATH = join(DATA_DIR, "kanban.db");
176
+ var db = null;
177
+ function getDb() {
178
+ if (!db) {
179
+ if (!existsSync(DATA_DIR)) {
180
+ mkdirSync(DATA_DIR, { recursive: true });
181
+ }
182
+ db = new Database(DB_PATH);
183
+ db.pragma("journal_mode = WAL");
184
+ db.pragma("foreign_keys = ON");
185
+ initSchema(db);
186
+ }
187
+ return db;
188
+ }
189
+ function closeDb() {
190
+ if (db) {
191
+ db.close();
192
+ db = null;
193
+ }
194
+ }
195
+
196
+ // src/api/projects.ts
197
+ var router = Router();
198
+ var generateId = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 4);
199
+ var DEFAULT_TEMPLATES = [
200
+ { key: "bug-fix", title: "Bug fix", description: "Investigate and fix a reported bug. Include root cause analysis." },
201
+ { key: "add-tests", title: "Add tests", description: "Write unit/integration tests for existing functionality." },
202
+ { key: "refactor", title: "Refactor", description: "Improve code structure without changing behavior. Focus on readability and maintainability." },
203
+ { key: "code-review", title: "Code review", description: "Review recent changes for bugs, performance issues, and best practices." },
204
+ { key: "documentation", title: "Documentation", description: "Add or update documentation, comments, or README files." }
205
+ ];
206
+ function seedDefaultTemplates(projectId) {
207
+ const db2 = getDb();
208
+ const seededKeys = db2.prepare("SELECT template_key FROM seeded_templates WHERE project_id = ?").all(projectId);
209
+ const seededKeySet = new Set(seededKeys.map((row) => row.template_key));
210
+ const templatesToSeed = DEFAULT_TEMPLATES.filter((t) => !seededKeySet.has(t.key));
211
+ if (templatesToSeed.length === 0) {
212
+ return;
213
+ }
214
+ const maxPosition = db2.prepare("SELECT COALESCE(MAX(position), -1) as max FROM templates WHERE project_id = ?").get(projectId);
215
+ const insertTemplateStmt = db2.prepare(`
216
+ INSERT INTO templates (id, project_id, title, description, position)
217
+ VALUES (?, ?, ?, ?, ?)
218
+ `);
219
+ const markSeededStmt = db2.prepare(`
220
+ INSERT INTO seeded_templates (project_id, template_key)
221
+ VALUES (?, ?)
222
+ `);
223
+ db2.transaction(() => {
224
+ templatesToSeed.forEach((template, index) => {
225
+ insertTemplateStmt.run(nanoid(), projectId, template.title, template.description, maxPosition.max + 1 + index);
226
+ markSeededStmt.run(projectId, template.key);
227
+ });
228
+ })();
229
+ }
230
+ router.get("/", (_req, res) => {
231
+ const db2 = getDb();
232
+ const projects = db2.prepare("SELECT * FROM projects ORDER BY created_at DESC").all();
233
+ res.json(projects);
234
+ });
235
+ router.get("/:id", (req, res) => {
236
+ const db2 = getDb();
237
+ const project = db2.prepare("SELECT * FROM projects WHERE id = ?").get(req.params.id);
238
+ if (!project) {
239
+ res.status(404).json({ error: "Project not found" });
240
+ return;
241
+ }
242
+ if (req.query.heartbeat === "true") {
243
+ db2.prepare("UPDATE projects SET claude_last_seen = CURRENT_TIMESTAMP WHERE id = ?").run(req.params.id);
244
+ const updated = db2.prepare("SELECT * FROM projects WHERE id = ?").get(req.params.id);
245
+ res.json(updated);
246
+ return;
247
+ }
248
+ res.json(project);
249
+ });
250
+ router.post("/", (req, res) => {
251
+ const { path, name } = req.body;
252
+ if (!path || !name) {
253
+ res.status(400).json({ error: "path and name are required" });
254
+ return;
255
+ }
256
+ const db2 = getDb();
257
+ const existing = db2.prepare("SELECT * FROM projects WHERE path = ?").get(path);
258
+ if (existing) {
259
+ res.json(existing);
260
+ return;
261
+ }
262
+ const id = `kbn-${generateId()}`;
263
+ const stmt = db2.prepare("INSERT INTO projects (id, path, name) VALUES (?, ?, ?)");
264
+ stmt.run(id, path, name);
265
+ seedDefaultTemplates(id);
266
+ const project = db2.prepare("SELECT * FROM projects WHERE id = ?").get(id);
267
+ res.status(201).json(project);
268
+ });
269
+ router.delete("/:id", (req, res) => {
270
+ const db2 = getDb();
271
+ const result = db2.prepare("DELETE FROM projects WHERE id = ?").run(req.params.id);
272
+ if (result.changes === 0) {
273
+ res.status(404).json({ error: "Project not found" });
274
+ return;
275
+ }
276
+ res.json({ success: true });
277
+ });
278
+ router.post("/:id/pause", (req, res) => {
279
+ const db2 = getDb();
280
+ const project = db2.prepare("SELECT * FROM projects WHERE id = ?").get(req.params.id);
281
+ if (!project) {
282
+ res.status(404).json({ error: "Project not found" });
283
+ return;
284
+ }
285
+ db2.prepare("UPDATE projects SET paused = 1 WHERE id = ?").run(req.params.id);
286
+ const updated = db2.prepare("SELECT * FROM projects WHERE id = ?").get(req.params.id);
287
+ res.json(updated);
288
+ });
289
+ router.post("/:id/resume", (req, res) => {
290
+ const db2 = getDb();
291
+ const project = db2.prepare("SELECT * FROM projects WHERE id = ?").get(req.params.id);
292
+ if (!project) {
293
+ res.status(404).json({ error: "Project not found" });
294
+ return;
295
+ }
296
+ db2.prepare("UPDATE projects SET paused = 0 WHERE id = ?").run(req.params.id);
297
+ const updated = db2.prepare("SELECT * FROM projects WHERE id = ?").get(req.params.id);
298
+ res.json(updated);
299
+ });
300
+ router.post("/:id/heartbeat", (req, res) => {
301
+ const db2 = getDb();
302
+ const project = db2.prepare("SELECT * FROM projects WHERE id = ?").get(req.params.id);
303
+ if (!project) {
304
+ res.status(404).json({ error: "Project not found" });
305
+ return;
306
+ }
307
+ db2.prepare("UPDATE projects SET claude_last_seen = CURRENT_TIMESTAMP WHERE id = ?").run(req.params.id);
308
+ const updated = db2.prepare("SELECT * FROM projects WHERE id = ?").get(req.params.id);
309
+ res.json(updated);
310
+ });
311
+ function formatDateForSqlite(date) {
312
+ const pad = (n) => n.toString().padStart(2, "0");
313
+ return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}`;
314
+ }
315
+ router.get("/:id/stats", (req, res) => {
316
+ const db2 = getDb();
317
+ const projectId = req.params.id;
318
+ const project = db2.prepare("SELECT * FROM projects WHERE id = ?").get(projectId);
319
+ if (!project) {
320
+ res.status(404).json({ error: "Project not found" });
321
+ return;
322
+ }
323
+ const now = /* @__PURE__ */ new Date();
324
+ const startOfTodayUtc = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
325
+ const startOfWeekUtc = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - now.getUTCDay()));
326
+ const startOfToday = formatDateForSqlite(startOfTodayUtc);
327
+ const startOfWeek = formatDateForSqlite(startOfWeekUtc);
328
+ const completedToday = db2.prepare(`
329
+ SELECT COUNT(*) as count FROM tasks
330
+ WHERE project_id = ? AND status = 'done' AND completed_at >= ?
331
+ `).get(projectId, startOfToday);
332
+ const completedThisWeek = db2.prepare(`
333
+ SELECT COUNT(*) as count FROM tasks
334
+ WHERE project_id = ? AND status = 'done' AND completed_at >= ?
335
+ `).get(projectId, startOfWeek);
336
+ const totalCompleted = db2.prepare(`
337
+ SELECT COUNT(*) as count FROM tasks
338
+ WHERE project_id = ? AND status = 'done'
339
+ `).get(projectId);
340
+ const tasksByStatus = db2.prepare(`
341
+ SELECT status, COUNT(*) as count FROM tasks
342
+ WHERE project_id = ?
343
+ GROUP BY status
344
+ `).all(projectId);
345
+ const completedTasks = db2.prepare(`
346
+ SELECT started_at, completed_at FROM tasks
347
+ WHERE project_id = ? AND status = 'done' AND started_at IS NOT NULL AND completed_at IS NOT NULL
348
+ `).all(projectId);
349
+ let totalTimeMs = 0;
350
+ let tasksWithTime = 0;
351
+ for (const task of completedTasks) {
352
+ const started = new Date(task.started_at).getTime();
353
+ const completed = new Date(task.completed_at).getTime();
354
+ if (completed > started) {
355
+ totalTimeMs += completed - started;
356
+ tasksWithTime++;
357
+ }
358
+ }
359
+ const avgTimeMs = tasksWithTime > 0 ? Math.round(totalTimeMs / tasksWithTime) : 0;
360
+ res.json({
361
+ completedToday: completedToday.count,
362
+ completedThisWeek: completedThisWeek.count,
363
+ totalCompleted: totalCompleted.count,
364
+ totalTimeMs,
365
+ avgTimeMs,
366
+ tasksWithTime,
367
+ tasksByStatus: Object.fromEntries(tasksByStatus.map((row) => [row.status, row.count]))
368
+ });
369
+ });
370
+ var projects_default = router;
371
+
372
+ // src/api/tasks.ts
373
+ import { Router as Router2 } from "express";
374
+ import { nanoid as nanoid3 } from "nanoid";
375
+
376
+ // src/utils/heartbeat.ts
377
+ function updateProjectHeartbeat(projectId) {
378
+ const db2 = getDb();
379
+ db2.prepare(
380
+ "UPDATE projects SET claude_last_seen = CURRENT_TIMESTAMP WHERE id = ?"
381
+ ).run(projectId);
382
+ }
383
+ function updateProjectHeartbeatForTask(taskId) {
384
+ const db2 = getDb();
385
+ const task = db2.prepare("SELECT project_id FROM tasks WHERE id = ?").get(taskId);
386
+ if (task) {
387
+ updateProjectHeartbeat(task.project_id);
388
+ }
389
+ }
390
+
391
+ // src/utils/activity.ts
392
+ import { nanoid as nanoid2 } from "nanoid";
393
+ function logTaskActivity(taskId, type, oldValue = null, newValue = null) {
394
+ const db2 = getDb();
395
+ const id = nanoid2();
396
+ db2.prepare(`
397
+ INSERT INTO task_activity (id, task_id, type, old_value, new_value)
398
+ VALUES (?, ?, ?, ?, ?)
399
+ `).run(id, taskId, type, oldValue, newValue);
400
+ }
401
+
402
+ // src/utils/mappers.ts
403
+ function rowToTask(row) {
404
+ return {
405
+ ...row,
406
+ blocked: Boolean(row.blocked)
407
+ };
408
+ }
409
+ function rowToComment(row) {
410
+ return {
411
+ ...row,
412
+ seen: Boolean(row.seen)
413
+ };
414
+ }
415
+ function rowToTaskActivity(row) {
416
+ return {
417
+ ...row
418
+ };
419
+ }
420
+ function rowToAttachment(row) {
421
+ return {
422
+ ...row
423
+ };
424
+ }
425
+ function rowToPrompt(row) {
426
+ return {
427
+ ...row
428
+ };
429
+ }
430
+
431
+ // src/api/tasks.ts
432
+ var router2 = Router2();
433
+ router2.get("/project/:projectId", (req, res) => {
434
+ const db2 = getDb();
435
+ const { status } = req.query;
436
+ let query = "SELECT * FROM tasks WHERE project_id = ?";
437
+ const params = [req.params.projectId];
438
+ if (status) {
439
+ query += " AND status = ?";
440
+ params.push(status);
441
+ }
442
+ query += " ORDER BY position ASC";
443
+ const tasks = db2.prepare(query).all(...params);
444
+ res.json(tasks.map(rowToTask));
445
+ });
446
+ router2.get("/:id", (req, res) => {
447
+ const db2 = getDb();
448
+ const task = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(req.params.id);
449
+ if (!task) {
450
+ res.status(404).json({ error: "Task not found" });
451
+ return;
452
+ }
453
+ const commentsRaw = db2.prepare("SELECT * FROM comments WHERE task_id = ? ORDER BY created_at ASC").all(req.params.id);
454
+ const activitiesRaw = db2.prepare("SELECT * FROM task_activity WHERE task_id = ? ORDER BY created_at ASC").all(req.params.id);
455
+ const result = {
456
+ ...rowToTask(task),
457
+ comments: commentsRaw.map(rowToComment),
458
+ activities: activitiesRaw.map(rowToTaskActivity)
459
+ };
460
+ res.json(result);
461
+ });
462
+ router2.post("/project/:projectId", (req, res) => {
463
+ const { title, description, status = "backlog" } = req.body;
464
+ if (!title) {
465
+ res.status(400).json({ error: "title is required" });
466
+ return;
467
+ }
468
+ const db2 = getDb();
469
+ const projectId = req.params.projectId;
470
+ const project = db2.prepare("SELECT id FROM projects WHERE id = ?").get(projectId);
471
+ if (!project) {
472
+ res.status(404).json({ error: "Project not found" });
473
+ return;
474
+ }
475
+ const maxPosition = db2.prepare("SELECT COALESCE(MAX(position), -1) as max FROM tasks WHERE project_id = ? AND status = ?").get(projectId, status);
476
+ const id = nanoid3();
477
+ const position = maxPosition.max + 1;
478
+ db2.prepare(`
479
+ INSERT INTO tasks (id, project_id, title, description, status, position)
480
+ VALUES (?, ?, ?, ?, ?, ?)
481
+ `).run(id, projectId, title, description || null, status, position);
482
+ logTaskActivity(id, "created", null, status);
483
+ const task = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
484
+ res.status(201).json(rowToTask(task));
485
+ });
486
+ router2.patch("/:id", (req, res) => {
487
+ const db2 = getDb();
488
+ const task = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(req.params.id);
489
+ if (!task) {
490
+ res.status(404).json({ error: "Task not found" });
491
+ return;
492
+ }
493
+ const allowedFields = ["title", "description", "blocked", "current_activity"];
494
+ const updates = [];
495
+ const values = [];
496
+ for (const field of allowedFields) {
497
+ if (req.body[field] !== void 0) {
498
+ updates.push(`${field} = ?`);
499
+ values.push(field === "blocked" ? req.body[field] ? 1 : 0 : req.body[field]);
500
+ }
501
+ }
502
+ if (updates.length === 0) {
503
+ res.json(rowToTask(task));
504
+ return;
505
+ }
506
+ updates.push("updated_at = CURRENT_TIMESTAMP");
507
+ values.push(req.params.id);
508
+ db2.prepare(`UPDATE tasks SET ${updates.join(", ")} WHERE id = ?`).run(...values);
509
+ if (req.body.title !== void 0 && req.body.title !== task.title) {
510
+ logTaskActivity(req.params.id, "title_change", task.title, req.body.title);
511
+ }
512
+ if (req.body.description !== void 0 && req.body.description !== task.description) {
513
+ logTaskActivity(req.params.id, "description_change", task.description, req.body.description);
514
+ }
515
+ if (req.body.blocked !== void 0 && Boolean(req.body.blocked) !== Boolean(task.blocked)) {
516
+ logTaskActivity(req.params.id, "blocked_change", String(Boolean(task.blocked)), String(Boolean(req.body.blocked)));
517
+ }
518
+ if (req.body.current_activity !== void 0) {
519
+ updateProjectHeartbeat(task.project_id);
520
+ }
521
+ const updated = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(req.params.id);
522
+ res.json(rowToTask(updated));
523
+ });
524
+ router2.post("/:id/move", (req, res) => {
525
+ const { status, position, starting_commit } = req.body;
526
+ if (!status || position === void 0) {
527
+ res.status(400).json({ error: "status and position are required" });
528
+ return;
529
+ }
530
+ const db2 = getDb();
531
+ const task = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(req.params.id);
532
+ if (!task) {
533
+ res.status(404).json({ error: "Task not found" });
534
+ return;
535
+ }
536
+ const oldStatus = task.status;
537
+ const oldPosition = task.position;
538
+ db2.transaction(() => {
539
+ if (oldStatus === status) {
540
+ if (position > oldPosition) {
541
+ db2.prepare(`
542
+ UPDATE tasks SET position = position - 1
543
+ WHERE project_id = ? AND status = ? AND position > ? AND position <= ?
544
+ `).run(task.project_id, status, oldPosition, position);
545
+ } else if (position < oldPosition) {
546
+ db2.prepare(`
547
+ UPDATE tasks SET position = position + 1
548
+ WHERE project_id = ? AND status = ? AND position >= ? AND position < ?
549
+ `).run(task.project_id, status, position, oldPosition);
550
+ }
551
+ } else {
552
+ db2.prepare(`
553
+ UPDATE tasks SET position = position - 1
554
+ WHERE project_id = ? AND status = ? AND position > ?
555
+ `).run(task.project_id, oldStatus, oldPosition);
556
+ db2.prepare(`
557
+ UPDATE tasks SET position = position + 1
558
+ WHERE project_id = ? AND status = ? AND position >= ?
559
+ `).run(task.project_id, status, position);
560
+ }
561
+ if (status === "in_progress") {
562
+ if (starting_commit) {
563
+ db2.prepare(`
564
+ UPDATE tasks SET status = ?, position = ?, starting_commit = ?, started_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
565
+ WHERE id = ?
566
+ `).run(status, position, starting_commit, req.params.id);
567
+ } else {
568
+ db2.prepare(`
569
+ UPDATE tasks SET status = ?, position = ?, started_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
570
+ WHERE id = ?
571
+ `).run(status, position, req.params.id);
572
+ }
573
+ } else if (status === "done") {
574
+ db2.prepare(`
575
+ UPDATE tasks SET status = ?, position = ?, completed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
576
+ WHERE id = ?
577
+ `).run(status, position, req.params.id);
578
+ } else {
579
+ db2.prepare(`
580
+ UPDATE tasks SET status = ?, position = ?, updated_at = CURRENT_TIMESTAMP
581
+ WHERE id = ?
582
+ `).run(status, position, req.params.id);
583
+ }
584
+ })();
585
+ if (oldStatus !== status) {
586
+ logTaskActivity(req.params.id, "status_change", oldStatus, status);
587
+ }
588
+ if (status === "in_progress" || status === "done") {
589
+ updateProjectHeartbeat(task.project_id);
590
+ }
591
+ const updated = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(req.params.id);
592
+ res.json(rowToTask(updated));
593
+ });
594
+ router2.delete("/:id", (req, res) => {
595
+ const db2 = getDb();
596
+ const task = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(req.params.id);
597
+ if (!task) {
598
+ res.status(404).json({ error: "Task not found" });
599
+ return;
600
+ }
601
+ db2.transaction(() => {
602
+ db2.prepare(`
603
+ UPDATE tasks SET position = position - 1
604
+ WHERE project_id = ? AND status = ? AND position > ?
605
+ `).run(task.project_id, task.status, task.position);
606
+ db2.prepare("DELETE FROM tasks WHERE id = ?").run(req.params.id);
607
+ })();
608
+ res.json({ success: true });
609
+ });
610
+ router2.delete("/project/:projectId/status/:status", (req, res) => {
611
+ const db2 = getDb();
612
+ const { projectId, status } = req.params;
613
+ const project = db2.prepare("SELECT id FROM projects WHERE id = ?").get(projectId);
614
+ if (!project) {
615
+ res.status(404).json({ error: "Project not found" });
616
+ return;
617
+ }
618
+ const validStatuses = ["backlog", "ready", "in_progress", "done"];
619
+ if (!validStatuses.includes(status)) {
620
+ res.status(400).json({ error: "Invalid status" });
621
+ return;
622
+ }
623
+ const result = db2.prepare("DELETE FROM tasks WHERE project_id = ? AND status = ?").run(projectId, status);
624
+ res.json({ success: true, deleted: result.changes });
625
+ });
626
+ var tasks_default = router2;
627
+
628
+ // src/api/comments.ts
629
+ import { Router as Router3 } from "express";
630
+ import { nanoid as nanoid4 } from "nanoid";
631
+ var router3 = Router3();
632
+ router3.get("/task/:taskId", (req, res) => {
633
+ const db2 = getDb();
634
+ const { since } = req.query;
635
+ let query = "SELECT * FROM comments WHERE task_id = ?";
636
+ const params = [req.params.taskId];
637
+ if (since) {
638
+ query += " AND created_at > ?";
639
+ params.push(since);
640
+ }
641
+ query += " ORDER BY created_at ASC";
642
+ const comments = db2.prepare(query).all(...params);
643
+ res.json(comments.map(rowToComment));
644
+ });
645
+ router3.post("/task/:taskId", (req, res) => {
646
+ const { author, content } = req.body;
647
+ if (!author || !content) {
648
+ res.status(400).json({ error: "author and content are required" });
649
+ return;
650
+ }
651
+ if (author !== "user" && author !== "claude") {
652
+ res.status(400).json({ error: "author must be 'user' or 'claude'" });
653
+ return;
654
+ }
655
+ const db2 = getDb();
656
+ const taskId = req.params.taskId;
657
+ const task = db2.prepare("SELECT id FROM tasks WHERE id = ?").get(taskId);
658
+ if (!task) {
659
+ res.status(404).json({ error: "Task not found" });
660
+ return;
661
+ }
662
+ const id = nanoid4();
663
+ db2.prepare(`
664
+ INSERT INTO comments (id, task_id, author, content)
665
+ VALUES (?, ?, ?, ?)
666
+ `).run(id, taskId, author, content);
667
+ logTaskActivity(taskId, "comment_added", null, author);
668
+ if (author === "claude") {
669
+ updateProjectHeartbeatForTask(taskId);
670
+ }
671
+ const comment = db2.prepare("SELECT * FROM comments WHERE id = ?").get(id);
672
+ res.status(201).json(rowToComment(comment));
673
+ });
674
+ router3.delete("/:commentId", (req, res) => {
675
+ const db2 = getDb();
676
+ const commentId = req.params.commentId;
677
+ const comment = db2.prepare("SELECT * FROM comments WHERE id = ?").get(commentId);
678
+ if (!comment) {
679
+ res.status(404).json({ error: "Comment not found" });
680
+ return;
681
+ }
682
+ if (comment.seen) {
683
+ res.status(400).json({ error: "Cannot delete a comment that has been seen" });
684
+ return;
685
+ }
686
+ db2.prepare("DELETE FROM comments WHERE id = ?").run(commentId);
687
+ res.status(204).send();
688
+ });
689
+ router3.patch("/:commentId/seen", (req, res) => {
690
+ const db2 = getDb();
691
+ const commentId = req.params.commentId;
692
+ const comment = db2.prepare("SELECT * FROM comments WHERE id = ?").get(commentId);
693
+ if (!comment) {
694
+ res.status(404).json({ error: "Comment not found" });
695
+ return;
696
+ }
697
+ db2.prepare("UPDATE comments SET seen = 1 WHERE id = ?").run(commentId);
698
+ const updated = db2.prepare("SELECT * FROM comments WHERE id = ?").get(commentId);
699
+ res.json(rowToComment(updated));
700
+ });
701
+ router3.patch("/task/:taskId/mark-seen", (req, res) => {
702
+ const db2 = getDb();
703
+ const taskId = req.params.taskId;
704
+ db2.prepare("UPDATE comments SET seen = 1 WHERE task_id = ? AND author = 'user' AND seen = 0").run(taskId);
705
+ updateProjectHeartbeatForTask(taskId);
706
+ const comments = db2.prepare("SELECT * FROM comments WHERE task_id = ? ORDER BY created_at ASC").all(taskId);
707
+ res.json(comments.map(rowToComment));
708
+ });
709
+ router3.get("/task/:taskId/wait-for-reply", async (req, res) => {
710
+ const db2 = getDb();
711
+ const taskId = req.params.taskId;
712
+ const since = req.query.since;
713
+ const task = db2.prepare("SELECT id FROM tasks WHERE id = ?").get(taskId);
714
+ if (!task) {
715
+ res.status(404).json({ error: "Task not found", deleted: true });
716
+ return;
717
+ }
718
+ const pollInterval = 1e3;
719
+ const timeout = 3e4;
720
+ const startTime2 = Date.now();
721
+ const checkForReply = () => {
722
+ const taskExists = db2.prepare("SELECT id FROM tasks WHERE id = ?").get(taskId);
723
+ if (!taskExists) {
724
+ return null;
725
+ }
726
+ let query = "SELECT * FROM comments WHERE task_id = ? AND author = 'user'";
727
+ const params = [taskId];
728
+ if (since) {
729
+ query += " AND created_at > ?";
730
+ params.push(since);
731
+ }
732
+ query += " ORDER BY created_at DESC LIMIT 1";
733
+ const row = db2.prepare(query).get(...params);
734
+ return row ? rowToComment(row) : null;
735
+ };
736
+ const poll = () => {
737
+ const taskExists = db2.prepare("SELECT id FROM tasks WHERE id = ?").get(taskId);
738
+ if (!taskExists) {
739
+ res.json({ deleted: true });
740
+ return;
741
+ }
742
+ const reply = checkForReply();
743
+ if (reply) {
744
+ res.json(reply);
745
+ return;
746
+ }
747
+ if (Date.now() - startTime2 >= timeout) {
748
+ res.json({ timeout: true });
749
+ return;
750
+ }
751
+ setTimeout(poll, pollInterval);
752
+ };
753
+ poll();
754
+ });
755
+ var comments_default = router3;
756
+
757
+ // src/api/templates.ts
758
+ import { Router as Router4 } from "express";
759
+ import { nanoid as nanoid5 } from "nanoid";
760
+ var router4 = Router4();
761
+ router4.get("/project/:projectId", (req, res) => {
762
+ const db2 = getDb();
763
+ const templates = db2.prepare("SELECT * FROM templates WHERE project_id = ? ORDER BY position ASC").all(req.params.projectId);
764
+ res.json(templates);
765
+ });
766
+ router4.get("/:id", (req, res) => {
767
+ const db2 = getDb();
768
+ const template = db2.prepare("SELECT * FROM templates WHERE id = ?").get(req.params.id);
769
+ if (!template) {
770
+ res.status(404).json({ error: "Template not found" });
771
+ return;
772
+ }
773
+ res.json(template);
774
+ });
775
+ router4.post("/project/:projectId", (req, res) => {
776
+ const { title, description } = req.body;
777
+ if (!title) {
778
+ res.status(400).json({ error: "title is required" });
779
+ return;
780
+ }
781
+ const db2 = getDb();
782
+ const projectId = req.params.projectId;
783
+ const project = db2.prepare("SELECT id FROM projects WHERE id = ?").get(projectId);
784
+ if (!project) {
785
+ res.status(404).json({ error: "Project not found" });
786
+ return;
787
+ }
788
+ const maxPosition = db2.prepare("SELECT COALESCE(MAX(position), -1) as max FROM templates WHERE project_id = ?").get(projectId);
789
+ const id = nanoid5();
790
+ const position = maxPosition.max + 1;
791
+ db2.prepare(`
792
+ INSERT INTO templates (id, project_id, title, description, position)
793
+ VALUES (?, ?, ?, ?, ?)
794
+ `).run(id, projectId, title, description || null, position);
795
+ const template = db2.prepare("SELECT * FROM templates WHERE id = ?").get(id);
796
+ res.status(201).json(template);
797
+ });
798
+ router4.patch("/:id", (req, res) => {
799
+ const db2 = getDb();
800
+ const template = db2.prepare("SELECT * FROM templates WHERE id = ?").get(req.params.id);
801
+ if (!template) {
802
+ res.status(404).json({ error: "Template not found" });
803
+ return;
804
+ }
805
+ const allowedFields = ["title", "description"];
806
+ const updates = [];
807
+ const values = [];
808
+ for (const field of allowedFields) {
809
+ if (req.body[field] !== void 0) {
810
+ updates.push(`${field} = ?`);
811
+ values.push(req.body[field]);
812
+ }
813
+ }
814
+ if (updates.length === 0) {
815
+ res.json(template);
816
+ return;
817
+ }
818
+ values.push(req.params.id);
819
+ db2.prepare(`UPDATE templates SET ${updates.join(", ")} WHERE id = ?`).run(...values);
820
+ const updated = db2.prepare("SELECT * FROM templates WHERE id = ?").get(req.params.id);
821
+ res.json(updated);
822
+ });
823
+ router4.post("/:id/move", (req, res) => {
824
+ const { position } = req.body;
825
+ if (position === void 0) {
826
+ res.status(400).json({ error: "position is required" });
827
+ return;
828
+ }
829
+ const db2 = getDb();
830
+ const template = db2.prepare("SELECT * FROM templates WHERE id = ?").get(req.params.id);
831
+ if (!template) {
832
+ res.status(404).json({ error: "Template not found" });
833
+ return;
834
+ }
835
+ const oldPosition = template.position;
836
+ db2.transaction(() => {
837
+ if (position > oldPosition) {
838
+ db2.prepare(`
839
+ UPDATE templates SET position = position - 1
840
+ WHERE project_id = ? AND position > ? AND position <= ?
841
+ `).run(template.project_id, oldPosition, position);
842
+ } else if (position < oldPosition) {
843
+ db2.prepare(`
844
+ UPDATE templates SET position = position + 1
845
+ WHERE project_id = ? AND position >= ? AND position < ?
846
+ `).run(template.project_id, position, oldPosition);
847
+ }
848
+ db2.prepare(`UPDATE templates SET position = ? WHERE id = ?`).run(position, req.params.id);
849
+ })();
850
+ const updated = db2.prepare("SELECT * FROM templates WHERE id = ?").get(req.params.id);
851
+ res.json(updated);
852
+ });
853
+ router4.delete("/:id", (req, res) => {
854
+ const db2 = getDb();
855
+ const template = db2.prepare("SELECT * FROM templates WHERE id = ?").get(req.params.id);
856
+ if (!template) {
857
+ res.status(404).json({ error: "Template not found" });
858
+ return;
859
+ }
860
+ db2.transaction(() => {
861
+ db2.prepare(`
862
+ UPDATE templates SET position = position - 1
863
+ WHERE project_id = ? AND position > ?
864
+ `).run(template.project_id, template.position);
865
+ db2.prepare("DELETE FROM templates WHERE id = ?").run(req.params.id);
866
+ })();
867
+ res.json({ success: true });
868
+ });
869
+ router4.post("/:id/create-task", (req, res) => {
870
+ const { status = "backlog" } = req.body;
871
+ const validStatuses = ["backlog", "ready"];
872
+ if (!validStatuses.includes(status)) {
873
+ res.status(400).json({ error: "Invalid status. Must be backlog or ready" });
874
+ return;
875
+ }
876
+ const db2 = getDb();
877
+ const template = db2.prepare("SELECT * FROM templates WHERE id = ?").get(req.params.id);
878
+ if (!template) {
879
+ res.status(404).json({ error: "Template not found" });
880
+ return;
881
+ }
882
+ const maxPosition = db2.prepare("SELECT COALESCE(MAX(position), -1) as max FROM tasks WHERE project_id = ? AND status = ?").get(template.project_id, status);
883
+ const id = nanoid5();
884
+ const position = maxPosition.max + 1;
885
+ db2.prepare(`
886
+ INSERT INTO tasks (id, project_id, title, description, status, position)
887
+ VALUES (?, ?, ?, ?, ?, ?)
888
+ `).run(id, template.project_id, template.title, template.description, status, position);
889
+ const task = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
890
+ res.status(201).json({
891
+ ...task,
892
+ blocked: Boolean(task.blocked)
893
+ });
894
+ });
895
+ var templates_default = router4;
896
+
897
+ // src/api/attachments.ts
898
+ import { Router as Router5 } from "express";
899
+ import { nanoid as nanoid6 } from "nanoid";
900
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, unlinkSync, writeFileSync, readFileSync } from "fs";
901
+ import { homedir as homedir2 } from "os";
902
+ import { join as join2, extname } from "path";
903
+ var router5 = Router5();
904
+ var DATA_DIR2 = join2(homedir2(), ".claude-queue");
905
+ var ATTACHMENTS_DIR = join2(DATA_DIR2, "attachments");
906
+ function ensureAttachmentsDir() {
907
+ if (!existsSync2(ATTACHMENTS_DIR)) {
908
+ mkdirSync2(ATTACHMENTS_DIR, { recursive: true });
909
+ }
910
+ }
911
+ router5.get("/task/:taskId", (req, res) => {
912
+ const db2 = getDb();
913
+ const taskId = req.params.taskId;
914
+ const attachments = db2.prepare("SELECT * FROM attachments WHERE task_id = ? ORDER BY created_at ASC").all(taskId);
915
+ res.json(attachments.map(rowToAttachment));
916
+ });
917
+ router5.get("/template/:templateId", (req, res) => {
918
+ const db2 = getDb();
919
+ const templateId = req.params.templateId;
920
+ const attachments = db2.prepare("SELECT * FROM attachments WHERE template_id = ? ORDER BY created_at ASC").all(templateId);
921
+ res.json(attachments.map(rowToAttachment));
922
+ });
923
+ router5.post("/task/:taskId", (req, res) => {
924
+ const db2 = getDb();
925
+ const taskId = req.params.taskId;
926
+ const task = db2.prepare("SELECT id FROM tasks WHERE id = ?").get(taskId);
927
+ if (!task) {
928
+ res.status(404).json({ error: "Task not found" });
929
+ return;
930
+ }
931
+ const { filename, data, mimeType } = req.body;
932
+ if (!filename || !data || !mimeType) {
933
+ res.status(400).json({ error: "filename, data, and mimeType are required" });
934
+ return;
935
+ }
936
+ ensureAttachmentsDir();
937
+ const id = nanoid6();
938
+ const ext = extname(filename) || ".bin";
939
+ const storedFilename = `${id}${ext}`;
940
+ const filePath = join2(ATTACHMENTS_DIR, storedFilename);
941
+ const buffer = Buffer.from(data, "base64");
942
+ writeFileSync(filePath, buffer);
943
+ db2.prepare(`
944
+ INSERT INTO attachments (id, task_id, filename, original_name, mime_type, size)
945
+ VALUES (?, ?, ?, ?, ?, ?)
946
+ `).run(id, taskId, storedFilename, filename, mimeType, buffer.length);
947
+ const attachment = db2.prepare("SELECT * FROM attachments WHERE id = ?").get(id);
948
+ res.status(201).json(rowToAttachment(attachment));
949
+ });
950
+ router5.post("/template/:templateId", (req, res) => {
951
+ const db2 = getDb();
952
+ const templateId = req.params.templateId;
953
+ const template = db2.prepare("SELECT id FROM templates WHERE id = ?").get(templateId);
954
+ if (!template) {
955
+ res.status(404).json({ error: "Template not found" });
956
+ return;
957
+ }
958
+ const { filename, data, mimeType } = req.body;
959
+ if (!filename || !data || !mimeType) {
960
+ res.status(400).json({ error: "filename, data, and mimeType are required" });
961
+ return;
962
+ }
963
+ ensureAttachmentsDir();
964
+ const id = nanoid6();
965
+ const ext = extname(filename) || ".bin";
966
+ const storedFilename = `${id}${ext}`;
967
+ const filePath = join2(ATTACHMENTS_DIR, storedFilename);
968
+ const buffer = Buffer.from(data, "base64");
969
+ writeFileSync(filePath, buffer);
970
+ db2.prepare(`
971
+ INSERT INTO attachments (id, template_id, filename, original_name, mime_type, size)
972
+ VALUES (?, ?, ?, ?, ?, ?)
973
+ `).run(id, templateId, storedFilename, filename, mimeType, buffer.length);
974
+ const attachment = db2.prepare("SELECT * FROM attachments WHERE id = ?").get(id);
975
+ res.status(201).json(rowToAttachment(attachment));
976
+ });
977
+ router5.get("/:attachmentId/file", (req, res) => {
978
+ const db2 = getDb();
979
+ const attachmentId = req.params.attachmentId;
980
+ const attachment = db2.prepare("SELECT * FROM attachments WHERE id = ?").get(attachmentId);
981
+ if (!attachment) {
982
+ res.status(404).json({ error: "Attachment not found" });
983
+ return;
984
+ }
985
+ const filePath = join2(ATTACHMENTS_DIR, attachment.filename);
986
+ if (!existsSync2(filePath)) {
987
+ res.status(404).json({ error: "File not found" });
988
+ return;
989
+ }
990
+ res.setHeader("Content-Type", attachment.mime_type);
991
+ res.setHeader("Content-Disposition", `inline; filename="${attachment.original_name}"`);
992
+ res.send(readFileSync(filePath));
993
+ });
994
+ router5.get("/:attachmentId/path", (req, res) => {
995
+ const db2 = getDb();
996
+ const attachmentId = req.params.attachmentId;
997
+ const attachment = db2.prepare("SELECT * FROM attachments WHERE id = ?").get(attachmentId);
998
+ if (!attachment) {
999
+ res.status(404).json({ error: "Attachment not found" });
1000
+ return;
1001
+ }
1002
+ const filePath = join2(ATTACHMENTS_DIR, attachment.filename);
1003
+ if (!existsSync2(filePath)) {
1004
+ res.status(404).json({ error: "File not found" });
1005
+ return;
1006
+ }
1007
+ res.json({ path: filePath });
1008
+ });
1009
+ router5.delete("/:attachmentId", (req, res) => {
1010
+ const db2 = getDb();
1011
+ const attachmentId = req.params.attachmentId;
1012
+ const attachment = db2.prepare("SELECT * FROM attachments WHERE id = ?").get(attachmentId);
1013
+ if (!attachment) {
1014
+ res.status(404).json({ error: "Attachment not found" });
1015
+ return;
1016
+ }
1017
+ const filePath = join2(ATTACHMENTS_DIR, attachment.filename);
1018
+ if (existsSync2(filePath)) {
1019
+ unlinkSync(filePath);
1020
+ }
1021
+ db2.prepare("DELETE FROM attachments WHERE id = ?").run(attachmentId);
1022
+ res.status(204).send();
1023
+ });
1024
+ var attachments_default = router5;
1025
+
1026
+ // src/api/prompts.ts
1027
+ import { Router as Router6 } from "express";
1028
+ import { nanoid as nanoid7 } from "nanoid";
1029
+ var router6 = Router6();
1030
+ router6.get("/master", (req, res) => {
1031
+ const db2 = getDb();
1032
+ const prompt = db2.prepare("SELECT * FROM prompts WHERE type = 'master' LIMIT 1").get();
1033
+ if (!prompt) {
1034
+ res.json(null);
1035
+ return;
1036
+ }
1037
+ res.json(rowToPrompt(prompt));
1038
+ });
1039
+ router6.put("/master", (req, res) => {
1040
+ const db2 = getDb();
1041
+ const { content } = req.body;
1042
+ if (typeof content !== "string") {
1043
+ res.status(400).json({ error: "content is required" });
1044
+ return;
1045
+ }
1046
+ const existing = db2.prepare("SELECT * FROM prompts WHERE type = 'master' LIMIT 1").get();
1047
+ if (existing) {
1048
+ db2.prepare(`
1049
+ UPDATE prompts SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
1050
+ `).run(content, existing.id);
1051
+ } else {
1052
+ const id = nanoid7();
1053
+ db2.prepare(`
1054
+ INSERT INTO prompts (id, project_id, type, content)
1055
+ VALUES (?, NULL, 'master', ?)
1056
+ `).run(id, content);
1057
+ }
1058
+ const prompt = db2.prepare("SELECT * FROM prompts WHERE type = 'master' LIMIT 1").get();
1059
+ res.json(rowToPrompt(prompt));
1060
+ });
1061
+ router6.get("/project/:projectId", (req, res) => {
1062
+ const db2 = getDb();
1063
+ const projectId = req.params.projectId;
1064
+ const prompt = db2.prepare("SELECT * FROM prompts WHERE type = 'project' AND project_id = ? LIMIT 1").get(projectId);
1065
+ if (!prompt) {
1066
+ res.json(null);
1067
+ return;
1068
+ }
1069
+ res.json(rowToPrompt(prompt));
1070
+ });
1071
+ router6.put("/project/:projectId", (req, res) => {
1072
+ const db2 = getDb();
1073
+ const projectId = req.params.projectId;
1074
+ const { content } = req.body;
1075
+ if (typeof content !== "string") {
1076
+ res.status(400).json({ error: "content is required" });
1077
+ return;
1078
+ }
1079
+ const project = db2.prepare("SELECT id FROM projects WHERE id = ?").get(projectId);
1080
+ if (!project) {
1081
+ res.status(404).json({ error: "Project not found" });
1082
+ return;
1083
+ }
1084
+ const existing = db2.prepare("SELECT * FROM prompts WHERE type = 'project' AND project_id = ? LIMIT 1").get(projectId);
1085
+ if (existing) {
1086
+ db2.prepare(`
1087
+ UPDATE prompts SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
1088
+ `).run(content, existing.id);
1089
+ } else {
1090
+ const id = nanoid7();
1091
+ db2.prepare(`
1092
+ INSERT INTO prompts (id, project_id, type, content)
1093
+ VALUES (?, ?, 'project', ?)
1094
+ `).run(id, projectId, content);
1095
+ }
1096
+ const prompt = db2.prepare("SELECT * FROM prompts WHERE type = 'project' AND project_id = ? LIMIT 1").get(projectId);
1097
+ res.json(rowToPrompt(prompt));
1098
+ });
1099
+ router6.delete("/project/:projectId", (req, res) => {
1100
+ const db2 = getDb();
1101
+ const projectId = req.params.projectId;
1102
+ db2.prepare("DELETE FROM prompts WHERE type = 'project' AND project_id = ?").run(projectId);
1103
+ res.status(204).send();
1104
+ });
1105
+ var prompts_default = router6;
1106
+
1107
+ // src/api/health.ts
1108
+ import { Router as Router7 } from "express";
1109
+ import { readFileSync as readFileSync2 } from "fs";
1110
+ import { fileURLToPath } from "url";
1111
+ import { dirname, join as join3 } from "path";
1112
+ var __dirname = dirname(fileURLToPath(import.meta.url));
1113
+ var pkg = JSON.parse(readFileSync2(join3(__dirname, "..", "..", "package.json"), "utf-8"));
1114
+ var router7 = Router7();
1115
+ var startTime = Date.now();
1116
+ router7.get("/", (_req, res) => {
1117
+ const db2 = getDb();
1118
+ const projects = db2.prepare("SELECT id, name FROM projects").all();
1119
+ const projectsWithCounts = projects.map((p) => {
1120
+ const count = db2.prepare("SELECT COUNT(*) as count FROM tasks WHERE project_id = ?").get(p.id);
1121
+ return { ...p, tasks: count.count };
1122
+ });
1123
+ const uptimeMs = Date.now() - startTime;
1124
+ const uptimeMinutes = Math.floor(uptimeMs / 6e4);
1125
+ const uptimeHours = Math.floor(uptimeMinutes / 60);
1126
+ const uptime = uptimeHours > 0 ? `${uptimeHours}h ${uptimeMinutes % 60}m` : `${uptimeMinutes}m`;
1127
+ res.json({
1128
+ status: "ok",
1129
+ version: pkg.version,
1130
+ uptime,
1131
+ database: "connected",
1132
+ projects: projectsWithCounts
1133
+ });
1134
+ });
1135
+ var health_default = router7;
1136
+
1137
+ // src/api/maintenance.ts
1138
+ import { Router as Router8 } from "express";
1139
+ var router8 = Router8();
1140
+ router8.delete("/tasks/done", (req, res) => {
1141
+ const db2 = getDb();
1142
+ const result = db2.prepare("DELETE FROM tasks WHERE status = 'done'").run();
1143
+ res.json({ success: true, deleted: result.changes });
1144
+ });
1145
+ router8.delete("/tasks/all", (req, res) => {
1146
+ const db2 = getDb();
1147
+ const result = db2.prepare("DELETE FROM tasks").run();
1148
+ res.json({ success: true, deleted: result.changes });
1149
+ });
1150
+ router8.delete("/projects/all", (req, res) => {
1151
+ const db2 = getDb();
1152
+ const result = db2.prepare("DELETE FROM projects").run();
1153
+ res.json({ success: true, deleted: result.changes });
1154
+ });
1155
+ router8.delete("/activity/all", (req, res) => {
1156
+ const db2 = getDb();
1157
+ const result = db2.prepare("DELETE FROM task_activity").run();
1158
+ res.json({ success: true, deleted: result.changes });
1159
+ });
1160
+ router8.get("/stats", (_req, res) => {
1161
+ const db2 = getDb();
1162
+ const projectCount = db2.prepare("SELECT COUNT(*) as count FROM projects").get();
1163
+ const taskCount = db2.prepare("SELECT COUNT(*) as count FROM tasks").get();
1164
+ const tasksByStatus = db2.prepare(`
1165
+ SELECT status, COUNT(*) as count
1166
+ FROM tasks
1167
+ GROUP BY status
1168
+ `).all();
1169
+ const commentCount = db2.prepare("SELECT COUNT(*) as count FROM comments").get();
1170
+ const activityCount = db2.prepare("SELECT COUNT(*) as count FROM task_activity").get();
1171
+ const templateCount = db2.prepare("SELECT COUNT(*) as count FROM templates").get();
1172
+ res.json({
1173
+ projects: projectCount.count,
1174
+ tasks: {
1175
+ total: taskCount.count,
1176
+ byStatus: Object.fromEntries(tasksByStatus.map((row) => [row.status, row.count]))
1177
+ },
1178
+ comments: commentCount.count,
1179
+ activities: activityCount.count,
1180
+ templates: templateCount.count
1181
+ });
1182
+ });
1183
+ router8.post("/vacuum", (_req, res) => {
1184
+ const db2 = getDb();
1185
+ db2.exec("VACUUM");
1186
+ res.json({ success: true });
1187
+ });
1188
+ var maintenance_default = router8;
1189
+
1190
+ // src/logger.ts
1191
+ import { createWriteStream, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
1192
+ import { join as join4 } from "path";
1193
+ import { homedir as homedir3 } from "os";
1194
+ var KANBAN_DIR = join4(homedir3(), ".claude-queue");
1195
+ var LOG_FILE = join4(KANBAN_DIR, "server.log");
1196
+ var logStream = null;
1197
+ function ensureLogFile() {
1198
+ if (logStream) {
1199
+ return logStream;
1200
+ }
1201
+ if (!existsSync3(KANBAN_DIR)) {
1202
+ mkdirSync3(KANBAN_DIR, { recursive: true });
1203
+ }
1204
+ logStream = createWriteStream(LOG_FILE, { flags: "a" });
1205
+ return logStream;
1206
+ }
1207
+ function log(message) {
1208
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1209
+ const line = `[${timestamp}] ${message}
1210
+ `;
1211
+ process.stdout.write(line);
1212
+ const stream = ensureLogFile();
1213
+ stream.write(line);
1214
+ }
1215
+ function logRequest(method, url, status, duration) {
1216
+ log(`${method} ${url} ${status} ${duration}ms`);
1217
+ }
1218
+
1219
+ // src/index.ts
1220
+ var generateId2 = customAlphabet2("abcdefghijklmnopqrstuvwxyz0123456789", 4);
1221
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
1222
+ function ensureDevProject() {
1223
+ const db2 = getDb();
1224
+ const projects = db2.prepare("SELECT * FROM projects").all();
1225
+ if (projects.length === 0) {
1226
+ const projectRoot = process.env.DEV_PROJECT_ROOT || process.cwd();
1227
+ const name = projectRoot.split("/").pop() || "dev";
1228
+ const id = `kbn-${generateId2()}`;
1229
+ db2.prepare("INSERT INTO projects (id, path, name) VALUES (?, ?, ?)").run(id, projectRoot, name);
1230
+ seedDefaultTemplates(id);
1231
+ log(`Auto-created dev project: ${id} (${name})`);
1232
+ }
1233
+ }
1234
+ function createServer(port = 3333) {
1235
+ const app = express();
1236
+ app.use(cors());
1237
+ app.use(express.json({ limit: "50mb" }));
1238
+ app.use((req, res, next) => {
1239
+ const start = Date.now();
1240
+ res.on("finish", () => {
1241
+ const duration = Date.now() - start;
1242
+ const isHealthCheck = req.path === "/health" || req.path === "/api/health";
1243
+ if (!isHealthCheck) {
1244
+ logRequest(req.method, req.path, res.statusCode, duration);
1245
+ }
1246
+ });
1247
+ next();
1248
+ });
1249
+ app.use("/api/projects", projects_default);
1250
+ app.use("/api/tasks", tasks_default);
1251
+ app.use("/api/comments", comments_default);
1252
+ app.use("/api/templates", templates_default);
1253
+ app.use("/api/attachments", attachments_default);
1254
+ app.use("/api/prompts", prompts_default);
1255
+ app.use("/api/maintenance", maintenance_default);
1256
+ app.use("/health", health_default);
1257
+ const isDev = process.env.NODE_ENV === "development";
1258
+ const viteDevPort = process.env.VITE_DEV_PORT || "5173";
1259
+ if (isDev) {
1260
+ app.use(
1261
+ createProxyMiddleware({
1262
+ target: `http://localhost:${viteDevPort}`,
1263
+ changeOrigin: true,
1264
+ ws: true
1265
+ })
1266
+ );
1267
+ } else {
1268
+ const uiPaths = [
1269
+ join5(__dirname2, "../ui"),
1270
+ // npm package: dist/server/../ui = dist/ui
1271
+ join5(__dirname2, "../../ui/dist")
1272
+ // development: dist/../../ui/dist
1273
+ ];
1274
+ const uiPath = uiPaths.find((p) => existsSync4(join5(p, "index.html")));
1275
+ if (uiPath) {
1276
+ app.use(express.static(uiPath));
1277
+ app.get("*", (_req, res) => {
1278
+ res.sendFile(join5(uiPath, "index.html"));
1279
+ });
1280
+ }
1281
+ }
1282
+ const server = app.listen(port, () => {
1283
+ log(`Server running on http://localhost:${port}`);
1284
+ if (isDev) {
1285
+ ensureDevProject();
1286
+ }
1287
+ });
1288
+ const shutdown = () => {
1289
+ log("Shutting down...");
1290
+ server.close(() => {
1291
+ closeDb();
1292
+ process.exit(0);
1293
+ });
1294
+ };
1295
+ process.on("SIGINT", shutdown);
1296
+ process.on("SIGTERM", shutdown);
1297
+ return { app, server };
1298
+ }
1299
+ if (process.argv[1] === fileURLToPath2(import.meta.url)) {
1300
+ const port = parseInt(process.env.PORT || "3333", 10);
1301
+ createServer(port);
1302
+ }
1303
+ export {
1304
+ createServer,
1305
+ getDb
1306
+ };