-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.ts
More file actions
158 lines (123 loc) · 4.75 KB
/
server.ts
File metadata and controls
158 lines (123 loc) · 4.75 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
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";
import dotenv from "dotenv";
import express from "express";
import { createServer as createViteServer } from "vite";
import Anthropic from "@anthropic-ai/sdk";
import * as cheerio from "cheerio";
// Load .env from the same directory as server.ts
const __serverDir = dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: resolve(__serverDir, ".env") });
const apiKey = process.env.ANTHROPIC_API_KEY;
console.log(
`[Boot] ANTHROPIC_API_KEY: ${apiKey ? `✓ loaded (${apiKey.slice(0, 8)}...)` : "✗ MISSING — check .env"}`
);
const client = apiKey ? new Anthropic({ apiKey }) : null;
// Simple in-memory cache
const enrichmentCache = new Map<string, any>();
async function startServer() {
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json({ limit: "1mb" }));
app.get("/api/health", (_, res) => {
res.json({ status: "ok" });
});
app.post("/api/enrich", async (req, res) => {
if (!client) {
return res
.status(500)
.json({ error: "Anthropic API key not configured. Add ANTHROPIC_API_KEY to your .env file." });
}
try {
const { url } = req.body;
if (!url) {
return res.status(400).json({ error: "URL is required" });
}
try {
new URL(url);
} catch {
return res.status(400).json({ error: "Invalid URL format" });
}
if (enrichmentCache.has(url)) {
return res.json({ ...enrichmentCache.get(url), cached: true });
}
console.log(`🔍 Enriching: ${url}`);
// Fetch website content with timeout
let textContent = "";
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
const response = await fetch(url, {
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0",
},
signal: controller.signal,
});
clearTimeout(timeout);
if (response.ok) {
const html = await response.text();
const $ = cheerio.load(html);
$("script, style, nav, footer, svg, img, noscript").remove();
textContent = $("body").text().replace(/\s+/g, " ").trim().substring(0, 12000);
}
} catch (fetchErr: any) {
console.warn(`[Enrich] Website fetch failed: ${fetchErr.message} — using domain only`);
}
const domainOnly = !textContent || textContent.length < 150;
const contentSection = domainOnly
? `Company domain: ${url}\n(Website content could not be fetched — infer from the domain name and URL.)`
: `Website Content:\n${textContent}`;
const prompt = `You are a VC analyst AI. Analyze this company and return a JSON object with EXACTLY these fields:
{
"summary": "1-2 professional sentences describing what this company does",
"what_they_do": ["bullet point 1", "bullet point 2", "bullet point 3"],
"keywords": ["tag1", "tag2", "tag3", "tag4", "tag5"],
"derived_signals": ["signal1", "signal2", "signal3"]
}
"what_they_do" should have 3-5 items. "keywords" should have 5-8 items. "derived_signals" should have 2-4 items (e.g. "Hiring", "API Product", "Enterprise Focus", "B2B SaaS", "Open Source").
Respond with ONLY the JSON object — no markdown, no explanation.
${contentSection}`;
const message = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
const rawText =
message.content[0]?.type === "text" ? message.content[0].text : "";
// Strip markdown fences if present
const cleaned = rawText
.replace(/^```(?:json)?\s*/i, "")
.replace(/\s*```$/i, "")
.trim();
const enrichmentData = JSON.parse(cleaned);
const finalResponse = {
...enrichmentData,
sources: [{ url, timestamp: new Date().toISOString() }],
cached: false,
};
enrichmentCache.set(url, finalResponse);
res.json(finalResponse);
} catch (error: any) {
console.error("❌ Enrichment error:", error.message);
if (error.name === "AbortError") {
return res.status(504).json({ error: "Website fetch timed out" });
}
res.status(500).json({
error: error.message || "Failed to enrich company data",
});
}
});
// Vite middleware (dev only)
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "spa",
});
app.use(vite.middlewares);
}
app.listen(PORT, "0.0.0.0", () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});
}
startServer();