/** * Codex Link ("Codex Bağlama") — the pure half. * * AITURK IDE ships a small local Python bridge that lets the OpenAI Codex * installed on this machine use the IDE's models and combos. Turning the * feature on means editing a file the IDE does not own (Codex's `config.toml`) * and registering a login item, so every text transformation lives here as a * dependency-free, byte-preserving function that can be proven without * Electron, a filesystem, or a registry. `codex-link.ts` owns the decisions and * the side effects. * * Invariants: * - Only the line carrying OUR trailing comment is ever written or removed. * - An `openai_base_url` that is not ours means someone else's bridge is in * charge: report `foreign` and change nothing. * - Everything else in the file — including its line endings and BOM — comes * back byte for byte. */ export const CODEX_LINK_PORT = 8795 export const CODEX_LINK_ORIGIN = `http://127.0.0.1:${CODEX_LINK_PORT}` export const CODEX_LINK_STATUS_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama/status` export const CODEX_LINK_STOP_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama/stop` export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama/refresh` /** The trailing comment that marks the line as ours. */ export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama' // Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports // another version is restarted so an updated IDE never keeps an old bridge alive. export const CODEX_BRIDGE_VERSION = '7' export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}` export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' export const CODEX_LINK_RUN_VALUE = 'AITURK-IDE-Codex-Baglama' export const CODEX_LINK_LAUNCHER_NAME = 'baslat.pyw' // Windows documents Run-key command lines as "no longer than 260 characters". // A longer value is not reliably started at sign-in, so the direct command is // only used when it fits (see autostartPlan). export const CODEX_LINK_RUN_COMMAND_MAX = 260 /** Who owns the top-level `openai_base_url` in a Codex config. */ export type CodexConfigOwner = 'absent' | 'foreign' | 'ours' interface Line { eol: string text: string } const BOM = '\uFEFF' const KEY_PATTERN = /^[ \t]*(?:openai_base_url|"openai_base_url"|'openai_base_url')[ \t]*=/ // Split into lines WITHOUT losing anything: joining `text + eol` of every entry // reproduces the input exactly, whatever mix of \n, \r\n and \r it used. function splitLines(source: string): Line[] { const lines: Line[] = [] let start = 0 while (start < source.length) { let end = start while (end < source.length && source[end] !== '\n' && source[end] !== '\r') { end += 1 } let eol = '' if (end < source.length) { eol = source[end] === '\r' && source[end + 1] === '\n' ? '\r\n' : source[end] } lines.push({ eol, text: source.slice(start, end) }) start = end + eol.length } return lines } function joinLines(lines: Line[]): string { return lines.map(line => line.text + line.eol).join('') } function splitBom(source: string): { body: string; bom: string } { return source.startsWith(BOM) ? { body: source.slice(1), bom: BOM } : { body: source, bom: '' } } type MultilineQuote = "'''" | '"""' | null interface ValueState { depth: number multiline: MultilineQuote } // Index just past the `=` of a `key = value` line, or the line length when the // line holds no value (blank, comment, malformed). Quoted keys may contain `=`. function valueStart(text: string): number { let pos = 0 while (pos < text.length) { const char = text[pos] if (char === '#') { return text.length } if (char === '=') { return pos + 1 } if (char === '"' || char === "'") { pos += 1 while (pos < text.length && text[pos] !== char) { pos += char === '"' && text[pos] === '\\' ? 2 : 1 } } pos += 1 } return text.length } // Walk a value just far enough to know whether the NEXT line is still inside it // (multi-line string or array). That is the only TOML the top-level scan needs: // without it, text inside a multi-line value could pass for a key or a table // header. function scanValue(text: string, from: number, state: ValueState): ValueState { let { depth, multiline } = state let pos = from while (pos < text.length) { if (multiline) { if (multiline === '"""' && text[pos] === '\\') { pos += 2 continue } if (text.startsWith(multiline, pos)) { pos += 3 // Up to two quotes may sit directly before the closing delimiter. while (text[pos] === multiline[0]) { pos += 1 } multiline = null continue } pos += 1 continue } const char = text[pos] if (char === '#') { break } if (text.startsWith('"""', pos) || text.startsWith("'''", pos)) { multiline = text.slice(pos, pos + 3) as MultilineQuote pos += 3 continue } if (char === '"' || char === "'") { pos += 1 while (pos < text.length && text[pos] !== char) { pos += char === '"' && text[pos] === '\\' ? 2 : 1 } pos += 1 continue } if (char === '[' || char === '{') { depth += 1 } else if (char === ']' || char === '}') { depth = Math.max(0, depth - 1) } pos += 1 } return { depth, multiline } } // Indices of the top-level `openai_base_url` lines: those before the first // `[table]` header, outside comments and multi-line values. function topLevelEntries(lines: Line[]): { index: number; ours: boolean }[] { const entries: { index: number; ours: boolean }[] = [] let state: ValueState = { depth: 0, multiline: null } for (let index = 0; index < lines.length; index += 1) { const { text } = lines[index] let from = 0 if (!state.multiline && state.depth === 0) { if (text.trimStart().startsWith('[')) { break } if (KEY_PATTERN.test(text)) { // Ours by marker or by value: Codex may rewrite its config and drop the comment. entries.push({ index, ours: text.includes(CODEX_LINK_MARKER) || text.includes(`"${CODEX_LINK_ORIGIN}/backend-api/codex"`) }) } from = valueStart(text) } state = scanValue(text, from, state) } return entries } function ownerOf(entries: { ours: boolean }[]): CodexConfigOwner { if (entries.some(entry => !entry.ours)) { return 'foreign' } return entries.length > 0 ? 'ours' : 'absent' } /** Who owns the top-level `openai_base_url` of this config text. */ export function inspectCodexConfig(source: string): CodexConfigOwner { return ownerOf(topLevelEntries(splitLines(splitBom(source).body))) } export interface CodexConfigEdit { changed: boolean owner: CodexConfigOwner text: string } /** * Point Codex at the bridge. Idempotent; a foreign `openai_base_url` leaves the * text untouched and reports `foreign`. * * The line goes on the very first line: top-level keys must precede the first * table header, and the top of the file is the one position that is always * top-level without having to understand the values below it. */ export function addCodexLinkLine(source: string): CodexConfigEdit { const { body, bom } = splitBom(source) const lines = splitLines(body) const entries = topLevelEntries(lines) const owner = ownerOf(entries) if (owner === 'foreign') { return { changed: false, owner, text: source } } if (owner === 'ours') { const [first, ...duplicates] = entries if (duplicates.length === 0 && lines[first.index].text === CODEX_LINK_LINE) { return { changed: false, owner, text: source } } // A stale copy of our line (older address) or an accidental duplicate: // normalize to exactly one current line, in place. const drop = new Set(duplicates.map(entry => entry.index)) lines[first.index] = { ...lines[first.index], text: CODEX_LINK_LINE } return { changed: true, owner, text: bom + joinLines(lines.filter((_line, index) => !drop.has(index))) } } const eol = body.includes('\r\n') ? '\r\n' : '\n' return { changed: true, owner: 'ours', text: `${bom}${CODEX_LINK_LINE}${eol}${body}` } } /** Remove our line (and only ours). A foreign key is left exactly as it was. */ export function removeCodexLinkLine(source: string): CodexConfigEdit { const { body, bom } = splitBom(source) const lines = splitLines(body) const entries = topLevelEntries(lines) const drop = new Set(entries.filter(entry => entry.ours).map(entry => entry.index)) const remaining = ownerOf(entries.filter(entry => !entry.ours)) if (drop.size === 0) { return { changed: false, owner: remaining, text: source } } return { changed: true, owner: remaining, text: bom + joinLines(lines.filter((_line, index) => !drop.has(index))) } } // ── Preference file ───────────────────────────────────────────────────────── export interface CodexLinkSettings { enabled: boolean /** Every field of the stored object, so a rewrite keeps what it did not set. */ stored: Record } /** Missing, unreadable or malformed means ON: the feature is on by default. */ export function parseCodexLinkSettings(raw: null | string | undefined): CodexLinkSettings { if (typeof raw !== 'string') { return { enabled: true, stored: {} } } try { const parsed = JSON.parse(raw) if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { return { enabled: true, stored: {} } } return { enabled: parsed.enabled !== false, stored: parsed as Record } } catch { return { enabled: true, stored: {} } } } export function serializeCodexLinkSettings(stored: Record, enabled: boolean): string { const kapsam = typeof stored.kapsam === 'string' && stored.kapsam ? stored.kapsam : 'ide' return `${JSON.stringify({ ...stored, enabled, kapsam }, null, 2)}\n` } // ── Bridge configuration ──────────────────────────────────────────────────── /** `bridge.json`, rewritten on every apply. */ export function codexBridgeConfig() { return { port: CODEX_LINK_PORT, chatgpt_backend: 'https://chatgpt.com', routes: {}, nine_router_catalog: { enabled: false }, pano_kanca_user: false, ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 8, dongu_art_arda: 3 } } } export function serializeCodexBridgeConfig(): string { return `${JSON.stringify(codexBridgeConfig(), null, 2)}\n` } // ── Bridge command line ───────────────────────────────────────────────────── export interface CodexBridgeLaunch { bridgeHome: string bridgeScript: string hermesHome: string } /** Arguments after the interpreter — identical for the spawn and the login item. */ export function codexBridgeArgs({ bridgeHome, bridgeScript, hermesHome }: CodexBridgeLaunch): string[] { return [bridgeScript, '--home', bridgeHome, '--hermes-home', hermesHome] } // A trailing backslash would escape the closing quote (`"C:\dir\"`). function quoted(value: string): string { return `"${value.replace(/[\\/]+$/, '')}"` } /** `"" "" --home "" --hermes-home ""` */ export function codexBridgeCommandLine(python: string, launch: CodexBridgeLaunch): string { const [script, homeFlag, home, hermesFlag, hermes] = codexBridgeArgs(launch) return `${quoted(python)} ${quoted(script)} ${homeFlag} ${quoted(home)} ${hermesFlag} ${quoted(hermes)}` } /** * A tiny windowless launcher for the login item, used when the direct command * would not fit a Run value. It runs the bridge exactly as the direct command * would: same script, same arguments, the script's folder first on sys.path so * its helper modules import. */ export function codexBridgeLauncherSource(launch: CodexBridgeLaunch): string { const argv = JSON.stringify(codexBridgeArgs(launch)) return [ '# Written by AITURK IDE (Settings > Codex Baglama). Regenerated on every sync; do not edit.', 'import os', 'import runpy', 'import sys', '', `ARGV = ${argv}`, '', 'sys.argv = list(ARGV)', 'sys.path.insert(0, os.path.dirname(ARGV[0]))', "runpy.run_path(ARGV[0], run_name='__main__')", '' ].join('\n') } export interface CodexAutostartPlan { command: string /** Launcher file to write first; null when the direct command is used. */ launcher: { path: string; source: string } | null } /** * The login-item command. Prefer the direct bridge command; fall back to the * launcher when that exceeds the Run-value limit. Returns null when not even * the launcher form fits (nothing is registered rather than something that * silently never starts). */ export function codexAutostartPlan( python: string, launch: CodexBridgeLaunch, launcherPath: string ): CodexAutostartPlan | null { const direct = codexBridgeCommandLine(python, launch) if (direct.length <= CODEX_LINK_RUN_COMMAND_MAX) { return { command: direct, launcher: null } } const viaLauncher = `${quoted(python)} ${quoted(launcherPath)}` if (viaLauncher.length > CODEX_LINK_RUN_COMMAND_MAX) { return null } return { command: viaLauncher, launcher: { path: launcherPath, source: codexBridgeLauncherSource(launch) } } }