opencode-talk-bridge 0.2.7__tar.gz

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.
Files changed (51) hide show
  1. opencode_talk_bridge-0.2.7/.env.example +79 -0
  2. opencode_talk_bridge-0.2.7/.github/workflows/ci.yml +37 -0
  3. opencode_talk_bridge-0.2.7/.github/workflows/publish.yml +61 -0
  4. opencode_talk_bridge-0.2.7/.gitignore +35 -0
  5. opencode_talk_bridge-0.2.7/CHANGELOG.md +176 -0
  6. opencode_talk_bridge-0.2.7/LICENSE +21 -0
  7. opencode_talk_bridge-0.2.7/PKG-INFO +284 -0
  8. opencode_talk_bridge-0.2.7/README.md +253 -0
  9. opencode_talk_bridge-0.2.7/deploy/com.leiverkus.opencode-talk-bridge.plist +56 -0
  10. opencode_talk_bridge-0.2.7/docs/publishing.md +58 -0
  11. opencode_talk_bridge-0.2.7/docs/smoke-test.md +172 -0
  12. opencode_talk_bridge-0.2.7/pyproject.toml +62 -0
  13. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/__init__.py +8 -0
  14. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/__main__.py +116 -0
  15. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/allowlist.py +33 -0
  16. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/bridge.py +1014 -0
  17. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/commands.py +60 -0
  18. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/config.py +169 -0
  19. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/events.py +85 -0
  20. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/init.py +118 -0
  21. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/messages.py +226 -0
  22. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/opencode.py +418 -0
  23. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/pending.py +115 -0
  24. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/permissions.py +79 -0
  25. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/scheduler.py +144 -0
  26. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/sessions.py +141 -0
  27. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/status.py +88 -0
  28. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/streaming.py +78 -0
  29. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/stt.py +48 -0
  30. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/talk.py +171 -0
  31. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/tts.py +43 -0
  32. opencode_talk_bridge-0.2.7/src/opencode_talk_bridge/webdav.py +93 -0
  33. opencode_talk_bridge-0.2.7/tests/conftest.py +22 -0
  34. opencode_talk_bridge-0.2.7/tests/test_allowlist.py +37 -0
  35. opencode_talk_bridge-0.2.7/tests/test_bridge.py +740 -0
  36. opencode_talk_bridge-0.2.7/tests/test_commands.py +42 -0
  37. opencode_talk_bridge-0.2.7/tests/test_config.py +60 -0
  38. opencode_talk_bridge-0.2.7/tests/test_events.py +64 -0
  39. opencode_talk_bridge-0.2.7/tests/test_init.py +92 -0
  40. opencode_talk_bridge-0.2.7/tests/test_main.py +53 -0
  41. opencode_talk_bridge-0.2.7/tests/test_messages.py +34 -0
  42. opencode_talk_bridge-0.2.7/tests/test_opencode.py +201 -0
  43. opencode_talk_bridge-0.2.7/tests/test_pending.py +62 -0
  44. opencode_talk_bridge-0.2.7/tests/test_permissions.py +68 -0
  45. opencode_talk_bridge-0.2.7/tests/test_scheduler.py +72 -0
  46. opencode_talk_bridge-0.2.7/tests/test_sessions.py +58 -0
  47. opencode_talk_bridge-0.2.7/tests/test_status.py +42 -0
  48. opencode_talk_bridge-0.2.7/tests/test_streaming.py +69 -0
  49. opencode_talk_bridge-0.2.7/tests/test_talk.py +168 -0
  50. opencode_talk_bridge-0.2.7/tests/test_voice.py +63 -0
  51. opencode_talk_bridge-0.2.7/tests/test_webdav.py +82 -0
@@ -0,0 +1,79 @@
1
+ # opencode-talk-bridge configuration.
2
+ # Copy to `.env` and fill in. NEVER commit the real `.env`.
3
+
4
+ # --- Nextcloud Talk (consumed by nextcloud-talk-core) ----------------------
5
+ # Base URL of your Nextcloud instance.
6
+ NC_URL=https://cloud.example.com
7
+ # Your Nextcloud username (the bot/control account).
8
+ NC_USER=your-username
9
+ # An APP PASSWORD (Settings -> Security -> App passwords), NOT your login password.
10
+ NC_APP_PASSWORD=xxxxx-xxxxx-xxxxx-xxxxx-xxxxx
11
+
12
+ # --- Which conversations to watch ------------------------------------------
13
+ # Comma-separated Talk conversation tokens, or "all" to watch every conversation
14
+ # the account participates in. A single dedicated control conversation is the
15
+ # recommended setup.
16
+ TALK_CONVERSATIONS=abcdef12
17
+
18
+ # --- Security: command allowlist (MANDATORY) -------------------------------
19
+ # Comma-separated Talk user IDs (the stable login id, e.g. "jdoe", NOT the
20
+ # display name) allowed to issue commands. Messages from anyone else are
21
+ # ignored. The bridge REFUSES TO START if this is empty.
22
+ ALLOWED_USERS=jdoe
23
+
24
+ # --- OpenCode server -------------------------------------------------------
25
+ # Base URL of a running `opencode serve` (or `opencode web`). Default 4096.
26
+ OPENCODE_URL=http://127.0.0.1:4096
27
+ # Optional HTTP Basic-Auth for the OpenCode server. Leave blank if unsecured.
28
+ # (`opencode serve`/`web` reads OPENCODE_SERVER_USERNAME/PASSWORD on its side.)
29
+ OPENCODE_USERNAME=
30
+ OPENCODE_PASSWORD=
31
+ # Workspace directory OpenCode sessions operate in. Defaults to the bridge CWD.
32
+ OPENCODE_DIRECTORY=
33
+ # Optional default model as "providerID/modelID" (e.g. anthropic/claude-sonnet-4-6).
34
+ # Leave blank to use the OpenCode server's configured default.
35
+ OPENCODE_MODEL=
36
+
37
+ # --- Local storage & interface ---------------------------------------------
38
+ # SQLite file mapping Talk conversations to OpenCode sessions.
39
+ DB_PATH=bridge.sqlite3
40
+ # JSON status file the menubar app polls.
41
+ STATUS_FILE=status.json
42
+ # WebDAV folder (relative to your Nextcloud user root) the bridge uploads code /
43
+ # long-output attachments into before sharing them, e.g. "/opencode-talk-bridge".
44
+ # The folder is created on demand. Leave blank to disable attachments (long
45
+ # output is then posted as text). No desktop sync client required.
46
+ SHARE_WEBDAV_DIR=
47
+
48
+ # --- Interaction / streaming ------------------------------------------------
49
+ # Stream the assistant reply by editing one Talk message as it is generated.
50
+ RESPONSE_STREAMING=true
51
+ # Minimum delay between streamed edits (ms). Each edit is a full REST call.
52
+ STREAM_THROTTLE_MS=1500
53
+ # Post "💻 tool" / "💭 thinking" service messages while the agent works.
54
+ HIDE_TOOL_MESSAGES=false
55
+ HIDE_THINKING=true
56
+ # Notify on activity of detached/non-current sessions.
57
+ TRACK_BACKGROUND_SESSIONS=true
58
+ # Max entries shown in pickers (/sessions, /model, …).
59
+ LIST_LIMIT=10
60
+ # UI language for bridge messages: de or en.
61
+ BOT_LOCALE=de
62
+
63
+ # --- Voice (optional, Phase 3) ----------------------------------------------
64
+ # Speech-to-text for incoming voice notes (Whisper-compatible endpoint).
65
+ STT_API_URL=
66
+ STT_API_KEY=
67
+ STT_MODEL=whisper-large-v3-turbo
68
+ STT_LANGUAGE=
69
+ # Text-to-speech for spoken replies (per-conversation /tts toggle).
70
+ # Requires SHARE_WEBDAV_DIR to upload the audio before sharing.
71
+ TTS_API_URL=
72
+ TTS_API_KEY=
73
+ TTS_MODEL=gpt-4o-mini-tts
74
+ TTS_VOICE=alloy
75
+ # Max concurrent scheduled tasks (/task).
76
+ TASK_LIMIT=10
77
+
78
+ # --- Logging ----------------------------------------------------------------
79
+ LOG_LEVEL=INFO
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Set up Python ${{ matrix.python-version }}
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+
25
+ - name: Install
26
+ run: |
27
+ python -m pip install --upgrade pip
28
+ pip install -e ".[dev]" # nextcloud-talk-core comes from PyPI
29
+
30
+ - name: Ruff lint
31
+ run: ruff check src tests
32
+
33
+ - name: Ruff format check
34
+ run: ruff format --check src tests
35
+
36
+ - name: Pytest
37
+ run: pytest
@@ -0,0 +1,61 @@
1
+ name: Publish to PyPI
2
+
3
+ # Publishes the package to PyPI on every version tag (vX.Y.Z), using PyPI
4
+ # Trusted Publishing (OIDC) — no API token is stored in the repo.
5
+ #
6
+ # One-time PyPI setup (see docs/publishing.md): add a Trusted Publisher for the
7
+ # project pointing at this repo, workflow `publish.yml`, and environment `pypi`.
8
+
9
+ on:
10
+ push:
11
+ tags: ["v*"]
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ build:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.12"
25
+
26
+ - name: Guard — tag must match the package version
27
+ run: |
28
+ TAG="${GITHUB_REF_NAME#v}"
29
+ PKG="$(python -c 'import tomllib;print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')"
30
+ echo "tag=$TAG pyproject=$PKG"
31
+ test "$TAG" = "$PKG" || { echo "::error::tag v$TAG != pyproject version $PKG"; exit 1; }
32
+
33
+ - name: Build sdist + wheel
34
+ run: |
35
+ python -m pip install --upgrade build
36
+ python -m build
37
+
38
+ - name: Check metadata
39
+ run: |
40
+ python -m pip install --upgrade twine
41
+ python -m twine check dist/*
42
+
43
+ - uses: actions/upload-artifact@v4
44
+ with:
45
+ name: dist
46
+ path: dist/
47
+
48
+ publish:
49
+ needs: build
50
+ runs-on: ubuntu-latest
51
+ environment: pypi
52
+ permissions:
53
+ id-token: write # required for Trusted Publishing (OIDC)
54
+ steps:
55
+ - uses: actions/download-artifact@v4
56
+ with:
57
+ name: dist
58
+ path: dist/
59
+
60
+ - name: Publish to PyPI
61
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,35 @@
1
+ # Secrets — never commit a real environment file.
2
+ .env
3
+
4
+ # Runtime artifacts
5
+ *.sqlite
6
+ *.sqlite3
7
+ *.db
8
+ status.json
9
+ *.log
10
+
11
+ # Python
12
+ __pycache__/
13
+ *.py[cod]
14
+ .venv/
15
+ venv/
16
+ *.egg-info/
17
+ build/
18
+ dist/
19
+ .eggs/
20
+
21
+ # Tooling caches
22
+ .pytest_cache/
23
+ .ruff_cache/
24
+ .coverage
25
+ htmlcov/
26
+ .coverage.*
27
+
28
+ # macOS
29
+ .DS_Store
30
+
31
+ # Local editor / agent settings
32
+ .claude/
33
+
34
+ # Internal design brief — not published
35
+ BRIEFING-bridge.md
@@ -0,0 +1,176 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.7] - 2026-06-01
10
+
11
+ First release published to **PyPI** — `uv tool install opencode-talk-bridge`
12
+ (or `pipx install …`) now works without a git URL.
13
+
14
+ ### Added
15
+ - PyPI Trusted-Publishing workflow (`.github/workflows/publish.yml`): on each
16
+ `vX.Y.Z` tag it guards the tag↔version match, builds, `twine check`s, and
17
+ publishes to PyPI via OIDC (no token). Package classifiers + a
18
+ `docs/publishing.md` setup guide.
19
+
20
+ ## [0.2.6] - 2026-06-01
21
+
22
+ ### Changed
23
+ - `nextcloud-talk-core` is now a normal **PyPI dependency** (`>=1.0.2,<2`)
24
+ instead of a pinned git URL — it has been published to PyPI. Simpler installs
25
+ and resolution; dropped `allow-direct-references` and the git-dep notes in the
26
+ README/CI. Verified our used API (TalkClient/OCSClient methods) is unchanged in
27
+ 1.0.2 and the full suite passes against it.
28
+
29
+ ## [0.2.5] - 2026-06-01
30
+
31
+ ### Fixed
32
+ - **No server text leaks into chat.** The prompt worker caught generic
33
+ exceptions and posted `Fehler: {exc}`; an `OpenCodeError` carries up to ~300
34
+ chars of the OpenCode HTTP response body, which could reach Talk. The worker
35
+ (both session-setup and prompt paths) now catches `OpenCodeError` and posts a
36
+ generic `oc_error` message, logging the details only — matching the command
37
+ handlers. Remaining `error: {exc}` paths only fire for local validation errors
38
+ (e.g. a bad model string), which carry no server body.
39
+
40
+ ## [0.2.4] - 2026-05-31
41
+
42
+ Hardening pass from a code review.
43
+
44
+ ### Fixed
45
+ - **Error isolation.** Command handlers run inline on the poll thread and only
46
+ caught `OpenCodeDownError`; an OpenCode HTTP 4xx/5xx (`OpenCodeError`) could
47
+ escape and kill a conversation's poll thread. The poll loop now isolates every
48
+ message (logs + notifies + keeps polling), and command dispatch catches
49
+ `OpenCodeError` with a clean message.
50
+ - **`/task` validation.** Reject `0`-minute schedules and empty prompts
51
+ (`minutes >= 1` and a non-empty prompt are now required).
52
+
53
+ ### Internal
54
+ - Added tests for `__main__` (CLI wiring: `--check`/`--init`/run, 0→90%) and the
55
+ Talk gateway / message parsing (57→95%); overall coverage 76→82%.
56
+
57
+ ## [0.2.3] - 2026-05-31
58
+
59
+ ### Fixed
60
+ - `/sessions`, `/commands`, and `/skills` are **project-scoped** in OpenCode, but
61
+ the bridge listed them against the global `OPENCODE_DIRECTORY` and ignored the
62
+ per-conversation project chosen via `/projects`. They now use the
63
+ conversation's directory, so `/sessions` shows the same sessions as the
64
+ OpenCode desktop/TUI for that project (and follows `/projects` switches).
65
+ `/projects` itself is global (all known projects), matching desktop.
66
+
67
+ ## [0.2.2] - 2026-05-31
68
+
69
+ ### Added
70
+ - `opencode-talk-bridge --init` — interactive `.env` wizard. Prompts for the
71
+ essentials (Talk credentials, allowlist, OpenCode URL), hides the app password,
72
+ refuses to overwrite without confirmation, and writes the file `chmod 600`.
73
+
74
+ ## [0.2.1] - 2026-05-31
75
+
76
+ ### Added
77
+ - `/skills` — browse & run OpenCode skills (via `GET /skill`).
78
+
79
+ ### Fixed
80
+ - `run_command` now always sends the required `arguments` field — without it the
81
+ command endpoint returned HTTP 400, so `/commands` (and any command run) was
82
+ broken.
83
+ - `/commands` no longer lists skills (entries with `source == "skill"`); those
84
+ live under `/skills`.
85
+
86
+ ## [0.2.0] - 2026-05-31
87
+
88
+ Feature-parity push toward `grinev/opencode-telegram-bot`, adapted to Talk.
89
+
90
+ ### Added
91
+ - **Live response streaming**: the assistant reply is streamed into a single
92
+ Talk message via editing (`PUT`, throttled by `STREAM_THROTTLE_MS`), instead
93
+ of one post at the end. Toggle with `RESPONSE_STREAMING`.
94
+ - **Tool & thinking notices** from the SSE stream (`💻 bash`, `💭 …`), each tool
95
+ announced once per turn; toggle with `HIDE_TOOL_MESSAGES` / `HIDE_THINKING`.
96
+ - **Agent questions** (`question.asked`) are surfaced into Talk and answered by
97
+ a numbered option or free text.
98
+ - **Numbered-text pickers** (Talk has no buttons): `/sessions` (switch),
99
+ `/model` (pick), `/agent` (pick plan/build/…); `/agent <name>` sets directly.
100
+ - OpenCode client wrappers for sessions (rename/revert/unrevert/fork), projects,
101
+ worktrees, models, agents, commands, MCP, and question reply/reject.
102
+
103
+ - **Breadth commands** (Phase 2): `/rename`, `/detach`, `/projects`,
104
+ `/worktree`, `/commands` (run with streaming), `/mcps` (toggle), and
105
+ `/messages` → revert/fork. Projects/worktrees bind a per-conversation
106
+ directory for new sessions.
107
+ - **Background-session notifications** when a non-foreground session goes idle
108
+ (`TRACK_BACKGROUND_SESSIONS`).
109
+ - **i18n**: user-facing strings localised via `messages.py` (German + English),
110
+ selected by `BOT_LOCALE`.
111
+ - **Voice & files** (Phase 3): incoming file/image attachments are inlined into
112
+ the prompt as OpenCode file parts; voice notes are transcribed (STT,
113
+ Whisper-compatible) into the prompt; optional spoken replies (TTS) shared as
114
+ audio, toggled per-conversation with `/tts`. Config: `STT_*`, `TTS_*`.
115
+ - **Scheduled tasks** (Phase 3): `/task <min> <prompt>` (or `/task every <min>
116
+ …`) and `/tasklist`; a daemon scheduler fires due tasks, persisted in SQLite.
117
+
118
+ ### Changed
119
+ - Internal refactor: unified pending-interaction registry (`pending.py`),
120
+ streaming substrate (`streaming.py`), and typed SSE dispatch (`events.py`).
121
+ - `TalkGateway.send` now returns the message id; added `edit`/`download`.
122
+
123
+ ## [0.1.1] - 2026-05-30
124
+
125
+ ### Changed
126
+ - File attachments now upload to Nextcloud directly via **WebDAV** (`PUT`,
127
+ creating the target folder on demand) before sharing into the conversation.
128
+ This removes the previous dependency on a desktop sync client having already
129
+ uploaded the file, which could make `share_file` fail with "not found".
130
+ - Attachment config simplified to a single `SHARE_WEBDAV_DIR` (the server-side
131
+ folder), replacing `SHARE_DIR` + `SHARE_WEBDAV_ROOT`.
132
+
133
+ ### Added
134
+ - `webdav.py`: minimal WebDAV client (reuses the app-password credentials).
135
+
136
+ ## [0.1.0] - 2026-05-30
137
+
138
+ Initial release.
139
+
140
+ ### Added
141
+ - Polling bridge between Nextcloud Talk and a local OpenCode instance — no
142
+ webhooks, works on institutional Talk instances without admin access.
143
+ - OpenCode HTTP + SSE client (`opencode.py`), verified against the
144
+ `opencode serve` 1.15 OpenAPI: health, session create/list, blocking prompt,
145
+ abort, global permission reply, and the `/global/event` stream.
146
+ - Mandatory user allowlist enforced on the stable OCS `actorId` with
147
+ `actorType == "users"`; the bridge refuses to start with an empty allowlist.
148
+ - Permission prompts for dangerous agent actions surfaced into Talk, answered
149
+ with `ja`/`immer`/`nein` and replied via `POST /permission/{id}/reply`.
150
+ - SQLite session store mapping each Talk conversation to an OpenCode session,
151
+ model, and last-known-message cursor; persists across restarts.
152
+ - Slash-commands: `/new`, `/session`, `/model`, `/stop`, `/status`, `/help`.
153
+ - File attachments for long output / code, written to a Nextcloud share folder
154
+ and shared into the conversation instead of posting a large text block.
155
+ - Atomic JSON status file as a stable contract for a supervising menubar app.
156
+ - CLI entrypoint with `--check` and graceful SIGINT/SIGTERM shutdown.
157
+ - launchd user-agent example (`deploy/`), README threat model, and the
158
+ `.env.example` template.
159
+ - Test suite (mocked Talk + OpenCode, no live calls) and CI matrix on
160
+ Python 3.10–3.13 with ruff lint + format checks.
161
+
162
+ ### Dependencies
163
+ - `nextcloud-talk-core`, pinned to the `core-v1.0.0` git tag.
164
+ - `httpx >= 0.27`.
165
+
166
+ [Unreleased]: https://github.com/leiverkus/opencode-talk-bridge/compare/v0.2.7...HEAD
167
+ [0.2.7]: https://github.com/leiverkus/opencode-talk-bridge/compare/v0.2.6...v0.2.7
168
+ [0.2.6]: https://github.com/leiverkus/opencode-talk-bridge/compare/v0.2.5...v0.2.6
169
+ [0.2.5]: https://github.com/leiverkus/opencode-talk-bridge/compare/v0.2.4...v0.2.5
170
+ [0.2.4]: https://github.com/leiverkus/opencode-talk-bridge/compare/v0.2.3...v0.2.4
171
+ [0.2.3]: https://github.com/leiverkus/opencode-talk-bridge/compare/v0.2.2...v0.2.3
172
+ [0.2.2]: https://github.com/leiverkus/opencode-talk-bridge/compare/v0.2.1...v0.2.2
173
+ [0.2.1]: https://github.com/leiverkus/opencode-talk-bridge/compare/v0.2.0...v0.2.1
174
+ [0.2.0]: https://github.com/leiverkus/opencode-talk-bridge/compare/v0.1.1...v0.2.0
175
+ [0.1.1]: https://github.com/leiverkus/opencode-talk-bridge/compare/v0.1.0...v0.1.1
176
+ [0.1.0]: https://github.com/leiverkus/opencode-talk-bridge/releases/tag/v0.1.0
@@ -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.