aether-cache 0.1.4__tar.gz → 0.1.5__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.
- aether_cache-0.1.5/LICENSE +21 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/PKG-INFO +7 -2
- {aether_cache-0.1.4 → aether_cache-0.1.5}/README.md +4 -1
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/__init__.py +1 -1
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/cli.py +7 -2
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/portability.py +70 -13
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/index.html +4 -1
- {aether_cache-0.1.4 → aether_cache-0.1.5}/pyproject.toml +5 -3
- {aether_cache-0.1.4 → aether_cache-0.1.5}/.gitignore +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/__main__.py +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/config.py +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/connect.py +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/engine/aether-engine.jar +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/engine.py +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/javart.py +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/repos.py +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/server.py +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/fonts/OFL.txt +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui.py +0 -0
- {aether_cache-0.1.4 → aether_cache-0.1.5}/docs/README.md +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anish Chugh
|
|
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.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: aether-cache
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
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
|
|
7
7
|
Author: Anish Chugh
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
8
10
|
Requires-Python: >=3.10
|
|
9
11
|
Requires-Dist: fastembed>=0.3
|
|
10
12
|
Requires-Dist: mcp>=1.2
|
|
@@ -366,4 +368,7 @@ aether_cache/ the Python package behind the `aether-cache` comman
|
|
|
366
368
|
memory_mcp/calibrate.py similarity calibration for duplicate/contradiction thresholds
|
|
367
369
|
vector_bench.py HNSW benchmark (writes results.json)
|
|
368
370
|
crash_test.py kill -9 crash-safety test
|
|
369
|
-
```
|
|
371
|
+
```
|
|
372
|
+
## License
|
|
373
|
+
|
|
374
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -353,4 +353,7 @@ aether_cache/ the Python package behind the `aether-cache` comman
|
|
|
353
353
|
memory_mcp/calibrate.py similarity calibration for duplicate/contradiction thresholds
|
|
354
354
|
vector_bench.py HNSW benchmark (writes results.json)
|
|
355
355
|
crash_test.py kill -9 crash-safety test
|
|
356
|
-
```
|
|
356
|
+
```
|
|
357
|
+
## License
|
|
358
|
+
|
|
359
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -263,8 +263,13 @@ def cmd_import(args) -> int:
|
|
|
263
263
|
if left_out:
|
|
264
264
|
print(f"\nLeft out ({len(left_out)}):")
|
|
265
265
|
for n in left_out:
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
if n["similar"]:
|
|
267
|
+
what = n["similar"]["id"] or "another note here"
|
|
268
|
+
why = f"very similar to {what}: \"{n['similar']['text']}\""
|
|
269
|
+
elif n["missing"]:
|
|
270
|
+
why = f"mentions {', '.join(n['missing'])}, which doesn't exist (probably out of date)"
|
|
271
|
+
else:
|
|
272
|
+
why = "doesn't look like a fact about the project"
|
|
268
273
|
print(f" - {n['text']}\n {why}")
|
|
269
274
|
if known:
|
|
270
275
|
print(f"\nAlready remembered: {len(known)}")
|
|
@@ -253,10 +253,60 @@ def _key(file: str, text: str) -> str:
|
|
|
253
253
|
return hashlib.sha1(f"{file}\n{text}".encode()).hexdigest()[:12]
|
|
254
254
|
|
|
255
255
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
256
|
+
# Same thresholds as the engine (Main.java): at or above DUPLICATE it treats a new memory as the same one,
|
|
257
|
+
# at or above CONFLICT as a possible contradiction. A note scoring that close to a memory you already have
|
|
258
|
+
# would only be skipped or flagged after importing, so it's flagged before instead.
|
|
259
|
+
DUPLICATE_SIMILARITY = 0.97
|
|
260
|
+
CONFLICT_SIMILARITY = 0.80
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _cosine(a: List[float], b: List[float]) -> float:
|
|
264
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
265
|
+
na = sum(x * x for x in a) ** 0.5
|
|
266
|
+
nb = sum(y * y for y in b) ** 0.5
|
|
267
|
+
return dot / (na * nb) if na and nb else 0.0
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _flag_similar(project: str, notes: List[dict]) -> Dict[str, List[str]]:
|
|
271
|
+
"""Compares each new note by meaning with the project's current memories, and with the notes before it
|
|
272
|
+
in this import. Marks near-copies as known and close matches as similar (unselected). Returns the
|
|
273
|
+
notes' embeddings by key, so importing them doesn't compute them twice."""
|
|
274
|
+
fresh = [n for n in notes if not n["known"]]
|
|
275
|
+
if not fresh:
|
|
276
|
+
return {}
|
|
277
|
+
vectors = server.embed_many([n["text"] for n in fresh])
|
|
278
|
+
pipe = server.db().pipeline(transaction=False)
|
|
279
|
+
for v in vectors:
|
|
280
|
+
pipe.execute_command("MEM.SEARCH", project, 1, "STATUS", "active", "VECTOR", *v)
|
|
281
|
+
try:
|
|
282
|
+
hits = pipe.execute()
|
|
283
|
+
except Exception:
|
|
284
|
+
hits = [server.run("MEM.SEARCH", project, 1, "STATUS", "active", "VECTOR", *v) for v in vectors]
|
|
285
|
+
floats = [[float(x) for x in v] for v in vectors]
|
|
286
|
+
for i, (note, hit) in enumerate(zip(fresh, hits)):
|
|
287
|
+
if hit:
|
|
288
|
+
mid, score, text = hit[0][0], float(hit[0][1]), hit[0][2]
|
|
289
|
+
if score >= DUPLICATE_SIMILARITY:
|
|
290
|
+
note["known"] = True
|
|
291
|
+
note["selected"] = False
|
|
292
|
+
continue
|
|
293
|
+
if score >= CONFLICT_SIMILARITY:
|
|
294
|
+
note["similar"] = {"id": mid, "text": text, "score": round(score, 2)}
|
|
295
|
+
note["selected"] = False
|
|
296
|
+
continue
|
|
297
|
+
for j in range(i): # two notes in this import that say the same thing, e.g. in CLAUDE.md and a rule
|
|
298
|
+
other = fresh[j]
|
|
299
|
+
if other["known"] or other.get("similar"):
|
|
300
|
+
continue
|
|
301
|
+
score = _cosine(floats[i], floats[j])
|
|
302
|
+
if score >= CONFLICT_SIMILARITY:
|
|
303
|
+
note["similar"] = {"id": None, "text": other["text"], "score": round(score, 2)}
|
|
304
|
+
note["selected"] = False
|
|
305
|
+
break
|
|
306
|
+
return {n["key"]: v for n, v in zip(fresh, vectors)}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _scan(project: str, root: Path) -> tuple:
|
|
260
310
|
if not root.is_dir():
|
|
261
311
|
raise NotesError(f"{root} isn't a folder.")
|
|
262
312
|
existing = set()
|
|
@@ -280,27 +330,34 @@ def scan(project: str, root: Path) -> dict:
|
|
|
280
330
|
linked, missing = mentioned_paths(note, root, skip=rels)
|
|
281
331
|
known = n in existing or body in existing
|
|
282
332
|
notes.append({"key": _key(rel, note), "text": note, "file": rel, "files": linked,
|
|
283
|
-
"missing": missing, "known": known, "weak": found["weak"],
|
|
333
|
+
"missing": missing, "known": known, "weak": found["weak"], "similar": None,
|
|
284
334
|
"selected": not known and not missing and not found["weak"]})
|
|
285
335
|
count += 1
|
|
286
336
|
per_file.append({"path": rel, "notes": count})
|
|
287
|
-
|
|
337
|
+
vectors = _flag_similar(project, notes)
|
|
338
|
+
return {"project": project, "root": str(root), "files": per_file, "notes": notes}, vectors
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def scan(project: str, root: Path) -> dict:
|
|
342
|
+
"""Everything import would bring in from this repo's notes files, tagged for review: known (already
|
|
343
|
+
remembered, word for word or nearly), similar (close in meaning to a memory you have, or to another note
|
|
344
|
+
here), missing (mentions a file that doesn't exist, so probably already stale), weak (doesn't read like
|
|
345
|
+
a fact about the project). Only notes with none of these are selected by default."""
|
|
346
|
+
return _scan(project, root)[0]
|
|
288
347
|
|
|
289
348
|
|
|
290
349
|
def apply(project: str, root: Path, keys: Optional[List[str]] = None) -> dict:
|
|
291
350
|
"""Saves the chosen notes as memories (by key from scan; None means the ones selected by default).
|
|
292
351
|
Notes that turn out to be already known are skipped, never duplicated."""
|
|
293
|
-
found =
|
|
294
|
-
|
|
352
|
+
found, vectors = _scan(project, root)
|
|
353
|
+
wanted = set(keys or ())
|
|
354
|
+
chosen = [n for n in found["notes"] if not n["known"] and (n["selected"] if keys is None else n["key"] in wanted)]
|
|
295
355
|
result = {"saved": [], "duplicates": 0, "conflicts": 0}
|
|
296
|
-
|
|
297
|
-
return result
|
|
298
|
-
vectors = server.embed_many([n["text"] for n in chosen])
|
|
299
|
-
for note, vector in zip(chosen, vectors):
|
|
356
|
+
for note in chosen:
|
|
300
357
|
args = ["MEM.ADD", project, note["text"], "SOURCE", f"{note['file']} (imported)", "SESSION", "import"]
|
|
301
358
|
if note["files"]:
|
|
302
359
|
args += ["FILES", server.link_files(note["files"], root)]
|
|
303
|
-
mem_id, outcome, _related = server.run(*args, "VECTOR", *
|
|
360
|
+
mem_id, outcome, _related = server.run(*args, "VECTOR", *vectors[note["key"]])
|
|
304
361
|
if outcome == "duplicate":
|
|
305
362
|
result["duplicates"] += 1
|
|
306
363
|
continue
|
|
@@ -903,7 +903,10 @@ function renderImport() {
|
|
|
903
903
|
n.files.length ? h("span", { class: "linked" }, "Linked to ", n.files.map((p, i) => [i ? ", " : "", h("span", { class: "mono" }, pathNodes(p))])) : null,
|
|
904
904
|
n.missing.length ? h("span", { class: "warn" }, "Mentions ", n.missing.map((p, i) => [i ? ", " : "", h("span", { class: "mono" }, pathNodes(p))]),
|
|
905
905
|
n.missing.length === 1 ? ", which doesn’t exist. Probably out of date." : ", which don’t exist. Probably out of date.") : null,
|
|
906
|
-
n.
|
|
906
|
+
n.similar ? h("span", { class: "warn" }, n.similar.id
|
|
907
|
+
? ["Very similar to ", h("span", { class: "mono" }, n.similar.id), ", which you already have: “", richText(n.similar.text), "”"]
|
|
908
|
+
: ["Very similar to another note here: “", richText(n.similar.text), "”"]) : null,
|
|
909
|
+
n.weak && !n.missing.length && !n.similar ? h("span", {}, "Doesn’t look like a fact about the project, so it’s unchecked.") : null,
|
|
907
910
|
].filter(Boolean);
|
|
908
911
|
return h("label", { class: "pick" }, box,
|
|
909
912
|
h("span", {}, h("span", { class: "text", id: `t-${n.key}` }, richText(n.text)), tags.length ? h("span", { class: "tags" }, tags) : null));
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling>=1.
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "aether-cache"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
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"
|
|
11
11
|
authors = [{ name = "Anish Chugh" }]
|
|
12
|
+
license = "MIT"
|
|
13
|
+
license-files = ["LICENSE"]
|
|
12
14
|
dependencies = [
|
|
13
15
|
"mcp>=1.2",
|
|
14
16
|
"fastembed>=0.3",
|
|
@@ -27,7 +29,7 @@ packages = ["aether_cache"]
|
|
|
27
29
|
artifacts = ["aether_cache/engine/aether-engine.jar"] # *.jar is gitignored; ship this one anyway
|
|
28
30
|
|
|
29
31
|
[tool.hatch.build.targets.sdist]
|
|
30
|
-
include = ["aether_cache", "README.md", "pyproject.toml"]
|
|
32
|
+
include = ["aether_cache", "README.md", "LICENSE", "pyproject.toml"]
|
|
31
33
|
artifacts = ["aether_cache/engine/aether-engine.jar"]
|
|
32
34
|
|
|
33
35
|
[tool.uv]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/fonts/ibm-plex-mono-latin-400-normal.woff2
RENAMED
|
File without changes
|
{aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/fonts/ibm-plex-mono-latin-500-normal.woff2
RENAMED
|
File without changes
|
{aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/fonts/ibm-plex-sans-latin-400-normal.woff2
RENAMED
|
File without changes
|
{aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/fonts/ibm-plex-sans-latin-500-normal.woff2
RENAMED
|
File without changes
|
{aether_cache-0.1.4 → aether_cache-0.1.5}/aether_cache/ui/fonts/ibm-plex-sans-latin-600-normal.woff2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|