aether-cache 0.1.3__tar.gz → 0.1.4__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 (24) hide show
  1. {aether_cache-0.1.3 → aether_cache-0.1.4}/PKG-INFO +8 -4
  2. {aether_cache-0.1.3 → aether_cache-0.1.4}/README.md +7 -3
  3. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/__init__.py +1 -1
  4. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/cli.py +37 -4
  5. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/engine.py +36 -14
  6. aether_cache-0.1.4/aether_cache/javart.py +174 -0
  7. {aether_cache-0.1.3 → aether_cache-0.1.4}/pyproject.toml +1 -1
  8. {aether_cache-0.1.3 → aether_cache-0.1.4}/.gitignore +0 -0
  9. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/__main__.py +0 -0
  10. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/config.py +0 -0
  11. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/connect.py +0 -0
  12. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/engine/aether-engine.jar +0 -0
  13. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/portability.py +0 -0
  14. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/repos.py +0 -0
  15. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/server.py +0 -0
  16. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/ui/fonts/OFL.txt +0 -0
  17. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/ui/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  18. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/ui/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  19. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/ui/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  20. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/ui/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  21. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/ui/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  22. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/ui/index.html +0 -0
  23. {aether_cache-0.1.3 → aether_cache-0.1.4}/aether_cache/ui.py +0 -0
  24. {aether_cache-0.1.3 → aether_cache-0.1.4}/docs/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: aether-cache
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Shared memory for AI coding tools that stays true to your code.
5
5
  Project-URL: Homepage, https://anishchugh05.github.io/Aether-Cache-Redis-Compatible-Vector-Store/
6
6
  Project-URL: Repository, https://github.com/anishchugh05/Aether-Cache-Redis-Compatible-Vector-Store
@@ -104,9 +104,8 @@ m1 retired; m3 replaces it.
104
104
 
105
105
  ## Quick start
106
106
 
107
- You need **Java 11+** (`brew install openjdk@21` on macOS, or [adoptium.net](https://adoptium.net)) and
108
- [**uv**](https://docs.astral.sh/uv/) (`curl -LsSf https://astral.sh/uv/install.sh | sh`), which also takes care of
109
- Python for you. Then:
107
+ You need [**uv**](https://docs.astral.sh/uv/) (`curl -LsSf https://astral.sh/uv/install.sh | sh`), which also takes
108
+ care of Python for you. Then:
110
109
 
111
110
  ```sh
112
111
  uv tool install aether-cache
@@ -121,6 +120,10 @@ repo and try:
121
120
 
122
121
  Then start a new session and ask how to run the tests.
123
122
 
123
+ The engine runs on Java 11+. If you have it, it's used. If you don't, `setup` downloads a private copy (about
124
+ 50 MB) into `~/.aether-cache/java`, checks it against its published checksum, and uses it only for Aether Cache;
125
+ nothing is installed system-wide, and `aether-cache uninstall` removes it.
126
+
124
127
  Already keep notes for your AI in `CLAUDE.md`, `AGENTS.md`, or Cursor rules? Run `aether-cache import` in the repo
125
128
  (or use **Import notes** in the dashboard) to turn them into memories, linked to the files they mention.
126
129
 
@@ -354,6 +357,7 @@ src/main/java/Main.java the engine (single file, from the CodeCrafters setu
354
357
  aether_cache/ the Python package behind the `aether-cache` command
355
358
  server.py MCP server: embeddings, git staleness, AGENTS.md/CLAUDE.md sync
356
359
  engine.py finds Java, starts and stops the engine in the background
360
+ javart.py downloads a private copy of Java when the computer has none
357
361
  connect.py registers Aether Cache with Claude Code, Cursor, and Codex
358
362
  cli.py the `aether-cache` command
359
363
  ui.py, ui/index.html the dashboard (`aether-cache ui`): a local server, its API, and the page
@@ -91,9 +91,8 @@ m1 retired; m3 replaces it.
91
91
 
92
92
  ## Quick start
93
93
 
94
- You need **Java 11+** (`brew install openjdk@21` on macOS, or [adoptium.net](https://adoptium.net)) and
95
- [**uv**](https://docs.astral.sh/uv/) (`curl -LsSf https://astral.sh/uv/install.sh | sh`), which also takes care of
96
- Python for you. Then:
94
+ You need [**uv**](https://docs.astral.sh/uv/) (`curl -LsSf https://astral.sh/uv/install.sh | sh`), which also takes
95
+ care of Python for you. Then:
97
96
 
98
97
  ```sh
99
98
  uv tool install aether-cache
@@ -108,6 +107,10 @@ repo and try:
108
107
 
109
108
  Then start a new session and ask how to run the tests.
110
109
 
110
+ The engine runs on Java 11+. If you have it, it's used. If you don't, `setup` downloads a private copy (about
111
+ 50 MB) into `~/.aether-cache/java`, checks it against its published checksum, and uses it only for Aether Cache;
112
+ nothing is installed system-wide, and `aether-cache uninstall` removes it.
113
+
111
114
  Already keep notes for your AI in `CLAUDE.md`, `AGENTS.md`, or Cursor rules? Run `aether-cache import` in the repo
112
115
  (or use **Import notes** in the dashboard) to turn them into memories, linked to the files they mention.
113
116
 
@@ -341,6 +344,7 @@ src/main/java/Main.java the engine (single file, from the CodeCrafters setu
341
344
  aether_cache/ the Python package behind the `aether-cache` command
342
345
  server.py MCP server: embeddings, git staleness, AGENTS.md/CLAUDE.md sync
343
346
  engine.py finds Java, starts and stops the engine in the background
347
+ javart.py downloads a private copy of Java when the computer has none
344
348
  connect.py registers Aether Cache with Claude Code, Cursor, and Codex
345
349
  cli.py the `aether-cache` command
346
350
  ui.py, ui/index.html the dashboard (`aether-cache ui`): a local server, its API, and the page
@@ -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.3"
3
+ __version__ = "0.1.4"
@@ -17,11 +17,38 @@ def _say(mark: str, text: str) -> None:
17
17
 
18
18
  # ---------- commands ----------
19
19
 
20
+ def _java_label(java: str) -> str:
21
+ return f"private copy in {java_dir()}" if engine.is_private(java) else java
22
+
23
+
24
+ def java_dir():
25
+ from .javart import JAVA_DIR
26
+ return JAVA_DIR
27
+
28
+
29
+ def _java_for_setup():
30
+ """The computer's own Java if it has a recent enough one; otherwise downloads a private copy."""
31
+ try:
32
+ return engine.find_java()
33
+ except engine.EngineError as missing:
34
+ from . import javart
35
+ reason = str(missing).splitlines()[0] # "Java isn't installed." or "Found Java 8, which is too old."
36
+ print(f"[ .... ] {reason} Downloading a private copy of Java {javart.FEATURE_VERSION} for Aether Cache "
37
+ f"(about 50 MB, only the first time).\n"
38
+ f" It goes in {javart.JAVA_DIR}, only Aether Cache uses it, and nothing else on your "
39
+ f"computer changes.", flush=True)
40
+ try:
41
+ javart.download()
42
+ except javart.JavaDownloadError as e:
43
+ raise engine.EngineError(str(e))
44
+ return engine.find_java()
45
+
46
+
20
47
  def cmd_setup(args) -> int:
21
48
  print(f"Setting up Aether Cache {__version__}\n")
22
49
  try:
23
- java, major = engine.find_java()
24
- _say(OK, f"Java {major} ({java})")
50
+ java, major = _java_for_setup()
51
+ _say(OK, f"Java {major} ({_java_label(java)})")
25
52
  _say(OK, engine.start())
26
53
  except engine.EngineError as e:
27
54
  _say(FAIL, str(e))
@@ -31,7 +58,10 @@ def cmd_setup(args) -> int:
31
58
  if config.FAKE_EMBEDDER:
32
59
  _say(OK, "Embedding model: test mode, nothing to download")
33
60
  else:
34
- print("[ .... ] Downloading the embedding model (about 90 MB, only the first time)...", flush=True)
61
+ if any(config.MODELS_DIR.glob("*")):
62
+ print("[ .... ] Loading the embedding model...", flush=True)
63
+ else:
64
+ print("[ .... ] Downloading the embedding model (about 90 MB, only the first time)...", flush=True)
35
65
  try:
36
66
  from .server import embed
37
67
  embed("warm up")
@@ -121,7 +151,7 @@ def cmd_doctor(args) -> int:
121
151
  _say(OK, f"Aether Cache {__version__}, Python {sys.version.split()[0]} ({sys.executable})")
122
152
  try:
123
153
  java, major = engine.find_java()
124
- _say(OK, f"Java {major} ({java})")
154
+ _say(OK, f"Java {major} ({_java_label(java)})")
125
155
  except engine.EngineError as e:
126
156
  _say(FAIL, str(e))
127
157
  problems += 1
@@ -329,6 +359,9 @@ def cmd_uninstall(args) -> int:
329
359
  shutil.rmtree(config.HOME, ignore_errors=True)
330
360
  _say(OK, f"Deleted {config.HOME}")
331
361
  elif has_data:
362
+ from . import javart
363
+ if javart.remove():
364
+ _say(OK, f"Removed Aether Cache's private copy of Java ({javart.JAVA_DIR})")
332
365
  _say(OK, f"Kept your memories in {config.HOME}. Reinstall any time and they're back.")
333
366
  print(f"\nLast step, remove the program itself:\n {_uninstall_command()}")
334
367
  return 1 if failed else 0
@@ -34,30 +34,44 @@ def java_install_hint() -> str:
34
34
  how = "winget install EclipseAdoptium.Temurin.21.JRE (or download it from https://adoptium.net)"
35
35
  else:
36
36
  how = "sudo apt install openjdk-21-jre-headless (or your distro's equivalent, or https://adoptium.net)"
37
- return f"Aether Cache needs Java {MIN_JAVA} or newer. Install it with:\n {how}"
37
+ return (f"Run `aether-cache setup` and it downloads a private copy of Java for you, "
38
+ f"or install Java {MIN_JAVA}+ yourself:\n {how}")
39
+
40
+
41
+ def _java_version(java: str) -> Optional[int]:
42
+ try:
43
+ out = subprocess.run([java, "-version"], capture_output=True, text=True, timeout=15)
44
+ except (OSError, subprocess.SubprocessError):
45
+ return None
46
+ match = re.search(r'version "(\d+)(?:\.(\d+))?', out.stderr + out.stdout)
47
+ if out.returncode != 0 or not match:
48
+ return None
49
+ major = int(match.group(1))
50
+ if major == 1 and match.group(2): # old-style "1.8.0" means Java 8
51
+ major = int(match.group(2))
52
+ return major
38
53
 
39
54
 
40
55
  def find_java() -> Tuple[str, int]:
41
56
  """(path to a working java, its major version). Raises EngineError if there's no usable Java.
57
+ Prefers the computer's own Java; falls back to the private copy `setup` downloads when there isn't one.
42
58
  Runs `java -version` rather than trusting the PATH: macOS ships a /usr/bin/java stub that only
43
59
  says "Unable to locate a Java Runtime"."""
60
+ from .javart import private_java
44
61
  candidates = []
45
- if os.environ.get("JAVA_HOME"):
46
- candidates.append(str(Path(os.environ["JAVA_HOME"]) / "bin" / ("java.exe" if os.name == "nt" else "java")))
47
- if shutil.which("java"):
48
- candidates.append(shutil.which("java"))
62
+ if not os.environ.get("AETHER_IGNORE_SYSTEM_JAVA"): # testing only: act as if this computer had no Java
63
+ if os.environ.get("JAVA_HOME"):
64
+ candidates.append(str(Path(os.environ["JAVA_HOME"]) / "bin" / ("java.exe" if os.name == "nt" else "java")))
65
+ if shutil.which("java"):
66
+ candidates.append(shutil.which("java"))
67
+ private = private_java()
68
+ if private:
69
+ candidates.append(str(private))
49
70
  too_old = None
50
71
  for java in candidates:
51
- try:
52
- out = subprocess.run([java, "-version"], capture_output=True, text=True, timeout=15)
53
- except (OSError, subprocess.SubprocessError):
72
+ major = _java_version(java)
73
+ if major is None:
54
74
  continue
55
- match = re.search(r'version "(\d+)(?:\.(\d+))?', out.stderr + out.stdout)
56
- if out.returncode != 0 or not match:
57
- continue
58
- major = int(match.group(1))
59
- if major == 1 and match.group(2): # old-style "1.8.0" means Java 8
60
- major = int(match.group(2))
61
75
  if major >= MIN_JAVA:
62
76
  return java, major
63
77
  too_old = major
@@ -66,6 +80,14 @@ def find_java() -> Tuple[str, int]:
66
80
  raise EngineError("Java isn't installed.\n" + java_install_hint())
67
81
 
68
82
 
83
+ def is_private(java: str) -> bool:
84
+ from .javart import JAVA_DIR
85
+ try:
86
+ return Path(java).resolve().is_relative_to(JAVA_DIR.resolve())
87
+ except (OSError, ValueError):
88
+ return False
89
+
90
+
69
91
  # ---------- talking to the engine ----------
70
92
 
71
93
  def client(timeout: Optional[float] = None) -> redis.Redis:
@@ -0,0 +1,174 @@
1
+ """
2
+ A private copy of Java, for people who don't have it.
3
+
4
+ The engine needs Java 11 or newer. If this computer has one, it's used and nothing is downloaded. If not,
5
+ `aether-cache setup` downloads Eclipse Temurin (the standard free build of Java) into ~/.aether-cache/java,
6
+ checks the download against the checksum its publisher lists, and uses it only for Aether Cache: nothing is
7
+ installed system-wide, and `aether-cache uninstall` removes it.
8
+ """
9
+ import hashlib
10
+ import os
11
+ import platform
12
+ import shutil
13
+ import sys
14
+ import tarfile
15
+ import tempfile
16
+ import time
17
+ import zipfile
18
+ from pathlib import Path
19
+ from typing import Callable, Optional
20
+
21
+ from . import config
22
+
23
+ FEATURE_VERSION = 21 # a long-term-support release
24
+ API = os.environ.get("AETHER_JAVA_API", "https://api.adoptium.net/v3")
25
+ JAVA_DIR = config.HOME / "java"
26
+
27
+
28
+ class JavaDownloadError(Exception):
29
+ pass
30
+
31
+
32
+ def private_java() -> Optional[Path]:
33
+ """The java program inside our private copy, if it has been downloaded."""
34
+ name = "java.exe" if os.name == "nt" else "java"
35
+ for pattern in (f"*/Contents/Home/bin/{name}", f"*/bin/{name}"): # macOS keeps it inside a bundle
36
+ for p in sorted(JAVA_DIR.glob(pattern)):
37
+ if p.is_file():
38
+ return p
39
+ return None
40
+
41
+
42
+ def _target() -> tuple:
43
+ """(os, architecture) in the names the download service uses."""
44
+ system = platform.system()
45
+ machine = platform.machine().lower()
46
+ os_name = {"Darwin": "mac", "Windows": "windows", "Linux": "linux"}.get(system)
47
+ if os_name == "linux" and Path("/etc/alpine-release").exists():
48
+ os_name = "alpine-linux"
49
+ arch = {"x86_64": "x64", "amd64": "x64", "arm64": "aarch64", "aarch64": "aarch64"}.get(machine)
50
+ if not os_name or not arch:
51
+ raise JavaDownloadError(f"There's no ready-made Java download for this computer ({system} {machine}).")
52
+ return os_name, arch
53
+
54
+
55
+ def _client():
56
+ import httpx # installed with the MCP library; respects HTTPS_PROXY like the model download does
57
+ return httpx.Client(follow_redirects=True, timeout=httpx.Timeout(30.0, read=60.0))
58
+
59
+
60
+ def _find_package(client) -> dict:
61
+ os_name, arch = _target()
62
+ url = f"{API}/assets/latest/{FEATURE_VERSION}/hotspot"
63
+ r = client.get(url, params={"architecture": arch, "image_type": "jre", "os": os_name, "vendor": "eclipse"})
64
+ r.raise_for_status()
65
+ try:
66
+ package = r.json()[0]["binary"]["package"]
67
+ return {"link": package["link"], "sha256": package["checksum"].lower(),
68
+ "size": int(package.get("size") or 0), "name": package["name"]}
69
+ except (ValueError, LookupError, TypeError, AttributeError):
70
+ raise JavaDownloadError(f"The Java download service sent an unexpected answer for {os_name} {arch}.")
71
+
72
+
73
+ def _download(client, pkg: dict, dest: Path, progress: Callable[[int, int], None]) -> None:
74
+ digest = hashlib.sha256()
75
+ done = 0
76
+ with client.stream("GET", pkg["link"]) as r:
77
+ r.raise_for_status()
78
+ total = pkg["size"] or int(r.headers.get("content-length") or 0)
79
+ with open(dest, "wb") as f:
80
+ for chunk in r.iter_bytes(1 << 16):
81
+ f.write(chunk)
82
+ digest.update(chunk)
83
+ done += len(chunk)
84
+ progress(done, total)
85
+ if digest.hexdigest() != pkg["sha256"]:
86
+ raise JavaDownloadError("The Java download didn't match its published checksum, so it wasn't used. "
87
+ "Try again; if it keeps happening, install Java yourself from https://adoptium.net.")
88
+
89
+
90
+ def _extract(archive: Path, into: Path) -> None:
91
+ if archive.name.endswith(".zip"):
92
+ with zipfile.ZipFile(archive) as z:
93
+ for member in z.namelist(): # refuse paths that would land outside the folder
94
+ if member.startswith(("/", "\\")) or ".." in Path(member).parts:
95
+ raise JavaDownloadError("The Java download contained an unsafe file path.")
96
+ z.extractall(into)
97
+ else:
98
+ with tarfile.open(archive) as t:
99
+ if hasattr(tarfile, "data_filter"):
100
+ t.extractall(into, filter="data") # rejects unsafe paths and links
101
+ else:
102
+ for member in t.getmembers():
103
+ if member.name.startswith("/") or ".." in Path(member.name).parts:
104
+ raise JavaDownloadError("The Java download contained an unsafe file path.")
105
+ t.extractall(into)
106
+ if os.name != "nt": # make sure the programs are runnable (zip files drop permissions)
107
+ for bin_dir in into.glob("*/**/bin"):
108
+ for f in bin_dir.iterdir():
109
+ if f.is_file():
110
+ f.chmod(f.stat().st_mode | 0o755)
111
+
112
+
113
+ _last_shown = [-1]
114
+
115
+
116
+ def _bar(done: int, total: int) -> None:
117
+ if not sys.stdout.isatty():
118
+ return
119
+ step = int(done * 100 / total) if total else done >> 20 # redraw once per percent (or per MB)
120
+ if step == _last_shown[0]:
121
+ return
122
+ _last_shown[0] = step
123
+ mb = done / 1_048_576
124
+ if total:
125
+ width = 24
126
+ filled = int(width * done / total)
127
+ line = f" [{'#' * filled}{'.' * (width - filled)}] {mb:5.1f} of {total / 1_048_576:.0f} MB"
128
+ else:
129
+ line = f" {mb:5.1f} MB"
130
+ print("\r" + line, end="", flush=True)
131
+
132
+
133
+ def download(progress: Callable[[int, int], None] = _bar, attempts: int = 3) -> Path:
134
+ """Downloads and unpacks Java into ~/.aether-cache/java. Returns the path to its java program."""
135
+ config.HOME.mkdir(parents=True, exist_ok=True)
136
+ last_error = None
137
+ for attempt in range(1, attempts + 1):
138
+ work = Path(tempfile.mkdtemp(prefix="java-download-", dir=config.HOME))
139
+ try:
140
+ with _client() as client:
141
+ pkg = _find_package(client)
142
+ archive = work / pkg["name"]
143
+ _download(client, pkg, archive, progress)
144
+ if progress is _bar and sys.stdout.isatty():
145
+ print()
146
+ unpacked = work / "unpacked"
147
+ unpacked.mkdir()
148
+ _extract(archive, unpacked)
149
+ if JAVA_DIR.exists():
150
+ shutil.rmtree(JAVA_DIR)
151
+ os.replace(unpacked, JAVA_DIR) # appears all at once, so a half-unpacked Java is never used
152
+ java = private_java()
153
+ if java is None:
154
+ raise JavaDownloadError("The Java download didn't contain a java program.")
155
+ return java
156
+ except JavaDownloadError:
157
+ raise # a bad checksum or a bad archive won't fix itself by retrying
158
+ except Exception as e: # network trouble: try again
159
+ last_error = e
160
+ if progress is _bar and sys.stdout.isatty():
161
+ print()
162
+ if attempt < attempts:
163
+ time.sleep(2 * attempt)
164
+ finally:
165
+ shutil.rmtree(work, ignore_errors=True)
166
+ raise JavaDownloadError(f"Couldn't download Java ({last_error}). Check your internet connection and run "
167
+ f"`aether-cache setup` again, or install Java yourself from https://adoptium.net.")
168
+
169
+
170
+ def remove() -> bool:
171
+ if JAVA_DIR.exists():
172
+ shutil.rmtree(JAVA_DIR, ignore_errors=True)
173
+ return True
174
+ return False
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "aether-cache"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "Shared memory for AI coding tools that stays true to your code."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes