@lobehub/lobehub 2.0.0-next.160 → 2.0.0-next.161

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.
Files changed (67) hide show
  1. package/.env.example +10 -0
  2. package/CHANGELOG.md +25 -0
  3. package/changelog/v1.json +9 -0
  4. package/e2e/src/steps/hooks.ts +1 -0
  5. package/locales/ar/setting.json +25 -0
  6. package/locales/bg-BG/setting.json +25 -0
  7. package/locales/de-DE/setting.json +25 -0
  8. package/locales/en-US/setting.json +25 -0
  9. package/locales/es-ES/setting.json +25 -0
  10. package/locales/fa-IR/setting.json +25 -0
  11. package/locales/fr-FR/setting.json +25 -0
  12. package/locales/it-IT/setting.json +25 -0
  13. package/locales/ja-JP/setting.json +25 -0
  14. package/locales/ko-KR/setting.json +25 -0
  15. package/locales/nl-NL/setting.json +25 -0
  16. package/locales/pl-PL/setting.json +25 -0
  17. package/locales/pt-BR/setting.json +25 -0
  18. package/locales/ru-RU/setting.json +25 -0
  19. package/locales/tr-TR/setting.json +25 -0
  20. package/locales/vi-VN/setting.json +25 -0
  21. package/locales/zh-CN/setting.json +25 -0
  22. package/locales/zh-TW/setting.json +25 -0
  23. package/next.config.ts +13 -1
  24. package/package.json +3 -1
  25. package/packages/const/src/index.ts +1 -0
  26. package/packages/const/src/klavis.ts +163 -0
  27. package/packages/database/migrations/meta/_journal.json +1 -1
  28. package/packages/database/src/core/migrations.json +1 -1
  29. package/packages/database/src/models/plugin.ts +1 -1
  30. package/packages/types/src/message/common/tools.ts +9 -0
  31. package/packages/types/src/serverConfig.ts +1 -0
  32. package/packages/types/src/tool/plugin.ts +10 -0
  33. package/src/config/klavis.ts +41 -0
  34. package/src/features/ChatInput/ActionBar/Tools/KlavisServerItem.tsx +351 -0
  35. package/src/features/ChatInput/ActionBar/Tools/index.tsx +56 -4
  36. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +174 -6
  37. package/src/features/ChatInput/ActionBar/components/ActionDropdown.tsx +3 -1
  38. package/src/helpers/toolEngineering/index.test.ts +3 -0
  39. package/src/helpers/toolEngineering/index.ts +13 -2
  40. package/src/libs/klavis/index.ts +36 -0
  41. package/src/locales/default/setting.ts +25 -0
  42. package/src/server/globalConfig/index.ts +2 -0
  43. package/src/server/routers/lambda/index.ts +2 -0
  44. package/src/server/routers/lambda/klavis.ts +249 -0
  45. package/src/server/routers/tools/index.ts +2 -0
  46. package/src/server/routers/tools/klavis.ts +80 -0
  47. package/src/server/services/mcp/index.ts +61 -15
  48. package/src/services/import/index.test.ts +658 -0
  49. package/src/services/mcp.test.ts +1 -1
  50. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +2 -3
  51. package/src/store/chat/slices/plugin/action.test.ts +0 -1
  52. package/src/store/chat/slices/plugin/actions/internals.ts +22 -2
  53. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +108 -0
  54. package/src/store/serverConfig/index.ts +1 -1
  55. package/src/store/serverConfig/selectors.ts +1 -0
  56. package/src/store/tool/initialState.ts +4 -1
  57. package/src/store/tool/selectors/index.ts +1 -0
  58. package/src/store/tool/slices/builtin/selectors.ts +25 -3
  59. package/src/store/tool/slices/klavisStore/action.test.ts +512 -0
  60. package/src/store/tool/slices/klavisStore/action.ts +375 -0
  61. package/src/store/tool/slices/klavisStore/index.ts +4 -0
  62. package/src/store/tool/slices/klavisStore/initialState.ts +25 -0
  63. package/src/store/tool/slices/klavisStore/selectors.test.ts +371 -0
  64. package/src/store/tool/slices/klavisStore/selectors.ts +123 -0
  65. package/src/store/tool/slices/klavisStore/types.ts +100 -0
  66. package/src/store/tool/slices/plugin/selectors.ts +16 -13
  67. package/src/store/tool/store.ts +4 -1
@@ -0,0 +1,512 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { lambdaClient, toolsClient } from '@/libs/trpc/client';
5
+
6
+ import { useToolStore } from '../../store';
7
+ import { KlavisServerStatus } from './types';
8
+
9
+ vi.mock('zustand/traditional');
10
+
11
+ vi.mock('@/libs/trpc/client', () => ({
12
+ lambdaClient: {
13
+ klavis: {
14
+ createServerInstance: { mutate: vi.fn() },
15
+ deleteServerInstance: { mutate: vi.fn() },
16
+ getKlavisPlugins: { query: vi.fn() },
17
+ getServerInstance: { query: vi.fn() },
18
+ updateKlavisPlugin: { mutate: vi.fn() },
19
+ },
20
+ },
21
+ toolsClient: {
22
+ klavis: {
23
+ callTool: { mutate: vi.fn() },
24
+ listTools: { query: vi.fn() },
25
+ },
26
+ },
27
+ }));
28
+
29
+ describe('klavisStore actions', () => {
30
+ describe('callKlavisTool', () => {
31
+ it('should call tool successfully and return result', async () => {
32
+ const { result } = renderHook(() => useToolStore());
33
+
34
+ act(() => {
35
+ useToolStore.setState({
36
+ servers: [],
37
+ loadingServerIds: new Set(),
38
+ executingToolIds: new Set(),
39
+ });
40
+ });
41
+
42
+ const mockResponse = {
43
+ content: 'Tool result',
44
+ success: true,
45
+ state: { content: [], isError: false },
46
+ };
47
+ vi.mocked(toolsClient.klavis.callTool.mutate).mockResolvedValue(mockResponse as any);
48
+
49
+ let callResult;
50
+ await act(async () => {
51
+ callResult = await result.current.callKlavisTool({
52
+ serverUrl: 'https://klavis.ai/gmail',
53
+ toolName: 'sendEmail',
54
+ toolArgs: { to: 'test@example.com' },
55
+ });
56
+ });
57
+
58
+ expect(callResult).toEqual({ data: mockResponse, success: true });
59
+ expect(toolsClient.klavis.callTool.mutate).toHaveBeenCalledWith({
60
+ serverUrl: 'https://klavis.ai/gmail',
61
+ toolName: 'sendEmail',
62
+ toolArgs: { to: 'test@example.com' },
63
+ });
64
+ });
65
+
66
+ it('should handle error and return error result', async () => {
67
+ const { result } = renderHook(() => useToolStore());
68
+
69
+ act(() => {
70
+ useToolStore.setState({
71
+ servers: [],
72
+ loadingServerIds: new Set(),
73
+ executingToolIds: new Set(),
74
+ });
75
+ });
76
+
77
+ vi.mocked(toolsClient.klavis.callTool.mutate).mockRejectedValue(
78
+ new Error('Tool call failed'),
79
+ );
80
+
81
+ let callResult;
82
+ await act(async () => {
83
+ callResult = await result.current.callKlavisTool({
84
+ serverUrl: 'https://klavis.ai/gmail',
85
+ toolName: 'sendEmail',
86
+ });
87
+ });
88
+
89
+ expect(callResult).toEqual({ error: 'Tool call failed', success: false });
90
+ expect(result.current.executingToolIds.has('https://klavis.ai/gmail:sendEmail')).toBe(false);
91
+ });
92
+ });
93
+
94
+ describe('createKlavisServer', () => {
95
+ it('should create server successfully', async () => {
96
+ const { result } = renderHook(() => useToolStore());
97
+
98
+ act(() => {
99
+ useToolStore.setState({
100
+ servers: [],
101
+ loadingServerIds: new Set(),
102
+ executingToolIds: new Set(),
103
+ });
104
+ });
105
+
106
+ const mockResponse = {
107
+ identifier: 'gmail',
108
+ instanceId: 'inst-123',
109
+ isAuthenticated: true,
110
+ oauthUrl: undefined,
111
+ serverName: 'Gmail',
112
+ serverUrl: 'https://klavis.ai/gmail',
113
+ };
114
+ vi.mocked(lambdaClient.klavis.createServerInstance.mutate).mockResolvedValue(mockResponse);
115
+
116
+ let server;
117
+ await act(async () => {
118
+ server = await result.current.createKlavisServer({
119
+ identifier: 'gmail',
120
+ serverName: 'Gmail',
121
+ userId: 'user-123',
122
+ });
123
+ });
124
+
125
+ expect(server).toMatchObject({
126
+ identifier: 'gmail',
127
+ instanceId: 'inst-123',
128
+ isAuthenticated: true,
129
+ serverName: 'Gmail',
130
+ serverUrl: 'https://klavis.ai/gmail',
131
+ status: KlavisServerStatus.CONNECTED,
132
+ });
133
+ expect(result.current.servers).toHaveLength(1);
134
+ expect(result.current.loadingServerIds.has('gmail')).toBe(false);
135
+ });
136
+
137
+ it('should create server with pending auth when oauth needed', async () => {
138
+ const { result } = renderHook(() => useToolStore());
139
+
140
+ act(() => {
141
+ useToolStore.setState({
142
+ servers: [],
143
+ loadingServerIds: new Set(),
144
+ executingToolIds: new Set(),
145
+ });
146
+ });
147
+
148
+ const mockResponse = {
149
+ identifier: 'github',
150
+ instanceId: 'inst-123',
151
+ isAuthenticated: false,
152
+ oauthUrl: 'https://oauth.klavis.ai/github',
153
+ serverName: 'GitHub',
154
+ serverUrl: 'https://klavis.ai/github',
155
+ };
156
+ vi.mocked(lambdaClient.klavis.createServerInstance.mutate).mockResolvedValue(mockResponse);
157
+
158
+ let server;
159
+ await act(async () => {
160
+ server = await result.current.createKlavisServer({
161
+ identifier: 'github',
162
+ serverName: 'GitHub',
163
+ userId: 'user-123',
164
+ });
165
+ });
166
+
167
+ expect(server).toMatchObject({
168
+ status: KlavisServerStatus.PENDING_AUTH,
169
+ oauthUrl: 'https://oauth.klavis.ai/github',
170
+ });
171
+ });
172
+
173
+ it('should update existing server if already exists', async () => {
174
+ const { result } = renderHook(() => useToolStore());
175
+
176
+ act(() => {
177
+ useToolStore.setState({
178
+ servers: [
179
+ {
180
+ identifier: 'gmail',
181
+ serverName: 'Gmail',
182
+ instanceId: 'old-inst',
183
+ serverUrl: 'https://old.klavis.ai/gmail',
184
+ status: KlavisServerStatus.ERROR,
185
+ isAuthenticated: false,
186
+ createdAt: Date.now(),
187
+ },
188
+ ],
189
+ loadingServerIds: new Set(),
190
+ executingToolIds: new Set(),
191
+ });
192
+ });
193
+
194
+ const mockResponse = {
195
+ identifier: 'gmail',
196
+ instanceId: 'new-inst',
197
+ isAuthenticated: true,
198
+ oauthUrl: undefined,
199
+ serverName: 'Gmail',
200
+ serverUrl: 'https://klavis.ai/gmail',
201
+ };
202
+ vi.mocked(lambdaClient.klavis.createServerInstance.mutate).mockResolvedValue(mockResponse);
203
+
204
+ await act(async () => {
205
+ await result.current.createKlavisServer({
206
+ identifier: 'gmail',
207
+ serverName: 'Gmail',
208
+ userId: 'user-123',
209
+ });
210
+ });
211
+
212
+ expect(result.current.servers).toHaveLength(1);
213
+ expect(result.current.servers[0].instanceId).toBe('new-inst');
214
+ });
215
+
216
+ it('should handle creation error', async () => {
217
+ const { result } = renderHook(() => useToolStore());
218
+
219
+ act(() => {
220
+ useToolStore.setState({
221
+ servers: [],
222
+ loadingServerIds: new Set(),
223
+ executingToolIds: new Set(),
224
+ });
225
+ });
226
+
227
+ vi.mocked(lambdaClient.klavis.createServerInstance.mutate).mockRejectedValue(
228
+ new Error('Creation failed'),
229
+ );
230
+
231
+ let server;
232
+ await act(async () => {
233
+ server = await result.current.createKlavisServer({
234
+ identifier: 'gmail',
235
+ serverName: 'Gmail',
236
+ userId: 'user-123',
237
+ });
238
+ });
239
+
240
+ expect(server).toBeUndefined();
241
+ expect(result.current.servers).toHaveLength(0);
242
+ expect(result.current.loadingServerIds.has('gmail')).toBe(false);
243
+ });
244
+ });
245
+
246
+ // Note: useFetchUserKlavisServers uses SWR hook and requires different testing approach
247
+ // The SWR hook tests should be done in integration tests or with SWR testing utilities
248
+
249
+ describe('refreshKlavisServerTools', () => {
250
+ it('should refresh tools for authenticated server', async () => {
251
+ const { result } = renderHook(() => useToolStore());
252
+
253
+ act(() => {
254
+ useToolStore.setState({
255
+ servers: [
256
+ {
257
+ identifier: 'gmail',
258
+ serverName: 'Gmail',
259
+ instanceId: 'inst-1',
260
+ serverUrl: 'https://klavis.ai/gmail',
261
+ status: KlavisServerStatus.PENDING_AUTH,
262
+ isAuthenticated: false,
263
+ createdAt: Date.now(),
264
+ },
265
+ ],
266
+ loadingServerIds: new Set(),
267
+ executingToolIds: new Set(),
268
+ });
269
+ });
270
+
271
+ vi.mocked(lambdaClient.klavis.getServerInstance.query).mockResolvedValue({
272
+ isAuthenticated: true,
273
+ authNeeded: false,
274
+ } as any);
275
+
276
+ vi.mocked(toolsClient.klavis.listTools.query).mockResolvedValue({
277
+ tools: [{ name: 'sendEmail', description: 'Send email', inputSchema: { type: 'object' } }],
278
+ });
279
+
280
+ vi.mocked(lambdaClient.klavis.updateKlavisPlugin.mutate).mockResolvedValue({} as any);
281
+
282
+ await act(async () => {
283
+ await result.current.refreshKlavisServerTools('gmail');
284
+ });
285
+
286
+ expect(result.current.servers[0].status).toBe(KlavisServerStatus.CONNECTED);
287
+ expect(result.current.servers[0].isAuthenticated).toBe(true);
288
+ expect(result.current.servers[0].tools).toHaveLength(1);
289
+ });
290
+
291
+ it('should remove server when auth failed and auth needed', async () => {
292
+ const { result } = renderHook(() => useToolStore());
293
+
294
+ act(() => {
295
+ useToolStore.setState({
296
+ servers: [
297
+ {
298
+ identifier: 'gmail',
299
+ serverName: 'Gmail',
300
+ instanceId: 'inst-1',
301
+ serverUrl: 'https://klavis.ai/gmail',
302
+ status: KlavisServerStatus.PENDING_AUTH,
303
+ isAuthenticated: false,
304
+ createdAt: Date.now(),
305
+ },
306
+ ],
307
+ loadingServerIds: new Set(),
308
+ executingToolIds: new Set(),
309
+ });
310
+ });
311
+
312
+ vi.mocked(lambdaClient.klavis.getServerInstance.query).mockResolvedValue({
313
+ isAuthenticated: false,
314
+ authNeeded: true,
315
+ } as any);
316
+
317
+ vi.mocked(lambdaClient.klavis.deleteServerInstance.mutate).mockResolvedValue({} as any);
318
+
319
+ await act(async () => {
320
+ await result.current.refreshKlavisServerTools('gmail');
321
+ });
322
+
323
+ expect(result.current.servers).toHaveLength(0);
324
+ expect(lambdaClient.klavis.deleteServerInstance.mutate).toHaveBeenCalled();
325
+ });
326
+
327
+ it('should do nothing when server not found', async () => {
328
+ vi.mocked(lambdaClient.klavis.getServerInstance.query).mockClear();
329
+
330
+ const { result } = renderHook(() => useToolStore());
331
+
332
+ act(() => {
333
+ useToolStore.setState({
334
+ servers: [],
335
+ loadingServerIds: new Set(),
336
+ executingToolIds: new Set(),
337
+ });
338
+ });
339
+
340
+ await act(async () => {
341
+ await result.current.refreshKlavisServerTools('non-existent');
342
+ });
343
+
344
+ expect(lambdaClient.klavis.getServerInstance.query).not.toHaveBeenCalled();
345
+ });
346
+
347
+ it('should handle refresh error', async () => {
348
+ const { result } = renderHook(() => useToolStore());
349
+
350
+ act(() => {
351
+ useToolStore.setState({
352
+ servers: [
353
+ {
354
+ identifier: 'gmail',
355
+ serverName: 'Gmail',
356
+ instanceId: 'inst-1',
357
+ serverUrl: 'https://klavis.ai/gmail',
358
+ status: KlavisServerStatus.CONNECTED,
359
+ isAuthenticated: true,
360
+ createdAt: Date.now(),
361
+ },
362
+ ],
363
+ loadingServerIds: new Set(),
364
+ executingToolIds: new Set(),
365
+ });
366
+ });
367
+
368
+ vi.mocked(lambdaClient.klavis.getServerInstance.query).mockResolvedValue({
369
+ isAuthenticated: true,
370
+ authNeeded: false,
371
+ } as any);
372
+
373
+ vi.mocked(toolsClient.klavis.listTools.query).mockRejectedValue(new Error('Refresh failed'));
374
+
375
+ await act(async () => {
376
+ await result.current.refreshKlavisServerTools('gmail');
377
+ });
378
+
379
+ expect(result.current.servers[0].status).toBe(KlavisServerStatus.ERROR);
380
+ expect(result.current.servers[0].errorMessage).toBe('Refresh failed');
381
+ });
382
+ });
383
+
384
+ describe('removeKlavisServer', () => {
385
+ it('should remove server from state and call API', async () => {
386
+ const { result } = renderHook(() => useToolStore());
387
+
388
+ act(() => {
389
+ useToolStore.setState({
390
+ servers: [
391
+ {
392
+ identifier: 'gmail',
393
+ serverName: 'Gmail',
394
+ instanceId: 'inst-1',
395
+ serverUrl: 'https://klavis.ai/gmail',
396
+ status: KlavisServerStatus.CONNECTED,
397
+ isAuthenticated: true,
398
+ createdAt: Date.now(),
399
+ },
400
+ ],
401
+ loadingServerIds: new Set(),
402
+ executingToolIds: new Set(),
403
+ });
404
+ });
405
+
406
+ vi.mocked(lambdaClient.klavis.deleteServerInstance.mutate).mockResolvedValue({} as any);
407
+
408
+ await act(async () => {
409
+ await result.current.removeKlavisServer('gmail');
410
+ });
411
+
412
+ expect(result.current.servers).toHaveLength(0);
413
+ expect(lambdaClient.klavis.deleteServerInstance.mutate).toHaveBeenCalledWith({
414
+ identifier: 'gmail',
415
+ instanceId: 'inst-1',
416
+ });
417
+ });
418
+
419
+ it('should handle remove when server not found', async () => {
420
+ vi.mocked(lambdaClient.klavis.deleteServerInstance.mutate).mockClear();
421
+
422
+ const { result } = renderHook(() => useToolStore());
423
+
424
+ act(() => {
425
+ useToolStore.setState({
426
+ servers: [],
427
+ loadingServerIds: new Set(),
428
+ executingToolIds: new Set(),
429
+ });
430
+ });
431
+
432
+ await act(async () => {
433
+ await result.current.removeKlavisServer('non-existent');
434
+ });
435
+
436
+ expect(lambdaClient.klavis.deleteServerInstance.mutate).not.toHaveBeenCalled();
437
+ });
438
+
439
+ it('should handle API error gracefully', async () => {
440
+ const { result } = renderHook(() => useToolStore());
441
+
442
+ act(() => {
443
+ useToolStore.setState({
444
+ servers: [
445
+ {
446
+ identifier: 'gmail',
447
+ serverName: 'Gmail',
448
+ instanceId: 'inst-1',
449
+ serverUrl: 'https://klavis.ai/gmail',
450
+ status: KlavisServerStatus.CONNECTED,
451
+ isAuthenticated: true,
452
+ createdAt: Date.now(),
453
+ },
454
+ ],
455
+ loadingServerIds: new Set(),
456
+ executingToolIds: new Set(),
457
+ });
458
+ });
459
+
460
+ vi.mocked(lambdaClient.klavis.deleteServerInstance.mutate).mockRejectedValue(
461
+ new Error('Delete failed'),
462
+ );
463
+
464
+ await act(async () => {
465
+ await result.current.removeKlavisServer('gmail');
466
+ });
467
+
468
+ expect(result.current.servers).toHaveLength(0);
469
+ });
470
+ });
471
+
472
+ describe('completeKlavisServerAuth', () => {
473
+ it('should call refreshKlavisServerTools', async () => {
474
+ const { result } = renderHook(() => useToolStore());
475
+
476
+ act(() => {
477
+ useToolStore.setState({
478
+ servers: [
479
+ {
480
+ identifier: 'gmail',
481
+ serverName: 'Gmail',
482
+ instanceId: 'inst-1',
483
+ serverUrl: 'https://klavis.ai/gmail',
484
+ status: KlavisServerStatus.PENDING_AUTH,
485
+ isAuthenticated: false,
486
+ createdAt: Date.now(),
487
+ },
488
+ ],
489
+ loadingServerIds: new Set(),
490
+ executingToolIds: new Set(),
491
+ });
492
+ });
493
+
494
+ vi.mocked(lambdaClient.klavis.getServerInstance.query).mockResolvedValue({
495
+ isAuthenticated: true,
496
+ authNeeded: false,
497
+ } as any);
498
+
499
+ vi.mocked(toolsClient.klavis.listTools.query).mockResolvedValue({
500
+ tools: [],
501
+ });
502
+
503
+ vi.mocked(lambdaClient.klavis.updateKlavisPlugin.mutate).mockResolvedValue({} as any);
504
+
505
+ await act(async () => {
506
+ await result.current.completeKlavisServerAuth('gmail');
507
+ });
508
+
509
+ expect(lambdaClient.klavis.getServerInstance.query).toHaveBeenCalled();
510
+ });
511
+ });
512
+ });