{"version":3,"sources":["../src/cli/utils/codex-config-toml.ts"],"sourcesContent":["/**\n * Idempotent merge of the LangWatch [otel] activation block into\n * ~/.codex/config.toml.\n *\n * Codex 0.130+ links the opentelemetry-otlp Rust SDK but its\n * exporter is gated on a `[otel]` block in `~/.codex/config.toml` —\n * env vars alone are a silent no-op. The Path B install flow needs\n * to write this block for the user so the drawer / CLI surface\n * can collapse to a single command.\n *\n * Why a handwritten merger and not a TOML library: the file may\n * contain valid TOML the user authored by hand, and we want to\n * preserve ordering + comments verbatim. The merger only ever\n * appends a marker-bracketed block at the end of the file and\n * regex-replaces the same block on re-runs. No structural rewrite\n * of the existing TOML.\n *\n * Marker comments:\n *   # >>> langwatch otel begin >>>\n *   …\n *   # <<< langwatch otel end <<<\n */\n\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\n\nconst BEGIN = \"# >>> langwatch otel begin >>>\";\nconst END = \"# <<< langwatch otel end <<<\";\n\nexport interface CodexOtelBlockInputs {\n  /** Full OTLP endpoint, e.g. https://app.langwatch.ai/api/otel */\n  endpoint: string;\n  /** Plaintext personal ingest key (sk-lw-<...>). */\n  ingestionToken: string;\n  /** Logical environment label (e.g. user@org). Lands on resource.deployment.environment.name. */\n  environment?: string;\n}\n\n/** Default config.toml path under the user's home directory. */\nexport function defaultCodexConfigPath(): string {\n  const codexHome = process.env.CODEX_HOME;\n  if (codexHome) return path.join(codexHome, \"config.toml\");\n  return path.join(os.homedir(), \".codex\", \"config.toml\");\n}\n\n/**\n * Build the bracketed [otel] + [otel.trace_exporter.otlp-http] block.\n * Returned WITH leading + trailing markers and a trailing newline.\n *\n * codex 0.137+ separates `trace_exporter` (spans) from `exporter`\n * (logs) in its config schema. We emit the trace_exporter form so\n * Path B span ingestion fires; the older `[otel.exporter.otlp-http]`\n * form is silently ignored on traces in the current schema.\n */\nexport function buildCodexOtelBlock(inputs: CodexOtelBlockInputs): string {\n  const env = inputs.environment ?? \"langwatch\";\n  // The header key is sent via OTEL_EXPORTER_OTLP_HEADERS at runtime so\n  // the toml block never persists the secret; the user only commits\n  // the endpoint + environment. We embed a note pointing at the env\n  // var so a reader of config.toml can audit the wiring.\n  return [\n    BEGIN,\n    `# Managed by 'langwatch ingest install codex'. Re-running the`,\n    `# command updates this block in place; remove the marker pair`,\n    `# above and below to opt back out.`,\n    `# Authorization header lives in OTEL_EXPORTER_OTLP_HEADERS;`,\n    `# this file persists only the endpoint + environment label.`,\n    \"[otel]\",\n    `environment = \"${env}\"`,\n    \"\",\n    \"[otel.trace_exporter.otlp-http]\",\n    `endpoint = \"${inputs.endpoint}\"`,\n    `protocol = \"json\"`,\n    END,\n    \"\",\n  ].join(\"\\n\");\n}\n\n/**\n * Merge result returned by writeCodexOtelBlock so callers can\n * report which action was taken without re-reading the file.\n */\nexport type CodexOtelWriteAction = \"created\" | \"updated\" | \"unchanged\";\n\nexport interface CodexOtelWriteResult {\n  action: CodexOtelWriteAction;\n  path: string;\n}\n\n/**\n * Idempotent merge into the codex config.toml. Behaviour:\n *\n * - If the file does not exist: create the parent dir if needed,\n *   write the block as the entire file contents.\n * - If the file exists with NO marker pair: append the block + a\n *   leading blank line so it doesn't fuse with the prior section.\n * - If the file exists WITH a marker pair: regex-replace the\n *   bracketed region. The replacement is byte-for-byte the same\n *   when the inputs haven't changed → returns 'unchanged'.\n */\nexport function writeCodexOtelBlock(\n  inputs: CodexOtelBlockInputs,\n  options: { filePath?: string } = {},\n): CodexOtelWriteResult {\n  const filePath = options.filePath ?? defaultCodexConfigPath();\n  const block = buildCodexOtelBlock(inputs);\n\n  if (!fs.existsSync(filePath)) {\n    fs.mkdirSync(path.dirname(filePath), { recursive: true });\n    fs.writeFileSync(filePath, block, { mode: 0o600 });\n    return { action: \"created\", path: filePath };\n  }\n\n  const prior = fs.readFileSync(filePath, \"utf8\");\n  const re = new RegExp(\n    `${escapeRe(BEGIN)}[\\\\s\\\\S]*?${escapeRe(END)}\\\\n?`,\n    \"m\",\n  );\n  if (re.test(prior)) {\n    const next = prior.replace(re, block);\n    if (next === prior) return { action: \"unchanged\", path: filePath };\n    fs.writeFileSync(filePath, next, { mode: 0o600 });\n    return { action: \"updated\", path: filePath };\n  }\n\n  const sep = prior.endsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\";\n  fs.writeFileSync(filePath, prior + sep + block, { mode: 0o600 });\n  return { action: \"updated\", path: filePath };\n}\n\nfunction escapeRe(s: string): string {\n  return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nconst GW_BEGIN = \"# >>> langwatch gateway begin >>>\";\nconst GW_END = \"# <<< langwatch gateway end <<<\";\n\nexport interface CodexGatewayBlockInputs {\n  /** Gateway base URL, e.g. https://gateway.langwatch.ai */\n  gatewayUrl: string;\n  /**\n   * Env var name codex should read the API key from. Defaults to\n   * OPENAI_API_KEY because that's the standard codex env. The\n   * wrapper still sets OPENAI_API_KEY to the user's VK before\n   * spawning codex, so this matches the wrapper's env injection\n   * out of the box.\n   */\n  envKey?: string;\n}\n\nexport interface CodexGatewayWriteResult {\n  action: CodexOtelWriteAction;\n  /**\n   * The ~/.codex/config.toml path that received the\n   * [model_providers.langwatch] block.\n   */\n  path: string;\n  /**\n   * The separate ~/.codex/<profile>.config.toml path that received\n   * the profile body. codex 0.134+ rejects [profiles.X] entries\n   * inside config.toml when the user passes --profile X, requiring\n   * a sibling file named <profile>.config.toml.\n   */\n  profilePath: string;\n  /**\n   * Result of the profile-file write. Independent of `action` so\n   * callers can report both writes accurately.\n   */\n  profileAction: CodexOtelWriteAction;\n  /**\n   * The profile name codex must be invoked with to actually route\n   * through the langwatch provider — e.g. `codex --profile\n   * langwatch-gateway`. Returned so the wrapper doesn't have to\n   * hardcode the name in two places.\n   */\n  profile: string;\n}\n\nconst PROFILE_NAME = \"langwatch-gateway\";\n\n/**\n * Build the additive [model_providers.langwatch] block that lives\n * in ~/.codex/config.toml. Codex 0.130+ defaults to ChatGPT OAuth\n * and ignores OPENAI_API_KEY unless an explicit model_provider\n * config is selected with `name = \"OpenAI\"`, `env_key`, and\n * `wire_api = \"responses\"` (the \"chat\" wire_api is no longer\n * supported per the codex binary strings dump).\n *\n * Codex 0.134+ rejects a [profiles.<name>] entry inside\n * config.toml when the user passes --profile <name>; the profile\n * body is now written to a sibling file (see buildCodexGatewayProfileFile).\n */\nexport function buildCodexGatewayBlock(\n  inputs: CodexGatewayBlockInputs,\n): string {\n  const envKey = inputs.envKey ?? \"OPENAI_API_KEY\";\n  const cleanedBase = inputs.gatewayUrl.replace(/\\/+$/, \"\");\n  const baseUrl = cleanedBase.endsWith(\"/v1\") ? cleanedBase : `${cleanedBase}/v1`;\n  return [\n    GW_BEGIN,\n    `# Managed by 'langwatch codex' (Path A wrapper). Re-running the`,\n    `# wrapper updates this block in place; remove the marker pair`,\n    `# above and below to opt back out.`,\n    `# The wrapper spawns codex with --profile ${PROFILE_NAME} so this`,\n    `# provider doesn't change codex's default model_provider.`,\n    `# The matching profile body lives at ~/.codex/${PROFILE_NAME}.config.toml`,\n    `# (codex 0.134+ requires the profile in a separate file).`,\n    `[model_providers.langwatch]`,\n    `name = \"OpenAI\"`,\n    `base_url = \"${baseUrl}\"`,\n    `env_key = \"${envKey}\"`,\n    `wire_api = \"responses\"`,\n    GW_END,\n    \"\",\n  ].join(\"\\n\");\n}\n\n/**\n * Build the contents of the sibling profile file\n * (~/.codex/langwatch-gateway.config.toml). The filename IS the\n * profile name; the body holds the settings that previously went\n * under [profiles.langwatch-gateway] inside config.toml.\n *\n * We DO NOT bracket this file with langwatch markers because the\n * file is entirely owned by langwatch — the wrapper creates it\n * fresh on every invocation. Hand-edits to it will be overwritten\n * (a header comment explains this to anyone reading the file).\n */\nexport function buildCodexGatewayProfileFile(): string {\n  return [\n    `# Managed by 'langwatch codex' (Path A wrapper).`,\n    `# This file is the body of the '${PROFILE_NAME}' codex profile,`,\n    `# selected at spawn time via 'codex --profile ${PROFILE_NAME}'.`,\n    `# The matching [model_providers.langwatch] entry lives in`,\n    `# ~/.codex/config.toml, bracketed by langwatch marker comments.`,\n    `# Re-running 'langwatch codex' regenerates this file in place;`,\n    `# remove it and the [model_providers.langwatch] block in`,\n    `# config.toml to opt back out.`,\n    `model_provider = \"langwatch\"`,\n    \"\",\n  ].join(\"\\n\");\n}\n\n/** Default path for the sibling profile file. */\nexport function defaultCodexProfilePath(profile: string = PROFILE_NAME): string {\n  const codexHome = process.env.CODEX_HOME;\n  const baseDir = codexHome ?? path.join(os.homedir(), \".codex\");\n  return path.join(baseDir, `${profile}.config.toml`);\n}\n\n/**\n * Idempotent merge of the gateway provider block into config.toml\n * + write of the sibling profile file. Both writes happen in one\n * call so the wrapper can't end up with a half-installed state.\n *\n * config.toml: regex-replace inside the marker pair or append. The\n * [otel] marker pair (Path B) coexists independently — a user who\n * runs both Path A and Path B keeps both blocks; only one fires per\n * invocation per the no-double-trace rule.\n *\n * <profile>.config.toml: full-file replace. The file is entirely\n * owned by langwatch.\n */\nexport function writeCodexGatewayBlock(\n  inputs: CodexGatewayBlockInputs,\n  options: { filePath?: string; profilePath?: string } = {},\n): CodexGatewayWriteResult {\n  const filePath = options.filePath ?? defaultCodexConfigPath();\n  const profilePath = options.profilePath ?? defaultCodexProfilePath();\n  const block = buildCodexGatewayBlock(inputs);\n  const profileBody = buildCodexGatewayProfileFile();\n\n  let action: CodexOtelWriteAction;\n  if (!fs.existsSync(filePath)) {\n    fs.mkdirSync(path.dirname(filePath), { recursive: true });\n    fs.writeFileSync(filePath, block, { mode: 0o600 });\n    action = \"created\";\n  } else {\n    const prior = fs.readFileSync(filePath, \"utf8\");\n    const re = new RegExp(\n      `${escapeRe(GW_BEGIN)}[\\\\s\\\\S]*?${escapeRe(GW_END)}\\\\n?`,\n      \"m\",\n    );\n    if (re.test(prior)) {\n      const next = prior.replace(re, block);\n      if (next === prior) {\n        action = \"unchanged\";\n      } else {\n        fs.writeFileSync(filePath, next, { mode: 0o600 });\n        action = \"updated\";\n      }\n    } else {\n      const sep = prior.endsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\";\n      fs.writeFileSync(filePath, prior + sep + block, { mode: 0o600 });\n      action = \"updated\";\n    }\n  }\n\n  let profileAction: CodexOtelWriteAction;\n  if (!fs.existsSync(profilePath)) {\n    fs.mkdirSync(path.dirname(profilePath), { recursive: true });\n    fs.writeFileSync(profilePath, profileBody, { mode: 0o600 });\n    profileAction = \"created\";\n  } else {\n    const priorProfile = fs.readFileSync(profilePath, \"utf8\");\n    if (priorProfile === profileBody) {\n      profileAction = \"unchanged\";\n    } else {\n      fs.writeFileSync(profilePath, profileBody, { mode: 0o600 });\n      profileAction = \"updated\";\n    }\n  }\n\n  return { action, path: filePath, profilePath, profileAction, profile: PROFILE_NAME };\n}\n\n/** Exported so callers + tests can reference the profile name from one place. */\nexport const CODEX_GATEWAY_PROFILE_NAME = PROFILE_NAME;\n"],"mappings":"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","names":[]}