opencode-anthropic-fix 0.0.14

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,525 @@
1
+ # Contributing
2
+
3
+ This document covers the architecture, implementation details, and development workflow for the multi-account fork of `opencode-anthropic-auth`.
4
+
5
+ ## Development Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/actualyze-ai/opencode-anthropic-auth.git
9
+ cd opencode-anthropic-auth
10
+ npm install
11
+ npm run install:link # Symlink plugin + CLI for live development
12
+ npm test # Run all tests (~2s)
13
+ ```
14
+
15
+ ### Running Tests
16
+
17
+ ```bash
18
+ npm test # Single run
19
+ npm run test:watch # Watch mode
20
+ ```
21
+
22
+ Tests use [vitest](https://vitest.dev/). All modules are tested in isolation with mocked dependencies.
23
+
24
+ ### Linting and Formatting
25
+
26
+ ```bash
27
+ npm run lint # Check for lint errors
28
+ npm run lint:fix # Fix auto-fixable lint errors
29
+ npm run format # Format all files with Prettier
30
+ npm run format:check # Check formatting without writing
31
+ ```
32
+
33
+ Git hooks enforce quality automatically:
34
+
35
+ - **Pre-commit:** `lint-staged` runs Prettier and ESLint on staged `.mjs` files, Prettier on `.json`/`.md` files
36
+ - **Pre-push:** full test suite + Prettier format check
37
+
38
+ ## Project Structure
39
+
40
+ ```
41
+ opencode-anthropic-auth/
42
+ index.mjs Plugin entry point (OAuth flow, fetch interceptor, retry loop, slash commands)
43
+ index.test.mjs Plugin integration tests (lifecycle, fetch, transforms, slash commands)
44
+ cli.mjs Standalone CLI (17 subcommands, auth flows, live usage quotas)
45
+ cli.test.mjs CLI command tests (auth + account management + IO capture)
46
+ package.json Dependencies: @openauthjs/openauth (prod), esbuild + vitest + eslint + prettier (dev)
47
+ eslint.config.mjs ESLint flat config
48
+ .prettierrc Prettier config
49
+ .prettierignore Prettier ignore patterns
50
+ .husky/ Git hooks (pre-commit: lint-staged, pre-push: test + format check)
51
+ lib/
52
+ oauth.mjs Shared OAuth helpers (authorize, exchange, revoke) — used by both plugin and CLI
53
+ accounts.mjs AccountManager class (pool management, selection, persistence)
54
+ accounts.test.mjs AccountManager tests
55
+ rotation.mjs HealthScoreTracker, TokenBucketTracker, selectAccount()
56
+ rotation.test.mjs Selection algorithm tests
57
+ backoff.mjs Rate limit parsing, backoff calculation
58
+ backoff.test.mjs Backoff calculation tests
59
+ config.mjs Config loader/saver, validation, env overrides
60
+ config.test.mjs Config loading/validation tests
61
+ storage.mjs Account persistence (atomic writes, deduplication)
62
+ storage.test.mjs Storage persistence tests
63
+ scripts/
64
+ build.mjs esbuild bundler (produces dist/)
65
+ install.mjs Unified installer (link/copy/uninstall)
66
+ dist/ Build output (gitignored)
67
+ opencode-anthropic-auth-plugin.js Bundled plugin (self-contained)
68
+ opencode-anthropic-auth-cli.mjs Bundled CLI (self-contained)
69
+ ```
70
+
71
+ ## Architecture Overview
72
+
73
+ ```mermaid
74
+ graph TB
75
+ subgraph OpenCode
76
+ OC[OpenCode Runtime] -->|loads plugin| Plugin
77
+ OC -->|calls| Fetch[provider.fetch]
78
+ OC -->|/anthropic| SlashCmd[Slash Command Handler]
79
+ end
80
+
81
+ subgraph Plugin["index.mjs (Plugin)"]
82
+ Auth[Auth Methods]
83
+ Loader[Auth Loader]
84
+ SysTransform[System Prompt Transform]
85
+ FetchInterceptor[Fetch Interceptor]
86
+ SlashCmd -->|in-process| CLI[cli.mjs dispatch]
87
+ SlashCmd -->|login/reauth| OAuthFlow[Slash OAuth Flow]
88
+ end
89
+
90
+ subgraph Core["lib/ (Core)"]
91
+ OAuth[OAuth Helpers]
92
+ AM[AccountManager]
93
+ Rotation[Rotation Engine]
94
+ Backoff[Backoff Calculator]
95
+ Config[Config Loader]
96
+ Storage[Account Storage]
97
+ end
98
+
99
+ subgraph External
100
+ Anthropic[Anthropic API]
101
+ Disk[(~/.config/opencode/)]
102
+ end
103
+
104
+ Auth -->|authorize/exchange| OAuth
105
+ OAuthFlow -->|authorize/exchange| OAuth
106
+ CLI -->|account commands| Storage
107
+ Auth -->|add account| AM
108
+ Loader -->|init| AM
109
+ FetchInterceptor -->|select account| AM
110
+ FetchInterceptor -->|build headers| Anthropic
111
+ FetchInterceptor -->|on account-specific errors| Backoff
112
+ AM -->|select| Rotation
113
+ AM -->|persist| Storage
114
+ Storage -->|read/write| Disk
115
+ Config -->|read/write| Disk
116
+ OAuth -->|token exchange| Anthropic
117
+ ```
118
+
119
+ ## Plugin Lifecycle
120
+
121
+ The plugin integrates with OpenCode through five hooks:
122
+
123
+ ```mermaid
124
+ sequenceDiagram
125
+ participant User
126
+ participant OpenCode
127
+ participant Plugin
128
+ participant AccountManager
129
+ participant Anthropic
130
+
131
+ Note over OpenCode,Plugin: Phase 1: Authentication
132
+ User->>OpenCode: Ctrl+K → Connect Provider
133
+ OpenCode->>Plugin: methods[0].authorize()
134
+ Plugin->>User: Show OAuth URL
135
+ User->>Anthropic: Authorize in browser
136
+ User->>Plugin: Paste auth code
137
+ Plugin->>Anthropic: Exchange code for tokens
138
+ Plugin->>AccountManager: addAccount(tokens)
139
+ AccountManager->>AccountManager: saveToDisk()
140
+ Plugin->>OpenCode: Return credentials
141
+
142
+ Note over OpenCode,Plugin: Phase 2: Loader (runs on next API call)
143
+ OpenCode->>Plugin: loader(getAuth, provider)
144
+ Plugin->>Plugin: Zero out model costs
145
+ Plugin->>AccountManager: Load from disk + auth fallback
146
+ Plugin->>OpenCode: Return {apiKey, fetch}
147
+
148
+ Note over OpenCode,Plugin: Phase 3: Request Handling
149
+ User->>OpenCode: Send message
150
+ OpenCode->>Plugin: fetch(url, init)
151
+ Plugin->>AccountManager: getCurrentAccount()
152
+ AccountManager->>AccountManager: selectAccount(strategy)
153
+ Plugin->>Plugin: Refresh token if expired
154
+ Plugin->>Plugin: Transform body + URL + headers
155
+ Plugin->>Anthropic: POST /v1/messages?beta=true
156
+ alt Success (200)
157
+ Anthropic->>Plugin: Response stream
158
+ Plugin->>Plugin: Strip mcp_ prefixes
159
+ Plugin->>OpenCode: Transformed response
160
+ else Account-specific error (429/401 or 400/403 billing/quota/permission)
161
+ Anthropic->>Plugin: Error response
162
+ Plugin->>AccountManager: markRateLimited(account, reason)
163
+ Plugin->>Plugin: Retry with next account (try each account once)
164
+ else Service-wide error (500/503/529)
165
+ Anthropic->>Plugin: Error response
166
+ Plugin->>OpenCode: Return error directly
167
+ end
168
+ ```
169
+
170
+ ## Request Transformation Pipeline
171
+
172
+ Every API request goes through a multi-stage transformation:
173
+
174
+ ```mermaid
175
+ flowchart LR
176
+ subgraph Input
177
+ Body[Request Body]
178
+ URL[Request URL]
179
+ Headers[Request Headers]
180
+ end
181
+
182
+ subgraph Transform
183
+ TB[transformRequestBody]
184
+ TU[transformRequestUrl]
185
+ TH[buildRequestHeaders]
186
+ end
187
+
188
+ subgraph Output
189
+ OB["Sanitized body<br/>(OpenCode→Claude Code,<br/>tool name prefixing)"]
190
+ OU["Modified URL<br/>(?beta=true)"]
191
+ OH["OAuth headers<br/>(Bearer token,<br/>anthropic-beta,<br/>user-agent)"]
192
+ end
193
+
194
+ Body --> TB --> OB
195
+ URL --> TU --> OU
196
+ Headers --> TH --> OH
197
+
198
+ subgraph Response
199
+ RS[Response Stream] --> Strip["Strip mcp_ prefixes<br/>from tool names"]
200
+ end
201
+ ```
202
+
203
+ ### Body Transformations
204
+
205
+ | Step | What | Why |
206
+ | -------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
207
+ | System prompt sanitization | Replace "OpenCode" with "Claude Code", "opencode" with "Claude" (preserves paths like `/path/to/opencode-foo`) | Anthropic's API blocks the string "OpenCode" |
208
+ | Tool definition prefixing | Add `mcp_` prefix to `tools[].name` | Required by Anthropic's OAuth API |
209
+ | Tool use prefixing | Add `mcp_` prefix to `tool_use` blocks in `messages[].content` | Matches the tool definition prefixes |
210
+
211
+ ### URL Transformations
212
+
213
+ | Step | What | Why |
214
+ | --------- | ------------------------------------- | --------------------------- |
215
+ | Beta flag | Append `?beta=true` to `/v1/messages` | Enables OAuth beta features |
216
+
217
+ ### Header Transformations
218
+
219
+ | Step | What | Why |
220
+ | ---------------- | -------------------------------------------------- | ------------------------------ |
221
+ | Authorization | `Bearer <access_token>` | OAuth authentication |
222
+ | Beta headers | `oauth-2025-04-20,interleaved-thinking-2025-05-14` | Required beta features |
223
+ | User agent | `claude-cli/2.1.2 (external, cli)` | Identifies as Claude CLI |
224
+ | Remove x-api-key | Delete if present | OAuth uses Bearer, not API key |
225
+
226
+ ### Response Transformations
227
+
228
+ | Step | What | Why |
229
+ | -------------- | -------------------------------------------- | ---------------------------- |
230
+ | Strip prefixes | Remove `mcp_` from `"name"` fields in stream | Undo the tool name prefixing |
231
+
232
+ ## Account Selection Engine
233
+
234
+ ### Strategy: Sticky (Default)
235
+
236
+ ```mermaid
237
+ flowchart TD
238
+ Start[Select Account] --> HasCurrent{Current account<br/>exists?}
239
+ HasCurrent -->|Yes| IsAvailable{Available and<br/>healthy?}
240
+ IsAvailable -->|Yes| UseCurrent[Use current account]
241
+ IsAvailable -->|No| NextAvailable[Use next available<br/>via cursor]
242
+ HasCurrent -->|No| NextAvailable
243
+ ```
244
+
245
+ Stays on one account until it fails or is rate-limited. Best for single-account use.
246
+
247
+ ### Strategy: Round-Robin
248
+
249
+ ```mermaid
250
+ flowchart TD
251
+ Start[Select Account] --> Filter[Filter enabled,<br/>non-rate-limited]
252
+ Filter --> Pick["Pick accounts[cursor % count]"]
253
+ Pick --> Advance[cursor++]
254
+ ```
255
+
256
+ Rotates through accounts on every request. Spreads load evenly.
257
+
258
+ ### Strategy: Hybrid
259
+
260
+ ```mermaid
261
+ flowchart TD
262
+ Start[Select Account] --> Filter[Filter enabled,<br/>non-rate-limited,<br/>healthy, has tokens]
263
+ Filter --> Score[Calculate hybrid score<br/>for each candidate]
264
+ Score --> Stickiness[Add stickiness bonus<br/>to current account]
265
+ Stickiness --> Compare{Best score vs<br/>current score}
266
+ Compare -->|"Advantage < threshold"| KeepCurrent[Keep current account]
267
+ Compare -->|"Advantage >= threshold"| SwitchBest[Switch to best account]
268
+ ```
269
+
270
+ **Hybrid score formula:**
271
+
272
+ ```
273
+ score = (healthScore × 2) + ((tokens / maxTokens) × 500) + min(secondsSinceUsed, 3600) × 0.1
274
+ ```
275
+
276
+ - **Health component** (0-200): Rewards reliable accounts
277
+ - **Token component** (0-500): Prefers accounts with available rate limit budget (`tokens / maxTokens` ratio scaled to 0-500)
278
+ - **Freshness component** (0-360): Slight preference for less recently used accounts
279
+ - **Stickiness bonus** (+150): Added to current account to prevent unnecessary switching
280
+ - **Switch threshold** (100): Must beat current by this much to trigger a switch
281
+
282
+ ### Health Score System
283
+
284
+ Each account has a health score (0-100) that tracks reliability:
285
+
286
+ | Event | Score Change |
287
+ | ------------------ | ------------ |
288
+ | Successful request | +1 |
289
+ | Rate limited | -10 |
290
+ | General failure | -20 |
291
+ | Passive recovery | +2 per hour |
292
+
293
+ Accounts below `min_usable` (default: 50) are skipped by the hybrid strategy.
294
+
295
+ ### Token Bucket Rate Limiting
296
+
297
+ Client-side rate limiting prevents sending requests to accounts that are likely to be rate-limited:
298
+
299
+ | Parameter | Default | Description |
300
+ | ------------------------------ | ------- | -------------------------------- |
301
+ | `max_tokens` | 50 | Maximum tokens per account |
302
+ | `regeneration_rate_per_minute` | 6 | Tokens regenerated per minute |
303
+ | `initial_tokens` | 50 | Starting tokens for new accounts |
304
+
305
+ Each request consumes 1 token. When an account runs out, it's skipped until tokens regenerate.
306
+
307
+ ## Rate Limit Handling
308
+
309
+ ### Error Classification
310
+
311
+ | HTTP Status / Body Classifier | Parsed Reason | Default Backoff |
312
+ | -------------------------------------------------- | --------------------- | ---------------------------- |
313
+ | 429 | `RATE_LIMIT_EXCEEDED` | 30s |
314
+ | 401 | `AUTH_FAILED` | 5s |
315
+ | 400/403 with quota/billing/auth/permission signals | `QUOTA_EXHAUSTED` | 1m, 5m, 30m, 2h (escalating) |
316
+ | 400/403 with rate-limit signals | `RATE_LIMIT_EXCEEDED` | 30s |
317
+ | 500/503/529 | service-wide | no account switching |
318
+
319
+ The `Retry-After` header always takes precedence over calculated backoffs.
320
+
321
+ ### Retry Flow
322
+
323
+ ```mermaid
324
+ flowchart TD
325
+ Request[Send Request] --> Check{Response OK?}
326
+ Check -->|200| Success[Mark success,<br/>return response]
327
+ Check -->|500/503/529| ReturnError[Return error response]
328
+ Check -->|429/401/400/403| Classify{Account-specific?}
329
+ Classify -->|No| ReturnError
330
+ Classify -->|Yes| Parse[Parse error reason]
331
+ Parse --> Mark[Mark account rate-limited]
332
+ Mark --> Attempts{Untried accounts left?}
333
+ Attempts -->|Yes| NextAccount[Select next account]
334
+ Attempts -->|No| Throw[Throw account exhausted]
335
+ NextAccount --> Request
336
+ ```
337
+
338
+ ## Account Storage
339
+
340
+ ### File Format
341
+
342
+ Accounts are stored at `~/.config/opencode/anthropic-accounts.json`:
343
+
344
+ ```json
345
+ {
346
+ "version": 1,
347
+ "accounts": [
348
+ {
349
+ "email": "alice@example.com",
350
+ "refreshToken": "rt_...",
351
+ "addedAt": 1706000000000,
352
+ "lastUsed": 1706000100000,
353
+ "enabled": true,
354
+ "rateLimitResetTimes": {},
355
+ "consecutiveFailures": 0,
356
+ "lastFailureTime": null
357
+ }
358
+ ],
359
+ "activeIndex": 0
360
+ }
361
+ ```
362
+
363
+ ### Safety Measures
364
+
365
+ - **Atomic writes:** Write to temp file, then rename (prevents corruption on crash)
366
+ - **File permissions:** `0600` (owner read/write only)
367
+ - **Gitignore:** Auto-generated `.gitignore` in the config directory
368
+ - **Deduplication:** Accounts with the same refresh token are merged (keeps most recently used)
369
+ - **Debounced saves:** The plugin debounces disk writes to 1 second to avoid excessive I/O
370
+ - **Max accounts:** Hard limit of 10 accounts
371
+
372
+ ## OpenCode Plugin API
373
+
374
+ ### How Plugins Work
375
+
376
+ OpenCode plugins export an async function that receives a `{ client }` object and returns hooks:
377
+
378
+ ```javascript
379
+ export async function AnthropicAuthPlugin({ client }) {
380
+ return {
381
+ // Hook: Register slash commands
382
+ config: async (input) => {
383
+ input.command ??= {};
384
+ input.command["anthropic"] = {
385
+ template: "/anthropic",
386
+ description: "Manage Anthropic multi-account auth",
387
+ };
388
+ },
389
+
390
+ // Hook: Handle slash commands (before default execution)
391
+ "command.execute.before": async (input) => {
392
+ if (input.command !== "anthropic") return;
393
+ // Handle /anthropic subcommands...
394
+ throw new Error("__HANDLED__"); // Prevents default handler
395
+ },
396
+
397
+ // Hook: Transform system prompts
398
+ "experimental.chat.system.transform": (input, output) => { ... },
399
+
400
+ // Hook: Authentication
401
+ auth: {
402
+ provider: "anthropic", // Which provider this plugin handles
403
+ loader(getAuth, provider) { ... }, // Called after auth is stored
404
+ methods: [ // Auth methods shown in Connect dialog
405
+ { label: "...", type: "oauth", authorize: async () => { ... } },
406
+ { label: "...", type: "api" },
407
+ ],
408
+ },
409
+ };
410
+ }
411
+ ```
412
+
413
+ ### Key Integration Points
414
+
415
+ | Hook | When It Runs | What It Does |
416
+ | ------------------------------------ | ------------------------------------------- | -------------------------------------------------- |
417
+ | `config` | Plugin initialization | Registers `/anthropic` slash command |
418
+ | `command.execute.before` | User runs `/anthropic ...` | Routes to slash command handler (in-process CLI) |
419
+ | `auth.methods[].authorize()` | User clicks "Connect Provider" | Starts OAuth flow, returns URL + callback |
420
+ | `auth.methods[].callback(code)` | User pastes auth code | Exchanges code for tokens, adds account |
421
+ | `auth.loader(getAuth, provider)` | After auth is stored, on each state refresh | Initializes AccountManager, returns custom `fetch` |
422
+ | `experimental.chat.system.transform` | Before each API call | Prepends "Claude Code" prefix to system prompt |
423
+
424
+ ### Slash Command Architecture
425
+
426
+ The `/anthropic` slash command runs CLI commands **deterministically in-process** (no subprocess spawning or PATH resolution). The plugin calls `cliMain(argv, { io })` directly, capturing console output via `AsyncLocalStorage`-based IO routing.
427
+
428
+ OAuth flows (`login`, `reauth`) are two-step in slash mode:
429
+
430
+ 1. `/anthropic login` or `/anthropic reauth <N>` &mdash; starts OAuth flow, stores PKCE verifier in-memory with 10-minute TTL
431
+ 2. `/anthropic login complete <code#state>` or `/anthropic reauth complete <code#state>` &mdash; exchanges code for tokens
432
+
433
+ Destructive commands (`remove`, `logout`) auto-inject `--force` to avoid readline prompts. Interactive `manage` is blocked with guidance to use granular commands.
434
+
435
+ ### The `loader` Return Value
436
+
437
+ The loader returns an object with `apiKey` and `fetch`:
438
+
439
+ ```javascript
440
+ return {
441
+ apiKey: "", // Empty string (OAuth doesn't use API keys)
442
+ async fetch(input, init) {
443
+ // This replaces the default fetch for all Anthropic API calls
444
+ // Handles: account selection, token refresh, request transformation,
445
+ // retry loop, response transformation
446
+ },
447
+ };
448
+ ```
449
+
450
+ OpenCode calls this `fetch` function instead of the global `fetch` for all requests to the Anthropic provider.
451
+
452
+ ## Known Limitations
453
+
454
+ ### Custom Provider Namespace
455
+
456
+ We investigated using a custom provider ID (e.g., `anthropic-oauth`) to avoid the conflict entirely. This doesn't work because OpenCode's `mergeProvider()` silently drops providers not found in the models.dev database:
457
+
458
+ ```javascript
459
+ function mergeProvider(providerID, provider) {
460
+ const existing = providers[providerID];
461
+ if (existing) {
462
+ /* merge */ return;
463
+ }
464
+ const match = database[providerID];
465
+ if (!match) return; // ← silently drops unknown providers
466
+ /* ... */
467
+ }
468
+ ```
469
+
470
+ A custom provider requires `opencode.json` config with at least one model definition to bootstrap into the database. This adds significant config management overhead for marginal benefit.
471
+
472
+ ## Testing
473
+
474
+ ### Test Structure
475
+
476
+ | File | Tests | Coverage |
477
+ | ------------------- | ----------------------------------------------------------- | ---------------------------- |
478
+ | `backoff.test.mjs` | Rate limit parsing, backoff calculation | `lib/backoff.mjs` |
479
+ | `rotation.test.mjs` | Health scores, token buckets, selection algorithms | `lib/rotation.mjs` |
480
+ | `config.test.mjs` | Config loading, validation, env overrides | `lib/config.mjs` |
481
+ | `storage.test.mjs` | Account persistence, deduplication, atomic writes | `lib/storage.mjs` |
482
+ | `accounts.test.mjs` | AccountManager lifecycle, pool management, empty bootstrap | `lib/accounts.mjs` |
483
+ | `index.test.mjs` | Plugin lifecycle, fetch interceptor, transforms, slash cmds | `index.mjs`, `lib/oauth.mjs` |
484
+ | `cli.test.mjs` | CLI auth + account commands, IO capture, live usage quotas | `cli.mjs` |
485
+
486
+ ### Writing Tests
487
+
488
+ Tests mock filesystem and network calls. Use the existing patterns:
489
+
490
+ ```javascript
491
+ import { describe, it, expect, vi, beforeEach } from "vitest";
492
+
493
+ // Mock dependencies before importing the module under test
494
+ vi.mock("./lib/storage.mjs", () => ({
495
+ loadAccounts: vi.fn(),
496
+ saveAccounts: vi.fn(),
497
+ getStoragePath: vi.fn(() => "/mock/path"),
498
+ }));
499
+
500
+ // Import after mocking
501
+ import { cmdList } from "./cli.mjs";
502
+ ```
503
+
504
+ ### Running Specific Tests
505
+
506
+ ```bash
507
+ npx vitest run backoff # Run backoff tests only
508
+ npx vitest run --reporter=verbose # Verbose output
509
+ ```
510
+
511
+ ## Dependencies
512
+
513
+ | Package | Type | Purpose |
514
+ | ---------------------- | ---------- | --------------------------------------------------------- |
515
+ | `@openauthjs/openauth` | Production | PKCE code generation for OAuth flow |
516
+ | `@opencode-ai/plugin` | Dev | Plugin API type definitions (used via JSDoc) |
517
+ | `esbuild` | Dev | Bundles plugin + CLI into single files |
518
+ | `vitest` | Dev | Test runner |
519
+ | `eslint` | Dev | Linter (flat config) |
520
+ | `@eslint/js` | Dev | ESLint recommended rules |
521
+ | `prettier` | Dev | Code formatter |
522
+ | `husky` | Dev | Git hooks (pre-commit: lint-staged, pre-push: test + fmt) |
523
+ | `lint-staged` | Dev | Runs prettier + eslint on staged files |
524
+
525
+ The plugin has **one production dependency** (`@openauthjs/openauth`), which is bundled into the dist output by esbuild. The bundled files have zero external dependencies beyond Node.js built-ins.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 SST
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.