# Project export: Kaikei

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Turns bank reconciliation from hours of spreadsheet hunting into a guided, auditable desktop workflow.
- Devpost: https://devpost.com/software/kaikei
- GitHub: https://github.com/mauroentey/kaikei
- Video: https://www.youtube.com/embed/hCSCFhjmvX8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Mauricio Samper (7 commits)

## Devpost submission (written by the team)

### Overview

Kaikei is an AI-assisted bank reconciliation desktop app for macOS and Windows. It compares an accounting ledger with one or more bank statements, finds direct and grouped matches, identifies discrepancies, and exports an audit-ready report. A deterministic local engine handles the arithmetic; GPT-5.6 reviews ambiguous exceptions through Codex App Server and returns schema-constrained JSON. Kaikei uses the user's ChatGPT session instead of requiring an API key and keeps the accountant in control of every suggested adjustment.

### Inspiration

Bank reconciliation is essential, but for many small businesses and accounting teams it is still a monthly exercise in copying data between banking portals, accounting software and spreadsheets. Equal values may occur on different dates, one bank movement may correspond to several ledger entries, and real exceptions—fees, duplicates, deposits in transit or unrecorded transactions—are hidden among hundreds of normal rows. I wanted to build a tool for the person who must explain and sign off on the reconciliation, not only produce a match percentage. Kaikei was inspired by the Colombian accounting workflow, where evidence, traceability and professional review matter as much as speed. The goal was a desktop experience that accountants could understand immediately, without deploying a server, configuring an API key or surrendering the final decision to a black box.

### What it does

Kaikei lets the user load one accounting ledger and one or more bank statements in XLSX, CSV, OFX, QFX or PDF format. It detects likely columns, lets the accountant correct the mapping, and normalizes dates, values, descriptions, references, debits and credits. A deterministic reconciliation engine first searches for auditable 1:1 and grouped 1:N/N:1 matches using amount, sign, date windows and references. GPT-5.6 then reviews the remaining exceptions through Codex App Server, looking for patterns, risks and explainable findings. Its response is constrained by JSON Schema and validated again locally with Zod before it reaches the interface. The result is a complete reconciliation workspace with: matched and unmatched transactions; book and bank differences; duplicate and anomaly findings; suggested follow-up controls and accounting adjustments; charts and reconciliation metrics; and exportable Excel, executive PDF and JSON reports. Kaikei includes tailored context for Colombian private companies, nonprofit entities and public-sector organizations. It never posts an accounting entry automatically: suggested adjustments remain subject to evidence, review and approval.

### How we built it

Kaikei is a cross-platform Electron application with a React and TypeScript interface. The Electron main process parses financial files locally, isolates file-system access from the renderer, and runs the deterministic matching engine. The renderer receives only normalized previews and report data. For AI analysis, Electron starts codex app-server over stdio. account/read reuses the user's current ChatGPT/Codex session, while account/login/start handles sign-in when required. The app creates an ephemeral, read-only thread and sends only the normalized movements, deterministic candidates and reconciliation rules. turn/start.outputSchema constrains the final GPT-5.6 response, and Zod performs a second local validation before the report is accepted. Codex was also the development collaborator for the project. Starting from a plain-language accounting workflow, Codex helped research the Colombian reconciliation context, translate it into product requirements, design the Electron/App Server architecture, implement the parsers and matching engine, build the interface, create tests, diagnose packaged-app issues, produce installers and prepare the documentation. Mauricio made the core product decisions: use the user's ChatGPT login instead of an API key, keep file handling local, combine deterministic matching with AI reasoning, and require human review for every adjustment.

### Challenges we ran into

The first challenge was that bank and accounting files are inconsistent. The same concept can appear as a signed value, separate debit and credit columns, localized dates, or a PDF text table. We addressed this with explicit normalization, editable column mapping and format-specific parsing. The second challenge was avoiding a reconciliation that merely “looks intelligent.” Matching must remain explainable and numerically reliable. We therefore separated responsibilities: deterministic code proposes matches and calculates totals; GPT-5.6 focuses on ambiguous exceptions, patterns and narrative findings. Structured AI output was another challenge. A report screen cannot depend on loosely formatted prose, so we use JSON Schema at generation time and Zod validation at the application boundary. Finally, packaging a secure cross-platform Electron app while communicating with Codex App Server required careful process management, sandboxing, preload isolation, login-state handling and real packaged-app testing on macOS.

### Accomplishments we're proud of

We shipped a complete, installable desktop product rather than a chat mockup or isolated proof of concept. Kaikei works without a custom backend and without asking the user for an OpenAI API key. The hybrid engine keeps arithmetic deterministic while using GPT-5.6 where language understanding and exception reasoning add the most value. Every AI report is schema-constrained, locally validated and presented for human review. The app supports five common financial-file formats, grouped matching, charts and three export formats. The interface includes consent, security boundaries and clear professional-accountability language. The repository includes sample data, Colombian reconciliation research, automated tests and reproducible packaging instructions.

### What we learned

We learned that the strongest use of an advanced model in accounting is not to replace deterministic controls, but to sit on top of them. Code should establish totals, candidate matches and invariants; GPT-5.6 should explain the long tail of exceptions and turn raw discrepancies into actionable review. We also learned that structured output changes what is possible. Once the model response is treated as a typed application boundary instead of chat text, AI analysis can safely drive dashboards, exports and workflow states. Most importantly, Codex proved useful beyond code generation. It helped move continuously between domain research, architecture, UX, implementation, testing, packaging and communication while Mauricio retained the product and accounting decisions.

### What's next

The next milestone is a signed and notarized production release for macOS and Windows. From there, Kaikei will add reusable templates for more Colombian banks and accounting systems, persistent reconciliation history, multi-account batch processing, reviewer approval workflows and stronger audit trails. We also plan to add direct integrations with accounting platforms, configurable organization policies, richer evidence attachments and bilingual Spanish/English reporting. Longer term, Kaikei can become a general financial close assistant: not only identifying differences, but preserving the reasoning, evidence and approvals behind every resolved exception.

## README (from the GitHub repository)

# Kaikei

[English version](README.en.md)

Aplicación Electron para macOS y Windows que concilia un auxiliar contable contra uno o varios extractos bancarios. Usa la sesión de ChatGPT mediante **Codex App Server**; no solicita una API key.

> Creado y mantenido por **Mauricio Samper** — Bogotá, Colombia<br>
> Contacto: [mauro@entey.net](mailto:mauro@entey.net)

<p align="center">
  <img src="assets/logo-kaikei.png" alt="Logo de Kaikei" width="180">
</p>

## OpenAI Build Week 2026

**Track:** Work & Productivity<br>
**Tagline:** From two financial records to one trusted answer.<br>
**Submission draft:** [Elevator pitch, Devpost story and demo script](docs/BUILDWEEK_SUBMISSION.md)

Kaikei was created during OpenAI Build Week from a plain-language accounting workflow. It is a working Electron product—not a chat mockup—with local financial-file parsing, deterministic reconciliation, GPT-5.6 exception analysis, schema-constrained output, dashboards and exportable reports.

### Cómo usamos Codex para crear Kaikei

Codex fue el colaborador de ingeniería durante todo el proyecto. Partimos de una descripción en lenguaje natural del proceso de conciliación bancaria y trabajamos de forma iterativa: Mauricio definía el problema, las restricciones contables y las decisiones de producto; Codex inspeccionaba el repositorio, proponía el siguiente cambio, implementaba el código y verificaba el resultado con pruebas, typecheck y builds ejecutables.

En concreto, usamos Codex para investigar el contexto colombiano de conciliación, convertir el flujo contable en requisitos, diseñar la arquitectura con Electron y Codex App Server, implementar los parsers y el motor de cruces, construir la interfaz, escribir pruebas automatizadas, diagnosticar fallos de la aplicación empaquetada, generar instaladores y preparar la documentación y los materiales de la demo.

Mauricio Samper tomó las decisiones clave: reutilizar la sesión de ChatGPT del usuario en vez de pedir una API key; mantener el procesamiento de archivos local; usar reglas determinísticas para la aritmética y los cruces; reservar GPT-5.6 para excepciones ambiguas y hallazgos; y exigir evidencia, revisión y aprobación humana para cada ajuste sugerido. Codex aceleró la investigación, la implementación y la validación, sin reemplazar esas decisiones de producto ni el criterio contable.

Codex también forma parte del producto terminado. Kaikei ejecuta GPT-5.6 mediante `codex app-server` en un thread efímero y de solo lectura. El modelo recibe movimientos normalizados y candidatos determinísticos; su reporte final debe cumplir `turn/start.outputSchema`, y Zod vuelve a validarlo antes de que la interfaz o los exportadores puedan consumirlo.

Los jueces pueden usar los dos XLSX sintéticos incluidos: [`auxiliar_contable_sintetico_kaikei.xlsx`](outputs/019f86b6-372d-7103-912c-6632663ce348/auxiliar_contable_sintetico_kaikei.xlsx) y [`extracto_bancario_sintetico_kaikei.xlsx`](outputs/019f86b6-372d-7103-912c-6632663ce348/extracto_bancario_sintetico_kaikei.xlsx). Contienen 30 y 31 movimientos ficticios, respectivamente, con cruces directos, tres agrupaciones, una diferencia de valor, un duplicado, comisión bancaria, GMF, intereses, consignación en tránsito y cheque pendiente. El historial de commits y el thread principal de Codex documentan el trabajo realizado durante el periodo de Build Week.

## Qué incluye

- Inicio de sesión administrado por `codex app-server` y apertura del OAuth de ChatGPT en el navegador.
- Lectura local de XLSX, CSV, OFX, QFX y PDF.
- Detección y corrección manual del mapeo de fecha, descripción, referencia, valor, débito, crédito, tipo y saldo.
- Motor auditable de cruces por valor, signo, ventana de fechas, referencia y agrupaciones 1:N/N:1.
- Revisión de excepciones con Codex y salida validada contra JSON Schema.
- Dashboard de resultados, partidas pendientes, hallazgos, controles y ajustes sugeridos.
- Exportación final a Excel, PDF ejecutivo y JSON.
- Tratamiento diferenciado para empresas privadas, ESAL y entidades públicas.

## Capturas

### Inicio

![Pantalla de inicio de Kaikei](docs/screenshots/inicio.png)

### Carga y preparación de archivos

![Carga del auxiliar contable y los extractos bancarios](docs/screenshots/carga-archivos.png)

### Reporte de conciliación

![Dashboard de conciliación de Kaikei](docs/screenshots/dashboard.png)

## Requisitos

- Node.js 24 o superior para desarrollo.
- ChatGPT/Codex instalado, o un ejecutable `codex` disponible en `PATH`.
- Una cuenta de ChatGPT con acceso a Codex.

Kaikei busca automáticamente el binario de ChatGPT en macOS y rutas comunes de Windows. Si no lo encuentra, la pantalla de acceso permite seleccionarlo manualmente. El binario no se incluye en el instalador de este repositorio.

## Descargas

Los artefactos generados quedan en `release/`:

| Plataforma | Instalador |
| --- | --- |
| Windows x64 | `Kaikei-0.1.1-win-x64.exe` |
| macOS Apple Silicon | `Kaikei-0.1.1-mac-arm64.dmg` |

Los instaladores de esta versión de desarrollo no están firmados digitalmente ni notarizados. Para distribución pública deben configurarse certificados de Apple Developer ID y Windows Code Signing.

Tagged releases also publish platform ZIP bundles. Cwenti verifies and embeds
those complete Electron bundles, so Kaikei is installed together with the
launcher rather than downloaded at first use.

## Desarrollo

```bash
npm install
npm run dev
```

Validación completa:

```bash
npm run verify
```

Empaquetado:

```bash
npm run dist:mac
npm run dist:win
```

El workflow `.github/workflows/desktop.yml` ejecuta auditoría, typecheck, pruebas,
build y smoke Electron en macOS y Windows. Las etiquetas `v*` publican los
artefactos de `release/`.

## Flujo técnico

1. Electron inicia `codex app-server` por `stdio` y realiza el handshake JSONL.
2. `account/read` reutiliza una sesión existente; `account/login/start` inicia el acceso con ChatGPT cuando hace falta.
3. Los archivos se leen y normalizan en el proceso principal. El renderer recibe solo una vista previa y metadatos.
4. El motor local propone cruces determinísticos y detecta duplicados.
5. Se inicia un thread efímero, `read-only`, sin aprobaciones ni uso de herramientas. Codex recibe movimientos normalizados, candidatos y reglas de revisión.
6. `turn/start.outputSchema` obliga a que el mensaje final cumpla el esquema del reporte; Zod lo valida otra vez antes de mostrarlo.
7. Los exportadores trabajan sobre el reporte validado guardado en memoria durante la sesión.

La integración sigue la documentación oficial de [Codex App Server](https://learn.chatgpt.com/docs/app-server), que define autenticación, `account/login/start`, threads, turns, eventos y `outputSchema`.

## Alcance contable

La aplicación ayuda a preparar y documentar la conciliación. No registra asientos, no certifica estados financieros y no reemplaza al contador, revisor o aprobador. Las reglas y fuentes investigadas están en [docs/REGLAS_CONCILIACION_COLOMBIA.md](docs/REGLAS_CONCILIACION_COLOMBIA.md).

## Privacidad y seguridad

- `contextIsolation: true`, `nodeIntegration: false` y renderer en sandbox.
- Selección de archivos mediante diálogos nativos; no se exponen rutas al renderer.
- Sin servidor propio ni almacenamiento de contraseñas.
- Threads de análisis efímeros, sandbox de solo lectura y política de aprobación `never`.
- Consentimiento previo en UI antes de enviar movimientos normalizados a Codex.
- Límite de 25 MB y 15.000 filas por archivo.

Antes de distribuir comercialmente, completa el responsable, canales y política de tratamiento descritos en [docs/PRIVACIDAD_Y_PRODUCCION.md](docs/PRIVACIDAD_Y_PRODUCCION.md), firma los instaladores y valida los términos aplicables a tu organización.

## Prueba visual

En desarrollo puede abrirse una pantalla poblada sin enviar información real:

```text
http://127.0.0.1:5173/?demo=results
```

También están disponibles `?demo=home`, `?demo=files` y `?demo=processing`.

## License / Licencia

Kaikei is sou

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 23 recognized source files, 136 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (45 of 45)

```
.github/workflows/desktop.yml
.gitignore
assets/entitlements.mac.plist
assets/icon.icns
assets/logo-kaikei.icns
COMMERCIAL-LICENSE.md
docs/BUILDWEEK_SUBMISSION.md
docs/PRIVACIDAD_Y_PRODUCCION.md
docs/REGLAS_CONCILIACION_COLOMBIA.md
electron/codex-client.mjs
electron/exporter.mjs
electron/file-parser.mjs
electron/main.mjs
electron/preload.cjs
electron/reconciliation-engine.mjs
electron/report-schema.mjs
fixtures/auxiliar_demo.csv
fixtures/extracto_demo.csv
fixtures/report_demo.json
index.html
LICENSE
package.json
README.en.md
README.md
scripts/electron-smoke.mjs
src/App.tsx
src/components/Brand.tsx
src/components/FilePanel.tsx
src/demo.ts
src/main.tsx
src/screens/HomeScreen.tsx
src/screens/LoginScreen.tsx
src/screens/ProcessingScreen.tsx
src/screens/ResultsScreen.tsx
src/screens/WorkspaceScreen.tsx
src/styles.css
src/types.ts
src/vite-env.d.ts
tests/file-parser.test.mjs
tests/packaging.test.mjs
tests/reconciliation-engine.test.mjs
tests/report-schema.test.mjs
TRADEMARKS.md
tsconfig.json
vite.config.ts
```

### Dependencies

- package.json: @testing-library/jest-dom@6.9.1, @testing-library/react@16.3.1, @types/node@24.10.1, @types/papaparse@5.3.16, @types/react@19.2.7, @types/react-dom@19.2.3, @vitejs/plugin-react@6.0.3, concurrently@9.2.4, cross-env@10.1.0, electron@43.2.0, electron-builder@26.15.3, exceljs@4.4.0, jsdom@27.4.0, lucide-react@1.25.0, papaparse@5.5.4, pdfjs-dist@6.1.200, react@19.2.8, react-dom@19.2.8, recharts@3.10.0, typescript@7.0.2, vite@8.1.5, vitest@4.1.10, wait-on@9.0.3, zod@4.4.3

### Recent commits (newest first)

- Merge pull request #5 from mauroentey/codex/fix-macos-signing
- Fix Kaikei macOS package signing
- Harden Electron distribution pipeline (#4)
- Harden Electron distribution pipeline
- Adopt the Prosperity 3.0.0 license (#3)
- Adopt the Prosperity 3.0.0 license
- Merge pull request #1 from mauroentey/agent/document-codex-readme
- Añade datos sintéticos de conciliación
- Document how Codex built Kaikei
- Añade builds de evaluación al submission
- Prepara submission de OpenAI Build Week
- Añade identidad visual de Kaikei
- Publica Kaikei 0.1.0

## Key source files (fetched from GitHub, selected and truncated for size)

### TRADEMARKS.md

```markdown
# Trademarks

The software license does not grant permission to use the Cwenti or Kaikei
names, logos, icons, visual identity, promotional material, or domains.

---

## Español

La licencia no concede permiso para utilizar los nombres, logos, iconos,
identidad visual, material promocional o dominios de Cwenti o Kaikei.

```

### COMMERCIAL-LICENSE.md

```markdown
# Commercial licensing notice

This document is informational. It does not grant a commercial license or
replace the applicable commercial agreement.

The Prosperity Public License 3.0.0 permits one 30-day commercial trial per
organization, not one trial per user or device. After the trial, the organization
must stop commercial use or enter into a separate commercial license agreement.

Commercial contact: [mauro@entey.net](mailto:mauro@entey.net)

Website: [cwenti.com](https://cwenti.com)

---

## Español

Este documento es informativo. Prosperity Public License 3.0.0 permite una única
prueba comercial de 30 días por organización. Después se debe detener el uso
comercial o celebrar un contrato independiente.

```

### package.json

```
{
  "name": "kaikei-conciliacion",
  "version": "0.1.2",
  "private": true,
  "description": "Desktop bank reconciliation powered by Codex App Server.",
  "type": "module",
  "main": "electron/main.mjs",
  "author": {
    "name": "Mauricio Samper",
    "email": "mauro@entey.net"
  },
  "license": "SEE LICENSE IN LICENSE",
  "scripts": {
    "dev": "concurrently -k \"vite --host 127.0.0.1\" \"wait-on tcp:5173 && cross-env VITE_DEV_SERVER_URL=http://127.0.0.1:5173 electron .\"",
    "build": "vite build",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "test:electron": "electron scripts/electron-smoke.mjs",
    "audit:dependencies": "npm audit --audit-level=high",
    "test:watch": "vitest",
    "verify": "npm run audit:dependencies && npm run typecheck && npm run test && npm run build && npm run test:electron",
    "dist": "npm run verify && electron-builder",
    "dist:mac": "npm run dist:mac:arm64",
    "dist:mac:arm64": "npm run verify && electron-builder --mac dmg zip --arm64 --publish never",
    "dist:mac:x64": "npm run verify && electron-builder --mac dmg zip --x64 --publish never",
    "dist:win": "npm run verify && electron-builder --win nsis zip --x64 --publish never"
  },
  "dependencies": {
    "exceljs": "4.4.0",
    "lucide-react": "1.25.0",
    "papaparse": "5.5.4",
    "pdfjs-dist": "6.1.200",
    "react": "19.2.8",
    "react-dom": "19.2.8",
    "recharts": "3.10.0",
    "zod": "4.4.3"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "6.9.1",
    "@testing-library/react": "16.3.1",
    "@types/node": "24.10.1",
    "@types/papaparse": "5.3.16",
    "@types/react": "19.2.7",
    "@types/react-dom": "19.2.3",
    "@vitejs/plugin-react": "6.0.3",
    "concurrently": "9.2.4",
    "cross-env": "10.1.0",
    "electron": "43.2.0",
    "electron-builder": "26.15.3",
    "jsdom": "27.4.0",
    "typescript": "7.0.2",
    "vite": "8.1.5",
    "vitest": "4.1.10",
    "wait-on": "9.0.3"
  },
  "overrides": {
    "brace-expansion": "5.0.8",
    "fast-uri": "3.1.4",
    "tar": "7.5.22",
    "uuid": "11.1.1"
  },
  "build": {
    "appId": "co.kaikei.conciliacion",
    "productName": "Kaikei",
    "copyright": "Copyright © 2026 Mauricio Samper · Bogotá, Colombia",
    "asar": true,
    "files": [
      "dist/**/*",
      "electron/**/*",
      "assets/logo-kaikei.png",
      "LICENSE",
      "COMMERCIAL-LICENSE.md",
      "TRADEMARKS.md",
      "package.json"
    ],
    "directories": {
      "output": "release"
    },
    "artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
    "mac": {
      "category": "public.app-category.finance",
      "icon": "assets/logo-kaikei.icns",
      "identity": "-",
      "hardenedRuntime": true,
      "entitlements": "assets/entitlements.mac.plist",
      "entitlementsInherit": "assets/entitlements.mac.plist",
      "gatekeeperAssess": false,
      "target": [
        {
          "target": "dmg",
          "arch": ["arm64", "x64"]
        }
      ]
    },
    "dmg": {
      "sign": false
    },
    "win": {
      "icon": "assets/logo-kaikei.ico",
      "target": [
        {
          "target": "nsis",
          "arch": ["x64"]
        }
      ]
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "createDesktopShortcut": true
    }
  }
}

```

### src/main.tsx

```typescript
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./styles.css";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

```

### src/App.tsx

```typescript
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
import { AppHeader } from "./components/Brand";
import { LoginScreen } from "./screens/LoginScreen";
import { HomeScreen } from "./screens/HomeScreen";
import { WorkspaceScreen } from "./screens/WorkspaceScreen";
import { ProcessingScreen } from "./screens/ProcessingScreen";
import { demoAccountingFile, demoBankFile, demoReport } from "./demo";
import type {
  Account,
  ColumnMapping,
  FileSummary,
  ProgressEvent,
  ReconciliationDetails,
  ReconciliationOptions,
  Role,
  RunResult,
} from "./types";

type Screen = "home" | "files" | "processing" | "results";

const ResultsScreen = lazy(() => import("./screens/ResultsScreen").then((module) => ({ default: module.ResultsScreen })));

const defaultDetails: ReconciliationDetails = {
  entityName: "",
  entityType: "private",
  accountLabel: "",
  cutoffDate: new Date().toISOString().slice(0, 7) + "-01",
  bookBalance: "",
  bankBalance: "",
};

const defaultOptions: ReconciliationOptions = {
  amountTolerance: 1,
  dateToleranceDays: 3,
  allowGrouped: true,
};

export default function App() {
  const demo = useMemo(() => new URLSearchParams(window.location.search).get("demo"), []);
  const [account, setAccount] = useState<Account | null>(demo ? { type: "chatgpt", email: "contabilidad@horizonte.co", planType: "business" } : null);
  const [authLoading, setAuthLoading] = useState(!demo);
  const [loginAwaiting, setLoginAwaiting] = useState(false);
  const [authError, setAuthError] = useState("");
  const [codexStatus, setCodexStatus] = useState("Conectando con Codex App Server…");
  const [screen, setScreen] = useState<Screen>(demo === "results" ? "results" : demo === "files" ? "files" : demo === "processing" ? "processing" : "home");
  const [accountingFile, setAccountingFile] = useState<FileSummary | null>(demo ? demoAccountingFile : null);
  const [bankFiles, setBankFiles] = useState<FileSummary[]>(demo ? [demoBankFile] : []);
  const [details, setDetails] = useState<ReconciliationDetails>(demo ? {
    entityName: "Comercializadora Horizonte S.A.S.",
    entityType: "private",
    accountLabel: "Bancolombia · Cuenta corriente • 4821",
    cutoffDate: "2026-06-30",
    bookBalance: "128450800",
    bankBalance: "126970800",
  } : defaultDetails);
  const [options, setOptions] = useState(defaultOptions);
  const [privacyAccepted, setPrivacyAccepted] = useState(Boolean(demo));
  const [progress, setProgress] = useState<ProgressEvent>({ stage: "idle", message: "Preparando el análisis…", progress: 0 });
  const [result, setResult] = useState<RunResult | null>(demo ? { reportId: "demo", report: demoReport, engineWarnings: [], model: "GPT-5.6 Sol" } : null);
  const [workspaceError, setWorkspaceError] = useState("");

  useEffect(() => {
    if (demo || !window.kaikei) return;
    window.kaikei.codex.account()
      .then((response) => {
        setAccount(response.account);
        if (response.error) setAuthError(response.error);
      })
      .catch((error: Error) => setAuthError(error.message))
      .finally(() => setAuthLoading(false));

    const removeStatus = window.kaikei.codex.onStatus((payload) => setCodexStatus(payload.message));
    const removeLogin = window.kaikei.codex.onLogin((payload) => {
      setLoginAwaiting(false);
      if (payload.success && payload.account) {
        setAccount(payload.account);
        setAuthError("");
      } else setAuthError(payload.error || "No fue posible completar el inicio de sesión.");
    });
    const removeAccount = window.kaikei.codex.onAccount((payload) => setAccount(payload.account));
    const removeProgress = window.kaikei.reconciliation.onProgress(setProgress);
    return () => {
      removeStatus();
      removeLogin();
      removeAccount();
      removeProgress();
    };
  }, [demo]);

  const login = async () => {
    setAuthError("");
    setLoginAwaiting(true);
    try {
      await window.kaikei.codex.login();
    } catch (error) {
      setLoginAwaiting(false);
      setAuthError(error instanceof Error ? error.message : String(error));
    }
  };

  const chooseCodex = async () => {
    setAuthError("");
    try {
      const response = await window.kaikei.codex.chooseExecutable();
      if (response.account?.account) setAccount(response.account.account);
    } catch (error) {
      setAuthError(error instanceof Error ? error.message : String(error));
    }
  };

  const logout = async () => {
    if (!demo) await window.kaikei.codex.logout();
    setAccount(null);
    setScreen("home");
  };

  const selectFiles = async (role: Role) => {
    setWorkspaceError("");
    try {
      const files = await window.kaikei.files.select(role);
      if (role === "accounting" && files[0]) {
        if (accountingFile) await window.kaikei.files.remove(accountingFile.id);
        setAccountingFile(files[0]);
      }
      if (role === "bank") setBankFiles((current) => [...current, ...files]);
    } catch (error) {
      setWorkspaceError(error instanceof Error ? error.message : String(error));
    }
  };

  const removeFile = async (file: FileSummary) => {
    if (!demo) await window.kaikei.files.remove(file.id);
    if (file.role === "accounting") setAccountingFile(null);
    else setBankFiles((current) => current.filter((item) => item.id !== file.id));
  };

  const updateMapping = (fileId: string, mapping: ColumnMapping) => {
    if (accountingFile?.id === fileId) setAccountingFile({ ...accountingFile, mapping });
    setBankFiles((current) => current.map((file) => file.id === fileId ? { ...file, mapping } : file));
  };

  const run = async () => {
    if (!accountingFile || !bankFiles.length) return;
    setWorkspaceError("");
    setProgress({ stage: "starting", message: "Preparando la conciliación…", progress: 4 });
    setScreen("processing");
    try {
      const response = await window.kaikei.reconciliation.run({
        accounting: { fileId: accountingFile.id, mapping: accountingFile.mapping },
        
[truncated — 2057 more characters]
```

### electron/main.mjs

```
import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import crypto from "node:crypto";
import { CodexAppServer, discoverCodexExecutable } from "./codex-client.mjs";
import { parseFinancialFile } from "./file-parser.mjs";
import { normalizeRows, reconcileTransactions, roundCurrency } from "./reconciliation-engine.mjs";
import { buildExcelReport, buildPdfReport } from "./exporter.mjs";
import { reconciliationOutputSchema, parseReconciliationReport } from "./report-schema.mjs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const sessionFiles = new Map();
const sessionReports = new Map();
let mainWindow = null;
let codex = null;
let appSettings = {};

app.setName("Kaikei");
app.setAboutPanelOptions({
  applicationName: "Kaikei",
  applicationVersion: app.getVersion(),
  version: app.getVersion(),
  copyright: "© 2026 Mauricio Samper · Bogotá, Colombia",
  credits: "Conciliación bancaria asistida por Codex\nContacto: mauro@entey.net",
  website: "mailto:mauro@entey.net",
  iconPath: path.join(__dirname, "..", "assets", "logo-kaikei.png"),
});

async function loadSettings() {
  try {
    const content = await fs.readFile(path.join(app.getPath("userData"), "settings.json"), "utf8");
    appSettings = JSON.parse(content);
  } catch {
    appSettings = {};
  }
}

async function saveSettings() {
  await fs.mkdir(app.getPath("userData"), { recursive: true });
  await fs.writeFile(
    path.join(app.getPath("userData"), "settings.json"),
    JSON.stringify(appSettings, null, 2),
    "utf8",
  );
}

function createCodexClient() {
  codex?.stop();
  codex = new CodexAppServer({
    executablePath: discoverCodexExecutable(appSettings.codexPath),
    cwd: app.getPath("userData"),
    version: app.getVersion(),
  });
  codex.on("status", (payload) => sendToRenderer("codex:status-event", payload));
  codex.on("login-completed", async (payload) => {
    if (!payload.success) {
      sendToRenderer("codex:login-event", { success: false, error: payload.error || "No fue posible iniciar sesión." });
      return;
    }
    const account = await codex.getAccount().catch(() => ({ account: null }));
    sendToRenderer("codex:login-event", { success: true, account: account.account });
  });
  codex.on("account-updated", () => {
    codex.getAccount().then((account) => sendToRenderer("codex:account-event", account)).catch(() => {});
  });
  codex.on("analysis-progress", (payload) => sendToRenderer("reconciliation:progress", payload));
}

function sendToRenderer(channel, payload) {
  if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send(channel, payload);
}

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1440,
    height: 930,
    minWidth: 1120,
    minHeight: 720,
    show: false,
    titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default",
    backgroundColor: "#f5f3ed",
    webPreferences: {
      preload: path.join(__dirname, "preload.cjs"),
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: true,
    },
  });
  mainWindow.webContents.setWindowOpenHandler(({ url }) => {
    if (url.startsWith("https://")) shell.openExternal(url);
    return { action: "deny" };
  });
  mainWindow.webContents.on("will-navigate", (event, url) => {
    const current = mainWindow.webContents.getURL();
    if (url !== current && !url.startsWith("file://") && !url.startsWith("http://127.0.0.1")) event.preventDefault();
  });
  mainWindow.once("ready-to-show", () => mainWindow.show());

  if (process.env.VITE_DEV_SERVER_URL) mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
  else mainWindow.loadFile(path.join(__dirname, "..", "dist", "index.html"));
}

app.whenReady().then(async () => {
  await loadSettings();
  createCodexClient();
  registerIpcHandlers();
  createWindow();
  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on("window-all-closed", () => {
  codex?.stop();
  if (process.platform !== "darwin") app.quit();
});

app.on("before-quit", () => codex?.stop());

function registerIpcHandlers() {
  ipcMain.handle("codex:account", async () => {
    try {
      return await codex.getAccount();
    } catch (error) {
      return { account: null, requiresOpenaiAuth: true, error: userFacingCodexError(error) };
    }
  });

  ipcMain.handle("codex:login", async () => {
    try {
      const response = await codex.loginWithChatGPT();
      if (response.type !== "chatgpt" || !response.authUrl) throw new Error("Codex no devolvió una URL de autenticación.");
      await shell.openExternal(response.authUrl);
      return { awaiting: true, loginId: response.loginId };
    } catch (error) {
      throw new Error(userFacingCodexError(error));
    }
  });

  ipcMain.handle("codex:logout", async () => codex.logout());

  ipcMain.handle("codex:choose-executable", async () => {
    const result = await dialog.showOpenDialog(mainWindow, {
      title: "Selecciona el ejecutable de Codex",
      properties: ["openFile"],
      filters: process.platform === "win32"
        ? [{ name: "Codex", extensions: ["exe"] }]
        : [{ name: "Ejecutable", extensions: ["*"] }],
    });
    if (result.canceled || !result.filePaths[0]) return { canceled: true };
    appSettings.codexPath = result.filePaths[0];
    await saveSettings();
    createCodexClient();
    const account = await codex.getAccount();
    return { canceled: false, path: appSettings.codexPath, account };
  });

  ipcMain.handle("files:select", async (_event, { role }) => {
    if (role !== "accounting" && role !== "bank") throw new Error("Tipo de archivo inválido.");
    const result = await dialog.showOpenDialog(mainWindow, {
      title: role === "accounting" ? "Selecciona el auxiliar contable" : "Selecciona uno o varios extractos bancarios",
      properties: role === "bank" ? ["openFile", "multiSelec
[truncated — 11538 more characters]
```

### vite.config.ts

```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  base: "./",
  build: {
    outDir: "dist",
    sourcemap: true,
  },
  server: {
    port: 5173,
    strictPort: true,
  },
});

```

### index.html

```html
<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="theme-color" content="#0d342c" />
    <meta
      name="description"
      content="Kaikei: conciliación bancaria asistida por Codex para Colombia"
    />
    <title>Kaikei · Conciliación bancaria</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### src/vite-env.d.ts

```typescript
/// <reference types="vite/client" />

declare module "*.css";

```

### src/types.ts

```typescript
export type Role = "accounting" | "bank";
export type MappingField = "date" | "description" | "reference" | "debit" | "credit" | "amount" | "balance" | "type";
export type ColumnMapping = Record<MappingField, string>;

export interface Account {
  type: "chatgpt";
  email: string | null;
  planType: string;
}

export interface AccountResponse {
  account: Account | null;
  requiresOpenaiAuth: boolean;
  error?: string;
}

export interface FileSummary {
  id: string;
  role: Role;
  name: string;
  extension: string;
  size: number;
  sheetName: string;
  headers: string[];
  preview: Array<Record<string, unknown>>;
  rowCount: number;
  mapping: ColumnMapping;
  warnings: string[];
}

export interface ReconciliationDetails {
  entityName: string;
  entityType: "private" | "public" | "nonprofit";
  accountLabel: string;
  cutoffDate: string;
  bookBalance: string;
  bankBalance: string;
}

export interface ReconciliationOptions {
  amountTolerance: number;
  dateToleranceDays: number;
  allowGrouped: boolean;
}

export interface RunPayload {
  accounting: { fileId: string; mapping: ColumnMapping };
  bank: Array<{ fileId: string; mapping: ColumnMapping }>;
  details: ReconciliationDetails;
  options: ReconciliationOptions;
  privacyAccepted: boolean;
}

export interface ReconciliationReport {
  version: "1.0";
  metadata: {
    generatedAt: string;
    cutoffDate: string;
    currency: "COP";
    entityName: string;
    accountLabel: string;
    accountingFiles: string[];
    bankFiles: string[];
    methodology: string;
  };
  executiveSummary: string;
  metrics: {
    bookBalance: number;
    bankBalance: number;
    adjustedBookBalance: number;
    adjustedBankBalance: number;
    differenceBefore: number;
    differenceAfter: number;
    matchedAmount: number;
    matchedCount: number;
    unmatchedBookAmount: number;
    unmatchedBookCount: number;
    unmatchedBankAmount: number;
    unmatchedBankCount: number;
    reconciliationRate: number;
  };
  matches: Array<{
    id: string;
    bookTransactionIds: string[];
    bankTransactionIds: string[];
    amount: number;
    matchType: "exact" | "date_window" | "reference" | "grouped" | "suggested";
    confidence: number;
    explanation: string;
  }>;
  unmatchedBook: PendingBook[];
  unmatchedBank: PendingBank[];
  findings: Array<{
    id: string;
    severity: "info" | "warning" | "critical";
    title: string;
    description: string;
    evidenceIds: string[];
    recommendation: string;
  }>;
  adjustments: Array<{
    id: string;
    status: "suggested";
    debitAccount: string;
    creditAccount: string;
    amount: number;
    description: string;
    evidenceIds: string[];
    requiresApproval: boolean;
  }>;
  controls: Array<{
    id: string;
    name: string;
    status: "pass" | "review" | "fail";
    note: string;
  }>;
  scopeLimitations: string[];
  legalContext: string[];
}

export interface PendingBook {
  transactionId: string;
  category: "deposit_in_transit" | "outstanding_payment" | "book_error" | "unidentified" | "other";
  amount: number;
  date: string;
  description: string;
  explanation: string;
  suggestedAction: string;
}

export interface PendingBank {
  transactionId: string;
  category: "bank_fee" | "bank_interest" | "automatic_debit" | "automatic_credit" | "bank_error" | "unrecorded_entry" | "tax" | "unidentified" | "other";
  amount: number;
  date: string;
  description: string;
  explanation: string;
  suggestedAction: string;
}

export interface RunResult {
  reportId: string;
  report: ReconciliationReport;
  engineWarnings: string[];
  model: string;
}

export interface ProgressEvent {
  stage: string;
  message: string;
  progress?: number;
  detail?: string;
}

export interface KaikeiApi {
  codex: {
    account(): Promise<AccountResponse>;
    login(): Promise<{ awaiting: boolean; loginId: string }>;
    logout(): Promise<{ success: boolean }>;
    chooseExecutable(): Promise<{ canceled: boolean; path?: string; account?: AccountResponse }>;
    onStatus(callback: (payload: { state: string; message: string }) => void): () => void;
    onLogin(callback: (payload: { success: boolean; account?: Account; error?: string }) => void): () => void;
    onAccount(callback: (payload: AccountResponse) => void): () => void;
  };
  files: {
    select(role: Role): Promise<FileSummary[]>;
    remove(fileId: string): Promise<{ success: boolean }>;
  };
  reconciliation: {
    run(payload: RunPayload): Promise<RunResult>;
    onProgress(callback: (payload: ProgressEvent) => void): () => void;
  };
  report: {
    export(reportId: string, format: "xlsx" | "pdf" | "json"): Promise<{ canceled: boolean; path?: string }>;
  };
}

declare global {
  interface Window {
    kaikei: KaikeiApi;
  }
}

```

[14 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]