opencode-talk-bridge 0.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ """Text-to-speech via an OpenAI-compatible ``/audio/speech`` endpoint.
2
+
3
+ Used to synthesise the assistant reply into an audio file that is uploaded to
4
+ Nextcloud and shared into the conversation (toggled per-conversation by /tts).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import httpx
10
+
11
+
12
+ class TTSError(Exception):
13
+ """Synthesis failed."""
14
+
15
+
16
+ class TTSClient:
17
+ def __init__(
18
+ self,
19
+ url: str,
20
+ *,
21
+ api_key: str | None = None,
22
+ model: str = "gpt-4o-mini-tts",
23
+ voice: str = "alloy",
24
+ timeout: float = 120.0,
25
+ ) -> None:
26
+ self._url = url.rstrip("/") + "/audio/speech"
27
+ self._model = model
28
+ self._voice = voice
29
+ headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
30
+ self._client = httpx.Client(headers=headers, timeout=timeout)
31
+
32
+ def close(self) -> None:
33
+ self._client.close()
34
+
35
+ def synthesize(self, text: str) -> bytes:
36
+ body = {"model": self._model, "input": text, "voice": self._voice}
37
+ try:
38
+ resp = self._client.post(self._url, json=body)
39
+ except httpx.HTTPError as exc:
40
+ raise TTSError(f"synthesis request failed: {exc}") from exc
41
+ if resp.status_code >= 400:
42
+ raise TTSError(f"synthesis -> HTTP {resp.status_code}: {resp.text[:200]}")
43
+ return resp.content
@@ -0,0 +1,93 @@
1
+ """Minimal WebDAV client for uploading attachments to Nextcloud.
2
+
3
+ ``nextcloud-talk-core`` only speaks the OCS API; sharing a file into a Talk
4
+ conversation (``TalkClient.share_file``) requires the file to already exist on
5
+ the server. This client uploads the file first via WebDAV (``PUT``), creating
6
+ the target collection if needed (``MKCOL``), so the bridge does not depend on a
7
+ desktop sync client having uploaded it.
8
+
9
+ It reuses the same Basic-Auth app-password credentials as the OCS client.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from urllib.parse import quote
15
+
16
+ import httpx
17
+ from nextcloud_talk_core import Settings
18
+
19
+
20
+ class WebDavError(Exception):
21
+ """An upload or collection-creation failed."""
22
+
23
+
24
+ class WebDavClient:
25
+ def __init__(self, settings: Settings, *, timeout: float = 60.0) -> None:
26
+ self._user = settings.nc_user
27
+ # Files live under the user's principal collection.
28
+ self._base = f"{settings.nc_url}/remote.php/dav/files/{quote(settings.nc_user)}"
29
+ self._client = httpx.Client(
30
+ auth=httpx.BasicAuth(settings.nc_user, settings.nc_app_password),
31
+ timeout=timeout,
32
+ )
33
+
34
+ def close(self) -> None:
35
+ self._client.close()
36
+
37
+ def upload(self, remote_path: str, content: bytes, *, content_type: str = "text/markdown") -> str:
38
+ """Upload ``content`` to ``remote_path`` (relative to the user root).
39
+
40
+ Creates parent collections on demand. Returns the normalised path
41
+ (leading slash, suitable for ``TalkClient.share_file``).
42
+ """
43
+ path = "/" + remote_path.strip("/")
44
+ url = self._base + _encode_path(path)
45
+ resp = self._put(url, content, content_type)
46
+ if resp.status_code == 409: # parent collection missing
47
+ self._ensure_dir(_parent(path))
48
+ resp = self._put(url, content, content_type)
49
+ if resp.status_code >= 400:
50
+ raise WebDavError(f"PUT {path} -> HTTP {resp.status_code}: {resp.text[:200]}")
51
+ return path
52
+
53
+ def download(self, remote_path: str) -> bytes:
54
+ """Download a file by its path relative to the user root."""
55
+ path = "/" + remote_path.strip("/")
56
+ url = self._base + _encode_path(path)
57
+ try:
58
+ resp = self._client.get(url)
59
+ except httpx.HTTPError as exc:
60
+ raise WebDavError(f"download {path} failed: {exc}") from exc
61
+ if resp.status_code >= 400:
62
+ raise WebDavError(f"GET {path} -> HTTP {resp.status_code}")
63
+ return resp.content
64
+
65
+ def _put(self, url: str, content: bytes, content_type: str) -> httpx.Response:
66
+ try:
67
+ return self._client.request("PUT", url, content=content, headers={"Content-Type": content_type})
68
+ except httpx.HTTPError as exc:
69
+ raise WebDavError(f"upload failed: {exc}") from exc
70
+
71
+ def _ensure_dir(self, dir_path: str) -> None:
72
+ """MKCOL each segment of ``dir_path`` (idempotent)."""
73
+ parts = [p for p in dir_path.strip("/").split("/") if p]
74
+ cumulative = ""
75
+ for part in parts:
76
+ cumulative += "/" + part
77
+ url = self._base + _encode_path(cumulative)
78
+ try:
79
+ resp = self._client.request("MKCOL", url)
80
+ except httpx.HTTPError as exc:
81
+ raise WebDavError(f"MKCOL {cumulative} failed: {exc}") from exc
82
+ # 201 Created, or 405 Method Not Allowed (already exists) are both fine.
83
+ if resp.status_code not in (201, 405):
84
+ raise WebDavError(f"MKCOL {cumulative} -> HTTP {resp.status_code}")
85
+
86
+
87
+ def _encode_path(path: str) -> str:
88
+ """Percent-encode each path segment, preserving slashes."""
89
+ return "/" + "/".join(quote(seg) for seg in path.strip("/").split("/") if seg)
90
+
91
+
92
+ def _parent(path: str) -> str:
93
+ return path.rsplit("/", 1)[0] or "/"
@@ -0,0 +1,284 @@
1
+ Metadata-Version: 2.4
2
+ Name: opencode-talk-bridge
3
+ Version: 0.2.7
4
+ Summary: Drive a local OpenCode instance from Nextcloud Talk via polling — a self-hosted chat bridge for a coding agent.
5
+ Project-URL: Homepage, https://github.com/leiverkus/opencode-talk-bridge
6
+ Project-URL: Repository, https://github.com/leiverkus/opencode-talk-bridge
7
+ Author: Patrick Leiverkus
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai,bridge,coding-agent,nextcloud,opencode,talk
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Communications :: Chat
22
+ Classifier: Topic :: Software Development :: Code Generators
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: nextcloud-talk-core<2,>=1.0.2
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.9; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # opencode-talk-bridge
33
+
34
+ [![CI](https://github.com/leiverkus/opencode-talk-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/leiverkus/opencode-talk-bridge/actions/workflows/ci.yml)
35
+ [![Release](https://img.shields.io/github/v/release/leiverkus/opencode-talk-bridge?sort=semver)](https://github.com/leiverkus/opencode-talk-bridge/releases/latest)
36
+ [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.13-blue)](https://github.com/leiverkus/opencode-talk-bridge/blob/main/pyproject.toml)
37
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
38
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
39
+
40
+ Drive a local [OpenCode](https://opencode.ai) coding agent from
41
+ [Nextcloud Talk](https://nextcloud.com/talk/) — a self-hosted chat bridge.
42
+
43
+ An incoming Talk message is forwarded to a locally-running `opencode serve`
44
+ HTTP API; the agent's reply (long output / code as a file attachment) is posted
45
+ back into the conversation. The bridge uses **polling** (no webhooks), so it
46
+ works on institutional Talk instances where you have no admin access and cannot
47
+ register a webhook bot.
48
+
49
+ > ⚠️ **This bridge runs AI coding-agent actions on your machine, triggered from
50
+ > chat in infrastructure you may not control.** Read the
51
+ > [Threat model](#threat-model) first. A non-empty user allowlist is mandatory —
52
+ > the bridge refuses to start without one.
53
+
54
+ ---
55
+
56
+ ## How it works
57
+
58
+ ```
59
+ Nextcloud Talk ──long-poll──▶ bridge ──HTTP──▶ opencode serve (localhost)
60
+ (you type) ◀──post──────── bridge ◀──SSE──── (agent runs locally)
61
+ ```
62
+
63
+ - **Polling**, not webhooks: the bridge long-polls the Talk chat endpoint
64
+ (`lookIntoFuture=1` + `lastKnownMessageId`) using only your app password.
65
+ - **OpenCode stays local**: the bridge talks to `opencode serve` over
66
+ `127.0.0.1`. It does **not** start OpenCode — run it yourself, via launchd, or
67
+ via the companion menubar app. The bridge health-checks it and reports when
68
+ it is down.
69
+ - **Prompts block; permissions are concurrent**: `POST /session/{id}/message`
70
+ blocks until the turn finishes, so each prompt runs in a worker thread while a
71
+ shared SSE consumer (`/global/event`) routes permission requests back into the
72
+ conversation in real time.
73
+
74
+ ## Requirements
75
+
76
+ - Python ≥ 3.10, macOS or Linux.
77
+ - A Nextcloud account with **Talk** and an **app password**
78
+ (Settings → Security → App passwords) — not your login password.
79
+ - A running `opencode serve` (OpenCode ≥ 1.15). Default endpoint
80
+ `http://127.0.0.1:4096`.
81
+
82
+ ## Install
83
+
84
+ **One command** — installs the `opencode-talk-bridge` CLI into an isolated
85
+ environment and onto your `PATH` ([uv](https://docs.astral.sh/uv/) or
86
+ [pipx](https://pipx.pypa.io/)):
87
+
88
+ ```bash
89
+ uv tool install git+https://github.com/leiverkus/opencode-talk-bridge.git
90
+ # or: pipx install git+https://github.com/leiverkus/opencode-talk-bridge.git
91
+ ```
92
+
93
+ > Once the first PyPI release is published this becomes
94
+ > `uv tool install opencode-talk-bridge` (no git URL) — see
95
+ > [docs/publishing.md](docs/publishing.md).
96
+
97
+ Then create your config interactively and run:
98
+
99
+ ```bash
100
+ opencode-talk-bridge --init # interactive .env wizard (chmod 600)
101
+ opencode-talk-bridge --check # validate config + OpenCode health
102
+ opencode-talk-bridge # run (reads ./.env, or pass --env-file PATH)
103
+ ```
104
+
105
+ `--init` asks only for the essentials; every other option keeps a safe default
106
+ and can be added later (see [`.env.example`](.env.example) for the full list).
107
+
108
+ First run against a real Talk server? Follow the
109
+ [end-to-end smoke test](docs/smoke-test.md) — it tests the riskiest paths first
110
+ and covers the two-accounts gotcha.
111
+
112
+ Upgrade with `uv tool upgrade opencode-talk-bridge` (or `pipx upgrade …`).
113
+
114
+ <details>
115
+ <summary>From source (for development)</summary>
116
+
117
+ ```bash
118
+ git clone https://github.com/leiverkus/opencode-talk-bridge.git
119
+ cd opencode-talk-bridge
120
+ python3 -m venv .venv && source .venv/bin/activate
121
+ pip install -e ".[dev]" # editable + test/lint tools
122
+ cp .env.example .env # then edit .env
123
+ ```
124
+ </details>
125
+
126
+ The OCS client is the separate [`nextcloud-talk-core`](https://pypi.org/project/nextcloud-talk-core/)
127
+ package from PyPI (tracking the 1.x line) — never reimplemented or vendored here.
128
+
129
+ ## Configure
130
+
131
+ Edit `.env` (see [`.env.example`](.env.example) for the full list):
132
+
133
+ | Variable | Required | Purpose |
134
+ | --- | --- | --- |
135
+ | `NC_URL`, `NC_USER`, `NC_APP_PASSWORD` | ✅ | Nextcloud Talk credentials (app password). |
136
+ | `TALK_CONVERSATIONS` | ✅ | Conversation token(s) to watch, or `all`. |
137
+ | `ALLOWED_USERS` | ✅ | Comma-separated **user IDs** allowed to issue commands. Empty ⇒ refuses to start. |
138
+ | `OPENCODE_URL` | – | OpenCode base URL (default `http://127.0.0.1:4096`). |
139
+ | `OPENCODE_USERNAME` / `OPENCODE_PASSWORD` | – | Basic-Auth if your OpenCode server is secured. |
140
+ | `OPENCODE_DIRECTORY` | – | Workspace directory for OpenCode sessions. |
141
+ | `OPENCODE_MODEL` | – | Default model `providerID/modelID`. |
142
+ | `SHARE_WEBDAV_DIR` | – | WebDAV folder (relative to user root) for code/long-output **and TTS** attachments. Created on demand; blank disables attachments. |
143
+ | `RESPONSE_STREAMING`, `STREAM_THROTTLE_MS` | – | Live-stream replies via message editing (default on, 1500 ms throttle). |
144
+ | `HIDE_TOOL_MESSAGES`, `HIDE_THINKING` | – | Suppress `💻 tool` / `💭 thinking` notices. |
145
+ | `BOT_LOCALE` | – | UI language: `de` (default) or `en`. |
146
+ | `STT_API_URL` / `STT_API_KEY` / `STT_MODEL` / `STT_LANGUAGE` | – | Whisper-compatible speech-to-text for voice notes. |
147
+ | `TTS_API_URL` / `TTS_API_KEY` / `TTS_MODEL` / `TTS_VOICE` | – | OpenAI-compatible text-to-speech for `/tts` replies. |
148
+ | `TASK_LIMIT`, `LIST_LIMIT`, `TRACK_BACKGROUND_SESSIONS` | – | Scheduler limit, picker size, background notices. |
149
+ | `DB_PATH`, `STATUS_FILE`, `LOG_LEVEL` | – | Storage + logging. |
150
+
151
+ > **`ALLOWED_USERS` must be the stable user ID** (the login, e.g. `jdoe`), **not
152
+ > the display name.** The bridge matches the OCS `actorId` with
153
+ > `actorType == "users"`, so guests and bots can never impersonate an allowed
154
+ > user even if display names collide.
155
+
156
+ ## Run
157
+
158
+ ```bash
159
+ # Validate config + check OpenCode health, then exit:
160
+ opencode-talk-bridge --check
161
+
162
+ # Run the bridge (foreground):
163
+ opencode-talk-bridge # or: python -m opencode_talk_bridge
164
+ ```
165
+
166
+ ### Commands (in Talk)
167
+
168
+ Send any message (or a **voice note** / **file**) to prompt OpenCode. The reply
169
+ **streams live** into one message as it is generated. Slash-commands:
170
+
171
+ | Command | Effect |
172
+ | --- | --- |
173
+ | `/new` | Start a fresh OpenCode session for this conversation. |
174
+ | `/session` | Show the current session id. |
175
+ | `/sessions` | List & switch recent sessions. |
176
+ | `/rename <title>` | Rename the current session. |
177
+ | `/detach` | Detach from the current session. |
178
+ | `/messages` | Browse messages, then **revert** or **fork**. |
179
+ | `/model [providerID/modelID]` | Show, pick, or set the model. |
180
+ | `/agent [name]` | Show, pick, or set the agent (e.g. plan/build). |
181
+ | `/projects` | Switch the OpenCode project. |
182
+ | `/worktree` | Switch the git worktree. |
183
+ | `/commands` | Browse & run custom OpenCode commands. |
184
+ | `/skills` | Browse & run OpenCode skills. |
185
+ | `/mcps` | Enable/disable MCP servers. |
186
+ | `/tts` | Toggle spoken (audio) replies (needs `TTS_*` + `SHARE_WEBDAV_DIR`). |
187
+ | `/task <min> <prompt>` | Schedule a task (`/task every <min> …` to repeat). |
188
+ | `/tasklist` | List & delete scheduled tasks. |
189
+ | `/stop` | Abort the current run. |
190
+ | `/status` | Show bridge & OpenCode health. |
191
+ | `/help` | List commands. |
192
+
193
+ `/projects` lists every project the OpenCode backend knows (same as the desktop
194
+ app). `/sessions`, `/commands`, and `/skills` are **project-scoped**: they show
195
+ what the conversation's current project shows — pick a project with `/projects`
196
+ (or set a default `OPENCODE_DIRECTORY`). With no project bound, `/sessions` falls
197
+ back to a global list across all projects.
198
+
199
+ **No inline buttons** on Talk → every picker is a **numbered list**: reply with
200
+ the number. When OpenCode asks **permission** for a dangerous action, reply
201
+ **`ja`** (allow once), **`immer`** (allow for this session), or **`nein`** (deny);
202
+ agent **questions** are answered by their option number or free text.
203
+
204
+ Set `BOT_LOCALE=en` for English UI strings (default `de`).
205
+
206
+ ## Run under launchd (macOS)
207
+
208
+ A user-agent example is in [`deploy/`](deploy/). Edit the paths, then:
209
+
210
+ ```bash
211
+ cp deploy/com.leiverkus.opencode-talk-bridge.plist ~/Library/LaunchAgents/
212
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.leiverkus.opencode-talk-bridge.plist
213
+ # stop / uninstall:
214
+ launchctl bootout gui/$(id -u)/com.leiverkus.opencode-talk-bridge
215
+ ```
216
+
217
+ Secrets stay in `.env` (read from `WorkingDirectory`), never in the plist.
218
+
219
+ ## Status file (menubar-app contract)
220
+
221
+ The bridge atomically writes `STATUS_FILE` (JSON) on every state change. A
222
+ supervising app (e.g. the Swift menubar app) can poll it. The file is always
223
+ valid JSON (temp-write + rename). Schema:
224
+
225
+ ```json
226
+ {
227
+ "state": "polling",
228
+ "since": 1748600000,
229
+ "opencode_healthy": true,
230
+ "conversations": ["abcdef12"],
231
+ "last_error": null,
232
+ "version": "0.1.0"
233
+ }
234
+ ```
235
+
236
+ `state` ∈ `starting` · `polling` · `working` · `opencode_down` · `error` ·
237
+ `stopped`. `since` is a Unix timestamp; `conversations` lists watched tokens;
238
+ `last_error` is a human-readable string or `null`.
239
+
240
+ ## Threat model
241
+
242
+ This bridge **executes coding-agent actions on your machine** (file writes,
243
+ shell commands via OpenCode), **triggered by chat messages in a Nextcloud Talk
244
+ instance you may not administer** (e.g. a university server). Treat that Talk
245
+ instance as semi-trusted infrastructure. The trust boundary and mitigations:
246
+
247
+ - **Access control is the allowlist.** Only `actorType == "users"` IDs in
248
+ `ALLOWED_USERS` can issue commands; every other message is ignored. An empty
249
+ allowlist aborts startup. Anyone who can post as an allowlisted user can run
250
+ code as you — keep the list minimal and the control conversation private.
251
+ - **Dangerous actions require confirmation.** OpenCode's permission prompts
252
+ (shell, file writes) are surfaced into Talk and must be answered; nothing
253
+ runs without a `ja`/`immer`. Configure OpenCode's own permission policy
254
+ conservatively as defence in depth.
255
+ - **Secrets are never echoed.** The bridge does not post your app password or
256
+ tokens, and permission prompts show only the action kind and a length-capped
257
+ pattern — not raw command arguments that might contain secrets. Be aware that
258
+ OpenCode's **answer text itself** could contain sensitive repo content; it is
259
+ posted into Talk, whose admins can read it.
260
+ - **A compromised Talk server** could inject messages appearing to come from an
261
+ allowlisted user. The allowlist mitigates casual misuse, not a fully
262
+ compromised IdP/server. Do not point this at sensitive repos if that is your
263
+ threat model.
264
+ - **Local-only OpenCode.** Keep `opencode serve` bound to `127.0.0.1`. If you
265
+ expose it, secure it with `OPENCODE_USERNAME`/`OPENCODE_PASSWORD`.
266
+
267
+ ## Development
268
+
269
+ ```bash
270
+ ruff check src tests
271
+ ruff format --check src tests
272
+ pytest
273
+ ```
274
+
275
+ All tests use mocked HTTP for both Talk and OpenCode — no live calls. CI runs
276
+ the matrix on Python 3.10–3.13.
277
+
278
+ ## Changelog
279
+
280
+ See [CHANGELOG.md](CHANGELOG.md).
281
+
282
+ ## License
283
+
284
+ MIT © Patrick Leiverkus
@@ -0,0 +1,25 @@
1
+ opencode_talk_bridge/__init__.py,sha256=exajW8FF_KXe_QUGznQFmcUv5s4MHRySAITH1xz-lOk,337
2
+ opencode_talk_bridge/__main__.py,sha256=MZIwJSJ9Wn5BtPJjGUKjtFuMgpcJ7ZRScXG9Bqo-MAQ,3532
3
+ opencode_talk_bridge/allowlist.py,sha256=BpFevdSMMW6aYHLMHeJXIQ7IcPj--F0YmiWFaXl-Xz8,1421
4
+ opencode_talk_bridge/bridge.py,sha256=TbXCMhJlu-PBMeMpUF5BUXXn_75OJFv1GgAXlho5N0M,41388
5
+ opencode_talk_bridge/commands.py,sha256=xQ3ivjglQqeJCfW0lmy00PiB5THG9KpWhq8VAx6NQpk,1336
6
+ opencode_talk_bridge/config.py,sha256=AtQsLYH7N3tS7eG6z6evLAnsiXobw8d8R4Fv3IFwmKU,6148
7
+ opencode_talk_bridge/events.py,sha256=-5y0apug1FSfpZHK0uPvZC69wd_Vw_9eRVMx29MSdSQ,2209
8
+ opencode_talk_bridge/init.py,sha256=rZJnE-oiNp0PDf_vXoL-6orfqpbuuNi8q-JURPWzDUc,4439
9
+ opencode_talk_bridge/messages.py,sha256=67X0VJIwk43ymxTaJAOop5NJaG85Byzucfhfi3wmUT8,10813
10
+ opencode_talk_bridge/opencode.py,sha256=MyE1pdhS0ljr6gXcnIexpq0R5LwrZoNUXzt--g126fk,16019
11
+ opencode_talk_bridge/pending.py,sha256=u2zci56Ff0x_7HoHuLu7tVr0RKLzStmYCgOAsLBeSaw,3491
12
+ opencode_talk_bridge/permissions.py,sha256=rC8MiyGYW26yMrAm2WCzU_C0a_XUsbm0qSVkcZtzhn4,2683
13
+ opencode_talk_bridge/scheduler.py,sha256=1xXweIiBeLPAO8v02DzLebvA-13mOtw22ezFY089ElQ,4606
14
+ opencode_talk_bridge/sessions.py,sha256=bM-9J1x8v4Oc9qyf6OphFuPINTsdWh-YVNO5ISV5oQY,5375
15
+ opencode_talk_bridge/status.py,sha256=J6hUgnPOPjapqjyNU0v2TC7pAzaqcy0-aVviXdfPht0,2773
16
+ opencode_talk_bridge/streaming.py,sha256=rL94ckr1hTLAEDARCsqToCHyvH2kpFlRE5ATrrC7Pr8,2873
17
+ opencode_talk_bridge/stt.py,sha256=NRUuTblfKk7aKpembg6xj_8Uco-P2_U7UxeRWXQJFRk,1642
18
+ opencode_talk_bridge/talk.py,sha256=LY5OJoZ7s8gfZq2yDNJm4yF57gIjM3D4ntJE-B9eNHQ,5603
19
+ opencode_talk_bridge/tts.py,sha256=P-ijB8ZwHJaTbr8q1UorrHyhJMKJwjcTzzPBngkh_ng,1344
20
+ opencode_talk_bridge/webdav.py,sha256=M6H6B9qICQ4M6OqRYj-QQfK1n9ZP-GZWFESdhA-M9T0,3816
21
+ opencode_talk_bridge-0.2.7.dist-info/METADATA,sha256=AZced9Bx0AGRrvZnz1fLOEmd590V6dzMNXs_a76IsGA,12797
22
+ opencode_talk_bridge-0.2.7.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
23
+ opencode_talk_bridge-0.2.7.dist-info/entry_points.txt,sha256=H82IwEFIW6PleyEGzEeH8M2u2iKYohA-IBGUwevnPNI,76
24
+ opencode_talk_bridge-0.2.7.dist-info/licenses/LICENSE,sha256=fseWi5ej02eIb3nyWOsK_MIuBfK5uOFipv5WCGkrwCQ,1074
25
+ opencode_talk_bridge-0.2.7.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ opencode-talk-bridge = opencode_talk_bridge.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Leiverkus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.