aether-cache 0.1.0__tar.gz → 0.1.1__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 (28) hide show
  1. aether_cache-0.1.1/.gitignore +21 -0
  2. {aether_cache-0.1.0 → aether_cache-0.1.1}/PKG-INFO +9 -8
  3. {aether_cache-0.1.0 → aether_cache-0.1.1}/README.md +3 -2
  4. {aether_cache-0.1.0 → aether_cache-0.1.1}/aether_cache/__init__.py +1 -1
  5. {aether_cache-0.1.0 → aether_cache-0.1.1}/aether_cache/cli.py +5 -1
  6. {aether_cache-0.1.0 → aether_cache-0.1.1}/aether_cache/config.py +1 -0
  7. {aether_cache-0.1.0 → aether_cache-0.1.1}/aether_cache/engine/aether-engine.jar +0 -0
  8. {aether_cache-0.1.0 → aether_cache-0.1.1}/aether_cache/engine.py +37 -5
  9. {aether_cache-0.1.0 → aether_cache-0.1.1}/aether_cache/server.py +51 -20
  10. aether_cache-0.1.1/aether_cache/ui/fonts/OFL.txt +93 -0
  11. aether_cache-0.1.1/aether_cache/ui/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  12. aether_cache-0.1.1/aether_cache/ui/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  13. aether_cache-0.1.1/aether_cache/ui/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  14. aether_cache-0.1.1/aether_cache/ui/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  15. aether_cache-0.1.1/aether_cache/ui/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  16. aether_cache-0.1.1/aether_cache/ui/index.html +774 -0
  17. aether_cache-0.1.1/aether_cache/ui.py +349 -0
  18. aether_cache-0.1.1/pyproject.toml +34 -0
  19. aether_cache-0.1.0/aether_cache.egg-info/PKG-INFO +0 -353
  20. aether_cache-0.1.0/aether_cache.egg-info/SOURCES.txt +0 -16
  21. aether_cache-0.1.0/aether_cache.egg-info/dependency_links.txt +0 -1
  22. aether_cache-0.1.0/aether_cache.egg-info/entry_points.txt +0 -2
  23. aether_cache-0.1.0/aether_cache.egg-info/requires.txt +0 -3
  24. aether_cache-0.1.0/aether_cache.egg-info/top_level.txt +0 -1
  25. aether_cache-0.1.0/pyproject.toml +0 -28
  26. aether_cache-0.1.0/setup.cfg +0 -4
  27. {aether_cache-0.1.0 → aether_cache-0.1.1}/aether_cache/__main__.py +0 -0
  28. {aether_cache-0.1.0 → aether_cache-0.1.1}/aether_cache/connect.py +0 -0
@@ -0,0 +1,21 @@
1
+ *.jar
2
+ !aether_cache/engine/aether-engine.jar
3
+ target/
4
+ .idea/
5
+ src/main/java/embeddings_cache.npz
6
+ __pycache__/
7
+ *.egg-info/
8
+ build/
9
+ dist/
10
+
11
+ # AI tool and design-hook config (Impeccable etc.), kept local
12
+ .claude/
13
+ .agent/
14
+ .agents/
15
+ .codex/
16
+ .cursor/
17
+ .gemini/
18
+ .kiro/
19
+ .impeccable/
20
+ PRODUCT.md
21
+ DESIGN.md
@@ -1,14 +1,14 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: aether-cache
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Shared memory for AI coding tools that stays true to your code.
5
- Author: Anish Chugh
6
5
  Project-URL: Homepage, https://github.com/anishchugh05/Aether-Cache-Redis-Compatible-Vector-Store
6
+ Author: Anish Chugh
7
7
  Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
- Requires-Dist: mcp>=1.2
10
8
  Requires-Dist: fastembed>=0.3
9
+ Requires-Dist: mcp>=1.2
11
10
  Requires-Dist: redis>=5.0
11
+ Description-Content-Type: text/markdown
12
12
 
13
13
  [![progress-banner](https://backend.codecrafters.io/progress/redis/f43a7cd3-11e6-45dc-a8ba-9d6690c73ac0)](https://app.codecrafters.io/users/anishchugh05?r=2qF)
14
14
 
@@ -106,7 +106,7 @@ You need **Java 11+** (`brew install openjdk@21` on macOS, or [adoptium.net](htt
106
106
  Python for you. Then:
107
107
 
108
108
  ```sh
109
- uv tool install git+https://github.com/anishchugh05/Aether-Cache-Redis-Compatible-Vector-Store
109
+ uv tool install aether-cache
110
110
  aether-cache setup
111
111
  ```
112
112
 
@@ -124,6 +124,7 @@ the engine only accepts connections from it, and your memories live in `~/.aethe
124
124
  | Command | What it does |
125
125
  |---|---|
126
126
  | `aether-cache setup` | One-time setup: engine, model, and AI tools |
127
+ | `aether-cache ui` | Opens the dashboard: see what your AI remembers, and confirm or retire memories whose files changed |
127
128
  | `aether-cache status` | Is it running, and what's in this project's memory |
128
129
  | `aether-cache doctor` | Checks everything and tells you how to fix what's wrong |
129
130
  | `aether-cache list` / `recall "query"` / `explain m1` | Look at memories from the terminal |
@@ -322,7 +323,6 @@ Aether Cache is v1 and early. It works end to end in automated tests and is now
322
323
  - **A with-memory vs. without-memory evaluation** on real coding tasks, to measure whether it actually reduces
323
324
  re-explaining.
324
325
  - **AOF compaction:** the log only grows, and every status change adds a line.
325
- - **A dashboard** to browse, confirm, and retire memories in the browser.
326
326
 
327
327
  **Engine limitations:**
328
328
  - **Threading:** one OS thread per connection, not an event loop. That's fine for a local tool, but it won't scale to
@@ -346,8 +346,9 @@ aether_cache/ the Python package behind the `aether-cache` comman
346
346
  engine.py finds Java, starts and stops the engine in the background
347
347
  connect.py registers Aether Cache with Claude Code, Cursor, and Codex
348
348
  cli.py the `aether-cache` command
349
+ ui.py, ui/index.html the dashboard (`aether-cache ui`): a local server, its API, and the page
349
350
  engine/aether-engine.jar the prebuilt engine (rebuild with `python scripts/build_engine.py`)
350
351
  memory_mcp/calibrate.py similarity calibration for duplicate/contradiction thresholds
351
352
  vector_bench.py HNSW benchmark (writes results.json)
352
353
  crash_test.py kill -9 crash-safety test
353
- ```
354
+ ```
@@ -94,7 +94,7 @@ You need **Java 11+** (`brew install openjdk@21` on macOS, or [adoptium.net](htt
94
94
  Python for you. Then:
95
95
 
96
96
  ```sh
97
- uv tool install git+https://github.com/anishchugh05/Aether-Cache-Redis-Compatible-Vector-Store
97
+ uv tool install aether-cache
98
98
  aether-cache setup
99
99
  ```
100
100
 
@@ -112,6 +112,7 @@ the engine only accepts connections from it, and your memories live in `~/.aethe
112
112
  | Command | What it does |
113
113
  |---|---|
114
114
  | `aether-cache setup` | One-time setup: engine, model, and AI tools |
115
+ | `aether-cache ui` | Opens the dashboard: see what your AI remembers, and confirm or retire memories whose files changed |
115
116
  | `aether-cache status` | Is it running, and what's in this project's memory |
116
117
  | `aether-cache doctor` | Checks everything and tells you how to fix what's wrong |
117
118
  | `aether-cache list` / `recall "query"` / `explain m1` | Look at memories from the terminal |
@@ -310,7 +311,6 @@ Aether Cache is v1 and early. It works end to end in automated tests and is now
310
311
  - **A with-memory vs. without-memory evaluation** on real coding tasks, to measure whether it actually reduces
311
312
  re-explaining.
312
313
  - **AOF compaction:** the log only grows, and every status change adds a line.
313
- - **A dashboard** to browse, confirm, and retire memories in the browser.
314
314
 
315
315
  **Engine limitations:**
316
316
  - **Threading:** one OS thread per connection, not an event loop. That's fine for a local tool, but it won't scale to
@@ -334,6 +334,7 @@ aether_cache/ the Python package behind the `aether-cache` comman
334
334
  engine.py finds Java, starts and stops the engine in the background
335
335
  connect.py registers Aether Cache with Claude Code, Cursor, and Codex
336
336
  cli.py the `aether-cache` command
337
+ ui.py, ui/index.html the dashboard (`aether-cache ui`): a local server, its API, and the page
337
338
  engine/aether-engine.jar the prebuilt engine (rebuild with `python scripts/build_engine.py`)
338
339
  memory_mcp/calibrate.py similarity calibration for duplicate/contradiction thresholds
339
340
  vector_bench.py HNSW benchmark (writes results.json)
@@ -1,3 +1,3 @@
1
1
  """Aether Cache: shared memory for AI coding tools that stays true to your code."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.1"
@@ -59,7 +59,7 @@ def cmd_setup(args) -> int:
59
59
  ' "Remember that our tests need DOCKER_HOST set. It depends on docker-compose.yml."\n'
60
60
  "Then, in a new session:\n"
61
61
  ' "How do I run the tests?"\n'
62
- "\nCheck on things any time with `aether-cache status`.")
62
+ "\nSee and fix what your AI remembers any time with `aether-cache ui`.")
63
63
  return 0
64
64
 
65
65
 
@@ -220,6 +220,10 @@ def main(argv=None) -> None:
220
220
  func=lambda a: _tool_output("sync_to_files"))
221
221
  sub.add_parser("check", help="retire memories whose linked files changed").set_defaults(
222
222
  func=lambda a: _tool_output("check_stale"))
223
+ p = sub.add_parser("ui", help="open the dashboard: see and fix what your AI tools remember")
224
+ p.add_argument("--port", type=int, help="port for the dashboard (default 7381)")
225
+ p.add_argument("--no-open", action="store_true", help="don't open a browser, just print the link")
226
+ p.set_defaults(func=lambda a: __import__("aether_cache.ui", fromlist=["serve"]).serve(a.port, not a.no_open))
223
227
  sub.add_parser("serve", help="run the MCP server (your AI tool runs this for you)").set_defaults(func=cmd_serve)
224
228
 
225
229
  args = parser.parse_args(argv)
@@ -17,6 +17,7 @@ HOME = Path(_env("AETHER_HOME", default=str(Path.home() / ".aether-cache"))).exp
17
17
  DATA_DIR = HOME / "data"
18
18
  LOG_FILE = HOME / "engine.log"
19
19
  PID_FILE = HOME / "engine.pid"
20
+ STAMP_FILE = HOME / "engine.version" # which engine file the running engine was started from
20
21
  MODELS_DIR = HOME / "models"
21
22
 
22
23
  # The engine listens on this machine only. The port is deliberately not Redis's 6379, so Aether Cache
@@ -153,10 +153,36 @@ def _log_tail(lines: int = 15) -> str:
153
153
  return ""
154
154
 
155
155
 
156
+ _fingerprint = None
157
+
158
+
159
+ def jar_fingerprint() -> str:
160
+ """Identifies this version of the engine file, so a running engine from an older version can be spotted."""
161
+ global _fingerprint
162
+ if _fingerprint is None:
163
+ import hashlib
164
+ _fingerprint = hashlib.sha256(JAR.read_bytes()).hexdigest()[:16] if JAR.is_file() else ""
165
+ return _fingerprint
166
+
167
+
168
+ def needs_upgrade() -> bool:
169
+ """True if the engine we started earlier came from a different engine file than this install's
170
+ (e.g. after `uv tool upgrade aether-cache`). Engines we didn't start are left alone."""
171
+ if running_pid() is None:
172
+ return False
173
+ try:
174
+ stamp = config.STAMP_FILE.read_text().strip()
175
+ except OSError:
176
+ stamp = "" # started by a version from before stamps existed
177
+ return stamp != jar_fingerprint()
178
+
179
+
156
180
  def start(wait: float = 60) -> str:
157
- """Starts the engine in the background (if it isn't already running) and waits until it answers."""
181
+ """Starts the engine in the background (if it isn't already running) and waits until it answers.
182
+ If the running engine is from an older install, restarts it on the new one (no data is lost:
183
+ every acknowledged write is already on disk)."""
158
184
  state = probe()
159
- if state == "aether":
185
+ if state == "aether" and not needs_upgrade():
160
186
  return f"Engine already running on {config.HOST}:{config.PORT}."
161
187
  if state == "other":
162
188
  raise EngineError(port_taken_message())
@@ -166,10 +192,14 @@ def start(wait: float = 60) -> str:
166
192
  if not JAR.is_file():
167
193
  raise EngineError(f"The engine file is missing ({JAR}). Reinstall Aether Cache.")
168
194
 
195
+ upgraded = False
169
196
  with _start_lock():
170
197
  state = probe() # another session may have started it while we waited for the lock
171
198
  if state == "aether":
172
- return f"Engine already running on {config.HOST}:{config.PORT}."
199
+ if not needs_upgrade():
200
+ return f"Engine already running on {config.HOST}:{config.PORT}."
201
+ stop()
202
+ upgraded = True
173
203
  if state == "other":
174
204
  raise EngineError(port_taken_message())
175
205
  java, _ = find_java()
@@ -201,7 +231,9 @@ def start(wait: float = 60) -> str:
201
231
  + _log_tail())
202
232
  state = probe()
203
233
  if state == "aether":
204
- started = f"Engine started on {config.HOST}:{config.PORT} (data in {config.DATA_DIR})."
234
+ config.STAMP_FILE.write_text(jar_fingerprint())
235
+ started = (f"Engine {'updated and restarted' if upgraded else 'started'} on {config.HOST}:{config.PORT} "
236
+ f"(data in {config.DATA_DIR}).")
205
237
  return f"{note}\n{started}" if note else started
206
238
  if state == "other":
207
239
  process.terminate()
@@ -211,7 +243,7 @@ def start(wait: float = 60) -> str:
211
243
 
212
244
 
213
245
  def ensure_running() -> None:
214
- if probe() != "aether":
246
+ if probe() != "aether" or needs_upgrade():
215
247
  start()
216
248
 
217
249
 
@@ -170,9 +170,28 @@ def git_toplevel() -> Optional[Path]:
170
170
  return None
171
171
 
172
172
 
173
+ REPO_KEY = "aether:repo:" # aether:repo:<project> -> the repo's folder, so the dashboard can check its files
174
+ _registered = set()
175
+
176
+
173
177
  def project_name() -> str:
174
178
  """The project memories belong to: AETHER_PROJECT, else the git repo's folder name, else this folder's."""
175
- return config.project_override() or (git_toplevel() or workdir()).name
179
+ name = config.project_override() or (git_toplevel() or workdir()).name
180
+ _remember_repo_location(name)
181
+ return name
182
+
183
+
184
+ def _remember_repo_location(project: str) -> None:
185
+ """Records where this project's repo lives (once per session), so `aether-cache ui`, which runs
186
+ from anywhere, can still check its memories' files. Best effort: never gets in the AI's way."""
187
+ top = git_toplevel()
188
+ if top is None or (project, str(top)) in _registered:
189
+ return
190
+ _registered.add((project, str(top)))
191
+ try:
192
+ db().set(REPO_KEY + project, str(top))
193
+ except Exception:
194
+ _registered.discard((project, str(top)))
176
195
 
177
196
 
178
197
  def repo_root() -> Path:
@@ -180,12 +199,12 @@ def repo_root() -> Path:
180
199
  return git_toplevel() or workdir()
181
200
 
182
201
 
183
- def relative(path: str) -> str:
202
+ def relative(path: str, root: Optional[Path] = None) -> str:
184
203
  """Makes a path repo-relative, so memories still match if the repo is opened from a subfolder."""
185
204
  p = Path(path.strip())
186
205
  if p.is_absolute():
187
206
  try:
188
- return p.resolve().relative_to(repo_root().resolve()).as_posix()
207
+ return p.resolve().relative_to((root or repo_root()).resolve()).as_posix()
189
208
  except ValueError:
190
209
  return str(p)
191
210
  return p.as_posix()
@@ -197,9 +216,9 @@ def blob_hash(path: Path) -> str:
197
216
  return hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest()
198
217
 
199
218
 
200
- def content_hashes(paths: List[str]) -> dict:
219
+ def content_hashes(paths: List[str], root: Optional[Path] = None) -> dict:
201
220
  """path -> hash of the file's current contents, or "missing"."""
202
- root = repo_root()
221
+ root = root or repo_root()
203
222
  result = {}
204
223
  for p in paths:
205
224
  try:
@@ -209,25 +228,28 @@ def content_hashes(paths: List[str]) -> dict:
209
228
  return result
210
229
 
211
230
 
212
- def link_files(paths: List[str]) -> str:
231
+ def link_files(paths: List[str], root: Optional[Path] = None) -> str:
213
232
  """["a.py", "b.yml"] -> "a.py@<hash>,b.yml@<hash>": the files plus a snapshot of their contents."""
214
- rel = [relative(p) for p in paths if p.strip()]
215
- hashes = content_hashes(rel)
233
+ root = root or repo_root()
234
+ rel = [relative(p, root) for p in paths if p.strip()]
235
+ hashes = content_hashes(rel, root)
216
236
  return ",".join(f"{p}@{hashes[p]}" for p in rel)
217
237
 
218
238
 
219
- def check_staleness() -> List[str]:
239
+ def check_staleness(project: Optional[str] = None, root: Optional[Path] = None) -> List[str]:
220
240
  """Marks active memories stale when a file they depend on has changed since they were saved.
221
- Returns one line per memory that just went stale."""
241
+ Returns one line per memory that just went stale. Defaults to the current project and repo."""
242
+ project = project or project_name()
243
+ root = root or repo_root()
222
244
  newly_stale = []
223
245
  linked = [] # (id, text, [(path, saved hash)])
224
- for mid, _status, files, text in run("MEM.LIST", project_name()):
246
+ for mid, _status, files, text in run("MEM.LIST", project):
225
247
  entries = [f.rsplit("@", 1) for f in files.split(",") if "@" in f]
226
248
  if entries:
227
249
  linked.append((mid, text, entries))
228
250
  if not linked:
229
251
  return newly_stale
230
- current = content_hashes(sorted({path for _, _, entries in linked for path, _ in entries}))
252
+ current = content_hashes(sorted({path for _, _, entries in linked for path, _ in entries}), root)
231
253
  for mid, text, entries in linked:
232
254
  changed = [path for path, saved in entries if current.get(path) != saved]
233
255
  if changed:
@@ -237,6 +259,19 @@ def check_staleness() -> List[str]:
237
259
  return newly_stale
238
260
 
239
261
 
262
+ def confirm(memory_id: str, root: Optional[Path] = None) -> Optional[bool]:
263
+ """Marks a memory still true and re-links it to the current versions of its files.
264
+ Returns whether it has linked files, or None if there's no such memory."""
265
+ r = run("MEM.GET", memory_id)
266
+ if r is None:
267
+ return None
268
+ files = [f.rsplit("@", 1)[0] for f in dict(zip(r[0::2], r[1::2]))["files"].split(",") if f]
269
+ if files:
270
+ run("MEM.FILES", memory_id, link_files(files, root), "REASON", "confirmed still true")
271
+ run("MEM.STATUS", memory_id, "active", "REASON", "confirmed still true")
272
+ return bool(files)
273
+
274
+
240
275
  # ---------- tools the AI can call ----------
241
276
 
242
277
  @mcp.tool()
@@ -363,16 +398,12 @@ def confirm_memory(memory_id: str) -> str:
363
398
  """Mark a memory as still true (e.g. after it was retired because a linked file changed, but the
364
399
  fact itself didn't). Re-links it to the current version of its files."""
365
400
  try:
366
- r = run("MEM.GET", memory_id)
367
- if r is None:
368
- return f"No memory with id {memory_id}."
369
- files = [f.rsplit("@", 1)[0] for f in dict(zip(r[0::2], r[1::2]))["files"].split(",") if f]
370
- if files:
371
- run("MEM.FILES", memory_id, link_files(files), "REASON", "confirmed still true")
372
- run("MEM.STATUS", memory_id, "active", "REASON", "confirmed still true")
401
+ linked = confirm(memory_id)
373
402
  except EngineDown as e:
374
403
  return store_down(e)
375
- return f"Confirmed {memory_id}; it's active again" + (" and linked to the current file versions." if files else ".")
404
+ if linked is None:
405
+ return f"No memory with id {memory_id}."
406
+ return f"Confirmed {memory_id}; it's active again" + (" and linked to the current file versions." if linked else ".")
376
407
 
377
408
 
378
409
  # ---------- syncing to AGENTS.md / CLAUDE.md ----------
@@ -0,0 +1,93 @@
1
+ Copyright 2019 IBM Corp. All rights reserved. IBMPlexSans-Italic[wdth,wght].ttf: Copyright 2019 IBM Corp. All rights reserved.
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.