src/config.js (4119 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 // The token of the hub is never a setting: it comes only from IDEAMINE_COOP_TOKEN. 56 coop_url: { 57 env: 'IDEAMINE_COOP_URL', 58 value: '', 59 about: 'coop hub whose agents build the ideas dropped in Doing (empty: off; needs IDEAMINE_COOP_TOKEN)', 60 }, 61 coop_session: { env: 'IDEAMINE_COOP_SESSION', value: 'workers', about: 'coop session of those agents' }, 62 }; 63 64 const configPath = () => path.join(home(), 'config.json'); 65 66 function readFile() { 67 try { 68 return JSON.parse(fs.readFileSync(configPath(), 'utf8')); 69 } catch { 70 return {}; 71 } 72 } 73 74 function setting(key) { 75 const s = SETTINGS[key]; 76 if (!s) throw new Error(`unknown setting "${key}" (use: ${Object.keys(SETTINGS).join(', ')})`); 77 return s; 78 } 79 80 function parse(key, raw) { 81 if (typeof setting(key).value !== 'number') return String(raw).trim(); 82 const n = Number(raw); 83 if (String(raw).trim() === '' || !Number.isFinite(n) || n < 0 || n > 1) throw new Error(`${key} must be a number from 0 to 1`); 84 return n; 85 } 86 87 /** The value and where it comes from: env, file, or default. */ 88 function lookup(key) { 89 const s = setting(key); 90 const env = process.env[s.env]; 91 if (env !== undefined && env !== '') return { value: parse(key, env), from: s.env }; 92 const saved = readFile()[key]; 93 if (saved !== undefined) return { value: parse(key, saved), from: 'config.json' }; 94 return { value: s.value, from: 'default' }; 95 } 96 97 export function get(key) { 98 return lookup(key).value; 99 } 100 101 /** Save a setting. An empty value removes it from the file, so the default applies again. */ 102 export function set(key, value) { 103 const clean = value == null || String(value).trim() === '' ? undefined : parse(key, value); 104 withLock(() => { 105 const all = readFile(); 106 if (clean === undefined) delete all[key]; 107 else all[key] = clean; 108 writeAtomic(configPath(), JSON.stringify(all, null, 2) + '\n'); 109 }); 110 return get(key); 111 } 112 113 /** Every setting, one line each, with its source. */ 114 export function describe() { 115 return Object.keys(SETTINGS) 116 .map((key) => { 117 const { value, from } = lookup(key); 118 return `${key.padEnd(17)} ${String(value === '' ? '(off)' : value).padEnd(28)} ${from} ยท ${SETTINGS[key].about}`; 119 }) 120 .join('\n'); 121 }