Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
tests:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
python: "3.13"
- os: ubuntu-latest
python: "3.14"
- os: windows-latest
python: "3.13"
- os: windows-latest
python: "3.14"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Run tests
run: python -m unittest discover -s tests -v
40 changes: 31 additions & 9 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ configuración local.

Las alertas de voz se silencian entre las 23:00 y las 07:00. Mencionan el
proyecto, la duración y el título del chat cuando Codex lo proporciona. Teams y
Discord incluyen el mensaje final del asistente como un resumen limitado.
Discord incluyen por defecto el mensaje final como resumen limitado; configura
`include_summary` como `false` para omitir por completo el texto de la respuesta.

Cada canal se ejecuta de forma independiente: el fallo de un webhook no impide
que funcionen la voz o el otro webhook. El notificador mantiene un registro
Expand All @@ -34,7 +35,7 @@ final del asistente.

- Python 3.10 o posterior.
- Codex con acceso a `~/.codex/config.toml`.
- Voz en Windows: SAPI, normalmente incluido con Windows.
- Voz en Windows: SAPI, normalmente incluido con Windows; Piper es opcional.
- Voz en Linux: Piper (recomendado), `spd-say`, `espeak-ng` o `espeak`.

Teams y Discord no necesitan un motor de voz local.
Expand All @@ -55,11 +56,17 @@ El instalador:
3. Copia `config.example.json` a la ruta de configuración local si todavía no
existe.

Los archivos existentes de Codex se respaldan antes de modificarlos. Una
configuración existente del notificador se conserva salvo que se use
Los archivos existentes de Codex se respaldan antes de modificarlos. Las
escrituras usan archivos temporales y reemplazo atómico, por lo que un fallo no
destruye el destino. Una configuración existente del notificador se conserva salvo que se use
explícitamente `--force-config`. Después de instalar o cambiar el hook, abre un
chat nuevo y usa `/hooks` para revisarlo y confiar en él.

El instalador modifica únicamente `notify` en la raíz de `config.toml`; no
reemplaza claves `notify` dentro de secciones TOML ni hooks ajenos. En POSIX,
las carpetas de estado son privadas y las marcas, logs y configuraciones nuevas
usan permisos solo para el propietario cuando corresponde.

Opciones del instalador:

```text
Expand Down Expand Up @@ -89,6 +96,7 @@ canales:
"enabled": false,
"minimum_seconds": 300,
"notify_when_duration_unknown": false,
"include_summary": true,
"summary_max_chars": 1800,
"summary_tail_chars": 600,
"webhook_url": ""
Expand All @@ -97,6 +105,7 @@ canales:
"enabled": false,
"minimum_seconds": 300,
"notify_when_duration_unknown": false,
"include_summary": true,
"summary_max_chars": 1800,
"webhook_url": ""
},
Expand All @@ -122,6 +131,9 @@ canales:
`minimum_seconds` es inclusivo. `notify_when_duration_unknown` omite únicamente
el umbral de duración: el canal debe seguir habilitado, los canales de webhook
necesitan una URL HTTPS válida y la voz sigue respetando el horario silencioso.
`include_summary` es `true` por compatibilidad. Con `false`, Teams omite su
sección de resumen y Discord omite la descripción del embed, pero conserva los
metadatos. Ambos canales siguen aceptando cualquier URL HTTPS.

El instalador no combina claves nuevas dentro de una configuración existente.
Al actualizar, compara tu archivo local con `config.example.json` y añade
Expand Down Expand Up @@ -162,9 +174,12 @@ PowerShell para usar directamente el dispositivo de audio de Windows.
completo). `null` conserva el volumen predeterminado. La opción se aplica a
Piper en Linux/WSL y a SAPI en Windows.

En Windows, `spanish_voice` y `english_voice` pueden contener parte del nombre
de una voz SAPI instalada. Los valores vacíos seleccionan automáticamente una
voz del idioma correspondiente.
En Windows, si `piper_executable` está vacío se conserva SAPI: `spanish_voice` y
`english_voice` son fragmentos opcionales del nombre de una voz instalada. Si
no está vacío, se resuelve por PATH o como ruta expandida a `piper.exe`, y esos
campos son rutas a modelos `.onnx` de español/inglés. Un Piper configurado pero
inválido produce un error visible y no cambia silenciosamente a SAPI; SAPI
continúa siendo el valor predeterminado sin configuración.

Configura `quiet_start` y `quiet_end` con la misma hora para deshabilitar el
horario silencioso.
Expand Down Expand Up @@ -206,18 +221,25 @@ python notifier.py voice-test es
python notifier.py voice-test en
```

Cada comando selecciona el modelo Piper correspondiente al idioma. En Linux
nativo reproduce el WAV temporal con `paplay`, `pw-play`, `aplay` o `ffplay`.
Cada comando selecciona el modelo Piper correspondiente cuando Piper está
configurado. En Windows sin Piper prueba SAPI; en Linux reproduce el WAV
temporal con `paplay`, `pw-play`, `aplay` o `ffplay`.

Estos comandos reproducen audio; las pruebas automatizadas simulan las llamadas
de voz y webhook.

`notify` devuelve éxito a Codex aunque falle un canal; los fallos se registran
de forma independiente. La sintaxis manual inválida devuelve un código
distinto de cero, al igual que `voice-test` si la voz no puede ejecutarse.

## Ejecutar las pruebas

```powershell
python -m unittest discover -s tests -v
```

GitHub Actions ejecuta esta suite en Ubuntu y Windows con Python 3.13 y 3.14.

## Cómo funciona

El hook `UserPromptSubmit` registra una marca de inicio sin guardar el prompt.
Expand Down
40 changes: 32 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ The installer copies `config.example.json` when no local configuration exists.

Voice alerts are muted from 23:00 to 07:00. They mention the project, duration,
and chat title when Codex provides one. Teams and Discord include the final
assistant message as a bounded summary.
assistant message as a bounded summary by default; set `include_summary` to
`false` in either channel to omit assistant-response text completely.

Each channel runs independently: a failed webhook does not prevent voice or the
other webhook from running. The notifier keeps a privacy-conscious daily trace
Expand All @@ -31,7 +32,7 @@ log and never stores the prompt or final assistant message.

- Python 3.10 or later.
- Codex with an accessible `~/.codex/config.toml`.
- Windows voice: SAPI, normally included with Windows.
- Windows voice: SAPI, normally included with Windows; Piper is optional.
- Linux voice: Piper (recommended), `spd-say`, `espeak-ng`, or `espeak`.

Teams and Discord do not require a local speech engine.
Expand All @@ -52,11 +53,17 @@ The installer:
3. Copies `config.example.json` to the local configuration path when that file
does not already exist.

Existing Codex files are backed up before modification. An existing notifier
configuration is preserved unless `--force-config` is explicitly used. After
Existing Codex files are backed up before modification. Writes use temporary
files and atomic replacement, so a failed write leaves the destination intact.
An existing notifier configuration is preserved unless `--force-config` is explicitly used. After
installing or changing the hook, open a new chat and use `/hooks` to review and
trust it.

The installer changes only a root-level `notify` setting in `config.toml`;
`notify` keys inside TOML sections and unrelated hooks are preserved. State
directories are private on POSIX and marker/log/config files are written with
owner-only permissions where that platform supports them.

Installer options:

```text
Expand Down Expand Up @@ -85,6 +92,7 @@ Default locations:
"enabled": false,
"minimum_seconds": 300,
"notify_when_duration_unknown": false,
"include_summary": true,
"summary_max_chars": 1800,
"summary_tail_chars": 600,
"webhook_url": ""
Expand All @@ -93,6 +101,7 @@ Default locations:
"enabled": false,
"minimum_seconds": 300,
"notify_when_duration_unknown": false,
"include_summary": true,
"summary_max_chars": 1800,
"webhook_url": ""
},
Expand All @@ -118,6 +127,9 @@ Default locations:
`minimum_seconds` is inclusive. `notify_when_duration_unknown` bypasses only
the duration threshold: the channel must still be enabled, webhook channels
still need a valid HTTPS URL, and voice still respects quiet hours.
`include_summary` defaults to `true` for compatibility. With `false`, Teams
omits its summary section and Discord omits the embed description; metadata is
still sent. Teams and Discord continue accepting any HTTPS webhook URL.

The installer does not merge new keys into an existing configuration. When
upgrading, compare your local file with `config.example.json` and add any new
Expand Down Expand Up @@ -158,8 +170,12 @@ it uses the Windows audio device directly.
volume). `null` preserves the default volume. The option applies to Piper on
Linux/WSL and SAPI on Windows.

On Windows, `spanish_voice` and `english_voice` can contain part of an installed
SAPI voice name. Empty values select a matching language voice automatically.
On Windows, an empty `piper_executable` keeps the existing SAPI behavior:
`spanish_voice` and `english_voice` are optional fragments of installed SAPI
voice names. If `piper_executable` is non-empty, it is resolved through PATH or
as an expanded path to `piper.exe`, and those fields instead name existing
Spanish/English `.onnx` models. Piper failures are reported as errors and do
not silently fall back to SAPI. SAPI remains the zero-configuration default.

Set `quiet_start` and `quiet_end` to the same time to disable quiet hours.

Expand Down Expand Up @@ -198,17 +214,25 @@ python notifier.py voice-test es
python notifier.py voice-test en
```

Each command selects the matching Piper model. Native Linux plays the temporary
WAV with `paplay`, `pw-play`, `aplay`, or `ffplay`.
Each command selects the matching Piper model when Piper is configured. On
Windows without Piper it tests SAPI; native Linux plays a Piper WAV with
`paplay`, `pw-play`, `aplay`, or `ffplay`.

These commands play audio; automated tests mock speech and webhook calls.

`notify` returns success to Codex even when an individual channel fails; those
failures are logged independently. Invalid manual syntax returns non-zero, and
`voice-test` returns non-zero when the selected voice cannot execute.

## Run tests

```powershell
python -m unittest discover -s tests -v
```

GitHub Actions runs this suite on Ubuntu and Windows with Python 3.13 and
Python 3.14.

## How it works

The `UserPromptSubmit` hook records a start marker without storing the prompt.
Expand Down
3 changes: 3 additions & 0 deletions codex_notifier/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Maintainable implementation package for Codex Notifier."""

__version__ = "1.0"
1 change: 1 addition & 0 deletions codex_notifier/channels/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""External notification and local voice channels."""
17 changes: 17 additions & 0 deletions codex_notifier/channels/discord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Discord webhook transport."""

from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

from .webhook import send_webhook_notification


def send(webhook_url: str, payload: dict) -> None:
parts = urlsplit(webhook_url)
query = dict(parse_qsl(parts.query, keep_blank_values=True))
query["wait"] = "true"
confirmed_url = urlunsplit((parts.scheme, parts.netloc, parts.path,
urlencode(query), parts.fragment))
send_webhook_notification("Discord", confirmed_url, payload)


send_discord_notification = send
10 changes: 10 additions & 0 deletions codex_notifier/channels/teams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Microsoft Teams webhook transport."""

from .webhook import send_webhook_notification


def send(webhook_url: str, payload: dict) -> None:
send_webhook_notification("Teams", webhook_url, payload)


send_teams_notification = send
Loading
Loading