diff --git a/desktop/src/agents/cli/hermes-config-secrets.cjs b/desktop/src/agents/cli/hermes-config-secrets.cjs index 31ef51f..27f9fcf 100644 --- a/desktop/src/agents/cli/hermes-config-secrets.cjs +++ b/desktop/src/agents/cli/hermes-config-secrets.cjs @@ -2,6 +2,7 @@ const fs = require('node:fs') const path = require('node:path') const yaml = require('js-yaml') const { nativeIdentityDirectory } = require('../native-config-directory.cjs') +const { hermesDotenvAssignments } = require('../hermes-dotenv.cjs') const { configSecrets, nativeSecretEnvKeys } = require('./native-secret-environment.cjs') function hermesConfigSecrets({ env, home, platform, declaredKeys }) { @@ -20,19 +21,7 @@ function hermesConfigSecrets({ env, home, platform, declaredKeys }) { const secrets = new Set(configSecrets(configs)) const declared = new Set(nativeSecretEnvKeys(declaredKeys)) try { - const values = { ...env } - for (const line of read('.env').split(/\r?\n/)) { - const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/) - if (!match) continue - const [, key, raw] = match - const quoted = raw.match(/^(['"])(.*?)\1\s*(?:#.*)?$/) - if (/^['"]/.test(raw) && !quoted) continue - let value = quoted ? quoted[2] : raw.replace(/\s+#.*$/, '').trim() - if (quoted?.[1] === '"') value = value.replace(/\\([nr"\\])/g, (_, c) => ({ n: '\n', r: '\r', '"': '"', '\\': '\\' })[c]) - if (quoted?.[1] === "'") value = value.replace(/\\(['\\])/g, '$1') - value = value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, - (_, name, fallback) => Object.hasOwn(values, name) ? values[name] : fallback || '') - values[key] = value + for (const [key, value] of hermesDotenvAssignments(read('.env'), env)) { if ((declared.has(key) || /(?:API[_-]?KEY|ACCESS[_-]?KEY|TOKEN|SECRET|PASSWORD|AUTHORIZATION)/i.test(key)) && value && value.length <= 8192) secrets.add(value) } diff --git a/desktop/src/agents/hermes-config-summary.cjs b/desktop/src/agents/hermes-config-summary.cjs index 8ef3832..ddc3ca8 100644 --- a/desktop/src/agents/hermes-config-summary.cjs +++ b/desktop/src/agents/hermes-config-summary.cjs @@ -3,6 +3,7 @@ const os = require('node:os') const path = require('node:path') const yaml = require('js-yaml') const { redactSecrets } = require('../security/secret-redaction.cjs') +const { hermesDotenvAssignments } = require('./hermes-dotenv.cjs') function hermesProviderFromConfig(config, env = {}) { const model = config?.model && typeof config.model === 'object' && !Array.isArray(config.model) ? config.model : {} @@ -24,10 +25,8 @@ function hermesProviderEnvironment(root, env = {}) { const stat = fs.statSync(file) if (stat.isFile() && stat.size <= 1024 * 1024) { let provider - for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) { - if (!/^\s*(?:export\s+)?HERMES_INFERENCE_PROVIDER\s*=/.test(line)) continue - const match = line.match(/^\s*(?:export\s+)?HERMES_INFERENCE_PROVIDER\s*=\s*(?:"([A-Za-z0-9._:-]*)"|'([A-Za-z0-9._:-]*)'|([A-Za-z0-9._:-]*))\s*(?:#.*)?$/) - provider = match ? match[1] ?? match[2] ?? match[3] : '' + for (const [key, value] of hermesDotenvAssignments(fs.readFileSync(file, 'utf8'), env)) { + if (key === 'HERMES_INFERENCE_PROVIDER') provider = value ?? '' } if (provider !== undefined) return { ...env, HERMES_INFERENCE_PROVIDER: provider } } diff --git a/desktop/src/agents/hermes-dotenv.cjs b/desktop/src/agents/hermes-dotenv.cjs new file mode 100644 index 0000000..a81ed41 --- /dev/null +++ b/desktop/src/agents/hermes-dotenv.cjs @@ -0,0 +1,24 @@ +function hermesDotenvAssignments(content, env = {}) { + const values = { ...env } + const assignments = [] + for (const line of content.split(/\r?\n/)) { + const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/) + if (!match) continue + const [, key, raw] = match + const quoted = raw.match(/^(['"])(.*?)\1\s*(?:#.*)?$/) + if (/^['"]/.test(raw) && !quoted) { + assignments.push([key, null]) + continue + } + let value = quoted ? quoted[2] : raw.replace(/\s+#.*$/, '').trim() + if (quoted?.[1] === '"') value = value.replace(/\\([nr"\\])/g, (_, c) => ({ n: '\n', r: '\r', '"': '"', '\\': '\\' })[c]) + if (quoted?.[1] === "'") value = value.replace(/\\(['\\])/g, '$1') + value = value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, + (_, name, fallback) => Object.hasOwn(values, name) ? values[name] : fallback || '') + values[key] = value + assignments.push([key, value]) + } + return assignments +} + +module.exports = { hermesDotenvAssignments } diff --git a/desktop/test/agents/hermes-config-summary.test.cjs b/desktop/test/agents/hermes-config-summary.test.cjs index e5771e8..64b562d 100644 --- a/desktop/test/agents/hermes-config-summary.test.cjs +++ b/desktop/test/agents/hermes-config-summary.test.cjs @@ -39,6 +39,10 @@ test('Hermes summary reads the selected native environment provider without expo assert.equal(hermesConfiguredProvider({ home, env: { HERMES_HOME: home, HERMES_INFERENCE_PROVIDER: 'custom' } }), '') fs.writeFileSync(path.join(home, '.env'), 'HERMES_INFERENCE_PROVIDER=${OTHER_PROVIDER}\n') assert.equal(hermesConfiguredProvider({ home, env: { HERMES_HOME: home, HERMES_INFERENCE_PROVIDER: 'custom' } }), '') + fs.writeFileSync(path.join(home, '.env'), 'SELECTED_PROVIDER=openrouter\nHERMES_INFERENCE_PROVIDER=${SELECTED_PROVIDER}\n') + assert.equal(hermesConfiguredProvider({ home, env: { HERMES_HOME: home, SELECTED_PROVIDER: 'stale' } }), 'openrouter') + fs.writeFileSync(path.join(home, '.env'), 'HERMES_INFERENCE_PROVIDER=${UNSET_MELDWORK_PROVIDER:-custom:work}\n') + assert.equal(hermesConfiguredProvider({ home, env: { HERMES_HOME: home } }), 'custom:work') fs.writeFileSync(path.join(home, 'config.yaml'), 'model:\n provider: anthropic\n') assert.equal(hermesConfiguredProvider({ home, env: { HERMES_HOME: home } }), 'anthropic') })