@ai-sdk/devtools 0.0.5 → 0.0.7

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,286 @@
1
+ import { serve } from '@hono/node-server';
2
+ import { serveStatic } from '@hono/node-server/serve-static';
3
+ import { Hono } from 'hono';
4
+ import { cors } from 'hono/cors';
5
+ import { streamSSE } from 'hono/streaming';
6
+ import path from 'path';
7
+ import fs from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import {
10
+ getRuns,
11
+ getRunWithSteps,
12
+ getStepsForRun,
13
+ clearDatabase,
14
+ reloadDb,
15
+ } from '../db.js';
16
+
17
+ // SSE client management
18
+ type SSEClient = {
19
+ id: string;
20
+ controller: ReadableStreamDefaultController<string>;
21
+ };
22
+
23
+ const sseClients = new Set<SSEClient>();
24
+
25
+ const broadcastToClients = (event: string, data: Record<string, unknown>) => {
26
+ const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
27
+ for (const client of sseClients) {
28
+ try {
29
+ client.controller.enqueue(message);
30
+ } catch {
31
+ // Client disconnected, will be cleaned up
32
+ sseClients.delete(client);
33
+ }
34
+ }
35
+ };
36
+
37
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
38
+
39
+ // Determine if we're running from source (tsx) or built (dist)
40
+ const isDevMode =
41
+ __dirname.includes('/src/') || process.env.NODE_ENV === 'development';
42
+ const projectRoot = isDevMode
43
+ ? path.resolve(__dirname, '../..')
44
+ : path.resolve(__dirname, '../..');
45
+
46
+ // Client directory: dist/client in both cases
47
+ const clientDir = path.join(projectRoot, 'dist/client');
48
+
49
+ const app = new Hono();
50
+
51
+ // Enable CORS for development
52
+ app.use('/*', cors());
53
+
54
+ // API Routes
55
+ app.get('/api/runs', async c => {
56
+ const runs = await getRuns();
57
+ // Include step count, first message, and error status for each run
58
+ const runsWithMeta = await Promise.all(
59
+ runs.map(async run => {
60
+ const steps = await getStepsForRun(run.id);
61
+ let firstMessage = 'No user message';
62
+ let hasError = false;
63
+ let isInProgress = false;
64
+
65
+ // Extract last user message from first step
66
+ const firstStep = steps[0];
67
+ if (firstStep) {
68
+ try {
69
+ const input = JSON.parse(firstStep.input);
70
+ const userMsg = input?.prompt?.findLast(
71
+ (m: any) => m.role === 'user',
72
+ );
73
+ if (userMsg) {
74
+ const content =
75
+ typeof userMsg.content === 'string'
76
+ ? userMsg.content
77
+ : userMsg.content?.[0]?.text || '';
78
+ firstMessage =
79
+ content.slice(0, 60) + (content.length > 60 ? '...' : '');
80
+ }
81
+ } catch {
82
+ // Ignore JSON parse errors
83
+ }
84
+
85
+ // Check for errors
86
+ hasError = steps.some(s => s.error);
87
+ // Check if any step is still in progress (no output yet)
88
+ isInProgress = steps.some(s => s.duration_ms === null && !s.error);
89
+ }
90
+
91
+ return {
92
+ ...run,
93
+ stepCount: steps.length,
94
+ firstMessage,
95
+ hasError,
96
+ isInProgress,
97
+ type: firstStep?.type,
98
+ };
99
+ }),
100
+ );
101
+ return c.json(runsWithMeta);
102
+ });
103
+
104
+ app.get('/api/runs/:id', async c => {
105
+ const data = await getRunWithSteps(c.req.param('id'));
106
+ if (!data) {
107
+ return c.json({ error: 'Run not found' }, 404);
108
+ }
109
+ // Compute isInProgress from steps (any step without duration_ms or error)
110
+ const isInProgress = data.steps.some(s => s.duration_ms === null && !s.error);
111
+ return c.json({
112
+ run: { ...data.run, isInProgress },
113
+ steps: data.steps,
114
+ });
115
+ });
116
+
117
+ app.post('/api/clear', async c => {
118
+ await clearDatabase();
119
+ return c.json({ success: true });
120
+ });
121
+
122
+ // SSE endpoint for real-time updates
123
+ app.get('/api/events', c => {
124
+ return streamSSE(c, async stream => {
125
+ const clientId = crypto.randomUUID();
126
+
127
+ // Create a client wrapper that uses the stream
128
+ const client: SSEClient = {
129
+ id: clientId,
130
+ controller: null as unknown as ReadableStreamDefaultController<string>,
131
+ };
132
+
133
+ // Send initial connection message
134
+ await stream.writeSSE({
135
+ event: 'connected',
136
+ data: JSON.stringify({ clientId }),
137
+ });
138
+
139
+ // Register client for broadcasts
140
+ const originalWrite = stream.writeSSE.bind(stream);
141
+ client.controller = {
142
+ enqueue: (message: string) => {
143
+ // Parse the raw SSE message and use writeSSE
144
+ const lines = message.split('\n');
145
+ let event = 'message';
146
+ let data = '';
147
+ for (const line of lines) {
148
+ if (line.startsWith('event: ')) {
149
+ event = line.slice(7);
150
+ } else if (line.startsWith('data: ')) {
151
+ data = line.slice(6);
152
+ }
153
+ }
154
+ originalWrite({ event, data }).catch(() => {});
155
+ },
156
+ } as ReadableStreamDefaultController<string>;
157
+
158
+ sseClients.add(client);
159
+
160
+ // Keep connection alive with heartbeat
161
+ const heartbeat = setInterval(async () => {
162
+ try {
163
+ await stream.writeSSE({
164
+ event: 'heartbeat',
165
+ data: JSON.stringify({ time: Date.now() }),
166
+ });
167
+ } catch {
168
+ clearInterval(heartbeat);
169
+ }
170
+ }, 30000);
171
+
172
+ // Wait for client disconnect
173
+ try {
174
+ while (true) {
175
+ await stream.sleep(1000);
176
+ }
177
+ } finally {
178
+ clearInterval(heartbeat);
179
+ sseClients.delete(client);
180
+ }
181
+ });
182
+ });
183
+
184
+ // Notification endpoint (called by middleware)
185
+ app.post('/api/notify', async c => {
186
+ const body = await c.req.json();
187
+ // Reload database from disk to pick up changes from middleware
188
+ await reloadDb();
189
+ broadcastToClients('update', body);
190
+ return c.json({ success: true });
191
+ });
192
+
193
+ // Serve static files (pre-built React app)
194
+ app.use(
195
+ '/assets/*',
196
+ serveStatic({
197
+ root: clientDir.replace(/\/+$/, ''),
198
+ }),
199
+ );
200
+
201
+ // Fallback to index.html for SPA routing
202
+ app.get('*', async c => {
203
+ // In dev mode, redirect to Vite dev server
204
+ if (isDevMode) {
205
+ return c.html(`
206
+ <!DOCTYPE html>
207
+ <html>
208
+ <head>
209
+ <meta charset="UTF-8">
210
+ <title>AI SDK DevTools</title>
211
+ <style>
212
+ body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa; }
213
+ .container { text-align: center; }
214
+ a { color: #3b82f6; text-decoration: none; font-size: 1.25rem; }
215
+ a:hover { text-decoration: underline; }
216
+ p { color: #737373; margin-top: 1rem; }
217
+ </style>
218
+ </head>
219
+ <body>
220
+ <div class="container">
221
+ <h2>Development Mode</h2>
222
+ <a href="http://localhost:5173">Open DevTools UI →</a>
223
+ <p>This port (4983) only serves the API in dev mode.</p>
224
+ </div>
225
+ </body>
226
+ </html>
227
+ `);
228
+ }
229
+
230
+ const indexPath = path.join(clientDir, 'index.html');
231
+ try {
232
+ const html = fs.readFileSync(indexPath, 'utf-8');
233
+ return c.html(html);
234
+ } catch {
235
+ return c.text('DevTools client not built. Run `pnpm build` first.', 500);
236
+ }
237
+ });
238
+
239
+ export const startViewer = (port = 4983) => {
240
+ const isDev =
241
+ process.env.NODE_ENV === 'development' ||
242
+ process.argv[1]?.includes('/src/');
243
+
244
+ const server = serve(
245
+ {
246
+ fetch: app.fetch,
247
+ port,
248
+ },
249
+ () => {
250
+ if (isDev) {
251
+ console.log(`šŸ” AI SDK DevTools API running on port ${port}`);
252
+ console.log(` Open http://localhost:5173 for the dev UI`);
253
+ } else {
254
+ console.log(`šŸ” AI SDK DevTools running at http://localhost:${port}`);
255
+ }
256
+ },
257
+ );
258
+
259
+ server.on('error', (err: NodeJS.ErrnoException) => {
260
+ if (err.code === 'EADDRINUSE') {
261
+ console.error(`\nāŒ Port ${port} is already in use.`);
262
+ console.error(
263
+ `\n This likely means AI SDK DevTools is already running.`,
264
+ );
265
+ console.error(` Open http://localhost:${port} in your browser.\n`);
266
+ console.error(` To use a different port, set AI_SDK_DEVTOOLS_PORT:\n`);
267
+ console.error(` AI_SDK_DEVTOOLS_PORT=4984 npx ai-sdk-devtools\n`);
268
+ process.exit(1);
269
+ }
270
+ throw err;
271
+ });
272
+ };
273
+
274
+ // Allow running directly
275
+ const currentFile = fileURLToPath(import.meta.url);
276
+ const isDirectRun =
277
+ process.argv[1] === currentFile ||
278
+ process.argv[1]?.endsWith('/server.ts') ||
279
+ process.argv[1]?.endsWith('/server.js');
280
+
281
+ if (isDirectRun) {
282
+ const port = process.env.AI_SDK_DEVTOOLS_PORT
283
+ ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT)
284
+ : 4983;
285
+ startViewer(port);
286
+ }