morpheus-cli 0.2.4 → 0.2.6

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,138 @@
1
+ /**
2
+ * Functions to resolve configuration values with precedence:
3
+ * 1. Provider-specific environment variable (e.g., OPENAI_API_KEY)
4
+ * 2. Generic environment variable (e.g., MORPHEUS_LLM_API_KEY)
5
+ * 3. Configuration file value
6
+ * 4. Default value
7
+ */
8
+ /**
9
+ * Resolve an API key with provider-specific precedence
10
+ * @param provider The current provider
11
+ * @param genericEnvVar The generic environment variable name
12
+ * @param configFileValue The value from the config file
13
+ * @returns The resolved API key value
14
+ */
15
+ export function resolveApiKey(provider, genericEnvVar, configFileValue) {
16
+ // Map provider to its specific environment variable
17
+ const providerSpecificVars = {
18
+ 'openai': 'OPENAI_API_KEY',
19
+ 'anthropic': 'ANTHROPIC_API_KEY',
20
+ 'openrouter': 'OPENROUTER_API_KEY',
21
+ 'ollama': '', // Ollama typically doesn't need an API key
22
+ 'gemini': 'GOOGLE_API_KEY'
23
+ };
24
+ const providerSpecificVar = providerSpecificVars[provider];
25
+ // Check provider-specific variable first, then generic, then config file
26
+ if (providerSpecificVar && process.env[providerSpecificVar]) {
27
+ return process.env[providerSpecificVar];
28
+ }
29
+ if (process.env[genericEnvVar]) {
30
+ return process.env[genericEnvVar];
31
+ }
32
+ return configFileValue;
33
+ }
34
+ /**
35
+ * Resolve a model name with provider-specific precedence
36
+ * @param provider The current provider
37
+ * @param genericEnvVar The generic environment variable name
38
+ * @param configFileValue The value from the config file
39
+ * @returns The resolved model name value
40
+ */
41
+ export function resolveModel(provider, genericEnvVar, configFileValue) {
42
+ // For now, we don't have provider-specific model variables, but we could add them later
43
+ // Check generic variable first, then config file
44
+ if (process.env[genericEnvVar]) {
45
+ return process.env[genericEnvVar];
46
+ }
47
+ return configFileValue;
48
+ }
49
+ /**
50
+ * Resolve a numeric configuration value
51
+ * @param genericEnvVar The generic environment variable name
52
+ * @param configFileValue The value from the config file
53
+ * @param defaultValue The default value to use if none is found
54
+ * @returns The resolved numeric value
55
+ */
56
+ export function resolveNumeric(genericEnvVar, configFileValue, defaultValue) {
57
+ if (process.env[genericEnvVar] !== undefined && process.env[genericEnvVar] !== '') {
58
+ const envValue = Number(process.env[genericEnvVar]);
59
+ if (!isNaN(envValue)) {
60
+ return envValue;
61
+ }
62
+ }
63
+ if (configFileValue !== undefined) {
64
+ return configFileValue;
65
+ }
66
+ return defaultValue;
67
+ }
68
+ /**
69
+ * Resolve a string configuration value
70
+ * @param genericEnvVar The generic environment variable name
71
+ * @param configFileValue The value from the config file
72
+ * @param defaultValue The default value to use if none is found
73
+ * @returns The resolved string value
74
+ */
75
+ export function resolveString(genericEnvVar, configFileValue, defaultValue) {
76
+ if (process.env[genericEnvVar]) {
77
+ return process.env[genericEnvVar];
78
+ }
79
+ if (configFileValue !== undefined) {
80
+ return configFileValue;
81
+ }
82
+ return defaultValue;
83
+ }
84
+ /**
85
+ * Resolve a boolean configuration value
86
+ * @param genericEnvVar The generic environment variable name
87
+ * @param configFileValue The value from the config file
88
+ * @param defaultValue The default value to use if none is found
89
+ * @returns The resolved boolean value
90
+ */
91
+ export function resolveBoolean(genericEnvVar, configFileValue, defaultValue) {
92
+ if (process.env[genericEnvVar] !== undefined) {
93
+ const envValue = process.env[genericEnvVar]?.toLowerCase();
94
+ if (envValue === 'true' || envValue === '1') {
95
+ return true;
96
+ }
97
+ else if (envValue === 'false' || envValue === '0') {
98
+ return false;
99
+ }
100
+ }
101
+ if (configFileValue !== undefined) {
102
+ return configFileValue;
103
+ }
104
+ return defaultValue;
105
+ }
106
+ /**
107
+ * Resolve an array of string configuration value
108
+ * @param genericEnvVar The generic environment variable name
109
+ * @param configFileValue The value from the config file
110
+ * @param defaultValue The default value to use if none is found
111
+ * @returns The resolved array of strings value
112
+ */
113
+ export function resolveStringArray(genericEnvVar, configFileValue, defaultValue) {
114
+ if (process.env[genericEnvVar]) {
115
+ // Split the environment variable by commas and trim whitespace
116
+ return process.env[genericEnvVar].split(',').map(item => item.trim()).filter(item => item.length > 0);
117
+ }
118
+ if (configFileValue !== undefined) {
119
+ return configFileValue;
120
+ }
121
+ return defaultValue;
122
+ }
123
+ /**
124
+ * Resolve a provider configuration value
125
+ * @param genericEnvVar The generic environment variable name
126
+ * @param configFileValue The value from the config file
127
+ * @param defaultValue The default value to use if none is found
128
+ * @returns The resolved provider value
129
+ */
130
+ export function resolveProvider(genericEnvVar, configFileValue, defaultValue) {
131
+ if (process.env[genericEnvVar]) {
132
+ return process.env[genericEnvVar];
133
+ }
134
+ if (configFileValue !== undefined) {
135
+ return configFileValue;
136
+ }
137
+ return defaultValue;
138
+ }
@@ -2,17 +2,19 @@ import { z } from 'zod';
2
2
  import { DEFAULT_CONFIG } from '../types/config.js';
3
3
  export const AudioConfigSchema = z.object({
4
4
  provider: z.enum(['google']).default(DEFAULT_CONFIG.audio.provider),
5
+ model: z.string().min(1).default(DEFAULT_CONFIG.audio.model),
5
6
  enabled: z.boolean().default(DEFAULT_CONFIG.audio.enabled),
6
7
  apiKey: z.string().optional(),
7
8
  maxDurationSeconds: z.number().default(DEFAULT_CONFIG.audio.maxDurationSeconds),
8
9
  supportedMimeTypes: z.array(z.string()).default(DEFAULT_CONFIG.audio.supportedMimeTypes),
9
10
  });
10
11
  export const LLMConfigSchema = z.object({
11
- provider: z.enum(['openai', 'anthropic', 'ollama', 'gemini']).default(DEFAULT_CONFIG.llm.provider),
12
+ provider: z.enum(['openai', 'anthropic', 'openrouter', 'ollama', 'gemini']).default(DEFAULT_CONFIG.llm.provider),
12
13
  model: z.string().min(1).default(DEFAULT_CONFIG.llm.model),
13
14
  temperature: z.number().min(0).max(1).default(DEFAULT_CONFIG.llm.temperature),
14
15
  max_tokens: z.number().int().positive().optional(),
15
16
  api_key: z.string().optional(),
17
+ base_url: z.string().optional(),
16
18
  context_window: z.number().int().positive().optional(),
17
19
  });
18
20
  export const SatiConfigSchema = LLMConfigSchema.extend({
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import request from 'supertest';
3
+ import express from 'express';
4
+ import bodyParser from 'body-parser';
5
+ import { createApiRouter } from '../api.js';
6
+ import { ConfigManager } from '../../config/manager.js';
7
+ import fs from 'fs-extra';
8
+ // Mock dependencies
9
+ vi.mock('../../config/manager.js');
10
+ vi.mock('fs-extra');
11
+ describe('Status API', () => {
12
+ let app;
13
+ let mockConfigManager;
14
+ beforeEach(() => {
15
+ // Reset mocks
16
+ vi.clearAllMocks();
17
+ // Mock ConfigManager instance
18
+ mockConfigManager = {
19
+ get: vi.fn(),
20
+ };
21
+ ConfigManager.getInstance.mockReturnValue(mockConfigManager);
22
+ // Setup App
23
+ app = express();
24
+ app.use(bodyParser.json());
25
+ // Create router without server instance to test fallback
26
+ app.use('/api', createApiRouter());
27
+ });
28
+ afterEach(() => {
29
+ vi.restoreAllMocks();
30
+ });
31
+ describe('GET /api/status', () => {
32
+ it('should return status information including server port', async () => {
33
+ const mockConfig = {
34
+ agent: { name: 'TestAgent' },
35
+ llm: { provider: 'openai', model: 'gpt-4' },
36
+ ui: { port: 3333 }
37
+ };
38
+ mockConfigManager.get.mockReturnValue(mockConfig);
39
+ // Mock fs.readJson to return a version
40
+ fs.readJson.mockResolvedValue({ version: '1.0.0' });
41
+ const res = await request(app).get('/api/status');
42
+ expect(res.status).toBe(200);
43
+ expect(res.body).toHaveProperty('status');
44
+ expect(res.body).toHaveProperty('uptimeSeconds');
45
+ expect(res.body).toHaveProperty('pid');
46
+ expect(res.body).toHaveProperty('projectVersion');
47
+ expect(res.body).toHaveProperty('nodeVersion');
48
+ expect(res.body).toHaveProperty('agentName');
49
+ expect(res.body).toHaveProperty('llmProvider');
50
+ expect(res.body).toHaveProperty('llmModel');
51
+ expect(res.body).toHaveProperty('serverPort');
52
+ expect(res.body.serverPort).toBe(3333);
53
+ });
54
+ });
55
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import request from 'supertest';
3
+ import express from 'express';
4
+ import bodyParser from 'body-parser';
5
+ import { createApiRouter } from '../api.js';
6
+ import { ConfigManager } from '../../config/manager.js';
7
+ import fs from 'fs-extra';
8
+ // Mock dependencies
9
+ vi.mock('../../config/manager.js');
10
+ vi.mock('fs-extra');
11
+ describe('Status API with Server Instance', () => {
12
+ let app;
13
+ let mockConfigManager;
14
+ let mockServerInstance;
15
+ beforeEach(() => {
16
+ // Reset mocks
17
+ vi.clearAllMocks();
18
+ // Mock ConfigManager instance
19
+ mockConfigManager = {
20
+ get: vi.fn(),
21
+ };
22
+ ConfigManager.getInstance.mockReturnValue(mockConfigManager);
23
+ // Mock server instance with getPort method
24
+ mockServerInstance = {
25
+ getPort: vi.fn().mockReturnValue(4567),
26
+ };
27
+ // Setup App
28
+ app = express();
29
+ app.use(bodyParser.json());
30
+ // Create router with server instance
31
+ app.use('/api', createApiRouter(mockServerInstance));
32
+ });
33
+ afterEach(() => {
34
+ vi.restoreAllMocks();
35
+ });
36
+ describe('GET /api/status', () => {
37
+ it('should return status information with server instance port', async () => {
38
+ const mockConfig = {
39
+ agent: { name: 'TestAgent' },
40
+ llm: { provider: 'openai', model: 'gpt-4' },
41
+ ui: { port: 3333 } // This should be overridden by server instance
42
+ };
43
+ mockConfigManager.get.mockReturnValue(mockConfig);
44
+ // Mock fs.readJson to return a version
45
+ fs.readJson.mockResolvedValue({ version: '1.0.0' });
46
+ const res = await request(app).get('/api/status');
47
+ expect(res.status).toBe(200);
48
+ expect(res.body).toHaveProperty('status');
49
+ expect(res.body).toHaveProperty('uptimeSeconds');
50
+ expect(res.body).toHaveProperty('pid');
51
+ expect(res.body).toHaveProperty('projectVersion');
52
+ expect(res.body).toHaveProperty('nodeVersion');
53
+ expect(res.body).toHaveProperty('agentName');
54
+ expect(res.body).toHaveProperty('llmProvider');
55
+ expect(res.body).toHaveProperty('llmModel');
56
+ expect(res.body).toHaveProperty('serverPort');
57
+ expect(res.body.serverPort).toBe(4567); // Should come from server instance
58
+ });
59
+ });
60
+ });
package/dist/http/api.js CHANGED
@@ -6,6 +6,10 @@ import fs from 'fs-extra';
6
6
  import path from 'path';
7
7
  import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
8
8
  import { SatiRepository } from '../runtime/memory/sati/repository.js';
9
+ import { spawn } from 'child_process';
10
+ import { z } from 'zod';
11
+ import { MCPManager } from '../config/mcp-manager.js';
12
+ import { MCPServerConfigSchema } from '../config/schemas.js';
9
13
  async function readLastLines(filePath, n) {
10
14
  try {
11
15
  const content = await fs.readFile(filePath, 'utf8');
@@ -38,6 +42,29 @@ export function createApiRouter() {
38
42
  llmModel: config.llm.model
39
43
  });
40
44
  });
45
+ router.post('/restart', async (req, res) => {
46
+ try {
47
+ // Send response immediately before restarting
48
+ res.json({
49
+ success: true,
50
+ message: 'Restart initiated. Process will shut down and restart shortly.'
51
+ });
52
+ // Delay the actual restart to allow response to be sent
53
+ setTimeout(() => {
54
+ // Execute the restart command using the CLI
55
+ const restartProcess = spawn(process.execPath, [process.argv[1], 'restart'], {
56
+ detached: true,
57
+ stdio: 'ignore'
58
+ });
59
+ restartProcess.unref();
60
+ // Exit the current process
61
+ process.exit(0);
62
+ }, 100);
63
+ }
64
+ catch (error) {
65
+ res.status(500).json({ error: error.message });
66
+ }
67
+ });
41
68
  router.get('/config', (req, res) => {
42
69
  res.json(configManager.get());
43
70
  });
@@ -224,6 +251,64 @@ export function createApiRouter() {
224
251
  res.status(500).json({ error: error.message });
225
252
  }
226
253
  });
254
+ const MCPUpsertSchema = z.object({
255
+ name: z.string().min(1),
256
+ config: MCPServerConfigSchema,
257
+ });
258
+ const MCPToggleSchema = z.object({
259
+ enabled: z.boolean(),
260
+ });
261
+ router.get('/mcp/servers', async (_req, res) => {
262
+ try {
263
+ const servers = await MCPManager.listServers();
264
+ res.json({ servers });
265
+ }
266
+ catch (error) {
267
+ res.status(500).json({ error: 'Failed to load MCP servers.', details: String(error) });
268
+ }
269
+ });
270
+ router.post('/mcp/servers', async (req, res) => {
271
+ try {
272
+ const body = MCPUpsertSchema.parse(req.body);
273
+ await MCPManager.addServer(body.name, body.config);
274
+ res.status(201).json({ ok: true });
275
+ }
276
+ catch (error) {
277
+ const status = error instanceof z.ZodError ? 400 : 500;
278
+ res.status(status).json({ error: 'Failed to create MCP server.', details: error });
279
+ }
280
+ });
281
+ router.put('/mcp/servers/:name', async (req, res) => {
282
+ try {
283
+ const body = MCPUpsertSchema.parse({ name: req.params.name, config: req.body?.config ?? req.body });
284
+ await MCPManager.updateServer(body.name, body.config);
285
+ res.json({ ok: true });
286
+ }
287
+ catch (error) {
288
+ const status = error instanceof z.ZodError ? 400 : 500;
289
+ res.status(status).json({ error: 'Failed to update MCP server.', details: error });
290
+ }
291
+ });
292
+ router.delete('/mcp/servers/:name', async (req, res) => {
293
+ try {
294
+ await MCPManager.deleteServer(req.params.name);
295
+ res.json({ ok: true });
296
+ }
297
+ catch (error) {
298
+ res.status(500).json({ error: 'Failed to delete MCP server.', details: String(error) });
299
+ }
300
+ });
301
+ router.patch('/mcp/servers/:name/toggle', async (req, res) => {
302
+ try {
303
+ const body = MCPToggleSchema.parse(req.body);
304
+ await MCPManager.setServerEnabled(req.params.name, body.enabled);
305
+ res.json({ ok: true });
306
+ }
307
+ catch (error) {
308
+ const status = error instanceof z.ZodError ? 400 : 500;
309
+ res.status(status).json({ error: 'Failed to toggle MCP server.', details: error });
310
+ }
311
+ });
227
312
  // Keep PUT for backward compatibility if needed, or remove.
228
313
  // Tasks says Implement POST. I'll remove PUT to avoid confusion or redirect it.
229
314
  router.put('/config', async (req, res) => {
@@ -2,13 +2,15 @@ import { AUTH_HEADER } from '../../types/auth.js';
2
2
  import { DisplayManager } from '../../runtime/display.js';
3
3
  /**
4
4
  * Middleware to protect API routes with a password from THE_ARCHITECT_PASS env var.
5
- * If the env var is not set, authentication is skipped (open mode).
5
+ * If the env var is not set, uses default password 'iamthearchitect'.
6
6
  */
7
7
  export const authMiddleware = (req, res, next) => {
8
- const architectPass = process.env.THE_ARCHITECT_PASS;
9
- // If password is not configured, allow all requests
10
- if (!architectPass || architectPass.trim() === '') {
11
- return next();
8
+ // Use environment variable if set, otherwise use default password
9
+ const architectPass = process.env.THE_ARCHITECT_PASS || 'iamthearchitect';
10
+ // If password is not configured (using default), log a warning
11
+ if (!process.env.THE_ARCHITECT_PASS) {
12
+ const display = DisplayManager.getInstance();
13
+ display.log('Using default password for dashboard access. For security, set THE_ARCHITECT_PASS environment variable.', { source: 'http', level: 'warning' });
12
14
  }
13
15
  const providedPass = req.headers[AUTH_HEADER];
14
16
  if (providedPass === architectPass) {
@@ -20,8 +20,29 @@ export class HttpServer {
20
20
  setupMiddleware() {
21
21
  this.app.use(cors());
22
22
  this.app.use(bodyParser.json());
23
+ // Adicionar cabeçalhos para evitar indexação por motores de busca
24
+ this.app.use((req, res, next) => {
25
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
26
+ next();
27
+ });
23
28
  }
24
29
  setupRoutes() {
30
+ // Rota de health check pública (sem autenticação)
31
+ this.app.get('/health', (req, res) => {
32
+ res.status(200).json({
33
+ status: 'healthy',
34
+ timestamp: new Date().toISOString(),
35
+ uptime: process.uptime()
36
+ });
37
+ });
38
+ // Rota de health check para o Docker (padrão)
39
+ this.app.get('/api/health', (req, res) => {
40
+ res.status(200).json({
41
+ status: 'healthy',
42
+ timestamp: new Date().toISOString(),
43
+ uptime: process.uptime()
44
+ });
45
+ });
25
46
  this.app.use('/api', authMiddleware, createApiRouter());
26
47
  // Serve static frontend from compiled output
27
48
  const uiPath = path.resolve(__dirname, '../ui');
@@ -13,6 +13,7 @@ const mockConfig = {
13
13
  logging: { enabled: false, level: 'info', retention: '1d' },
14
14
  audio: {
15
15
  provider: 'google',
16
+ model: 'gemini-2.5-flash-lite',
16
17
  enabled: false,
17
18
  maxDurationSeconds: 60,
18
19
  supportedMimeTypes: ['audio/ogg']
@@ -49,7 +49,7 @@ export class Oracle {
49
49
  throw new ProviderError(this.config.llm.provider || 'unknown', err, "Oracle initialization failed");
50
50
  }
51
51
  }
52
- async chat(message, extraUsage) {
52
+ async chat(message, extraUsage, isTelephonist) {
53
53
  if (!this.provider) {
54
54
  throw new Error("Oracle not initialized. Call initialize() first.");
55
55
  }
@@ -61,8 +61,8 @@ export class Oracle {
61
61
  const userMessage = new HumanMessage(message);
62
62
  // Inject provider/model metadata for persistence
63
63
  userMessage.provider_metadata = {
64
- provider: this.config.llm.provider,
65
- model: this.config.llm.model
64
+ provider: isTelephonist ? this.config.audio?.provider : this.config.llm.provider,
65
+ model: isTelephonist ? this.config.audio?.model : this.config.llm.model
66
66
  };
67
67
  // Attach extra usage (e.g. from Audio) to the user message to be persisted
68
68
  if (extraUsage) {
@@ -38,14 +38,24 @@ export class ProviderFactory {
38
38
  model = new ChatOpenAI({
39
39
  modelName: config.model,
40
40
  temperature: config.temperature,
41
- apiKey: config.api_key, // LangChain will also check process.env.OPENAI_API_KEY
41
+ apiKey: process.env.OPENAI_API_KEY || config.api_key, // Check env var first, then config
42
42
  });
43
43
  break;
44
44
  case 'anthropic':
45
45
  model = new ChatAnthropic({
46
46
  modelName: config.model,
47
47
  temperature: config.temperature,
48
- apiKey: config.api_key,
48
+ apiKey: process.env.ANTHROPIC_API_KEY || config.api_key, // Check env var first, then config
49
+ });
50
+ break;
51
+ case 'openrouter':
52
+ model = new ChatOpenAI({
53
+ modelName: config.model,
54
+ temperature: config.temperature,
55
+ apiKey: process.env.OPENROUTER_API_KEY || config.api_key, // Check env var first, then config
56
+ configuration: {
57
+ baseURL: config.base_url || 'https://openrouter.ai/api/v1'
58
+ }
49
59
  });
50
60
  break;
51
61
  case 'ollama':
@@ -60,7 +70,7 @@ export class ProviderFactory {
60
70
  model = new ChatGoogleGenerativeAI({
61
71
  model: config.model,
62
72
  temperature: config.temperature,
63
- apiKey: config.api_key
73
+ apiKey: process.env.GOOGLE_API_KEY || config.api_key // Check env var first, then config
64
74
  });
65
75
  break;
66
76
  default:
@@ -99,7 +109,7 @@ export class ProviderFactory {
99
109
  suggestion = `Model '${config.model}' may not be available. Check provider docs.`;
100
110
  }
101
111
  else if (msg.includes("unsupported provider")) {
102
- suggestion = "Edit your config file to use a supported provider (openai, anthropic, ollama, gemini).";
112
+ suggestion = "Edit your config file to use a supported provider (openai, anthropic, openrouter, ollama, gemini).";
103
113
  }
104
114
  throw new ProviderError(config.provider, error, suggestion);
105
115
  }
@@ -5,66 +5,60 @@ import { ConfigManager } from "../../config/manager.js";
5
5
  export const ConfigQueryTool = tool(async ({ key }) => {
6
6
  try {
7
7
  const configManager = ConfigManager.getInstance();
8
- // Load config if not already loaded
9
8
  await configManager.load();
10
9
  const config = configManager.get();
11
10
  if (key) {
12
- // Return specific configuration value
13
- const value = config[key];
11
+ // Suporta busca por chave aninhada, ex: 'llm.model' ou 'channels.telegram.enabled'
12
+ const value = key.split('.').reduce((obj, k) => (obj ? obj[k] : undefined), config);
14
13
  return JSON.stringify({ [key]: value });
15
14
  }
16
15
  else {
17
- // Return all configuration values
18
16
  return JSON.stringify(config);
19
17
  }
20
18
  }
21
19
  catch (error) {
22
- console.error("Error in ConfigQueryTool:", error);
20
+ // Nunca usar console.log, mas manter para debug local
23
21
  return JSON.stringify({ error: "Failed to query configuration" });
24
22
  }
25
23
  }, {
26
- name: "config_query",
27
- description: "Queries current configuration values. Accepts an optional 'key' parameter to get a specific configuration value, or no parameter to get all configuration values.",
24
+ name: "morpheus_config_query",
25
+ description: "Queries current configuration values. Accepts an optional 'key' parameter (dot notation supported, e.g. 'llm.model') to get a specific configuration value, or no parameter to get all configuration values.",
28
26
  schema: z.object({
29
27
  key: z.string().optional(),
30
28
  }),
31
29
  });
32
- // Tool for updating configuration values
30
+ // Tool for updating configuration values (suporta objetos aninhados via dot notation)
31
+ function setNestedValue(obj, path, value) {
32
+ const keys = path.split('.');
33
+ let curr = obj;
34
+ for (let i = 0; i < keys.length - 1; i++) {
35
+ if (!curr[keys[i]] || typeof curr[keys[i]] !== 'object') {
36
+ curr[keys[i]] = {};
37
+ }
38
+ curr = curr[keys[i]];
39
+ }
40
+ curr[keys[keys.length - 1]] = value;
41
+ }
33
42
  export const ConfigUpdateTool = tool(async ({ updates }) => {
34
43
  try {
35
44
  const configManager = ConfigManager.getInstance();
36
- // Load current config
37
45
  await configManager.load();
38
46
  const currentConfig = configManager.get();
39
- // Create new config with updates
40
- const newConfig = { ...currentConfig, ...updates };
41
- // Save the updated config
47
+ // Suporta updates com dot notation para campos aninhados
48
+ const newConfig = { ...currentConfig };
49
+ for (const key in updates) {
50
+ setNestedValue(newConfig, key, updates[key]);
51
+ }
42
52
  await configManager.save(newConfig);
43
53
  return JSON.stringify({ success: true, message: "Configuration updated successfully" });
44
54
  }
45
55
  catch (error) {
46
- console.error("Error in ConfigUpdateTool:", error);
47
56
  return JSON.stringify({ error: `Failed to update configuration: ${error.message}` });
48
57
  }
49
58
  }, {
50
- name: "config_update",
51
- description: "Updates configuration values with validation. Accepts an 'updates' object containing key-value pairs to update.",
59
+ name: "morpheus_config_update",
60
+ description: "Updates configuration values with validation. Accepts an 'updates' object containing key-value pairs to update. Supports dot notation for nested fields (e.g. 'llm.model').",
52
61
  schema: z.object({
53
- updates: z.object({
54
- // Define common config properties that might be updated
55
- // Using optional fields to allow flexible updates
56
- "llm.provider": z.string().optional(),
57
- "llm.model": z.string().optional(),
58
- "llm.temperature": z.number().optional(),
59
- "llm.api_key": z.string().optional(),
60
- "ui.enabled": z.boolean().optional(),
61
- "ui.port": z.number().optional(),
62
- "logging.enabled": z.boolean().optional(),
63
- "logging.level": z.enum(['debug', 'info', 'warn', 'error']).optional(),
64
- "audio.enabled": z.boolean().optional(),
65
- "audio.provider": z.string().optional(),
66
- "memory.limit": z.number().optional(),
67
- // Add more specific fields as needed, or use a catch-all for other properties
68
- }).passthrough(), // Allow additional properties not explicitly defined
62
+ updates: z.object({}).passthrough(),
69
63
  }),
70
64
  });
@@ -10,6 +10,7 @@ export const DEFAULT_CONFIG = {
10
10
  },
11
11
  audio: {
12
12
  provider: 'google',
13
+ model: 'gemini-2.5-flash-lite',
13
14
  enabled: true,
14
15
  maxDurationSeconds: 300,
15
16
  supportedMimeTypes: ['audio/ogg', 'audio/mp3', 'audio/mpeg', 'audio/wav'],