loi-light 0.1.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.
Files changed (34) hide show
  1. loi_light-0.1.0/LICENSE +202 -0
  2. loi_light-0.1.0/PKG-INFO +78 -0
  3. loi_light-0.1.0/README.md +60 -0
  4. loi_light-0.1.0/pyproject.toml +36 -0
  5. loi_light-0.1.0/src/loil/__init__.py +11 -0
  6. loi_light-0.1.0/src/loil/__main__.py +6 -0
  7. loi_light-0.1.0/src/loil/adapters/__init__.py +36 -0
  8. loi_light-0.1.0/src/loil/adapters/base.py +80 -0
  9. loi_light-0.1.0/src/loil/adapters/claude_code.py +493 -0
  10. loi_light-0.1.0/src/loil/adapters/claude_settings.py +147 -0
  11. loi_light-0.1.0/src/loil/capture.py +336 -0
  12. loi_light-0.1.0/src/loil/cli.py +76 -0
  13. loi_light-0.1.0/src/loil/commands/__init__.py +0 -0
  14. loi_light-0.1.0/src/loil/commands/capture.py +127 -0
  15. loi_light-0.1.0/src/loil/commands/init.py +250 -0
  16. loi_light-0.1.0/src/loil/commands/ls.py +31 -0
  17. loi_light-0.1.0/src/loil/commands/verify.py +83 -0
  18. loi_light-0.1.0/src/loil/entry.py +27 -0
  19. loi_light-0.1.0/src/loil/errors.py +6 -0
  20. loi_light-0.1.0/src/loil/gitstate.py +86 -0
  21. loi_light-0.1.0/src/loil/hooks.py +87 -0
  22. loi_light-0.1.0/src/loil/log.py +37 -0
  23. loi_light-0.1.0/src/loil/record/__init__.py +0 -0
  24. loi_light-0.1.0/src/loil/record/config.py +142 -0
  25. loi_light-0.1.0/src/loil/record/project.py +141 -0
  26. loi_light-0.1.0/src/loil/record/registry.py +138 -0
  27. loi_light-0.1.0/src/loil/record/store.py +303 -0
  28. loi_light-0.1.0/src/loil/shim.py +92 -0
  29. loi_light-0.1.0/src/loil/snapshots.py +331 -0
  30. loi_light-0.1.0/src/loil/util/__init__.py +0 -0
  31. loi_light-0.1.0/src/loil/util/clock.py +32 -0
  32. loi_light-0.1.0/src/loil/util/fsutil.py +39 -0
  33. loi_light-0.1.0/src/loil/util/gitignore.py +84 -0
  34. loi_light-0.1.0/src/loil/util/ulid.py +63 -0
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: loi-light
3
+ Version: 0.1.0
4
+ Summary: Back up every Claude Code session of a repo, and the metadata needed to see how the idea evolved.
5
+ Keywords: claude-code,provenance,transcripts,backup
6
+ Author: Daking Rai
7
+ Author-email: Daking Rai <dakingrae@gmail.com>
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.11
16
+ Project-URL: Repository, https://github.com/Dakingrai/loi-light
17
+ Description-Content-Type: text/markdown
18
+
19
+ # loil — life of ideas, light
20
+
21
+ Claude Code deletes its transcripts after about thirty days. `loil` keeps them, and
22
+ everything around them that you would need later to see how an idea evolved, in a private
23
+ store on your disk. It does nothing else.
24
+
25
+ **Everything stays on your machine. `loil` never uploads, syncs or sends anything anywhere.**
26
+ It has no network code, no account, no telemetry and no cloud. The store is a folder on
27
+ your own disk (`~/.loil`, readable only by you), and the only things that leave it are the
28
+ ones you move yourself.
29
+
30
+ ## What it keeps
31
+
32
+ For every Claude Code session of a repo you `loil init`:
33
+
34
+ | in the store | what |
35
+ |---|---|
36
+ | `payloads/*.json` | the raw hook payloads (start, end, every compaction) |
37
+ | `transcript.jsonl.gz` | the conversation, byte for byte |
38
+ | `subagents/`, `tool-results/`, … | everything in the session's own Claude directory |
39
+ | `plans/<sha>.md` | every plan file the session named, every version |
40
+ | `file-history/*.gz` | Claude's copies of every file version it edited in the session |
41
+ | `git/start.json`, `git/end.json`, `git/diff-end.patch.gz` | HEAD, branch, status and the uncommitted diff |
42
+ | `claude-project-entry.json` | the project's entry in `~/.claude.json` (cost, tokens, model) |
43
+
44
+ And per project: snapshots of Claude's project memory and of the repo's agent configuration
45
+ (`CLAUDE.md`, `.claude/…`) whenever they change, your prompt history for the project with the
46
+ pasted text it references, and Claude's own `sessions-index.json`.
47
+
48
+ Everything is plain files: JSON, JSONL, gzip. `gunzip -c` gives you the original. Each file is
49
+ recorded with its size and sha256, and `loil verify` checks them all.
50
+
51
+ ## Use
52
+
53
+ ```sh
54
+ uv tool install loi-light # or: pipx install loi-light
55
+ cd ~/your/repo
56
+ loil init # hooks into .claude/settings.local.json; imports the sessions Claude still holds
57
+ loil ls # the sessions recorded for this repo
58
+ loil verify # re-hash every stored file
59
+ loil capture --backfill # import again (e.g. after sessions ran on another machine)
60
+ ```
61
+
62
+ The store is `~/.loil` (dirs 0700, files 0600). Set `LOIL_HOME` or `loil init --home PATH`
63
+ to keep it elsewhere. Nothing large or sensitive goes inside the repo: it gets
64
+ `.loil/config.toml` (commit it; it is the project's id) and two gitignored per-user files.
65
+
66
+ The store may hold what Claude's own files hold, including MCP environment values from
67
+ `.claude/settings*.json`. **Nothing in it is ever transmitted: it stays local unless you
68
+ copy it somewhere yourself.** If you point `LOIL_HOME` at a synced folder (iCloud, Dropbox),
69
+ that sync is yours, not loil's.
70
+
71
+ ## Status
72
+
73
+ Pre-alpha. Hooks, capture, backfill, `ls` and `verify` work for Claude Code. Not yet:
74
+ importing sessions from a `loi` store, a periodic capture to bound loss on a crash, Codex.
75
+
76
+ ## License
77
+
78
+ Apache-2.0
@@ -0,0 +1,60 @@
1
+ # loil — life of ideas, light
2
+
3
+ Claude Code deletes its transcripts after about thirty days. `loil` keeps them, and
4
+ everything around them that you would need later to see how an idea evolved, in a private
5
+ store on your disk. It does nothing else.
6
+
7
+ **Everything stays on your machine. `loil` never uploads, syncs or sends anything anywhere.**
8
+ It has no network code, no account, no telemetry and no cloud. The store is a folder on
9
+ your own disk (`~/.loil`, readable only by you), and the only things that leave it are the
10
+ ones you move yourself.
11
+
12
+ ## What it keeps
13
+
14
+ For every Claude Code session of a repo you `loil init`:
15
+
16
+ | in the store | what |
17
+ |---|---|
18
+ | `payloads/*.json` | the raw hook payloads (start, end, every compaction) |
19
+ | `transcript.jsonl.gz` | the conversation, byte for byte |
20
+ | `subagents/`, `tool-results/`, … | everything in the session's own Claude directory |
21
+ | `plans/<sha>.md` | every plan file the session named, every version |
22
+ | `file-history/*.gz` | Claude's copies of every file version it edited in the session |
23
+ | `git/start.json`, `git/end.json`, `git/diff-end.patch.gz` | HEAD, branch, status and the uncommitted diff |
24
+ | `claude-project-entry.json` | the project's entry in `~/.claude.json` (cost, tokens, model) |
25
+
26
+ And per project: snapshots of Claude's project memory and of the repo's agent configuration
27
+ (`CLAUDE.md`, `.claude/…`) whenever they change, your prompt history for the project with the
28
+ pasted text it references, and Claude's own `sessions-index.json`.
29
+
30
+ Everything is plain files: JSON, JSONL, gzip. `gunzip -c` gives you the original. Each file is
31
+ recorded with its size and sha256, and `loil verify` checks them all.
32
+
33
+ ## Use
34
+
35
+ ```sh
36
+ uv tool install loi-light # or: pipx install loi-light
37
+ cd ~/your/repo
38
+ loil init # hooks into .claude/settings.local.json; imports the sessions Claude still holds
39
+ loil ls # the sessions recorded for this repo
40
+ loil verify # re-hash every stored file
41
+ loil capture --backfill # import again (e.g. after sessions ran on another machine)
42
+ ```
43
+
44
+ The store is `~/.loil` (dirs 0700, files 0600). Set `LOIL_HOME` or `loil init --home PATH`
45
+ to keep it elsewhere. Nothing large or sensitive goes inside the repo: it gets
46
+ `.loil/config.toml` (commit it; it is the project's id) and two gitignored per-user files.
47
+
48
+ The store may hold what Claude's own files hold, including MCP environment values from
49
+ `.claude/settings*.json`. **Nothing in it is ever transmitted: it stays local unless you
50
+ copy it somewhere yourself.** If you point `LOIL_HOME` at a synced folder (iCloud, Dropbox),
51
+ that sync is yours, not loil's.
52
+
53
+ ## Status
54
+
55
+ Pre-alpha. Hooks, capture, backfill, `ls` and `verify` work for Claude Code. Not yet:
56
+ importing sessions from a `loi` store, a periodic capture to bound loss on a crash, Codex.
57
+
58
+ ## License
59
+
60
+ Apache-2.0
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "loi-light"
3
+ version = "0.1.0"
4
+ description = "Back up every Claude Code session of a repo, and the metadata needed to see how the idea evolved."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "Apache-2.0"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Daking Rai", email = "dakingrae@gmail.com" }]
10
+ keywords = ["claude-code", "provenance", "transcripts", "backup"]
11
+ classifiers = [
12
+ "Development Status :: 2 - Pre-Alpha",
13
+ "Environment :: Console",
14
+ "Operating System :: POSIX :: Linux",
15
+ "Operating System :: MacOS",
16
+ "Programming Language :: Python :: 3",
17
+ ]
18
+ dependencies = []
19
+
20
+ [project.urls]
21
+ Repository = "https://github.com/Dakingrai/loi-light"
22
+
23
+ [project.scripts]
24
+ loil = "loil.entry:main"
25
+
26
+ [build-system]
27
+ requires = ["uv_build>=0.11,<0.12"]
28
+ build-backend = "uv_build"
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "pytest>=9.1.1",
33
+ ]
34
+
35
+ [tool.uv.build-backend]
36
+ module-name = "loil"
@@ -0,0 +1,11 @@
1
+ """loil — back up the trace a coding agent leaves behind."""
2
+
3
+
4
+ def __getattr__(name: str):
5
+ # PEP 562: importlib.metadata costs ~70 ms; only pay it when someone asks, once.
6
+ if name == "__version__":
7
+ from importlib.metadata import version
8
+
9
+ globals()["__version__"] = v = version("loil")
10
+ return v
11
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from loil.entry import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,36 @@
1
+ """Coding-agent adapters: one per tool loil integrates (Claude Code today). An adapter
2
+ name is `^[a-z][a-z0-9-]*$` and is the key everywhere a session says which tool produced
3
+ it: `sessions/<agent>/`, `Session.agent`, and the hook contract `loil hook <agent> <event>`.
4
+
5
+ Stdlib only, and this module in particular stays tiny: `hooks.run` imports it on every
6
+ session start to validate the agent word. The adapter modules themselves are imported
7
+ lazily by `get`.
8
+ """
9
+
10
+ from loil.errors import LoilError
11
+
12
+ KNOWN = ("claude-code",)
13
+
14
+ _FIRST = "abcdefghijklmnopqrstuvwxyz"
15
+ _REST = frozenset(_FIRST + "0123456789-")
16
+
17
+
18
+ def check_agent(name: str) -> str:
19
+ """The adapter-name shape (`^[a-z][a-z0-9-]*$`), without importing `re`."""
20
+ if not isinstance(name, str) or not name or name[0] not in _FIRST or not all(c in _REST for c in name):
21
+ raise LoilError(f"invalid adapter name {name!r}; expected ^[a-z][a-z0-9-]*$")
22
+ return name
23
+
24
+
25
+ def is_known(name: str) -> bool:
26
+ return name in KNOWN
27
+
28
+
29
+ def get(name: str):
30
+ """The adapter for `name` (LoilError if the shape is wrong or no such adapter)."""
31
+ check_agent(name)
32
+ if name not in KNOWN:
33
+ raise LoilError(f"unknown coding-agent adapter {name!r}; known: {', '.join(KNOWN)}")
34
+ from importlib import import_module # importlib is already loaded by the interpreter
35
+
36
+ return import_module(f"loil.adapters.{name.replace('-', '_')}").ADAPTER
@@ -0,0 +1,80 @@
1
+ """The adapter interface. Stdlib only (the handlers run on the hook path).
2
+
3
+ An adapter with hooks lists them in `hook_events`; `loil init` installs
4
+ `<shim> hook <name> <event>` for each into the tool's settings file. A passive adapter
5
+ (a tool with no hooks) has `hook_events == ()` and relies on `discover` (find the sessions
6
+ the tool left on disk) and `capture` (copy their artifacts).
7
+
8
+ A plain class, not a dataclass: `dataclasses` costs ~14 ms on the hook path.
9
+ """
10
+
11
+ from pathlib import Path
12
+
13
+
14
+ class Source:
15
+ """One artifact to store: `kind` (transcript|subagent|tool-result|plan|file-history|
16
+ payload|git|claude-project-entry|other), the absolute `src` on disk — or `data`, the
17
+ bytes themselves, for content loil generates (a hook payload, a git snapshot) — and
18
+ `dst_rel` under the session's artifacts dir (`.gz` appended by the adapter when the
19
+ copy is gzipped; None for a plan, named by content hash). `feed`, if set, receives
20
+ every chunk read from `src`."""
21
+
22
+ __slots__ = ("kind", "src", "dst_rel", "feed", "data")
23
+
24
+ def __init__(self, kind, src, dst_rel, feed=None, *, data=None):
25
+ if (src is None) == (data is None):
26
+ raise ValueError("Source needs exactly one of src and data")
27
+ self.kind, self.src, self.dst_rel, self.feed, self.data = kind, src, dst_rel, feed, data
28
+
29
+ def __repr__(self):
30
+ return f"<Source {self.kind} {self.src if self.src is not None else '<data>'} -> {self.dst_rel}>"
31
+
32
+
33
+ class Discovered:
34
+ """A session found on disk by `Adapter.discover`: `id`, its `transcript_path`, and
35
+ `start`/`end` in loil's timestamp shape (None when the transcript carries none)."""
36
+
37
+ __slots__ = ("id", "transcript_path", "start", "end")
38
+
39
+ def __init__(self, id, transcript_path, start, end):
40
+ self.id, self.transcript_path, self.start, self.end = id, transcript_path, start, end
41
+
42
+ def __repr__(self):
43
+ return f"<Discovered {self.id} {self.start}..{self.end}>"
44
+
45
+
46
+ class Adapter:
47
+ #: `^[a-z][a-z0-9-]*$`; the directory name under `sessions/`
48
+ name: str = ""
49
+ #: (settings key, `loil hook <name> <event>` event word, timeout in seconds or None)
50
+ hook_events: tuple[tuple[str, str, int | None], ...] = ()
51
+ #: Whether `loil init` should add this adapter's settings file to `.gitignore`.
52
+ settings_are_local: bool = True
53
+
54
+ def settings_path(self, root: Path) -> Path | None:
55
+ """The per-user hook settings file of this tool in a checkout, or None."""
56
+ return None
57
+
58
+ def handle(self, event: str, payload: bytes) -> None:
59
+ """`loil hook <name> <event>` with a non-empty payload (raw stdin bytes). Stdlib
60
+ only; never imports argparse. Raise while the project is still unknown (the global
61
+ log), log to the project's own log afterwards — the hook always exits 0 either way.
62
+ Writes nothing to stdout: loil injects no context."""
63
+ return None
64
+
65
+ def sources(self, session):
66
+ """The raw artifacts of `session` on this machine, in copy order, as `Source`s:
67
+ the transcript first. A source whose `feed` is set gets every chunk of its bytes
68
+ while it is read (the plan-file scan rides the transcript read pass), and the
69
+ list may be a generator that yields plan sources *after* the transcripts were
70
+ read. Raise LoilError when the session cannot be captured here (no
71
+ `transcript_path`, transcript gone)."""
72
+ return []
73
+
74
+ def discover(self, root):
75
+ """The sessions this tool still holds on disk for the checkout at `root`
76
+ (`Discovered`s, oldest first), for backfill."""
77
+ return []
78
+
79
+ def __repr__(self) -> str:
80
+ return f"<Adapter {self.name}>"