@fenwave/agent 1.1.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 ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@fenwave/agent",
3
+ "version": "1.1.0",
4
+ "description": "Fenwave Docker Agent and CLI",
5
+ "keywords": [
6
+ "fenwave",
7
+ "fenleap",
8
+ "fenwave-idp"
9
+ ],
10
+ "homepage": "https://github.com/Fenleap/Fenwave-plugins#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/Fenleap/Fenwave-plugins/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Fenleap/Fenwave-plugins.git"
17
+ },
18
+ "license": "SEE LICENSE IN LICENSE",
19
+ "author": "Fenleap",
20
+ "type": "module",
21
+ "main": "index.js",
22
+ "bin": {
23
+ "fenwave": "index.js"
24
+ },
25
+ "scripts": {
26
+ "start": "node index.js server",
27
+ "agent": "node index.js"
28
+ },
29
+ "dependencies": {
30
+ "@aws-sdk/client-ecr": "^3.879.0",
31
+ "@aws-sdk/client-sts": "^3.879.0",
32
+ "axios": "^1.11.0",
33
+ "chalk": "^4.1.2",
34
+ "cli-table3": "^0.6.3",
35
+ "commander": "^11.0.0",
36
+ "dockerode": "^4.0.0",
37
+ "dotenv": "^16.0.3",
38
+ "google-auth-library": "^9.0.0",
39
+ "inquirer": "^8.2.5",
40
+ "js-yaml": "^4.1.0",
41
+ "open": "^11.0.0",
42
+ "ora": "^5.4.1",
43
+ "uuid": "^9.0.1",
44
+ "ws": "^8.15.1"
45
+ },
46
+ "engines": {
47
+ "node": ">=14.0.0"
48
+ }
49
+ }
@@ -0,0 +1,499 @@
1
+ import chalk from "chalk";
2
+ import { execSync } from "child_process";
3
+ import axios from "axios";
4
+ import {
5
+ checkPrerequisites,
6
+ displayPrerequisites,
7
+ getMissingPrerequisites,
8
+ } from "../utils/prerequisites.js";
9
+ import {
10
+ promptRegistrationToken,
11
+ promptConfirmation,
12
+ displayProgress,
13
+ displaySuccess,
14
+ displayError,
15
+ displayWarning,
16
+ displayInfo,
17
+ displayHeader,
18
+ displayStep,
19
+ displayKeyValue,
20
+ } from "../utils/prompts.js";
21
+ import {
22
+ handleError,
23
+ createError,
24
+ ErrorCode,
25
+ isUserCancellation,
26
+ } from "../utils/errorHandler.js";
27
+ import { getDeviceMetadata, displayDeviceInfo } from "../utils/deviceInfo.js";
28
+ import {
29
+ saveDeviceCredential,
30
+ isDeviceRegistered,
31
+ } from "../store/deviceCredentialStore.js";
32
+ import { saveNpmToken } from "../store/npmTokenStore.js";
33
+ import {
34
+ saveSetupProgress,
35
+ clearSetupState,
36
+ SetupStep,
37
+ isSetupInProgress,
38
+ } from "../store/setupState.js";
39
+ import { authenticateDockerWithECR } from "../utils/ecrAuth.js";
40
+ import { initializeConfig } from "../store/configStore.js";
41
+
42
+ const TOTAL_STEPS = 7;
43
+
44
+ /**
45
+ * Setup Wizard Orchestrator
46
+ * Guides user through complete agent setup
47
+ */
48
+ export class SetupWizard {
49
+ constructor(options = {}) {
50
+ this.backendUrl = options.backendUrl || "http://localhost:7007";
51
+ this.frontendUrl = options.frontendUrl || "http://localhost:3000";
52
+ this.awsRegion = options.awsRegion || "eu-west-1";
53
+ this.awsAccountId = options.awsAccountId || null; // Will be set from Backstage during registration
54
+ this.skipPrerequisites = options.skipPrerequisites || false;
55
+ this.registrationToken = options.token || null;
56
+ }
57
+
58
+ /**
59
+ * Run the complete setup wizard
60
+ */
61
+ async run() {
62
+ try {
63
+ // Welcome message
64
+ this.displayWelcome();
65
+
66
+ // Initialize and save agent configuration
67
+ const agentConfig = {
68
+ backendUrl: this.backendUrl,
69
+ frontendUrl: this.frontendUrl,
70
+ awsRegion: this.awsRegion,
71
+ awsAccountId: this.awsAccountId,
72
+ };
73
+
74
+ initializeConfig(agentConfig);
75
+
76
+ displayInfo("Agent configuration initialized:");
77
+ displayKeyValue("Backend URL", this.backendUrl);
78
+ displayKeyValue("Frontend URL", this.frontendUrl);
79
+ displayKeyValue("AWS Region", this.awsRegion);
80
+ if (this.awsAccountId) {
81
+ displayKeyValue("AWS Account ID", this.awsAccountId);
82
+ }
83
+ console.log("");
84
+
85
+ // Check if already registered
86
+ if (isDeviceRegistered()) {
87
+ displayWarning("Device is already registered!");
88
+ const shouldContinue = await promptConfirmation(
89
+ "Do you want to re-register (this will replace existing credentials)?",
90
+ false
91
+ );
92
+
93
+ if (!shouldContinue) {
94
+ displayInfo(
95
+ 'Setup cancelled. Use "fenwave status" to view current registration.'
96
+ );
97
+ return;
98
+ }
99
+ }
100
+
101
+ // Check for interrupted setup
102
+ if (isSetupInProgress()) {
103
+ const resume = await promptConfirmation(
104
+ "Found interrupted setup. Do you want to resume?",
105
+ true
106
+ );
107
+
108
+ if (!resume) {
109
+ clearSetupState();
110
+ }
111
+ }
112
+
113
+ // Step 1: Prerequisites Check
114
+ await this.checkPrerequisitesStep();
115
+
116
+ // Step 2: Device Registration
117
+ const registrationData = await this.registrationStep();
118
+
119
+ // Step 3: Docker Registry Configuration
120
+ await this.dockerRegistryStep(registrationData);
121
+
122
+ // Step 4: Pull DevApp Image
123
+ await this.pullImageStep(registrationData);
124
+
125
+ // Step 5: NPM Configuration
126
+ await this.npmConfigStep(registrationData);
127
+
128
+ // Step 6: Start Agent (optional)
129
+ await this.startAgentStep();
130
+
131
+ // Step 7: Complete
132
+ this.displayComplete(registrationData);
133
+
134
+ // Clear setup state
135
+ clearSetupState();
136
+ } catch (error) {
137
+ if (isUserCancellation(error)) {
138
+ displayWarning("\nSetup cancelled by user.");
139
+ displayInfo('Run "fenwave init" again to resume setup.\n');
140
+ } else {
141
+ handleError(error, "Setup Wizard");
142
+ }
143
+ process.exit(1);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Display welcome message
149
+ */
150
+ displayWelcome() {
151
+ displayHeader("Fenwave Dev Agent Setup Wizard");
152
+ console.log(
153
+ chalk.gray(
154
+ " This wizard will guide you through setting up the Fenwave Dev Agent.\n"
155
+ )
156
+ );
157
+ console.log(
158
+ chalk.gray(" You can interrupt the setup anytime and resume later.\n")
159
+ );
160
+ }
161
+
162
+ /**
163
+ * Step 1: Check prerequisites
164
+ */
165
+ async checkPrerequisitesStep() {
166
+ displayStep(1, TOTAL_STEPS, "Checking Prerequisites");
167
+
168
+ if (this.skipPrerequisites) {
169
+ displayInfo("Skipping prerequisites check (--skip-prerequisites flag)");
170
+ return;
171
+ }
172
+
173
+ const checkResults = await checkPrerequisites({
174
+ backendUrl: this.backendUrl,
175
+ });
176
+ const allPassed = displayPrerequisites(checkResults);
177
+
178
+ if (!allPassed) {
179
+ const missing = getMissingPrerequisites(checkResults);
180
+
181
+ console.log(chalk.yellow("\n⚠️ Missing prerequisites:\n"));
182
+ missing.forEach((item) => {
183
+ console.log(chalk.red(` ❌ ${item.name}: ${item.reason}`));
184
+ console.log(chalk.gray(` Action: ${item.action}\n`));
185
+ });
186
+
187
+ const shouldContinue = await promptConfirmation(
188
+ "Some prerequisites are missing. Continue anyway?",
189
+ false
190
+ );
191
+
192
+ if (!shouldContinue) {
193
+ throw createError(
194
+ ErrorCode.OPERATION_CANCELLED,
195
+ "Prerequisites check failed"
196
+ );
197
+ }
198
+ }
199
+
200
+ saveSetupProgress(SetupStep.PREREQUISITES, { allPassed });
201
+ }
202
+
203
+ /**
204
+ * Step 2: Device Registration
205
+ */
206
+ async registrationStep() {
207
+ displayStep(2, TOTAL_STEPS, "Device Registration");
208
+
209
+ // Get registration token
210
+ if (!this.registrationToken) {
211
+ this.registrationToken = await promptRegistrationToken(this.backendUrl);
212
+ }
213
+
214
+ // Collect device information
215
+ const deviceMetadata = await getDeviceMetadata();
216
+ displayInfo("Collected device information:");
217
+ displayDeviceInfo(deviceMetadata);
218
+
219
+ // Register device with backend
220
+ const registrationData = await displayProgress(
221
+ "Registering device with Backstage...",
222
+ this.registerDevice(deviceMetadata)
223
+ );
224
+
225
+ // Save device credentials
226
+ saveDeviceCredential({
227
+ deviceId: registrationData.deviceId,
228
+ deviceCredential: registrationData.deviceCredential,
229
+ userEntityRef: registrationData.userEntityRef,
230
+ deviceName: deviceMetadata.deviceName,
231
+ platform: deviceMetadata.platform,
232
+ agentVersion: deviceMetadata.agentVersion,
233
+ });
234
+
235
+ displaySuccess(`Device registered: ${registrationData.deviceId}`);
236
+ console.log(chalk.gray(` User: ${registrationData.userEntityRef}\n`));
237
+
238
+ saveSetupProgress(SetupStep.REGISTRATION, {
239
+ deviceId: registrationData.deviceId,
240
+ deviceName: deviceMetadata.deviceName,
241
+ });
242
+
243
+ return registrationData;
244
+ }
245
+
246
+ /**
247
+ * Register device with backend
248
+ */
249
+ async registerDevice(deviceMetadata) {
250
+ try {
251
+ const response = await axios.post(
252
+ `${this.backendUrl}/api/agent-cli/register`,
253
+ {
254
+ installToken: this.registrationToken,
255
+ deviceInfo: {
256
+ deviceName: deviceMetadata.deviceName,
257
+ platform: deviceMetadata.platform,
258
+ osVersion: deviceMetadata.osVersion,
259
+ agentVersion: deviceMetadata.agentVersion,
260
+ metadata: deviceMetadata.metadata,
261
+ },
262
+ },
263
+ { timeout: 10000 }
264
+ );
265
+
266
+ return {
267
+ deviceId: response.data.deviceId,
268
+ deviceCredential: response.data.deviceCredential,
269
+ npmToken: response.data.npmToken,
270
+ registryConfig: response.data.registryConfig,
271
+ userEntityRef: response.data.userEntityRef || "unknown",
272
+ };
273
+ } catch (error) {
274
+ if (error.response?.status === 401) {
275
+ throw createError(
276
+ ErrorCode.INVALID_TOKEN,
277
+ "Invalid or expired registration token"
278
+ );
279
+ }
280
+ if (error.response?.status === 429) {
281
+ throw createError(
282
+ ErrorCode.RATE_LIMIT_EXCEEDED,
283
+ "Too many registration attempts"
284
+ );
285
+ }
286
+ throw error;
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Step 3: Docker Registry Configuration
292
+ */
293
+ async dockerRegistryStep(registrationData) {
294
+ displayStep(3, TOTAL_STEPS, "Docker Registry Configuration");
295
+
296
+ const shouldConfigure = await promptConfirmation(
297
+ "Configure AWS ECR for Fenwave images?",
298
+ true
299
+ );
300
+
301
+ if (!shouldConfigure) {
302
+ displayInfo("Skipping Docker registry configuration");
303
+ saveSetupProgress(SetupStep.DOCKER_REGISTRY, { skipped: true });
304
+ return;
305
+ }
306
+
307
+ const { awsRegion, awsAccountId } = registrationData.registryConfig;
308
+
309
+ displayInfo(`Authenticating with AWS ECR (${awsRegion})...`);
310
+ displayInfo(
311
+ "🔐 Using Backstage-controlled credential flow (temporary credentials)"
312
+ );
313
+
314
+ try {
315
+ // Use Backstage-controlled credential flow
316
+ const authResult = await authenticateDockerWithECR(this.backendUrl);
317
+
318
+ displaySuccess("AWS ECR authenticated successfully");
319
+ displayInfo(`✅ Credentials expire at: ${authResult.expiration}`);
320
+ displayInfo(`📦 Registry: ${authResult.registryUri}`);
321
+
322
+ saveSetupProgress(SetupStep.DOCKER_REGISTRY, {
323
+ awsRegion: authResult.region,
324
+ awsAccountId: authResult.accountId,
325
+ registryUri: authResult.registryUri,
326
+ expiration: authResult.expiration,
327
+ });
328
+ } catch (error) {
329
+ displayError("Failed to authenticate with AWS ECR");
330
+ displayError(`Error: ${error.message}`);
331
+ displayWarning(
332
+ "Make sure your device is registered and Backstage is running"
333
+ );
334
+
335
+ const shouldContinue = await promptConfirmation(
336
+ "Continue without ECR authentication?",
337
+ true
338
+ );
339
+ if (!shouldContinue) {
340
+ throw createError(
341
+ ErrorCode.ECR_AUTH_FAILED,
342
+ "ECR authentication failed"
343
+ );
344
+ }
345
+
346
+ saveSetupProgress(SetupStep.DOCKER_REGISTRY, {
347
+ failed: true,
348
+ error: error.message,
349
+ });
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Step 4: Pull Fenwave DevApp Image
355
+ */
356
+ async pullImageStep(registrationData) {
357
+ displayStep(4, TOTAL_STEPS, "Pull Fenwave DevApp Image");
358
+
359
+ const shouldPull = await promptConfirmation(
360
+ "Pull Fenwave DevApp Docker image?",
361
+ true
362
+ );
363
+
364
+ if (!shouldPull) {
365
+ displayInfo("Skipping image pull");
366
+ saveSetupProgress(SetupStep.PULL_IMAGE, { skipped: true });
367
+ return;
368
+ }
369
+
370
+ const { dockerImage } = registrationData.registryConfig;
371
+
372
+ try {
373
+ displayInfo(`Pulling image: ${dockerImage}`);
374
+ displayWarning("This may take a few minutes...");
375
+
376
+ await this.pullDockerImage(dockerImage);
377
+
378
+ displaySuccess("Docker image pulled successfully");
379
+ saveSetupProgress(SetupStep.PULL_IMAGE, { dockerImage });
380
+ } catch (error) {
381
+ displayError("Failed to pull Docker image");
382
+
383
+ const shouldContinue = await promptConfirmation(
384
+ "Continue without pulling image?",
385
+ true
386
+ );
387
+ if (!shouldContinue) {
388
+ throw createError(
389
+ ErrorCode.DOCKER_PULL_FAILED,
390
+ "Docker image pull failed"
391
+ );
392
+ }
393
+
394
+ saveSetupProgress(SetupStep.PULL_IMAGE, { failed: true });
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Pull Docker image
400
+ */
401
+ async pullDockerImage(imageName) {
402
+ return new Promise((resolve, reject) => {
403
+ try {
404
+ execSync(`docker pull ${imageName}`, {
405
+ stdio: "inherit",
406
+ });
407
+ resolve();
408
+ } catch (error) {
409
+ reject(error);
410
+ }
411
+ });
412
+ }
413
+
414
+ /**
415
+ * Step 5: NPM Configuration
416
+ */
417
+ async npmConfigStep(registrationData) {
418
+ displayStep(5, TOTAL_STEPS, "NPM Configuration");
419
+
420
+ const { npmToken } = registrationData;
421
+
422
+ if (npmToken) {
423
+ saveNpmToken(npmToken);
424
+ displaySuccess("NPM token configured for future updates");
425
+ saveSetupProgress(SetupStep.NPM_CONFIG, { configured: true });
426
+ } else {
427
+ displayWarning("No NPM token provided by backend");
428
+ saveSetupProgress(SetupStep.NPM_CONFIG, { configured: false });
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Step 6: Start Agent (optional)
434
+ */
435
+ async startAgentStep() {
436
+ displayStep(6, TOTAL_STEPS, "Start Agent Service");
437
+
438
+ const shouldStart = await promptConfirmation("Start the agent now?", true);
439
+
440
+ if (!shouldStart) {
441
+ displayInfo("You can start the agent later with: fenwave login");
442
+ saveSetupProgress(SetupStep.START_AGENT, { started: false });
443
+ return;
444
+ }
445
+
446
+ displayInfo("Starting agent requires authentication...");
447
+ displayInfo("Please run: fenwave login");
448
+
449
+ saveSetupProgress(SetupStep.START_AGENT, { started: false });
450
+ }
451
+
452
+ /**
453
+ * Display completion message
454
+ */
455
+ displayComplete(registrationData) {
456
+ displayStep(7, TOTAL_STEPS, "Setup Complete! 🎉");
457
+
458
+ console.log("");
459
+ displayHeader("Setup Summary");
460
+
461
+ displayKeyValue({
462
+ "Device ID": registrationData.deviceId,
463
+ "Device Name": registrationData.deviceName || "N/A",
464
+ Platform: registrationData.platform || "N/A",
465
+ User: registrationData.userEntityRef || "N/A",
466
+ });
467
+
468
+ console.log("");
469
+ console.log(chalk.green.bold("✅ Setup completed successfully!\n"));
470
+
471
+ console.log(chalk.bold("Next Steps:\n"));
472
+ console.log(chalk.gray(" 1. Start the agent:"));
473
+ console.log(chalk.cyan(" $ fenwave login\n"));
474
+ console.log(chalk.gray(" 2. Check agent status:"));
475
+ console.log(chalk.cyan(" $ fenwave status\n"));
476
+ console.log(chalk.gray(" 3. View agent information:"));
477
+ console.log(chalk.cyan(" $ fenwave info\n"));
478
+
479
+ console.log(chalk.bold("Documentation:\n"));
480
+ console.log(
481
+ chalk.gray(" • View all commands: ") + chalk.cyan("fenwave --help")
482
+ );
483
+ console.log(
484
+ chalk.gray(" • Setup guide: ") + chalk.cyan("fenwave setup-guide")
485
+ );
486
+ console.log("");
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Run setup wizard
492
+ *
493
+ * @param {Object} options - Wizard options
494
+ * @returns {Promise<void>}
495
+ */
496
+ export async function runSetupWizard(options = {}) {
497
+ const wizard = new SetupWizard(options);
498
+ await wizard.run();
499
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Agent Session Store
3
+ * Stores agent session information in memory
4
+ * This is a separate module to avoid circular dependencies
5
+ */
6
+
7
+ // Store agent session info (will be set when server starts)
8
+ let agentSessionInfo = {
9
+ agentId: null,
10
+ userEntityRef: null,
11
+ sessionExpiresAt: null,
12
+ };
13
+
14
+ /**
15
+ * Set agent session info
16
+ * @param {object} sessionInfo - Session information
17
+ */
18
+ function setAgentSessionInfo(sessionInfo) {
19
+ agentSessionInfo.agentId = sessionInfo.agentId || null;
20
+ agentSessionInfo.userEntityRef = sessionInfo.userEntityRef || null;
21
+ agentSessionInfo.sessionExpiresAt = sessionInfo.sessionExpiresAt || null;
22
+ }
23
+
24
+ /**
25
+ * Get agent session info
26
+ * @returns {object} Agent session info
27
+ */
28
+ function getAgentSessionInfo() {
29
+ return agentSessionInfo;
30
+ }
31
+
32
+ /**
33
+ * Clear agent session info
34
+ */
35
+ function clearAgentSessionInfo() {
36
+ agentSessionInfo.agentId = null;
37
+ agentSessionInfo.userEntityRef = null;
38
+ agentSessionInfo.sessionExpiresAt = null;
39
+ }
40
+
41
+ export default {
42
+ setAgentSessionInfo,
43
+ getAgentSessionInfo,
44
+ clearAgentSessionInfo,
45
+ };
46
+
47
+ export {
48
+ setAgentSessionInfo,
49
+ getAgentSessionInfo,
50
+ clearAgentSessionInfo,
51
+ };
@@ -0,0 +1,113 @@
1
+ import os from 'os';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { loadConfig } from './configStore.js';
5
+
6
+ const fsPromises = fs.promises;
7
+
8
+ // Load configuration
9
+ const config = loadConfig();
10
+ const AGENT_ROOT_DIR = config.agentRootDir;
11
+ const AGENT_INFO_DIR = 'agent';
12
+ const AGENT_INFO_FILE = 'info.json';
13
+ const AGENT_INFO_PATH = path.join(
14
+ os.homedir(),
15
+ AGENT_ROOT_DIR,
16
+ AGENT_INFO_DIR,
17
+ AGENT_INFO_FILE
18
+ );
19
+
20
+ /**
21
+ * Agent Store for managing persistent agent information
22
+ */
23
+ class AgentStore {
24
+ constructor() {
25
+ this.agentInfo = null;
26
+ this.initialized = false;
27
+ }
28
+
29
+ /**
30
+ * Initialize the store
31
+ */
32
+ async initialize() {
33
+ if (this.initialized) return;
34
+
35
+ try {
36
+ // Ensure directory exists
37
+ await fsPromises.mkdir(path.dirname(AGENT_INFO_PATH), { recursive: true });
38
+ this.initialized = true;
39
+ } catch (error) {
40
+ console.error('Failed to initialize agent store:', error);
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Save agent start time
47
+ */
48
+ async saveAgentStartTime(startTime) {
49
+ await this.initialize();
50
+
51
+ const agentInfo = {
52
+ startTime: startTime.toISOString(),
53
+ lastUpdated: new Date().toISOString(),
54
+ };
55
+
56
+ try {
57
+ await fsPromises.writeFile(AGENT_INFO_PATH, JSON.stringify(agentInfo, null, 2));
58
+ this.agentInfo = agentInfo;
59
+ } catch (error) {
60
+ console.error('Failed to save agent start time:', error);
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Load agent start time
67
+ */
68
+ async loadAgentStartTime() {
69
+ await this.initialize();
70
+
71
+ try {
72
+ const data = await fsPromises.readFile(AGENT_INFO_PATH, 'utf8');
73
+ const agentInfo = JSON.parse(data);
74
+ this.agentInfo = agentInfo;
75
+ return new Date(agentInfo.startTime);
76
+ } catch (error) {
77
+ if (error.code === 'ENOENT') {
78
+ // File doesn't exist - agent not running or first time
79
+ return null;
80
+ }
81
+ console.error('Failed to load agent start time:', error);
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Clear agent info (when agent stops)
88
+ */
89
+ async clearAgentInfo() {
90
+ await this.initialize();
91
+
92
+ try {
93
+ await fsPromises.unlink(AGENT_INFO_PATH);
94
+ this.agentInfo = null;
95
+ } catch (error) {
96
+ if (error.code !== 'ENOENT') {
97
+ console.error('Failed to clear agent info:', error);
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Check if agent is currently running based on stored info
104
+ */
105
+ async isAgentRunning() {
106
+ const startTime = await this.loadAgentStartTime();
107
+ return startTime !== null;
108
+ }
109
+ }
110
+
111
+ // Export singleton instance
112
+ const agentStore = new AgentStore();
113
+ export default agentStore;