openmymemo 0.2.4__py3-none-any.whl

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.
openmymemo/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """OpenMyMemo: your AI memory as a local folder you own."""
2
+
3
+ __version__ = "0.2.4"
openmymemo/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
openmymemo/appfiles.py ADDED
@@ -0,0 +1,87 @@
1
+ """Writing other apps' files: their MCP configs (clients.py) and Codex's hooks.json (codex.py).
2
+
3
+ Like ``vault._atomic_write`` a file is replaced through a temporary file and a rename, so the app never reads
4
+ half a file. Because these files are the person's and not ours, a replacement also keeps the file's
5
+ permissions from its first byte (some hold API keys), writes through a symlink to the real file (dotfile
6
+ repositories), and the file as it was before OpenMyMemo first changed it is kept next to it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import contextlib
12
+ import os
13
+ import secrets
14
+ import shutil
15
+ import stat
16
+ from pathlib import Path
17
+
18
+ BACKUP_SUFFIX = ".openmymemo-backup"
19
+
20
+
21
+ def replace(path: Path, text: str) -> None:
22
+ """Atomically replace a file, keeping its permissions; a symlink keeps pointing at the real file."""
23
+ target = path.resolve() if path.is_symlink() else path
24
+ target.parent.mkdir(parents=True, exist_ok=True)
25
+ try:
26
+ mode = target.stat().st_mode & 0o777
27
+ except FileNotFoundError:
28
+ mode = None
29
+ data = text.encode("utf-8")
30
+ tmp = target.with_name(f".{target.name}.{secrets.token_hex(4)}.tmp")
31
+ # Born with the file's own permissions (private for a new file): a config holding API keys is never
32
+ # readable by others, not even for a moment.
33
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600 if mode is None else mode)
34
+ try:
35
+ with os.fdopen(fd, "wb") as f:
36
+ if mode is not None and hasattr(os, "fchmod"):
37
+ os.fchmod(f.fileno(), mode) # exactly, whatever the umask
38
+ f.write(data)
39
+ f.flush()
40
+ os.fsync(f.fileno())
41
+ os.replace(tmp, target)
42
+ except BaseException:
43
+ tmp.unlink(missing_ok=True)
44
+ raise
45
+
46
+
47
+ def _make_private(path: Path) -> None:
48
+ """chmod 0600 a regular file that others may read. Opened with O_NOFOLLOW, so a symlink (which isn't ours)
49
+ is never followed, not even one swapped in meanwhile. Windows has no such modes: nothing to do there."""
50
+ if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "fchmod"):
51
+ return
52
+ with contextlib.suppress(OSError): # not ours, gone, or a symlink: left as it is
53
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | getattr(os, "O_NONBLOCK", 0))
54
+ try:
55
+ st = os.fstat(fd)
56
+ if stat.S_ISREG(st.st_mode) and stat.S_IMODE(st.st_mode) & 0o077:
57
+ os.fchmod(fd, 0o600)
58
+ finally:
59
+ os.close(fd)
60
+
61
+
62
+ def backup(path: Path) -> None:
63
+ """Keep the file as it was before OpenMyMemo first changed it, once: ``<file>.openmymemo-backup``.
64
+
65
+ Written private to a temporary file and renamed into place only when complete, so the copy of a file
66
+ holding API keys is never readable by others, and a copy cut short is never taken for the backup. A copy
67
+ left by an older version, which took the original's permissions, is made private here too."""
68
+ copy = path.with_name(path.name + BACKUP_SUFFIX)
69
+ if copy.exists() or copy.is_symlink():
70
+ _make_private(copy)
71
+ return
72
+ if not path.exists():
73
+ return
74
+ source = path.resolve()
75
+ tmp = copy.with_name(f".{copy.name}.{secrets.token_hex(4)}.tmp")
76
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
77
+ try:
78
+ with os.fdopen(fd, "wb") as f, open(source, "rb") as src:
79
+ shutil.copyfileobj(src, f)
80
+ f.flush()
81
+ os.fsync(f.fileno())
82
+ shutil.copystat(source, tmp) # the original's times, now that it is all there
83
+ os.chmod(tmp, 0o600) # but readable by this user only, whatever the original allows (PRIVACY.md)
84
+ os.replace(tmp, copy)
85
+ except BaseException:
86
+ tmp.unlink(missing_ok=True)
87
+ raise