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.
@@ -0,0 +1,17 @@
1
+ node_modules/
2
+ dist/
3
+ .DS_Store
4
+ *.log
5
+ .env
6
+ .env.*
7
+ .worktrees/
8
+ .superpowers/
9
+ .output
10
+ .tanstack
11
+ .wrangler
12
+ .dev.vars
13
+ .nitro
14
+ dist-ssr
15
+ .npmrc
16
+ __pycache__/
17
+ *.egg-info/
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
@@ -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,3 @@
1
+ """Oort: open-source memory for AI agents. This package launches the Oort CLI from Python."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -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()