src/config.js (3753 bytes)
1 // Settings. `ideamine config` writes them to ~/.ideamine/config.json. An environment variable 2 // overrides the file, so a test or a single command can use other values. 3 4 import fs from 'node:fs'; 5 import path from 'node:path'; 6 import { home, withLock, writeAtomic } from './store.js'; 7 8 export const SETTINGS = { 9 embed_url: { 10 env: 'IDEAMINE_EMBED_URL', 11 value: 'http://127.0.0.1:11434/v1', 12 about: 'OpenAI-compatible embeddings API (Ollama, llama.cpp server)', 13 }, 14 embed_model: { env: 'IDEAMINE_EMBED_MODEL', value: 'nomic-embed-text', about: 'embedding model' }, 15 search_threshold: { 16 env: 'IDEAMINE_SEARCH_THRESHOLD', 17 value: 0.5, 18 about: 'lowest similarity (0 to 1) of a search result to the query', 19 }, 20 group_threshold: { 21 env: 'IDEAMINE_GROUP_THRESHOLD', 22 value: 0.65, 23 about: 'lowest mean similarity (0 to 1) of the ideas in a group; also the floor for related ideas', 24 }, 25 publish_url: { 26 env: 'IDEAMINE_PUBLISH_URL', 27 value: '', 28 about: 'dashboard server that gets index.html and data.json by HTTP PUT (empty: off)', 29 }, 30 serve_port: { 31 env: 'IDEAMINE_SERVE_PORT', 32 value: '4332', 33 about: 'port of `ideamine serve`, the dashboard with buttons on 127.0.0.1', 34 }, 35 serve_hosts: { 36 env: 'IDEAMINE_SERVE_HOSTS', 37 value: '', 38 about: 'more Host names that `ideamine serve` answers, comma-separated (for a server behind nginx)', 39 }, 40 sync_url: { 41 env: 'IDEAMINE_SYNC_URL', 42 value: '', 43 about: 'ideamine server that holds the archive for every machine (empty: the archive is here)', 44 }, 45 prompt_log: { 46 env: 'IDEAMINE_PROMPT_LOG', 47 value: 'off', 48 about: 'on: send each prompt to the ideamine server, for the Prompts tab of the dashboard', 49 }, 50 memstate_url: { 51 env: 'IDEAMINE_MEMSTATE_URL', 52 value: '', 53 about: 'memstated daemon that the Memory tab of `ideamine serve` reads (empty: no Memory tab)', 54 }, 55 }; 56 57 const configPath = () => path.join(home(), 'config.json'); 58 59 function readFile() { 60 try { 61 return JSON.parse(fs.readFileSync(configPath(), 'utf8')); 62 } catch { 63 return {}; 64 } 65 } 66 67 function setting(key) { 68 const s = SETTINGS[key]; 69 if (!s) throw new Error(`unknown setting "${key}" (use: ${Object.keys(SETTINGS).join(', ')})`); 70 return s; 71 } 72 73 function parse(key, raw) { 74 if (typeof setting(key).value !== 'number') return String(raw).trim(); 75 const n = Number(raw); 76 if (String(raw).trim() === '' || !Number.isFinite(n) || n < 0 || n > 1) throw new Error(`${key} must be a number from 0 to 1`); 77 return n; 78 } 79 80 /** The value and where it comes from: env, file, or default. */ 81 function lookup(key) { 82 const s = setting(key); 83 const env = process.env[s.env]; 84 if (env !== undefined && env !== '') return { value: parse(key, env), from: s.env }; 85 const saved = readFile()[key]; 86 if (saved !== undefined) return { value: parse(key, saved), from: 'config.json' }; 87 return { value: s.value, from: 'default' }; 88 } 89 90 export function get(key) { 91 return lookup(key).value; 92 } 93 94 /** Save a setting. An empty value removes it from the file, so the default applies again. */ 95 export function set(key, value) { 96 const clean = value == null || String(value).trim() === '' ? undefined : parse(key, value); 97 withLock(() => { 98 const all = readFile(); 99 if (clean === undefined) delete all[key]; 100 else all[key] = clean; 101 writeAtomic(configPath(), JSON.stringify(all, null, 2) + '\n'); 102 }); 103 return get(key); 104 } 105 106 /** Every setting, one line each, with its source. */ 107 export function describe() { 108 return Object.keys(SETTINGS) 109 .map((key) => { 110 const { value, from } = lookup(key); 111 return `${key.padEnd(17)} ${String(value === '' ? '(off)' : value).padEnd(28)} ${from} ยท ${SETTINGS[key].about}`; 112 }) 113 .join('\n'); 114 }