use-agent 1.0.0__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.
- use_agent-1.0.0/.github/workflows/deploy.yaml +45 -0
- use_agent-1.0.0/.github/workflows/testing.yaml +36 -0
- use_agent-1.0.0/.gitignore +22 -0
- use_agent-1.0.0/.pre-commit-config.yaml +23 -0
- use_agent-1.0.0/CLAUDE.md +221 -0
- use_agent-1.0.0/LICENSE +25 -0
- use_agent-1.0.0/PKG-INFO +396 -0
- use_agent-1.0.0/README.md +369 -0
- use_agent-1.0.0/config.example.toml +129 -0
- use_agent-1.0.0/pyproject.toml +103 -0
- use_agent-1.0.0/tests/__init__.py +0 -0
- use_agent-1.0.0/tests/test_cache.py +100 -0
- use_agent-1.0.0/tests/test_cli.py +74 -0
- use_agent-1.0.0/tests/test_config.py +93 -0
- use_agent-1.0.0/tests/test_gmail_headers.py +141 -0
- use_agent-1.0.0/tests/test_prompt_rendering.py +218 -0
- use_agent-1.0.0/tests/test_prune_scope.py +39 -0
- use_agent-1.0.0/tests/test_reconcile.py +96 -0
- use_agent-1.0.0/tests/test_report.py +173 -0
- use_agent-1.0.0/tests/test_reporter.py +116 -0
- use_agent-1.0.0/tests/test_settings.py +303 -0
- use_agent-1.0.0/tests/test_storage.py +159 -0
- use_agent-1.0.0/use_agent/__init__.py +8 -0
- use_agent-1.0.0/use_agent/__main__.py +4 -0
- use_agent-1.0.0/use_agent/agent.py +523 -0
- use_agent-1.0.0/use_agent/auth.py +60 -0
- use_agent-1.0.0/use_agent/cache.py +81 -0
- use_agent-1.0.0/use_agent/cli.py +311 -0
- use_agent-1.0.0/use_agent/config.py +70 -0
- use_agent-1.0.0/use_agent/gmail.py +500 -0
- use_agent-1.0.0/use_agent/prompts/classifier.md +215 -0
- use_agent-1.0.0/use_agent/prompts/reply.md +95 -0
- use_agent-1.0.0/use_agent/report.py +309 -0
- use_agent-1.0.0/use_agent/reporter.py +201 -0
- use_agent-1.0.0/use_agent/settings.py +207 -0
- use_agent-1.0.0/use_agent/storage.py +179 -0
- use_agent-1.0.0/use_agent/templates/report.html.j2 +138 -0
- use_agent-1.0.0/use_agent/tools.py +370 -0
- use_agent-1.0.0/uv.lock +1033 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
id-token: write
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout repository
|
|
14
|
+
uses: actions/checkout@v6
|
|
15
|
+
with:
|
|
16
|
+
fetch-depth: 0
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v5
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
run: uv python install 3.14
|
|
23
|
+
|
|
24
|
+
- name: Build
|
|
25
|
+
run: uv build
|
|
26
|
+
|
|
27
|
+
- name: Upload dist
|
|
28
|
+
uses: actions/upload-artifact@v4
|
|
29
|
+
with:
|
|
30
|
+
name: dist
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
publish:
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment: pypi
|
|
37
|
+
steps:
|
|
38
|
+
- name: Download dist
|
|
39
|
+
uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
|
|
44
|
+
- name: Publish package
|
|
45
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Testing
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: ["**"]
|
|
5
|
+
paths-ignore:
|
|
6
|
+
- 'docs/**'
|
|
7
|
+
- '*.md'
|
|
8
|
+
tags-ignore: ["*"]
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python: ["3.14"]
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout repository
|
|
17
|
+
uses: actions/checkout@v6
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python }}
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync
|
|
28
|
+
|
|
29
|
+
- name: Run ruff check
|
|
30
|
+
run: uv run ruff check .
|
|
31
|
+
|
|
32
|
+
- name: Run ruff format check
|
|
33
|
+
run: uv run ruff format --check .
|
|
34
|
+
|
|
35
|
+
- name: Run tests
|
|
36
|
+
run: uv run coverage run && uv run coverage report
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.venv/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.coverage
|
|
8
|
+
.coverage.*
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
|
|
12
|
+
# Local secrets & private config — NEVER commit
|
|
13
|
+
credentials.json
|
|
14
|
+
token.json
|
|
15
|
+
config.toml
|
|
16
|
+
*.pem
|
|
17
|
+
.env
|
|
18
|
+
.env.*
|
|
19
|
+
!.env.example
|
|
20
|
+
!config.example.toml
|
|
21
|
+
|
|
22
|
+
justfile
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v5.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-added-large-files
|
|
9
|
+
- id: check-merge-conflict
|
|
10
|
+
- id: check-toml
|
|
11
|
+
- id: debug-statements
|
|
12
|
+
|
|
13
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
14
|
+
rev: v0.15.11
|
|
15
|
+
hooks:
|
|
16
|
+
- id: ruff-check
|
|
17
|
+
args: [--fix]
|
|
18
|
+
- id: ruff-format
|
|
19
|
+
|
|
20
|
+
- repo: https://github.com/tombi-toml/tombi-pre-commit
|
|
21
|
+
rev: v0.7.25
|
|
22
|
+
hooks:
|
|
23
|
+
- id: tombi-format
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
`use-agent` is a Claude Agent that triages a Gmail inbox for
|
|
8
|
+
unsolicited sales email and bulk marketing. It replies to cold
|
|
9
|
+
sales pitches in the user's voice and archives them; for bulk
|
|
10
|
+
marketing it honors the RFC 8058 one-click unsubscribe and
|
|
11
|
+
Trashes the thread. It's built on the Claude Agent SDK; Gmail API
|
|
12
|
+
operations are exposed as tools via an in-process SDK MCP server.
|
|
13
|
+
|
|
14
|
+
Python 3.14, uv, hatchling + hatch-vcs, ruff. Runtime deps:
|
|
15
|
+
`claude-agent-sdk`, `google-api-python-client`,
|
|
16
|
+
`google-auth-oauthlib`, `jinja2`, `rich`.
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv sync # install runtime + dev deps
|
|
22
|
+
uv run use-agent auth # one-time Gmail OAuth
|
|
23
|
+
uv run use-agent run --dry-run # classify only
|
|
24
|
+
uv run use-agent run # full run: reply + archive
|
|
25
|
+
uv run use-agent run --daemon --interval 30m --logfile run.log
|
|
26
|
+
uv run use-agent run --json | jq . # stdout is pure JSON
|
|
27
|
+
|
|
28
|
+
uv run ruff check .
|
|
29
|
+
uv run ruff format --check .
|
|
30
|
+
uv run coverage run # pytest under coverage
|
|
31
|
+
uv run coverage report
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`ANTHROPIC_API_KEY` is required for any `run`.
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
The data flow is deliberately narrow. All user-specific context enters
|
|
39
|
+
through `config.toml`; everything else is derivable from the code.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
cli.main → Settings.load() → agent.run(settings, reporter)
|
|
43
|
+
↓
|
|
44
|
+
_render_system_prompt(settings)
|
|
45
|
+
↓
|
|
46
|
+
claude_agent_sdk.query(
|
|
47
|
+
prompt=<user-prompt>,
|
|
48
|
+
options=ClaudeAgentOptions(
|
|
49
|
+
system_prompt=<rendered>,
|
|
50
|
+
mcp_servers={'gmail': <built from GmailClient>},
|
|
51
|
+
allowed_tools=[mcp__gmail__search, ...],
|
|
52
|
+
setting_sources=[], # hermetic
|
|
53
|
+
model=<from config>,
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
↓
|
|
57
|
+
streaming AssistantMessage/TextBlock
|
|
58
|
+
↓
|
|
59
|
+
reporter.on_text(...) (→ narration logger)
|
|
60
|
+
↓
|
|
61
|
+
reporter.finish() (→ stdout: table/JSON)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Key module responsibilities:
|
|
65
|
+
|
|
66
|
+
- `gmail.GmailClient` is the only module that touches the Gmail REST
|
|
67
|
+
API. It returns frozen `Message` dataclasses so tool output stays
|
|
68
|
+
JSON-friendly.
|
|
69
|
+
- `tools.build_mcp_server(client)` wraps six `@claude_agent_sdk.tool`
|
|
70
|
+
closures (`search`, `get_message`, `reply`, `archive_and_mark_read`,
|
|
71
|
+
`unsubscribe_and_trash`, `trash`) over a `GmailClient` and returns
|
|
72
|
+
an SDK MCP server. The closure approach lets the tools share state
|
|
73
|
+
without a module-global client.
|
|
74
|
+
- The `unsubscribe_and_trash` tool takes the `list_unsubscribe` and
|
|
75
|
+
`list_unsubscribe_post` header strings as arguments (not just the
|
|
76
|
+
message_id) so it doesn't re-fetch the message. Headers come from
|
|
77
|
+
the preceding `get_message` call. The tool chooses between RFC 8058
|
|
78
|
+
one-click HTTPS POST, HTTPS GET, and mailto based on what the
|
|
79
|
+
headers expose (see `tools._pick_unsubscribe_action`).
|
|
80
|
+
- `agent.run()` is the orchestration entry point: renders the system
|
|
81
|
+
prompt, builds the tool server, configures `ClaudeAgentOptions`,
|
|
82
|
+
streams responses into the `Reporter`, and returns an exit code.
|
|
83
|
+
Python does no per-message logic — the agent drives the loop.
|
|
84
|
+
- `reporter.Reporter` owns all output. **`on_text` never writes to
|
|
85
|
+
stdout**; narration goes to the `use_agent.narration` logger
|
|
86
|
+
instead. `finish()` parses the last ` ```json ` fenced block from
|
|
87
|
+
the buffered text and renders it as a Rich table (`pretty`),
|
|
88
|
+
pipe-delimited ASCII (`plain`), or a JSON document (`json`).
|
|
89
|
+
- `settings.Settings` is a frozen dataclass loaded from
|
|
90
|
+
`config.toml` (path via `USE_AGENT_CONFIG` env var or
|
|
91
|
+
`~/.config/use-agent/config.toml`). If `[search] query` is omitted,
|
|
92
|
+
a default `in:inbox is:unread` query is built from the safelist
|
|
93
|
+
domains as `-from:<domain>` filters.
|
|
94
|
+
|
|
95
|
+
### Prompts are Jinja2 templates
|
|
96
|
+
|
|
97
|
+
`use_agent/prompts/classifier.md` and `reply.md` are Jinja2 templates
|
|
98
|
+
rendered at startup. Context injected by `agent._render_context`:
|
|
99
|
+
`user_name`, `organization`, `safelist_domains`, `vendor_names`,
|
|
100
|
+
`voice_guidelines`, `newsletter_keep_domains`,
|
|
101
|
+
`newsletter_keep_list_ids`, `reply_footer`, plus pre-rendered strings
|
|
102
|
+
(`voice_block`, `footer_block`, `footer_instruction`,
|
|
103
|
+
`hard_remove_examples`, etc.). `newsletter_keep_block` is rendered
|
|
104
|
+
separately in `_render_system_prompt` because only the top-level
|
|
105
|
+
system prompt template uses it. The pre-rendered strings exist
|
|
106
|
+
because the repo's pre-commit Markdown formatter strips blank lines
|
|
107
|
+
inside `{% if %}` blocks — avoid Jinja control flow in prompt
|
|
108
|
+
Markdown; pre-render composite blocks in Python and inject them as
|
|
109
|
+
single `{{ ... }}` substitutions.
|
|
110
|
+
|
|
111
|
+
`reply_footer` is itself a Jinja string (may reference `{{ user_name
|
|
112
|
+
}}` etc.) and is rendered once in `_render_context` before being
|
|
113
|
+
injected into the reply template.
|
|
114
|
+
|
|
115
|
+
### Editing agent behavior without touching Python
|
|
116
|
+
|
|
117
|
+
Most behavior tweaks belong in `config.toml` or the prompt Markdown:
|
|
118
|
+
|
|
119
|
+
- New classification signal → edit `prompts/classifier.md`
|
|
120
|
+
- Change reply voice / add a template → edit `prompts/reply.md`
|
|
121
|
+
- Add a vendor exemption → `[vendors] names` in `config.toml`
|
|
122
|
+
- Add a safelisted domain → `[safelist] domains` in `config.toml`
|
|
123
|
+
- Keep a bulk sender (community list, opted-in newsletter) →
|
|
124
|
+
`[newsletters] keep_domains` / `keep_list_ids` in `config.toml`
|
|
125
|
+
- Change footer text / disable footer → `[voice] footer` (or `""`)
|
|
126
|
+
- Change model → `[agent] model`
|
|
127
|
+
|
|
128
|
+
Python changes are only needed when the set of tools, output modes,
|
|
129
|
+
CLI flags, or Gmail operations needs to change.
|
|
130
|
+
|
|
131
|
+
### Hermetic SDK configuration
|
|
132
|
+
|
|
133
|
+
`agent.run()` passes `setting_sources=[]`, `settings=None`,
|
|
134
|
+
`skills=None` to `ClaudeAgentOptions` so the agent never merges in
|
|
135
|
+
the developer's `~/.claude/settings.json`, project-level Claude Code
|
|
136
|
+
config, plugins, or auto-discovered skills. The only MCP server the
|
|
137
|
+
agent sees is our Gmail one; the only tools it can call are the four
|
|
138
|
+
in `tools.ALLOWED_TOOLS`. Keep it that way — preserving isolation is
|
|
139
|
+
part of the trust model.
|
|
140
|
+
|
|
141
|
+
### Threaded replies
|
|
142
|
+
|
|
143
|
+
Gmail only collapses a reply into the original conversation when all
|
|
144
|
+
three are correct: `In-Reply-To: <original Message-ID>`,
|
|
145
|
+
`References: <chain> <original Message-ID>`, and `threadId` in the
|
|
146
|
+
`users.messages.send` request. `gmail.GmailClient.reply` assembles
|
|
147
|
+
all three from the fetched original. Don't regress this — dropping
|
|
148
|
+
`threadId` or the headers silently demotes replies into new threads.
|
|
149
|
+
|
|
150
|
+
### Unsubscribing (RFC 2369 / RFC 8058)
|
|
151
|
+
|
|
152
|
+
`gmail.unsubscribe_targets(list_unsubscribe, list_unsubscribe_post)`
|
|
153
|
+
parses the raw `List-Unsubscribe` header into `{http_urls, mailtos,
|
|
154
|
+
one_click}`. The splitter (`_split_list_unsubscribe`) respects
|
|
155
|
+
angle-bracket nesting because mailto URIs may contain literal
|
|
156
|
+
commas in a `body=` parameter — a plain `split(',')` breaks those.
|
|
157
|
+
|
|
158
|
+
The `unsubscribe_and_trash` tool calls `_pick_unsubscribe_action`,
|
|
159
|
+
which returns `(method, target)`:
|
|
160
|
+
|
|
161
|
+
1. `one_click_post` — RFC 8058: POST `List-Unsubscribe=One-Click`
|
|
162
|
+
as a form body to the HTTPS URI. This is what Gmail and Apple
|
|
163
|
+
Mail use when the user clicks the native "Unsubscribe" button.
|
|
164
|
+
2. `http_get` — GET the HTTPS URI.
|
|
165
|
+
3. `mailto` — send a standalone (non-threaded) email via
|
|
166
|
+
`GmailClient.send_unsubscribe_mail` to the address in the
|
|
167
|
+
mailto URI, using its `subject=` / `body=` parameters.
|
|
168
|
+
4. `none` — no endpoint available; Trash only.
|
|
169
|
+
|
|
170
|
+
Both the HTTP request (`_http_unsubscribe`) and the mailto send are
|
|
171
|
+
sync-blocking. They run inside the async MCP tool handler, which
|
|
172
|
+
matches the existing googleapiclient pattern throughout `tools.py`.
|
|
173
|
+
The HTTP request has a 10-second timeout (`_UNSUB_HTTP_TIMEOUT`) so
|
|
174
|
+
a stalled endpoint can't hang the whole run.
|
|
175
|
+
|
|
176
|
+
In-body HTML unsubscribe links are NEVER clicked — clicking
|
|
177
|
+
validates the address to shady senders who already ignored the
|
|
178
|
+
RFC. Messages with no `List-Unsubscribe` header get
|
|
179
|
+
`response_mode=delete` and go straight to Trash.
|
|
180
|
+
|
|
181
|
+
### Output contract
|
|
182
|
+
|
|
183
|
+
The agent is prompted to emit exactly one fenced ` ```json ` block
|
|
184
|
+
containing `{"results": [...]}`. The reporter's `_extract_summary`
|
|
185
|
+
walks the buffered text from the end backwards looking for the last
|
|
186
|
+
parseable JSON fence with a `results` array (or a bare list of rows
|
|
187
|
+
with a `classification` key). If the agent forgets to emit the
|
|
188
|
+
block, `finish()` returns exit code 1 — treat this as a real error.
|
|
189
|
+
|
|
190
|
+
### Logging
|
|
191
|
+
|
|
192
|
+
| Logger | Level | Purpose |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `use_agent.*` | INFO | Notable one-offs (OAuth flow, reply sent); lifecycle (run start, daemon tick, cache stats) is DEBUG |
|
|
195
|
+
| `use_agent.narration` | INFO | Agent's running commentary (buffered text) |
|
|
196
|
+
| `use_agent.tools` | DEBUG | Per-tool-call Gmail operations (search, get, reply, archive, unsubscribe, trash) |
|
|
197
|
+
| `claude_agent_sdk` | WARNING | Pinned; SDK INFO is too chatty |
|
|
198
|
+
|
|
199
|
+
All handlers write to stderr (plus the `--logfile` target when set)
|
|
200
|
+
so stdout stays pure in `--json` mode.
|
|
201
|
+
|
|
202
|
+
## Conventions
|
|
203
|
+
|
|
204
|
+
- **Module-level imports only.** A pre-commit hook rejects `from
|
|
205
|
+
pathlib import Path` / `from typing import Any` style imports —
|
|
206
|
+
use `import pathlib` / `import typing`, then `pathlib.Path(...)`,
|
|
207
|
+
`typing.Any`.
|
|
208
|
+
- Ruff: 79-char lines, single quotes, `py314` target. Config lives
|
|
209
|
+
in `pyproject.toml`.
|
|
210
|
+
- No compound shell commands: each `Bash` call is one command
|
|
211
|
+
(repo-wide preference, not project-specific, but still applies).
|
|
212
|
+
- `credentials.json`, `token.json`, and `config.toml` are all
|
|
213
|
+
gitignored. The repo itself contains no identifying information;
|
|
214
|
+
`config.example.toml` uses placeholder values only.
|
|
215
|
+
|
|
216
|
+
## Version
|
|
217
|
+
|
|
218
|
+
`use_agent.__version__` is read at runtime via
|
|
219
|
+
`importlib.metadata.version('use-agent')`, with a `0.0.0+unknown`
|
|
220
|
+
fallback. `[project] version` is pinned in `pyproject.toml`; hatch-vcs
|
|
221
|
+
is configured but not currently generating a version file.
|
use_agent-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Copyright (c) 2026 Gavin M. Roy, AWeber
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
5
|
+
are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
|
11
|
+
and/or other materials provided with the distribution.
|
|
12
|
+
* Neither the name of the copyright holder nor the names of its contributors
|
|
13
|
+
may be used to endorse or promote products derived from this software without
|
|
14
|
+
specific prior written permission.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
17
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
18
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
19
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
20
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
21
|
+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
22
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
23
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
|
24
|
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
25
|
+
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|