create-atsdc-stack 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,269 @@
1
+ /**
2
+ * Exa Search Integration
3
+ * Provides AI-powered search capabilities using Exa
4
+ * Documentation: https://docs.exa.ai
5
+ */
6
+
7
+ import Exa from 'exa-js';
8
+
9
+ /**
10
+ * Initialize Exa client
11
+ * Requires EXA_API_KEY environment variable
12
+ */
13
+ function getExaClient(): Exa {
14
+ const apiKey = import.meta.env.EXA_API_KEY || process.env.EXA_API_KEY;
15
+
16
+ if (!apiKey) {
17
+ throw new Error('EXA_API_KEY environment variable is not set');
18
+ }
19
+
20
+ return new Exa(apiKey);
21
+ }
22
+
23
+ /**
24
+ * Search options for Exa
25
+ */
26
+ export interface ExaSearchOptions {
27
+ /** Number of results to return (1-10, default: 10) */
28
+ numResults?: number;
29
+
30
+ /** Type of search: 'neural', 'keyword', or 'auto' (default: 'auto') */
31
+ type?: 'neural' | 'keyword' | 'auto';
32
+
33
+ /** Whether to use autoprompt to expand query (default: false) */
34
+ useAutoprompt?: boolean;
35
+
36
+ /** Category of content to search */
37
+ category?: 'company' | 'research paper' | 'news' | 'github' | 'tweet' | 'movie' | 'song' | 'personal site' | 'pdf';
38
+
39
+ /** Start crawl date (YYYY-MM-DD) */
40
+ startCrawlDate?: string;
41
+
42
+ /** End crawl date (YYYY-MM-DD) */
43
+ endCrawlDate?: string;
44
+
45
+ /** Start published date (YYYY-MM-DD) */
46
+ startPublishedDate?: string;
47
+
48
+ /** End published date (YYYY-MM-DD) */
49
+ endPublishedDate?: string;
50
+
51
+ /** Include domains (whitelist) */
52
+ includeDomains?: string[];
53
+
54
+ /** Exclude domains (blacklist) */
55
+ excludeDomains?: string[];
56
+
57
+ /** Whether to include page content */
58
+ contents?: {
59
+ text?: boolean | { maxCharacters?: number };
60
+ highlights?: boolean | { numSentences?: number; highlightsPerUrl?: number };
61
+ summary?: boolean | { query?: string };
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Exa search result
67
+ */
68
+ export interface ExaResult {
69
+ url: string;
70
+ title: string;
71
+ publishedDate?: string;
72
+ author?: string;
73
+ score?: number;
74
+ id: string;
75
+ text?: string;
76
+ highlights?: string[];
77
+ summary?: string;
78
+ }
79
+
80
+ /**
81
+ * Search the web using Exa
82
+ * @param query - The search query
83
+ * @param options - Search options
84
+ * @returns Array of search results
85
+ */
86
+ export async function searchWeb(
87
+ query: string,
88
+ options: ExaSearchOptions = {}
89
+ ): Promise<ExaResult[]> {
90
+ try {
91
+ const exa = getExaClient();
92
+
93
+ const response = await exa.searchAndContents(query, {
94
+ numResults: options.numResults || 10,
95
+ type: options.type || 'auto',
96
+ useAutoprompt: options.useAutoprompt || false,
97
+ category: options.category,
98
+ startCrawlDate: options.startCrawlDate,
99
+ endCrawlDate: options.endCrawlDate,
100
+ startPublishedDate: options.startPublishedDate,
101
+ endPublishedDate: options.endPublishedDate,
102
+ includeDomains: options.includeDomains,
103
+ excludeDomains: options.excludeDomains,
104
+ text: options.contents?.text,
105
+ highlights: options.contents?.highlights,
106
+ summary: options.contents?.summary,
107
+ });
108
+
109
+ return response.results.map((result: any) => ({
110
+ url: result.url,
111
+ title: result.title,
112
+ publishedDate: result.publishedDate,
113
+ author: result.author,
114
+ score: result.score,
115
+ id: result.id,
116
+ text: result.text,
117
+ highlights: result.highlights,
118
+ summary: result.summary,
119
+ }));
120
+ } catch (error) {
121
+ console.error('Exa search error:', error);
122
+ throw new Error('Failed to perform search');
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Find similar content to a given URL
128
+ * @param url - The URL to find similar content for
129
+ * @param options - Search options
130
+ * @returns Array of similar results
131
+ */
132
+ export async function findSimilar(
133
+ url: string,
134
+ options: Omit<ExaSearchOptions, 'type' | 'useAutoprompt'> = {}
135
+ ): Promise<ExaResult[]> {
136
+ try {
137
+ const exa = getExaClient();
138
+
139
+ const response = await exa.findSimilarAndContents(url, {
140
+ numResults: options.numResults || 10,
141
+ category: options.category,
142
+ startCrawlDate: options.startCrawlDate,
143
+ endCrawlDate: options.endCrawlDate,
144
+ startPublishedDate: options.startPublishedDate,
145
+ endPublishedDate: options.endPublishedDate,
146
+ includeDomains: options.includeDomains,
147
+ excludeDomains: options.excludeDomains,
148
+ text: options.contents?.text,
149
+ highlights: options.contents?.highlights,
150
+ summary: options.contents?.summary,
151
+ });
152
+
153
+ return response.results.map((result: any) => ({
154
+ url: result.url,
155
+ title: result.title,
156
+ publishedDate: result.publishedDate,
157
+ author: result.author,
158
+ score: result.score,
159
+ id: result.id,
160
+ text: result.text,
161
+ highlights: result.highlights,
162
+ summary: result.summary,
163
+ }));
164
+ } catch (error) {
165
+ console.error('Exa find similar error:', error);
166
+ throw new Error('Failed to find similar content');
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Get full page content for a list of URLs
172
+ * @param urls - Array of URLs to get content for
173
+ * @param options - Content options
174
+ * @returns Array of results with content
175
+ */
176
+ export async function getContents(
177
+ urls: string[],
178
+ options: ExaSearchOptions['contents'] = {}
179
+ ): Promise<ExaResult[]> {
180
+ try {
181
+ const exa = getExaClient();
182
+
183
+ const response = await exa.getContents(urls, {
184
+ text: options.text,
185
+ highlights: options.highlights,
186
+ summary: options.summary,
187
+ });
188
+
189
+ return response.results.map((result: any) => ({
190
+ url: result.url,
191
+ title: result.title,
192
+ publishedDate: result.publishedDate,
193
+ author: result.author,
194
+ id: result.id,
195
+ text: result.text,
196
+ highlights: result.highlights,
197
+ summary: result.summary,
198
+ }));
199
+ } catch (error) {
200
+ console.error('Exa get contents error:', error);
201
+ throw new Error('Failed to get contents');
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Search for recent news articles
207
+ * @param query - The search query
208
+ * @param daysBack - Number of days to look back (default: 7)
209
+ * @returns Array of news results
210
+ */
211
+ export async function searchNews(query: string, daysBack: number = 7): Promise<ExaResult[]> {
212
+ const endDate = new Date();
213
+ const startDate = new Date();
214
+ startDate.setDate(startDate.getDate() - daysBack);
215
+
216
+ return searchWeb(query, {
217
+ category: 'news',
218
+ startPublishedDate: startDate.toISOString().split('T')[0],
219
+ endPublishedDate: endDate.toISOString().split('T')[0],
220
+ numResults: 10,
221
+ contents: {
222
+ text: { maxCharacters: 1000 },
223
+ highlights: { numSentences: 3 },
224
+ },
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Search for research papers
230
+ * @param query - The search query
231
+ * @param options - Additional search options
232
+ * @returns Array of research paper results
233
+ */
234
+ export async function searchResearch(
235
+ query: string,
236
+ options: ExaSearchOptions = {}
237
+ ): Promise<ExaResult[]> {
238
+ return searchWeb(query, {
239
+ ...options,
240
+ category: 'research paper',
241
+ contents: {
242
+ text: { maxCharacters: 2000 },
243
+ summary: true,
244
+ ...options.contents,
245
+ },
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Search GitHub repositories
251
+ * @param query - The search query
252
+ * @param options - Additional search options
253
+ * @returns Array of GitHub results
254
+ */
255
+ export async function searchGitHub(
256
+ query: string,
257
+ options: ExaSearchOptions = {}
258
+ ): Promise<ExaResult[]> {
259
+ return searchWeb(query, {
260
+ ...options,
261
+ category: 'github',
262
+ includeDomains: ['github.com'],
263
+ contents: {
264
+ text: { maxCharacters: 1500 },
265
+ highlights: { numSentences: 5 },
266
+ ...options.contents,
267
+ },
268
+ });
269
+ }
@@ -0,0 +1,91 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { streamText } from 'ai';
3
+ import { z } from 'zod';
4
+
5
+ /**
6
+ * Vercel AI SDK Chat API Route
7
+ * Uses AI Gateway pattern - no provider-specific packages needed!
8
+ * Just pass model strings like 'openai/gpt-4o' or 'anthropic/claude-3-5-sonnet-20241022'
9
+ *
10
+ * Environment variables required:
11
+ * - OPENAI_API_KEY: Your OpenAI API key (or other provider keys)
12
+ */
13
+
14
+ // Validate request schema
15
+ const requestSchema = z.object({
16
+ messages: z.array(
17
+ z.object({
18
+ role: z.enum(['user', 'assistant', 'system']),
19
+ content: z.string(),
20
+ })
21
+ ),
22
+ model: z.string().optional().default('openai/gpt-4o'),
23
+ temperature: z.number().min(0).max(2).optional().default(0.7),
24
+ maxTokens: z.number().positive().optional().default(1000),
25
+ });
26
+
27
+ export const POST: APIRoute = async ({ request }) => {
28
+ try {
29
+ // Validate API key
30
+ if (!import.meta.env.OPENAI_API_KEY && !process.env.OPENAI_API_KEY) {
31
+ return new Response(
32
+ JSON.stringify({
33
+ error: 'OPENAI_API_KEY is not configured',
34
+ }),
35
+ {
36
+ status: 500,
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ },
40
+ }
41
+ );
42
+ }
43
+
44
+ // Parse and validate request body
45
+ const body = await request.json();
46
+ const validatedData = requestSchema.parse(body);
47
+
48
+ // Stream the response using Vercel AI SDK with AI Gateway
49
+ const result = streamText({
50
+ model: validatedData.model, // Use model string directly (e.g., 'openai/gpt-4o')
51
+ messages: validatedData.messages,
52
+ temperature: validatedData.temperature,
53
+ maxTokens: validatedData.maxTokens,
54
+ apiKey: import.meta.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY,
55
+ });
56
+
57
+ // Return the stream response
58
+ return result.toDataStreamResponse();
59
+ } catch (error) {
60
+ // Handle Zod validation errors
61
+ if (error instanceof z.ZodError) {
62
+ return new Response(
63
+ JSON.stringify({
64
+ error: 'Validation error',
65
+ details: error.errors,
66
+ }),
67
+ {
68
+ status: 400,
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ },
72
+ }
73
+ );
74
+ }
75
+
76
+ // Handle other errors
77
+ console.error('Chat API error:', error);
78
+ return new Response(
79
+ JSON.stringify({
80
+ error: 'Internal server error',
81
+ message: error instanceof Error ? error.message : 'Unknown error',
82
+ }),
83
+ {
84
+ status: 500,
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ },
88
+ }
89
+ );
90
+ }
91
+ };
@@ -0,0 +1,350 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { db } from '@/db/initialize';
3
+ import { posts } from '@/db/schema';
4
+ import {
5
+ createPostSchema,
6
+ updatePostSchema,
7
+ postQuerySchema,
8
+ type CreatePostInput,
9
+ type UpdatePostInput,
10
+ } from '@/db/validations';
11
+ import { eq, desc, and, ilike, or } from 'drizzle-orm';
12
+ import { z } from 'zod';
13
+
14
+ /**
15
+ * Posts API Routes
16
+ * Demonstrates CRUD operations with Drizzle ORM, Zod validation, and NanoID
17
+ */
18
+
19
+ // GET /api/posts - List posts with pagination and filtering
20
+ export const GET: APIRoute = async ({ request, url }) => {
21
+ try {
22
+ // Parse and validate query parameters
23
+ const queryParams = {
24
+ page: url.searchParams.get('page'),
25
+ limit: url.searchParams.get('limit'),
26
+ published: url.searchParams.get('published'),
27
+ featured: url.searchParams.get('featured'),
28
+ authorId: url.searchParams.get('authorId'),
29
+ search: url.searchParams.get('search'),
30
+ };
31
+
32
+ const validated = postQuerySchema.parse(queryParams);
33
+ const offset = (validated.page - 1) * validated.limit;
34
+
35
+ // Build query conditions
36
+ const conditions = [];
37
+
38
+ if (validated.published !== undefined) {
39
+ conditions.push(eq(posts.published, validated.published));
40
+ }
41
+
42
+ if (validated.featured !== undefined) {
43
+ conditions.push(eq(posts.featured, validated.featured));
44
+ }
45
+
46
+ if (validated.authorId) {
47
+ conditions.push(eq(posts.authorId, validated.authorId));
48
+ }
49
+
50
+ if (validated.search) {
51
+ conditions.push(
52
+ or(
53
+ ilike(posts.title, `%${validated.search}%`),
54
+ ilike(posts.content, `%${validated.search}%`)
55
+ )!
56
+ );
57
+ }
58
+
59
+ // Fetch posts with filtering and pagination
60
+ const results = await db
61
+ .select()
62
+ .from(posts)
63
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
64
+ .orderBy(desc(posts.createdAt))
65
+ .limit(validated.limit)
66
+ .offset(offset);
67
+
68
+ return new Response(
69
+ JSON.stringify({
70
+ data: results,
71
+ meta: {
72
+ page: validated.page,
73
+ limit: validated.limit,
74
+ total: results.length,
75
+ },
76
+ }),
77
+ {
78
+ status: 200,
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ },
82
+ }
83
+ );
84
+ } catch (error) {
85
+ if (error instanceof z.ZodError) {
86
+ return new Response(
87
+ JSON.stringify({
88
+ error: 'Validation error',
89
+ details: error.errors,
90
+ }),
91
+ {
92
+ status: 400,
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ },
96
+ }
97
+ );
98
+ }
99
+
100
+ console.error('GET /api/posts error:', error);
101
+ return new Response(
102
+ JSON.stringify({
103
+ error: 'Internal server error',
104
+ }),
105
+ {
106
+ status: 500,
107
+ headers: {
108
+ 'Content-Type': 'application/json',
109
+ },
110
+ }
111
+ );
112
+ }
113
+ };
114
+
115
+ // POST /api/posts - Create a new post
116
+ export const POST: APIRoute = async ({ request }) => {
117
+ try {
118
+ const body = await request.json();
119
+
120
+ // Validate request body with Zod
121
+ const validated: CreatePostInput = createPostSchema.parse(body);
122
+
123
+ // Insert post into database (NanoID is auto-generated)
124
+ const [newPost] = await db
125
+ .insert(posts)
126
+ .values({
127
+ ...validated,
128
+ publishedAt: validated.published ? new Date() : null,
129
+ })
130
+ .returning();
131
+
132
+ return new Response(
133
+ JSON.stringify({
134
+ data: newPost,
135
+ message: 'Post created successfully',
136
+ }),
137
+ {
138
+ status: 201,
139
+ headers: {
140
+ 'Content-Type': 'application/json',
141
+ },
142
+ }
143
+ );
144
+ } catch (error) {
145
+ if (error instanceof z.ZodError) {
146
+ return new Response(
147
+ JSON.stringify({
148
+ error: 'Validation error',
149
+ details: error.errors,
150
+ }),
151
+ {
152
+ status: 400,
153
+ headers: {
154
+ 'Content-Type': 'application/json',
155
+ },
156
+ }
157
+ );
158
+ }
159
+
160
+ console.error('POST /api/posts error:', error);
161
+ return new Response(
162
+ JSON.stringify({
163
+ error: 'Internal server error',
164
+ }),
165
+ {
166
+ status: 500,
167
+ headers: {
168
+ 'Content-Type': 'application/json',
169
+ },
170
+ }
171
+ );
172
+ }
173
+ };
174
+
175
+ // PUT /api/posts - Update an existing post
176
+ export const PUT: APIRoute = async ({ request }) => {
177
+ try {
178
+ const body = await request.json();
179
+
180
+ // Validate request body with Zod
181
+ const validated: UpdatePostInput = updatePostSchema.parse(body);
182
+ const { id, ...updateData } = validated;
183
+
184
+ if (!id) {
185
+ return new Response(
186
+ JSON.stringify({
187
+ error: 'Post ID is required',
188
+ }),
189
+ {
190
+ status: 400,
191
+ headers: {
192
+ 'Content-Type': 'application/json',
193
+ },
194
+ }
195
+ );
196
+ }
197
+
198
+ // Update post in database
199
+ const [updatedPost] = await db
200
+ .update(posts)
201
+ .set({
202
+ ...updateData,
203
+ updatedAt: new Date(),
204
+ publishedAt:
205
+ updateData.published !== undefined
206
+ ? updateData.published
207
+ ? new Date()
208
+ : null
209
+ : undefined,
210
+ })
211
+ .where(eq(posts.id, id))
212
+ .returning();
213
+
214
+ if (!updatedPost) {
215
+ return new Response(
216
+ JSON.stringify({
217
+ error: 'Post not found',
218
+ }),
219
+ {
220
+ status: 404,
221
+ headers: {
222
+ 'Content-Type': 'application/json',
223
+ },
224
+ }
225
+ );
226
+ }
227
+
228
+ return new Response(
229
+ JSON.stringify({
230
+ data: updatedPost,
231
+ message: 'Post updated successfully',
232
+ }),
233
+ {
234
+ status: 200,
235
+ headers: {
236
+ 'Content-Type': 'application/json',
237
+ },
238
+ }
239
+ );
240
+ } catch (error) {
241
+ if (error instanceof z.ZodError) {
242
+ return new Response(
243
+ JSON.stringify({
244
+ error: 'Validation error',
245
+ details: error.errors,
246
+ }),
247
+ {
248
+ status: 400,
249
+ headers: {
250
+ 'Content-Type': 'application/json',
251
+ },
252
+ }
253
+ );
254
+ }
255
+
256
+ console.error('PUT /api/posts error:', error);
257
+ return new Response(
258
+ JSON.stringify({
259
+ error: 'Internal server error',
260
+ }),
261
+ {
262
+ status: 500,
263
+ headers: {
264
+ 'Content-Type': 'application/json',
265
+ },
266
+ }
267
+ );
268
+ }
269
+ };
270
+
271
+ // DELETE /api/posts - Delete a post
272
+ export const DELETE: APIRoute = async ({ request, url }) => {
273
+ try {
274
+ const id = url.searchParams.get('id');
275
+
276
+ if (!id) {
277
+ return new Response(
278
+ JSON.stringify({
279
+ error: 'Post ID is required',
280
+ }),
281
+ {
282
+ status: 400,
283
+ headers: {
284
+ 'Content-Type': 'application/json',
285
+ },
286
+ }
287
+ );
288
+ }
289
+
290
+ // Validate ID format (NanoID is 21 characters)
291
+ if (id.length !== 21) {
292
+ return new Response(
293
+ JSON.stringify({
294
+ error: 'Invalid post ID format',
295
+ }),
296
+ {
297
+ status: 400,
298
+ headers: {
299
+ 'Content-Type': 'application/json',
300
+ },
301
+ }
302
+ );
303
+ }
304
+
305
+ // Delete post from database
306
+ const [deletedPost] = await db
307
+ .delete(posts)
308
+ .where(eq(posts.id, id))
309
+ .returning();
310
+
311
+ if (!deletedPost) {
312
+ return new Response(
313
+ JSON.stringify({
314
+ error: 'Post not found',
315
+ }),
316
+ {
317
+ status: 404,
318
+ headers: {
319
+ 'Content-Type': 'application/json',
320
+ },
321
+ }
322
+ );
323
+ }
324
+
325
+ return new Response(
326
+ JSON.stringify({
327
+ message: 'Post deleted successfully',
328
+ }),
329
+ {
330
+ status: 200,
331
+ headers: {
332
+ 'Content-Type': 'application/json',
333
+ },
334
+ }
335
+ );
336
+ } catch (error) {
337
+ console.error('DELETE /api/posts error:', error);
338
+ return new Response(
339
+ JSON.stringify({
340
+ error: 'Internal server error',
341
+ }),
342
+ {
343
+ status: 500,
344
+ headers: {
345
+ 'Content-Type': 'application/json',
346
+ },
347
+ }
348
+ );
349
+ }
350
+ };