aryx-cli 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.
@@ -0,0 +1,85 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import ThemeSelector from '../components/ThemeSelector.js';
4
+ import { theme, markSetupDone } from '../theme.js';
5
+ import { BRAND_NAME, VERSION } from '../constants.js';
6
+
7
+ const SECURITY_NOTES = [
8
+ { title: `${BRAND_NAME} can make mistakes`, desc: `You should always review ${BRAND_NAME}'s responses, especially when running code.` },
9
+ { title: `${BRAND_NAME} executes commands carefully, but responsibility is yours`, desc: 'You should always review and verify code before running it, especially if it modifies files or installs dependencies.' },
10
+ ];
11
+
12
+ const BANNER = [
13
+ ' █████╗ ██████╗ ██╗ ██╗██╗ ██╗',
14
+ ' ██╔══██╗██╔══██╗╚██╗ ██╔╝╚██╗██╔╝',
15
+ ' ███████║██████╔╝ ╚████╔╝ ╚███╔╝ ',
16
+ ' ██║ ██║██║ ██║ ██║ ██╔╝ ██╗',
17
+ ' ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝',
18
+ ];
19
+
20
+ const BannerHeader: React.FC<{ showTagline?: boolean }> = ({ showTagline }) => (
21
+ <Box flexDirection="column" paddingX={1} paddingTop={1} marginBottom={1}>
22
+ <Text>
23
+ <Text color={theme.colors.primary}>Welcome to {BRAND_NAME} </Text>
24
+ <Text color="gray">v{VERSION}</Text>
25
+ </Text>
26
+ <Box flexDirection="column" marginTop={1}>
27
+ {BANNER.map((line, i) => (
28
+ <Text key={i} color={theme.colors.primary}>{line}</Text>
29
+ ))}
30
+ </Box>
31
+ {showTagline && <Box paddingTop={1}><Text color="gray">Let's get started.</Text></Box>}
32
+ </Box>
33
+ );
34
+
35
+ const SetupScreen: React.FC<{ onComplete: () => void }> = ({ onComplete }) => {
36
+ const [phase, setPhase] = useState<'theme' | 'security'>('theme');
37
+
38
+ useInput((_, key) => {
39
+ if (phase === 'security' && key.return) {
40
+ markSetupDone();
41
+ onComplete();
42
+ }
43
+ });
44
+
45
+ return (
46
+ <Box
47
+ key={phase}
48
+ flexDirection="column">
49
+ <BannerHeader showTagline={phase === 'theme'} />
50
+
51
+ {phase === 'theme' ? (
52
+ <ThemeSelector
53
+ onClose={() => setPhase('security')}
54
+ isSetup />
55
+ ) : (
56
+ <Box
57
+ flexDirection="column"
58
+ paddingX={2}
59
+ marginTop={1}>
60
+ <Text bold>Security notes:</Text>
61
+
62
+ <Box flexDirection="column" marginTop={1}>
63
+ {SECURITY_NOTES.map((note, i) => (
64
+ <Box key={i} marginTop={i > 0 ? 1 : 0}>
65
+ <Text>{i + 1}. </Text>
66
+
67
+ <Box flexDirection="column">
68
+ <Text bold>{note.title}</Text>
69
+ <Text color="gray">{note.desc}</Text>
70
+ </Box>
71
+ </Box>
72
+ ))}
73
+ </Box>
74
+
75
+ <Box marginTop={1}>
76
+ <Text color={theme.colors.primary}>Press <Text bold>Enter</Text> to continue...</Text>
77
+ </Box>
78
+ </Box>
79
+ )}
80
+ </Box>
81
+ );
82
+
83
+ };
84
+
85
+ export default SetupScreen;
@@ -0,0 +1,187 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { FILE_TOOLS, executeTool, formatDiff } from './fileTools.js';
4
+ import { BRAND_NAME_LOWER, OPENROUTER_API_KEY, OPENROUTER_API_KEY_BACKUP, OPENROUTER_MODEL } from '../constants.js';
5
+
6
+ const FILE_WRITE_TOOLS = new Set(['create_file', 'write_file', 'edit_file', 'append_file']);
7
+
8
+ export interface ChatCompletionRequest {
9
+ messages: { role: 'user' | 'assistant'; content: string }[];
10
+ }
11
+
12
+ export interface UsageData {
13
+ inputTokens: number;
14
+ outputTokens: number;
15
+ totalTokens: number;
16
+ cost: number;
17
+ model: string;
18
+ responseTimeMs: number;
19
+ }
20
+
21
+ export interface ChatCompletionResponse {
22
+ content: string;
23
+ usage?: UsageData;
24
+ }
25
+
26
+ const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
27
+
28
+ const getConfig = () => {
29
+ return {
30
+ apiKey: OPENROUTER_API_KEY,
31
+ backupKey: OPENROUTER_API_KEY_BACKUP || undefined,
32
+ model: OPENROUTER_MODEL,
33
+ };
34
+ };
35
+
36
+ type OpenRouterMessage = {
37
+ content?: string | Array<{ text?: string }>;
38
+ tool_calls?: Array<{ id: string; function: { name: string; arguments: string } }>;
39
+ };
40
+
41
+ type OpenRouterResponse = {
42
+ error?: { message?: string };
43
+ choices?: Array<{ message?: OpenRouterMessage }>;
44
+ usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number; cost?: number };
45
+ model?: string;
46
+ };
47
+
48
+ const extractContent = (data: OpenRouterResponse): string => {
49
+ const content = data.choices?.[0]?.message?.content;
50
+
51
+ if (typeof content === 'string') return content.trim();
52
+
53
+ if (Array.isArray(content)) {
54
+ return content.map((item) => item.text || '').join('').trim();
55
+ }
56
+
57
+ return '';
58
+ };
59
+
60
+ const fetchOpenRouter = async (messages: object[], apiKey: string, model: string, backupKey?: string) => {
61
+ const t0 = Date.now();
62
+
63
+ const tryFetch = async (key: string) => {
64
+ const res = await fetch(OPENROUTER_URL, {
65
+ method: 'POST',
66
+ headers: {
67
+ authorization: `Bearer ${key}`,
68
+ 'content-type': 'application/json'
69
+ },
70
+ body: JSON.stringify({ model, messages, tools: FILE_TOOLS }),
71
+ });
72
+
73
+ const data = await res.json() as OpenRouterResponse;
74
+ return { res, data };
75
+ };
76
+
77
+ let { res, data } = await tryFetch(apiKey);
78
+
79
+ if (!res.ok && backupKey) {
80
+ ({ res, data } = await tryFetch(backupKey));
81
+ }
82
+
83
+ if (!res.ok) {
84
+ throw new Error(data.error?.message || `OpenRouter failed (${res.status})`);
85
+ }
86
+
87
+ return {
88
+ message: data.choices?.[0]?.message,
89
+ usage: data.usage,
90
+ model: data.model,
91
+ responseTimeMs: Date.now() - t0
92
+ };
93
+ };
94
+
95
+ export const chatService = {
96
+ fetchResponse: async (request: ChatCompletionRequest): Promise<ChatCompletionResponse> => {
97
+ const { apiKey, backupKey, model } = getConfig();
98
+
99
+ const messages: object[] = [
100
+ { role: 'system', content: `You are a helpful AI with file system access. CWD: ${process.cwd()}
101
+
102
+ ## About the Creator
103
+ ${BRAND_NAME_LOWER} CLI was created by Vasu Bhalodiya.
104
+ - Full Name: Vasu Bhalodiya
105
+ - Age: 20
106
+ - Location: Rajkot, Gujarat, India
107
+ - Role: Frontend Developer
108
+ - Company: Prolix Technikos
109
+ - Tech Stack: React.js, Next.js, JavaScript, TypeScript, TailwindCSS
110
+ - GitHub: https://github.com/vasubhalodiya
111
+ - LinkedIn: https://www.linkedin.com/in/vasubhalodiya
112
+ - Portfolio: https://www.vasubhalodiya.in
113
+
114
+ If anyone asks about "Vasu", "Vasu Bhalodiya", or the creator/developer of ${BRAND_NAME_LOWER}, provide the above information.` },
115
+ ...request.messages,
116
+ ];
117
+
118
+ // filePath -> content BEFORE any changes this session
119
+ const originals = new Map<string, string>();
120
+ let lastUsage: UsageData | undefined;
121
+
122
+ for (let i = 0; i < 10; i++) {
123
+ const {
124
+ message: msg,
125
+ usage,
126
+ model: respModel,
127
+ responseTimeMs
128
+ } = await fetchOpenRouter(messages, apiKey, model, backupKey);
129
+
130
+ if (!msg) throw new Error('Empty response');
131
+ messages.push(msg);
132
+
133
+ if (usage) {
134
+ lastUsage = {
135
+ inputTokens: usage.prompt_tokens ?? 0,
136
+ outputTokens: usage.completion_tokens ?? 0,
137
+ totalTokens: usage.total_tokens ?? 0,
138
+ cost: usage.cost ?? 0,
139
+ model: respModel ?? model,
140
+ responseTimeMs,
141
+ };
142
+ }
143
+
144
+ if (!msg.tool_calls?.length) {
145
+ // Build one diff per changed file
146
+ const diffs: string[] = [];
147
+
148
+ for (const [fp, oldContent] of originals) {
149
+ const newContent = existsSync(fp) ? readFileSync(fp, 'utf8') : '';
150
+
151
+ if (newContent !== oldContent) {
152
+ diffs.push(formatDiff(fp, oldContent, newContent));
153
+ }
154
+ }
155
+
156
+ const aiText = extractContent({ choices: [{ message: msg }] }) || 'Done.';
157
+ const prefix = diffs.join('\n\n');
158
+
159
+ return {
160
+ content: prefix ? prefix + '\n\n' + aiText : aiText,
161
+ usage: lastUsage
162
+ };
163
+ }
164
+
165
+ for (const tc of msg.tool_calls) {
166
+ const args = JSON.parse(tc.function.arguments || '{}');
167
+
168
+ // Snapshot original content before first write to this file
169
+ if (FILE_WRITE_TOOLS.has(tc.function.name) && args.file_path) {
170
+ const fp = resolve(process.cwd(), args.file_path);
171
+ if (!originals.has(fp)) {
172
+ originals.set(fp, existsSync(fp) ? readFileSync(fp, 'utf8') : '');
173
+ }
174
+ }
175
+
176
+ const output = executeTool(tc.function.name, args);
177
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: output });
178
+ }
179
+ }
180
+
181
+ return {
182
+ content: 'Max iterations reached.',
183
+ usage: lastUsage
184
+ };
185
+ },
186
+ };
187
+
@@ -0,0 +1,100 @@
1
+ import { existsSync, unlinkSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { readConfig, writeConfig } from './config.js';
5
+ import { FIREBASE_API_KEY } from '../constants.js';
6
+
7
+ const CFG = join(homedir(), '.aryx', 'config.json');
8
+
9
+ export type AuthData = {
10
+ uid: string;
11
+ email: string;
12
+ displayName: string;
13
+ photoURL: string;
14
+ idToken: string;
15
+ refreshToken: string;
16
+ idTokenExpiry: number;
17
+ };
18
+
19
+ export const readAuth = (): AuthData | null => {
20
+ const cfg = readConfig();
21
+ const auth = cfg.auth as AuthData | undefined;
22
+
23
+ if (!auth?.uid) return null;
24
+
25
+ return auth;
26
+ };
27
+
28
+ export const saveAuth = (data: AuthData): void => {
29
+ writeConfig({
30
+ ...readConfig(),
31
+ auth: data
32
+ });
33
+ };
34
+
35
+ export const clearAuth = (): void => {
36
+ const cfg = readConfig();
37
+ delete cfg.auth;
38
+
39
+ writeConfig(cfg);
40
+ };
41
+
42
+ export const deleteConfig = (): void => {
43
+ try {
44
+ if (existsSync(CFG)) {
45
+ unlinkSync(CFG);
46
+ }
47
+ } catch {
48
+ // Ignore errors
49
+ }
50
+ };
51
+
52
+ export const isLoggedIn = (): boolean => {
53
+ return readAuth() !== null;
54
+ };
55
+
56
+ export const getValidToken = async (): Promise<string | null> => {
57
+ const auth = readAuth();
58
+ if (!auth) return null;
59
+
60
+ if (Date.now() < auth.idTokenExpiry) {
61
+ return auth.idToken;
62
+ }
63
+
64
+ // idToken expired — refresh via Firebase REST API
65
+ try {
66
+ const apiKey = FIREBASE_API_KEY;
67
+
68
+ const res = await fetch(
69
+ `https://securetoken.googleapis.com/v1/token?key=${apiKey}`,
70
+ {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Content-Type': 'application/x-www-form-urlencoded'
74
+ },
75
+ body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(auth.refreshToken)}`,
76
+ }
77
+ );
78
+
79
+ if (!res.ok) {
80
+ clearAuth();
81
+ return null;
82
+ }
83
+
84
+ const json = await res.json() as { id_token: string; refresh_token: string };
85
+
86
+ const updated: AuthData = {
87
+ ...auth,
88
+ idToken: json.id_token,
89
+ refreshToken: json.refresh_token,
90
+ idTokenExpiry: Date.now() + 3_600_000,
91
+ };
92
+
93
+ saveAuth(updated);
94
+
95
+ return updated.idToken;
96
+ } catch {
97
+ return null;
98
+ }
99
+ };
100
+
@@ -0,0 +1,30 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+
5
+ const CONFIG_DIR = join(homedir(), '.aryx');
6
+ const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
7
+
8
+ /** Read the shared config file (~/.aryx/config.json). */
9
+ export const readConfig = (): Record<string, unknown> => {
10
+ try {
11
+ if (existsSync(CONFIG_PATH)) {
12
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
13
+ }
14
+ } catch {
15
+ /* corrupted / unreadable — treat as empty */
16
+ }
17
+
18
+ return {};
19
+ };
20
+
21
+ /** Write the shared config file (~/.aryx/config.json). */
22
+ export const writeConfig = (data: Record<string, unknown>): void => {
23
+ try {
24
+ mkdirSync(CONFIG_DIR, { recursive: true });
25
+ writeFileSync(CONFIG_PATH, JSON.stringify(data));
26
+ } catch {
27
+ /* permission error — silently ignore */
28
+ }
29
+ };
30
+
@@ -0,0 +1,257 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { diffLines } from 'diff';
5
+ import { theme, DIFF_COLORS } from '../theme.js';
6
+
7
+ type Args = Record<string, string>;
8
+ type Tool = { description: string; params: string[]; optional?: string[]; handler: (a: Args) => string };
9
+
10
+ // ─── Diff Engine ─────────────────────────────────────────────────────────────
11
+
12
+ const formatDiff = (fp: string, oldSrc: string, newSrc: string): string => {
13
+ const rel = path.relative(process.cwd(), fp).replace(/\\/g, '/');
14
+ const { add: addClr, rm: rmClr } = DIFF_COLORS[theme.diffTheme];
15
+ const useBg = theme.diffTheme <= 2;
16
+ const isNew = !oldSrc;
17
+
18
+ type Op = { t: 's' | 'a' | 'r'; l: string };
19
+
20
+ const ops: Op[] = diffLines(oldSrc || '', newSrc).flatMap(ch => {
21
+ const t = ch.added ? 'a' : ch.removed ? 'r' : 's';
22
+ const lines = ch.value.split('\n');
23
+
24
+ if (lines.at(-1) === '') lines.pop();
25
+
26
+ return lines.map(l => ({ t, l }) as Op);
27
+ });
28
+
29
+ const added = ops.filter(o => o.t === 'a').length;
30
+ const rem = ops.filter(o => o.t === 'r').length;
31
+
32
+ if (!added && !rem) return `No changes: ${rel}`;
33
+
34
+ // visible = changed ± 2 context lines
35
+ const vis = new Set<number>();
36
+ ops.forEach((o, i) => {
37
+ if (o.t !== 's') {
38
+ for (let c = Math.max(0, i - 2); c <= Math.min(ops.length - 1, i + 2); c++) {
39
+ vis.add(c);
40
+ }
41
+ }
42
+ });
43
+
44
+ const hdr = chalk.bold(`${isNew ? 'Create' : 'Update'}(${rel})`);
45
+ const sum = ` └─ Added ${added} lines${isNew ? '' : `, removed ${rem} lines`}`;
46
+ const body: string[] = [];
47
+
48
+ let ol = 0, nl = 0, hidden = false;
49
+
50
+ ops.forEach((o, i) => {
51
+ if (o.t === 's') {
52
+ ol++;
53
+ nl++;
54
+ } else if (o.t === 'r') {
55
+ ol++;
56
+ } else {
57
+ nl++;
58
+ }
59
+
60
+ if (!vis.has(i)) {
61
+ hidden = true;
62
+ return;
63
+ }
64
+
65
+ if (hidden) {
66
+ body.push(chalk.gray(' ...'));
67
+ hidden = false;
68
+ }
69
+
70
+ const ln = String(o.t === 'a' ? nl : ol).padStart(4);
71
+
72
+ if (o.t === 's') {
73
+ body.push(chalk.gray(` ${ln} ${o.l}`));
74
+ } else if (o.t === 'r') {
75
+ body.push(useBg ? chalk.bgHex(rmClr).black(`- ${ln} ${o.l}`) : chalk.hex(rmClr)(`- ${ln} ${o.l}`));
76
+ } else {
77
+ body.push(useBg ? chalk.bgHex(addClr).black(`+ ${ln} ${o.l}`) : chalk.hex(addClr)(`+ ${ln} ${o.l}`));
78
+ }
79
+ });
80
+
81
+ return [hdr, sum, '', ...body].join('\n');
82
+ };
83
+
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+
86
+ const r = (p: string) => path.resolve(process.cwd(), p || '.');
87
+ const mk = (fp: string) => fs.mkdirSync(path.dirname(fp), { recursive: true });
88
+
89
+ // ─── Add / remove tools here only ────────────────────────────────────────────
90
+
91
+ const TOOLS: Record<string, Tool> = {
92
+
93
+ create_file: {
94
+ description: 'Create a new file with content (fails if already exists)',
95
+ params: ['file_path', 'content'],
96
+ handler: (a) => {
97
+ const fp = r(a.file_path);
98
+ mk(fp);
99
+
100
+ if (fs.existsSync(fp)) return `Already exists: ${fp}`;
101
+
102
+ fs.writeFileSync(fp, a.content, 'utf8');
103
+ return `Created: ${fp}`;
104
+ },
105
+ },
106
+
107
+ read_file: {
108
+ description: 'Read and return the full contents of a file',
109
+ params: ['file_path'],
110
+ handler: (a) => {
111
+ return fs.readFileSync(r(a.file_path), 'utf8');
112
+ },
113
+ },
114
+
115
+ write_file: {
116
+ description: 'Overwrite a file with new content (creates if missing)',
117
+ params: ['file_path', 'content'],
118
+ handler: (a) => {
119
+ const fp = r(a.file_path);
120
+ mk(fp);
121
+ fs.writeFileSync(fp, a.content, 'utf8');
122
+
123
+ return `Written: ${fp}`;
124
+ },
125
+ },
126
+
127
+ append_file: {
128
+ description: 'Append content to the end of a file',
129
+ params: ['file_path', 'content'],
130
+ handler: (a) => {
131
+ const fp = r(a.file_path);
132
+ mk(fp);
133
+ fs.appendFileSync(fp, a.content, 'utf8');
134
+
135
+ return `Appended to: ${fp}`;
136
+ },
137
+ },
138
+
139
+ edit_file: {
140
+ description: 'Find old_text in a file and replace it with new_text',
141
+ params: ['file_path', 'old_text', 'new_text'],
142
+ handler: (a) => {
143
+ const fp = r(a.file_path);
144
+ const src = fs.readFileSync(fp, 'utf8');
145
+
146
+ if (!src.includes(a.old_text)) return `Text not found in: ${fp}`;
147
+
148
+ fs.writeFileSync(fp, src.replace(a.old_text, a.new_text), 'utf8');
149
+ return `Edited: ${fp}`;
150
+ },
151
+ },
152
+
153
+ delete_file: {
154
+ description: 'Delete a single file',
155
+ params: ['file_path'],
156
+ handler: (a) => {
157
+ fs.unlinkSync(r(a.file_path));
158
+ return `Deleted: ${a.file_path}`;
159
+ },
160
+ },
161
+
162
+ copy_file: {
163
+ description: 'Copy a file from src path to dest path',
164
+ params: ['src', 'dest'],
165
+ handler: (a) => {
166
+ const dest = r(a.dest);
167
+ mk(dest);
168
+ fs.copyFileSync(r(a.src), dest);
169
+
170
+ return `Copied → ${dest}`;
171
+ },
172
+ },
173
+
174
+ move_file: {
175
+ description: 'Move or rename a file from src to dest',
176
+ params: ['src', 'dest'],
177
+ handler: (a) => {
178
+ const dest = r(a.dest);
179
+ mk(dest);
180
+ fs.renameSync(r(a.src), dest);
181
+
182
+ return `Moved → ${dest}`;
183
+ },
184
+ },
185
+
186
+ list_files: {
187
+ description: 'List files and folders in a directory',
188
+ params: [],
189
+ optional: ['dir_path'],
190
+ handler: (a) => {
191
+ const entries = fs.readdirSync(r(a.dir_path), { withFileTypes: true });
192
+
193
+ return entries
194
+ .map(e => `${e.isDirectory() ? '[dir] ' : '[file]'} ${e.name}`)
195
+ .join('\n') || '(empty)';
196
+ },
197
+ },
198
+
199
+ make_dir: {
200
+ description: 'Create a directory (and any missing parent directories)',
201
+ params: ['dir_path'],
202
+ handler: (a) => {
203
+ fs.mkdirSync(r(a.dir_path), { recursive: true });
204
+ return `Directory created: ${a.dir_path}`;
205
+ },
206
+ },
207
+
208
+ delete_dir: {
209
+ description: 'Recursively delete a directory and all its contents',
210
+ params: ['dir_path'],
211
+ handler: (a) => {
212
+ fs.rmSync(r(a.dir_path), { recursive: true, force: true });
213
+ return `Directory deleted: ${a.dir_path}`;
214
+ },
215
+ },
216
+
217
+ file_exists: {
218
+ description: 'Check whether a file or directory exists',
219
+ params: ['file_path'],
220
+ handler: (a) => {
221
+ return fs.existsSync(r(a.file_path))
222
+ ? `Exists: ${a.file_path}`
223
+ : `Not found: ${a.file_path}`;
224
+ },
225
+ },
226
+
227
+ };
228
+
229
+ // ─────────────────────────────────────────────────────────────────────────────
230
+
231
+ // Auto-build OpenRouter tool definitions from TOOLS map
232
+ export const FILE_TOOLS = Object.entries(TOOLS).map(([name, { description, params, optional = [] }]) => ({
233
+ type: 'function',
234
+ function: {
235
+ name,
236
+ description,
237
+ parameters: {
238
+ type: 'object',
239
+ properties: Object.fromEntries(
240
+ [...params, ...optional].map(k => [k, { type: 'string' }])
241
+ ),
242
+ required: params,
243
+ },
244
+ },
245
+ }));
246
+
247
+ export { formatDiff };
248
+
249
+ // Auto-dispatch to handler
250
+ export const executeTool = (name: string, args: Args): string => {
251
+ try {
252
+ return TOOLS[name]?.handler(args) ?? `Unknown tool: ${name}`;
253
+ } catch (e: any) {
254
+ return `Error: ${e.message}`;
255
+ }
256
+ };
257
+