@ai-sdk/devtools 0.0.0-4115c213-20260122152721

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,313 @@
1
+ // src/viewer/server.ts
2
+ import { serve } from "@hono/node-server";
3
+ import { serveStatic } from "@hono/node-server/serve-static";
4
+ import { Hono } from "hono";
5
+ import { cors } from "hono/cors";
6
+ import { streamSSE } from "hono/streaming";
7
+ import path2 from "path";
8
+ import fs2 from "fs";
9
+ import { fileURLToPath } from "url";
10
+
11
+ // src/db.ts
12
+ import path from "node:path";
13
+ import fs from "node:fs";
14
+ var DB_DIR = path.join(process.cwd(), ".devtools");
15
+ var DB_PATH = path.join(DB_DIR, "generations.json");
16
+ var DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT) : 4983;
17
+ var notifyServer = (event) => {
18
+ notifyServerAsync(event);
19
+ };
20
+ var notifyServerAsync = async (event) => {
21
+ try {
22
+ await fetch(`http://localhost:${DEVTOOLS_PORT}/api/notify`, {
23
+ method: "POST",
24
+ headers: { "Content-Type": "application/json" },
25
+ body: JSON.stringify({ event, timestamp: Date.now() })
26
+ });
27
+ } catch {
28
+ }
29
+ };
30
+ var ensureGitignore = () => {
31
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
32
+ if (!fs.existsSync(gitignorePath)) {
33
+ return;
34
+ }
35
+ const content = fs.readFileSync(gitignorePath, "utf-8");
36
+ const lines = content.split("\n");
37
+ const alreadyIgnored = lines.some(
38
+ (line) => line.trim() === ".devtools" || line.trim() === ".devtools/"
39
+ );
40
+ if (!alreadyIgnored) {
41
+ const newContent = content.endsWith("\n") ? `${content}.devtools
42
+ ` : `${content}
43
+ .devtools
44
+ `;
45
+ fs.writeFileSync(gitignorePath, newContent);
46
+ }
47
+ };
48
+ var readDb = () => {
49
+ try {
50
+ if (fs.existsSync(DB_PATH)) {
51
+ const content = fs.readFileSync(DB_PATH, "utf-8");
52
+ return JSON.parse(content);
53
+ }
54
+ } catch {
55
+ }
56
+ return { runs: [], steps: [] };
57
+ };
58
+ var writeDb = (db) => {
59
+ const isFirstRun = !fs.existsSync(DB_DIR);
60
+ if (isFirstRun) {
61
+ fs.mkdirSync(DB_DIR, { recursive: true });
62
+ ensureGitignore();
63
+ }
64
+ fs.writeFileSync(DB_PATH, JSON.stringify(db, null, 2));
65
+ };
66
+ var dbCache = null;
67
+ var getDb = () => {
68
+ if (!dbCache) {
69
+ dbCache = readDb();
70
+ }
71
+ return dbCache;
72
+ };
73
+ var saveDb = (db) => {
74
+ dbCache = db;
75
+ writeDb(db);
76
+ };
77
+ var reloadDb = async () => {
78
+ dbCache = readDb();
79
+ };
80
+ var getRuns = async () => {
81
+ const db = getDb();
82
+ return [...db.runs].sort(
83
+ (a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime()
84
+ );
85
+ };
86
+ var getStepsForRun = async (runId) => {
87
+ const db = getDb();
88
+ return db.steps.filter((s) => s.run_id === runId).sort((a, b) => a.step_number - b.step_number);
89
+ };
90
+ var getRunWithSteps = async (runId) => {
91
+ const db = getDb();
92
+ const run = db.runs.find((r) => r.id === runId);
93
+ if (!run) return null;
94
+ const steps = await getStepsForRun(runId);
95
+ return { run, steps };
96
+ };
97
+ var clearDatabase = async () => {
98
+ const db = { runs: [], steps: [] };
99
+ saveDb(db);
100
+ notifyServer("clear");
101
+ };
102
+
103
+ // src/viewer/server.ts
104
+ var sseClients = /* @__PURE__ */ new Set();
105
+ var broadcastToClients = (event, data) => {
106
+ const message = `event: ${event}
107
+ data: ${JSON.stringify(data)}
108
+
109
+ `;
110
+ for (const client of sseClients) {
111
+ try {
112
+ client.controller.enqueue(message);
113
+ } catch {
114
+ sseClients.delete(client);
115
+ }
116
+ }
117
+ };
118
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
119
+ var isDevMode = __dirname.includes("/src/") || process.env.NODE_ENV === "development";
120
+ var projectRoot = isDevMode ? path2.resolve(__dirname, "../..") : path2.resolve(__dirname, "../..");
121
+ var clientDir = path2.join(projectRoot, "dist/client");
122
+ var app = new Hono();
123
+ app.use("/*", cors());
124
+ app.get("/api/runs", async (c) => {
125
+ const runs = await getRuns();
126
+ const runsWithMeta = await Promise.all(
127
+ runs.map(async (run) => {
128
+ const steps = await getStepsForRun(run.id);
129
+ let firstMessage = "No user message";
130
+ let hasError = false;
131
+ let isInProgress = false;
132
+ const firstStep = steps[0];
133
+ if (firstStep) {
134
+ try {
135
+ const input = JSON.parse(firstStep.input);
136
+ const userMsg = input?.prompt?.findLast(
137
+ (m) => m.role === "user"
138
+ );
139
+ if (userMsg) {
140
+ const content = typeof userMsg.content === "string" ? userMsg.content : userMsg.content?.[0]?.text || "";
141
+ firstMessage = content.slice(0, 60) + (content.length > 60 ? "..." : "");
142
+ }
143
+ } catch {
144
+ }
145
+ hasError = steps.some((s) => s.error);
146
+ isInProgress = steps.some((s) => s.duration_ms === null && !s.error);
147
+ }
148
+ return {
149
+ ...run,
150
+ stepCount: steps.length,
151
+ firstMessage,
152
+ hasError,
153
+ isInProgress,
154
+ type: firstStep?.type
155
+ };
156
+ })
157
+ );
158
+ return c.json(runsWithMeta);
159
+ });
160
+ app.get("/api/runs/:id", async (c) => {
161
+ const data = await getRunWithSteps(c.req.param("id"));
162
+ if (!data) {
163
+ return c.json({ error: "Run not found" }, 404);
164
+ }
165
+ const isInProgress = data.steps.some((s) => s.duration_ms === null && !s.error);
166
+ return c.json({
167
+ run: { ...data.run, isInProgress },
168
+ steps: data.steps
169
+ });
170
+ });
171
+ app.post("/api/clear", async (c) => {
172
+ await clearDatabase();
173
+ return c.json({ success: true });
174
+ });
175
+ app.get("/api/events", (c) => {
176
+ return streamSSE(c, async (stream) => {
177
+ const clientId = crypto.randomUUID();
178
+ const client = {
179
+ id: clientId,
180
+ controller: null
181
+ };
182
+ await stream.writeSSE({
183
+ event: "connected",
184
+ data: JSON.stringify({ clientId })
185
+ });
186
+ const originalWrite = stream.writeSSE.bind(stream);
187
+ client.controller = {
188
+ enqueue: (message) => {
189
+ const lines = message.split("\n");
190
+ let event = "message";
191
+ let data = "";
192
+ for (const line of lines) {
193
+ if (line.startsWith("event: ")) {
194
+ event = line.slice(7);
195
+ } else if (line.startsWith("data: ")) {
196
+ data = line.slice(6);
197
+ }
198
+ }
199
+ originalWrite({ event, data }).catch(() => {
200
+ });
201
+ }
202
+ };
203
+ sseClients.add(client);
204
+ const heartbeat = setInterval(async () => {
205
+ try {
206
+ await stream.writeSSE({
207
+ event: "heartbeat",
208
+ data: JSON.stringify({ time: Date.now() })
209
+ });
210
+ } catch {
211
+ clearInterval(heartbeat);
212
+ }
213
+ }, 3e4);
214
+ try {
215
+ while (true) {
216
+ await stream.sleep(1e3);
217
+ }
218
+ } finally {
219
+ clearInterval(heartbeat);
220
+ sseClients.delete(client);
221
+ }
222
+ });
223
+ });
224
+ app.post("/api/notify", async (c) => {
225
+ const body = await c.req.json();
226
+ await reloadDb();
227
+ broadcastToClients("update", body);
228
+ return c.json({ success: true });
229
+ });
230
+ app.use(
231
+ "/assets/*",
232
+ serveStatic({
233
+ root: clientDir.replace(/\/+$/, "")
234
+ })
235
+ );
236
+ app.get("*", async (c) => {
237
+ if (isDevMode) {
238
+ return c.html(`
239
+ <!DOCTYPE html>
240
+ <html>
241
+ <head>
242
+ <meta charset="UTF-8">
243
+ <title>AI SDK DevTools</title>
244
+ <style>
245
+ body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa; }
246
+ .container { text-align: center; }
247
+ a { color: #3b82f6; text-decoration: none; font-size: 1.25rem; }
248
+ a:hover { text-decoration: underline; }
249
+ p { color: #737373; margin-top: 1rem; }
250
+ </style>
251
+ </head>
252
+ <body>
253
+ <div class="container">
254
+ <h2>Development Mode</h2>
255
+ <a href="http://localhost:5173">Open DevTools UI \u2192</a>
256
+ <p>This port (4983) only serves the API in dev mode.</p>
257
+ </div>
258
+ </body>
259
+ </html>
260
+ `);
261
+ }
262
+ const indexPath = path2.join(clientDir, "index.html");
263
+ try {
264
+ const html = fs2.readFileSync(indexPath, "utf-8");
265
+ return c.html(html);
266
+ } catch {
267
+ return c.text("DevTools client not built. Run `pnpm build` first.", 500);
268
+ }
269
+ });
270
+ var startViewer = (port = 4983) => {
271
+ const isDev = process.env.NODE_ENV === "development" || process.argv[1]?.includes("/src/");
272
+ const server = serve(
273
+ {
274
+ fetch: app.fetch,
275
+ port
276
+ },
277
+ () => {
278
+ if (isDev) {
279
+ console.log(`\u{1F50D} AI SDK DevTools API running on port ${port}`);
280
+ console.log(` Open http://localhost:5173 for the dev UI`);
281
+ } else {
282
+ console.log(`\u{1F50D} AI SDK DevTools running at http://localhost:${port}`);
283
+ }
284
+ }
285
+ );
286
+ server.on("error", (err) => {
287
+ if (err.code === "EADDRINUSE") {
288
+ console.error(`
289
+ \u274C Port ${port} is already in use.`);
290
+ console.error(
291
+ `
292
+ This likely means AI SDK DevTools is already running.`
293
+ );
294
+ console.error(` Open http://localhost:${port} in your browser.
295
+ `);
296
+ console.error(` To use a different port, set AI_SDK_DEVTOOLS_PORT:
297
+ `);
298
+ console.error(` AI_SDK_DEVTOOLS_PORT=4984 npx ai-sdk-devtools
299
+ `);
300
+ process.exit(1);
301
+ }
302
+ throw err;
303
+ });
304
+ };
305
+ var currentFile = fileURLToPath(import.meta.url);
306
+ var isDirectRun = process.argv[1] === currentFile || process.argv[1]?.endsWith("/server.ts") || process.argv[1]?.endsWith("/server.js");
307
+ if (isDirectRun) {
308
+ const port = process.env.AI_SDK_DEVTOOLS_PORT ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT) : 4983;
309
+ startViewer(port);
310
+ }
311
+ export {
312
+ startViewer
313
+ };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@ai-sdk/devtools",
3
+ "version": "0.0.0-4115c213-20260122152721",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "bin": {
16
+ "devtools": "./bin/cli.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "!src/**/*.test.ts",
22
+ "!src/**/*.test-d.ts",
23
+ "!src/**/__snapshots__",
24
+ "!src/**/__fixtures__",
25
+ "bin"
26
+ ],
27
+ "dependencies": {
28
+ "@hono/node-server": "^1.13.7",
29
+ "hono": "^4.6.14",
30
+ "@ai-sdk/provider": "0.0.0-4115c213-20260122152721"
31
+ },
32
+ "devDependencies": {
33
+ "@radix-ui/react-collapsible": "^1.1.12",
34
+ "@radix-ui/react-scroll-area": "^1.2.10",
35
+ "@radix-ui/react-slot": "^1.2.4",
36
+ "@radix-ui/react-tooltip": "^1.2.8",
37
+ "@tailwindcss/vite": "^4.1.17",
38
+ "@types/node": "20.17.24",
39
+ "@types/react": "^18",
40
+ "@types/react-dom": "^18",
41
+ "@vitejs/plugin-react": "^4.3.4",
42
+ "class-variance-authority": "^0.7.1",
43
+ "clsx": "^2.1.1",
44
+ "concurrently": "^9.1.0",
45
+ "dotenv": "^17.2.3",
46
+ "lucide-react": "^0.556.0",
47
+ "react": "^18 || ^19",
48
+ "react-dom": "^18 || ^19",
49
+ "tailwind-merge": "^3.4.0",
50
+ "tailwindcss": "^4.1.17",
51
+ "tsup": "^8",
52
+ "tsx": "^4.19.2",
53
+ "tw-animate-css": "^1.4.0",
54
+ "typescript": "5.8.3",
55
+ "vaul": "^1.1.2",
56
+ "vite": "^6.0.3",
57
+ "zod": "3.25.76",
58
+ "ai": "0.0.0-4115c213-20260122152721"
59
+ },
60
+ "scripts": {
61
+ "dev": "concurrently -k \"pnpm dev:api\" \"pnpm dev:client\"",
62
+ "dev:api": "tsx src/viewer/server.ts",
63
+ "dev:client": "vite",
64
+ "example": "tsx examples/basic/index.ts",
65
+ "start": "node dist/viewer/server.js",
66
+ "build": "pnpm build:client && pnpm build:server",
67
+ "build:client": "vite build",
68
+ "build:server": "tsup"
69
+ }
70
+ }
package/src/db.ts ADDED
@@ -0,0 +1,242 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+
4
+ const DB_DIR = path.join(process.cwd(), '.devtools');
5
+ const DB_PATH = path.join(DB_DIR, 'generations.json');
6
+ const DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT
7
+ ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT)
8
+ : 4983;
9
+
10
+ /**
11
+ * Notify the devtools server that data has changed.
12
+ * Fire-and-forget: doesn't block, ignores errors if server isn't running.
13
+ */
14
+ const notifyServer = (event: 'run' | 'step' | 'step-update' | 'clear') => {
15
+ notifyServerAsync(event);
16
+ };
17
+
18
+ /**
19
+ * Notify the devtools server and wait for the request to complete.
20
+ * Used during process cleanup to ensure notifications are sent before exit.
21
+ */
22
+ export const notifyServerAsync = async (
23
+ event: 'run' | 'step' | 'step-update' | 'clear',
24
+ ): Promise<void> => {
25
+ try {
26
+ await fetch(`http://localhost:${DEVTOOLS_PORT}/api/notify`, {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ event, timestamp: Date.now() }),
30
+ });
31
+ } catch {
32
+ // Ignore errors - server might not be running
33
+ }
34
+ };
35
+
36
+ export interface Run {
37
+ id: string;
38
+ started_at: string;
39
+ }
40
+
41
+ export interface Step {
42
+ id: string;
43
+ run_id: string;
44
+ step_number: number;
45
+ type: 'generate' | 'stream';
46
+ model_id: string;
47
+ provider: string | null;
48
+ started_at: string;
49
+ duration_ms: number | null;
50
+ input: string;
51
+ output: string | null;
52
+ usage: string | null;
53
+ error: string | null;
54
+ raw_request: string | null;
55
+ raw_response: string | null;
56
+ raw_chunks: string | null;
57
+ provider_options: string | null;
58
+ }
59
+
60
+ export interface StepResult {
61
+ duration_ms: number;
62
+ output: string | null;
63
+ usage: string | null;
64
+ error: string | null;
65
+ raw_request?: string | null;
66
+ raw_response?: string | null;
67
+ raw_chunks?: string | null;
68
+ }
69
+
70
+ interface Database {
71
+ runs: Run[];
72
+ steps: Step[];
73
+ }
74
+
75
+ /**
76
+ * Ensure .devtools is in .gitignore.
77
+ * Only writes if .gitignore exists and doesn't already contain .devtools.
78
+ */
79
+ const ensureGitignore = (): void => {
80
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
81
+
82
+ if (!fs.existsSync(gitignorePath)) {
83
+ return;
84
+ }
85
+
86
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
87
+ const lines = content.split('\n');
88
+
89
+ // Check if .devtools is already ignored (exact match or with trailing slash)
90
+ const alreadyIgnored = lines.some(
91
+ line => line.trim() === '.devtools' || line.trim() === '.devtools/',
92
+ );
93
+
94
+ if (!alreadyIgnored) {
95
+ const newContent = content.endsWith('\n')
96
+ ? `${content}.devtools\n`
97
+ : `${content}\n.devtools\n`;
98
+ fs.writeFileSync(gitignorePath, newContent);
99
+ }
100
+ };
101
+
102
+ const readDb = (): Database => {
103
+ try {
104
+ if (fs.existsSync(DB_PATH)) {
105
+ const content = fs.readFileSync(DB_PATH, 'utf-8');
106
+ return JSON.parse(content);
107
+ }
108
+ } catch {
109
+ // If file is corrupted, start fresh
110
+ }
111
+ return { runs: [], steps: [] };
112
+ };
113
+
114
+ const writeDb = (db: Database): void => {
115
+ const isFirstRun = !fs.existsSync(DB_DIR);
116
+
117
+ if (isFirstRun) {
118
+ fs.mkdirSync(DB_DIR, { recursive: true });
119
+ ensureGitignore();
120
+ }
121
+
122
+ fs.writeFileSync(DB_PATH, JSON.stringify(db, null, 2));
123
+ };
124
+
125
+ // In-memory cache for performance
126
+ let dbCache: Database | null = null;
127
+
128
+ const getDb = (): Database => {
129
+ if (!dbCache) {
130
+ dbCache = readDb();
131
+ }
132
+ return dbCache;
133
+ };
134
+
135
+ const saveDb = (db: Database): void => {
136
+ dbCache = db;
137
+ writeDb(db);
138
+ };
139
+
140
+ /**
141
+ * Reload the database from disk.
142
+ * Used by the viewer server to pick up changes made by the middleware.
143
+ */
144
+ export const reloadDb = async (): Promise<void> => {
145
+ dbCache = readDb();
146
+ };
147
+
148
+ export const createRun = async (id: string): Promise<Run> => {
149
+ const db = getDb();
150
+ const started_at = new Date().toISOString();
151
+
152
+ // Check if run already exists
153
+ const existing = db.runs.find(r => r.id === id);
154
+ if (existing) {
155
+ return existing;
156
+ }
157
+
158
+ const run: Run = { id, started_at };
159
+ db.runs.push(run);
160
+ saveDb(db);
161
+ notifyServer('run');
162
+ return run;
163
+ };
164
+
165
+ export const createStep = async (
166
+ step: Omit<
167
+ Step,
168
+ | 'duration_ms'
169
+ | 'output'
170
+ | 'usage'
171
+ | 'error'
172
+ | 'raw_request'
173
+ | 'raw_response'
174
+ | 'raw_chunks'
175
+ >,
176
+ ): Promise<void> => {
177
+ const db = getDb();
178
+ const newStep: Step = {
179
+ ...step,
180
+ duration_ms: null,
181
+ output: null,
182
+ usage: null,
183
+ error: null,
184
+ raw_request: null,
185
+ raw_response: null,
186
+ raw_chunks: null,
187
+ };
188
+ db.steps.push(newStep);
189
+ saveDb(db);
190
+ notifyServer('step');
191
+ };
192
+
193
+ export const updateStepResult = async (
194
+ stepId: string,
195
+ result: StepResult,
196
+ ): Promise<void> => {
197
+ const db = getDb();
198
+ const step = db.steps.find(s => s.id === stepId);
199
+ if (step) {
200
+ step.duration_ms = result.duration_ms;
201
+ step.output = result.output;
202
+ step.usage = result.usage;
203
+ step.error = result.error;
204
+ step.raw_request = result.raw_request ?? null;
205
+ step.raw_response = result.raw_response ?? null;
206
+ step.raw_chunks = result.raw_chunks ?? null;
207
+ saveDb(db);
208
+ notifyServer('step-update');
209
+ }
210
+ };
211
+
212
+ export const getRuns = async (): Promise<Run[]> => {
213
+ const db = getDb();
214
+ // Return runs sorted by started_at DESC
215
+ return [...db.runs].sort(
216
+ (a, b) =>
217
+ new Date(b.started_at).getTime() - new Date(a.started_at).getTime(),
218
+ );
219
+ };
220
+
221
+ export const getStepsForRun = async (runId: string): Promise<Step[]> => {
222
+ const db = getDb();
223
+ return db.steps
224
+ .filter(s => s.run_id === runId)
225
+ .sort((a, b) => a.step_number - b.step_number);
226
+ };
227
+
228
+ export const getRunWithSteps = async (
229
+ runId: string,
230
+ ): Promise<{ run: Run; steps: Step[] } | null> => {
231
+ const db = getDb();
232
+ const run = db.runs.find(r => r.id === runId);
233
+ if (!run) return null;
234
+ const steps = await getStepsForRun(runId);
235
+ return { run, steps };
236
+ };
237
+
238
+ export const clearDatabase = async (): Promise<void> => {
239
+ const db: Database = { runs: [], steps: [] };
240
+ saveDb(db);
241
+ notifyServer('clear');
242
+ };
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { devToolsMiddleware } from './middleware.js';