@ai-sdk/devtools 1.0.0-canary.29 → 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.
- package/dist/index.js +5 -2
- package/dist/viewer/server.js +72 -13
- package/package.json +6 -6
- package/src/db.ts +52 -11
- package/src/viewer/server.ts +43 -7
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
|
|
5
|
-
var
|
|
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);
|
package/dist/viewer/server.js
CHANGED
|
@@ -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
|
|
15
|
-
var
|
|
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
|
-
|
|
107
|
+
const validatedRemoteDbPath = validateRemoteDbPath(remoteDbPath2);
|
|
108
|
+
if (validatedRemoteDbPath) {
|
|
79
109
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"bin"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@hono/node-server": "^1.19.
|
|
29
|
-
"hono": "^4.12.
|
|
30
|
-
"@ai-sdk/provider": "4.0.0
|
|
28
|
+
"@hono/node-server": "^1.19.13",
|
|
29
|
+
"hono": "^4.12.25",
|
|
30
|
+
"@ai-sdk/provider": "4.0.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
"tw-animate-css": "^1.4.0",
|
|
54
54
|
"typescript": "5.8.3",
|
|
55
55
|
"vaul": "^1.1.2",
|
|
56
|
-
"vite": "^6.4.
|
|
56
|
+
"vite": "^6.4.3",
|
|
57
57
|
"vitest": "^4.1.6",
|
|
58
58
|
"zod": "3.25.76",
|
|
59
|
-
"ai": "7.0.0
|
|
59
|
+
"ai": "7.0.0"
|
|
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
|
|
5
|
-
const
|
|
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
|
|
147
|
-
* instead of the local CWD-based path, so the viewer works regardless of
|
|
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
|
-
|
|
193
|
+
const validatedRemoteDbPath = validateRemoteDbPath(remoteDbPath);
|
|
194
|
+
if (validatedRemoteDbPath) {
|
|
152
195
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
}
|
package/src/viewer/server.ts
CHANGED
|
@@ -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
|
|
86
|
+
const origin = c.req.header('origin');
|
|
87
|
+
if (origin && !getAllowedOrigins().has(origin)) {
|
|
88
|
+
return c.text('Forbidden', 403);
|
|
89
|
+
}
|
|
61
90
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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) {
|