morpheus-cli 0.6.6 → 0.6.7

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.
@@ -7,8 +7,58 @@ import { homedir } from "os";
7
7
  import Database from "better-sqlite3";
8
8
  import { TaskRepository } from "../tasks/repository.js";
9
9
  import { TaskRequestContext } from "../tasks/context.js";
10
+ import { isEnvVarSet } from "../../config/precedence.js";
10
11
  // ─── Shared ───────────────────────────────────────────────────────────────────
11
12
  const shortMemoryDbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
13
+ /**
14
+ * Map of config paths to their corresponding environment variable names.
15
+ * Used to check if a config field is being overridden by an env var.
16
+ */
17
+ const CONFIG_TO_ENV_MAP = {
18
+ 'llm.provider': ['MORPHEUS_LLM_PROVIDER'],
19
+ 'llm.model': ['MORPHEUS_LLM_MODEL'],
20
+ 'llm.temperature': ['MORPHEUS_LLM_TEMPERATURE'],
21
+ 'llm.max_tokens': ['MORPHEUS_LLM_MAX_TOKENS'],
22
+ 'llm.api_key': ['MORPHEUS_LLM_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GOOGLE_API_KEY', 'OPENROUTER_API_KEY'],
23
+ 'llm.context_window': ['MORPHEUS_LLM_CONTEXT_WINDOW'],
24
+ 'sati.provider': ['MORPHEUS_SATI_PROVIDER'],
25
+ 'sati.model': ['MORPHEUS_SATI_MODEL'],
26
+ 'sati.temperature': ['MORPHEUS_SATI_TEMPERATURE'],
27
+ 'sati.api_key': ['MORPHEUS_SATI_API_KEY'],
28
+ 'neo.provider': ['MORPHEUS_NEO_PROVIDER'],
29
+ 'neo.model': ['MORPHEUS_NEO_MODEL'],
30
+ 'neo.temperature': ['MORPHEUS_NEO_TEMPERATURE'],
31
+ 'neo.api_key': ['MORPHEUS_NEO_API_KEY'],
32
+ 'apoc.provider': ['MORPHEUS_APOC_PROVIDER'],
33
+ 'apoc.model': ['MORPHEUS_APOC_MODEL'],
34
+ 'apoc.temperature': ['MORPHEUS_APOC_TEMPERATURE'],
35
+ 'apoc.api_key': ['MORPHEUS_APOC_API_KEY'],
36
+ 'apoc.working_dir': ['MORPHEUS_APOC_WORKING_DIR'],
37
+ 'apoc.timeout_ms': ['MORPHEUS_APOC_TIMEOUT_MS'],
38
+ 'trinity.provider': ['MORPHEUS_TRINITY_PROVIDER'],
39
+ 'trinity.model': ['MORPHEUS_TRINITY_MODEL'],
40
+ 'trinity.temperature': ['MORPHEUS_TRINITY_TEMPERATURE'],
41
+ 'trinity.api_key': ['MORPHEUS_TRINITY_API_KEY'],
42
+ 'audio.provider': ['MORPHEUS_AUDIO_PROVIDER'],
43
+ 'audio.model': ['MORPHEUS_AUDIO_MODEL'],
44
+ 'audio.apiKey': ['MORPHEUS_AUDIO_API_KEY'],
45
+ 'audio.maxDurationSeconds': ['MORPHEUS_AUDIO_MAX_DURATION'],
46
+ };
47
+ /**
48
+ * Checks if a config field is overridden by an environment variable.
49
+ */
50
+ function isFieldOverriddenByEnv(fieldPath) {
51
+ const envVars = CONFIG_TO_ENV_MAP[fieldPath];
52
+ if (!envVars)
53
+ return false;
54
+ return envVars.some(envVar => isEnvVarSet(envVar));
55
+ }
56
+ /**
57
+ * Gets all fields that are overridden by environment variables.
58
+ */
59
+ function getOverriddenFields() {
60
+ return Object.keys(CONFIG_TO_ENV_MAP).filter(isFieldOverriddenByEnv);
61
+ }
12
62
  // ─── Config ───────────────────────────────────────────────────────────────────
13
63
  function setNestedValue(obj, dotPath, value) {
14
64
  const keys = dotPath.split(".");
@@ -43,23 +93,37 @@ export const ConfigQueryTool = tool(async ({ key }) => {
43
93
  }),
44
94
  });
45
95
  export const ConfigUpdateTool = tool(async ({ updates }) => {
46
- try {
47
- const configManager = ConfigManager.getInstance();
48
- await configManager.load();
49
- const currentConfig = configManager.get();
50
- const newConfig = { ...currentConfig };
51
- for (const key in updates) {
52
- setNestedValue(newConfig, key, updates[key]);
96
+ const configManager = ConfigManager.getInstance();
97
+ await configManager.load();
98
+ // Check if any fields are overridden by environment variables
99
+ const overriddenFields = [];
100
+ for (const key in updates) {
101
+ if (isFieldOverriddenByEnv(key)) {
102
+ overriddenFields.push(key);
53
103
  }
54
- await configManager.save(newConfig);
55
- return JSON.stringify({ success: true, message: "Configuration updated successfully" });
56
104
  }
57
- catch (error) {
58
- return JSON.stringify({ error: `Failed to update configuration: ${error.message}` });
105
+ if (overriddenFields.length > 0) {
106
+ const envVarNames = overriddenFields
107
+ .flatMap(field => CONFIG_TO_ENV_MAP[field] || [])
108
+ .filter((v, i, arr) => arr.indexOf(v) === i) // Remove duplicates
109
+ .join(', ');
110
+ const errorMsg = `BLOCKED_BY_ENV: Cannot update ${overriddenFields.join(', ')} because these fields are controlled by environment variables (${envVarNames}). To change them, edit your .env file and restart Morpheus.`;
111
+ throw new Error(errorMsg);
112
+ }
113
+ const currentConfig = configManager.get();
114
+ const newConfig = { ...currentConfig };
115
+ for (const key in updates) {
116
+ setNestedValue(newConfig, key, updates[key]);
59
117
  }
118
+ await configManager.save(newConfig);
119
+ return JSON.stringify({
120
+ success: true,
121
+ message: "Configuration updated successfully",
122
+ updatedFields: Object.keys(updates),
123
+ });
60
124
  }, {
61
125
  name: "morpheus_config_update",
62
- 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').",
126
+ 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'). Note: Fields overridden by environment variables cannot be updated.",
63
127
  schema: z.object({
64
128
  updates: z.object({}).passthrough(),
65
129
  }),
@@ -77,30 +141,39 @@ export const DiagnosticTool = tool(async () => {
77
141
  const config = configManager.get();
78
142
  const requiredFields = ["llm", "logging", "ui"];
79
143
  const missingFields = requiredFields.filter((field) => !(field in config));
144
+ // Check MORPHEUS_SECRET
145
+ const hasSecret = !!process.env.MORPHEUS_SECRET;
80
146
  if (missingFields.length === 0) {
81
147
  const sati = config.sati;
82
148
  const apoc = config.apoc;
149
+ const details = {
150
+ oracleProvider: config.llm?.provider,
151
+ oracleModel: config.llm?.model,
152
+ satiProvider: sati?.provider ?? `${config.llm?.provider} (inherited)`,
153
+ satiModel: sati?.model ?? `${config.llm?.model} (inherited)`,
154
+ apocProvider: apoc?.provider ?? `${config.llm?.provider} (inherited)`,
155
+ apocModel: apoc?.model ?? `${config.llm?.model} (inherited)`,
156
+ apocWorkingDir: apoc?.working_dir ?? "not set",
157
+ uiEnabled: config.ui?.enabled,
158
+ uiPort: config.ui?.port,
159
+ morpheusSecret: hasSecret ? "configured ✓" : "NOT SET ⚠️",
160
+ };
83
161
  components.config = {
84
- status: "healthy",
85
- message: "Configuration is valid and complete",
86
- details: {
87
- oracleProvider: config.llm?.provider,
88
- oracleModel: config.llm?.model,
89
- satiProvider: sati?.provider ?? `${config.llm?.provider} (inherited)`,
90
- satiModel: sati?.model ?? `${config.llm?.model} (inherited)`,
91
- apocProvider: apoc?.provider ?? `${config.llm?.provider} (inherited)`,
92
- apocModel: apoc?.model ?? `${config.llm?.model} (inherited)`,
93
- apocWorkingDir: apoc?.working_dir ?? "not set",
94
- uiEnabled: config.ui?.enabled,
95
- uiPort: config.ui?.port,
96
- },
162
+ status: hasSecret ? "healthy" : "warning",
163
+ message: hasSecret
164
+ ? "Configuration is valid and complete"
165
+ : "Configuration is valid but MORPHEUS_SECRET is not set. API keys and database passwords will be stored in plaintext.",
166
+ details,
97
167
  };
98
168
  }
99
169
  else {
100
170
  components.config = {
101
171
  status: "warning",
102
172
  message: `Missing required configuration fields: ${missingFields.join(", ")}`,
103
- details: { missingFields },
173
+ details: {
174
+ missingFields,
175
+ morpheusSecret: hasSecret ? "configured ✓" : "NOT SET ⚠️",
176
+ },
104
177
  };
105
178
  }
106
179
  }