@kirosnn/mosaic 0.0.7

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 (154) hide show
  1. package/.mosaic/mosaic.local.jsonc +0 -0
  2. package/MOSAIC.md +188 -0
  3. package/README.md +127 -0
  4. package/docs/mosaic.png +0 -0
  5. package/package.json +42 -0
  6. package/src/agent/Agent.ts +131 -0
  7. package/src/agent/context.ts +96 -0
  8. package/src/agent/index.ts +2 -0
  9. package/src/agent/prompts/systemPrompt.ts +138 -0
  10. package/src/agent/prompts/toolsPrompt.ts +139 -0
  11. package/src/agent/provider/anthropic.ts +122 -0
  12. package/src/agent/provider/google.ts +124 -0
  13. package/src/agent/provider/mistral.ts +117 -0
  14. package/src/agent/provider/ollama.ts +531 -0
  15. package/src/agent/provider/openai.ts +220 -0
  16. package/src/agent/provider/xai.ts +122 -0
  17. package/src/agent/tools/bash.ts +20 -0
  18. package/src/agent/tools/definitions.ts +27 -0
  19. package/src/agent/tools/edit.ts +23 -0
  20. package/src/agent/tools/executor.ts +751 -0
  21. package/src/agent/tools/explore.ts +18 -0
  22. package/src/agent/tools/exploreExecutor.ts +320 -0
  23. package/src/agent/tools/glob.ts +16 -0
  24. package/src/agent/tools/grep.ts +19 -0
  25. package/src/agent/tools/index.ts +4 -0
  26. package/src/agent/tools/list.ts +20 -0
  27. package/src/agent/tools/question.ts +20 -0
  28. package/src/agent/tools/read.ts +15 -0
  29. package/src/agent/tools/write.ts +21 -0
  30. package/src/agent/types.ts +155 -0
  31. package/src/components/App.tsx +174 -0
  32. package/src/components/CommandsModal.tsx +77 -0
  33. package/src/components/CustomInput.tsx +328 -0
  34. package/src/components/Main.tsx +1112 -0
  35. package/src/components/Notification.tsx +91 -0
  36. package/src/components/SelectList.tsx +47 -0
  37. package/src/components/Setup.tsx +528 -0
  38. package/src/components/ShortcutsModal.tsx +67 -0
  39. package/src/components/Welcome.tsx +39 -0
  40. package/src/components/main/ApprovalPanel.tsx +134 -0
  41. package/src/components/main/ChatPage.tsx +516 -0
  42. package/src/components/main/HomePage.tsx +111 -0
  43. package/src/components/main/QuestionPanel.tsx +85 -0
  44. package/src/components/main/ThinkingIndicator.tsx +101 -0
  45. package/src/components/main/types.ts +55 -0
  46. package/src/components/main/wrapText.ts +41 -0
  47. package/src/index.tsx +212 -0
  48. package/src/utils/approvalBridge.ts +129 -0
  49. package/src/utils/commands/echo.ts +22 -0
  50. package/src/utils/commands/help.ts +25 -0
  51. package/src/utils/commands/index.ts +68 -0
  52. package/src/utils/commands/init.ts +68 -0
  53. package/src/utils/commands/redo.ts +74 -0
  54. package/src/utils/commands/registry.ts +29 -0
  55. package/src/utils/commands/sessions.ts +129 -0
  56. package/src/utils/commands/types.ts +20 -0
  57. package/src/utils/commands/undo.ts +75 -0
  58. package/src/utils/commands/web.ts +77 -0
  59. package/src/utils/config.ts +357 -0
  60. package/src/utils/diff.ts +201 -0
  61. package/src/utils/diffRendering.tsx +62 -0
  62. package/src/utils/exploreBridge.ts +87 -0
  63. package/src/utils/fileChangeTracker.ts +98 -0
  64. package/src/utils/fileChangesBridge.ts +18 -0
  65. package/src/utils/history.ts +106 -0
  66. package/src/utils/markdown.tsx +232 -0
  67. package/src/utils/models.ts +304 -0
  68. package/src/utils/questionBridge.ts +122 -0
  69. package/src/utils/terminalUtils.ts +25 -0
  70. package/src/utils/toolFormatting.ts +384 -0
  71. package/src/utils/undoRedo.ts +429 -0
  72. package/src/utils/undoRedoBridge.ts +45 -0
  73. package/src/utils/undoRedoDb.ts +338 -0
  74. package/src/utils/uninstall.ts +45 -0
  75. package/src/utils/version.ts +3 -0
  76. package/src/web/app.tsx +606 -0
  77. package/src/web/assets/css/ChatPage.css +212 -0
  78. package/src/web/assets/css/FileExplorer.css +202 -0
  79. package/src/web/assets/css/HomePage.css +119 -0
  80. package/src/web/assets/css/Markdown.css +178 -0
  81. package/src/web/assets/css/MessageItem.css +160 -0
  82. package/src/web/assets/css/Sidebar.css +208 -0
  83. package/src/web/assets/css/SidebarModal.css +137 -0
  84. package/src/web/assets/css/ThinkingIndicator.css +47 -0
  85. package/src/web/assets/css/ToolMessage.css +148 -0
  86. package/src/web/assets/css/global.css +226 -0
  87. package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
  88. package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
  89. package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
  90. package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
  91. package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
  92. package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
  93. package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
  94. package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
  95. package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
  96. package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
  97. package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
  98. package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
  99. package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
  100. package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
  101. package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
  102. package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
  103. package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
  104. package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
  105. package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
  106. package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
  107. package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
  108. package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
  109. package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
  110. package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
  111. package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
  112. package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
  113. package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
  114. package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
  115. package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
  116. package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
  117. package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
  118. package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
  119. package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
  120. package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
  121. package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
  122. package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
  123. package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
  124. package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
  125. package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
  126. package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
  127. package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
  128. package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
  129. package/src/web/assets/images/favicon-v2.svg +6 -0
  130. package/src/web/assets/images/favicon.png +0 -0
  131. package/src/web/assets/images/foruse.svg +5 -0
  132. package/src/web/assets/images/logo_black.svg +5 -0
  133. package/src/web/assets/images/logo_white.svg +5 -0
  134. package/src/web/assets/images/logoblack.png +0 -0
  135. package/src/web/assets/images/logowhite.png +0 -0
  136. package/src/web/build.ts +23 -0
  137. package/src/web/components/ApprovalPanel.tsx +191 -0
  138. package/src/web/components/ChatPage.tsx +273 -0
  139. package/src/web/components/FileExplorer.tsx +162 -0
  140. package/src/web/components/HomePage.tsx +121 -0
  141. package/src/web/components/MessageItem.tsx +178 -0
  142. package/src/web/components/Modal.tsx +30 -0
  143. package/src/web/components/QuestionPanel.tsx +149 -0
  144. package/src/web/components/Setup.tsx +211 -0
  145. package/src/web/components/Sidebar.tsx +292 -0
  146. package/src/web/components/ThinkingIndicator.tsx +85 -0
  147. package/src/web/logo_black.svg +5 -0
  148. package/src/web/logo_white.svg +5 -0
  149. package/src/web/router.ts +46 -0
  150. package/src/web/server.tsx +662 -0
  151. package/src/web/storage.ts +92 -0
  152. package/src/web/types.ts +17 -0
  153. package/src/web/utils.ts +61 -0
  154. package/tsconfig.json +33 -0
@@ -0,0 +1,220 @@
1
+ import { streamText, CoreMessage } from 'ai';
2
+ import { createOpenAI } from '@ai-sdk/openai';
3
+ import { AgentEvent, Provider, ProviderConfig, ProviderSendOptions } from '../types';
4
+
5
+ export class OpenAIProvider implements Provider {
6
+ async *sendMessage(
7
+ messages: CoreMessage[],
8
+ config: ProviderConfig,
9
+ options?: ProviderSendOptions
10
+ ): AsyncGenerator<AgentEvent> {
11
+ const cleanApiKey = config.apiKey?.trim().replace(/[\r\n]+/g, '');
12
+ const cleanModel = config.model.trim().replace(/[\r\n]+/g, '');
13
+
14
+ const openai = createOpenAI({
15
+ apiKey: cleanApiKey,
16
+ });
17
+
18
+ type OpenAIEndpoint = 'responses' | 'chat' | 'completion';
19
+
20
+ const pickModel = (endpoint: OpenAIEndpoint) => {
21
+ switch (endpoint) {
22
+ case 'responses':
23
+ return openai.responses(cleanModel);
24
+ case 'chat':
25
+ return openai.chat(cleanModel);
26
+ case 'completion':
27
+ return openai.completion(cleanModel);
28
+ }
29
+ };
30
+
31
+ const run = async function* (
32
+ endpoint: OpenAIEndpoint,
33
+ strictJsonSchema: boolean
34
+ ): AsyncGenerator<AgentEvent> {
35
+ const result = streamText({
36
+ model: pickModel(endpoint),
37
+ messages: messages,
38
+ system: config.systemPrompt,
39
+ tools: config.tools,
40
+ maxSteps: config.maxSteps ?? 10,
41
+ abortSignal: options?.abortSignal,
42
+ providerOptions: {
43
+ openai: {
44
+ strictJsonSchema,
45
+ reasoningEffort: 'medium',
46
+ },
47
+ },
48
+ });
49
+
50
+ let stepCounter = 0;
51
+
52
+ for await (const chunk of result.fullStream as any) {
53
+ const c: any = chunk;
54
+ switch (c.type) {
55
+ case 'reasoning':
56
+ if (c.textDelta) {
57
+ yield {
58
+ type: 'reasoning-delta',
59
+ content: c.textDelta,
60
+ };
61
+ }
62
+ break;
63
+
64
+ case 'text-delta':
65
+ yield {
66
+ type: 'text-delta',
67
+ content: c.textDelta,
68
+ };
69
+ break;
70
+
71
+ case 'step-start':
72
+ yield {
73
+ type: 'step-start',
74
+ stepNumber: typeof c.stepIndex === 'number' ? c.stepIndex : stepCounter,
75
+ };
76
+ stepCounter++;
77
+ break;
78
+
79
+ case 'step-finish':
80
+ yield {
81
+ type: 'step-finish',
82
+ stepNumber:
83
+ typeof c.stepIndex === 'number' ? c.stepIndex : Math.max(0, stepCounter - 1),
84
+ finishReason: String(c.finishReason ?? 'stop'),
85
+ };
86
+ break;
87
+
88
+ case 'tool-call':
89
+ yield {
90
+ type: 'tool-call-end',
91
+ toolCallId: String(c.toolCallId ?? ''),
92
+ toolName: String(c.toolName ?? ''),
93
+ args: (c.args ?? {}) as Record<string, unknown>,
94
+ };
95
+ break;
96
+
97
+ case 'tool-result':
98
+ yield {
99
+ type: 'tool-result',
100
+ toolCallId: String(c.toolCallId ?? ''),
101
+ toolName: String(c.toolName ?? ''),
102
+ result: c.result,
103
+ };
104
+ break;
105
+
106
+ case 'finish':
107
+ yield {
108
+ type: 'finish',
109
+ finishReason: String(c.finishReason ?? 'stop'),
110
+ usage: c.usage,
111
+ };
112
+ break;
113
+
114
+ case 'error':
115
+ {
116
+ const err = c.error;
117
+ const msg =
118
+ err instanceof Error
119
+ ? err.message
120
+ : typeof err === 'string'
121
+ ? err
122
+ : 'Unknown error';
123
+ yield {
124
+ type: 'error',
125
+ error: msg,
126
+ };
127
+ }
128
+ break;
129
+ }
130
+ }
131
+
132
+ return;
133
+ };
134
+
135
+ const classifyEndpointError = (msg: string): OpenAIEndpoint | null => {
136
+ const m = msg || '';
137
+ if (m.includes('v1/chat/completions')) {
138
+ if (m.toLowerCase().includes('not a chat model')) return 'responses';
139
+ }
140
+ if (m.includes('v1/responses')) {
141
+ if (m.toLowerCase().includes('not supported') || m.toLowerCase().includes('unknown')) return 'chat';
142
+ }
143
+ if (m.includes('v1/completions')) {
144
+ return 'completion';
145
+ }
146
+ if (m.toLowerCase().includes('did you mean to use v1/completions')) {
147
+ return 'completion';
148
+ }
149
+ return null;
150
+ };
151
+
152
+ try {
153
+ yield* run('responses', true);
154
+ } catch (error) {
155
+ if (options?.abortSignal?.aborted) return;
156
+ const msg = error instanceof Error ? error.message : String(error);
157
+ const looksLikeStrictSchemaError =
158
+ msg.includes('Invalid schema for function') &&
159
+ msg.includes('required') &&
160
+ msg.includes('properties');
161
+
162
+ if (looksLikeStrictSchemaError) {
163
+ try {
164
+ yield* run('responses', false);
165
+ return;
166
+ } catch (retryError) {
167
+ if (options?.abortSignal?.aborted) return;
168
+ yield {
169
+ type: 'error',
170
+ error: retryError instanceof Error ? retryError.message : 'Unknown error occurred',
171
+ };
172
+ return;
173
+ }
174
+ }
175
+
176
+ const fallbackEndpoint = classifyEndpointError(msg);
177
+ if (fallbackEndpoint && fallbackEndpoint !== 'responses') {
178
+ try {
179
+ yield* run(fallbackEndpoint, true);
180
+ return;
181
+ } catch (endpointError) {
182
+ if (options?.abortSignal?.aborted) return;
183
+ const endpointMsg = endpointError instanceof Error ? endpointError.message : String(endpointError);
184
+ const strictSchemaFromFallback =
185
+ endpointMsg.includes('Invalid schema for function') &&
186
+ endpointMsg.includes('required') &&
187
+ endpointMsg.includes('properties');
188
+
189
+ if (strictSchemaFromFallback) {
190
+ try {
191
+ yield* run(fallbackEndpoint, false);
192
+ return;
193
+ } catch (endpointRetryError) {
194
+ if (options?.abortSignal?.aborted) return;
195
+ yield {
196
+ type: 'error',
197
+ error:
198
+ endpointRetryError instanceof Error
199
+ ? endpointRetryError.message
200
+ : 'Unknown error occurred',
201
+ };
202
+ return;
203
+ }
204
+ }
205
+
206
+ yield {
207
+ type: 'error',
208
+ error: endpointMsg || 'Unknown error occurred',
209
+ };
210
+ return;
211
+ }
212
+ }
213
+
214
+ yield {
215
+ type: 'error',
216
+ error: msg || 'Unknown error occurred',
217
+ };
218
+ }
219
+ }
220
+ }
@@ -0,0 +1,122 @@
1
+ import { streamText, CoreMessage } from 'ai';
2
+ import { createXai } from '@ai-sdk/xai';
3
+ import { AgentEvent, Provider, ProviderConfig, ProviderSendOptions } from '../types';
4
+
5
+ export class XaiProvider implements Provider {
6
+ async *sendMessage(
7
+ messages: CoreMessage[],
8
+ config: ProviderConfig,
9
+ options?: ProviderSendOptions
10
+ ): AsyncGenerator<AgentEvent> {
11
+ const cleanApiKey = config.apiKey?.trim().replace(/[\r\n]+/g, '');
12
+ const cleanModel = config.model.trim().replace(/[\r\n]+/g, '');
13
+
14
+ const xai = createXai({
15
+ apiKey: cleanApiKey,
16
+ });
17
+
18
+ const result = streamText({
19
+ model: xai(cleanModel),
20
+ messages: messages,
21
+ system: config.systemPrompt,
22
+ tools: config.tools,
23
+ maxSteps: config.maxSteps || 10,
24
+ abortSignal: options?.abortSignal,
25
+ providerOptions: {
26
+ xai: {
27
+ reasoningEffort: 'high',
28
+ },
29
+ },
30
+ });
31
+
32
+ try {
33
+ let stepCounter = 0;
34
+
35
+ for await (const chunk of result.fullStream as any) {
36
+ const c: any = chunk;
37
+ switch (c.type) {
38
+ case 'reasoning':
39
+ if (c.textDelta) {
40
+ yield {
41
+ type: 'reasoning-delta',
42
+ content: c.textDelta,
43
+ };
44
+ }
45
+ break;
46
+
47
+ case 'text-delta':
48
+ yield {
49
+ type: 'text-delta',
50
+ content: c.textDelta,
51
+ };
52
+ break;
53
+
54
+ case 'step-start':
55
+ yield {
56
+ type: 'step-start',
57
+ stepNumber: typeof c.stepIndex === 'number' ? c.stepIndex : stepCounter,
58
+ };
59
+ stepCounter++;
60
+ break;
61
+
62
+ case 'step-finish':
63
+ yield {
64
+ type: 'step-finish',
65
+ stepNumber:
66
+ typeof c.stepIndex === 'number' ? c.stepIndex : Math.max(0, stepCounter - 1),
67
+ finishReason: String(c.finishReason ?? 'stop'),
68
+ };
69
+ break;
70
+
71
+ case 'tool-call':
72
+ yield {
73
+ type: 'tool-call-end',
74
+ toolCallId: String(c.toolCallId ?? ''),
75
+ toolName: String(c.toolName ?? ''),
76
+ args: (c.args ?? {}) as Record<string, unknown>,
77
+ };
78
+ break;
79
+
80
+ case 'tool-result':
81
+ yield {
82
+ type: 'tool-result',
83
+ toolCallId: String(c.toolCallId ?? ''),
84
+ toolName: String(c.toolName ?? ''),
85
+ result: c.result,
86
+ };
87
+ break;
88
+
89
+ case 'finish':
90
+ yield {
91
+ type: 'finish',
92
+ finishReason: String(c.finishReason ?? 'stop'),
93
+ usage: c.usage,
94
+ };
95
+ break;
96
+
97
+ case 'error':
98
+ {
99
+ const err = c.error;
100
+ const msg =
101
+ err instanceof Error
102
+ ? err.message
103
+ : typeof err === 'string'
104
+ ? err
105
+ : 'Unknown error';
106
+ yield {
107
+ type: 'error',
108
+ error: msg,
109
+ };
110
+ }
111
+ break;
112
+ }
113
+ }
114
+ } catch (error) {
115
+ if (options?.abortSignal?.aborted) return;
116
+ yield {
117
+ type: 'error',
118
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
119
+ };
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,20 @@
1
+ import { tool, type CoreTool } from 'ai';
2
+ import { z } from 'zod';
3
+ import { executeTool } from './executor';
4
+
5
+ export const bash: CoreTool = tool({
6
+ description: 'Execute a shell command in the workspace. Add --timeout <ms> at the END of your command for long-running processes (max 90000ms). IMPORTANT: This operation requires user approval - the user will see the command that will be executed and must approve before it runs. If rejected, ask the user for clarification using the question tool.',
7
+ parameters: z.object({
8
+ command: z.string().describe('The shell command to execute. Add --timeout <ms> at the end (max 90000ms) for: dev servers (--timeout 5000), builds (--timeout 90000), tests (--timeout 60000), installs (--timeout 90000), interactive CLIs with menus/options (--timeout 5000). You will receive the command output and must analyze it to determine if it succeeded or failed.'),
9
+ }),
10
+ execute: async (args) => {
11
+ const result = await executeTool('bash', args);
12
+ if (!result.success) {
13
+ const errorMessage = result.error || 'Unknown error occurred';
14
+ return result.userMessage
15
+ ? { error: errorMessage, userMessage: result.userMessage }
16
+ : { error: errorMessage };
17
+ }
18
+ return result.result;
19
+ },
20
+ });
@@ -0,0 +1,27 @@
1
+ import type { CoreTool } from 'ai';
2
+
3
+ import { bash } from './bash.ts';
4
+ import { list } from './list.ts';
5
+ import { read } from './read.ts';
6
+ import { write } from './write.ts';
7
+ import { glob } from './glob.ts';
8
+ import { grep } from './grep.ts';
9
+ import { edit } from './edit.ts';
10
+ import { question } from './question.ts';
11
+ import { explore } from './explore.ts';
12
+
13
+ export const tools: Record<string, CoreTool> = {
14
+ read,
15
+ write,
16
+ list,
17
+ bash,
18
+ glob,
19
+ grep,
20
+ edit,
21
+ question,
22
+ explore,
23
+ };
24
+
25
+ export function getTools(): Record<string, CoreTool> {
26
+ return tools;
27
+ }
@@ -0,0 +1,23 @@
1
+ import { tool, type CoreTool } from 'ai';
2
+ import { z } from 'zod';
3
+ import { executeTool } from './executor';
4
+
5
+ export const edit: CoreTool = tool({
6
+ description: 'Edit a specific part of a file by replacing old content with new content. More precise than rewriting the entire file. IMPORTANT: This operation requires user approval - the user will see a preview showing the old and new content and must approve before changes are made. If rejected, ask the user for clarification using the question tool.',
7
+ parameters: z.object({
8
+ path: z.string().describe('The path to the file relative to the workspace root'),
9
+ old_content: z.string().describe('The exact text content to find and replace'),
10
+ new_content: z.string().describe('The new text content to replace with'),
11
+ occurrence: z.number().nullable().optional().describe('Which occurrence to replace (1 for first, 2 for second, etc. Use null for 1)'),
12
+ }),
13
+ execute: async (args) => {
14
+ const result = await executeTool('edit', args);
15
+ if (!result.success) {
16
+ const errorMessage = result.error || 'Unknown error occurred';
17
+ return result.userMessage
18
+ ? { error: errorMessage, userMessage: result.userMessage }
19
+ : { error: errorMessage };
20
+ }
21
+ return result;
22
+ },
23
+ });