refactor: switch the whole project to English
talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.
Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.
Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d30bd95ad
commit
85fa64cfe6
95 files changed
+4707
-4854
No files matched your search
@@ -1,40 +1,41 @@
|
||||
# talk
|
||||
|
||||
> **Status: WIP** — aktive Entwicklung (talk 2.0). Spezifikation mit
|
||||
> Entscheidungs-Nachtrag: [`docs/spec.md`](docs/spec.md)
|
||||
> **Status: WIP**, under active development. Specification with the decision
|
||||
> addendum: [`docs/spec.md`](docs/spec.md)
|
||||
|
||||
**talk** macht Skripte und KI-Agenten hörbar: Ein Aufruf — `talk "Build fertig"` —
|
||||
spricht eine kurze Nachricht über eine TTS-Provider-Kette
|
||||
(ElevenLabs → OpenAI → Edge-TTS → OS-Stimme), serialisiert durch einen
|
||||
Hintergrund-Daemon, den talk beim ersten Aufruf selbst startet. Ein
|
||||
statisches Binary, cross-platform (Windows/macOS/Linux, amd64/arm64), keine
|
||||
Runtime-Abhängigkeiten.
|
||||
**talk** gives scripts and AI agents a voice: one call, `talk "Build done"`,
|
||||
speaks a short announcement through a chain of TTS providers
|
||||
(ElevenLabs → OpenAI → Edge TTS → OS voice), serialized by a background
|
||||
daemon that talk starts itself on first use. One static binary,
|
||||
cross-platform (Windows, macOS, Linux; amd64 and arm64), no runtime
|
||||
dependencies.
|
||||
|
||||
## Befehle
|
||||
## Commands
|
||||
|
||||
| Befehl | Zweck |
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `talk "Text"` | Ansage — Abkürzung für `talk say`, non-blocking |
|
||||
| `talk say "Text"` | Ansage (Text als Argument oder von stdin) |
|
||||
| `talk wrap -- <Befehl>` | Befehl ausführen, Erfolg/Misserfolg + Dauer ansagen |
|
||||
| `talk mute` / `talk unmute` | Ansagen dauerhaft stummschalten / Stummschaltung aufheben |
|
||||
| `talk status` | Daemon-Status (Queue, Mute, Provider, Version) |
|
||||
| `talk cancel` | laufende und geplante Ansagen abbrechen |
|
||||
| `talk daemon run\|start\|stop` | Daemon im Vordergrund betreiben / im Hintergrund starten / stoppen (normalerweise nicht nötig, siehe unten) |
|
||||
| `talk voices` | bekannte Stimmen je Provider/Sprache, Default markiert (`--provider`, `--lang`, `--json`) |
|
||||
| `talk test` | Testsatz sprechen; zeigt Provider und Zeiten (`--lang`, `--provider`, `--voice`) |
|
||||
| `talk doctor` | Selbstdiagnose: Config, Daemon, Audio, Provider, Mute, Gesetze (`--probe`, `--json`) |
|
||||
| `talk setup` | Ersteinrichtung in wenigen Fragen: Stimmen probehören, Ruhezeit, config.toml |
|
||||
| `talk "text"` | announce; shorthand for `talk say`, non-blocking |
|
||||
| `talk say "text"` | announce (text as argument or from stdin) |
|
||||
| `talk wrap -- <command>` | run a command, announce success/failure and duration |
|
||||
| `talk mute` / `talk unmute` | mute all announcements persistently / turn them back on |
|
||||
| `talk status` | daemon status (queue, mute, provider, laws, version) |
|
||||
| `talk cancel` | cancel the playing and all queued announcements |
|
||||
| `talk daemon run\|start\|stop` | run the daemon in the foreground / start it in the background / stop it (normally not needed, see below) |
|
||||
| `talk voices` | known voices per provider and language, default marked (`--provider`, `--lang`, `--json`) |
|
||||
| `talk test` | speak a test sentence; shows provider and timings (`--lang`, `--provider`, `--voice`) |
|
||||
| `talk doctor` | self-diagnosis: config, daemon, audio, providers, notifications, mute, laws (`--probe`, `--json`) |
|
||||
| `talk setup` | first-time setup in a few questions: language, voices with preview, quiet hours, config.toml |
|
||||
|
||||
Say-Flags (gelten auch für `talk "Text"`): `--wait`, `--now`, `--priority`,
|
||||
Say flags (also for `talk "text"`): `--wait`, `--now`, `--priority`,
|
||||
`--voice`, `--lang`, `--provider`.
|
||||
|
||||
## Installation
|
||||
|
||||
**Release-Binary** (Linux, macOS, Windows; amd64/arm64): von den
|
||||
[Releases](https://code.m3ta.dev/m3tam3re/talk/releases) das passende
|
||||
`talk_<os>_<arch>` laden, Prüfsumme gegen `checksums.txt` vergleichen, als
|
||||
`talk` in den PATH legen. Keine weiteren Abhängigkeiten.
|
||||
**Release binary** (Linux, macOS, Windows; amd64/arm64): download the
|
||||
matching `talk_<os>_<arch>` from the
|
||||
[releases](https://code.m3ta.dev/m3tam3re/talk/releases), verify the
|
||||
checksum against `checksums.txt`, put it in your PATH as `talk`. No other
|
||||
dependencies.
|
||||
|
||||
```console
|
||||
curl -LO https://code.m3ta.dev/m3tam3re/talk/releases/latest/download/talk_linux_amd64
|
||||
@@ -42,209 +43,215 @@ sha256sum -c --ignore-missing <(curl -sL https://code.m3ta.dev/m3tam3re/talk/rel
|
||||
install -m 755 talk_linux_amd64 ~/.local/bin/talk
|
||||
```
|
||||
|
||||
**Nix** (Flake): `nix run git+https://code.m3ta.dev/m3tam3re/talk -- doctor`
|
||||
oder in der Konfiguration `inputs.talk.url = "git+https://code.m3ta.dev/m3tam3re/talk";`
|
||||
und `inputs.talk.packages.${system}.talk` bzw. `inputs.talk.overlays.default`.
|
||||
Das Paket bringt espeak-ng (OS-Stimme) und libnotify (Toast) mit.
|
||||
**Nix** (flake): `nix run git+https://code.m3ta.dev/m3tam3re/talk -- doctor`,
|
||||
or in your configuration `inputs.talk.url = "git+https://code.m3ta.dev/m3tam3re/talk";`
|
||||
and `inputs.talk.packages.${system}.talk` or `inputs.talk.overlays.default`.
|
||||
The package brings espeak-ng (OS voice) and libnotify (toast) along.
|
||||
|
||||
**Aus dem Quelltext:** `go install code.m3ta.dev/m3tam3re/talk/cmd/talk@latest`.
|
||||
**From source:** `go install code.m3ta.dev/m3tam3re/talk/cmd/talk@latest`.
|
||||
|
||||
## Erste Schritte
|
||||
## Getting started
|
||||
|
||||
```console
|
||||
talk doctor # Was ist da, was fehlt? Eine Zeile pro Punkt.
|
||||
talk test # Hörst du etwas? Welche Stimme war das?
|
||||
talk setup # Stimme aussuchen, Ruhezeit, fertig.
|
||||
talk "Hallo Welt" # Ab jetzt: Skripte und Agenten rufen genau das.
|
||||
talk doctor # What is there, what is missing? One line per item.
|
||||
talk test # Do you hear something? Which voice was it?
|
||||
talk setup # Pick a language and voices, set quiet hours, done.
|
||||
talk "Hello world" # From now on scripts and agents call exactly this.
|
||||
```
|
||||
|
||||
Ohne jeden Key funktioniert talk sofort: Es nutzt die kostenlose Edge-
|
||||
Stimme von Microsoft (Internet nötig) und sonst die Stimme des
|
||||
Betriebssystems. Wer eine Premium-Stimme will, setzt `TALK_ELEVENLABS_KEY`
|
||||
oder `TALK_OPENAI_KEY` als Umgebungsvariable — Keys gehören nie in die
|
||||
config.toml.
|
||||
talk works without any key: it uses Microsoft's free Edge voice (needs
|
||||
internet) and otherwise your operating system's voice. For a premium voice,
|
||||
set `TALK_ELEVENLABS_KEY` or `TALK_OPENAI_KEY` as an environment variable.
|
||||
Keys never go into config.toml.
|
||||
|
||||
### Diagnose und Einrichtung
|
||||
### Diagnosis and setup
|
||||
|
||||
- **`talk doctor`** berichtet zeilenweise mit ✔ (ok), ⚠ (Hinweis), ✘
|
||||
(Fehler): Config-Datei, Daemon (läuft / startet beim nächsten
|
||||
Sprechbefehl / fremder Dienst auf dem Port), Audio-Ausgabe (oto
|
||||
initialisierbar? `null` = kein Ton), jede Provider-Stufe (Key vorhanden,
|
||||
Endpunkt, Default-Stimme; OS-Stimme: welcher Mechanismus gefunden
|
||||
wurde), Mute, Gesetze inkl. aktiver Ruhezeit. Exit 0 ohne ✘, sonst 1;
|
||||
ein nicht laufender Daemon ist ⚠. `--probe` macht echte Synthese-Proben
|
||||
(Netz), `--json` liefert die Liste maschinenlesbar. Auf Windows-Konsolen
|
||||
stehen `[OK]`/`[WARN]`/`[FAIL]` statt der Symbole.
|
||||
- **`talk test`** spricht einen festen Testsatz direkt in diesem Prozess
|
||||
(nicht über den Daemon; Mute und Ruhezeit gelten hier nicht, ein aktives
|
||||
Mute wird genannt) und zeigt, welche Stufe gesprochen hat und wie lange
|
||||
Synthese und Wiedergabe gedauert haben.
|
||||
- **`talk voices`** listet den kuratierten Stimmen-Katalog (ElevenLabs-
|
||||
Premade, OpenAI, Edge-Neural je Locale) als Tabelle; `*` markiert die
|
||||
Stimme, die talk ohne `--voice` nimmt (config `voice.<provider>.<lang>`
|
||||
oder Repo-Default). Config-Einträge außerhalb des Katalogs erscheinen
|
||||
als eigene Zeile. Jede Voice-ID des Providers funktioniert auch ohne
|
||||
Katalog-Eintrag.
|
||||
- **`talk setup`** fragt im Terminal: Keys (nur Hinweis — ohne Key läuft
|
||||
die Edge-Stimme), Stimme für Deutsch und Englisch der aktiven Stufe
|
||||
(Nummer aus dem Katalog oder Voice-ID, probehören, übernehmen),
|
||||
Ruhezeit. Dann schreibt es die config.toml — eine bestehende Datei wird
|
||||
zusammengeführt (andere Einstellungen bleiben) und als `.bak` gesichert
|
||||
— und spricht einen Testsatz. `q` bricht jederzeit ab, geschrieben wird
|
||||
erst ganz am Ende. Ohne Terminal (Pipe, Agent) stellt setup keine Fragen
|
||||
und zeigt nur die wirksame Konfiguration.
|
||||
- **`talk doctor`** reports one line per item with ✔ (ok), ⚠ (note), ✘
|
||||
(error): config file, daemon (running / starts on the next speak command
|
||||
/ foreign service on the port), audio output (can oto initialize? `null`
|
||||
= no sound), every provider tier (key present, endpoint, default voice;
|
||||
OS voice: which mechanism was found), notifications, mute, laws
|
||||
including an active quiet window. Exit 0 without ✘, else 1; a daemon that
|
||||
is not running is ⚠. `--probe` runs real synthesis probes (network),
|
||||
`--json` returns the list for machines. Windows consoles get
|
||||
`[OK]`/`[WARN]`/`[FAIL]` instead of the symbols.
|
||||
- **`talk test`** speaks a fixed test sentence directly in this process (not
|
||||
via the daemon; mute and quiet hours do not apply here, an active mute is
|
||||
mentioned) and shows which tier spoke and how long synthesis and playback
|
||||
took.
|
||||
- **`talk voices`** lists the curated voice catalog (ElevenLabs premade,
|
||||
OpenAI, Edge neural voices per locale) as a table; `*` marks the voice
|
||||
talk uses without `--voice` (config `voice.<provider>.<lang>` or the
|
||||
built-in default). Config entries outside the catalog appear as their own
|
||||
row. Any voice ID the provider knows works without a catalog entry.
|
||||
- **`talk setup`** asks in the terminal: keys (only a hint; without a key
|
||||
the Edge voice speaks), the fallback language, a voice for each language
|
||||
of the active tier (number from the catalog or a voice ID, preview,
|
||||
accept), quiet hours. Then it writes config.toml, merging an existing
|
||||
file (other settings stay) and backing it up as `.bak`, and speaks a
|
||||
test sentence. `q` aborts at any point; the file is written only at the
|
||||
very end. Without a terminal (pipe, agent) setup asks nothing and only
|
||||
shows the effective configuration.
|
||||
|
||||
### Language
|
||||
|
||||
talk detects German and English per announcement (word lists plus umlauts;
|
||||
no ML) and picks the voice accordingly. When it cannot tell (numbers,
|
||||
single words, ties) it uses the configured fallback language, which is also
|
||||
the language of talk's own announcements such as the `wrap` result:
|
||||
|
||||
```toml
|
||||
lang = "en" # de | en; default en (env TALK_LANG)
|
||||
```
|
||||
|
||||
## Daemon
|
||||
|
||||
Es gibt genau ein Binary. Der Daemon ist `talk daemon run` — derselbe Code,
|
||||
im Hintergrund. Er besitzt die Ansage-Queue und spricht seriell ab — genau
|
||||
eine Ansage gleichzeitig, Reihenfolge `now` > `high` > `normal` > `low`
|
||||
(FIFO je Level). Der HTTP-Server lauscht nur auf `127.0.0.1`; Port: Default
|
||||
`7800`, config `[server] port`, Env `TALK_PORT` (schlagen den Default).
|
||||
There is exactly one binary. The daemon is `talk daemon run`: the same code,
|
||||
in the background. It owns the announcement queue and plays serially, exactly
|
||||
one announcement at a time, in the order `now` > `high` > `normal` > `low`
|
||||
(FIFO per level). The HTTP server listens on `127.0.0.1` only; port: default
|
||||
`7800`, config `[server] port`, env `TALK_PORT`.
|
||||
|
||||
**Autostart:** Läuft kein Daemon, startet `talk say` (und `talk wrap`) ihn
|
||||
beim ersten Sprechbefehl selbst — losgelöst vom Terminal, mit Protokoll in
|
||||
`daemon.log` und Prozess-ID in `daemon.pid` im Config-Dir; stderr meldet
|
||||
einmalig „Daemon gestartet". Der Daemon läuft dann weiter (Leerlauf kostet
|
||||
nichts Nennenswertes), bis `talk daemon stop`, Abmelden oder Neustart.
|
||||
Parallele erste Aufrufe sind harmlos: Verliert einer das Port-Rennen,
|
||||
benutzt er den Daemon des anderen. Wer den Daemon selbst verwaltet
|
||||
(systemd, launchd, Task Scheduler mit `talk daemon run`), schaltet den
|
||||
Autostart ab:
|
||||
**Autostart:** when no daemon runs, `talk say` (and `talk wrap`) start it on
|
||||
the first speak command, detached from the terminal, logging to `daemon.log`
|
||||
with its PID in `daemon.pid` in the config directory; stderr says "daemon
|
||||
started" once. The daemon then keeps running (idle costs next to nothing)
|
||||
until `talk daemon stop`, logout or reboot. Parallel first calls are
|
||||
harmless: whoever loses the port race uses the other one's daemon. If you
|
||||
manage the daemon yourself (systemd, launchd, Task Scheduler with `talk
|
||||
daemon run`), turn autostart off:
|
||||
|
||||
```toml
|
||||
[daemon]
|
||||
autostart = false # Env TALK_AUTOSTART=false
|
||||
autostart = false # env TALK_AUTOSTART=false
|
||||
```
|
||||
|
||||
Steuerbefehle (`mute`, `unmute`, `status`, `cancel`) starten den Daemon
|
||||
bewusst nicht: `talk status` sagt dann „läuft nicht" (Exit 1), `talk mute`
|
||||
schreibt die Mute-Datei direkt.
|
||||
Control commands (`mute`, `unmute`, `status`, `cancel`) deliberately do not
|
||||
start the daemon: `talk status` then says "not running" (exit 1), `talk
|
||||
mute` writes the mute file directly.
|
||||
|
||||
| Endpunkt | Wirkung |
|
||||
| Endpoint | Effect |
|
||||
| --- | --- |
|
||||
| `POST /speak` | `{text, priority?, provider?, voice?, lang?, now?}` → `202 {"id":"…"}` (+ `"truncated":true`, wenn auf `max_text` gekürzt); bei Mute: `202 {"id":"","muted":true}`; bei Gesetz-Verwurf: `202 {"id":"","discarded":"rate_limit"\|"quiet_hours"}` (+ `"toast":true` im Toast-Modus) — verworfen, dokumentiertes Verhalten |
|
||||
| `OPTIONS /speak` | CORS-Preflight → `204` mit Freigabe-Headern, wenn die `Origin` in `server.allowed_origins` steht (siehe unten); sonst `204` ohne CORS-Header |
|
||||
| `POST /speak` | `{text, priority?, provider?, voice?, lang?, now?}` → `202 {"id":"…"}` (+ `"truncated":true` when cut to `max_text`); muted: `202 {"id":"","muted":true}`; law discard: `202 {"id":"","discarded":"rate_limit"\|"quiet_hours"}` (+ `"toast":true` in toast mode); documented behaviour, not an error |
|
||||
| `OPTIONS /speak` | CORS preflight → `204` with allow headers when the `Origin` is listed in `server.allowed_origins` (see below); otherwise `204` without CORS headers |
|
||||
| `GET /status` | `{"version", "queue", "queue_ids", "current", "muted", "provider", "rate_limit": {"interval", "allow_high_burst", "next_slot"}, "quiet_hours": {"window", "active", "mode", "until"}, "limits": {"max_text", "max_age"}, "mcp": true, "cors": [origins]}` |
|
||||
| `POST /cancel` | bricht die laufende Ansage ab + leert die Queue → `200 {"cancelled":n}` |
|
||||
| `POST /mute` / `POST /unmute` | persistenter Mute-State → `200 {"muted":bool}` |
|
||||
| `POST /shutdown` | fährt den Daemon herunter (`talk daemon stop`) → `200 {"ok":true}` |
|
||||
| `POST /mcp` | MCP (Streamable HTTP): JSON-RPC `initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call` — ein Tool `speak` (siehe unten) |
|
||||
| `GET /mcp` | `405` — der Daemon bietet keine server-initiierten Streams an |
|
||||
| `POST /cancel` | aborts the playing announcement and empties the queue → `200 {"cancelled":n}` |
|
||||
| `POST /mute` / `POST /unmute` | persistent mute state → `200 {"muted":bool}` |
|
||||
| `POST /shutdown` | shuts the daemon down (`talk daemon stop`) → `200 {"ok":true}` |
|
||||
| `POST /mcp` | MCP (Streamable HTTP): JSON-RPC `initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call`; one tool `speak` (see below) |
|
||||
| `GET /mcp` | `405`; the daemon offers no server-initiated streams |
|
||||
|
||||
- **CLI-Verhalten:** `talk say` (und `talk "Text"`) erkennt den Daemon via
|
||||
`GET /status`. Läuft er: enqueue + sofortige Rückkehr (non-blocking,
|
||||
stdout erhält die Item-ID); `--wait` pollt bis zur Fertigstellung;
|
||||
`--now` springt vor die Warteschlange; bei Mute sagt die CLI `muted`,
|
||||
bei Gesetz-Verwurf `discarded: <grund>`. `talk mute`/`unmute`/`status`/
|
||||
`cancel` sind Daemon-Aufrufe mit plain Output.
|
||||
- **Mute ist persistent** (Datei `mute` im Config-Dir, Inhalt `true`/
|
||||
`false`) und überlebt Daemon-Restarts. `talk mute` ohne laufenden Daemon
|
||||
schreibt die Datei direkt — der Daemon liest sie bei jeder Ansage neu,
|
||||
der Zustand greift also auch offline.
|
||||
- **Direkt-Pfad:** Ist kein Daemon erreichbar und keiner startbar
|
||||
(Autostart aus, Port fremd belegt, Start gescheitert), spricht `talk`
|
||||
direkt im eigenen Prozess — blockierend, ohne Warteschlange (parallele
|
||||
Aufrufe können überlappen), mit Hinweis und Grund auf stderr. Mute wird
|
||||
auch hier respektiert. Nichts bricht still.
|
||||
- **CLI behaviour:** `talk say` (and `talk "text"`) detects the daemon via
|
||||
`GET /status`. If it runs: enqueue and return at once (non-blocking,
|
||||
stdout gets the item ID); `--wait` polls until spoken; `--now` jumps the
|
||||
queue; when muted the CLI prints `muted`, on a law discard
|
||||
`discarded: <reason>`. `talk mute`/`unmute`/`status`/`cancel` are daemon
|
||||
calls with plain output.
|
||||
- **Mute is persistent** (file `mute` in the config directory, content
|
||||
`true`/`false`) and survives daemon restarts. `talk mute` without a
|
||||
running daemon writes the file directly; the daemon re-reads it on every
|
||||
announcement, so the state applies offline too.
|
||||
- **Direct path:** if no daemon is reachable and none can be started
|
||||
(autostart off, port taken by something else, start failed), `talk`
|
||||
speaks directly in its own process: blocking, without a queue (parallel
|
||||
calls may overlap), with a note and the reason on stderr. Mute is
|
||||
honoured here too. Nothing fails silently.
|
||||
|
||||
### Rate-Limit + Ruhezeiten (Task 05)
|
||||
### Rate limit, quiet hours, length and age
|
||||
|
||||
Der Daemon erzwingt das Anti-Spam-Gesetz als Backstop unter allem
|
||||
Prompt-Verhalten. **Rate-Limit:** Jede *gestartete* Ansage bucht ein
|
||||
Zeitfenster — Default **1 Ansage / 30 s**. Bis zum Fenster-Ablauf gilt:
|
||||
The daemon enforces the anti-spam laws as a backstop under whatever a
|
||||
prompt does. **Rate limit:** every *started* announcement books a time
|
||||
window, default **1 announcement / 30 s**. Until the window elapses:
|
||||
|
||||
- `priority=low` wird **verworfen** (schon beim Eintritt, die 202-Antwort
|
||||
nennt `discarded: "rate_limit"`); war das Fenster beim Eintritt noch
|
||||
frei, entscheidet der Worker später.
|
||||
- `normal` **parkt** in der Queue und spielt zum freien Slot (in `/status`
|
||||
als `rate_limit.next_slot` sichtbar).
|
||||
- `priority=high` darf mit `queue.allow_high_burst = true` (Default)
|
||||
**bewusst verstoßen**; ohne Burst-Recht wartet high wie normal.
|
||||
- `now:true` **umgeht auch das Rate-Limit** — der dokumentierte Ausbruch
|
||||
für Dringendes (startet trotzdem ein neues Fenster).
|
||||
- `priority=low` is **discarded** (already on admission; the 202 response
|
||||
says `discarded: "rate_limit"`); if the window was open on admission, the
|
||||
worker decides later.
|
||||
- `normal` **parks** in the queue and plays at the next open slot (visible
|
||||
in `/status` as `rate_limit.next_slot`).
|
||||
- `priority=high` may **deliberately break** the limit with
|
||||
`queue.allow_high_burst = true` (default); without burst rights high
|
||||
waits like normal.
|
||||
- `now:true` **bypasses the rate limit too**; the documented escape for
|
||||
urgent matters (it still books a new window).
|
||||
|
||||
**Ruhezeiten:** `quiet_hours = "18:00-08:00"` (lokale Zeit, überspannt
|
||||
Mitternacht; leer/`off` = aus — bewusst kein Default-Fenster, Ruhezeiten
|
||||
sind Opt-in wie Mute). In der Ruhezeit verfällt Audio, auch für high
|
||||
(Burst ist ein Rate-Recht, kein Ruhe-Recht):
|
||||
**Quiet hours:** `quiet_hours = "18:00-08:00"` (local time, may span
|
||||
midnight; empty/`off` = disabled; deliberately no default window, quiet
|
||||
hours are opt-in like mute). During quiet hours audio is dropped, also for
|
||||
high (burst is a rate right, not a quiet right):
|
||||
|
||||
- `quiet_mode = "discard"` (Default): still verwerfen.
|
||||
- `quiet_mode = "toast"`: Desktop-Benachrichtigung statt Audio — über die
|
||||
Bordmittel des Systems (Linux `notify-send`, macOS Notification Center,
|
||||
Windows Toast), best effort: fehlt das Werkzeug, bleibt es bei der
|
||||
Protokoll-Zeile, `talk doctor` zeigt es unter „Toast".
|
||||
- `now:true` **bricht auch die Ruhezeit** (bewusster Ausbruch).
|
||||
- `quiet_mode = "discard"` (default): drop silently.
|
||||
- `quiet_mode = "toast"`: a desktop notification instead of audio, through
|
||||
the system's own tools (Linux `notify-send`, macOS Notification Center,
|
||||
Windows toast), best effort: if the tool is missing, only the log line
|
||||
remains and `talk doctor` shows it under "Toast".
|
||||
- `now:true` **breaks out of quiet hours too** (a deliberate interruption).
|
||||
|
||||
**Länge und Alter:** Zwei weitere Gesetze gegen das, was Prompts gern tun
|
||||
— die Langantwort vorlesen und Ansagen stapeln:
|
||||
**Length and age:** two more laws against what prompts like to do, reading
|
||||
out the long answer and piling up announcements:
|
||||
|
||||
- `max_text` (Default **400 Zeichen**, etwa zwei bis drei Sätze): Längere
|
||||
Texte werden beim Eintritt an einer Wortgrenze **gekürzt** und so
|
||||
gesprochen; die Antwort meldet `"truncated": true`, die CLI warnt auf
|
||||
stderr, das MCP-Ergebnis sagt es im Text. Der Nutzer hört den Anfang
|
||||
statt zehn Minuten Vortrag, das API-Budget bleibt begrenzt.
|
||||
- `max_age` (Default **2 Minuten**): Was länger in der Queue parkt (Rate-
|
||||
Limit, Stau), ist keine Nachricht mehr und wird beim Fälligwerden
|
||||
**verworfen** (`discarded: "expired"` im Protokoll; die 202-Antwort beim
|
||||
Eintritt bleibt eine Zusage, denn beim Eintritt ist nichts veraltet).
|
||||
`now:true` wartet nie lange und ist ausgenommen.
|
||||
- `max_text` (default **400 characters**, two to three sentences): longer
|
||||
texts are **cut** at a word boundary on admission and spoken like that;
|
||||
the response reports `"truncated": true`, the CLI warns on stderr, the
|
||||
MCP result says so in its text. The user hears the beginning instead of a
|
||||
ten-minute lecture, and the API budget stays bounded.
|
||||
- `max_age` (default **2 minutes**): whatever parks longer in the queue
|
||||
(rate limit, backlog) is no longer news and is **discarded** when it
|
||||
comes due (`discarded: "expired"` in the log; the 202 on admission stays a
|
||||
promise, because nothing is stale on admission). `now:true` never waits
|
||||
long and is exempt.
|
||||
|
||||
**Rangordnung:** manuelles Mute gewinnt immer > `now` > veraltet (`max_age`)
|
||||
> Ruhezeit > Rate-Limit; `max_text` greift beim Eintritt auf alles.
|
||||
Konfiguration (config.toml, Env schlägt Datei):
|
||||
**Precedence:** manual mute always wins > `now` > stale (`max_age`) > quiet
|
||||
hours > rate limit; `max_text` applies on admission to everything.
|
||||
Configuration (config.toml; env beats the file):
|
||||
|
||||
```toml
|
||||
[queue]
|
||||
rate_limit = "30s" # Dauer; "0s" = unbegrenzt (TALK_RATE_LIMIT)
|
||||
allow_high_burst = true # high darf verstoßen (TALK_ALLOW_HIGH_BURST)
|
||||
max_text = 400 # Zeichen; 0 = unbegrenzt (TALK_MAX_TEXT)
|
||||
max_age = "2m" # Wartezeit; "0s" = unbegrenzt (TALK_MAX_AGE)
|
||||
rate_limit = "30s" # duration; "0s" = unlimited (TALK_RATE_LIMIT)
|
||||
allow_high_burst = true # high may break the limit (TALK_ALLOW_HIGH_BURST)
|
||||
max_text = 400 # characters; 0 = unlimited (TALK_MAX_TEXT)
|
||||
max_age = "2m" # longest wait; "0s" = unlimited (TALK_MAX_AGE)
|
||||
|
||||
quiet_hours = "18:00-08:00" # leer = aus; Env TALK_QUIET_HOURS ("off" = aus)
|
||||
quiet_hours = "18:00-08:00" # empty = off; env TALK_QUIET_HOURS ("off" = off)
|
||||
quiet_mode = "discard" # discard | toast (TALK_QUIET_MODE)
|
||||
```
|
||||
|
||||
`talk status` zeigt den Gesetz-State: `rate-limit: 30s (high-burst
|
||||
erlaubt), nächster slot …`, `ruhezeit: 18:00-08:00 (modus discard,
|
||||
aktiv bis 08:00)` und `limits: max. 400 Zeichen, max. Wartezeit 2m0s`.
|
||||
`talk status` shows the law state: `rate-limit: 30s (high burst allowed),
|
||||
next slot …`, `quiet-hours: 18:00-08:00 (mode discard, active until 08:00)`
|
||||
and `limits: max 400 characters, max wait 2m0s`.
|
||||
|
||||
### CORS-Tür + MCP-Endpoint (Task 06)
|
||||
### CORS door and MCP endpoint
|
||||
|
||||
**CORS** gilt bewusst nur für `POST /speak` — genau die eine Tür, die eine
|
||||
spätere Browser-Anbindung (z. B. LibreChat) braucht; Steuer-Endpunkte
|
||||
bleiben browserseitig unerreicht. Die Erlaubnis ist eine exakte Origin-
|
||||
Liste; `"*"` ist erlaubt, aber nicht Default; **leere Liste (Default) =
|
||||
gar keine CORS-Header = der Browser blockt**:
|
||||
**CORS** applies to `POST /speak` only, the one door a later browser
|
||||
integration (for example a web chat UI) needs; control endpoints stay out of
|
||||
reach of browsers. The allow-list holds exact origins; `"*"` is allowed but
|
||||
not the default; **an empty list (default) means no CORS headers at all**:
|
||||
|
||||
```toml
|
||||
[server]
|
||||
allowed_origins = ["https://chat.az-gruppe.com"] # Env TALK_ALLOWED_ORIGINS
|
||||
allowed_origins = ["https://chat.example.com"] # env TALK_ALLOWED_ORIGINS
|
||||
```
|
||||
|
||||
- `OPTIONS /speak` (Preflight) von gelisteter Origin → `204` mit
|
||||
`Access-Control-Allow-Origin` (die Origin bzw. `*`), `-Methods: POST,
|
||||
- `OPTIONS /speak` (preflight) from a listed origin → `204` with
|
||||
`Access-Control-Allow-Origin` (the origin or `*`), `-Methods: POST,
|
||||
OPTIONS`, `-Headers: Content-Type`, `Max-Age: 600`.
|
||||
- Echter `POST /speak` mit gelisteter `Origin` trägt denselben ACAO-Header.
|
||||
- **Die Tür ist serverseitig zu, nicht nur browserseitig.** CORS allein
|
||||
verhindert nur das *Lesen* der Antwort; einen POST mit `text/plain`-Body
|
||||
oder einen Formular-POST schickt jede Webseite ohne Preflight ab. Deshalb
|
||||
lehnt der Daemon jede Anfrage mit fremdem `Origin`-Header mit `403` ab, die
|
||||
nicht POST /speak von einer gelisteten Origin ist — also ungelistete
|
||||
Origins auf /speak, jeden Browser-Zugriff auf `/status`, `/mute`,
|
||||
`/unmute`, `/cancel` und `/mcp`, auch von gelisteten Origins. Nur der
|
||||
Preflight `OPTIONS /speak` bleibt für ungelistete Origins ein `204` ohne
|
||||
CORS-Header (ohne Wirkung; der Browser verwirft den geplanten POST).
|
||||
CLI, curl und MCP-Clients senden keinen `Origin`-Header und sind nicht
|
||||
betroffen.
|
||||
- A real `POST /speak` with a listed `Origin` carries the same header.
|
||||
- **The door is shut server-side, not only browser-side.** CORS alone only
|
||||
prevents *reading* the response; any web page can send a POST with a
|
||||
`text/plain` body or a form POST without a preflight. Therefore the
|
||||
daemon rejects every request with a foreign `Origin` header with `403`
|
||||
unless it is POST /speak from a listed origin: unlisted origins on
|
||||
/speak, and any browser access to `/status`, `/mute`, `/unmute`,
|
||||
`/cancel` and `/mcp`, even from listed origins. Only the preflight
|
||||
`OPTIONS /speak` stays a `204` without CORS headers for unlisted origins
|
||||
(it has no effect; the browser drops the planned POST). CLI, curl and MCP
|
||||
clients send no `Origin` header and are unaffected.
|
||||
|
||||
**MCP** (`POST /mcp`, Streamable HTTP): schlanke eigene Implementierung
|
||||
des JSON-RPC-Subsets ohne SDK — `initialize` (angefragte
|
||||
`protocolVersion` wird geechot; Ergebnis `protocolVersion`,
|
||||
`capabilities: {tools:{}}`, `serverInfo`), `notifications/initialized`
|
||||
(Notification → `202` ohne Body), `ping`, `tools/list`,
|
||||
`tools/call`. Der Server ist zustandslos (keine Session-IDs) und pusht
|
||||
nichts (daher `GET /mcp` → `405`). Ein Tool:
|
||||
**MCP** (`POST /mcp`, Streamable HTTP): a lean own implementation of the
|
||||
JSON-RPC subset, no SDK: `initialize` (the requested `protocolVersion` is
|
||||
echoed; result `protocolVersion`, `capabilities: {tools:{}}`,
|
||||
`serverInfo`), `notifications/initialized` (notification → `202` without
|
||||
body), `ping`, `tools/list`, `tools/call`. The server is stateless (no
|
||||
session IDs) and pushes nothing (hence `GET /mcp` → `405`). One tool:
|
||||
|
||||
```json
|
||||
{"name": "speak",
|
||||
@@ -257,13 +264,13 @@ nichts (daher `GET /mcp` → `405`). Ein Tool:
|
||||
"required": ["text"]}}
|
||||
```
|
||||
|
||||
`tools/call` fährt exakt denselben Aufnahme-Pfad wie `POST /speak`
|
||||
(inklusive Mute, Rate-Limit, Ruhezeiten). Ergebnis ist immer Text-Content:
|
||||
`"queued: <id>"` bei Aufnahme; `muted`/`discarded: <grund>` mit Erklärung
|
||||
— bewusst `isError: false`, denn ein Verwurf ist das korrekte
|
||||
dokumentierte Ergebnis, kein Protokollfehler. Validierungsfehler
|
||||
(fehlender Text, ungültige Priorität, unbekanntes Tool) sind dagegen
|
||||
JSON-RPC-Fehler (`-32602`). Handshake als curl-Sequenz:
|
||||
`tools/call` takes exactly the same admission path as `POST /speak`
|
||||
(including mute, rate limit, quiet hours, truncation). The result is always
|
||||
text content: `"queued: <id>"` on admission; `muted`/`discarded: <reason>`
|
||||
with an explanation, deliberately `isError: false`, because a discard is
|
||||
the correct documented result, not a protocol error. Validation errors
|
||||
(missing text, invalid priority, unknown tool) are JSON-RPC errors
|
||||
(`-32602`). Handshake as a curl sequence:
|
||||
|
||||
```sh
|
||||
curl -s http://127.0.0.1:7800/mcp -H 'Content-Type: application/json' \
|
||||
@@ -271,140 +278,142 @@ curl -s http://127.0.0.1:7800/mcp -H 'Content-Type: application/json' \
|
||||
curl -s http://127.0.0.1:7800/mcp -H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
|
||||
curl -s http://127.0.0.1:7800/mcp -H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"speak","arguments":{"text":"Build fertig"}}}'
|
||||
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"speak","arguments":{"text":"Build done"}}}'
|
||||
```
|
||||
|
||||
### Provider-Kette (Task 03)
|
||||
### Provider chain
|
||||
|
||||
Ohne Pin läuft die Vier-Stufen-Fallback-Kette
|
||||
**ElevenLabs → OpenAI TTS → Edge-TTS → OS built-in**: Bei fehlendem Key,
|
||||
HTTP-Fehler, Timeout oder Netz-Problem fällt die Kette automatisch auf die
|
||||
nächste Stufe; jeder Fallback wird auf stderr protokolliert (nie still).
|
||||
Pinnen per `--provider=<name>` oder `provider = "<name>"` in der
|
||||
config.toml.
|
||||
Without a pin, the four-tier fallback chain runs
|
||||
**ElevenLabs → OpenAI TTS → Edge TTS → OS built-in**: on a missing key, an
|
||||
HTTP error, a timeout or a network problem the chain falls through to the
|
||||
next tier automatically; every fallback is logged to stderr (never silent).
|
||||
Pin with `--provider=<name>` or `provider = "<name>"` in config.toml.
|
||||
|
||||
| Stufe | Key (Env) | Default-Endpunkt | Audio |
|
||||
| Tier | Key (env) | Default endpoint | Audio |
|
||||
| --- | --- | --- | --- |
|
||||
| `elevenlabs` | `TALK_ELEVENLABS_KEY` | `https://api.elevenlabs.io` | mp3 |
|
||||
| `openai` | `TALK_OPENAI_KEY` | `https://api.openai.com` | mp3 |
|
||||
| `edge` | — (kostenlos) | `wss://speech.platform.bing.com/...` | mp3 |
|
||||
| `osbuiltin` | — | lokale OS-TTS | WAV/PCM |
|
||||
| `edge` | none (free) | `wss://speech.platform.bing.com/...` | mp3 |
|
||||
| `osbuiltin` | none | local OS TTS | WAV/PCM |
|
||||
|
||||
Keys laufen ausschließlich über Env-Variablen (nie in die config.toml).
|
||||
Endpunkte sind konfigurierbar — Datei `[providers.<name>] endpoint = "…"`
|
||||
bzw. Env `TALK_ELEVENLABS_ENDPOINT` / `TALK_OPENAI_ENDPOINT` /
|
||||
`TALK_EDGE_ENDPOINT` (Env schlägt Datei; so zeigen Tests/CI die Kette auf
|
||||
Fake-Server). Das OpenAI-Modell ist konfigurierbar
|
||||
(`[providers.openai] model`, Default `tts-1`).
|
||||
Keys come exclusively from environment variables (never config.toml).
|
||||
Endpoints are configurable: file `[providers.<name>] endpoint = "…"` or env
|
||||
`TALK_ELEVENLABS_ENDPOINT` / `TALK_OPENAI_ENDPOINT` / `TALK_EDGE_ENDPOINT`
|
||||
(env beats the file; this is how tests and CI point the chain at fake
|
||||
servers). The OpenAI model is configurable (`[providers.openai] model`,
|
||||
default `tts-1`).
|
||||
|
||||
Stimmen: neutralen Repo-Defaults je Provider+Sprache (ElevenLabs
|
||||
„Rachel", OpenAI „alloy", Edge `de-DE-KatjaNeural`/`en-US-AriaNeural`)
|
||||
kann man pro Sprache überordnen — config.toml:
|
||||
Voices: the neutral built-in defaults per provider and language (ElevenLabs
|
||||
"Rachel", OpenAI "alloy", Edge `de-DE-KatjaNeural`/`en-US-AriaNeural`) can
|
||||
be overridden per language in config.toml:
|
||||
|
||||
```toml
|
||||
[voice.elevenlabs]
|
||||
de = "21m00Tcm4TlvDq8ikWAM" # eigene Stimme je Sprache
|
||||
de = "21m00Tcm4TlvDq8ikWAM" # your own voice per language
|
||||
en = "21m00Tcm4TlvDq8ikWAM"
|
||||
|
||||
[voice.edge]
|
||||
de = "de-DE-KatjaNeural"
|
||||
```
|
||||
|
||||
Rangfolge: `--voice`-Flag > `voice.<provider>.<lang>` > Repo-Default.
|
||||
MP3 wird von der Binärdatei selbst dekodiert (reiner Go-Dekoder, kein
|
||||
ffmpeg, CGO-frei).
|
||||
Precedence: `--voice` flag > `voice.<provider>.<lang>` > built-in default.
|
||||
MP3 is decoded by the binary itself (pure Go decoder, no ffmpeg, CGO-free).
|
||||
|
||||
### OS-TTS auf Linux (Dev-Abhängigkeit espeak)
|
||||
Edge TTS is an undocumented Microsoft endpoint with a DRM token scheme that
|
||||
Microsoft changes now and then. When that happens, the edge tier fails with
|
||||
a clear "protocol changed?" error and the chain falls through to the OS
|
||||
voice; `talk doctor --probe` makes it visible.
|
||||
|
||||
Der OS-Provider `osbuiltin` nutzt auf Linux zwei Wege:
|
||||
### OS TTS on Linux
|
||||
|
||||
- **Audio-Bytes** (Wiedergabe über den eingebauten oto-Sink): benötigt
|
||||
`espeak` (oder `espeak-ng`). Auf NixOS als Dev-Abhängigkeit:
|
||||
The `osbuiltin` provider uses two routes on Linux:
|
||||
|
||||
- **Audio bytes** (playback through the built-in oto sink): needs
|
||||
`espeak-ng` (or `espeak`). On NixOS as a dev dependency:
|
||||
`nix shell nixpkgs#espeak-ng`.
|
||||
- **Direktansage** ohne espeak: `spd-say` (speech-dispatcher) — spielt
|
||||
selbst über den Daemon ab, kann aber nach `spd-say --help` keine Bytes
|
||||
in Dateien/Pipes liefern. `talk` nutzt diesen Weg automatisch, wenn
|
||||
kein espeak vorhanden ist.
|
||||
- **Direct speech** without espeak: `spd-say` (speech-dispatcher) plays by
|
||||
itself through its daemon but cannot deliver bytes to a file or pipe.
|
||||
`talk` takes this route automatically when espeak is missing.
|
||||
|
||||
`TALK_AUDIO_SINK=null` (oder `audio.sink = "null"` in der config.toml)
|
||||
schaltet jede Tongabe ab — für CI und Server.
|
||||
`TALK_AUDIO_SINK=null` (or `audio.sink = "null"` in config.toml) turns all
|
||||
sound off, for CI and servers.
|
||||
|
||||
## `talk wrap` — Befehle annoncieren (Task 07)
|
||||
## `talk wrap`: announce commands
|
||||
|
||||
`talk wrap -- <Befehl> [Argumente …]` führt den Befehl aus und macht das
|
||||
Ergebnis hörbar: stdin/stdout/stderr werden durchgereicht — stdout gehört
|
||||
allein dem Befehl, Informationszeilen von talk (z. B. die Item-ID der
|
||||
Ansage) gehen auf stderr, damit Pipes und Redirects sauber bleiben. Die
|
||||
Wandzeit wird gemessen und nach dem Ende des Befehls angesagt:
|
||||
`talk wrap -- <command> [args …]` runs the command and makes the result
|
||||
audible: stdin/stdout/stderr are passed through; stdout belongs to the
|
||||
command alone, talk's information lines (for example the announcement's
|
||||
item ID) go to stderr so pipes and redirects stay clean. Wall time is
|
||||
measured and announced after the command ends:
|
||||
|
||||
- Erfolg: „sleep erfolgreich. Dauer: 2 Sekunden."
|
||||
- Misserfolg: „sh fehlgeschlagen, Code 2." (ohne Dauer)
|
||||
- success: "sleep succeeded. Duration: 2 seconds."
|
||||
- failure: "sh failed, code 2." (without duration)
|
||||
|
||||
```console
|
||||
talk wrap -- sleep 2
|
||||
talk wrap --wait -- make test
|
||||
```
|
||||
|
||||
- **`--` schirmt den Befehl komplett ab:** Das erste allein stehende `--`
|
||||
beendet die talk-Flag-Auswertung — alles danach geht unverändert an den
|
||||
Befehl. `talk wrap -- foo --bar` reicht `--bar` an foo weiter, statt es
|
||||
als talk-Flag zu fehlzuinterpretieren. talk-Flags für die Ansage stehen
|
||||
deshalb davor (`talk wrap --wait -- …`); ohne `--` gelten die üblichen
|
||||
Regeln (Flags überall, auch nach Positionalargumenten).
|
||||
- **Label** der Ansage: `wrap.label` in der config.toml (Env
|
||||
`TALK_WRAP_LABEL` schlägt die Datei) oder der Basisname des Befehls
|
||||
(`/usr/bin/make` → „make"). Die Sprache erkennt langdetect am Label
|
||||
(de/en, deutscher Default); `--lang` überschreibt.
|
||||
- **Exit-Code-Politik:** wrap exitet immer mit dem Code des Befehls —
|
||||
scheitert die Ansage selbst, bleibt der Befehls-Code trotzdem (nur eine
|
||||
Warning auf stderr; talk-Fehler ≠ Befehls-Fehler). Nicht startbare
|
||||
Befehle (Binary nicht gefunden) zählen als 127 (Shell-Konvention),
|
||||
Signaltode portabel als 1. Benutzungsfehler von talk selbst (kein
|
||||
Befehl, ungültige Flags) scheitern vor dem Start des Befehls.
|
||||
- **Ansageweg:** derselbe wie say — Daemon (enqueue, non-blocking;
|
||||
`--wait` respektiert; Autostart), ohne Daemon der Direkt-Pfad.
|
||||
wrap erbt die say-Flags (`--wait`, `--now`, `--priority`, `--voice`,
|
||||
`--lang`, `--provider`) und reicht sie an die Ansage durch.
|
||||
- **`--` shields the command completely:** the first standalone `--` ends
|
||||
talk's flag parsing; everything after it goes to the command unchanged.
|
||||
`talk wrap -- foo --bar` passes `--bar` to foo instead of misreading it
|
||||
as a talk flag. talk's flags for the announcement therefore go before it
|
||||
(`talk wrap --wait -- …`); without `--` the usual rules apply (flags
|
||||
anywhere, also after positional arguments).
|
||||
- **Label** of the announcement: `wrap.label` in config.toml (env
|
||||
`TALK_WRAP_LABEL` beats the file) or the command's base name
|
||||
(`/usr/bin/make` → "make"). Language detection runs on the label (de/en,
|
||||
falling back to the configured `lang`); `--lang` overrides.
|
||||
- **Exit code policy:** wrap always exits with the command's code; if the
|
||||
announcement itself fails, the command's code still stands (only a
|
||||
warning on stderr; talk errors are not command errors). Commands that
|
||||
cannot start (binary not found) count as 127 (shell convention), signal
|
||||
deaths portably as 1. Usage errors of talk itself (no command, invalid
|
||||
flags) fail before the command starts.
|
||||
- **Announcement path:** the same as say: daemon (enqueue, non-blocking;
|
||||
`--wait` honoured; autostart), without a daemon the direct path. wrap
|
||||
inherits the say flags (`--wait`, `--now`, `--priority`, `--voice`,
|
||||
`--lang`, `--provider`) and passes them on.
|
||||
|
||||
```toml
|
||||
[wrap]
|
||||
label = "Deployment" # Ansage-Label statt Befehls-Basisname (TALK_WRAP_LABEL)
|
||||
label = "Deployment" # announcement label instead of the command's base name (TALK_WRAP_LABEL)
|
||||
```
|
||||
|
||||
## Für Agenten: Skill und Standing-Order
|
||||
## For agents: skill and standing order
|
||||
|
||||
[`skills/talk/SKILL.md`](skills/talk/SKILL.md) ist die Etikette als
|
||||
Skill-Artefakt: **striktes Opt-in** (Sprechen nur auf Aufforderung in der
|
||||
Session oder Standing-Order in der AGENTS.md, session-sticky bis Widerruf),
|
||||
Inhalt = Status · nächster Schritt · wofür der Nutzer gebraucht wird, nie
|
||||
die Langantwort, `muted`/`discarded` respektieren. Der Drei-Zeilen-Block
|
||||
für Projekt-AGENTS.md steht in
|
||||
[`skills/talk/AGENTS-snippet.md`](skills/talk/AGENTS-snippet.md). Der
|
||||
Daemon erzwingt Rate-Limit, Längen- und Alters-Grenze, Mute und Ruhezeit
|
||||
unabhängig davon, was im Prompt steht.
|
||||
[`skills/talk/SKILL.md`](skills/talk/SKILL.md) is the etiquette as a skill
|
||||
artifact: **strict opt-in** (speak only when asked in the session or by a
|
||||
standing order in the project's AGENTS.md, session-sticky until revoked),
|
||||
content = status · next step · what the user is needed for, never the long
|
||||
answer, honour `muted`/`discarded`. The three-line block for project
|
||||
AGENTS.md files is in
|
||||
[`skills/talk/AGENTS-snippet.md`](skills/talk/AGENTS-snippet.md). The daemon
|
||||
enforces rate limit, length and age limits, mute and quiet hours regardless
|
||||
of what the prompt says.
|
||||
|
||||
## Entwicklung
|
||||
## Development
|
||||
|
||||
```console
|
||||
go build ./... # bauen
|
||||
go test ./... # Tests
|
||||
go vet ./... # statische Analyse
|
||||
gofmt -l . # Formatierung prüfen (leerer Output = ok)
|
||||
nix develop # devShell mit go, gopls, espeak-ng, libnotify
|
||||
go build ./... # build
|
||||
go test ./... # tests
|
||||
go vet ./... # static analysis
|
||||
gofmt -l . # formatting check (empty output = ok)
|
||||
nix develop # devShell with go, gopls, espeak-ng, libnotify
|
||||
```
|
||||
|
||||
CI: `.gitea/workflows/test.yml` (Gitea Actions) läuft vet, gofmt-Check, Tests
|
||||
und Build bei jedem Push und Pull Request.
|
||||
CI: `.gitea/workflows/test.yml` (Gitea Actions) runs vet, the gofmt check,
|
||||
tests and build on every push and pull request.
|
||||
|
||||
**Release:** Tag `v*` pushen → `.gitea/workflows/release.yml` testet, baut
|
||||
mit `scripts/release.sh` die sechs Targets (statisch, `CGO_ENABLED=0`,
|
||||
Version per ldflags) und legt ein Gitea-Release mit `talk_<os>_<arch>[.exe]`
|
||||
und `checksums.txt` an. Lokal: `scripts/release.sh 1.2.3` → `dist/`. Bewusst
|
||||
ohne GoReleaser: ein Shell-Skript über `go build` ist dasselbe Artefakt,
|
||||
lokal wie in CI, ohne zusätzliches Werkzeug. `nix build .#talk` baut das
|
||||
Nix-Paket (nach Änderungen an go.mod den `vendorHash` in flake.nix
|
||||
nachziehen — `nix build` nennt den erwarteten Wert).
|
||||
**Release:** push a `v*` tag → `.gitea/workflows/release.yml` tests, builds
|
||||
the six targets with `scripts/release.sh` (static, `CGO_ENABLED=0`, version
|
||||
via ldflags) and creates a Gitea release with `talk_<os>_<arch>[.exe]` and
|
||||
`checksums.txt`. Locally: `scripts/release.sh 1.2.3` → `dist/`. Deliberately
|
||||
without GoReleaser: a shell script around `go build` yields the same
|
||||
artifact locally and in CI without another tool. `nix build .#talk` builds
|
||||
the Nix package (after changes to go.mod, update the `vendorHash` in
|
||||
flake.nix; `nix build` prints the expected value).
|
||||
|
||||
## Lizenz
|
||||
## License
|
||||
|
||||
MIT — siehe [LICENSE](LICENSE).
|
||||
MIT, see [LICENSE](LICENSE).
|
||||
Reference in new issue
Block a user