@arcteninc/core 0.0.139 → 0.0.140

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,69 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Wrapper for cli-sync.ts
4
+ * Handles TypeScript execution via tsx
5
+ */
6
+
7
+ const { spawn, execSync } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ const scriptPath = path.join(__dirname, 'cli-sync.ts');
12
+
13
+ // Check if script exists
14
+ if (!fs.existsSync(scriptPath)) {
15
+ console.error('❌ cli-sync.ts not found');
16
+ process.exit(1);
17
+ }
18
+
19
+ // Try tsx first (most reliable for TypeScript)
20
+ function tryTsx() {
21
+ try {
22
+ execSync('npx tsx --version', { stdio: 'ignore' });
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ // Try bun
30
+ function tryBun() {
31
+ try {
32
+ execSync('bun --version', { stdio: 'ignore' });
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ let runner, args;
40
+
41
+ if (tryTsx()) {
42
+ runner = 'npx';
43
+ args = ['tsx', scriptPath, ...process.argv.slice(2)];
44
+ } else if (tryBun()) {
45
+ runner = 'bun';
46
+ args = [scriptPath, ...process.argv.slice(2)];
47
+ } else {
48
+ console.error('❌ No TypeScript runner found.');
49
+ console.error('');
50
+ console.error('Install one of:');
51
+ console.error(' npm install -g tsx');
52
+ console.error(' OR install Bun: https://bun.sh');
53
+ process.exit(1);
54
+ }
55
+
56
+ const child = spawn(runner, args, {
57
+ stdio: 'inherit',
58
+ cwd: process.cwd(),
59
+ shell: true
60
+ });
61
+
62
+ child.on('error', (error) => {
63
+ console.error('❌ Failed to run sync:', error.message);
64
+ process.exit(1);
65
+ });
66
+
67
+ child.on('exit', (code) => {
68
+ process.exit(code || 0);
69
+ });
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Arcten Sync - Sync tools from codebase to dashboard
4
+ *
5
+ * Runs:
6
+ * 1. arcten-extract-types (generates .arcten/tool-metadata.ts)
7
+ * 2. Generates .arcten/arcten.tools.ts with imports and arrays
8
+ * 3. Updates .arcten/sync-state.json for diff tracking
9
+ * 4. Syncs to dashboard (functions are READONLY)
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as readline from 'readline';
15
+ import { execSync } from 'child_process';
16
+
17
+ interface ToolMetadata {
18
+ generated: string;
19
+ discoveredFrom: string[];
20
+ functions: Record<string, {
21
+ name: string;
22
+ description?: string;
23
+ parameters: unknown;
24
+ returnType: string;
25
+ isAsync: boolean;
26
+ }>;
27
+ toolOrder: string[];
28
+ }
29
+
30
+ interface SyncState {
31
+ lastSync: string;
32
+ toolsFile: string;
33
+ functions: Record<string, {
34
+ classification: 'safe' | 'sensitive';
35
+ }>;
36
+ }
37
+
38
+ interface ClassificationResult {
39
+ source: string;
40
+ classifications: Record<string, 'safe' | 'sensitive'>;
41
+ safe: string[];
42
+ sensitive: string[];
43
+ }
44
+
45
+ // State
46
+ let rl: readline.Interface;
47
+ let apiKey: string;
48
+ let projectId: string;
49
+ let serverUrl: string;
50
+
51
+ /**
52
+ * Read tool-metadata.ts to get current functions
53
+ */
54
+ function readToolMetadata(projectRoot: string): ToolMetadata | null {
55
+ const metadataPath = path.join(projectRoot, '.arcten', 'tool-metadata.ts');
56
+
57
+ if (!fs.existsSync(metadataPath)) {
58
+ return null;
59
+ }
60
+
61
+ const content = fs.readFileSync(metadataPath, 'utf-8');
62
+
63
+ // Extract the object from "export const toolMetadata = { ... } as const;"
64
+ const match = content.match(/export const toolMetadata = ({[\s\S]*}) as const;/);
65
+ if (!match) {
66
+ return null;
67
+ }
68
+
69
+ try {
70
+ // Use eval to parse the object (it's generated code, safe to eval)
71
+ const metadata = eval(`(${match[1]})`);
72
+ return metadata as ToolMetadata;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Read sync-state.json to get previous state
80
+ */
81
+ function readSyncState(projectRoot: string): SyncState | null {
82
+ const statePath = path.join(projectRoot, '.arcten', 'sync-state.json');
83
+
84
+ if (!fs.existsSync(statePath)) {
85
+ return null;
86
+ }
87
+
88
+ try {
89
+ const content = fs.readFileSync(statePath, 'utf-8');
90
+ const parsed = JSON.parse(content) as SyncState;
91
+ // Validate the structure
92
+ if (!parsed.functions || typeof parsed.functions !== 'object') {
93
+ return null;
94
+ }
95
+ return parsed;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Write sync-state.json
103
+ */
104
+ function writeSyncState(projectRoot: string, state: SyncState): void {
105
+ const arctenDir = path.join(projectRoot, '.arcten');
106
+ if (!fs.existsSync(arctenDir)) {
107
+ fs.mkdirSync(arctenDir, { recursive: true });
108
+ }
109
+
110
+ const statePath = path.join(arctenDir, 'sync-state.json');
111
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
112
+ }
113
+
114
+ /**
115
+ * Generate arcten.tools.ts with imports and arrays
116
+ */
117
+ function generateToolsFile(
118
+ projectRoot: string,
119
+ toolsFilePath: string,
120
+ safeTools: string[],
121
+ sensitiveTools: string[]
122
+ ): void {
123
+ const arctenDir = path.join(projectRoot, '.arcten');
124
+ if (!fs.existsSync(arctenDir)) {
125
+ fs.mkdirSync(arctenDir, { recursive: true });
126
+ }
127
+
128
+ // Calculate relative import path from .arcten to tools file
129
+ const toolsFileRelative = path.relative(arctenDir, path.join(projectRoot, toolsFilePath));
130
+ const importPath = toolsFileRelative.replace(/\.ts$/, '').replace(/\\/g, '/');
131
+
132
+ const allTools = [...safeTools, ...sensitiveTools];
133
+
134
+ const content = `// .arcten/arcten.tools.ts - AUTO-GENERATED by arcten sync
135
+ // DO NOT EDIT - run \`arcten sync\` to regenerate
136
+
137
+ import {
138
+ ${allTools.map(t => ` ${t},`).join('\n')}
139
+ } from '${importPath}';
140
+
141
+ // Safe tools - auto-execute without user approval
142
+ export const safeTools = [
143
+ ${safeTools.map(t => ` ${t},`).join('\n')}
144
+ ] as const;
145
+
146
+ // Sensitive tools - require user approval before execution
147
+ export const sensitiveTools = [
148
+ ${sensitiveTools.map(t => ` ${t},`).join('\n')}
149
+ ] as const;
150
+
151
+ // All tools combined
152
+ export const allTools = [...safeTools, ...sensitiveTools];
153
+
154
+ // Tool names for the safeToolNames prop
155
+ export const safeToolNames = safeTools.map(fn => fn.name);
156
+
157
+ // Type exports
158
+ export type SafeToolName = typeof safeTools[number]['name'];
159
+ export type SensitiveToolName = typeof sensitiveTools[number]['name'];
160
+ export type AllToolName = SafeToolName | SensitiveToolName;
161
+ `;
162
+
163
+ const outputPath = path.join(arctenDir, 'arcten.tools.ts');
164
+ fs.writeFileSync(outputPath, content);
165
+ }
166
+
167
+ /**
168
+ * Classify tools using AI or pattern fallback
169
+ */
170
+ async function classifyTools(
171
+ tools: Array<{ name: string; description?: string }>,
172
+ existingClassifications: Record<string, { classification: 'safe' | 'sensitive' }>
173
+ ): Promise<ClassificationResult> {
174
+ // Filter to only new tools (not already classified)
175
+ const newTools = tools.filter(t => !existingClassifications[t.name]);
176
+
177
+ if (newTools.length === 0) {
178
+ // All tools already classified
179
+ const classifications: Record<string, 'safe' | 'sensitive'> = {};
180
+ for (const tool of tools) {
181
+ classifications[tool.name] = existingClassifications[tool.name]?.classification || 'sensitive';
182
+ }
183
+ return {
184
+ source: 'existing',
185
+ classifications,
186
+ safe: tools.filter(t => classifications[t.name] === 'safe').map(t => t.name),
187
+ sensitive: tools.filter(t => classifications[t.name] === 'sensitive').map(t => t.name),
188
+ };
189
+ }
190
+
191
+ // Try AI classification for new tools
192
+ try {
193
+ const response = await fetch(`${serverUrl}/ai-init/classify`, {
194
+ method: 'POST',
195
+ headers: {
196
+ 'Content-Type': 'application/json',
197
+ 'x-arcten-api-key': apiKey,
198
+ },
199
+ body: JSON.stringify({
200
+ tools: newTools.map(t => ({ name: t.name, jsDoc: t.description })),
201
+ }),
202
+ });
203
+
204
+ if (response.ok) {
205
+ const aiResult = await response.json() as ClassificationResult;
206
+
207
+ // Merge with existing classifications
208
+ const mergedClassifications: Record<string, 'safe' | 'sensitive'> = {};
209
+ for (const tool of tools) {
210
+ if (existingClassifications[tool.name]) {
211
+ mergedClassifications[tool.name] = existingClassifications[tool.name].classification;
212
+ } else {
213
+ mergedClassifications[tool.name] = aiResult.classifications[tool.name] || 'sensitive';
214
+ }
215
+ }
216
+
217
+ return {
218
+ source: 'ai',
219
+ classifications: mergedClassifications,
220
+ safe: tools.filter(t => mergedClassifications[t.name] === 'safe').map(t => t.name),
221
+ sensitive: tools.filter(t => mergedClassifications[t.name] === 'sensitive').map(t => t.name),
222
+ };
223
+ }
224
+ } catch {
225
+ // Fall through to pattern fallback
226
+ }
227
+
228
+ // Pattern-based fallback
229
+ const safePatterns = /^(get|list|search|find|fetch|load|calculate|count)/i;
230
+ const sensitivePatterns = /^(create|update|delete|set|add|remove|regenerate)/i;
231
+ const sensitiveReads = /^(get|fetch).*(api.?key|credential|secret|password|token)/i;
232
+
233
+ const classifications: Record<string, 'safe' | 'sensitive'> = {};
234
+
235
+ for (const tool of tools) {
236
+ if (existingClassifications[tool.name]) {
237
+ classifications[tool.name] = existingClassifications[tool.name].classification;
238
+ } else if (sensitiveReads.test(tool.name)) {
239
+ classifications[tool.name] = 'sensitive';
240
+ } else if (sensitivePatterns.test(tool.name)) {
241
+ classifications[tool.name] = 'sensitive';
242
+ } else if (safePatterns.test(tool.name)) {
243
+ classifications[tool.name] = 'safe';
244
+ } else {
245
+ classifications[tool.name] = 'sensitive'; // Default to sensitive
246
+ }
247
+ }
248
+
249
+ return {
250
+ source: 'pattern-fallback',
251
+ classifications,
252
+ safe: tools.filter(t => classifications[t.name] === 'safe').map(t => t.name),
253
+ sensitive: tools.filter(t => classifications[t.name] === 'sensitive').map(t => t.name),
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Sync to dashboard
259
+ */
260
+ async function syncToDashboard(
261
+ tools: Array<{ name: string; description?: string; requiresApproval: boolean }>
262
+ ): Promise<{ success: boolean; error?: string }> {
263
+ try {
264
+ // Get JWT token
265
+ const tokenResponse = await fetch(`${serverUrl}/token`, {
266
+ method: 'POST',
267
+ headers: { 'Content-Type': 'application/json' },
268
+ body: JSON.stringify({ apiKey }),
269
+ });
270
+
271
+ if (!tokenResponse.ok) {
272
+ return { success: false, error: 'Failed to authenticate' };
273
+ }
274
+
275
+ const { clientToken: token } = await tokenResponse.json();
276
+
277
+ // Format tools for sync
278
+ const toolsData = tools.map(tool => ({
279
+ name: tool.name,
280
+ description: tool.description || `Function: ${tool.name}`,
281
+ signature: JSON.stringify({}),
282
+ isEnabled: true,
283
+ isOverridable: true,
284
+ requiresApproval: tool.requiresApproval,
285
+ sensitiveParams: [],
286
+ readonly: true, // Functions are readonly in dashboard
287
+ syncedAt: Date.now(),
288
+ }));
289
+
290
+ // Sync to dashboard
291
+ const syncResponse = await fetch(`${serverUrl}/tools/sync`, {
292
+ method: 'POST',
293
+ headers: {
294
+ 'Content-Type': 'application/json',
295
+ 'Authorization': `Bearer ${token}`,
296
+ },
297
+ body: JSON.stringify({
298
+ projectId,
299
+ tools: toolsData,
300
+ }),
301
+ });
302
+
303
+ if (!syncResponse.ok) {
304
+ const error = await syncResponse.text();
305
+ return { success: false, error: `Sync failed: ${error}` };
306
+ }
307
+
308
+ return { success: true };
309
+ } catch (error: any) {
310
+ return { success: false, error: error.message };
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Check for API key
316
+ */
317
+ function checkApiKey(): { apiKey: string; projectId: string } | null {
318
+ const envFiles = ['.env.local', '.env'];
319
+
320
+ for (const envFile of envFiles) {
321
+ const envPath = path.join(process.cwd(), envFile);
322
+ if (fs.existsSync(envPath)) {
323
+ const envContent = fs.readFileSync(envPath, 'utf-8');
324
+ const match = envContent.match(/ARCTEN_API_KEY=([^\s\n]+)/);
325
+ if (match) {
326
+ const key = match[1];
327
+ const projectIdMatch = key.match(/sk_(proj_[a-zA-Z0-9]+)_/);
328
+ if (projectIdMatch) {
329
+ return { apiKey: key, projectId: projectIdMatch[1] };
330
+ }
331
+ }
332
+ }
333
+ }
334
+
335
+ return null;
336
+ }
337
+
338
+ /**
339
+ * Ask user a yes/no question
340
+ */
341
+ function askYesNo(question: string): Promise<boolean> {
342
+ return new Promise((resolve) => {
343
+ rl.question(`${question} (y/n) `, (answer) => {
344
+ resolve(answer.trim().toLowerCase().startsWith('y'));
345
+ });
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Main sync function
351
+ */
352
+ async function main() {
353
+ const projectRoot = process.cwd();
354
+ const args = process.argv.slice(2);
355
+ const autoYes = args.includes('--yes') || args.includes('-y');
356
+ const quiet = autoYes; // Quiet mode when auto-yes
357
+
358
+ if (!quiet) {
359
+ console.log('');
360
+ console.log('Arcten Sync');
361
+ console.log('');
362
+ }
363
+
364
+ // Check for API key
365
+ const auth = checkApiKey();
366
+ if (!auth) {
367
+ if (!quiet) {
368
+ console.log('No API key found. Run `arcten init` first.');
369
+ }
370
+ process.exit(1);
371
+ }
372
+
373
+ apiKey = auth.apiKey;
374
+ projectId = auth.projectId;
375
+ serverUrl = process.env.ARCTEN_SERVER_URL || 'https://api.arcten.com';
376
+
377
+ // Create readline interface (only if interactive)
378
+ if (!autoYes) {
379
+ rl = readline.createInterface({
380
+ input: process.stdin,
381
+ output: process.stdout,
382
+ });
383
+ }
384
+
385
+ try {
386
+ // Step 1: Run arcten-extract-types to update tool-metadata.ts
387
+ if (!quiet) console.log('Extracting tool types...');
388
+ // Get script directory (ESM compatible)
389
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname).replace(/^\/([A-Z]:)/, '$1');
390
+
391
+ try {
392
+ // Try running the wrapper script directly first
393
+ const extractScript = path.join(scriptDir, 'cli-extract-types-auto-wrapper.js');
394
+ if (fs.existsSync(extractScript)) {
395
+ execSync(`node "${extractScript}"`, {
396
+ cwd: projectRoot,
397
+ stdio: quiet ? 'pipe' : ['inherit', 'pipe', 'pipe'],
398
+ });
399
+ if (!quiet) console.log(' Done');
400
+ } else {
401
+ // Fall back to npx
402
+ execSync('npx arcten-extract-types', {
403
+ cwd: projectRoot,
404
+ stdio: quiet ? 'pipe' : ['inherit', 'pipe', 'pipe'],
405
+ });
406
+ if (!quiet) console.log(' Done');
407
+ }
408
+ } catch (error: any) {
409
+ if (!quiet) console.log(' Warning: Could not run extract-types, continuing with existing metadata...');
410
+ }
411
+
412
+ // Step 2: Read tool-metadata.ts
413
+ if (!quiet) console.log('Reading tool metadata...');
414
+ const metadata = readToolMetadata(projectRoot);
415
+
416
+ if (!metadata) {
417
+ if (!quiet) {
418
+ console.log('');
419
+ console.log('No tool-metadata.ts found.');
420
+ console.log('Make sure you have a tools file (e.g., app/tools.ts) with exported functions.');
421
+ console.log('');
422
+ }
423
+ if (rl) rl.close();
424
+ process.exit(1);
425
+ }
426
+
427
+ const currentFunctions = Object.keys(metadata.functions);
428
+ const toolsFile = metadata.discoveredFrom[0] || 'app/tools.ts';
429
+
430
+ if (!quiet) console.log(`Found ${currentFunctions.length} functions from ${toolsFile}`);
431
+
432
+ // Step 2: Read previous sync state
433
+ const previousState = readSyncState(projectRoot);
434
+ const previousFunctions = previousState
435
+ ? Object.keys(previousState.functions)
436
+ : [];
437
+
438
+ // Step 3: Compute diff
439
+ const newFunctions = currentFunctions.filter(f => !previousFunctions.includes(f));
440
+ const removedFunctions = previousFunctions.filter(f => !currentFunctions.includes(f));
441
+
442
+ // If no changes and in auto mode, exit silently
443
+ if (newFunctions.length === 0 && removedFunctions.length === 0 && previousState) {
444
+ if (autoYes) {
445
+ // Silent exit - no changes needed
446
+ if (rl) rl.close();
447
+ process.exit(0);
448
+ }
449
+
450
+ console.log('');
451
+ console.log('No changes detected since last sync.');
452
+
453
+ const resync = await askYesNo('Re-sync anyway?');
454
+ if (!resync) {
455
+ console.log('Sync cancelled.');
456
+ if (rl) rl.close();
457
+ process.exit(0);
458
+ }
459
+ }
460
+
461
+ // Show changes if any
462
+ if (newFunctions.length > 0) {
463
+ console.log('');
464
+ console.log(`New functions since last sync:`);
465
+ for (const fn of newFunctions) {
466
+ console.log(` + ${fn}`);
467
+ }
468
+ }
469
+
470
+ if (removedFunctions.length > 0) {
471
+ console.log('');
472
+ console.log(`Removed functions:`);
473
+ for (const fn of removedFunctions) {
474
+ console.log(` - ${fn}`);
475
+ }
476
+ }
477
+
478
+ // Step 4: Classify tools (AI for new, preserve existing)
479
+ if (!quiet) {
480
+ console.log('');
481
+ console.log('Classifying tools...');
482
+ }
483
+
484
+ const existingClassifications = previousState?.functions || {};
485
+ const toolsToClassify = currentFunctions.map(name => ({
486
+ name,
487
+ description: metadata.functions[name]?.description,
488
+ }));
489
+
490
+ const classification = await classifyTools(toolsToClassify, existingClassifications);
491
+
492
+ if (!quiet) {
493
+ console.log('');
494
+ console.log(`Classification (${classification.source}):`);
495
+ console.log(` Safe (auto-execute): ${classification.safe.length} tools`);
496
+ if (classification.safe.length > 0 && classification.safe.length <= 10) {
497
+ console.log(` ${classification.safe.join(', ')}`);
498
+ }
499
+ console.log(` Sensitive (needs approval): ${classification.sensitive.length} tools`);
500
+ if (classification.sensitive.length > 0 && classification.sensitive.length <= 10) {
501
+ console.log(` ${classification.sensitive.join(', ')}`);
502
+ }
503
+ }
504
+
505
+ // Step 5: Confirm with user (skip if auto-yes)
506
+ if (!autoYes) {
507
+ console.log('');
508
+ const proceed = await askYesNo('Does this look right?');
509
+
510
+ if (!proceed) {
511
+ console.log('');
512
+ console.log('You can adjust classifications in .arcten/sync-state.json and re-run sync.');
513
+ if (rl) rl.close();
514
+ process.exit(0);
515
+ }
516
+ }
517
+
518
+ // Step 6: Generate arcten.tools.ts
519
+ if (!quiet) {
520
+ console.log('');
521
+ console.log('Generating .arcten/arcten.tools.ts...');
522
+ }
523
+ generateToolsFile(projectRoot, toolsFile.replace(/\\/g, '/'), classification.safe, classification.sensitive);
524
+ if (!quiet) console.log(' Created .arcten/arcten.tools.ts');
525
+
526
+ // Step 7: Update sync-state.json
527
+ const newState: SyncState = {
528
+ lastSync: new Date().toISOString(),
529
+ toolsFile,
530
+ functions: {},
531
+ };
532
+ for (const name of currentFunctions) {
533
+ newState.functions[name] = {
534
+ classification: classification.classifications[name],
535
+ };
536
+ }
537
+ writeSyncState(projectRoot, newState);
538
+ if (!quiet) console.log(' Updated .arcten/sync-state.json');
539
+
540
+ // Step 8: Sync to dashboard (skip logging in quiet mode unless there's an error)
541
+ if (!quiet) {
542
+ console.log('');
543
+ console.log('Syncing to dashboard...');
544
+ }
545
+
546
+ const toolsForDashboard = currentFunctions.map(name => ({
547
+ name,
548
+ description: metadata.functions[name]?.description,
549
+ requiresApproval: classification.classifications[name] === 'sensitive',
550
+ }));
551
+
552
+ const syncResult = await syncToDashboard(toolsForDashboard);
553
+
554
+ if (syncResult.success) {
555
+ if (!quiet) console.log(` Synced ${toolsForDashboard.length} tools to dashboard`);
556
+ } else {
557
+ // Always show dashboard errors
558
+ console.log(` Warning: Dashboard sync failed: ${syncResult.error}`);
559
+ if (!quiet) console.log(' Local files were still updated.');
560
+ }
561
+
562
+ if (!quiet) {
563
+ console.log('');
564
+ console.log('Sync complete!');
565
+ console.log('');
566
+ console.log('Usage in your components:');
567
+ console.log('');
568
+ console.log(' import { allTools, safeToolNames } from "./.arcten/arcten.tools";');
569
+ console.log('');
570
+ console.log(' <ArctenAgent');
571
+ console.log(' tools={allTools}');
572
+ console.log(' safeToolNames={safeToolNames}');
573
+ console.log(' />');
574
+ console.log('');
575
+ } else {
576
+ // Brief message when there were changes in quiet mode
577
+ if (newFunctions.length > 0 || removedFunctions.length > 0) {
578
+ console.log(`✓ Synced ${newFunctions.length} new, ${removedFunctions.length} removed tools`);
579
+ }
580
+ }
581
+
582
+ } catch (error: any) {
583
+ console.error('Error:', error.message);
584
+ process.exit(1);
585
+ } finally {
586
+ if (rl) rl.close();
587
+ }
588
+ }
589
+
590
+ // Run
591
+ main().catch((error) => {
592
+ console.error('Fatal error:', error);
593
+ process.exit(1);
594
+ });
595
+
596
+ export { main };