1193 lines · 59.6 KB · JavaScript
RawDownload
'use strict';// lilt: Electron main process. Pure text helpers come first and are// exported so check.js can test them under plain Node. const fs = require('fs');const path = require('path');const os = require('os');const http = require('http');const https = require('https');const net = require('net');const { spawn, execFile } = require('child_process'); // ---------------------------------------------------------------- pure helpers const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');// Spaces in a phrase match any run of whitespace or light punctuation.const phrase = s => esc(s.trim()).replace(/\s+/g, '[\\s,.-]+'); function countWords(text) {  return (text || '').trim().split(/\s+/).filter(Boolean).length;} function wpm(words, ms) {  return ms > 0 ? Math.round(words / (ms / 60000)) : 0;} // Dictionary entries with `to` are spoken-form replacements ("btw" -> "by the way").function applyDictionary(text, dict) {  for (const d of dict || []) {    if (!d.to || !d.word || !d.word.trim()) continue;    text = text.replace(new RegExp(`(?<![\\p{L}\\p{N}])${phrase(d.word)}(?![\\p{L}\\p{N}])`, 'giu'), d.to);  }  return text;} // Replaces spoken snippet triggers with placeholder tokens so the LLM cannot// rewrite them; `restore` puts the snippet text back afterwards.function expandSnippets(text, snippets) {  const slots = [];  for (const s of snippets || []) {    if (!s.trigger || !s.trigger.trim()) continue;    const re = new RegExp(`(?<![\\p{L}\\p{N}])${phrase(s.trigger)}[.!?,]?(?![\\p{L}\\p{N}])`, 'giu');    text = text.replace(re, () => { slots.push(s.text); return `[[S${slots.length - 1}]]`; });  }  return { text, slots };} function restoreSnippets(text, slots) {  return text.replace(/\[\[S(\d+)\]\]/g, (_, i) => slots[+i] ?? '');} function appCategory(cls = '', title = '') {  const c = cls.toLowerCase(), t = title.toLowerCase();  if (/signal|telegram|discord|whatsapp|element|messages|vesktop|webcord|beeper/.test(c)) return 'personal';  if (/slack|teams|zulip|mattermost/.test(c)) return 'work';  if (/thunderbird|geary|evolution|mutt|mailspring|betterbird/.test(c) || /gmail|outlook|proton mail|inbox/.test(t)) return 'email';  return 'other';} const TERMINALS = ['st', 'st-256color', 'kitty', 'alacritty', 'foot', 'xterm', 'urxvt', 'wezterm', 'ghostty', 'konsole', 'gnome-terminal', 'gnome-terminal-server', 'xfce4-terminal', 'tilix', 'terminator'];function isTerminal(cls = '') {  const c = cls.toLowerCase();  return TERMINALS.includes(c) || /terminal|term$/.test(c);} const KEYS = { 9: 'Escape', 37: 'Left Ctrl', 105: 'Right Ctrl', 64: 'Left Alt', 108: 'Right Alt', 50: 'Left Shift', 62: 'Right Shift',  133: 'Left Super', 134: 'Right Super', 66: 'Caps Lock', 135: 'Menu', 65: 'Space', 95: 'F11', 96: 'F12', 118: 'Insert', 127: 'Pause', 78: 'Scroll Lock' };function keyLabel(code) {  if (KEYS[code]) return KEYS[code];  if (code >= 67 && code <= 76) return 'F' + (code - 66);  return 'Key ' + code;} // 16-bit mono PCM WAV from Float32 samples.function wavBuffer(f32, rate = 16000) {  const b = Buffer.alloc(44 + f32.length * 2);  b.write('RIFF', 0); b.writeUInt32LE(36 + f32.length * 2, 4); b.write('WAVE', 8);  b.write('fmt ', 12); b.writeUInt32LE(16, 16); b.writeUInt16LE(1, 20); b.writeUInt16LE(1, 22);  b.writeUInt32LE(rate, 24); b.writeUInt32LE(rate * 2, 28); b.writeUInt16LE(2, 32); b.writeUInt16LE(16, 34);  b.write('data', 36); b.writeUInt32LE(f32.length * 2, 40);  for (let i = 0; i < f32.length; i++) b.writeInt16LE(Math.round(Math.max(-1, Math.min(1, f32[i])) * 32767), 44 + i * 2);  return b;} // Whisper's markers for silence or noise, e.g. "[BLANK_AUDIO]" or "(music)".function stripMarkers(text) {  return (text || '').replace(/\[[^\]]*\]|\([^)]*(music|silence|blank|noise|inaudible)[^)]*\)/gi, '').replace(/\s+/g, ' ').trim();} const STYLE = {  formal: 'Use proper capitalization and full punctuation.',  casual: 'Use normal capitalization but lighter punctuation, like a relaxed message; drop the final period.',  veryCasual: 'Write in all lowercase with minimal punctuation, like a quick text.',};const EDIT = {  light: 'Clean it up: remove filler words (um, uh, like, you know), stutters and repeated words, apply self-corrections ("no wait, I mean X" keeps only X), fix punctuation, capitalization and obvious grammar. Keep every other word as spoken.',  medium: 'Rewrite it for clarity and concision: remove filler words, false starts and repetition, apply self-corrections ("no wait, I mean X" keeps only X), tighten wordy phrasing, fix grammar. Keep the speaker\'s meaning, point of view, every fact and name.',};function cleanupPrompt(level, style, category) {  return 'You are a dictation editor, not an assistant. You receive a transcript of something the user said, inside <dictation> tags, that they want typed into ' +    (category === 'other' ? 'an app' : `a ${category} app`) + `. ${EDIT[level] || EDIT.light} ${STYLE[style] || STYLE.formal} ` +    'If it is a question or a command, it stays a question or a command addressed to the same person. Never answer it, never add anything. ' +    'Tokens like [[S0]] are placeholders: keep them exactly as written. Output only the edited text, no tags.';}// Few-shot turns that pin the model to editing, shaped to the chosen style.const SHOTS = [  ['um can you uh send me the the report by friday', 'Can you send me the report by Friday?'],  ['find me a good restaurant near the office', 'Find me a good restaurant near the office.'],  ['so we meet tuesday no wait wednesday at like 3', 'So we meet Wednesday at 3.'],];function cleanupMessages(level, style, category, text) {  const shape = o => style === 'veryCasual' ? o.toLowerCase().replace(/\.$/, '') : style === 'casual' ? o.replace(/\.$/, '') : o;  const m = [{ role: 'system', content: cleanupPrompt(level, style, category) }];  for (const [a, b] of SHOTS) m.push({ role: 'user', content: `<dictation>${a}</dictation>` }, { role: 'assistant', content: shape(b) });  m.push({ role: 'user', content: `<dictation>${text}</dictation>` });  return m;} // Share of words in `out` that never appeared in `src`. Cleanup mostly deletes,// so a high value means the model answered or rewrote instead of editing.function drift(src, out) {  const w = t => t.toLowerCase().replace(/[^\p{L}\p{N}\s\[\]']/gu, ' ').split(/\s+/).filter(Boolean);  const have = new Set(w(src)), words = w(out);  return words.length ? words.filter(x => !have.has(x)).length / words.length : 0;} // What the assistant agent receives for one spoken (or typed) command.function assistantPrompt(text, win = {}, system = '') {  const w = win.cls ? `\n\nFocused window: ${win.cls}${win.title ? ` "${win.title}"` : ''}` : '';  const sys = system.trim() ? `Kris's standing instructions:\n${system.trim()}\n\n` : '';  return `First read ~/.config/agents/AGENTS.md and follow it; the other files in ~/.config/agents are yours to use.\n\n${sys}` +    `Voice command from Kris (speech to text, may contain recognition errors): ${text}${w}`;} // grok's streaming JSON: the answer is the last {"type":"result"} line. Plain text passes through.function cliResult(out) {  for (const l of out.trim().split('\n').reverse()) {    try { const j = JSON.parse(l); if (j && j.type === 'result') return String(j.result || ''); } catch {}  }  return out;} const P = { assistantPrompt, cliResult, countWords, wpm, applyDictionary, expandSnippets, restoreSnippets, appCategory, isTerminal, keyLabel, wavBuffer, stripMarkers, cleanupPrompt, cleanupMessages, drift };module.exports = P;if (process.type !== 'browser') return; // plain Node (check.js): stop here // ---------------------------------------------------------------- electron app const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, screen, clipboard, shell, session, globalShortcut, nativeTheme } = require('electron'); const VERSION = require('./package.json').version;const HOME = os.homedir();const DATA = path.join(HOME, '.local/share/lilt');// The app was called Flow at first; carry its data folder over once.const OLD_DATA = path.join(HOME, '.local/share/flow');if (!fs.existsSync(DATA) && fs.existsSync(OLD_DATA)) fs.renameSync(OLD_DATA, DATA);const MODELS = path.join(DATA, 'models');const AUDIO = path.join(DATA, 'audio');const DB = path.join(DATA, 'db.json');const VAD = path.join(MODELS, 'ggml-silero-v5.1.2.bin');const MCP_PORT = 7865;const OLLAMA = 'http://127.0.0.1:11434';const CLI = process.argv.indexOf('--transcribe');const ASK = process.argv.indexOf('--ask');const HF = 'https://huggingface.co'; const CATALOG = [  { id: 'large-v3-turbo', name: 'Whisper Ultra Turbo V3', engine: 'whisper', file: 'ggml-large-v3-turbo.bin', size: '1.6 GB', langs: '100+', url: `${HF}/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin` },  { id: 'large-v3', name: 'Whisper Ultra V3', engine: 'whisper', file: 'ggml-large-v3.bin', size: '3.1 GB', langs: '100+', url: `${HF}/ggerganov/whisper.cpp/resolve/main/ggml-large-v3.bin` },  { id: 'parakeet-v3', name: 'Parakeet V3 (NVIDIA)', engine: 'parakeet', file: 'ggml-parakeet-tdt-0.6b-v3-f16.bin', size: '1.3 GB', langs: '25', url: `${HF}/ggml-org/parakeet-GGUF/resolve/main/ggml-parakeet-tdt-0.6b-v3-f16.bin` },  { id: 'small', name: 'Whisper Standard', engine: 'whisper', file: 'ggml-small.bin', size: '488 MB', langs: '100+', url: `${HF}/ggerganov/whisper.cpp/resolve/main/ggml-small.bin` },]; const DEFAULTS = {  theme: 'system', firstName: (n => n.charAt(0).toUpperCase() + n.slice(1))(os.userInfo().username), lastName: '', email: '',  hotkey: { keycode: 108, label: 'Right Alt' }, micId: '', micLabel: '', language: 'en',  model: 'large-v3-turbo', llm: 'ollama', llmModel: 'qwen3.5:4b', cleanup: 'light',  styles: { personal: 'casual', work: 'formal', email: 'formal', other: 'casual' },  transformsOptIn: true, autoTransform: { enabled: false, id: 'polish' },  launchAtLogin: false, showBarAlways: true, showInDock: true, sounds: true, muteWhileDictating: false,  scratchpadInBar: false, privacy: { contextAwareness: true, storeAudio: true },  notetaker: { notifyBefore: 15, showNextMeeting: true, autoDetect: true, autoStart: false, maxMinutes: 120,    stopOnCallEnd: true, hideFromCapture: true, notepadView: 'open', liveTranscript: true,    calendarNotice: 'off', consentNotice: false, linkSharing: 'anyone', autoShareWho: 'no one', autoShareFor: 'all meetings' },  vibe: { variableRecognition: false, fileTagging: true },  assistant: { enabled: true, model: 'claude-sonnet', hotkey: { keycode: 50, label: 'Left Shift' }, systemPrompt: '' },  dismissed: {},}; const newId = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6); function seed() {  const t = Date.now();  const d = (word, extra) => ({ id: newId(), word, ts: t, ...extra });  return {    settings: structuredClone(DEFAULTS),    history: [],    dictionary: [d('btw', { to: 'by the way' })],    snippets: [      { id: newId(), trigger: 'organize thoughts prompt', text: 'Organize these unstructured thoughts into a clear, polished version without changing the meaning.', ts: t },    ],    transforms: [      { id: 'polish', name: 'Polish', desc: 'Improve clarity and conciseness', key: '1',        prompt: 'Rewrite the text to be clearer and more concise. Fix grammar and flow, keep the meaning, tone and any names or numbers. Output only the rewritten text.' },      { id: 'prompt-engineer', name: 'Prompt Engineer', desc: 'Constructs optimal prompts', key: '2',        prompt: 'Turn the text into a well-structured prompt for an AI assistant: state the goal, the relevant context, constraints and the expected output format. Keep every detail the user gave. Output only the prompt.' },    ],    notes: [],    meetings: [],    commands: [],  };} // Fills in settings keys added after the db was created, without touching user values.function merge(def, cur) {  if (!cur || typeof cur !== 'object' || Array.isArray(cur)) return structuredClone(def);  for (const k of Object.keys(def)) {    const dv = def[k];    if (dv && typeof dv === 'object' && !Array.isArray(dv)) cur[k] = merge(dv, cur[k]);    else if (!(k in cur)) cur[k] = dv;  }  return cur;} let db;function load() {  fs.mkdirSync(AUDIO, { recursive: true });  fs.mkdirSync(MODELS, { recursive: true });  if (fs.existsSync(DB)) {    try { db = JSON.parse(fs.readFileSync(DB, 'utf8')); }    catch { fs.renameSync(DB, `${DB}.bad-${Date.now()}`); } // keep a corrupt file instead of overwriting it  }  if (!db) db = seed();  const s = seed();  for (const k of ['history', 'dictionary', 'snippets', 'transforms', 'notes', 'meetings', 'commands']) if (!Array.isArray(db[k])) db[k] = s[k];  db.settings = merge(DEFAULTS, db.settings);  // commands from before the log file existed, oldest first  if (CLI < 0 && db.commands.length && !fs.existsSync(ASSIST_LOG))    fs.writeFileSync(ASSIST_LOG, db.commands.filter(c => c.reply).reverse().map(c => JSON.stringify(c) + '\n').join(''));} let saveTimer = null;function writeNow() {  clearTimeout(saveTimer); saveTimer = null;  if (CLI > -1) return; // the --transcribe test mode never writes  fs.writeFileSync(DB + '.tmp', JSON.stringify(db));  fs.renameSync(DB + '.tmp', DB);}function save() { clearTimeout(saveTimer); saveTimer = setTimeout(writeNow, 300); } // ---------------------------------------------------------------- runtime + push const rt = { version: VERSION, dataDir: DATA, mcpUrl: `http://127.0.0.1:${MCP_PORT}/mcp`, engine: 'off', engineError: '', llm: 'off',  recording: false, recordingKind: 'dictate', processing: false, notetaker: false, assistant: false };const progress = {}; // model id -> 0..1 while downloading function models() {  return CATALOG.map(m => ({ id: m.id, name: m.name, engine: m.engine, size: m.size, langs: m.langs,    installed: fs.existsSync(path.join(MODELS, m.file)), progress: progress[m.id] }));}function runtime() {  return { ...rt, hotkeyLabel: db.settings.hotkey.label, models: models(), assistantDir: ASSIST_DIR, assistantLog: ASSIST_LOG,    assistantDirOk: fs.existsSync(path.join(ASSIST_DIR, 'AGENTS.md')),    assistantModels: ASSISTANTS.map(({ id, name, cli, desc }) => ({ id, name, cli, desc, available: !!installed[cli] })) };}function state() { return { ...db, runtime: runtime() }; } let hub = null, bar = null, tray = null, trayKey = '', quitting = false, pushTimer = null;function push() {  clearTimeout(pushTimer);  pushTimer = setTimeout(() => {    if (hub && !hub.isDestroyed() && !hub.webContents.isCrashed()) hub.webContents.send('state', state());    barSend({ type: 'state', ...runtime(), settings: { scratchpadInBar: db.settings.scratchpadInBar, sounds: db.settings.sounds, showBarAlways: db.settings.showBarAlways } });    updateBarVisibility();    const key = `${!!rec}${!!nt}${!!assist}`;    if (tray && key !== trayKey) { trayKey = key; tray.setContextMenu(trayMenu()); }  }, 50);}const changed = () => { save(); push(); };const barSend = msg => { if (bar && !bar.isDestroyed() && !bar.webContents.isCrashed()) bar.webContents.send('bar', msg); }; // ---------------------------------------------------------------- process helpers function run(cmd, args, opts = {}) {  return new Promise(resolve => {    execFile(cmd, args, { timeout: 15000, maxBuffer: 64 << 20, ...opts }, (err, stdout, stderr) =>      resolve({ ok: !err, out: (stdout || '').toString(), err: (stderr || (err && err.message) || '').toString() }));  });} function freePort() {  return new Promise(res => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => res(p)); }); });} function request(url, { method = 'GET', body, headers = {}, timeout = 0 } = {}) {  return new Promise((resolve, reject) => {    const req = http.request(url, { method, headers, timeout }, res => {      const chunks = [];      res.on('data', c => chunks.push(c));      res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString() }));    });    req.on('error', reject);    req.on('timeout', () => req.destroy(new Error('timeout')));    if (body) req.write(body);    req.end();  });} const sleep = ms => new Promise(r => setTimeout(r, ms)); // ---------------------------------------------------------------- speech engine let whisper = null, whisperPort = 0, whisperReady = null, whisperFails = 0; function currentModel() { return CATALOG.find(m => m.id === db.settings.model) || CATALOG[0]; } function stopWhisper() {  if (whisper) { const w = whisper; whisper = null; w.kill(); }} async function startEngine() {  stopWhisper();  const m = currentModel(), file = path.join(MODELS, m.file);  if (!fs.existsSync(file)) { rt.engine = 'off'; rt.engineError = `${m.name} is not downloaded`; push(); return; }  if (m.engine === 'parakeet') { rt.engine = 'ready'; rt.engineError = ''; push(); return; }  rt.engine = 'loading'; rt.engineError = ''; push();  whisperPort = await freePort();  const args = ['-m', file, '--host', '127.0.0.1', '--port', String(whisperPort), '-dev', '0', '-t', '8'];  if (fs.existsSync(VAD)) args.push('--vad', '-vm', VAD);  const child = spawn('whisper-server', args, { stdio: ['ignore', 'ignore', 'pipe'] });  whisper = child;  let tail = '';  child.stderr.on('data', d => { tail = (tail + d).slice(-2000); });  child.on('error', e => { rt.engine = 'error'; rt.engineError = e.message; push(); });  child.on('exit', code => {    if (whisper !== child) return; // replaced on purpose    whisper = null;    rt.engine = 'error'; rt.engineError = `whisper-server exited (${code}): ${tail.trim().split('\n').slice(-3).join(' ')}`; push();    if (++whisperFails <= 3 && !quitting) setTimeout(startEngine, 3000);  });  whisperReady = (async () => {    for (let i = 0; i < 240 && whisper === child; i++) {      try { if ((await request(`http://127.0.0.1:${whisperPort}/`, { timeout: 1000 })).status === 200) { rt.engine = 'ready'; whisperFails = 0; push(); return true; } } catch {}      await sleep(250);    }    return false;  })();} async function transcribeFile(file, { prompt = true } = {}) {  const m = currentModel();  if (m.engine === 'parakeet') {    const r = await run('parakeet-cli', ['-np', '-m', path.join(MODELS, m.file), '-f', file], { timeout: 600000 });    if (!r.ok) throw new Error(r.err.trim().split('\n').pop());    return stripMarkers(r.out);  }  if (!whisper) await startEngine();  if (!(await whisperReady)) throw new Error(rt.engineError || 'speech engine not ready');  const words = prompt ? db.dictionary.filter(d => !d.to).map(d => d.word).join(', ') : '';  const fields = { temperature: '0', response_format: 'json', language: db.settings.language || 'en' };  if (words) fields.prompt = words;  const boundary = '----lilt' + newId();  const parts = Object.entries(fields).map(([k, v]) => Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${k}"\r\n\r\n${v}\r\n`));  parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="a.wav"\r\nContent-Type: audio/wav\r\n\r\n`),    fs.readFileSync(file), Buffer.from(`\r\n--${boundary}--\r\n`));  const body = Buffer.concat(parts);  const r = await request(`http://127.0.0.1:${whisperPort}/inference`, { method: 'POST', body,    headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': body.length } });  if (r.status !== 200) throw new Error(`whisper-server ${r.status}: ${r.body.slice(0, 200)}`);  return stripMarkers(JSON.parse(r.body).text);} // ---------------------------------------------------------------- language model let ollamaChild = null; async function ollamaUp() {  try { return (await request(`${OLLAMA}/api/tags`, { timeout: 1500 })).status === 200; } catch { return false; }} // Cloud models through the CLIs already signed in on this machine. Each call runs// headless from an empty folder with tools, hooks, MCP and settings off, so the// CLI only rewrites text. Prompts go over stdin (meeting transcripts outgrow argv).const CLOUD = {  'claude-haiku': {    bin: 'claude', name: 'Claude Haiku 5.5',    args: sys => ['-p', '--model', 'claude-haiku-5-5', '--effort', 'low', '--system-prompt', sys, '--tools', '',      '--no-session-persistence', '--setting-sources', '', '--strict-mcp-config', '--disable-slash-commands', '--output-format', 'text'],  },  'codex-luna': {    // service_tier "fast" is Codex fast mode; the -fast model ids are refused on ChatGPT-account logins.    bin: 'codex', name: 'GPT-5.6 Luna', file: true,    args: (sys, out) => ['exec', '-m', 'gpt-5.6-luna', '-c', 'model_reasoning_effort="low"', '-c', 'service_tier="fast"',      '--skip-git-repo-check', '--ephemeral', '-s', 'read-only', '--color', 'never', '-o', out, '-'],  },};const CLI_DIR = path.join(DATA, 'cli'); // scratch for CLI output files, never /tmp function cliEnv() {  const env = { ...process.env };  delete env.ELECTRON_RUN_AS_NODE; delete env.CLAUDECODE; delete env.CLAUDE_CODE_ENTRYPOINT;  // codex keeps its login where ~/.profile points it; lilt started from a launcher never read that file  if (!env.CODEX_HOME && fs.existsSync(path.join(HOME, '.config/codex'))) env.CODEX_HOME = path.join(HOME, '.config/codex');  env.PATH = [path.join(HOME, '.local/bin'), path.join(HOME, '.npm-global/bin'), env.PATH].join(':');  return env;} // Flatten system + few-shot turns into one prompt for a CLI.function flatten(messages) {  const sys = messages[0].role === 'system' ? messages[0].content : '';  const turns = messages.slice(sys ? 1 : 0);  const last = turns.pop().content;  let ex = '';  for (let i = 0; i + 1 < turns.length; i += 2) ex += `${turns[i].content}\n<output>${turns[i + 1].content}</output>\n\n`;  const prompt = ex ? `Examples of input and the exact output expected:\n\n${ex}Now the real input. Reply with only the output text, no tags:\n\n${last}` : last;  return { sys, prompt };} function cloudChat(cfg, messages) {  const { sys, prompt } = flatten(messages);  fs.mkdirSync(CLI_DIR, { recursive: true });  const out = path.join(CLI_DIR, `${newId()}.txt`);  // codex has no system-prompt flag, so the instructions lead the prompt  const input = cfg.file ? `${sys}\n\n${prompt}` : prompt;  return new Promise((resolve, reject) => {    const child = spawn(cfg.bin, cfg.args(sys, out), { cwd: CLI_DIR, env: cliEnv(), stdio: ['pipe', 'pipe', 'pipe'] });    let so = '', se = '';    const timer = setTimeout(() => { child.kill(); reject(new Error(`${cfg.name} timed out`)); }, 60000);    child.stdout.on('data', d => { so += d; });    child.stderr.on('data', d => { se = (se + d).slice(-2000); });    child.on('error', e => { clearTimeout(timer); reject(e); });    child.on('close', code => {      clearTimeout(timer);      let text = so;      if (cfg.file) { try { text = fs.readFileSync(out, 'utf8'); } catch { text = ''; } fs.rmSync(out, { force: true }); }      text = text.replace(/<\/?output>/g, '').trim();      if (code !== 0 || !text) return reject(new Error(`${cfg.name} failed (${code}): ${se.trim().slice(-300)}`));      resolve(text);    });    child.stdin.end(input);  });} async function startLLM() {  rt.llm = 'loading'; push();  const cloud = CLOUD[db.settings.llm];  if (cloud) {    const r = await run('sh', ['-c', `command -v ${cloud.bin}`], { env: cliEnv() });    rt.llm = r.ok ? 'ready' : 'off'; push(); return;  }  if (!(await ollamaUp())) {    ollamaChild = spawn('ollama', ['serve'], { stdio: 'ignore' });    ollamaChild.on('error', () => { ollamaChild = null; });    for (let i = 0; i < 40 && !(await ollamaUp()); i++) await sleep(250);  }  if (!(await ollamaUp())) { rt.llm = 'off'; push(); return; }  const model = db.settings.llmModel;  const tags = JSON.parse((await request(`${OLLAMA}/api/tags`)).body).models || [];  if (!tags.some(t => t.name === model || t.name === model + ':latest')) {    const r = await run('ollama', ['pull', model], { timeout: 0 });    if (!r.ok) { rt.llm = 'off'; push(); return; }  }  try { await chat('Reply with OK.', 'OK'); rt.llm = 'ready'; } catch { rt.llm = 'off'; }  push();} // chat(system, user) or chat(messagesArray)async function chat(system, user, opts = {}) {  const messages = Array.isArray(system) ? system : [{ role: 'system', content: system }, { role: 'user', content: user }];  if (Array.isArray(system)) opts = user || {};  if (CLOUD[db.settings.llm]) return cloudChat(CLOUD[db.settings.llm], messages);  const body = JSON.stringify({ model: db.settings.llmModel, think: false, stream: false, keep_alive: '30m',    options: { temperature: 0.2, ...opts }, messages });  const r = await request(`${OLLAMA}/api/chat`, { method: 'POST', body, headers: { 'Content-Type': 'application/json' }, timeout: 300000 });  if (r.status !== 200) throw new Error(`ollama ${r.status}: ${r.body.slice(0, 200)}`);  return JSON.parse(r.body).message.content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();} // ---------------------------------------------------------------- text pipeline // raw transcript -> final text. Any LLM failure keeps the deterministic result.async function finish(raw, win = {}) {  const s = db.settings;  const cat = s.privacy.contextAwareness ? appCategory(win.cls, win.title) : 'other';  let text = applyDictionary(raw, db.dictionary);  const { text: held, slots } = expandSnippets(text, db.snippets);  const bare = held.replace(/\[\[S\d+\]\]/g, '').replace(/[\s.,!?]/g, '');  let out = held;  if (s.cleanup !== 'none' && bare && rt.llm === 'ready') {    try {      const c = await chat(cleanupMessages(s.cleanup, s.styles[cat], cat, held));      const ok = c && slots.every((_, i) => c.includes(`[[S${i}]]`)) && drift(held, c) <= (s.cleanup === 'medium' ? 0.6 : 0.35);      if (ok) out = c; else console.error('cleanup rejected:', JSON.stringify(c));    } catch (e) { console.error('cleanup failed:', e.message); }  }  const at = s.autoTransform, tf = at.enabled && db.transforms.find(t => t.id === at.id);  if (tf && bare && rt.llm === 'ready') {    try {      const c = await chat(transformPrompt(tf) + ' Keep tokens like [[S0]] exactly as written.', `<text>${out}</text>`);      if (c && slots.every((_, i) => c.includes(`[[S${i}]]`))) out = c;

Lines 1–500 of 1193.