surf-cli 0.7.0__tar.gz → 0.7.1__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 (37) hide show
  1. {surf_cli-0.7.0 → surf_cli-0.7.1}/.agents/skills/surf/SKILL.md +5 -13
  2. {surf_cli-0.7.0 → surf_cli-0.7.1}/.agents/skills/surf/references/about.md +1 -1
  3. {surf_cli-0.7.0 → surf_cli-0.7.1}/.agents/skills/surf/references/usage.md +1 -1
  4. {surf_cli-0.7.0 → surf_cli-0.7.1}/.claude-plugin/marketplace.json +2 -2
  5. surf_cli-0.7.1/CONTRIBUTING.md +53 -0
  6. surf_cli-0.7.1/PKG-INFO +272 -0
  7. surf_cli-0.7.1/README.md +237 -0
  8. {surf_cli-0.7.0 → surf_cli-0.7.1}/flake.nix +1 -1
  9. {surf_cli-0.7.0 → surf_cli-0.7.1}/plugins/surf/.claude-plugin/plugin.json +1 -1
  10. {surf_cli-0.7.0 → surf_cli-0.7.1}/plugins/surf/plugin.json +1 -1
  11. {surf_cli-0.7.0 → surf_cli-0.7.1}/pyproject.toml +2 -2
  12. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/__init__.py +1 -1
  13. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/logic.py +35 -0
  14. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/test_logic.py +57 -0
  15. {surf_cli-0.7.0 → surf_cli-0.7.1}/uv.lock +1 -1
  16. surf_cli-0.7.0/PKG-INFO +0 -177
  17. surf_cli-0.7.0/README.md +0 -142
  18. {surf_cli-0.7.0 → surf_cli-0.7.1}/.agents/skills/surf/references/surf.pdf +0 -0
  19. {surf_cli-0.7.0 → surf_cli-0.7.1}/.agents/skills/surf/references/surf.tex +0 -0
  20. {surf_cli-0.7.0 → surf_cli-0.7.1}/.github/workflows/publish.yml +0 -0
  21. {surf_cli-0.7.0 → surf_cli-0.7.1}/.gitignore +0 -0
  22. {surf_cli-0.7.0 → surf_cli-0.7.1}/.grok-plugin/marketplace.json +0 -0
  23. {surf_cli-0.7.0 → surf_cli-0.7.1}/LICENSE +0 -0
  24. {surf_cli-0.7.0 → surf_cli-0.7.1}/RELEASING.md +0 -0
  25. {surf_cli-0.7.0 → surf_cli-0.7.1}/flake.lock +0 -0
  26. {surf_cli-0.7.0 → surf_cli-0.7.1}/scripts/deploy.sh +0 -0
  27. {surf_cli-0.7.0 → surf_cli-0.7.1}/scripts/lint.sh +0 -0
  28. {surf_cli-0.7.0 → surf_cli-0.7.1}/scripts/test.sh +0 -0
  29. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/__main__.py +0 -0
  30. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/adapters.py +0 -0
  31. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/models.py +0 -0
  32. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/orchestrator.py +0 -0
  33. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/test_adapters.py +0 -0
  34. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/test_extract_section_perf_smoke.py +0 -0
  35. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/test_models.py +0 -0
  36. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/test_orchestrator.py +0 -0
  37. {surf_cli-0.7.0 → surf_cli-0.7.1}/src/surf/test_tex_logic.py +0 -0
@@ -8,7 +8,7 @@ metadata:
8
8
  github_username: saintx
9
9
  email: alex@saintx.us
10
10
  twitter: alexsaintx
11
- surf-version: "0.7.0"
11
+ surf-version: "0.7.1"
12
12
  ---
13
13
 
14
14
  # Surf: Progressive Context Disclosure
@@ -42,19 +42,11 @@ Resolve with `surf` against the section heading; do not bulk-ingest a whole refe
42
42
  | Platform aggregation (batch section extraction) | `surf references/usage.md "Platform aggregation (batch section extraction)"` |
43
43
  | Additional options | `surf references/usage.md "Additional options"` |
44
44
 
45
- ### Whitepaper (TeX and PDF examples)
45
+ ### TeX and PDF
46
46
 
47
- The same paper ships as `references/surf.tex` and `references/surf.pdf`; both list the same tree, so any row below works against either file.
47
+ `references/surf.tex` and `references/surf.pdf` are one document in two forms. The same address works on either.
48
48
 
49
49
  | Intent | Section extraction |
50
50
  |--------|--------------------|
51
- | List a TeX paper's sections | `surf references/surf.tex --list` |
52
- | List a PDF's outline | `surf references/surf.pdf --list` |
53
- | Read the abstract | `surf references/surf.tex "abstract"` |
54
- | Why surf exists | `surf references/surf.pdf "Background"` |
55
- | What a thin index looks like | `surf references/surf.tex "A Thin Index Shaped by Intent"` |
56
- | Who composes context | `surf references/surf.pdf "Agentic Context Composition"` |
57
- | Check a skill mechanically | `surf references/surf.tex "Skills You Can Check"` |
58
- | Compose indexes across skills | `surf references/surf.pdf "Indexes over Indexes"` |
59
- | Formats and corpora surf addresses | `surf references/surf.tex "Markdown, TeX, and PDF"` |
60
- | How the paper checks itself | `surf references/surf.pdf "This Paper"` |
51
+ | Extract a section from TeX | `surf references/surf.tex "Markdown, TeX, and PDF"` |
52
+ | Extract a section from PDF | `surf references/surf.pdf "Markdown, TeX, and PDF"` |
@@ -5,7 +5,7 @@ metadata:
5
5
  github_username: saintx
6
6
  email: alex@saintx.us
7
7
  twitter: alexsaintx
8
- surf-version: "0.7.0"
8
+ surf-version: "0.7.1"
9
9
  ---
10
10
  # Surf — About
11
11
 
@@ -5,7 +5,7 @@ metadata:
5
5
  github_username: saintx
6
6
  email: alex@saintx.us
7
7
  twitter: alexsaintx
8
- surf-version: "0.7.0"
8
+ surf-version: "0.7.1"
9
9
  ---
10
10
  # Surf Usage
11
11
 
@@ -3,14 +3,14 @@
3
3
  "owner": { "name": "Alexander R. Saint Croix", "email": "alex@saintx.us" },
4
4
  "metadata": {
5
5
  "description": "Plugins shipped with the surf CLI.",
6
- "version": "0.7.0"
6
+ "version": "0.7.1"
7
7
  },
8
8
  "plugins": [
9
9
  {
10
10
  "name": "surf",
11
11
  "source": "./plugins/surf",
12
12
  "description": "Extract a markdown heading, TeX section, or PDF outline item without loading the rest of the file.",
13
- "version": "0.7.0",
13
+ "version": "0.7.1",
14
14
  "category": "tooling",
15
15
  "keywords": ["surf", "markdown", "tex", "pdf", "context", "skills"]
16
16
  }
@@ -0,0 +1,53 @@
1
+ # Contributing to surf
2
+
3
+ How to get a working tree, run the tests, and pass lint. The release procedure is in [RELEASING.md](https://github.com/saintx/surf-cli/blob/main/RELEASING.md).
4
+
5
+ ## Environment
6
+
7
+ Python 3.12+ and [uv](https://docs.astral.sh/uv/). Either of these gives a shell with the dev dependencies installed:
8
+
9
+ ```bash
10
+ nix develop # python312 + uv, then uv sync
11
+ uv sync # without Nix; the dev group is installed by default
12
+ ```
13
+
14
+ Run the checkout's `surf` without installing it:
15
+
16
+ ```bash
17
+ uv run surf --version
18
+ ```
19
+
20
+ ## Layout
21
+
22
+ ```
23
+ src/surf/
24
+ models.py data types
25
+ logic.py heading trees, section extraction; pure, no I/O
26
+ adapters.py file reading, TeX expansion, pypdf
27
+ orchestrator.py CLI: argument parsing, dispatch, exit codes
28
+ test_*.py tests, beside the modules they cover
29
+ plugins/surf/ the agent skill and its manifests
30
+ scripts/ test.sh, lint.sh, deploy.sh
31
+ ```
32
+
33
+ The layers only depend downward, and `pypdf` is confined to `adapters.py`. Four `import-linter` contracts in `pyproject.toml` enforce this, and `scripts/lint.sh` runs them.
34
+
35
+ ## Tests
36
+
37
+ ```bash
38
+ scripts/test.sh # uv run pytest src/surf -q
39
+ ```
40
+
41
+ Tests live in `src/surf/test_*.py` and are excluded from the wheel. The root `tests/` directory is unused.
42
+
43
+ ## Lint
44
+
45
+ ```bash
46
+ scripts/lint.sh
47
+ ```
48
+
49
+ Runs, in order: `isort`, `black` (line length 100), `autoflake` (unused imports), `pyright` (basic mode, unused imports are errors), and `lint-imports` against the layer contracts. The first three rewrite files in place; run it before committing.
50
+
51
+ ## Version
52
+
53
+ `surf --version` must agree with the version in every location listed in [RELEASING.md](https://github.com/saintx/surf-cli/blob/main/RELEASING.md). Changes that are not a release do not bump it.
@@ -0,0 +1,272 @@
1
+ Metadata-Version: 2.5
2
+ Name: surf-cli
3
+ Version: 0.7.1
4
+ Summary: List headings, or extract one section by name, from Markdown, TeX, or PDF.
5
+ Project-URL: Homepage, https://github.com/saintx/surf-cli
6
+ Project-URL: Repository, https://github.com/saintx/surf-cli
7
+ Project-URL: Issues, https://github.com/saintx/surf-cli/issues
8
+ Project-URL: Author, https://github.com/saintx
9
+ Author-email: "Alexander R. Saint Croix" <alex@saintx.us>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Alexander R. Saint Croix
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Requires-Python: >=3.12
33
+ Requires-Dist: pypdf>=6.17.0
34
+ Description-Content-Type: text/markdown
35
+
36
+ # surf
37
+
38
+ List headings, or extract one section by name, from Markdown, TeX, or PDF.
39
+
40
+ Distribution `surf-cli`. Command `surf`. Python 3.12+; pypdf is the one runtime dependency, for PDF outlines. Only the section you name is printed, so the rest of the file never enters an agent's context.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ uv tool install surf-cli # or: pipx install surf-cli, or: pip install surf-cli
46
+ surf --version
47
+ ```
48
+
49
+ With Nix:
50
+
51
+ ```bash
52
+ nix profile install github:saintx/surf-cli # add ?ref=X.Y.Z to pin a release
53
+ ```
54
+
55
+ ## Quick start
56
+
57
+ The examples run against the reference files shipped with the agent skill:
58
+
59
+ ```bash
60
+ cd plugins/surf/skills/surf/references
61
+ ```
62
+
63
+ A file with no heading named returns the map: frontmatter, then the heading tree, none of the body.
64
+
65
+ ```
66
+ $ surf about.md
67
+ ---
68
+ metadata:
69
+ author:
70
+ name: Alexander R. Saint Croix
71
+ github_username: saintx
72
+ email: alex@saintx.us
73
+ twitter: alexsaintx
74
+ surf-version: "0.7.1"
75
+ ---
76
+
77
+ - Surf — About
78
+ - Overview
79
+ - When to use
80
+ ```
81
+
82
+ A heading returns that section and stops at the next heading of the same or higher level.
83
+
84
+ ```
85
+ $ surf about.md "When to use"
86
+ ## When to use
87
+
88
+ Invoke when skimming markdown, TeX, or PDF files, checking what a file contains, ...
89
+ ```
90
+
91
+ The same two commands work on TeX, addressed by sectioning commands and the `abstract` environment, and on PDF, addressed by outline bookmarks. `surf.tex` and `surf.pdf` are one paper in two forms and list the same tree from either.
92
+
93
+ ```
94
+ $ surf surf.tex --list
95
+ - abstract
96
+ - Background
97
+ - A Thin Index Shaped by Intent
98
+ - Agentic Context Composition
99
+ - Skills You Can Check
100
+ - Indexes over Indexes
101
+ - Markdown, TeX, and PDF
102
+ - This Paper
103
+
104
+ $ surf surf.tex "Markdown, TeX, and PDF"
105
+ \section{Markdown, TeX, and PDF}
106
+
107
+ surf addresses a markdown file by its ATX headings, a TeX file by its sectioning
108
+ commands and its \texttt{abstract} environment, and a PDF by its outline. ...
109
+
110
+ $ surf surf.pdf "Markdown, TeX, and PDF"
111
+ ```
112
+
113
+ ## Addressing
114
+
115
+ Suppose `guide.md` is:
116
+
117
+ ```markdown
118
+ ---
119
+ title: Guide
120
+ tags: [cli, docs]
121
+ ---
122
+ # Guide
123
+
124
+ Intro paragraph.
125
+
126
+ ## Install
127
+
128
+ ### Usage
129
+
130
+ Install usage text.
131
+
132
+ ## API
133
+
134
+ ### Usage
135
+
136
+ API usage text.
137
+
138
+ #### Examples
139
+
140
+ Deep example.
141
+ ```
142
+
143
+ Its map is:
144
+
145
+ ```
146
+ $ surf guide.md
147
+ ---
148
+ title: Guide
149
+ tags: [cli, docs]
150
+ ---
151
+
152
+ - Guide
153
+ - Install
154
+ - Usage
155
+ - API
156
+ - Usage
157
+ - Examples
158
+ ```
159
+
160
+ **Get one section.** Name the heading. The section runs to the next heading of the same or higher level, so a parent brings its children.
161
+
162
+ ```
163
+ $ surf guide.md "API"
164
+ ## API
165
+
166
+ ### Usage
167
+
168
+ API usage text.
169
+
170
+ #### Examples
171
+
172
+ Deep example.
173
+ ```
174
+
175
+ **Pick between two headings with the same name.** Write the path from the parent. A bare name matches the first occurrence in the file.
176
+
177
+ ```
178
+ $ surf guide.md "API#Usage"
179
+ ### Usage
180
+
181
+ API usage text.
182
+
183
+ #### Examples
184
+
185
+ Deep example.
186
+ ```
187
+
188
+ **List only the top N levels.** `--level N` on a listing shows ranks 1 through N.
189
+
190
+ ```
191
+ $ surf guide.md --list --level 2
192
+ - Guide
193
+ - Install
194
+ - API
195
+ ```
196
+
197
+ **Extract only at an exact level.** `--level N` on an extract matches rank N and nothing else. `Usage` exists only at rank 3, so rank 2 is not found.
198
+
199
+ ```
200
+ $ surf guide.md "Examples" --level 4
201
+ #### Examples
202
+
203
+ Deep example.
204
+
205
+ $ surf guide.md "Usage" --level 2
206
+ Error: heading "Usage" not found in guide.md.
207
+ ```
208
+
209
+ **Use a link as the address.** Obsidian wikilinks and markdown links resolve directly, `#` segments included.
210
+
211
+ ```bash
212
+ surf "[[guide#API#Usage]]"
213
+ surf "[API](guide.md#API)"
214
+ ```
215
+
216
+ **Frontmatter only, or headings only.** `-f` prints the YAML block and nothing else. `--list` prints the tree and no YAML. `--full` prints the YAML and then the named section.
217
+
218
+ ```bash
219
+ surf -f guide.md
220
+ surf --list guide.md
221
+ surf guide.md "Install" --full
222
+ ```
223
+
224
+ **Drop the heading line, or write to a file.**
225
+
226
+ ```bash
227
+ surf guide.md "API" --no-heading
228
+ surf guide.md "API" -o api.md
229
+ ```
230
+
231
+ **What counts as a heading.** On markdown, the ATX heading text. On TeX, the brace title of a sectioning command, plus `abstract` for the abstract environment; `\input` and `\include` of `.tex` files expand relative to the file you name, while graphics, comments, shell pipes, absolute paths, `..` segments, and macro-built paths do not. On PDF, an outline bookmark title; the extract is the page text from that bookmark to the next, and when the next bookmark shares a page, that page is included. Rank 1 is `#` on markdown, the shallowest sectioning command in the TeX file (`\section` in an article), and the outline's top rank on PDF.
232
+
233
+ Matching ignores case. `surf guide` resolves to `guide.md`; `.tex` and `.pdf` need their suffix. Any other UTF-8 file is addressed as ATX markdown.
234
+
235
+ **When there is nothing to address.** A file with no headings, or a PDF with no outline, prints `no structural index` and line and byte counts (page and byte counts on PDF), and exits 0. A heading that is not there exits 1 with `Error: heading "X" not found in guide.md.` A missing file or bad usage exits 2.
236
+
237
+ **Scan a directory.** Frontmatter across many files, no body loaded:
238
+
239
+ ```bash
240
+ ls docs/*.md | xargs -I {} surf -f {}
241
+ ```
242
+
243
+ `surf --help` lists every flag.
244
+
245
+ ## Agent skill
246
+
247
+ `plugins/surf` packages a skill that teaches an agent how to use surf. It carries a Claude Code manifest, an Agent Plugins 1.0.0 manifest, and the skill itself at `plugins/surf/skills/surf`, in the Agent Skills format that Claude Code, Codex, Gemini CLI, Cursor, and Grok Build read. The skill shells out to `surf`, so install the CLI first.
248
+
249
+ Claude Code installs it from the marketplace in this repo:
250
+
251
+ ```bash
252
+ claude plugin marketplace add saintx/surf-cli
253
+ claude plugin install surf@surf-cli
254
+ ```
255
+
256
+ Any harness that reads a skills directory takes a copy:
257
+
258
+ ```bash
259
+ cp -R plugins/surf/skills/surf ~/.agents/skills/surf
260
+ ```
261
+
262
+ This checkout links `.agents/skills/surf`, `.claude/skills/surf`, and `.grok/skills/surf` into the plugin, so an agent working in the repo already has it.
263
+
264
+ The paper in `plugins/surf/skills/surf/references/surf.pdf` (also `surf.tex`) is about what surf can be used to build. surf itself is the primitive.
265
+
266
+ ## Contributing
267
+
268
+ Development setup, tests, and lint are in [CONTRIBUTING.md](https://github.com/saintx/surf-cli/blob/main/CONTRIBUTING.md). The release procedure is in [RELEASING.md](https://github.com/saintx/surf-cli/blob/main/RELEASING.md).
269
+
270
+ ## License
271
+
272
+ [MIT](https://github.com/saintx/surf-cli/blob/main/LICENSE).
@@ -0,0 +1,237 @@
1
+ # surf
2
+
3
+ List headings, or extract one section by name, from Markdown, TeX, or PDF.
4
+
5
+ Distribution `surf-cli`. Command `surf`. Python 3.12+; pypdf is the one runtime dependency, for PDF outlines. Only the section you name is printed, so the rest of the file never enters an agent's context.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ uv tool install surf-cli # or: pipx install surf-cli, or: pip install surf-cli
11
+ surf --version
12
+ ```
13
+
14
+ With Nix:
15
+
16
+ ```bash
17
+ nix profile install github:saintx/surf-cli # add ?ref=X.Y.Z to pin a release
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ The examples run against the reference files shipped with the agent skill:
23
+
24
+ ```bash
25
+ cd plugins/surf/skills/surf/references
26
+ ```
27
+
28
+ A file with no heading named returns the map: frontmatter, then the heading tree, none of the body.
29
+
30
+ ```
31
+ $ surf about.md
32
+ ---
33
+ metadata:
34
+ author:
35
+ name: Alexander R. Saint Croix
36
+ github_username: saintx
37
+ email: alex@saintx.us
38
+ twitter: alexsaintx
39
+ surf-version: "0.7.1"
40
+ ---
41
+
42
+ - Surf — About
43
+ - Overview
44
+ - When to use
45
+ ```
46
+
47
+ A heading returns that section and stops at the next heading of the same or higher level.
48
+
49
+ ```
50
+ $ surf about.md "When to use"
51
+ ## When to use
52
+
53
+ Invoke when skimming markdown, TeX, or PDF files, checking what a file contains, ...
54
+ ```
55
+
56
+ The same two commands work on TeX, addressed by sectioning commands and the `abstract` environment, and on PDF, addressed by outline bookmarks. `surf.tex` and `surf.pdf` are one paper in two forms and list the same tree from either.
57
+
58
+ ```
59
+ $ surf surf.tex --list
60
+ - abstract
61
+ - Background
62
+ - A Thin Index Shaped by Intent
63
+ - Agentic Context Composition
64
+ - Skills You Can Check
65
+ - Indexes over Indexes
66
+ - Markdown, TeX, and PDF
67
+ - This Paper
68
+
69
+ $ surf surf.tex "Markdown, TeX, and PDF"
70
+ \section{Markdown, TeX, and PDF}
71
+
72
+ surf addresses a markdown file by its ATX headings, a TeX file by its sectioning
73
+ commands and its \texttt{abstract} environment, and a PDF by its outline. ...
74
+
75
+ $ surf surf.pdf "Markdown, TeX, and PDF"
76
+ ```
77
+
78
+ ## Addressing
79
+
80
+ Suppose `guide.md` is:
81
+
82
+ ```markdown
83
+ ---
84
+ title: Guide
85
+ tags: [cli, docs]
86
+ ---
87
+ # Guide
88
+
89
+ Intro paragraph.
90
+
91
+ ## Install
92
+
93
+ ### Usage
94
+
95
+ Install usage text.
96
+
97
+ ## API
98
+
99
+ ### Usage
100
+
101
+ API usage text.
102
+
103
+ #### Examples
104
+
105
+ Deep example.
106
+ ```
107
+
108
+ Its map is:
109
+
110
+ ```
111
+ $ surf guide.md
112
+ ---
113
+ title: Guide
114
+ tags: [cli, docs]
115
+ ---
116
+
117
+ - Guide
118
+ - Install
119
+ - Usage
120
+ - API
121
+ - Usage
122
+ - Examples
123
+ ```
124
+
125
+ **Get one section.** Name the heading. The section runs to the next heading of the same or higher level, so a parent brings its children.
126
+
127
+ ```
128
+ $ surf guide.md "API"
129
+ ## API
130
+
131
+ ### Usage
132
+
133
+ API usage text.
134
+
135
+ #### Examples
136
+
137
+ Deep example.
138
+ ```
139
+
140
+ **Pick between two headings with the same name.** Write the path from the parent. A bare name matches the first occurrence in the file.
141
+
142
+ ```
143
+ $ surf guide.md "API#Usage"
144
+ ### Usage
145
+
146
+ API usage text.
147
+
148
+ #### Examples
149
+
150
+ Deep example.
151
+ ```
152
+
153
+ **List only the top N levels.** `--level N` on a listing shows ranks 1 through N.
154
+
155
+ ```
156
+ $ surf guide.md --list --level 2
157
+ - Guide
158
+ - Install
159
+ - API
160
+ ```
161
+
162
+ **Extract only at an exact level.** `--level N` on an extract matches rank N and nothing else. `Usage` exists only at rank 3, so rank 2 is not found.
163
+
164
+ ```
165
+ $ surf guide.md "Examples" --level 4
166
+ #### Examples
167
+
168
+ Deep example.
169
+
170
+ $ surf guide.md "Usage" --level 2
171
+ Error: heading "Usage" not found in guide.md.
172
+ ```
173
+
174
+ **Use a link as the address.** Obsidian wikilinks and markdown links resolve directly, `#` segments included.
175
+
176
+ ```bash
177
+ surf "[[guide#API#Usage]]"
178
+ surf "[API](guide.md#API)"
179
+ ```
180
+
181
+ **Frontmatter only, or headings only.** `-f` prints the YAML block and nothing else. `--list` prints the tree and no YAML. `--full` prints the YAML and then the named section.
182
+
183
+ ```bash
184
+ surf -f guide.md
185
+ surf --list guide.md
186
+ surf guide.md "Install" --full
187
+ ```
188
+
189
+ **Drop the heading line, or write to a file.**
190
+
191
+ ```bash
192
+ surf guide.md "API" --no-heading
193
+ surf guide.md "API" -o api.md
194
+ ```
195
+
196
+ **What counts as a heading.** On markdown, the ATX heading text. On TeX, the brace title of a sectioning command, plus `abstract` for the abstract environment; `\input` and `\include` of `.tex` files expand relative to the file you name, while graphics, comments, shell pipes, absolute paths, `..` segments, and macro-built paths do not. On PDF, an outline bookmark title; the extract is the page text from that bookmark to the next, and when the next bookmark shares a page, that page is included. Rank 1 is `#` on markdown, the shallowest sectioning command in the TeX file (`\section` in an article), and the outline's top rank on PDF.
197
+
198
+ Matching ignores case. `surf guide` resolves to `guide.md`; `.tex` and `.pdf` need their suffix. Any other UTF-8 file is addressed as ATX markdown.
199
+
200
+ **When there is nothing to address.** A file with no headings, or a PDF with no outline, prints `no structural index` and line and byte counts (page and byte counts on PDF), and exits 0. A heading that is not there exits 1 with `Error: heading "X" not found in guide.md.` A missing file or bad usage exits 2.
201
+
202
+ **Scan a directory.** Frontmatter across many files, no body loaded:
203
+
204
+ ```bash
205
+ ls docs/*.md | xargs -I {} surf -f {}
206
+ ```
207
+
208
+ `surf --help` lists every flag.
209
+
210
+ ## Agent skill
211
+
212
+ `plugins/surf` packages a skill that teaches an agent how to use surf. It carries a Claude Code manifest, an Agent Plugins 1.0.0 manifest, and the skill itself at `plugins/surf/skills/surf`, in the Agent Skills format that Claude Code, Codex, Gemini CLI, Cursor, and Grok Build read. The skill shells out to `surf`, so install the CLI first.
213
+
214
+ Claude Code installs it from the marketplace in this repo:
215
+
216
+ ```bash
217
+ claude plugin marketplace add saintx/surf-cli
218
+ claude plugin install surf@surf-cli
219
+ ```
220
+
221
+ Any harness that reads a skills directory takes a copy:
222
+
223
+ ```bash
224
+ cp -R plugins/surf/skills/surf ~/.agents/skills/surf
225
+ ```
226
+
227
+ This checkout links `.agents/skills/surf`, `.claude/skills/surf`, and `.grok/skills/surf` into the plugin, so an agent working in the repo already has it.
228
+
229
+ The paper in `plugins/surf/skills/surf/references/surf.pdf` (also `surf.tex`) is about what surf can be used to build. surf itself is the primitive.
230
+
231
+ ## Contributing
232
+
233
+ Development setup, tests, and lint are in [CONTRIBUTING.md](https://github.com/saintx/surf-cli/blob/main/CONTRIBUTING.md). The release procedure is in [RELEASING.md](https://github.com/saintx/surf-cli/blob/main/RELEASING.md).
234
+
235
+ ## License
236
+
237
+ [MIT](https://github.com/saintx/surf-cli/blob/main/LICENSE).
@@ -55,7 +55,7 @@
55
55
 
56
56
  surf = pythonPkgs.buildPythonApplication {
57
57
  pname = "surf";
58
- version = "0.7.0";
58
+ version = "0.7.1";
59
59
  src = cliSrc;
60
60
  pyproject = true;
61
61
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Extract a markdown heading, TeX section, or PDF outline item without loading the rest of the file. Skill for agents using the surf CLI.",
5
5
  "author": { "name": "Alexander R. Saint Croix", "email": "alex@saintx.us" },
6
6
  "homepage": "https://github.com/saintx/surf-cli",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "surf",
4
- "version": "0.7.0",
4
+ "version": "0.7.1",
5
5
  "description": "Extract a markdown heading, TeX section, or PDF outline item without loading the rest of the file. Skill for agents using the surf CLI.",
6
6
  "author": { "name": "Alexander R. Saint Croix", "email": "alex@saintx.us", "url": "https://github.com/saintx" },
7
7
  "homepage": "https://github.com/saintx/surf-cli",
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "surf-cli"
7
- version = "0.7.0"
8
- description = "Extract markdown, TeX, or PDF outline sections by heading with Obsidian link support"
7
+ version = "0.7.1"
8
+ description = "List headings, or extract one section by name, from Markdown, TeX, or PDF."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
11
11
  authors = [{ name = "Alexander R. Saint Croix", email = "alex@saintx.us" }]
@@ -1,5 +1,5 @@
1
1
  # __init__.py
2
2
  """surf-cli: heading-addressed markdown, TeX, and PDF extract."""
3
3
 
4
- __version__ = "0.7.0"
4
+ __version__ = "0.7.1"
5
5
  __version_tag__ = f"surf-v{__version__}"
@@ -41,6 +41,8 @@ from surf.models import (
41
41
  )
42
42
 
43
43
  _HEADING_RE = re.compile(r"^(#{1,6})\s+(.+)$")
44
+ _FENCE_OPEN_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})(.*)$")
45
+ _FENCE_CLOSE_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})\s*$")
44
46
  _TEX_LEVEL: Mapping[TexCommand, HeadingLevel] = {
45
47
  TexCommand.PART: HeadingLevel(1),
46
48
  TexCommand.CHAPTER: HeadingLevel(2),
@@ -108,9 +110,42 @@ def parse_link(target: CliTarget) -> ParsedLink:
108
110
  return ParsedLink(file_ref=FileRef(stripped) if stripped else None, heading_path=None)
109
111
 
110
112
 
113
+ def _fence_open(line: str) -> str | None:
114
+ """Return the fence string when line opens a fenced code block, else None.
115
+
116
+ CommonMark: up to three spaces of indent, then three or more backticks or
117
+ tildes. A backtick fence's info string may not contain a backtick.
118
+ """
119
+ m = _FENCE_OPEN_RE.match(line)
120
+ if m is None:
121
+ return None
122
+ fence, info = m.group(1), m.group(2)
123
+ if fence[0] == "`" and "`" in info:
124
+ return None
125
+ return fence
126
+
127
+
128
+ def _fence_closes(line: str, open_fence: str) -> bool:
129
+ """A closing fence uses the same character and is at least as long."""
130
+ m = _FENCE_CLOSE_RE.match(line)
131
+ if m is None:
132
+ return False
133
+ fence = m.group(1)
134
+ return fence[0] == open_fence[0] and len(fence) >= len(open_fence)
135
+
136
+
111
137
  def parse_headings(lines: Sequence[str]) -> tuple[HeadingRecord, ...]:
138
+ """ATX headings outside fenced code blocks. A `#` line inside a fence is code."""
112
139
  records: list[HeadingRecord] = []
140
+ open_fence: str | None = None
113
141
  for i, line in enumerate(lines):
142
+ if open_fence is not None:
143
+ if _fence_closes(line, open_fence):
144
+ open_fence = None
145
+ continue
146
+ open_fence = _fence_open(line)
147
+ if open_fence is not None:
148
+ continue
114
149
  m = _HEADING_RE.match(line)
115
150
  if m:
116
151
  records.append(
@@ -537,3 +537,60 @@ def test_format_empty_pdf_index_reports_pages_and_bytes() -> None:
537
537
  "pages: 18",
538
538
  "bytes: 13153200",
539
539
  ]
540
+
541
+
542
+ FENCED_MD = """# Doc
543
+
544
+ ## Real
545
+
546
+ ```markdown
547
+ # Not a heading
548
+ ## Also not
549
+ ```
550
+
551
+ ~~~
552
+ ### Tilde fenced
553
+ ~~~
554
+
555
+ ````md
556
+ ```
557
+ # Nested shorter fence stays open
558
+ ```
559
+ ````
560
+
561
+ ## After
562
+
563
+ # Indented code, not a heading
564
+
565
+ ```
566
+ # Unclosed fence runs to the end
567
+ ## Still code
568
+ """
569
+
570
+
571
+ def test_parse_headings_skips_fenced_code_blocks() -> None:
572
+ headings = parse_headings(FENCED_MD.splitlines())
573
+ assert [str(record.text) for record in headings] == ["Doc", "Real", "After"]
574
+
575
+
576
+ def test_extract_section_keeps_fenced_hash_lines_in_body() -> None:
577
+ lines = FENCED_MD.splitlines()
578
+ result = extract_section(lines, hp("Real"))
579
+ assert result is not None
580
+ body = "\n".join(result.lines)
581
+ assert "# Not a heading" in body
582
+ assert "### Tilde fenced" in body
583
+ assert "# Nested shorter fence stays open" in body
584
+ assert "## After" not in body
585
+
586
+
587
+ def test_backtick_fence_info_string_with_backtick_is_not_a_fence() -> None:
588
+ lines = ["# Top", "``` not `a` fence", "## Heading", "```"]
589
+ headings = parse_headings(lines)
590
+ assert [str(record.text) for record in headings] == ["Top", "Heading"]
591
+
592
+
593
+ def test_closing_fence_must_match_char_and_length() -> None:
594
+ lines = ["# Top", "````", "~~~~", "```", "## Inside", "````", "## Outside"]
595
+ headings = parse_headings(lines)
596
+ assert [str(record.text) for record in headings] == ["Top", "Outside"]
@@ -380,7 +380,7 @@ wheels = [
380
380
 
381
381
  [[package]]
382
382
  name = "surf-cli"
383
- version = "0.7.0"
383
+ version = "0.7.1"
384
384
  source = { editable = "." }
385
385
  dependencies = [
386
386
  { name = "pypdf" },
surf_cli-0.7.0/PKG-INFO DELETED
@@ -1,177 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: surf-cli
3
- Version: 0.7.0
4
- Summary: Extract markdown, TeX, or PDF outline sections by heading with Obsidian link support
5
- Project-URL: Homepage, https://github.com/saintx/surf-cli
6
- Project-URL: Repository, https://github.com/saintx/surf-cli
7
- Project-URL: Issues, https://github.com/saintx/surf-cli/issues
8
- Project-URL: Author, https://github.com/saintx
9
- Author-email: "Alexander R. Saint Croix" <alex@saintx.us>
10
- License: MIT License
11
-
12
- Copyright (c) 2026 Alexander R. Saint Croix
13
-
14
- Permission is hereby granted, free of charge, to any person obtaining a copy
15
- of this software and associated documentation files (the "Software"), to deal
16
- in the Software without restriction, including without limitation the rights
17
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
- copies of the Software, and to permit persons to whom the Software is
19
- furnished to do so, subject to the following conditions:
20
-
21
- The above copyright notice and this permission notice shall be included in all
22
- copies or substantial portions of the Software.
23
-
24
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
- SOFTWARE.
31
- License-File: LICENSE
32
- Requires-Python: >=3.12
33
- Requires-Dist: pypdf>=6.17.0
34
- Description-Content-Type: text/markdown
35
-
36
- # surf
37
-
38
- Extract one section of a markdown, TeX, or PDF file by its heading. The rest of the file never loads.
39
-
40
- ```bash
41
- uv tool install surf-cli
42
- cd plugins/surf/skills/surf/references
43
- ```
44
-
45
- A file with no heading named returns the map, frontmatter and then headings, and none of the body:
46
-
47
- ```
48
- $ surf about.md
49
- ---
50
- metadata:
51
- author:
52
- name: Alexander R. Saint Croix
53
- github_username: saintx
54
- email: alex@saintx.us
55
- twitter: alexsaintx
56
- surf-version: "0.7.0"
57
- ---
58
-
59
- - Surf — About
60
- - Overview
61
- - When to use
62
- ```
63
-
64
- A heading returns that section and stops at the next heading of the same or higher level:
65
-
66
- ```
67
- $ surf about.md "When to use"
68
- ## When to use
69
-
70
- Invoke when skimming markdown, TeX, or PDF files, checking what a file contains,
71
- listing structure, extracting a named address, or batch-scanning metadata across
72
- a directory. See `surf --help` for CLI flags. Skip when the full body is already
73
- needed. ...
74
- ```
75
-
76
- The same two commands work on TeX, addressed by sectioning commands and the `abstract` environment, and on PDF, addressed by outline bookmarks. The whitepaper in this directory ships in both forms and lists the same tree from either:
77
-
78
- ```
79
- $ surf surf.tex --list
80
- - abstract
81
- - Background
82
- - A Thin Index Shaped by Intent
83
- - Agentic Context Composition
84
- - Skills You Can Check
85
- - Indexes over Indexes
86
- - Markdown, TeX, and PDF
87
- - This Paper
88
-
89
- $ surf surf.pdf "Skills You Can Check"
90
- ```
91
-
92
- surf exists so that agent skills can be thin. The `SKILL.md` beside these files is a table of intents, each resolving to `surf path "Heading"`, over reference material the agent never reads whole. `surf.pdf` explains why the tool was built and what that pattern makes possible.
93
-
94
- ## Addressing
95
-
96
- `surf --list file.md` is the heading tree without YAML. `surf -f file.md` is YAML only. Nested paths (`Overview#Usage`) select a child when the same name appears under different parents.
97
-
98
- A heading is an address: ATX display text on markdown, brace title on TeX, the TeX `abstract` environment (addressed as `abstract`), or an outline bookmark title on PDF. On markdown and TeX the return value is that section through the next heading of the same or higher level. On PDF it is dest-to-next-dest page text; when the next dest is on the same page, that dest page is included.
99
-
100
- `--level` is 1 at the top of the heading tree. On markdown that is `#`. On TeX it is the shallowest command in the file, so `--level 1` is `\section` in an article. On PDF it is the outline's native rank. Listing with `--level N` includes ranks 1 through N. Named extract uses N as an exact match.
101
-
102
- TeX `\input` and `\include` of `.tex` files are expanded relative to the file you name. Graphics, comments, shell pipes, and macro-constructed paths are not.
103
-
104
- If the file has no headings, or a PDF has no outline, surf prints that it has no structural index, with line and byte counts on markdown and TeX, or page and byte counts on PDF. It does not dump the body.
105
-
106
- Python 3.12+. pypdf is the runtime dependency for PDF outline addressing.
107
-
108
- ## Agent skill
109
-
110
- `plugins/surf` packages the skill for agents. It carries a Claude Code manifest, an Agent Plugins 1.0.0 manifest, and the skill itself at `plugins/surf/skills/surf`, in the Agent Skills format that Claude Code, Codex, Gemini CLI, Cursor, and Grok Build read.
111
-
112
- Claude Code installs it from the marketplace in this repo:
113
-
114
- ```bash
115
- claude plugin marketplace add saintx/surf-cli
116
- claude plugin install surf@surf-cli
117
- ```
118
-
119
- Any harness that reads a skills directory takes a copy of the skill:
120
-
121
- ```bash
122
- cp -R plugins/surf/skills/surf ~/.agents/skills/surf
123
- ```
124
-
125
- The repo also carries `.agents/skills/surf`, `.claude/skills/surf`, and `.grok/skills/surf` as symlinks into the plugin, so an agent working in this checkout has the skill available.
126
-
127
- ## Install
128
-
129
- The distribution name is `surf-cli`. The command is `surf`. Python 3.12+.
130
-
131
- ```bash
132
- uv tool install surf-cli
133
- surf --version
134
- ```
135
-
136
- ```bash
137
- pipx install surf-cli
138
- surf --version
139
- ```
140
-
141
- ```bash
142
- pip install surf-cli
143
- ```
144
-
145
- ### Nix
146
-
147
- From a local clone, install the flake into the nix profile:
148
-
149
- ```bash
150
- nix profile install "git+file://${PWD}"
151
- surf --version
152
- ```
153
-
154
- Pin a tagged release with `?ref=X.Y.Z`. If `surf` is already in the profile under a different flake URL, remove it first:
155
-
156
- ```bash
157
- nix profile remove surf
158
- nix profile install "git+file://${PWD}?ref=X.Y.Z"
159
- ```
160
-
161
- `scripts/deploy.sh` syncs, tests, and retargets the profile at the latest local semver tag.
162
-
163
- ## Releasing
164
-
165
- Version locations, the bump checklist, tagging, and nix profile deployment are in [RELEASING.md](RELEASING.md).
166
-
167
- ## Development
168
-
169
- ```bash
170
- nix develop
171
- ```
172
-
173
- ## Tests
174
-
175
- ```bash
176
- scripts/test.sh # uv run pytest src/surf
177
- ```
surf_cli-0.7.0/README.md DELETED
@@ -1,142 +0,0 @@
1
- # surf
2
-
3
- Extract one section of a markdown, TeX, or PDF file by its heading. The rest of the file never loads.
4
-
5
- ```bash
6
- uv tool install surf-cli
7
- cd plugins/surf/skills/surf/references
8
- ```
9
-
10
- A file with no heading named returns the map, frontmatter and then headings, and none of the body:
11
-
12
- ```
13
- $ surf about.md
14
- ---
15
- metadata:
16
- author:
17
- name: Alexander R. Saint Croix
18
- github_username: saintx
19
- email: alex@saintx.us
20
- twitter: alexsaintx
21
- surf-version: "0.7.0"
22
- ---
23
-
24
- - Surf — About
25
- - Overview
26
- - When to use
27
- ```
28
-
29
- A heading returns that section and stops at the next heading of the same or higher level:
30
-
31
- ```
32
- $ surf about.md "When to use"
33
- ## When to use
34
-
35
- Invoke when skimming markdown, TeX, or PDF files, checking what a file contains,
36
- listing structure, extracting a named address, or batch-scanning metadata across
37
- a directory. See `surf --help` for CLI flags. Skip when the full body is already
38
- needed. ...
39
- ```
40
-
41
- The same two commands work on TeX, addressed by sectioning commands and the `abstract` environment, and on PDF, addressed by outline bookmarks. The whitepaper in this directory ships in both forms and lists the same tree from either:
42
-
43
- ```
44
- $ surf surf.tex --list
45
- - abstract
46
- - Background
47
- - A Thin Index Shaped by Intent
48
- - Agentic Context Composition
49
- - Skills You Can Check
50
- - Indexes over Indexes
51
- - Markdown, TeX, and PDF
52
- - This Paper
53
-
54
- $ surf surf.pdf "Skills You Can Check"
55
- ```
56
-
57
- surf exists so that agent skills can be thin. The `SKILL.md` beside these files is a table of intents, each resolving to `surf path "Heading"`, over reference material the agent never reads whole. `surf.pdf` explains why the tool was built and what that pattern makes possible.
58
-
59
- ## Addressing
60
-
61
- `surf --list file.md` is the heading tree without YAML. `surf -f file.md` is YAML only. Nested paths (`Overview#Usage`) select a child when the same name appears under different parents.
62
-
63
- A heading is an address: ATX display text on markdown, brace title on TeX, the TeX `abstract` environment (addressed as `abstract`), or an outline bookmark title on PDF. On markdown and TeX the return value is that section through the next heading of the same or higher level. On PDF it is dest-to-next-dest page text; when the next dest is on the same page, that dest page is included.
64
-
65
- `--level` is 1 at the top of the heading tree. On markdown that is `#`. On TeX it is the shallowest command in the file, so `--level 1` is `\section` in an article. On PDF it is the outline's native rank. Listing with `--level N` includes ranks 1 through N. Named extract uses N as an exact match.
66
-
67
- TeX `\input` and `\include` of `.tex` files are expanded relative to the file you name. Graphics, comments, shell pipes, and macro-constructed paths are not.
68
-
69
- If the file has no headings, or a PDF has no outline, surf prints that it has no structural index, with line and byte counts on markdown and TeX, or page and byte counts on PDF. It does not dump the body.
70
-
71
- Python 3.12+. pypdf is the runtime dependency for PDF outline addressing.
72
-
73
- ## Agent skill
74
-
75
- `plugins/surf` packages the skill for agents. It carries a Claude Code manifest, an Agent Plugins 1.0.0 manifest, and the skill itself at `plugins/surf/skills/surf`, in the Agent Skills format that Claude Code, Codex, Gemini CLI, Cursor, and Grok Build read.
76
-
77
- Claude Code installs it from the marketplace in this repo:
78
-
79
- ```bash
80
- claude plugin marketplace add saintx/surf-cli
81
- claude plugin install surf@surf-cli
82
- ```
83
-
84
- Any harness that reads a skills directory takes a copy of the skill:
85
-
86
- ```bash
87
- cp -R plugins/surf/skills/surf ~/.agents/skills/surf
88
- ```
89
-
90
- The repo also carries `.agents/skills/surf`, `.claude/skills/surf`, and `.grok/skills/surf` as symlinks into the plugin, so an agent working in this checkout has the skill available.
91
-
92
- ## Install
93
-
94
- The distribution name is `surf-cli`. The command is `surf`. Python 3.12+.
95
-
96
- ```bash
97
- uv tool install surf-cli
98
- surf --version
99
- ```
100
-
101
- ```bash
102
- pipx install surf-cli
103
- surf --version
104
- ```
105
-
106
- ```bash
107
- pip install surf-cli
108
- ```
109
-
110
- ### Nix
111
-
112
- From a local clone, install the flake into the nix profile:
113
-
114
- ```bash
115
- nix profile install "git+file://${PWD}"
116
- surf --version
117
- ```
118
-
119
- Pin a tagged release with `?ref=X.Y.Z`. If `surf` is already in the profile under a different flake URL, remove it first:
120
-
121
- ```bash
122
- nix profile remove surf
123
- nix profile install "git+file://${PWD}?ref=X.Y.Z"
124
- ```
125
-
126
- `scripts/deploy.sh` syncs, tests, and retargets the profile at the latest local semver tag.
127
-
128
- ## Releasing
129
-
130
- Version locations, the bump checklist, tagging, and nix profile deployment are in [RELEASING.md](RELEASING.md).
131
-
132
- ## Development
133
-
134
- ```bash
135
- nix develop
136
- ```
137
-
138
- ## Tests
139
-
140
- ```bash
141
- scripts/test.sh # uv run pytest src/surf
142
- ```
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes