oortmem 0.2.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.
- oortmem-0.2.0/.gitignore +17 -0
- oortmem-0.2.0/PKG-INFO +36 -0
- oortmem-0.2.0/README.md +17 -0
- oortmem-0.2.0/pyproject.toml +34 -0
- oortmem-0.2.0/src/oortmem/__init__.py +3 -0
- oortmem-0.2.0/src/oortmem/__main__.py +5 -0
- oortmem-0.2.0/src/oortmem/cli.py +108 -0
- oortmem-0.2.0/tests/test_cli.py +108 -0
oortmem-0.2.0/.gitignore
ADDED
oortmem-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: oortmem
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Open-source memory for AI agents. Runs the Oort CLI (Node 22.15 or newer) from Python.
|
|
5
|
+
Project-URL: Homepage, https://oortcloud.dev
|
|
6
|
+
Project-URL: Source, https://github.com/oortmem/oort
|
|
7
|
+
Project-URL: Documentation, https://oortcloud.dev/docs
|
|
8
|
+
Author: Oort
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: agents,ai,claude,llm,mcp,memory
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# oortmem
|
|
21
|
+
|
|
22
|
+
Open-source memory for AI agents. Old facts are closed out, not overwritten, and every recall can explain why it was picked.
|
|
23
|
+
|
|
24
|
+
This package installs the `oort` command for Python users. Oort itself is written in TypeScript and published on npm, so it needs **Node.js 22.15 or newer**. The launcher runs an `oort` you already have, or the matching npm release through `npx`. Arguments and exit codes pass straight through.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pipx install oortmem
|
|
28
|
+
oort setup
|
|
29
|
+
oort recall "where does the user live"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Without Node, `oort` tells you how to install it. You can also skip Python entirely with `npm install -g oortmem`.
|
|
33
|
+
|
|
34
|
+
- Docs: https://oortcloud.dev/docs
|
|
35
|
+
- Source: https://github.com/oortmem/oort
|
|
36
|
+
- License: Apache 2.0
|
oortmem-0.2.0/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# oortmem
|
|
2
|
+
|
|
3
|
+
Open-source memory for AI agents. Old facts are closed out, not overwritten, and every recall can explain why it was picked.
|
|
4
|
+
|
|
5
|
+
This package installs the `oort` command for Python users. Oort itself is written in TypeScript and published on npm, so it needs **Node.js 22.15 or newer**. The launcher runs an `oort` you already have, or the matching npm release through `npx`. Arguments and exit codes pass straight through.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pipx install oortmem
|
|
9
|
+
oort setup
|
|
10
|
+
oort recall "where does the user live"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Without Node, `oort` tells you how to install it. You can also skip Python entirely with `npm install -g oortmem`.
|
|
14
|
+
|
|
15
|
+
- Docs: https://oortcloud.dev/docs
|
|
16
|
+
- Source: https://github.com/oortmem/oort
|
|
17
|
+
- License: Apache 2.0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling==1.32.4"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oortmem"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Open-source memory for AI agents. Runs the Oort CLI (Node 22.15 or newer) from Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Oort" }]
|
|
13
|
+
keywords = ["ai", "agents", "memory", "mcp", "claude", "llm"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Software Development",
|
|
21
|
+
]
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://oortcloud.dev"
|
|
26
|
+
Source = "https://github.com/oortmem/oort"
|
|
27
|
+
Documentation = "https://oortcloud.dev/docs"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
oort = "oortmem.cli:main"
|
|
31
|
+
oortmem = "oortmem.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/oortmem"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Run the Oort CLI from Python.
|
|
2
|
+
|
|
3
|
+
Oort is written in TypeScript and ships on npm as `oortmem`; it needs Node 22.15 or newer.
|
|
4
|
+
This launcher runs an `oort` command that is already installed. Otherwise it runs the
|
|
5
|
+
matching npm release through npx. Arguments, stdin, stdout and the exit code pass through
|
|
6
|
+
unchanged.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Callable, Iterable, Optional, Sequence
|
|
17
|
+
|
|
18
|
+
from . import __version__
|
|
19
|
+
|
|
20
|
+
MIN_NODE = (22, 15)
|
|
21
|
+
NPM_SPEC = f"oortmem@{__version__}"
|
|
22
|
+
# Set on the child so a launcher that finds itself (or another copy of itself) on PATH
|
|
23
|
+
# skips the PATH lookup instead of starting itself forever.
|
|
24
|
+
GUARD = "OORTMEM_PY_LAUNCHER"
|
|
25
|
+
|
|
26
|
+
HELP = f"""oort needs Node.js {MIN_NODE[0]}.{MIN_NODE[1]} or newer, and none was found.
|
|
27
|
+
|
|
28
|
+
Install Node from https://nodejs.org, then run this command again. Or install the CLI
|
|
29
|
+
directly with npm:
|
|
30
|
+
|
|
31
|
+
npm install -g oortmem
|
|
32
|
+
|
|
33
|
+
Docs: https://oortcloud.dev/docs
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def node_version(node: str) -> Optional[tuple[int, int]]:
|
|
38
|
+
"""The (major, minor) of `node --version`, or None when it cannot be run or read."""
|
|
39
|
+
try:
|
|
40
|
+
out = subprocess.run(
|
|
41
|
+
[node, "--version"], capture_output=True, text=True, timeout=10, check=False
|
|
42
|
+
).stdout
|
|
43
|
+
except (OSError, subprocess.SubprocessError):
|
|
44
|
+
return None
|
|
45
|
+
m = re.match(r"v?(\d+)\.(\d+)", out.strip())
|
|
46
|
+
return (int(m.group(1)), int(m.group(2))) if m else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def path_candidates(name: str, path: Optional[str]) -> Iterable[str]:
|
|
50
|
+
"""Every executable called `name` on PATH, in PATH order."""
|
|
51
|
+
seen = set()
|
|
52
|
+
for d in (path or "").split(os.pathsep):
|
|
53
|
+
if not d:
|
|
54
|
+
continue
|
|
55
|
+
found = shutil.which(name, path=d)
|
|
56
|
+
if found and found not in seen:
|
|
57
|
+
seen.add(found)
|
|
58
|
+
yield found
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def same_file(a: str, b: Optional[str]) -> bool:
|
|
62
|
+
if not b:
|
|
63
|
+
return False
|
|
64
|
+
try:
|
|
65
|
+
return os.path.samefile(a, b)
|
|
66
|
+
except OSError:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def resolve(
|
|
71
|
+
env: Optional[dict] = None,
|
|
72
|
+
self_path: Optional[str] = None,
|
|
73
|
+
candidates: Callable[[str, Optional[str]], Iterable[str]] = path_candidates,
|
|
74
|
+
which: Callable[[str], Optional[str]] = shutil.which,
|
|
75
|
+
version: Callable[[str], Optional[tuple[int, int]]] = node_version,
|
|
76
|
+
) -> Optional[list[str]]:
|
|
77
|
+
"""The command to run: an installed `oort`, else npx with the pinned release, else None."""
|
|
78
|
+
env = os.environ if env is None else env
|
|
79
|
+
if not env.get(GUARD):
|
|
80
|
+
for oort in candidates("oort", env.get("PATH")):
|
|
81
|
+
if not same_file(oort, self_path):
|
|
82
|
+
return [oort]
|
|
83
|
+
node, npx = which("node"), which("npx")
|
|
84
|
+
if node and npx:
|
|
85
|
+
v = version(node)
|
|
86
|
+
if v is not None and v >= MIN_NODE:
|
|
87
|
+
return [npx, "--yes", NPM_SPEC]
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
92
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
93
|
+
if args == ["--launcher-version"]:
|
|
94
|
+
print(f"oortmem (Python launcher) {__version__}, runs {NPM_SPEC}")
|
|
95
|
+
return 0
|
|
96
|
+
self_path = shutil.which(sys.argv[0]) or sys.argv[0]
|
|
97
|
+
cmd = resolve(self_path=self_path)
|
|
98
|
+
if cmd is None:
|
|
99
|
+
sys.stderr.write(HELP)
|
|
100
|
+
return 1
|
|
101
|
+
try:
|
|
102
|
+
return subprocess.call(cmd + args, env={**os.environ, GUARD: "1"})
|
|
103
|
+
except KeyboardInterrupt:
|
|
104
|
+
return 130
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
sys.exit(main())
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import stat
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
10
|
+
|
|
11
|
+
from oortmem import __version__, cli # noqa: E402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def none(_name):
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Resolve(unittest.TestCase):
|
|
19
|
+
def test_prefers_an_installed_oort(self):
|
|
20
|
+
cmd = cli.resolve(
|
|
21
|
+
env={"PATH": "/x"},
|
|
22
|
+
candidates=lambda n, p: ["/usr/local/bin/oort"],
|
|
23
|
+
which=none,
|
|
24
|
+
)
|
|
25
|
+
self.assertEqual(cmd, ["/usr/local/bin/oort"])
|
|
26
|
+
|
|
27
|
+
def test_falls_back_to_npx_with_the_pinned_release(self):
|
|
28
|
+
which = {"node": "/n/node", "npx": "/n/npx"}.get
|
|
29
|
+
cmd = cli.resolve(env={}, candidates=lambda n, p: [], which=which, version=lambda _: (22, 15))
|
|
30
|
+
self.assertEqual(cmd, ["/n/npx", "--yes", f"oortmem@{__version__}"])
|
|
31
|
+
|
|
32
|
+
def test_old_node_is_not_used(self):
|
|
33
|
+
which = {"node": "/n/node", "npx": "/n/npx"}.get
|
|
34
|
+
cmd = cli.resolve(env={}, candidates=lambda n, p: [], which=which, version=lambda _: (22, 14))
|
|
35
|
+
self.assertIsNone(cmd)
|
|
36
|
+
|
|
37
|
+
def test_unreadable_node_version_is_not_used(self):
|
|
38
|
+
which = {"node": "/n/node", "npx": "/n/npx"}.get
|
|
39
|
+
cmd = cli.resolve(env={}, candidates=lambda n, p: [], which=which, version=lambda _: None)
|
|
40
|
+
self.assertIsNone(cmd)
|
|
41
|
+
|
|
42
|
+
def test_nothing_installed(self):
|
|
43
|
+
self.assertIsNone(cli.resolve(env={}, candidates=lambda n, p: [], which=none))
|
|
44
|
+
|
|
45
|
+
def test_guard_skips_path_lookup(self):
|
|
46
|
+
def boom(n, p):
|
|
47
|
+
raise AssertionError("PATH must not be searched under the guard")
|
|
48
|
+
|
|
49
|
+
cmd = cli.resolve(env={cli.GUARD: "1"}, candidates=boom, which=none)
|
|
50
|
+
self.assertIsNone(cmd)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class NeverRunsItself(unittest.TestCase):
|
|
54
|
+
"""The Python `oort` script must skip itself on PATH and use the real CLI after it."""
|
|
55
|
+
|
|
56
|
+
def setUp(self):
|
|
57
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
58
|
+
root = Path(self.tmp.name)
|
|
59
|
+
self.first, self.second = root / "a", root / "b"
|
|
60
|
+
self.first.mkdir()
|
|
61
|
+
self.second.mkdir()
|
|
62
|
+
self.me = self._exe(self.first / "oort")
|
|
63
|
+
self.real = self._exe(self.second / "oort")
|
|
64
|
+
self.path = os.pathsep.join([str(self.first), str(self.second)])
|
|
65
|
+
|
|
66
|
+
def tearDown(self):
|
|
67
|
+
self.tmp.cleanup()
|
|
68
|
+
|
|
69
|
+
def _exe(self, p):
|
|
70
|
+
p.write_text("#!/bin/sh\nexit 0\n")
|
|
71
|
+
p.chmod(p.stat().st_mode | stat.S_IXUSR)
|
|
72
|
+
return p
|
|
73
|
+
|
|
74
|
+
@unittest.skipIf(os.name == "nt", "POSIX shell scripts")
|
|
75
|
+
def test_skips_itself_and_finds_the_next(self):
|
|
76
|
+
cmd = cli.resolve(env={"PATH": self.path}, self_path=str(self.me), which=none)
|
|
77
|
+
self.assertEqual(cmd, [str(self.real)])
|
|
78
|
+
|
|
79
|
+
@unittest.skipIf(os.name == "nt", "POSIX shell scripts")
|
|
80
|
+
def test_only_itself_on_path_falls_through(self):
|
|
81
|
+
cmd = cli.resolve(env={"PATH": str(self.first)}, self_path=str(self.me), which=none)
|
|
82
|
+
self.assertIsNone(cmd)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Main(unittest.TestCase):
|
|
86
|
+
def test_launcher_version(self):
|
|
87
|
+
out = subprocess.run(
|
|
88
|
+
[sys.executable, "-m", "oortmem", "--launcher-version"],
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
env={**os.environ, "PYTHONPATH": str(Path(cli.__file__).parents[1])},
|
|
92
|
+
)
|
|
93
|
+
self.assertEqual(out.returncode, 0)
|
|
94
|
+
self.assertIn(f"oortmem@{__version__}", out.stdout)
|
|
95
|
+
|
|
96
|
+
def test_no_node_prints_help_and_exits_1(self):
|
|
97
|
+
out = subprocess.run(
|
|
98
|
+
[sys.executable, "-m", "oortmem", "setup"],
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
env={"PATH": "", "PYTHONPATH": str(Path(cli.__file__).parents[1])},
|
|
102
|
+
)
|
|
103
|
+
self.assertEqual(out.returncode, 1)
|
|
104
|
+
self.assertIn("npm install -g oortmem", out.stderr)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
unittest.main()
|