feat: webhook to Clawdbot when task activated + session auth for all mutations
This commit is contained in:
@@ -5,6 +5,37 @@ import { eq, asc, desc, sql, inArray } from "drizzle-orm";
|
||||
import { auth } from "../lib/auth";
|
||||
|
||||
const BEARER_TOKEN = process.env.API_BEARER_TOKEN || "hammer-dev-token";
|
||||
const CLAWDBOT_HOOK_URL = process.env.CLAWDBOT_HOOK_URL || "http://127.0.0.1:18789/hooks/agent";
|
||||
const CLAWDBOT_HOOK_TOKEN = process.env.CLAWDBOT_HOOK_TOKEN || "";
|
||||
|
||||
// Fire webhook to Clawdbot when a task is activated
|
||||
async function notifyTaskActivated(task: { id: string; title: string; description: string | null; source: string; priority: string }) {
|
||||
if (!CLAWDBOT_HOOK_TOKEN) {
|
||||
console.warn("CLAWDBOT_HOOK_TOKEN not set — skipping webhook");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const message = `🔨 Task activated in Hammer Queue:\n\nTitle: ${task.title}\nPriority: ${task.priority}\nSource: ${task.source}\nID: ${task.id}\n${task.description ? `\nDescription: ${task.description}` : ""}\n\nStart working on this task. Post progress notes to the queue API as you work:\ncurl -s -H "Authorization: Bearer $HAMMER_QUEUE_API_KEY" -H "Content-Type: application/json" -X POST "https://queue.donovankelly.xyz/api/tasks/${task.id}/notes" -d '{"note":"your update here"}'`;
|
||||
|
||||
await fetch(CLAWDBOT_HOOK_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${CLAWDBOT_HOOK_TOKEN}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
name: "HammerQueue",
|
||||
sessionKey: `hook:queue:${task.id}`,
|
||||
deliver: true,
|
||||
channel: "telegram",
|
||||
}),
|
||||
});
|
||||
console.log(`Webhook fired for task ${task.id}: ${task.title}`);
|
||||
} catch (err) {
|
||||
console.error("Failed to fire webhook:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Status sort order: active first, then queued, blocked, completed, cancelled
|
||||
const statusOrder = sql`CASE
|
||||
@@ -62,11 +93,11 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
return allTasks;
|
||||
})
|
||||
|
||||
// POST create task - requires auth
|
||||
// POST create task - requires session or bearer auth
|
||||
.post(
|
||||
"/",
|
||||
async ({ body, headers }) => {
|
||||
requireBearerAuth(headers);
|
||||
async ({ body, request, headers }) => {
|
||||
await requireSessionOrBearer(request, headers);
|
||||
// Get max position for queued tasks
|
||||
const maxPos = await db
|
||||
.select({ max: sql<number>`COALESCE(MAX(${tasks.position}), 0)` })
|
||||
@@ -120,11 +151,11 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
}
|
||||
)
|
||||
|
||||
// PATCH update task - requires auth
|
||||
// PATCH update task - requires session or bearer auth
|
||||
.patch(
|
||||
"/:id",
|
||||
async ({ params, body, headers }) => {
|
||||
requireBearerAuth(headers);
|
||||
async ({ params, body, request, headers }) => {
|
||||
await requireSessionOrBearer(request, headers);
|
||||
const updates: Record<string, any> = { updatedAt: new Date() };
|
||||
if (body.title !== undefined) updates.title = body.title;
|
||||
if (body.description !== undefined) updates.description = body.description;
|
||||
@@ -151,6 +182,12 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
.where(eq(tasks.id, params.id))
|
||||
.returning();
|
||||
if (!updated.length) throw new Error("Task not found");
|
||||
|
||||
// Fire webhook if task was just activated
|
||||
if (body.status === "active") {
|
||||
notifyTaskActivated(updated[0]);
|
||||
}
|
||||
|
||||
return updated[0];
|
||||
},
|
||||
{
|
||||
@@ -169,8 +206,8 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
// POST add progress note - requires auth
|
||||
.post(
|
||||
"/:id/notes",
|
||||
async ({ params, body, headers }) => {
|
||||
requireBearerAuth(headers);
|
||||
async ({ params, body, request, headers }) => {
|
||||
await requireSessionOrBearer(request, headers);
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
@@ -200,8 +237,8 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
// PATCH reorder tasks - requires auth
|
||||
.patch(
|
||||
"/reorder",
|
||||
async ({ body, headers }) => {
|
||||
requireBearerAuth(headers);
|
||||
async ({ body, request, headers }) => {
|
||||
await requireSessionOrBearer(request, headers);
|
||||
// body.ids is an ordered array of task IDs
|
||||
const updates = body.ids.map((id: string, index: number) =>
|
||||
db
|
||||
@@ -220,8 +257,8 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" })
|
||||
// DELETE task - requires auth
|
||||
.delete(
|
||||
"/:id",
|
||||
async ({ params, headers }) => {
|
||||
requireBearerAuth(headers);
|
||||
async ({ params, request, headers }) => {
|
||||
await requireSessionOrBearer(request, headers);
|
||||
const deleted = await db
|
||||
.delete(tasks)
|
||||
.where(eq(tasks.id, params.id))
|
||||
|
||||
Reference in New Issue
Block a user