@ai-sdk/devtools 0.0.1-beta.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/LICENSE +13 -0
- package/README.md +73 -0
- package/bin/cli.js +9 -0
- package/dist/client/assets/index-CHVkEbli.js +171 -0
- package/dist/client/assets/index-DSzyB1Eb.css +1 -0
- package/dist/client/index.html +19 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +384 -0
- package/dist/viewer/server.js +313 -0
- package/package.json +65 -0
|
@@ -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,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ai-sdk/devtools",
|
|
3
|
+
"version": "0.0.1-beta.0",
|
|
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
|
+
"@ai-sdk/devtools": "./bin/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"bin"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@hono/node-server": "^1.13.7",
|
|
24
|
+
"hono": "^4.6.14",
|
|
25
|
+
"@ai-sdk/provider": "3.0.0-beta.26"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@radix-ui/react-collapsible": "^1.1.12",
|
|
29
|
+
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
30
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
31
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
32
|
+
"@tailwindcss/vite": "^4.1.17",
|
|
33
|
+
"@types/node": "20.17.24",
|
|
34
|
+
"@types/react": "^18",
|
|
35
|
+
"@types/react-dom": "^18",
|
|
36
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
37
|
+
"class-variance-authority": "^0.7.1",
|
|
38
|
+
"clsx": "^2.1.1",
|
|
39
|
+
"concurrently": "^9.1.0",
|
|
40
|
+
"dotenv": "^17.2.3",
|
|
41
|
+
"lucide-react": "^0.556.0",
|
|
42
|
+
"react": "^18 || ^19",
|
|
43
|
+
"react-dom": "^18 || ^19",
|
|
44
|
+
"tailwind-merge": "^3.4.0",
|
|
45
|
+
"tailwindcss": "^4.1.17",
|
|
46
|
+
"tsup": "^8",
|
|
47
|
+
"tsx": "^4.19.2",
|
|
48
|
+
"tw-animate-css": "^1.4.0",
|
|
49
|
+
"typescript": "5.8.3",
|
|
50
|
+
"vaul": "^1.1.2",
|
|
51
|
+
"vite": "^6.0.3",
|
|
52
|
+
"zod": "3.25.76",
|
|
53
|
+
"ai": "6.0.0-beta.145"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"dev": "concurrently -k \"pnpm dev:api\" \"pnpm dev:client\"",
|
|
57
|
+
"dev:api": "tsx src/viewer/server.ts",
|
|
58
|
+
"dev:client": "vite",
|
|
59
|
+
"example": "tsx examples/basic/index.ts",
|
|
60
|
+
"start": "node dist/viewer/server.js",
|
|
61
|
+
"build": "pnpm build:client && pnpm build:server",
|
|
62
|
+
"build:client": "vite build",
|
|
63
|
+
"build:server": "tsup"
|
|
64
|
+
}
|
|
65
|
+
}
|