elport 0.1.3__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 (41) hide show
  1. elport-0.1.3/.github/workflows/ci.yml +48 -0
  2. elport-0.1.3/.github/workflows/release.yml +33 -0
  3. elport-0.1.3/.gitignore +37 -0
  4. elport-0.1.3/.pre-commit-config.yaml +27 -0
  5. elport-0.1.3/AGENTS.md +66 -0
  6. elport-0.1.3/LICENSE +21 -0
  7. elport-0.1.3/PKG-INFO +249 -0
  8. elport-0.1.3/README.md +227 -0
  9. elport-0.1.3/README_JA.md +238 -0
  10. elport-0.1.3/docs/DESIGN.md +197 -0
  11. elport-0.1.3/docs/ELABFTW-API.md +242 -0
  12. elport-0.1.3/docs/adr/0001-git-like-and-gh-like-command-vocabularies.md +38 -0
  13. elport-0.1.3/docs/adr/0002-remote-base-from-the-patch-response.md +30 -0
  14. elport-0.1.3/pyproject.toml +45 -0
  15. elport-0.1.3/skills/elport/SKILL.md +88 -0
  16. elport-0.1.3/src/elport/__init__.py +1 -0
  17. elport-0.1.3/src/elport/browse.py +54 -0
  18. elport-0.1.3/src/elport/cli.py +566 -0
  19. elport-0.1.3/src/elport/client.py +129 -0
  20. elport-0.1.3/src/elport/config.py +226 -0
  21. elport-0.1.3/src/elport/frontmatter.py +133 -0
  22. elport-0.1.3/src/elport/state.py +43 -0
  23. elport-0.1.3/src/elport/sync.py +1160 -0
  24. elport-0.1.3/src/elport/transclude.py +515 -0
  25. elport-0.1.3/tests/_sync_harness.py +93 -0
  26. elport-0.1.3/tests/conftest.py +20 -0
  27. elport-0.1.3/tests/test_browse.py +242 -0
  28. elport-0.1.3/tests/test_cli.py +1159 -0
  29. elport-0.1.3/tests/test_client.py +229 -0
  30. elport-0.1.3/tests/test_config.py +426 -0
  31. elport-0.1.3/tests/test_state.py +18 -0
  32. elport-0.1.3/tests/test_sync_attachments.py +140 -0
  33. elport-0.1.3/tests/test_sync_comments.py +96 -0
  34. elport-0.1.3/tests/test_sync_fetch.py +216 -0
  35. elport-0.1.3/tests/test_sync_merge.py +574 -0
  36. elport-0.1.3/tests/test_sync_permissions.py +597 -0
  37. elport-0.1.3/tests/test_sync_pull.py +1104 -0
  38. elport-0.1.3/tests/test_sync_push.py +1232 -0
  39. elport-0.1.3/tests/test_sync_status.py +233 -0
  40. elport-0.1.3/tests/test_transclude.py +1151 -0
  41. elport-0.1.3/uv.lock +803 -0
@@ -0,0 +1,48 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_call: # release.yml gates publishing on these checks.
8
+
9
+ # Cancel superseded runs on the same ref.
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ test:
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
24
+ steps:
25
+ - uses: actions/checkout@v7
26
+ with:
27
+ fetch-depth: 0 # hatch-vcs derives the version from tags.
28
+
29
+ # setup-uv stopped publishing major tags at v8; pin the full version.
30
+ # Caching defaults to `auto`, which is what we want here.
31
+ - uses: astral-sh/setup-uv@v10.2.0
32
+ with:
33
+ python-version: ${{ matrix.python-version }}
34
+
35
+ # --locked fails the build if uv.lock has drifted from pyproject.toml.
36
+ - run: uv sync --locked --extra dev
37
+
38
+ - name: ruff check
39
+ run: uv run ruff check
40
+
41
+ - name: ruff format
42
+ run: uv run ruff format --check
43
+
44
+ - name: ty check
45
+ run: uv run ty check src
46
+
47
+ - name: pytest
48
+ run: uv run pytest
@@ -0,0 +1,33 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ ci:
12
+ uses: ./.github/workflows/ci.yml
13
+
14
+ publish:
15
+ needs: ci
16
+ runs-on: ubuntu-latest
17
+ environment: pypi
18
+ permissions:
19
+ contents: read
20
+ id-token: write
21
+ steps:
22
+ - uses: actions/checkout@v7
23
+ with:
24
+ fetch-depth: 0 # hatch-vcs derives the version from tags.
25
+
26
+ - uses: astral-sh/setup-uv@v10.2.0
27
+
28
+ - run: uv build
29
+
30
+ - name: Check the wheel's version matches the tag
31
+ run: test "$(uvx --from dist/*.whl elport --version)" = "elport ${GITHUB_REF_NAME#v}"
32
+
33
+ - run: uv publish --trusted-publishing always
@@ -0,0 +1,37 @@
1
+ # macOS
2
+ .DS_Store
3
+ .AppleDouble
4
+ .LSOverride
5
+ ._*
6
+ .Spotlight-V100
7
+ .Trashes
8
+
9
+ # Windows
10
+ Thumbs.db
11
+ Thumbs.db:encryptable
12
+ ehthumbs.db
13
+ Desktop.ini
14
+ $RECYCLE.BIN/
15
+ *.lnk
16
+
17
+ # Linux
18
+ *~
19
+ .directory
20
+ .Trash-*
21
+ .fuse_hidden*
22
+ .nfs*
23
+
24
+ # Python
25
+ .venv/
26
+ __pycache__/
27
+ *.py[oc]
28
+ .ruff_cache/
29
+ .pytest_cache/
30
+ build/
31
+ dist/
32
+ wheels/
33
+ *.egg-info/
34
+
35
+ # Secrets / local config
36
+ .env
37
+ .elport.toml
@@ -0,0 +1,27 @@
1
+ repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: ruff-check
5
+ name: ruff check
6
+ entry: uv run ruff check --fix
7
+ language: system
8
+ types_or: [python, pyi]
9
+
10
+ - id: ruff-format
11
+ name: ruff format
12
+ entry: uv run ruff format
13
+ language: system
14
+ types_or: [python, pyi]
15
+
16
+ - id: ty-check
17
+ name: ty check
18
+ entry: uv run ty check src
19
+ language: system
20
+ pass_filenames: false
21
+
22
+ - id: pytest
23
+ name: pytest
24
+ entry: uv run pytest
25
+ language: system
26
+ pass_filenames: false
27
+ files: ^tests/
elport-0.1.3/AGENTS.md ADDED
@@ -0,0 +1,66 @@
1
+ # elport: project guide for AI agents
2
+
3
+ `elport` is a **git-like sync CLI for eLabFTW**. It push/pulls locally-written lab notes (Markdown/HTML + referenced
4
+ files) to/from eLabFTW entities. **Local is the source of truth.**
5
+
6
+ ## Documentation map (what is authoritative for what)
7
+
8
+ **Authority differs by role:**
9
+
10
+ - **Behavioral contract (authoritative) = the tests** (`tests/`). The core engine (reference scan/replace, path
11
+ safety, reverse transclusion) and the sync semantics (push/pull/status/diff, conflict detection, the two-form
12
+ base) are owned solely by the tests. **To change behavior, update the tests first**, then implement. Do not
13
+ create prose restatements of the contract (they become a drift source).
14
+ - **Why it is built this way = [docs/DESIGN.md](docs/DESIGN.md)** (frozen: background, scope, settled decisions).
15
+ Decisions made *after* those design sessions live in **[docs/adr/](docs/adr/)** and take precedence over DESIGN
16
+ where they overlap; DESIGN is not edited to match, it only gains a pointer.
17
+ - **Measured eLabFTW API facts = [docs/ELABFTW-API.md](docs/ELABFTW-API.md)** (observation log of the external
18
+ world; update only when the demo's version changes).
19
+ - **User-facing usage = [README.md](README.md)** (commands, front matter, config).
20
+ - **Driving elport from an agent = [skills/elport/SKILL.md](skills/elport/SKILL.md)** (for the downstream operating
21
+ context).
22
+
23
+ Design principles (DESIGN §2.1): single engine / markdown-native / explicit-and-simple / secret isolation. Keep
24
+ added features **thin** (YAGNI); prefer read-only, single-endpoint features.
25
+
26
+ ## Invariants you must not break (detailed authority is the tests, rationale is DESIGN)
27
+
28
+ - **Local is authoritative.** push is a full-body overwrite with conflict detection. Do not silently add two-way
29
+ auto-sync.
30
+ - push transclusion runs on the **outgoing copy** and **never rewrites the local original**.
31
+ - **base is two-form**: remote-base (the stored body returned by the push `PATCH`, not the sent bytes;
32
+ [ADR-0002](docs/adr/0002-remote-base-from-the-patch-response.md)) and local-base
33
+ (the local original at push/pull time). **Always compare same-form** (remote↔remote-base, local↔local-base). With
34
+ only one form, server normalization (non-convergent) yields a false conflict / permanent-dirty every time. See
35
+ the "Body normalization" section of ELABFTW-API.
36
+ - **Do not embed out-of-sync data (comments, steps, revision bodies) in the body.** Use a sidecar or terminal
37
+ output (do not pollute the source).
38
+ - Upload targets are **only files under the document directory.** Exclude schemed URLs, absolute paths, `..`
39
+ escapes, and **hidden files/dirs (any dot-prefixed path component, e.g. `.env`, `.git/config`)**. A pulled
40
+ remote body must not make the next push exfiltrate local secrets; **never parse inside code fences, inline code,
41
+ or HTML comments.**
42
+ - Attachment identity uses the **server-returned hash first, else basename + size** (same-name entries can coexist,
43
+ so check against all of them). **Never auto-delete attachments during push.**
44
+ - **Keep secrets (API keys) out of the project tree.** OS keyring + home-dir config only. **Never log the key.**
45
+
46
+ ## Non-obvious eLabFTW API traps (highlights; details in ELABFTW-API.md)
47
+
48
+ - **`content_type:2` (markdown) is settable via PATCH and respected by recent eLabFTW** (the create-time body bug
49
+ #6416 is fixed; very old instances may ignore it). **Verify `content_type == 2` after push** and abort otherwise.
50
+ - **`download.php?...` URLs use browser Cookie-session auth** and **do not work with the API key.** Embed them in
51
+ the body for humans, but the tool's own download (pull) uses the API
52
+ `GET /api/v2/{entity}/{id}/uploads/{id}?format=binary`.
53
+ - An upload's `long_name` **contains `/`**. When building download-URL queries, **percent-encode is mandatory.**
54
+ - **The body is normalized on every save and re-sending the normalized form does not converge** (even in md mode).
55
+ **HTML-unescape** URLs in the body before parsing them.
56
+ - **Revisions are readable** (`GET .../revisions[/{id}]`), but nothing ties a version to "my last push." Keep them
57
+ **read-only**; do not use them to auto-restore the 3-way merge ancestor base.
58
+
59
+ ## Testing and verification
60
+
61
+ - The core engine (reference scan/replace, path safety, reverse transclusion) and the sync semantics are
62
+ unit-tested **without network** (the API layer is mocked). These tests are the authority for behavior, so
63
+ **changes go test-first.**
64
+ - When live API behavior needs confirming, use **only the public demo <https://demo.elabftw.net>** (public,
65
+ periodically reset, disposable). Never use any other instance. Record the demo version a fact was measured on;
66
+ version-dependent behavior is confirmed on the demo's current version only.
elport-0.1.3/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 youthesame
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
elport-0.1.3/PKG-INFO ADDED
@@ -0,0 +1,249 @@
1
+ Metadata-Version: 2.5
2
+ Name: elport
3
+ Version: 0.1.3
4
+ Summary: Git-like sync CLI for eLabFTW (local-first push/pull of Markdown/HTML lab notes)
5
+ Project-URL: Repository, https://github.com/youthesame/elport
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: keyring>=25
10
+ Requires-Dist: pyyaml>=6.0
11
+ Requires-Dist: requests>=2.31
12
+ Requires-Dist: tomli-w>=1.0
13
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
14
+ Provides-Extra: dev
15
+ Requires-Dist: pre-commit>=4.6.0; extra == 'dev'
16
+ Requires-Dist: pytest-xdist>=3.8; extra == 'dev'
17
+ Requires-Dist: pytest>=9.0; extra == 'dev'
18
+ Requires-Dist: ruff>=0.16; extra == 'dev'
19
+ Requires-Dist: tomli>=2.0; extra == 'dev'
20
+ Requires-Dist: ty>=0.0.69; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # elport: a git-like sync CLI for eLabFTW
24
+
25
+ [English](https://github.com/youthesame/elport/blob/main/README.md) | [日本語](https://github.com/youthesame/elport/blob/main/README_JA.md)
26
+
27
+ Write lab notes locally in Markdown, with inline HTML like `<figure>` where you want it. Then `push`/`pull` them,
28
+ along with every figure and data file the body references, to eLabFTW entities. **Local is the source of truth.**
29
+
30
+ ## Install
31
+
32
+ Python 3.10+, installed as a `uv` tool (provides the `elport` command):
33
+
34
+ ```sh
35
+ uv tool install elport # upgrade: uv tool upgrade elport
36
+ ```
37
+
38
+ Or with pip:
39
+
40
+ ```sh
41
+ pip install elport # upgrade: pip install -U elport
42
+ ```
43
+
44
+ ## The core idea
45
+
46
+ One local file maps to one eLabFTW entry: Markdown with a YAML front matter block, inline HTML allowed.
47
+
48
+ On `push`, elport uploads every real local file the body references, in any notation, and swaps each path for its
49
+ real eLabFTW URL. The body goes up as raw Markdown, so `<figure>` and `$...$` render as-is. To link another entry
50
+ instead of uploading, write its eLabFTW URL. Code fences, inline code, and HTML comments are never parsed.
51
+
52
+ ## Quick start
53
+
54
+ ```sh
55
+ elport login labA # store base_url (config) + api_key (OS keyring), interactively
56
+ elport list # what do I already have on the server? (read-only)
57
+ elport new "CRISPR titration" # create the entity, scaffold report.md with id
58
+ # ...or start from an entity made in the Web UI: elport clone <id>
59
+ # ...edit report.md, drop fig1.png / data.csv next to it...
60
+ elport status # what would sync? (read-only)
61
+ elport push # upload references + push the body
62
+ ```
63
+
64
+ ## Commands
65
+
66
+ Commands that act on a local document take an optional `[<doc>]`, defaulting to `report.md`. Any name works.
67
+ Exit code `0` on success, `1` on failure. Run `elport <command> --help` for the full flags.
68
+
69
+ ### Sync
70
+
71
+ | Command | Summary |
72
+ |---|---|
73
+ | `push` | Upload the body and its referenced files to one entity. |
74
+ | `pull` | Fetch the body and metadata, and download referenced files. |
75
+ | `fetch` | Download every attachment, referenced or not. Read-only. |
76
+ | `status` | Report what would sync, and in which mode. Read-only. |
77
+ | `diff` | Diff local against remote, or against the last push with `--base`. |
78
+ | `merge` | Merge `.base.md` and `.remote.md` into `<doc>` after a conflict. Requires `git`. |
79
+
80
+ `push` creates the entity and writes `id` back to the front matter when `id` is unset, and it runs the mode and
81
+ conflict checks first. `merge` is local-only: it runs `git merge-file`, so `git` must be installed, but the folder
82
+ need not be a git repository.
83
+
84
+ ### Entities
85
+
86
+ | Command | Summary |
87
+ |---|---|
88
+ | `new "<title>"` | Create an entity and scaffold front matter. |
89
+ | `clone <id>` | Start `<doc>` from an entity that already exists, then pull. |
90
+ | `list` | List remote entities as `id date title`. Read-only. |
91
+ | `view <id>` | Print one remote body to stdout, exactly as stored. |
92
+ | `comments` | Print the remote comment thread. |
93
+ | `comment "<text>"` | Post one comment. Use the Web UI to edit or delete. |
94
+
95
+ `new` and `clone` write to `report.md` unless you pass `-o <doc>`. `list` defaults to your own entities
96
+ (`--scope self`) and returns 50 at a time, with `-q <term>`, `--limit`, `--offset`, and `--json` to narrow it.
97
+ `comments` prints to the terminal only, never into the body.
98
+
99
+ ### Setup
100
+
101
+ | Command | Summary |
102
+ |---|---|
103
+ | `login [<profile>]` | Store base_url in `config.toml`, api_key in the OS keyring. |
104
+ | `logout [<profile>]` | Remove the stored api_key. Keeps base_url. |
105
+ | `profile [use <name>]` | List profiles, or set the default one. |
106
+ | `whoami` | Check auth: user, team, role, key permissions, server version, scopes. |
107
+
108
+ ### Shared options
109
+
110
+ - `-n/--dry-run` rehearses a push without sending.
111
+ - `-f/--force` pushes over a changed remote, losing the Web-side change.
112
+ - `-y/--yes` skips the two confirmations: widening `read`/`write` beyond your team, and new uploads over 25 MiB.
113
+ A non-interactive run needs `-y` for either.
114
+ - `--profile <name>` and `--entity {experiments,items}` pick the destination.
115
+
116
+ ### pull and fetch
117
+
118
+ Neither command is a superset of the other.
119
+
120
+ - `pull` writes referenced files back by **basename only**. A subdirectory path like `assets/fig.png` lands as
121
+ `fig.png`.
122
+ - `pull` only downloads files the body links to. Attachments that sit on the entity without being embedded, like
123
+ raw data or spectra, stay on the server.
124
+ - `fetch` brings those down. It never parses the body, touches the base, or feeds the push manifest, so a fetched
125
+ file is not uploaded again unless you link it in the body yourself.
126
+ - If a local file differs, `fetch` leaves it in place and writes the remote copy beside it as `<name>.remote`.
127
+
128
+ ## Document format (front matter)
129
+
130
+ A YAML block at the top, generated or completed on push if absent:
131
+
132
+ ```markdown
133
+ ---
134
+ id: 42 # entity ID (auto-filled on creation)
135
+ entity: experiments # experiments | items (default experiments)
136
+ title: "experiment title" # optional; defaults to the file name on creation
137
+ tags: [CRISPR, PCR] # optional; add-only (remove tags in the Web UI)
138
+ category: Molecular Biology # optional; ID or existing category name (elport never creates categories)
139
+ status: Running # optional; ID or existing status name (elport never creates statuses)
140
+ profile: labA # optional; destination profile
141
+ read: team # optional; owner | owner+admin | team | account | public
142
+ write: owner # optional; same scale
143
+ ---
144
+
145
+ # Body. Markdown, inline HTML allowed ...
146
+ ```
147
+
148
+ The block holds only `id`, human metadata, and an optional `profile`. The base and the hashes live in state. elport
149
+ strips the front matter before sending the body.
150
+
151
+ `title`, `category`, `status`, `read`, and `write` are reflected **only when present**. Omit a key and elport leaves
152
+ that remote value untouched. `pull` writes them back from the server, so a key you never wrote may appear after a
153
+ pull, while a key you edited locally but have not pushed is kept. `tags` are add-only on both sides, so pull keeps
154
+ the union.
155
+
156
+ `read`/`write` set the eLabFTW base visibility only, so individual Web-UI grants survive. Widening beyond your team
157
+ asks for confirmation.
158
+
159
+ If the front matter and the CLI disagree on profile, entity, or id, elport **stops** rather than guessing.
160
+
161
+ ## Conflicts
162
+
163
+ Since you may also edit the body in the Web UI, `push` compares the current remote against the stored base first.
164
+
165
+ - **unchanged** proceeds.
166
+ - **changed** aborts. elport writes `<name>.base.md` (the ancestor) and `<name>.remote.md`. Run `elport merge` to
167
+ 3-way them into your file, resolve any `<<<<<<<`/`>>>>>>>` markers, then push. A body that still has markers is
168
+ refused.
169
+ - **no base on this machine** aborts. Run `elport pull` first, or `--force` to overwrite blind.
170
+
171
+ `--force` discards the Web-side change, so use it deliberately. eLabFTW keeps server-side history recoverable from
172
+ the Web UI as a safety net, but elport keeps no local history of its own. Want per-edit history? `git init` your
173
+ notes folder, since the files are plain Markdown.
174
+
175
+ ## Configuration & auth
176
+
177
+ **No credentials in the project.**
178
+
179
+ ### Where the key lives
180
+
181
+ `elport login` stores the API key in your OS keyring: Keychain, Credential Manager, or Secret Service. Everything
182
+ that is not a secret, base_url included, goes in `~/.config/elport/config.toml` with mode `600`. elport never
183
+ prints the key.
184
+
185
+ ```toml
186
+ # ~/.config/elport/config.toml
187
+ default_profile = "labA"
188
+
189
+ [profiles.labA]
190
+ base_url = "https://lab-a.example.org"
191
+ verify_ssl = true
192
+ ```
193
+
194
+ ### Which credentials win
195
+
196
+ elport checks three sources, in order, and stops at the first hit.
197
+
198
+ 1. `ELABFTW_BASE_URL` + `ELABFTW_API_KEY` in the environment. This is how CI authenticates.
199
+ 2. The keyring and config pair from `elport login`. The normal default.
200
+ 3. A plaintext key in `~/.config/elport/config.toml`. A last resort, used with a warning, and only when no keyring backend exists (e.g. WSL, headless servers).
201
+
202
+ ### Profiles
203
+
204
+ Profiles layer like settings.json, one per team:
205
+
206
+ ```
207
+ ~/.config/elport/config.toml → <project>/.elport.toml → <dir>/.elport.toml
208
+ ```
209
+
210
+ Your first `elport login` becomes the default. `elport profile use <name>` switches it, or set `profile:` in a
211
+ note's front matter to pin that one file.
212
+
213
+ ### .elportignore
214
+
215
+ `.elportignore` excludes referenced files, `.gitignore`-style, so they are never uploaded. It is additive across
216
+ the same layers as profiles, plus every `.elportignore` from the project root down to the document directory. Each
217
+ pattern is anchored at its own directory.
218
+
219
+ ## Learn more
220
+
221
+ - **Why it works this way** → [docs/DESIGN.md](https://github.com/youthesame/elport/blob/main/docs/DESIGN.md)
222
+ - **How eLabFTW's API actually behaves** → [docs/ELABFTW-API.md](https://github.com/youthesame/elport/blob/main/docs/ELABFTW-API.md)
223
+ - **The behavioral contract** → the test suite (`tests/`) is authoritative
224
+ - **Driving elport from an AI agent** → [skills/elport/SKILL.md](https://github.com/youthesame/elport/blob/main/skills/elport/SKILL.md)
225
+
226
+ ## Development
227
+
228
+ | Module | Role |
229
+ |---|---|
230
+ | `cli.py` | Argument parsing and dispatch |
231
+ | `sync.py` | push, pull, status, diff |
232
+ | `transclude.py` | Path ↔ URL rewriting, both directions |
233
+ | `client.py` | eLabFTW API wrapper |
234
+ | `state.py` | The stored base and its hashes |
235
+ | `config.py` | Profiles and credential resolution |
236
+ | `frontmatter.py` | The YAML block |
237
+ | `browse.py` | list and view |
238
+
239
+ **The tests are the authoritative behavioral contract, so change tests first.** You can check live API behavior
240
+ against <https://demo.elabftw.net>.
241
+
242
+ ## Acknowledgments
243
+
244
+ - [elab-doc-sync](https://github.com/Kosaku-Noba/elab-doc-sync)
245
+ - [elAPI](https://github.com/uhd-urz/elAPI)
246
+
247
+ ## License
248
+
249
+ MIT