This repository was archived by the owner on Oct 22, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 44
Expand file tree
/
Copy pathindex.ts
More file actions
221 lines (201 loc) · 6.17 KB
/
index.ts
File metadata and controls
221 lines (201 loc) · 6.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import dotenv from "dotenv";
import { createClient } from "rivetkit/client";
import { app } from "../workers/app";
import type { App } from "../workers/app";
import type { LinearWebhookEvent } from "../types";
// Load environment variables
dotenv.config();
// Create Hono app
const server = new Hono();
const PORT = process.env.PORT || 8080;
// Create actor client
const ACTOR_SERVER_URL =
process.env.ACTOR_SERVER_URL || "http://localhost:6420";
const client = createClient<App>(ACTOR_SERVER_URL);
// Middleware to initialize agent
server.use("*", async (c, next) => {
try {
// Initialize any new actor instances with repository settings
await next();
} catch (error) {
console.error("[SERVER] Error in middleware:", error);
return c.json(
{
status: "error",
statusEmoji: "❌",
message: error instanceof Error ? error.message : "Unknown error",
},
500,
);
}
});
// Route for Linear webhooks
server.post("/api/webhook/linear", async (c) => {
try {
// Get raw payload for signature verification
const rawBody = await c.req.text();
// Verify webhook signature
const signature = c.req.header("linear-signature");
const webhookSecret = process.env.LINEAR_WEBHOOK_SECRET;
if (webhookSecret) {
// Only verify if webhook secret is configured
const crypto = await import("crypto");
const computedSignature = crypto
.createHmac("sha256", webhookSecret)
.update(rawBody)
.digest("hex");
if (signature !== computedSignature) {
console.error("[SERVER] Invalid webhook signature");
return c.json(
{
status: "error",
statusEmoji: "❌",
message: "Invalid webhook signature",
},
401,
);
}
} else {
console.warn(
"[SERVER] LINEAR_WEBHOOK_SECRET not configured, skipping signature verification",
);
}
// Parse the webhook payload
const event = JSON.parse(rawBody) as LinearWebhookEvent;
console.log(
`[SERVER] Received Linear webhook: ${event.type} - ${event.action}`,
);
// Determine the issue ID to use as a tag for the actor
const issueId = event.data.issue?.id ?? event.data.id;
if (!issueId) {
console.error("[SERVER] No issue ID found in webhook event");
return c.json(
{
status: "error",
statusEmoji: "❌",
message: "No issue ID found in webhook event",
},
400,
);
}
// Create or get a coding agent instance with the issue ID as a key
// This ensures each issue gets its own actor instance
console.log(`[SERVER] Getting actor for issue: ${issueId}`);
const actorClient = client.codingAgent.getOrCreate(issueId).connect();
// Initialize the agent if needed
console.log(`[SERVER] Initializing actor for issue: ${issueId}`);
await actorClient.initialize();
// Determine which handler to use based on the event type and action
if (event.type === "Issue" && event.action === "create") {
// Handle new issue creation
console.log(
`[SERVER] Processing issue creation: ${issueId} - ${event.data.title}`,
);
const result = await actorClient.issueCreated(event);
return c.json({
status: "success",
message: result.message || "Issue creation event queued for processing",
requestId: result.requestId,
});
} else if (event.type === "Comment" && event.action === "create") {
// Handle new comment with enhanced logging
console.log(`[SERVER] Processing comment creation on issue: ${issueId}`);
console.log(
`[SERVER] Comment details: ID=${event.data.id}, Body="${event.data.body?.substring(0, 100)}${event.data.body && event.data.body.length > 100 ? "..." : ""}", UserIsBot=${event.data.user?.isBot}`,
);
// Early detection of bot comments to avoid unnecessary processing
if (event.data.user?.isBot) {
console.log(
`[SERVER] Skipping comment from bot user - preventing feedback loop`,
);
return c.json({
status: "skipped",
message: "Comment skipped - from bot user",
statusEmoji: "⏭",
});
}
// Check for bot emojis at the start of comment
if (
event.data.body &&
(event.data.body.startsWith("✅") ||
event.data.body.startsWith("❌") ||
event.data.body.startsWith("🤖"))
) {
console.log(
`[SERVER] Skipping comment with bot emoji: "${event.data.body?.substring(0, 20)}..."`,
);
return c.json({
status: "skipped",
message: "Comment skipped - contains bot emoji",
statusEmoji: "⏭",
});
}
const result = await actorClient.commentCreated(event);
console.log(
`[SERVER] Comment sent to actor for processing, requestId: ${result.requestId}`,
);
return c.json({
status: "success",
message:
result.message || "Comment creation event queued for processing",
requestId: result.requestId,
});
} else if (event.type === "Issue" && event.action === "update") {
// Handle issue updates (status changes)
console.log(
`[SERVER] Processing issue update: ${issueId} - New state: ${event.data.state?.name}`,
);
const result = await actorClient.issueUpdated(event);
return c.json({
status: "success",
message: result.message || "Issue update event queued for processing",
requestId: result.requestId,
});
} else {
// Unhandled event type
console.log(
`[SERVER] Unhandled event type: ${event.type} - ${event.action}`,
);
return c.json({
status: "skipped",
statusEmoji: "⏭",
message: "Event type not handled",
});
}
} catch (error) {
console.error("[SERVER] Error processing webhook:", error);
return c.json(
{
status: "error",
statusEmoji: "❌",
message: error instanceof Error ? error.message : "Unknown error",
},
500,
);
}
});
// Health check endpoint
server.get("/health", (c) => {
console.log("[SERVER] Health check requested");
return c.json({
status: "ok",
statusEmoji: "✅",
message: "Service is healthy",
});
});
// Start the server
console.log(`[SERVER] Starting server on port ${PORT}...`);
serve(
{
fetch: server.fetch,
port: Number(PORT),
},
(info) => {
console.log(`[SERVER] Running on port ${info.port}`);
console.log(
`[SERVER] Linear webhook URL: http://localhost:${info.port}/api/webhook/linear`,
);
},
);