@ai-sdk/devtools 1.0.0-canary.28 → 1.0.0-canary.30

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.
package/README.md CHANGED
@@ -14,20 +14,46 @@ pnpm add @ai-sdk/devtools
14
14
 
15
15
  ## Requirements
16
16
 
17
- - AI SDK v6 beta (`ai@^6.0.0-beta.0`)
17
+ - AI SDK v7 canary (`ai@canary`)
18
18
  - Node.js compatible runtime
19
19
 
20
20
  ## Usage
21
21
 
22
- ### 1. Add the middleware to your model
22
+ ### 1. Register the telemetry integration
23
+
24
+ Register `DevToolsTelemetry` globally so it captures all AI SDK calls:
25
+
26
+ ```typescript
27
+ import { registerTelemetry } from 'ai';
28
+ import { DevToolsTelemetry } from '@ai-sdk/devtools';
29
+
30
+ registerTelemetry(DevToolsTelemetry());
31
+ ```
32
+
33
+ Telemetry is enabled automatically once an integration is registered:
34
+
35
+ ```typescript
36
+ import { generateText } from 'ai';
37
+
38
+ const result = await generateText({
39
+ model: yourModel,
40
+ prompt: 'What cities are in the United States?',
41
+ });
42
+ ```
43
+
44
+ You can also pass the integration to individual calls instead of registering it
45
+ globally:
23
46
 
24
47
  ```typescript
25
- import { wrapLanguageModel } from 'ai';
26
- import { devToolsMiddleware } from '@ai-sdk/devtools';
48
+ import { streamText } from 'ai';
49
+ import { DevToolsTelemetry } from '@ai-sdk/devtools';
27
50
 
28
- const model = wrapLanguageModel({
29
- middleware: devToolsMiddleware(),
51
+ const result = streamText({
30
52
  model: yourModel,
53
+ prompt: 'Hello!',
54
+ telemetry: {
55
+ integrations: [DevToolsTelemetry()],
56
+ },
31
57
  });
32
58
  ```
33
59
 
@@ -39,21 +65,26 @@ npx @ai-sdk/devtools
39
65
 
40
66
  Open http://localhost:4983 to view your AI SDK interactions.
41
67
 
68
+ If you are using a monorepo, start DevTools from the same workspace where your
69
+ AI SDK code runs.
70
+
42
71
  ## How it works
43
72
 
44
- The middleware intercepts all `generateText` and `streamText` calls, capturing:
73
+ The `DevToolsTelemetry` integration hooks into the AI SDK telemetry lifecycle to
74
+ capture `generateText`, `streamText`, `generateObject`, and `streamObject` calls.
75
+ It captures:
45
76
 
46
77
  - Input parameters and prompts
47
78
  - Output content and tool calls
48
79
  - Token usage and timing
49
- - Raw provider request/response data
80
+ - Raw provider data
50
81
 
51
82
  Data is stored locally in a JSON file (`.devtools/generations.json`) and served through a web UI.
52
83
 
53
84
  ### Data flow
54
85
 
55
86
  ```
56
- AI SDK call devToolsMiddleware JSON file Hono API React UI
87
+ AI SDK call -> DevToolsTelemetry -> JSON file -> Hono API -> React UI
57
88
  ```
58
89
 
59
90
  ### Key concepts
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  // src/db.ts
2
2
  import path from "path";
3
3
  import fs from "fs";
4
- var DB_DIR = path.join(process.cwd(), ".devtools");
5
- var DB_PATH = path.join(DB_DIR, "generations.json");
4
+ var DEVTOOLS_DB_DIR = ".devtools";
5
+ var DEVTOOLS_DB_FILE = "generations.json";
6
+ var MAX_DB_BYTES = 100 * 1024 * 1024;
7
+ var DB_DIR = path.join(process.cwd(), DEVTOOLS_DB_DIR);
8
+ var DB_PATH = path.join(DB_DIR, DEVTOOLS_DB_FILE);
6
9
  var DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT) : 4983;
7
10
  var notifyServer = (event) => {
8
11
  notifyServerAsync(event);
@@ -596,7 +599,7 @@ function DevToolsTelemetry() {
596
599
  });
597
600
  state.stepStates.delete(stepResult.stepNumber);
598
601
  },
599
- onObjectStepFinish: async (event) => {
602
+ onObjectStepEnd: async (event) => {
600
603
  const stepResult = event;
601
604
  const state = callStates.get(stepResult.callId);
602
605
  if (!state) return;
@@ -2,7 +2,6 @@
2
2
  import { serve } from "@hono/node-server";
3
3
  import { serveStatic } from "@hono/node-server/serve-static";
4
4
  import { Hono } from "hono";
5
- import { cors } from "hono/cors";
6
5
  import { streamSSE } from "hono/streaming";
7
6
  import path2 from "path";
8
7
  import fs2 from "fs";
@@ -11,8 +10,11 @@ import { fileURLToPath } from "url";
11
10
  // src/db.ts
12
11
  import path from "path";
13
12
  import fs from "fs";
14
- var DB_DIR = path.join(process.cwd(), ".devtools");
15
- var DB_PATH = path.join(DB_DIR, "generations.json");
13
+ var DEVTOOLS_DB_DIR = ".devtools";
14
+ var DEVTOOLS_DB_FILE = "generations.json";
15
+ var MAX_DB_BYTES = 100 * 1024 * 1024;
16
+ var DB_DIR = path.join(process.cwd(), DEVTOOLS_DB_DIR);
17
+ var DB_PATH = path.join(DB_DIR, DEVTOOLS_DB_FILE);
16
18
  var DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT) : 4983;
17
19
  var notifyServer = (event) => {
18
20
  notifyServerAsync(event);
@@ -74,14 +76,40 @@ var saveDb = (db) => {
74
76
  dbCache = db;
75
77
  writeDb(db);
76
78
  };
79
+ var normalizeDevtoolsDbPath = (dbPath) => {
80
+ const resolvedPath = path.resolve(dbPath);
81
+ const dbDir = path.dirname(resolvedPath);
82
+ if (path.basename(resolvedPath) !== DEVTOOLS_DB_FILE || path.basename(dbDir) !== DEVTOOLS_DB_DIR) {
83
+ return void 0;
84
+ }
85
+ return resolvedPath;
86
+ };
87
+ var validateRemoteDbPath = (dbPath) => {
88
+ if (typeof dbPath !== "string") {
89
+ return void 0;
90
+ }
91
+ const normalizedPath = normalizeDevtoolsDbPath(dbPath);
92
+ if (!normalizedPath) {
93
+ return void 0;
94
+ }
95
+ try {
96
+ const stats = fs.statSync(normalizedPath);
97
+ if (!stats.isFile() || stats.size > MAX_DB_BYTES) {
98
+ return void 0;
99
+ }
100
+ const realPath = fs.realpathSync(normalizedPath);
101
+ return normalizeDevtoolsDbPath(realPath);
102
+ } catch {
103
+ return void 0;
104
+ }
105
+ };
77
106
  var reloadDb = async (remoteDbPath2) => {
78
- if (remoteDbPath2) {
107
+ const validatedRemoteDbPath = validateRemoteDbPath(remoteDbPath2);
108
+ if (validatedRemoteDbPath) {
79
109
  try {
80
- if (fs.existsSync(remoteDbPath2)) {
81
- const content = fs.readFileSync(remoteDbPath2, "utf-8");
82
- dbCache = JSON.parse(content);
83
- return;
84
- }
110
+ const content = fs.readFileSync(validatedRemoteDbPath, "utf-8");
111
+ dbCache = JSON.parse(content);
112
+ return;
85
113
  } catch {
86
114
  }
87
115
  }
@@ -131,8 +159,32 @@ var isDevMode = devEnv !== void 0 && devEnv !== "false" && devEnv !== "0";
131
159
  var projectRoot = path2.resolve(__dirname, "../..");
132
160
  var clientDir = path2.join(projectRoot, "dist/client");
133
161
  var remoteDbPath;
162
+ var viewerPort = 4983;
134
163
  var app = new Hono();
135
- app.use("/*", cors());
164
+ var getAllowedHosts = () => /* @__PURE__ */ new Set([
165
+ `localhost:${viewerPort}`,
166
+ `127.0.0.1:${viewerPort}`,
167
+ `[::1]:${viewerPort}`
168
+ ]);
169
+ var getAllowedOrigins = () => /* @__PURE__ */ new Set([
170
+ `http://localhost:${viewerPort}`,
171
+ `http://127.0.0.1:${viewerPort}`,
172
+ `http://[::1]:${viewerPort}`,
173
+ "http://localhost:5173",
174
+ "http://127.0.0.1:5173",
175
+ "http://[::1]:5173"
176
+ ]);
177
+ app.use("/api/*", async (c, next) => {
178
+ const host = c.req.header("host");
179
+ if (!host || !getAllowedHosts().has(host)) {
180
+ return c.text("Forbidden", 403);
181
+ }
182
+ const origin = c.req.header("origin");
183
+ if (origin && !getAllowedOrigins().has(origin)) {
184
+ return c.text("Forbidden", 403);
185
+ }
186
+ await next();
187
+ });
136
188
  app.get("/api/runs", async (c) => {
137
189
  await reloadDb(remoteDbPath);
138
190
  const runs = await getRuns();
@@ -258,8 +310,12 @@ app.get("/api/events", (c) => {
258
310
  });
259
311
  app.post("/api/notify", async (c) => {
260
312
  const body = await c.req.json();
261
- if (body.dbPath) {
262
- remoteDbPath = body.dbPath;
313
+ if (body.dbPath !== void 0) {
314
+ const validatedDbPath = validateRemoteDbPath(body.dbPath);
315
+ if (!validatedDbPath) {
316
+ return c.text("Invalid dbPath", 400);
317
+ }
318
+ remoteDbPath = validatedDbPath;
263
319
  }
264
320
  await reloadDb(remoteDbPath);
265
321
  broadcastToClients("update", body);
@@ -306,10 +362,12 @@ app.get("*", async (c) => {
306
362
  }
307
363
  });
308
364
  var startViewer = (port = 4983) => {
365
+ viewerPort = port;
309
366
  const server = serve(
310
367
  {
311
368
  fetch: app.fetch,
312
- port
369
+ port,
370
+ hostname: "localhost"
313
371
  },
314
372
  () => {
315
373
  if (isDevMode) {
@@ -346,5 +404,6 @@ if (isDirectRun) {
346
404
  startViewer(port);
347
405
  }
348
406
  export {
407
+ app,
349
408
  startViewer
350
409
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/devtools",
3
- "version": "1.0.0-canary.28",
3
+ "version": "1.0.0-canary.30",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -56,7 +56,7 @@
56
56
  "vite": "^6.4.2",
57
57
  "vitest": "^4.1.6",
58
58
  "zod": "3.25.76",
59
- "ai": "7.0.0-canary.166"
59
+ "ai": "7.0.0-canary.172"
60
60
  },
61
61
  "publishConfig": {
62
62
  "access": "public",
package/src/db.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
 
4
- const DB_DIR = path.join(process.cwd(), '.devtools');
5
- const DB_PATH = path.join(DB_DIR, 'generations.json');
4
+ const DEVTOOLS_DB_DIR = '.devtools';
5
+ const DEVTOOLS_DB_FILE = 'generations.json';
6
+ // Cap how many bytes we read from a remote database file. Guards against a
7
+ // synchronous hang / OOM if the file is enormous.
8
+ const MAX_DB_BYTES = 100 * 1024 * 1024; // 100 MB
9
+ const DB_DIR = path.join(process.cwd(), DEVTOOLS_DB_DIR);
10
+ const DB_PATH = path.join(DB_DIR, DEVTOOLS_DB_FILE);
6
11
  const DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT
7
12
  ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT)
8
13
  : 4983;
@@ -140,21 +145,57 @@ const saveDb = (db: Database): void => {
140
145
  writeDb(db);
141
146
  };
142
147
 
148
+ const normalizeDevtoolsDbPath = (dbPath: string): string | undefined => {
149
+ const resolvedPath = path.resolve(dbPath);
150
+ const dbDir = path.dirname(resolvedPath);
151
+
152
+ if (
153
+ path.basename(resolvedPath) !== DEVTOOLS_DB_FILE ||
154
+ path.basename(dbDir) !== DEVTOOLS_DB_DIR
155
+ ) {
156
+ return undefined;
157
+ }
158
+
159
+ return resolvedPath;
160
+ };
161
+
162
+ export const validateRemoteDbPath = (dbPath: unknown): string | undefined => {
163
+ if (typeof dbPath !== 'string') {
164
+ return undefined;
165
+ }
166
+
167
+ const normalizedPath = normalizeDevtoolsDbPath(dbPath);
168
+ if (!normalizedPath) {
169
+ return undefined;
170
+ }
171
+
172
+ try {
173
+ const stats = fs.statSync(normalizedPath);
174
+ if (!stats.isFile() || stats.size > MAX_DB_BYTES) {
175
+ return undefined;
176
+ }
177
+
178
+ const realPath = fs.realpathSync(normalizedPath);
179
+ return normalizeDevtoolsDbPath(realPath);
180
+ } catch {
181
+ return undefined;
182
+ }
183
+ };
184
+
143
185
  /**
144
186
  * Reload the database from disk.
145
187
  * Used by the viewer server to pick up changes made by the middleware/integration.
146
- * When a remote dbPath is provided (from a notify POST), reads from that path
147
- * instead of the local CWD-based path, so the viewer works regardless of where
148
- * it was started.
188
+ * When a valid remote dbPath is provided (from a notify POST), reads from that
189
+ * path instead of the local CWD-based path, so the viewer works regardless of
190
+ * where it was started.
149
191
  */
150
192
  export const reloadDb = async (remoteDbPath?: string): Promise<void> => {
151
- if (remoteDbPath) {
193
+ const validatedRemoteDbPath = validateRemoteDbPath(remoteDbPath);
194
+ if (validatedRemoteDbPath) {
152
195
  try {
153
- if (fs.existsSync(remoteDbPath)) {
154
- const content = fs.readFileSync(remoteDbPath, 'utf-8');
155
- dbCache = JSON.parse(content);
156
- return;
157
- }
196
+ const content = fs.readFileSync(validatedRemoteDbPath, 'utf-8');
197
+ dbCache = JSON.parse(content);
198
+ return;
158
199
  } catch {
159
200
  // Fall through to default
160
201
  }
@@ -350,7 +350,7 @@ export function DevToolsTelemetry(): Telemetry {
350
350
  state.stepStates.delete(stepResult.stepNumber);
351
351
  },
352
352
 
353
- onObjectStepFinish: async event => {
353
+ onObjectStepEnd: async event => {
354
354
  const stepResult = event as GenerateObjectStepEndEvent;
355
355
 
356
356
  const state = callStates.get(stepResult.callId);
@@ -1,7 +1,6 @@
1
1
  import { serve } from '@hono/node-server';
2
2
  import { serveStatic } from '@hono/node-server/serve-static';
3
3
  import { Hono } from 'hono';
4
- import { cors } from 'hono/cors';
5
4
  import { streamSSE } from 'hono/streaming';
6
5
  import path from 'path';
7
6
  import fs from 'fs';
@@ -12,6 +11,7 @@ import {
12
11
  getStepsForRun,
13
12
  clearDatabase,
14
13
  reloadDb,
14
+ validateRemoteDbPath,
15
15
  } from '../db.js';
16
16
 
17
17
  // SSE client management
@@ -56,11 +56,40 @@ const clientDir = path.join(projectRoot, 'dist/client');
56
56
  // Track the DB path from the most recent notify call so all reads
57
57
  // use the correct file, even when the viewer runs in a different CWD.
58
58
  let remoteDbPath: string | undefined;
59
+ let viewerPort = 4983;
60
+
61
+ export const app = new Hono();
62
+
63
+ const getAllowedHosts = () =>
64
+ new Set([
65
+ `localhost:${viewerPort}`,
66
+ `127.0.0.1:${viewerPort}`,
67
+ `[::1]:${viewerPort}`,
68
+ ]);
69
+
70
+ const getAllowedOrigins = () =>
71
+ new Set([
72
+ `http://localhost:${viewerPort}`,
73
+ `http://127.0.0.1:${viewerPort}`,
74
+ `http://[::1]:${viewerPort}`,
75
+ 'http://localhost:5173',
76
+ 'http://127.0.0.1:5173',
77
+ 'http://[::1]:5173',
78
+ ]);
79
+
80
+ app.use('/api/*', async (c, next) => {
81
+ const host = c.req.header('host');
82
+ if (!host || !getAllowedHosts().has(host)) {
83
+ return c.text('Forbidden', 403);
84
+ }
59
85
 
60
- const app = new Hono();
86
+ const origin = c.req.header('origin');
87
+ if (origin && !getAllowedOrigins().has(origin)) {
88
+ return c.text('Forbidden', 403);
89
+ }
61
90
 
62
- // Enable CORS for development
63
- app.use('/*', cors());
91
+ await next();
92
+ });
64
93
 
65
94
  // API Routes
66
95
  app.get('/api/runs', async c => {
@@ -230,9 +259,13 @@ app.get('/api/events', c => {
230
259
 
231
260
  // Notification endpoint (called by middleware/integration)
232
261
  app.post('/api/notify', async c => {
233
- const body = await c.req.json();
234
- if (body.dbPath) {
235
- remoteDbPath = body.dbPath;
262
+ const body = (await c.req.json()) as Record<string, unknown>;
263
+ if (body.dbPath !== undefined) {
264
+ const validatedDbPath = validateRemoteDbPath(body.dbPath);
265
+ if (!validatedDbPath) {
266
+ return c.text('Invalid dbPath', 400);
267
+ }
268
+ remoteDbPath = validatedDbPath;
236
269
  }
237
270
  // Reload database from disk, using the remote dbPath if provided so the
238
271
  // viewer works even when started from a different directory than the app.
@@ -288,10 +321,13 @@ app.get('*', async c => {
288
321
  });
289
322
 
290
323
  export const startViewer = (port = 4983) => {
324
+ viewerPort = port;
325
+
291
326
  const server = serve(
292
327
  {
293
328
  fetch: app.fetch,
294
329
  port,
330
+ hostname: 'localhost',
295
331
  },
296
332
  () => {
297
333
  if (isDevMode) {