fkf 5.0.0__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.
Files changed (128) hide show
  1. fkf/__init__.py +22 -0
  2. fkf/__main__.py +6 -0
  3. fkf/assets.py +117 -0
  4. fkf/assets_data/demo/fkf-demo-json.sh +20 -0
  5. fkf/assets_data/presets/bin/agent-memory-body.py +117 -0
  6. fkf/assets_data/presets/bin/agent-memory-files.py +225 -0
  7. fkf/assets_data/presets/bin/agent-prompt-body.py +205 -0
  8. fkf/assets_data/presets/bin/agent-prompts.py +295 -0
  9. fkf/assets_data/presets/bin/agent-session-trace.py +507 -0
  10. fkf/assets_data/presets/bin/agent-sessions.py +543 -0
  11. fkf/assets_data/presets/bin/atuin-history-json.py +186 -0
  12. fkf/assets_data/presets/bin/chrome-bookmarks.py +166 -0
  13. fkf/assets_data/presets/bin/chromium-pages.py +336 -0
  14. fkf/assets_data/presets/bin/fkf-hook.py +248 -0
  15. fkf/assets_data/presets/bin/gcloud-audit-json.py +134 -0
  16. fkf/assets_data/presets/bin/gcloud-auth-ready.sh +10 -0
  17. fkf/assets_data/presets/bin/gcloud-projects-json.sh +22 -0
  18. fkf/assets_data/presets/bin/gh-runs.py +243 -0
  19. fkf/assets_data/presets/bin/git-log-json.py +303 -0
  20. fkf/assets_data/presets/bin/github-commits-json.py +201 -0
  21. fkf/assets_data/presets/bin/github-events-json.py +190 -0
  22. fkf/assets_data/presets/bin/github-generic-list-json.py +82 -0
  23. fkf/assets_data/presets/bin/github-gists-json.sh +23 -0
  24. fkf/assets_data/presets/bin/github-list-json.py +208 -0
  25. fkf/assets_data/presets/bin/github-reviews-json.py +240 -0
  26. fkf/assets_data/presets/bin/github-search-json.py +307 -0
  27. fkf/assets_data/presets/bin/github-stars-json.sh +25 -0
  28. fkf/assets_data/presets/bin/gmail-body.py +148 -0
  29. fkf/assets_data/presets/bin/gmail-json.py +178 -0
  30. fkf/assets_data/presets/bin/gws-calendar-body.py +69 -0
  31. fkf/assets_data/presets/bin/gws-calendars-json.py +265 -0
  32. fkf/assets_data/presets/bin/gws-chat-message-body.py +58 -0
  33. fkf/assets_data/presets/bin/gws-chat-messages.py +182 -0
  34. fkf/assets_data/presets/bin/gws-doc-text.sh +20 -0
  35. fkf/assets_data/presets/bin/gws-meeting-notes-json.py +282 -0
  36. fkf/assets_data/presets/bin/gws-page-json.py +226 -0
  37. fkf/assets_data/presets/bin/gws-tasks.py +181 -0
  38. fkf/assets_data/presets/bin/huggingface-repositories-json.py +84 -0
  39. fkf/assets_data/presets/bin/jira-issues-json.py +144 -0
  40. fkf/assets_data/presets/bin/kaggle-competitions-json.sh +16 -0
  41. fkf/assets_data/presets/bin/kaggle-datasets-json.sh +17 -0
  42. fkf/assets_data/presets/bin/kaggle-json.py +126 -0
  43. fkf/assets_data/presets/bin/kaggle-kernels-json.py +141 -0
  44. fkf/assets_data/presets/bin/kaggle-models-json.py +120 -0
  45. fkf/assets_data/presets/bin/mise-tools-json.sh +16 -0
  46. fkf/assets_data/presets/bin/repository-facts.py +244 -0
  47. fkf/assets_data/presets/bin/rss-json.py +619 -0
  48. fkf/assets_data/presets/bin/writing-index.py +285 -0
  49. fkf/assets_data/presets/bin/writing-source-json.sh +11 -0
  50. fkf/assets_data/presets/evals/queries.yaml +20 -0
  51. fkf/assets_data/presets/minimal.yaml +17 -0
  52. fkf/assets_data/presets/personal.yaml +923 -0
  53. fkf/assets_data/presets/team.yaml +121 -0
  54. fkf/assets_data/skills/daily-brief/SKILL.md +41 -0
  55. fkf/assets_data/skills/fkf-learn/SKILL.md +97 -0
  56. fkf/assets_data/skills/fkf-learn/references/skill-evolution.md +15 -0
  57. fkf/assets_data/skills/fkf-use/SKILL.md +82 -0
  58. fkf/assets_data/skills/fkf-use/references/retrieval-feedback.md +22 -0
  59. fkf/assets_data/skills/fkf-use/references/source-and-graph.md +57 -0
  60. fkf/auth.py +62 -0
  61. fkf/base.py +204 -0
  62. fkf/bodies.py +794 -0
  63. fkf/build.py +273 -0
  64. fkf/cli.py +96 -0
  65. fkf/cli_ask.py +417 -0
  66. fkf/cli_browse.py +372 -0
  67. fkf/cli_integrate.py +225 -0
  68. fkf/cli_learn.py +189 -0
  69. fkf/cli_mcp.py +97 -0
  70. fkf/cli_operate.py +420 -0
  71. fkf/cli_setup.py +148 -0
  72. fkf/cli_support.py +258 -0
  73. fkf/cli_temporal.py +183 -0
  74. fkf/collection.py +84 -0
  75. fkf/config.py +1114 -0
  76. fkf/config_view.py +171 -0
  77. fkf/context.py +2620 -0
  78. fkf/day.py +1357 -0
  79. fkf/documents.py +910 -0
  80. fkf/errors.py +91 -0
  81. fkf/eval.py +381 -0
  82. fkf/fields.py +775 -0
  83. fkf/find.py +1000 -0
  84. fkf/graph.py +2393 -0
  85. fkf/harness.py +1091 -0
  86. fkf/helpers.py +173 -0
  87. fkf/init.py +1437 -0
  88. fkf/io.py +109 -0
  89. fkf/jsoncodec.py +247 -0
  90. fkf/learn.py +1113 -0
  91. fkf/learned.py +166 -0
  92. fkf/lexical.py +3452 -0
  93. fkf/listings.py +259 -0
  94. fkf/locking.py +335 -0
  95. fkf/logging_config.py +64 -0
  96. fkf/markdown.py +731 -0
  97. fkf/marked_block.py +132 -0
  98. fkf/mcp_paging.py +294 -0
  99. fkf/mcp_server.py +1334 -0
  100. fkf/new.py +253 -0
  101. fkf/output.py +127 -0
  102. fkf/pages.py +349 -0
  103. fkf/process.py +620 -0
  104. fkf/py.typed +0 -0
  105. fkf/query.py +234 -0
  106. fkf/read.py +484 -0
  107. fkf/scan.py +21 -0
  108. fkf/schedule.py +515 -0
  109. fkf/schema.py +494 -0
  110. fkf/selector.py +171 -0
  111. fkf/source_runtime.py +411 -0
  112. fkf/source_tests.py +142 -0
  113. fkf/status.py +1335 -0
  114. fkf/store.py +496 -0
  115. fkf/sync.py +1063 -0
  116. fkf/timeutil.py +305 -0
  117. fkf/trust.py +574 -0
  118. fkf/trust_service.py +237 -0
  119. fkf/uri.py +355 -0
  120. fkf/validation.py +487 -0
  121. fkf/verify.py +65 -0
  122. fkf/wiki_index.py +167 -0
  123. fkf-5.0.0.dist-info/METADATA +210 -0
  124. fkf-5.0.0.dist-info/RECORD +128 -0
  125. fkf-5.0.0.dist-info/WHEEL +4 -0
  126. fkf-5.0.0.dist-info/entry_points.txt +3 -0
  127. fkf-5.0.0.dist-info/licenses/LICENSE +21 -0
  128. fkf-5.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +2187 -0
fkf/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """Local, offline evidence retrieval for coding agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import version
6
+ from typing import NoReturn
7
+
8
+ __version__ = version("fkf")
9
+ DISPLAY_VERSION = f"v{__version__}"
10
+
11
+
12
+ def main() -> NoReturn:
13
+ """Run the ``fkf`` console command."""
14
+ from fkf.cli import app
15
+ from fkf.cli_support import exit_main
16
+ from fkf.logging_config import configure_logging
17
+
18
+ configure_logging()
19
+ exit_main(app)
20
+
21
+
22
+ __all__ = ["DISPLAY_VERSION", "__version__", "main"]
fkf/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from fkf import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
fkf/assets.py ADDED
@@ -0,0 +1,117 @@
1
+ """Wheel-safe access to FKF's exact bundled presets, helpers, skills, and evals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import struct
7
+ from collections.abc import Mapping
8
+ from importlib import resources
9
+ from importlib.resources.abc import Traversable
10
+ from pathlib import PurePosixPath
11
+ from typing import Final
12
+
13
+ PRESETS: Final = ("minimal", "personal", "team")
14
+ BUNDLED_SKILLS: Final = ("fkf-use", "fkf-learn", "daily-brief")
15
+ HOOK_SCRIPT: Final = "fkf-hook.py"
16
+ DEMO_HELPER: Final = "fkf-demo-json.sh"
17
+ _PYTHON_BYTECODE_SUFFIXES: Final = frozenset({".pyc", ".pyo"})
18
+
19
+
20
+ def asset_root() -> Traversable:
21
+ """Return the package resource tree without assuming a filesystem install."""
22
+ return resources.files("fkf").joinpath("assets_data")
23
+
24
+
25
+ def _parts(relative: str) -> tuple[str, ...]:
26
+ path = PurePosixPath(relative)
27
+ if not relative or path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts):
28
+ raise ValueError(f"invalid bundled asset path {relative!r}")
29
+ return path.parts
30
+
31
+
32
+ def asset(relative: str) -> Traversable:
33
+ """Resolve one confined package resource."""
34
+ return asset_root().joinpath(*_parts(relative))
35
+
36
+
37
+ def read_asset(relative: str) -> bytes:
38
+ """Read one exact bundled file from a source tree, wheel, or zip import."""
39
+ target = asset(relative)
40
+ if not target.is_file():
41
+ raise FileNotFoundError(f"bundled asset {relative!r} is missing or not a file")
42
+ return target.read_bytes()
43
+
44
+
45
+ def _walk_files(root: Traversable, prefix: str = "") -> list[tuple[str, bytes]]:
46
+ files: list[tuple[str, bytes]] = []
47
+ for entry in sorted(root.iterdir(), key=lambda item: item.name):
48
+ relative = f"{prefix}/{entry.name}" if prefix else entry.name
49
+ if entry.is_dir():
50
+ # Editable installs expose ignored interpreter caches that wheels and sdists omit.
51
+ if entry.name == "__pycache__":
52
+ continue
53
+ files.extend(_walk_files(entry, relative))
54
+ elif entry.is_file():
55
+ if PurePosixPath(entry.name).suffix in _PYTHON_BYTECODE_SUFFIXES:
56
+ continue
57
+ files.append((relative, entry.read_bytes()))
58
+ else:
59
+ raise ValueError(f"bundled asset {relative!r} is neither a regular file nor a directory")
60
+ return files
61
+
62
+
63
+ def asset_files(relative: str) -> tuple[tuple[str, bytes], ...]:
64
+ """Return all files below one bundled directory in stable relative-path order."""
65
+ root = asset(relative)
66
+ if not root.is_dir():
67
+ raise FileNotFoundError(f"bundled asset directory {relative!r} is missing")
68
+ return tuple(_walk_files(root))
69
+
70
+
71
+ def digest_tree(files: Mapping[str, bytes] | Traversable) -> str:
72
+ """Hash a tree with FKF's injective path/content framing."""
73
+ entries = _walk_files(files) if isinstance(files, Traversable) else sorted(files.items())
74
+ digest = hashlib.sha256()
75
+ for relative, content in entries:
76
+ normalized = PurePosixPath(relative).as_posix()
77
+ encoded = normalized.encode()
78
+ digest.update(b"P")
79
+ digest.update(struct.pack(">Q", len(encoded)))
80
+ digest.update(encoded)
81
+ digest.update(b"C")
82
+ digest.update(struct.pack(">Q", len(content)))
83
+ digest.update(content)
84
+ return digest.hexdigest()
85
+
86
+
87
+ def skill_digest(name: str) -> str:
88
+ """Return the exact digest of one closed-vocabulary bundled skill."""
89
+ if name not in BUNDLED_SKILLS:
90
+ raise ValueError(f"unknown bundled skill {name!r}")
91
+ target = asset(f"skills/{name}")
92
+ if not target.is_dir():
93
+ raise FileNotFoundError(f"bundled skill {name!r} is missing")
94
+ return digest_tree(target)
95
+
96
+
97
+ def shipped_helpers() -> dict[str, bytes]:
98
+ """Load every official helper by basename in deterministic order."""
99
+ helpers = dict(asset_files("presets/bin"))
100
+ if HOOK_SCRIPT not in helpers:
101
+ raise FileNotFoundError(f"bundled helper {HOOK_SCRIPT!r} is missing")
102
+ return helpers
103
+
104
+
105
+ __all__ = [
106
+ "BUNDLED_SKILLS",
107
+ "DEMO_HELPER",
108
+ "HOOK_SCRIPT",
109
+ "PRESETS",
110
+ "asset",
111
+ "asset_files",
112
+ "asset_root",
113
+ "digest_tree",
114
+ "read_asset",
115
+ "shipped_helpers",
116
+ "skill_digest",
117
+ ]
@@ -0,0 +1,20 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ [ "$#" -eq 2 ] || { echo "usage: fkf-demo-json.sh <source> <date>" >&2; exit 2; }
5
+ source=$1
6
+ date=$2
7
+ case "$source" in
8
+ github-pull-requests|google-calendar-events|google-gmail-emails|jira-issues|git-commits|shell-commands) ;;
9
+ *) echo "unknown synthetic source: $source" >&2; exit 2 ;;
10
+ esac
11
+ case "$date" in
12
+ ????-??-??) ;;
13
+ *) echo "date must be YYYY-MM-DD" >&2; exit 2 ;;
14
+ esac
15
+ case "$date" in
16
+ *[!0-9-]*) echo "date must be YYYY-MM-DD" >&2; exit 2 ;;
17
+ esac
18
+
19
+ printf '[{"id":"%s-%s-0","time":"%s","title":"Synthetic %s activity","url":"https://example.test/demo/%s/%s","repo":"repo:github.com/fmind/fkf","author":"person:email/demo@example.test","attendees":["person:email/demo@example.test"],"from":"person:email/demo@example.test","to":["person:email/reader@example.test"],"assignee":"person:email/demo@example.test","ticket":"ticket:DEMO-1"}]\n' \
20
+ "$source" "$date" "$date" "$source" "$source" "$date"
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env python3
2
+ """Print one bounded file from the reviewed coding-agent memory roots."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import stat
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ LIMIT = 4 * 1024 * 1024
12
+ READ_BYTES = 64 << 10
13
+ type PathPart = str | bytes | os.PathLike[str] | os.PathLike[bytes]
14
+
15
+
16
+ def reviewed_root(path: Path, home: Path) -> Path | None:
17
+ candidates = (
18
+ (home / ".claude" / "projects", lambda parts: len(parts) == 3 and parts[1] == "memory"),
19
+ (home / ".codex" / "memories", lambda parts: 1 <= len(parts) <= 2),
20
+ (home / ".gemini" / "tmp", lambda parts: len(parts) == 3 and parts[1] == "memory"),
21
+ (home / ".grok" / "memory", lambda parts: 1 <= len(parts) <= 2),
22
+ )
23
+ for root, shape in candidates:
24
+ try:
25
+ relative = path.relative_to(root)
26
+ except ValueError:
27
+ continue
28
+ if path.suffix == ".md" and ".." not in relative.parts and shape(relative.parts):
29
+ return root
30
+ return None
31
+
32
+
33
+ def fingerprint(value: os.stat_result) -> tuple[int, int, int, int, int, int]:
34
+ return value.st_dev, value.st_ino, value.st_mode, value.st_size, value.st_mtime_ns, value.st_ctime_ns
35
+
36
+
37
+ def open_directory(path: PathPart, parent: int | None = None) -> int:
38
+ inspected = os.stat(path, dir_fd=parent, follow_symlinks=False)
39
+ if not stat.S_ISDIR(inspected.st_mode):
40
+ raise RuntimeError("memory root is missing or linked")
41
+ flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
42
+ descriptor = os.open(path, flags, dir_fd=parent)
43
+ if fingerprint(inspected) != fingerprint(os.fstat(descriptor)):
44
+ os.close(descriptor)
45
+ raise RuntimeError("memory root changed while it was being opened")
46
+ return descriptor
47
+
48
+
49
+ def open_chain(home: Path, components: tuple[str, ...]) -> int:
50
+ descriptor = open_directory(home)
51
+ try:
52
+ for component in components:
53
+ child = open_directory(component, descriptor)
54
+ os.close(descriptor)
55
+ descriptor = child
56
+ except BaseException:
57
+ os.close(descriptor)
58
+ raise
59
+ return descriptor
60
+
61
+
62
+ def read_body(home: Path, path: Path) -> bytes:
63
+ relative = path.relative_to(home)
64
+ parent = open_chain(home, relative.parts[:-1])
65
+ descriptor = -1
66
+ try:
67
+ name = relative.parts[-1]
68
+ inspected = os.stat(name, dir_fd=parent, follow_symlinks=False)
69
+ if not stat.S_ISREG(inspected.st_mode):
70
+ raise RuntimeError("file is absent, linked, or outside the reviewed roots")
71
+ flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
72
+ descriptor = os.open(name, flags, dir_fd=parent)
73
+ opened = os.fstat(descriptor)
74
+ if fingerprint(inspected) != fingerprint(opened):
75
+ raise RuntimeError("memory file changed while it was being opened")
76
+ output = bytearray()
77
+ while chunk := os.read(descriptor, min(READ_BYTES, LIMIT + 1 - len(output))):
78
+ output.extend(chunk)
79
+ if len(output) > LIMIT:
80
+ raise RuntimeError(f"file exceeds the {LIMIT}-byte body limit")
81
+ after = os.fstat(descriptor)
82
+ current = os.stat(name, dir_fd=parent, follow_symlinks=False)
83
+ if fingerprint(opened) != fingerprint(after) or fingerprint(opened) != fingerprint(current):
84
+ raise RuntimeError("memory file changed while it was being read")
85
+ return bytes(output)
86
+ finally:
87
+ if descriptor >= 0:
88
+ os.close(descriptor)
89
+ os.close(parent)
90
+
91
+
92
+ def main(arguments: list[str]) -> int:
93
+ if arguments[:1] in (["--version"], ["-v"]):
94
+ sys.stdout.write("agent-memory-body.py (fkf base helper)\n")
95
+ return 0
96
+ if len(arguments) != 1:
97
+ sys.stderr.write("usage: agent-memory-body.py <absolute-memory-file>\n")
98
+ return 2
99
+ path = Path(arguments[0])
100
+ root = reviewed_root(path, Path(os.environ.get("HOME", "")))
101
+ if root is None:
102
+ sys.stderr.write("agent-memory-body.py: path is outside the reviewed harness memory roots\n")
103
+ return 2
104
+ try:
105
+ content = read_body(Path(os.environ.get("HOME", "")), path)
106
+ except RuntimeError as error:
107
+ sys.stderr.write(f"agent-memory-body.py: {error}\n")
108
+ return 1
109
+ except OSError:
110
+ sys.stderr.write("agent-memory-body.py: file is absent, linked, or outside the reviewed roots\n")
111
+ return 1
112
+ sys.stdout.buffer.write(content)
113
+ return 0
114
+
115
+
116
+ if __name__ == "__main__":
117
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ """Project bounded metadata for coding-agent memory files touched in a UTC window."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import re
9
+ import stat
10
+ import sys
11
+ from dataclasses import dataclass
12
+ from datetime import UTC, datetime
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ PREVIEW_BYTES = 65_536
17
+ PREVIEW_LINES = 256
18
+ READ_BYTES = 64 << 10
19
+ MAX_FILES = 8192
20
+ MAX_RECORDS = 8192
21
+ MAX_OUTPUT_BYTES = 64 << 20
22
+ type PathPart = str | bytes | os.PathLike[str] | os.PathLike[bytes]
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class MemoryFile:
27
+ path: Path
28
+ inspected: os.stat_result
29
+
30
+
31
+ def instant(value: str) -> datetime:
32
+ parsed = datetime.fromisoformat(value)
33
+ if parsed.tzinfo is None:
34
+ raise ValueError("timestamp must include an offset")
35
+ return parsed.astimezone(UTC)
36
+
37
+
38
+ def fingerprint(value: os.stat_result) -> tuple[int, int, int, int, int, int]:
39
+ return value.st_dev, value.st_ino, value.st_mode, value.st_size, value.st_mtime_ns, value.st_ctime_ns
40
+
41
+
42
+ def open_directory(path: PathPart, parent: int | None = None, expected: os.stat_result | None = None) -> int:
43
+ inspected = os.stat(path, dir_fd=parent, follow_symlinks=False)
44
+ if not stat.S_ISDIR(inspected.st_mode):
45
+ raise RuntimeError("memory root is missing or linked")
46
+ if expected is not None and fingerprint(expected) != fingerprint(inspected):
47
+ raise RuntimeError("memory root changed while it was being opened")
48
+ flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
49
+ descriptor = os.open(path, flags, dir_fd=parent)
50
+ if fingerprint(inspected) != fingerprint(os.fstat(descriptor)):
51
+ os.close(descriptor)
52
+ raise RuntimeError("memory root changed while it was being opened")
53
+ return descriptor
54
+
55
+
56
+ def open_chain(home: Path, components: tuple[str, ...]) -> int:
57
+ descriptor = open_directory(home)
58
+ try:
59
+ for component in components:
60
+ child = open_directory(component, descriptor)
61
+ os.close(descriptor)
62
+ descriptor = child
63
+ except BaseException:
64
+ os.close(descriptor)
65
+ raise
66
+ return descriptor
67
+
68
+
69
+ def read_prefix(home: Path, candidate: MemoryFile) -> bytes:
70
+ relative = candidate.path.relative_to(home)
71
+ parent = open_chain(home, relative.parts[:-1])
72
+ descriptor = -1
73
+ try:
74
+ name = relative.parts[-1]
75
+ inspected = os.stat(name, dir_fd=parent, follow_symlinks=False)
76
+ if not stat.S_ISREG(inspected.st_mode):
77
+ raise RuntimeError("memory file is missing or linked")
78
+ if fingerprint(candidate.inspected) != fingerprint(inspected):
79
+ raise RuntimeError("memory file changed while it was being opened")
80
+ flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
81
+ descriptor = os.open(name, flags, dir_fd=parent)
82
+ opened = os.fstat(descriptor)
83
+ if fingerprint(inspected) != fingerprint(opened):
84
+ raise RuntimeError("memory file changed while it was being opened")
85
+ output = bytearray()
86
+ while len(output) < PREVIEW_BYTES and (
87
+ chunk := os.read(descriptor, min(READ_BYTES, PREVIEW_BYTES - len(output)))
88
+ ):
89
+ output.extend(chunk)
90
+ after = os.fstat(descriptor)
91
+ current = os.stat(name, dir_fd=parent, follow_symlinks=False)
92
+ if fingerprint(opened) != fingerprint(after) or fingerprint(opened) != fingerprint(current):
93
+ raise RuntimeError("memory file changed while it was being read")
94
+ return bytes(output)
95
+ finally:
96
+ if descriptor >= 0:
97
+ os.close(descriptor)
98
+ os.close(parent)
99
+
100
+
101
+ def title_of(home: Path, candidate: MemoryFile) -> str:
102
+ preview = read_prefix(home, candidate).decode("utf-8", errors="replace").splitlines()[:PREVIEW_LINES]
103
+ front = bool(preview and preview[0] == "---")
104
+ for index, line in enumerate(preview):
105
+ if index == 0 and front:
106
+ continue
107
+ if front and line == "---":
108
+ front = False
109
+ continue
110
+ if front and line.startswith("name: "):
111
+ return line.removeprefix("name: ")
112
+ if not front and line.startswith("# "):
113
+ return line.removeprefix("# ")
114
+ return candidate.path.stem
115
+
116
+
117
+ def clean_title(value: str) -> str:
118
+ printable = "".join(" " if ord(character) < 32 or ord(character) == 127 else character for character in value)
119
+ return re.sub(r"\s+", " ", printable).strip()[:160]
120
+
121
+
122
+ def allowed_file(parts: tuple[str, ...], memory_component: bool) -> bool:
123
+ if memory_component:
124
+ return len(parts) == 3 and parts[1] == "memory"
125
+ return 1 <= len(parts) <= 2
126
+
127
+
128
+ def allowed_directory(parts: tuple[str, ...], memory_component: bool) -> bool:
129
+ if memory_component:
130
+ return len(parts) == 1 or (len(parts) == 2 and parts[1] == "memory")
131
+ return len(parts) == 1
132
+
133
+
134
+ def scan_files(
135
+ descriptor: int,
136
+ root: Path,
137
+ parts: tuple[str, ...],
138
+ memory_component: bool,
139
+ maximum: int,
140
+ results: list[MemoryFile],
141
+ ) -> None:
142
+ with os.scandir(descriptor) as entries:
143
+ for entry in entries:
144
+ relative = (*parts, entry.name)
145
+ inspected = entry.stat(follow_symlinks=False)
146
+ if stat.S_ISDIR(inspected.st_mode) and allowed_directory(relative, memory_component):
147
+ child = open_directory(entry.name, descriptor, inspected)
148
+ try:
149
+ scan_files(child, root, relative, memory_component, maximum, results)
150
+ finally:
151
+ os.close(child)
152
+ elif (
153
+ stat.S_ISREG(inspected.st_mode)
154
+ and entry.name.endswith(".md")
155
+ and allowed_file(relative, memory_component)
156
+ ):
157
+ if len(results) >= maximum:
158
+ raise RuntimeError(f"more than {MAX_FILES} memory files")
159
+ results.append(MemoryFile(root.joinpath(*relative), inspected))
160
+
161
+
162
+ def files(home: Path, root: Path, *, memory_component: bool, maximum: int) -> list[MemoryFile]:
163
+ try:
164
+ descriptor = open_chain(home, root.relative_to(home).parts)
165
+ except FileNotFoundError:
166
+ return []
167
+ results: list[MemoryFile] = []
168
+ try:
169
+ scan_files(descriptor, root, (), memory_component, maximum, results)
170
+ finally:
171
+ os.close(descriptor)
172
+ return sorted(results, key=lambda item: os.fspath(item.path))
173
+
174
+
175
+ def main(arguments: list[str]) -> int:
176
+ if len(arguments) != 2:
177
+ sys.stderr.write("usage: agent-memory-files.py <start> <end>\n")
178
+ return 2
179
+ try:
180
+ start, end = map(instant, arguments)
181
+ home = Path(os.environ.get("HOME", ""))
182
+ roots = (
183
+ (home / ".claude" / "projects", "claude", True),
184
+ (home / ".codex" / "memories", "codex", False),
185
+ (home / ".gemini" / "tmp", "gemini", True),
186
+ (home / ".grok" / "memory", "grok", False),
187
+ )
188
+ candidates: list[tuple[MemoryFile, str]] = []
189
+ for root, agent, memory_component in roots:
190
+ candidates.extend(
191
+ (candidate, agent)
192
+ for candidate in files(
193
+ home,
194
+ root,
195
+ memory_component=memory_component,
196
+ maximum=MAX_FILES - len(candidates),
197
+ )
198
+ )
199
+ records: list[dict[str, Any]] = []
200
+ for candidate, agent in candidates:
201
+ modified = datetime.fromtimestamp(candidate.inspected.st_mtime, UTC)
202
+ if not start <= modified < end:
203
+ continue
204
+ if len(records) >= MAX_RECORDS:
205
+ raise RuntimeError(f"more than {MAX_RECORDS} memory records")
206
+ records.append(
207
+ {
208
+ "id": os.fspath(candidate.path),
209
+ "time": modified.replace(microsecond=0).isoformat().replace("+00:00", "Z"),
210
+ "title": clean_title(title_of(home, candidate)),
211
+ "agent": agent,
212
+ }
213
+ )
214
+ output = (json.dumps(records, ensure_ascii=False, separators=(",", ":")) + "\n").encode()
215
+ if len(output) > MAX_OUTPUT_BYTES:
216
+ raise RuntimeError("output exceeds 64 MiB")
217
+ except (OSError, RuntimeError, UnicodeError, ValueError) as error:
218
+ sys.stderr.write(f"agent-memory-files.py: {error}\n")
219
+ return 1
220
+ sys.stdout.buffer.write(output)
221
+ return 0
222
+
223
+
224
+ if __name__ == "__main__":
225
+ raise SystemExit(main(sys.argv[1:]))