morpheus-cli 0.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.
Files changed (41) hide show
  1. package/README.md +129 -0
  2. package/bin/morpheus.js +19 -0
  3. package/dist/channels/__tests__/telegram.test.js +63 -0
  4. package/dist/channels/telegram.js +89 -0
  5. package/dist/cli/commands/config.js +81 -0
  6. package/dist/cli/commands/doctor.js +69 -0
  7. package/dist/cli/commands/init.js +108 -0
  8. package/dist/cli/commands/start.js +145 -0
  9. package/dist/cli/commands/status.js +21 -0
  10. package/dist/cli/commands/stop.js +28 -0
  11. package/dist/cli/index.js +38 -0
  12. package/dist/cli/utils/render.js +11 -0
  13. package/dist/config/__tests__/manager.test.js +11 -0
  14. package/dist/config/manager.js +55 -0
  15. package/dist/config/paths.js +15 -0
  16. package/dist/config/schemas.js +35 -0
  17. package/dist/config/utils.js +21 -0
  18. package/dist/http/__tests__/config_api.test.js +79 -0
  19. package/dist/http/api.js +134 -0
  20. package/dist/http/server.js +52 -0
  21. package/dist/runtime/__tests__/agent.test.js +91 -0
  22. package/dist/runtime/__tests__/agent_persistence.test.js +148 -0
  23. package/dist/runtime/__tests__/display.test.js +135 -0
  24. package/dist/runtime/__tests__/manual_start_verify.js +42 -0
  25. package/dist/runtime/__tests__/manual_us1.js +33 -0
  26. package/dist/runtime/agent.js +84 -0
  27. package/dist/runtime/display.js +146 -0
  28. package/dist/runtime/errors.js +16 -0
  29. package/dist/runtime/lifecycle.js +41 -0
  30. package/dist/runtime/memory/__tests__/sqlite.test.js +179 -0
  31. package/dist/runtime/memory/sqlite.js +192 -0
  32. package/dist/runtime/providers/factory.js +62 -0
  33. package/dist/runtime/scaffold.js +31 -0
  34. package/dist/runtime/types.js +1 -0
  35. package/dist/types/config.js +24 -0
  36. package/dist/types/display.js +1 -0
  37. package/dist/ui/assets/index-nNle8n-Z.css +1 -0
  38. package/dist/ui/assets/index-ySbKLOXZ.js +50 -0
  39. package/dist/ui/index.html +14 -0
  40. package/dist/ui/vite.svg +31 -0
  41. package/package.json +62 -0
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ <div align="center">
2
+ <img src="./assets/logo.png" alt="Morpheus Logo" width="220" />
3
+ </div>
4
+
5
+ # Morpheus
6
+
7
+ > **Morpheus is a local-first AI operator that bridges developers and machines.**
8
+
9
+ Morpheus is a local AI agent for developers, running as a CLI daemon that connects to **LLMs**, **local tools**, and **MCPs**, enabling interaction via **Terminal, Telegram, and Discord**. Inspired by the character Morpheus from *The Matrix*, the project acts as an **intelligent orchestrator**, bridging the gap between the developer and complex systems.
10
+
11
+ ## Technical Overview
12
+
13
+ Morpheus is built with **Node.js** and **TypeScript**, using **LangChain** as the orchestration engine. It runs as a background daemon process, managing connections to LLM providers (OpenAI, Anthropic, Ollama) and external channels (Telegram, Discord).
14
+
15
+ ### Core Components
16
+
17
+ - **Runtime (`src/runtime/`)**: The heart of the application. Manages the agent lifecycle, provider instantiation, and command execution.
18
+ - **CLI (`src/cli/`)**: Built with `commander`, handles user interaction, configuration, and daemon control (`start`, `stop`, `status`).
19
+ - **Configuration (`src/config/`)**: Singleton-based configuration manager using `zod` for validation and `js-yaml` for persistence (`~/.morpheus/config.yaml`).
20
+ - **Channels (`src/channels/`)**: Adapters for external communication. Currently supports Telegram (`telegraf`) with strict user whitelisting.
21
+
22
+ ## Prerequisites
23
+
24
+ - **Node.js**: >= 18.x
25
+ - **npm**: >= 9.x
26
+ - **TypeScript**: >= 5.x
27
+
28
+ ## Getting Started (Development)
29
+
30
+ This guide is for developers contributing to the Morpheus codebase.
31
+
32
+ ### 1. Clone & Install
33
+
34
+ ```bash
35
+ git clone https://github.com/your-org/morpheus.git
36
+ cd morpheus
37
+ npm install
38
+ ```
39
+
40
+ ### 2. Build
41
+
42
+ Compile TypeScript source to `dist/`.
43
+
44
+ ```bash
45
+ npm run build
46
+ ```
47
+
48
+ ### 3. Run the CLI
49
+
50
+ You can run the CLI directly from the source using `npm start`.
51
+
52
+ ```bash
53
+ # Initialize configuration (creates ~/.morpheus)
54
+ npm start -- init
55
+
56
+ # Start the daemon
57
+ npm start -- start
58
+
59
+ # Check status
60
+ npm start -- status
61
+ ```
62
+
63
+ ### 4. Configuration
64
+
65
+ The configuration file is located at `~/.morpheus/config.yaml`. You can edit it manually or use the CLI.
66
+
67
+ ```yaml
68
+ agent:
69
+ name: "Morpheus"
70
+ personality: "stoic, wise, and helpful"
71
+ llm:
72
+ provider: "openai" # options: openai, anthropic, ollama
73
+ model: "gpt-4-turbo"
74
+ temperature: 0.7
75
+ api_key: "sk-..."
76
+ channels:
77
+ telegram:
78
+ enabled: true
79
+ token: "YOUR_TELEGRAM_BOT_TOKEN"
80
+ allowedUsers: ["123456789"] # Your Telegram User ID
81
+ ```
82
+
83
+ ## Testing
84
+
85
+ We use **Vitest** for testing.
86
+
87
+ ```bash
88
+ # Run unit tests
89
+ npm test
90
+
91
+ # Run tests in watch mode
92
+ npm run test:watch
93
+ ```
94
+
95
+ ## Project Structure
96
+
97
+ ```text
98
+ .
99
+ ├── assets/ # Static assets
100
+ ├── bin/ # CLI entry point (morpheus.js)
101
+ ├── specs/ # Technical specifications & documentation
102
+ ├── src/
103
+ │ ├── channels/ # Communication adapters (Telegram, etc.)
104
+ │ ├── cli/ # CLI commands and logic
105
+ │ ├── config/ # Configuration management
106
+ │ ├── runtime/ # Core agent logic, lifecycle, and providers
107
+ │ ├── types/ # Shared TypeScript definitions
108
+ │ └── index.ts
109
+ └── package.json
110
+ ```
111
+
112
+ ## Roadmap
113
+
114
+ - [ ] **MCP Support**: Full integration with Model Context Protocol.
115
+ - [ ] **Discord Adapter**: Support for Discord interactions.
116
+ - [ ] **Web Dashboard**: Local UI for management and logs.
117
+ - [ ] **Plugin System**: Extend functionality via external modules.
118
+
119
+ ## Contributing
120
+
121
+ 1. Fork the repository.
122
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`).
123
+ 3. Commit your changes (`git commit -m 'feat: Add amazing feature'`).
124
+ 4. Push to the branch (`git push origin feature/amazing-feature`).
125
+ 5. Open a Pull Request.
126
+
127
+ ## License
128
+
129
+ MIT
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Suppress experimental warnings for JSON modules
4
+ const originalEmit = process.emit;
5
+ process.emit = function (name, data, ...args) {
6
+ if (
7
+ name === 'warning' &&
8
+ typeof data === 'object' &&
9
+ data.name === 'ExperimentalWarning' &&
10
+ data.message.includes('Importing JSON modules')
11
+ ) {
12
+ return false;
13
+ }
14
+ return originalEmit.apply(process, [name, data, ...args]);
15
+ };
16
+
17
+ // Use dynamic import to ensure the warning suppression is active before the module graph loads
18
+ const { cli } = await import('../dist/cli/index.js');
19
+ cli();
@@ -0,0 +1,63 @@
1
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { TelegramAdapter } from '../telegram.js';
3
+ // Mock dependencies
4
+ vi.mock('telegraf', () => {
5
+ return {
6
+ Telegraf: class {
7
+ telegram;
8
+ on;
9
+ launch;
10
+ stop;
11
+ constructor() {
12
+ this.telegram = {
13
+ getMe: vi.fn().mockResolvedValue({ username: 'test_bot' }),
14
+ };
15
+ this.on = vi.fn();
16
+ this.launch = vi.fn().mockResolvedValue(undefined);
17
+ this.stop = vi.fn();
18
+ }
19
+ }
20
+ };
21
+ });
22
+ vi.mock('../../runtime/display.js', () => ({
23
+ DisplayManager: {
24
+ getInstance: () => ({
25
+ log: vi.fn(),
26
+ }),
27
+ },
28
+ }));
29
+ describe('TelegramAdapter', () => {
30
+ let adapter;
31
+ let mockAgent;
32
+ beforeEach(() => {
33
+ mockAgent = {
34
+ chat: vi.fn(),
35
+ };
36
+ adapter = new TelegramAdapter(mockAgent);
37
+ });
38
+ afterEach(() => {
39
+ vi.clearAllMocks();
40
+ });
41
+ describe('Authorization', () => {
42
+ it('should be able to authorize trusted users', () => {
43
+ // Accessing private method for unit testing
44
+ const isAuthorized = adapter.isAuthorized.bind(adapter);
45
+ const allowed = ['123', '456'];
46
+ expect(isAuthorized('123', allowed)).toBe(true);
47
+ expect(isAuthorized('456', allowed)).toBe(true);
48
+ expect(isAuthorized('789', allowed)).toBe(false);
49
+ });
50
+ it('should handle numeric inputs converted to strings', () => {
51
+ const isAuthorized = adapter.isAuthorized.bind(adapter);
52
+ const allowed = ['123'];
53
+ // Telegram ID comes as string from our logic, but let's verify string comparison
54
+ expect(isAuthorized('123', allowed)).toBe(true);
55
+ expect(isAuthorized('1234', allowed)).toBe(false);
56
+ });
57
+ });
58
+ describe('Connection', () => {
59
+ it('should connect successfully with token', async () => {
60
+ await expect(adapter.connect('fake_token', ['123'])).resolves.not.toThrow();
61
+ });
62
+ });
63
+ });
@@ -0,0 +1,89 @@
1
+ import { Telegraf } from 'telegraf';
2
+ import chalk from 'chalk';
3
+ import { DisplayManager } from '../runtime/display.js';
4
+ export class TelegramAdapter {
5
+ bot = null;
6
+ isConnected = false;
7
+ display = DisplayManager.getInstance();
8
+ agent;
9
+ constructor(agent) {
10
+ this.agent = agent;
11
+ }
12
+ async connect(token, allowedUsers) {
13
+ if (this.isConnected) {
14
+ this.display.log('Telegram adapter already connected.', { source: 'Telegram', level: 'warning' });
15
+ return;
16
+ }
17
+ try {
18
+ this.display.log('Connecting to Telegram...', { source: 'Telegram' });
19
+ this.bot = new Telegraf(token);
20
+ // Verify token/connection
21
+ const me = await this.bot.telegram.getMe();
22
+ this.display.log(`✓ Telegram Connected: @${me.username}`, { source: 'Telegram', level: 'success' });
23
+ this.display.log(`Allowed Users: ${allowedUsers.join(', ')}`, { source: 'Telegram', level: 'info' });
24
+ // Listen for messages
25
+ this.bot.on('text', async (ctx) => {
26
+ const user = ctx.from.username || ctx.from.first_name;
27
+ const userId = ctx.from.id.toString();
28
+ const text = ctx.message.text;
29
+ // AUTH GUARD
30
+ if (!this.isAuthorized(userId, allowedUsers)) {
31
+ this.display.log(`Unauthorized access attempt by @${user} (ID: ${userId})`, { source: 'Telegram', level: 'warning' });
32
+ return; // Silent fail for security
33
+ }
34
+ this.display.log(`@${user}: ${text}`, { source: 'Telegram' });
35
+ try {
36
+ // Send "typing" status
37
+ await ctx.sendChatAction('typing');
38
+ // Process with Agent
39
+ const response = await this.agent.chat(text);
40
+ if (response) {
41
+ await ctx.reply(response);
42
+ this.display.log(`Responded to @${user}`, { source: 'Telegram' });
43
+ }
44
+ }
45
+ catch (error) {
46
+ this.display.log(`Error processing message for @${user}: ${error.message}`, { source: 'Telegram', level: 'error' });
47
+ try {
48
+ await ctx.reply("Sorry, I encountered an error while processing your request.");
49
+ }
50
+ catch (e) {
51
+ // Ignore reply error
52
+ }
53
+ }
54
+ });
55
+ this.bot.launch().catch((err) => {
56
+ if (this.isConnected) {
57
+ this.display.log(`Telegram bot error: ${err}`, { source: 'Telegram', level: 'error' });
58
+ }
59
+ });
60
+ this.isConnected = true;
61
+ process.once('SIGINT', () => this.disconnect());
62
+ process.once('SIGTERM', () => this.disconnect());
63
+ }
64
+ catch (error) {
65
+ this.display.log(`Failed to connect to Telegram: ${error.message}`, { source: 'Telegram', level: 'error' });
66
+ this.isConnected = false;
67
+ this.bot = null;
68
+ throw error;
69
+ }
70
+ }
71
+ isAuthorized(userId, allowedUsers) {
72
+ return allowedUsers.includes(userId);
73
+ }
74
+ async disconnect() {
75
+ if (!this.isConnected || !this.bot) {
76
+ return;
77
+ }
78
+ this.display.log('Disconnecting Telegram...', { source: 'Telegram', level: 'warning' });
79
+ try {
80
+ this.bot.stop();
81
+ }
82
+ catch (e) {
83
+ // Ignore stop errors
84
+ }
85
+ this.isConnected = false;
86
+ this.bot = null;
87
+ this.display.log(chalk.gray('Telegram disconnected.'), { source: 'Telegram' });
88
+ }
89
+ }
@@ -0,0 +1,81 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import open from 'open';
4
+ import fs from 'fs-extra';
5
+ import { PATHS } from '../../config/paths.js';
6
+ import { scaffold } from '../../runtime/scaffold.js';
7
+ import { ConfigManager } from '../../config/manager.js';
8
+ function parseValue(value) {
9
+ if (value.toLowerCase() === 'true')
10
+ return true;
11
+ if (value.toLowerCase() === 'false')
12
+ return false;
13
+ if (!Number.isNaN(Number(value)) && value.trim() !== '') {
14
+ return Number(value);
15
+ }
16
+ return value;
17
+ }
18
+ export const configCommand = new Command('config')
19
+ .description('View or edit configuration')
20
+ .option('-e, --edit', 'Open config file in default editor')
21
+ .action(async (options) => {
22
+ try {
23
+ await scaffold(); // Ensure config exits
24
+ if (options.edit) {
25
+ console.log(chalk.cyan(`Opening config file: ${PATHS.config}`));
26
+ await open(PATHS.config);
27
+ }
28
+ else {
29
+ console.log(chalk.bold('Configuration File:'), chalk.cyan(PATHS.config));
30
+ console.log(chalk.gray('---'));
31
+ if (await fs.pathExists(PATHS.config)) {
32
+ const content = await fs.readFile(PATHS.config, 'utf8');
33
+ console.log(content);
34
+ }
35
+ else {
36
+ console.log(chalk.yellow('Config file not found (scaffold should have created it).'));
37
+ }
38
+ console.log(chalk.gray('---'));
39
+ }
40
+ }
41
+ catch (error) {
42
+ console.error(chalk.red('Failed to handle config command:'), error.message);
43
+ process.exit(1);
44
+ }
45
+ });
46
+ configCommand.command('set')
47
+ .description('Set a configuration value')
48
+ .argument('<key>', 'Configuration key (e.g. channels.telegram.enabled)')
49
+ .argument('<value>', 'Value to set')
50
+ .action(async (key, value) => {
51
+ try {
52
+ await scaffold(); // Ensure config loads
53
+ const manager = ConfigManager.getInstance();
54
+ await manager.load();
55
+ const parsedValue = parseValue(value);
56
+ try {
57
+ await manager.set(key, parsedValue);
58
+ console.log(chalk.green(`✓ Set ${key} = ${parsedValue}`));
59
+ }
60
+ catch (e) {
61
+ // Fallback: If Zod fails, maybe it was a string that looked like a number?
62
+ if (typeof parsedValue === 'number') {
63
+ try {
64
+ await manager.set(key, value); // Try original string
65
+ console.log(chalk.green(`✓ Set ${key} = "${value}" (treated as string)`));
66
+ return;
67
+ }
68
+ catch (ignored) { }
69
+ }
70
+ throw e;
71
+ }
72
+ }
73
+ catch (error) {
74
+ console.error(chalk.red('Failed to set config:'), error.message || error);
75
+ if (error.issues) {
76
+ // Zod error
77
+ console.error(chalk.red('Validation issues:'), JSON.stringify(error.issues, null, 2));
78
+ }
79
+ process.exit(1);
80
+ }
81
+ });
@@ -0,0 +1,69 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ import { PATHS } from '../../config/paths.js';
6
+ import { ConfigManager } from '../../config/manager.js';
7
+ export const doctorCommand = new Command('doctor')
8
+ .description('Diagnose environment and configuration issues')
9
+ .action(async () => {
10
+ console.log(chalk.bold('Morpheus Doctor'));
11
+ console.log(chalk.gray('================'));
12
+ let allPassed = true;
13
+ // 1. Check Node.js Version
14
+ const nodeVersion = process.version;
15
+ const majorVersion = parseInt(nodeVersion.replace('v', '').split('.')[0], 10);
16
+ if (majorVersion >= 18) {
17
+ console.log(chalk.green('✓') + ` Node.js Version: ${nodeVersion} (Satisfied)`);
18
+ }
19
+ else {
20
+ console.log(chalk.red('✗') + ` Node.js Version: ${nodeVersion} (Required: >=18)`);
21
+ allPassed = false;
22
+ }
23
+ // 2. Check Configuration
24
+ try {
25
+ if (await fs.pathExists(PATHS.config)) {
26
+ await ConfigManager.getInstance().load();
27
+ console.log(chalk.green('✓') + ' Configuration: Valid');
28
+ }
29
+ else {
30
+ console.log(chalk.yellow('!') + ' Configuration: Missing (will be created on start)');
31
+ }
32
+ }
33
+ catch (error) {
34
+ console.log(chalk.red('✗') + ` Configuration: Invalid (${error.message})`);
35
+ allPassed = false;
36
+ }
37
+ // 3. Check Permissions
38
+ try {
39
+ await fs.ensureDir(PATHS.root);
40
+ const testFile = path.join(PATHS.root, '.perm-test');
41
+ await fs.writeFile(testFile, 'test');
42
+ await fs.remove(testFile);
43
+ console.log(chalk.green('✓') + ` Permissions: Write access to ${PATHS.root}`);
44
+ }
45
+ catch (error) {
46
+ console.log(chalk.red('✗') + ` Permissions: Cannot write to ${PATHS.root}`);
47
+ allPassed = false;
48
+ }
49
+ // 4. Check Logs Permissions
50
+ try {
51
+ await fs.ensureDir(PATHS.logs);
52
+ const testLogFile = path.join(PATHS.logs, '.perm-test');
53
+ await fs.writeFile(testLogFile, 'test');
54
+ await fs.remove(testLogFile);
55
+ console.log(chalk.green('✓') + ` Logs: Write access to ${PATHS.logs}`);
56
+ }
57
+ catch (error) {
58
+ console.log(chalk.red('✗') + ` Logs: Cannot write to ${PATHS.logs}`);
59
+ allPassed = false;
60
+ }
61
+ console.log(chalk.gray('================'));
62
+ if (allPassed) {
63
+ console.log(chalk.green('Diagnostics Passed. You are ready to run Morpheus!'));
64
+ }
65
+ else {
66
+ console.log(chalk.red('Issues detected. Please fix them before running Morpheus.'));
67
+ process.exit(1);
68
+ }
69
+ });
@@ -0,0 +1,108 @@
1
+ import { Command } from 'commander';
2
+ import { input, select, password, confirm, checkbox } from '@inquirer/prompts';
3
+ import chalk from 'chalk';
4
+ import { ConfigManager } from '../../config/manager.js';
5
+ import { renderBanner } from '../utils/render.js';
6
+ import { DisplayManager } from '../../runtime/display.js';
7
+ import { scaffold } from '../../runtime/scaffold.js';
8
+ export const initCommand = new Command('init')
9
+ .description('Initialize Morpheus configuration')
10
+ .action(async () => {
11
+ const display = DisplayManager.getInstance();
12
+ renderBanner();
13
+ // Ensure directory exists
14
+ await scaffold();
15
+ display.log(chalk.blue('Let\'s set up your Morpheus agent!'));
16
+ try {
17
+ const name = await input({
18
+ message: 'Name your agent:',
19
+ default: 'morpheus',
20
+ });
21
+ const personality = await input({
22
+ message: 'Describe its personality:',
23
+ default: 'helpful and concise',
24
+ });
25
+ const provider = await select({
26
+ message: 'Select LLM Provider:',
27
+ choices: [
28
+ { name: 'OpenAI', value: 'openai' },
29
+ { name: 'Anthropic', value: 'anthropic' },
30
+ { name: 'Ollama', value: 'ollama' },
31
+ { name: 'Google Gemini', value: 'gemini' },
32
+ ],
33
+ });
34
+ let defaultModel = 'gpt-3.5-turbo';
35
+ switch (provider) {
36
+ case 'openai':
37
+ defaultModel = 'gpt-4o';
38
+ break;
39
+ case 'anthropic':
40
+ defaultModel = 'claude-3-5-sonnet-20240620';
41
+ break;
42
+ case 'ollama':
43
+ defaultModel = 'llama3';
44
+ break;
45
+ case 'gemini':
46
+ defaultModel = 'gemini-pro';
47
+ break;
48
+ }
49
+ const model = await input({
50
+ message: 'Enter Model Name:',
51
+ default: defaultModel,
52
+ });
53
+ let apiKey;
54
+ if (provider !== 'ollama') {
55
+ apiKey = await password({
56
+ message: 'Enter API Key (leave empty if using env vars):',
57
+ });
58
+ }
59
+ const configManager = ConfigManager.getInstance();
60
+ // Update config
61
+ await configManager.set('agent.name', name);
62
+ await configManager.set('agent.personality', personality);
63
+ await configManager.set('llm.provider', provider);
64
+ await configManager.set('llm.model', model);
65
+ if (apiKey) {
66
+ await configManager.set('llm.api_key', apiKey);
67
+ }
68
+ // External Channels Configuration
69
+ const configureChannels = await confirm({
70
+ message: 'Do you want to configure external channels?',
71
+ default: false,
72
+ });
73
+ if (configureChannels) {
74
+ const channels = await checkbox({
75
+ message: 'Select channels to enable:',
76
+ choices: [
77
+ { name: 'Telegram', value: 'telegram' },
78
+ ],
79
+ });
80
+ if (channels.includes('telegram')) {
81
+ display.log(chalk.yellow('\n--- Telegram Configuration ---'));
82
+ display.log(chalk.gray('1. Create a bot via @BotFather to get your token.'));
83
+ display.log(chalk.gray('2. Get your User ID via @userinfobot.\n'));
84
+ const token = await password({
85
+ message: 'Enter Telegram Bot Token:',
86
+ validate: (value) => value.length > 0 || 'Token is required.'
87
+ });
88
+ const allowedUsersInput = await input({
89
+ message: 'Enter Allowed User IDs (comma separated):',
90
+ validate: (value) => value.length > 0 || 'At least one user ID is required for security.'
91
+ });
92
+ const allowedUsers = allowedUsersInput.split(',').map(id => id.trim()).filter(id => id.length > 0);
93
+ await configManager.set('channels.telegram.enabled', true);
94
+ await configManager.set('channels.telegram.token', token);
95
+ await configManager.set('channels.telegram.allowedUsers', allowedUsers);
96
+ }
97
+ }
98
+ display.log(chalk.green('\nConfiguration saved successfully!'));
99
+ display.log(chalk.cyan(`Run 'morpheus start' to launch ${name}.`));
100
+ }
101
+ catch (error) {
102
+ if (error instanceof Error && error.message.includes('force closed')) {
103
+ display.log(chalk.yellow('\nSetup cancelled.'));
104
+ return;
105
+ }
106
+ display.log(chalk.red('\nFailed to save configuration: ' + error.message));
107
+ }
108
+ });