@game_ryo/lsji 0.1.1 → 0.3.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/package.json +12 -7
- package/src/cli.js +395 -62
- package/src/execution/budget/circuit-breaker.js +245 -0
- package/src/execution/budget/cost-tracker.js +387 -0
- package/src/execution/budget/index.js +63 -0
- package/src/execution/budget/token-counter.js +159 -0
- package/src/execution/engine.js +428 -0
- package/src/execution/hitl/approval-gate.js +210 -0
- package/src/execution/hitl/index.js +12 -0
- package/src/execution/hitl/notifier.js +151 -0
- package/src/execution/hitl/store.js +311 -0
- package/src/execution/idempotency.js +312 -0
- package/src/execution/index.js +14 -0
- package/src/index.js +80 -4
- package/src/llm/index.js +21 -0
- package/src/llm/llm-agent.js +357 -0
- package/src/llm/memory/conversation.js +271 -0
- package/src/llm/memory/episodic.js +312 -0
- package/src/llm/memory/index.js +12 -0
- package/src/llm/memory/semantic.js +324 -0
- package/src/llm/plugins/index.js +202 -0
- package/src/llm/prompt-manager.js +332 -0
- package/src/llm/providers/anthropic.js +250 -0
- package/src/llm/providers/base.js +116 -0
- package/src/llm/providers/local.js +163 -0
- package/src/llm/providers/openai.js +212 -0
- package/src/llm/tools/registry.js +342 -0
- package/src/server/index.js +416 -0
- package/src/server/ui/index.html +16 -0
- package/src/server/ui/package.json +19 -0
- package/src/server/ui/src/main.jsx +10 -0
- package/src/server/ui/src/styles.css +260 -0
- package/src/server/ui/vite.config.js +27 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSJI Runtime Server
|
|
3
|
+
*
|
|
4
|
+
* HTTP + WebSocket server for agent control panel.
|
|
5
|
+
* Provides real-time thought logs, approval queue, budget monitoring.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import express from 'express';
|
|
9
|
+
import { createServer } from 'http';
|
|
10
|
+
import { Server } from 'socket.io';
|
|
11
|
+
import cors from 'cors';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { dirname, resolve, join } from 'path';
|
|
14
|
+
import { createExecutionEngine } from '../execution/engine.js';
|
|
15
|
+
import { createApprovalGate } from '../execution/hitl/approval-gate.js';
|
|
16
|
+
import { createBudgetController } from '../execution/budget/index.js';
|
|
17
|
+
import { createLLMAgent } from '../llm/llm-agent.js';
|
|
18
|
+
import { createStorage } from '../storage/index.js';
|
|
19
|
+
import { createToolRegistry } from '../llm/tools/registry.js';
|
|
20
|
+
import { loadPlugins } from '../llm/plugins/index.js';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
24
|
+
|
|
25
|
+
// Server state
|
|
26
|
+
const activeRuns = new Map(); // runId -> { agent, workflowId, status, startTime }
|
|
27
|
+
const connectedClients = new Set();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create and configure Express app
|
|
31
|
+
*/
|
|
32
|
+
export function createApp(config = {}) {
|
|
33
|
+
const app = express();
|
|
34
|
+
const httpServer = createServer(app);
|
|
35
|
+
|
|
36
|
+
// Socket.io with CORS
|
|
37
|
+
const io = new Server(httpServer, {
|
|
38
|
+
cors: {
|
|
39
|
+
origin: config.corsOrigin || '*',
|
|
40
|
+
methods: ['GET', 'POST'],
|
|
41
|
+
credentials: true,
|
|
42
|
+
},
|
|
43
|
+
pingTimeout: 60000,
|
|
44
|
+
pingInterval: 25000,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Middleware
|
|
48
|
+
app.use(cors({ origin: config.corsOrigin || '*', credentials: true }));
|
|
49
|
+
app.use(express.json({ limit: '10mb' }));
|
|
50
|
+
app.use(express.urlencoded({ extended: true }));
|
|
51
|
+
|
|
52
|
+
// Health check
|
|
53
|
+
app.get('/health', (req, res) => {
|
|
54
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime() });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// API: List active runs
|
|
58
|
+
app.get('/api/runs', (req, res) => {
|
|
59
|
+
const runs = Array.from(activeRuns.entries()).map(([runId, data]) => ({
|
|
60
|
+
runId,
|
|
61
|
+
workflowId: data.workflowId,
|
|
62
|
+
status: data.status,
|
|
63
|
+
startTime: data.startTime,
|
|
64
|
+
duration: Date.now() - data.startTime,
|
|
65
|
+
task: data.task?.slice(0, 100),
|
|
66
|
+
}));
|
|
67
|
+
res.json({ runs });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// API: Get run details
|
|
71
|
+
app.get('/api/runs/:runId', (req, res) => {
|
|
72
|
+
const run = activeRuns.get(req.params.runId);
|
|
73
|
+
if (!run) {
|
|
74
|
+
return res.status(404).json({ error: 'Run not found' });
|
|
75
|
+
}
|
|
76
|
+
res.json({
|
|
77
|
+
runId: req.params.runId,
|
|
78
|
+
workflowId: run.workflowId,
|
|
79
|
+
status: run.status,
|
|
80
|
+
startTime: run.startTime,
|
|
81
|
+
task: run.task,
|
|
82
|
+
steps: run.steps,
|
|
83
|
+
budget: run.budget,
|
|
84
|
+
currentThought: run.currentThought,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// API: Start new agent run
|
|
89
|
+
app.post('/api/runs', async (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const {
|
|
92
|
+
task,
|
|
93
|
+
workflowId,
|
|
94
|
+
llm = { provider: 'openai', model: 'gpt-4o-mini' },
|
|
95
|
+
budget = { maxCostPerRun: 10 },
|
|
96
|
+
hitl = { enabled: true },
|
|
97
|
+
plugins = [],
|
|
98
|
+
} = req.body;
|
|
99
|
+
|
|
100
|
+
if (!task) {
|
|
101
|
+
return res.status(400).json({ error: 'Task is required' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const runId = `run_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
105
|
+
const wfId = workflowId || `wf_${Date.now()}`;
|
|
106
|
+
|
|
107
|
+
// Initialize storage
|
|
108
|
+
const storage = await createStorage(config.storage?.type || 'sqlite', config.storage?.options || {});
|
|
109
|
+
|
|
110
|
+
// Create execution engine
|
|
111
|
+
const execution = await createExecutionEngine({
|
|
112
|
+
storage: { type: config.storage?.type || 'sqlite', options: config.storage?.options || {} },
|
|
113
|
+
checkpointInterval: 3,
|
|
114
|
+
idempotency: {},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Create approval gate
|
|
118
|
+
const hitlGate = await createApprovalGate({
|
|
119
|
+
store: { type: config.storage?.type || 'sqlite', options: config.storage?.options || {} },
|
|
120
|
+
notifier: { console: true },
|
|
121
|
+
defaultTimeout: 300000,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Create budget controller
|
|
125
|
+
const budgetCtrl = createBudgetController(budget);
|
|
126
|
+
|
|
127
|
+
// Load plugins
|
|
128
|
+
const pluginTools = await loadPlugins(plugins);
|
|
129
|
+
|
|
130
|
+
// Create tool registry with plugins
|
|
131
|
+
const toolRegistry = createToolRegistry({
|
|
132
|
+
approvalGate: hitlGate,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Register plugin tools
|
|
136
|
+
for (const [name, tool] of Object.entries(pluginTools)) {
|
|
137
|
+
toolRegistry.register({ ...tool, category: 'plugin' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Create LLM agent
|
|
141
|
+
const agent = await createLLMAgent({
|
|
142
|
+
llm,
|
|
143
|
+
execution: { storage: { type: config.storage?.type || 'sqlite', options: config.storage?.options || {} } },
|
|
144
|
+
hitl: { enabled: hitl.enabled !== false, defaultTimeout: 300000 },
|
|
145
|
+
budget,
|
|
146
|
+
memory: { conversation: true, episodic: true },
|
|
147
|
+
tools: { custom: toolRegistry },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Store run info
|
|
151
|
+
const runData = {
|
|
152
|
+
agent,
|
|
153
|
+
workflowId: wfId,
|
|
154
|
+
status: 'running',
|
|
155
|
+
startTime: Date.now(),
|
|
156
|
+
task,
|
|
157
|
+
steps: [],
|
|
158
|
+
budget: budgetCtrl.getStatus(runId),
|
|
159
|
+
currentThought: null,
|
|
160
|
+
execution,
|
|
161
|
+
hitl: hitlGate,
|
|
162
|
+
budgetCtrl,
|
|
163
|
+
};
|
|
164
|
+
activeRuns.set(runId, runData);
|
|
165
|
+
|
|
166
|
+
// Execute agent task
|
|
167
|
+
runAgent(runId, task, hitlGate, budgetCtrl, wfId, io);
|
|
168
|
+
|
|
169
|
+
res.json({ runId, workflowId: wfId, status: 'started' });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('Failed to start run:', error);
|
|
172
|
+
res.status(500).json({ error: error.message });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// API: Approve/reject pending approval
|
|
177
|
+
app.post('/api/approvals/:approvalId', async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
const { approvalId } = req.params;
|
|
180
|
+
const { action, reason } = req.body; // action: 'approve' | 'reject'
|
|
181
|
+
|
|
182
|
+
// Find the run with this approval
|
|
183
|
+
let targetRun = null;
|
|
184
|
+
for (const [runId, run] of activeRuns) {
|
|
185
|
+
const approval = await run.hitl.getApproval(approvalId);
|
|
186
|
+
if (approval) {
|
|
187
|
+
targetRun = run;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!targetRun) {
|
|
193
|
+
return res.status(404).json({ error: 'Approval not found in any active run' });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let result;
|
|
197
|
+
if (action === 'approve') {
|
|
198
|
+
result = await targetRun.hitl.approve(approvalId, { decider: 'ui-user', reason });
|
|
199
|
+
} else if (action === 'reject') {
|
|
200
|
+
result = await targetRun.hitl.reject(approvalId, { decider: 'ui-user', reason });
|
|
201
|
+
} else {
|
|
202
|
+
return res.status(400).json({ error: 'Invalid action. Use approve or reject' });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Broadcast to all clients
|
|
206
|
+
io.emit('approval:updated', { approvalId, status: result.status, runId: getRunIdForApproval(approvalId) });
|
|
207
|
+
|
|
208
|
+
res.json(result);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error('Approval error:', error);
|
|
211
|
+
res.status(500).json({ error: error.message });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// API: List pending approvals
|
|
216
|
+
app.get('/api/approvals', async (req, res) => {
|
|
217
|
+
try {
|
|
218
|
+
const allApprovals = [];
|
|
219
|
+
for (const [runId, run] of activeRuns) {
|
|
220
|
+
const approvals = await run.hitl.getPendingApprovals(50);
|
|
221
|
+
allApprovals.push(...approvals.map(a => ({ ...a, runId })));
|
|
222
|
+
}
|
|
223
|
+
res.json({ approvals: allApprovals });
|
|
224
|
+
} catch (error) {
|
|
225
|
+
res.status(500).json({ error: error.message });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// API: Budget status
|
|
230
|
+
app.get('/api/budget/:budgetId?', (req, res) => {
|
|
231
|
+
const budgetId = req.params.budgetId || 'default';
|
|
232
|
+
// Find run with this budget
|
|
233
|
+
for (const [runId, run] of activeRuns) {
|
|
234
|
+
if (run.budgetCtrl) {
|
|
235
|
+
return res.json(run.budgetCtrl.getStatus(budgetId));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Fallback to global
|
|
239
|
+
const budgetCtrl = createBudgetController({});
|
|
240
|
+
res.json(budgetCtrl.getStatus(budgetId));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// API: Stop a run
|
|
244
|
+
app.post('/api/runs/:runId/stop', async (req, res) => {
|
|
245
|
+
const runId = req.params.runId;
|
|
246
|
+
const run = activeRuns.get(runId);
|
|
247
|
+
if (!run) {
|
|
248
|
+
return res.status(404).json({ error: 'Run not found' });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
run.status = 'stopped';
|
|
252
|
+
await run.agent?.shutdown();
|
|
253
|
+
await run.execution?.storage?.close();
|
|
254
|
+
|
|
255
|
+
io.emit('run:stopped', { runId });
|
|
256
|
+
res.json({ success: true });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Serve static UI files (production)
|
|
260
|
+
const uiPath = resolve(__dirname, 'ui/dist');
|
|
261
|
+
app.use(express.static(uiPath));
|
|
262
|
+
|
|
263
|
+
// SPA fallback
|
|
264
|
+
app.get('*', (req, res) => {
|
|
265
|
+
if (!req.path.startsWith('/api') && !req.path.startsWith('/health')) {
|
|
266
|
+
res.sendFile(join(uiPath, 'index.html'), (err) => {
|
|
267
|
+
if (err) res.status(404).send('UI not built. Run `npm run build:ui` first.');
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Socket.io connection handling
|
|
273
|
+
io.on('connection', (socket) => {
|
|
274
|
+
connectedClients.add(socket.id);
|
|
275
|
+
console.log(`Client connected: ${socket.id} (total: ${connectedClients.size})`);
|
|
276
|
+
|
|
277
|
+
// Send current state
|
|
278
|
+
socket.emit('init', {
|
|
279
|
+
runs: Array.from(activeRuns.entries()).map(([runId, data]) => ({
|
|
280
|
+
runId,
|
|
281
|
+
workflowId: data.workflowId,
|
|
282
|
+
status: data.status,
|
|
283
|
+
startTime: data.startTime,
|
|
284
|
+
task: data.task?.slice(0, 100),
|
|
285
|
+
})),
|
|
286
|
+
approvals: await getAllPendingApprovals(),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Subscribe to run updates
|
|
290
|
+
socket.on('subscribe:run', (runId) => {
|
|
291
|
+
socket.join(`run:${runId}`);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
socket.on('unsubscribe:run', (runId) => {
|
|
295
|
+
socket.leave(`run:${runId}`);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
socket.on('disconnect', () => {
|
|
299
|
+
connectedClients.delete(socket.id);
|
|
300
|
+
console.log(`Client disconnected: ${socket.id} (total: ${connectedClients.size})`);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Broadcast helper
|
|
305
|
+
function broadcast(event, data) {
|
|
306
|
+
io.emit(event, data);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Helper to find runId for approval
|
|
310
|
+
function getRunIdForApproval(approvalId) {
|
|
311
|
+
for (const [runId, run] of activeRuns) {
|
|
312
|
+
// This is async but we need sync here - in practice would cache
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function getAllPendingApprovals() {
|
|
318
|
+
const all = [];
|
|
319
|
+
for (const [runId, run] of activeRuns) {
|
|
320
|
+
const approvals = await run.hitl.getPendingApprovals(50);
|
|
321
|
+
all.push(...approvals.map(a => ({ ...a, runId })));
|
|
322
|
+
}
|
|
323
|
+
return all;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { app, httpServer, io, broadcast, activeRuns };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Run agent task with real-time updates
|
|
331
|
+
*/
|
|
332
|
+
async function runAgent(runId, task, hitlGate, budgetCtrl, workflowId, io) {
|
|
333
|
+
const run = activeRuns.get(runId);
|
|
334
|
+
if (!run) return;
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
// Override agent's run method to emit thought logs
|
|
338
|
+
const originalRun = run.agent.run.bind(run.agent);
|
|
339
|
+
|
|
340
|
+
// We'll monkey-patch to emit thought events
|
|
341
|
+
let step = 0;
|
|
342
|
+
|
|
343
|
+
const result = await run.agent.run(task, {
|
|
344
|
+
runId,
|
|
345
|
+
budgetId: runId,
|
|
346
|
+
hitlRequired: ['file_write', 'api_call', 'send_email', 'code_exec', 'db_query'],
|
|
347
|
+
maxSteps: 50,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
run.status = result.success ? 'completed' : 'failed';
|
|
351
|
+
run.steps = result.steps;
|
|
352
|
+
run.budget = run.budgetCtrl.getStatus(runId);
|
|
353
|
+
|
|
354
|
+
io.to(`run:${runId}`).emit('run:completed', { runId, result });
|
|
355
|
+
broadcast('run:updated', { runId, status: run.status, result });
|
|
356
|
+
|
|
357
|
+
} catch (error) {
|
|
358
|
+
run.status = 'error';
|
|
359
|
+
run.error = error.message;
|
|
360
|
+
io.to(`run:${runId}`).emit('run:error', { runId, error: error.message });
|
|
361
|
+
broadcast('run:updated', { runId, status: 'error', error: error.message });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Start the server
|
|
367
|
+
*/
|
|
368
|
+
export async function startServer(config = {}) {
|
|
369
|
+
const { app, httpServer, io, broadcast, activeRuns: runs } = createApp(config);
|
|
370
|
+
|
|
371
|
+
const port = config.port || process.env.LSJI_SERVER_PORT || 3456;
|
|
372
|
+
const host = config.host || '0.0.0.0';
|
|
373
|
+
|
|
374
|
+
return new Promise((resolve) => {
|
|
375
|
+
httpServer.listen(port, host, () => {
|
|
376
|
+
console.log(`LSJI Server running at http://${host}:${port}`);
|
|
377
|
+
console.log(`WebSocket ready for connections`);
|
|
378
|
+
resolve({ app, httpServer, io, broadcast, activeRuns: runs, port, host });
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Stop the server
|
|
385
|
+
*/
|
|
386
|
+
export async function stopServer(server) {
|
|
387
|
+
// Stop all active runs
|
|
388
|
+
for (const [runId, run] of server.activeRuns) {
|
|
389
|
+
run.status = 'stopped';
|
|
390
|
+
await run.agent?.shutdown();
|
|
391
|
+
await run.execution?.storage?.close();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Close connections
|
|
395
|
+
server.io.close();
|
|
396
|
+
await new Promise(resolve => server.httpServer.close(resolve));
|
|
397
|
+
console.log('LSJI Server stopped');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// CLI entry point
|
|
401
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
402
|
+
const config = {
|
|
403
|
+
port: parseInt(process.argv[2]) || 3456,
|
|
404
|
+
storage: { type: 'sqlite', options: { path: './lsji.db' } },
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
startServer(config).catch(console.error);
|
|
408
|
+
|
|
409
|
+
// Graceful shutdown
|
|
410
|
+
process.on('SIGINT', async () => {
|
|
411
|
+
console.log('\nShutting down...');
|
|
412
|
+
process.exit(0);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export { activeRuns, connectedClients };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>LSJI Control Panel</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #e6edf3; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lsji-control-panel",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"preview": "vite preview"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"react": "^18.3.1",
|
|
12
|
+
"react-dom": "^18.3.1",
|
|
13
|
+
"socket.io-client": "^4.7.5"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
17
|
+
"vite": "^5.4.0"
|
|
18
|
+
}
|
|
19
|
+
}
|