surf-cli 0.7.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. surf_cli-0.7.0/.agents/skills/surf/SKILL.md +60 -0
  2. surf_cli-0.7.0/.agents/skills/surf/references/about.md +18 -0
  3. surf_cli-0.7.0/.agents/skills/surf/references/surf.pdf +0 -0
  4. surf_cli-0.7.0/.agents/skills/surf/references/surf.tex +298 -0
  5. surf_cli-0.7.0/.agents/skills/surf/references/usage.md +136 -0
  6. surf_cli-0.7.0/.claude-plugin/marketplace.json +18 -0
  7. surf_cli-0.7.0/.github/workflows/publish.yml +47 -0
  8. surf_cli-0.7.0/.gitignore +16 -0
  9. surf_cli-0.7.0/.grok-plugin/marketplace.json +15 -0
  10. surf_cli-0.7.0/LICENSE +21 -0
  11. surf_cli-0.7.0/PKG-INFO +177 -0
  12. surf_cli-0.7.0/README.md +142 -0
  13. surf_cli-0.7.0/RELEASING.md +56 -0
  14. surf_cli-0.7.0/flake.lock +61 -0
  15. surf_cli-0.7.0/flake.nix +160 -0
  16. surf_cli-0.7.0/plugins/surf/.claude-plugin/plugin.json +10 -0
  17. surf_cli-0.7.0/plugins/surf/plugin.json +11 -0
  18. surf_cli-0.7.0/pyproject.toml +87 -0
  19. surf_cli-0.7.0/scripts/deploy.sh +112 -0
  20. surf_cli-0.7.0/scripts/lint.sh +9 -0
  21. surf_cli-0.7.0/scripts/test.sh +4 -0
  22. surf_cli-0.7.0/src/surf/__init__.py +5 -0
  23. surf_cli-0.7.0/src/surf/__main__.py +7 -0
  24. surf_cli-0.7.0/src/surf/adapters.py +174 -0
  25. surf_cli-0.7.0/src/surf/logic.py +557 -0
  26. surf_cli-0.7.0/src/surf/models.py +148 -0
  27. surf_cli-0.7.0/src/surf/orchestrator.py +300 -0
  28. surf_cli-0.7.0/src/surf/test_adapters.py +218 -0
  29. surf_cli-0.7.0/src/surf/test_extract_section_perf_smoke.py +73 -0
  30. surf_cli-0.7.0/src/surf/test_logic.py +539 -0
  31. surf_cli-0.7.0/src/surf/test_models.py +30 -0
  32. surf_cli-0.7.0/src/surf/test_orchestrator.py +692 -0
  33. surf_cli-0.7.0/src/surf/test_tex_logic.py +535 -0
  34. surf_cli-0.7.0/uv.lock +419 -0
@@ -0,0 +1,60 @@
1
+ ---
2
+ description: Use instead of Read to skim markdown, TeX, or PDF files. Progressive
3
+ context disclosure by heading or outline. See `surf --help` for usage.
4
+ name: surf
5
+ metadata:
6
+ author:
7
+ name: Alexander R. Saint Croix
8
+ github_username: saintx
9
+ email: alex@saintx.us
10
+ twitter: alexsaintx
11
+ surf-version: "0.7.0"
12
+ ---
13
+
14
+ # Surf: Progressive Context Disclosure
15
+
16
+ Extract a markdown heading, TeX section, or PDF outline item via `surf` without loading the rest of the file. Default `surf ~/path/to/file.md` (no heading) is the file map, not the body.
17
+
18
+ ## Load units (section routing)
19
+
20
+ Resolve with `surf` against the section heading; do not bulk-ingest a whole reference for one fact.
21
+
22
+ ### About / orientation
23
+
24
+ | Intent | Section extraction |
25
+ |--------|--------------------|
26
+ | Overview | `surf references/about.md "Overview"` |
27
+ | When to use | `surf references/about.md "When to use"` |
28
+
29
+ ### Usage
30
+
31
+ | Intent | Section extraction |
32
+ |--------|--------------------|
33
+ | Default: file map | `surf references/usage.md "Default: file map"` |
34
+ | Frontmatter only | `surf references/usage.md "Frontmatter only"` |
35
+ | Heading tree only | `surf references/usage.md "Heading tree only"` |
36
+ | Targeted section | `surf references/usage.md "Targeted section"` |
37
+ | TeX | `surf references/usage.md "TeX"` |
38
+ | PDF | `surf references/usage.md "PDF"` |
39
+ | Level | `surf references/usage.md "Level"` |
40
+ | Wikilink and link input | `surf references/usage.md "Wikilink and link input"` |
41
+ | Batch scanning | `surf references/usage.md "Batch scanning"` |
42
+ | Platform aggregation (batch section extraction) | `surf references/usage.md "Platform aggregation (batch section extraction)"` |
43
+ | Additional options | `surf references/usage.md "Additional options"` |
44
+
45
+ ### Whitepaper (TeX and PDF examples)
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.
48
+
49
+ | Intent | Section extraction |
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"` |
@@ -0,0 +1,18 @@
1
+ ---
2
+ metadata:
3
+ author:
4
+ name: Alexander R. Saint Croix
5
+ github_username: saintx
6
+ email: alex@saintx.us
7
+ twitter: alexsaintx
8
+ surf-version: "0.7.0"
9
+ ---
10
+ # Surf — About
11
+
12
+ ## Overview
13
+
14
+ Progressive context disclosure — extract a heading, TeX section, or PDF outline item without loading the rest of the file. Default `surf ~/path/to/file.md` (no heading) is the file map: YAML (if present) plus the heading list, not the body. Same default on `.tex` and `.pdf`: the heading or outline list, or `no structural index` when there is none. Then `-f`, `--list`, or a heading for one index or one section. Wikilink resolution, batch metadata scanning, and batch section extraction across a directory. Use instead of Read to skim markdown, TeX, or PDF files.
15
+
16
+ ## When to use
17
+
18
+ Invoke when skimming markdown, TeX, or PDF files, checking what a file contains, listing structure, extracting a named address, or batch-scanning metadata across a directory. See `surf --help` for CLI flags. Skip when the full body is already needed. Do not skip `.tex` or `.pdf`. Other existing UTF-8 files are addressed as ATX markdown. Do not use `surf ~/path/to/file.md` with no heading as a substitute for Read.
@@ -0,0 +1,298 @@
1
+ \documentclass[11pt]{article}
2
+
3
+ \usepackage[margin=1.1in]{geometry}
4
+ \usepackage[T1]{fontenc}
5
+ \usepackage{lmodern}
6
+ \usepackage{microtype}
7
+ \usepackage{booktabs}
8
+ \usepackage{fancyvrb}
9
+ \usepackage{upquote}
10
+ \usepackage[hidelinks,bookmarks=true,bookmarksopen=true,bookmarksdepth=3]{hyperref}
11
+
12
+ \fvset{fontsize=\small,frame=none,xleftmargin=1.5em}
13
+
14
+ \title{\normalsize\textsc{Whitepaper}\\[0.6em]
15
+ \LARGE surf: Context Traversal without Ingestion\thanks{Marc McCahill is
16
+ credited with coining the phrase ``surfing the Internet.'' This tool lets agents
17
+ surf the seas of context.}}
18
+ \author{Alexander R. Saint Croix\\\texttt{alex@saintx.us}}
19
+ \date{September 2026}
20
+
21
+ \begin{document}
22
+
23
+ \maketitle
24
+
25
+ \pdfbookmark[1]{abstract}{abstract}
26
+ \begin{abstract}
27
+ surf is a command-line tool that returns one section of a markdown, TeX, or PDF
28
+ document by heading, and returns the document's heading tree when no heading is
29
+ given. I built it so that agent skills can be written as thin indexes: a
30
+ \texttt{SKILL.md} that lists the intents an agent might arrive with and, for each
31
+ one, a single command that pulls the exact section of reference material that
32
+ intent needs. The reference corpus can then grow without the index changing, an
33
+ agent loads only what its task requires, and the relationship between index and
34
+ corpus can be checked by machine. The surf skill ships this paper as its TeX and
35
+ PDF example, so every command it describes can be run against it.
36
+ \end{abstract}
37
+
38
+ \section{Background}
39
+
40
+ Three approaches to giving a language model context have a defect in common.
41
+ Each tends to hand the model more than the task needs and leave it to sort out
42
+ which part applies.
43
+
44
+ Retrieval-augmented generation (RAG) selects chunks of a corpus by their
45
+ resemblance to a query and places them in the prompt. Resemblance to a query and
46
+ relevance to the agent's task are different measures, and RAG can compute only
47
+ the first. The model receives passages that look like the question and then
48
+ spends attention deciding which of them bear on the work.
49
+
50
+ Long instruction files are the second approach. A \texttt{CLAUDE.md} or
51
+ \texttt{AGENTS.md} loads on every turn, and a file that has grown to cover every
52
+ situation a project might present is, on any given turn, mostly about other
53
+ situations. The model attends over all of it to find the part that applies.
54
+
55
+ Skills are the third, and they improve on this by adding agency. A skill loads
56
+ when the agent decides it applies, or when a person invokes it directly, so the
57
+ decision to bring material into context moves from a RAG pipeline or a static
58
+ file to the agent itself. But the decision stops at the skill boundary. Once
59
+ invoked, the entire \texttt{SKILL.md} lands in context, and a long one carries
60
+ the same situational content a long instruction file does. The agency costs the
61
+ whole file.
62
+
63
+ In January 2026 I kept some skill reference material as JSON and wrote the rows
64
+ of a \texttt{SKILL.md} as \texttt{jq} queries against those files. The agent ran
65
+ the query it needed and received the value. The file itself never entered
66
+ context. I call this context traversal without ingestion.
67
+
68
+ Most of my reference material is structured prose, though. A markdown file
69
+ carries a heading tree the way a JSON file carries keys, and what it lacked was a
70
+ command-line utility that would list that tree for an agent or return the prose
71
+ under one path through it. I wrote surf in March 2026 to be that utility. It
72
+ addresses a section by its heading and returns the section. Run against the
73
+ usage reference that ships with the surf skill:
74
+
75
+ \begin{Verbatim}
76
+ $ surf references/usage.md --list
77
+ - Surf Usage
78
+ - Default: file map
79
+ - Frontmatter only
80
+ - Heading tree only
81
+ - Targeted section
82
+ - TeX
83
+ - PDF
84
+ - Level
85
+ - Wikilink and link input
86
+ - Batch scanning
87
+ - Platform aggregation (batch section extraction)
88
+ - Additional options
89
+
90
+ $ surf references/usage.md "Level"
91
+ ## Level
92
+
93
+ `--level 1` is the top rank. On markdown, rank 1 is `#`. A file whose
94
+ first heading is `##` needs `--level 2` to list it. On TeX, rank 1 is
95
+ the shallowest command in that file. In an article that starts at
96
+ `\section`, `--level 1` is `\section`. On PDF, rank 1 is the top
97
+ outline rank.
98
+
99
+ `--list --level N` prints ranks 1 through N. A named heading with
100
+ `--level N` matches only a heading at rank N.
101
+ \end{Verbatim}
102
+
103
+ The agent has seen twelve lines of headings and one section from the middle of
104
+ the file, and it has not opened the file. By June I was building skill indexes
105
+ shaped by intent on top of this, and in September surf gained TeX sectioning
106
+ commands and PDF outlines so that the same skills could index academic papers.
107
+
108
+ \section{A Thin Index Shaped by Intent}
109
+
110
+ A skill built on surf has two parts. The \texttt{SKILL.md} is a short file: one
111
+ sentence stating what the skill does, followed by a table grouped by the intents
112
+ an agent might have on arrival. Each row pairs an intent with one command that
113
+ resolves to a section of a reference file by its heading.
114
+
115
+ \begin{Verbatim}
116
+ | Intent | Section extraction |
117
+ |-----------------------------|-----------------------------------------------|
118
+ | See what a file contains | surf references/usage.md "Default: file map" |
119
+ | Pull one section | surf references/usage.md "Targeted section" |
120
+ | Address a TeX paper | surf references/usage.md "TeX" |
121
+ \end{Verbatim}
122
+
123
+ Those three rows come from the surf skill's own index, and each resolves
124
+ against the \texttt{usage.md} that ships beside this paper.
125
+
126
+ The \texttt{references/} directory holds the corpus. I break reference files
127
+ into sections of twenty to eighty lines and write each so that an agent arriving
128
+ at it with no surrounding context can act from it alone.
129
+
130
+ The two parts have different jobs and change for different reasons. The index
131
+ is intensional. It defines, by intent, which sections matter. The corpus is
132
+ extensional. It holds the sections themselves. Datalog makes the same split
133
+ between an intensional database of rules and an extensional database of ground
134
+ facts \cite{ceri1989}, and the properties carry over. A row in the index holds a
135
+ query, never a copy of the content, and the query resolves against the corpus at
136
+ the moment the agent runs it. So the corpus can be rewritten or extended without
137
+ touching the index, and the index can be regrouped by intent without touching
138
+ the corpus. The only thing both sides depend on is the heading tree. The index
139
+ changes when the structure of the corpus changes, and at no other time.
140
+
141
+ A surf call with no heading returns that structure. \texttt{surf file.md}
142
+ prints the file's frontmatter and then its heading tree, and nothing from the
143
+ body. An agent's first move on an unfamiliar file is to look at the map, and its
144
+ second is to select a section. Neither move loads the file.
145
+
146
+ \section{Agentic Context Composition}
147
+
148
+ The question that separates this pattern from RAG is who decides what enters
149
+ the context window. In RAG a component outside the model decides, before the
150
+ model has seen anything. In a skill built on surf the model reads the intent
151
+ index, judges what its current task needs, and issues the pulls itself. Agent
152
+ inference, rather than a similarity score, composes the context.
153
+
154
+ Invocation loads the index and nothing else. This holds whether the agent chose
155
+ the skill or a person invoked it by name. When I tell an agent to surf my
156
+ holdings for papers on a topic, I have forced the skill to load, and I have
157
+ forced only the index. Which papers it lists, which sections it pulls, and in
158
+ what order remain the agent's decisions against the task I gave it.
159
+
160
+ The cost structure follows from this. In a long \texttt{SKILL.md}, presenting an
161
+ option to the agent costs the full text behind that option, whether or not the
162
+ agent takes it. In a thin index, presenting an option costs one row, and taking
163
+ it costs one section. A skill can therefore lay out every route it supports, and
164
+ an agent that needs one route pays for one section while an agent that needs
165
+ none pays for a table.
166
+
167
+ Models perform worse as the volume of irrelevant material in the context window
168
+ grows, even when the material they need is present \cite{liu2024,hong2025}.
169
+ Every token the model attends to that has no bearing on the task is a token of
170
+ inference spent on sorting rather than on working. A skill that hands the agent
171
+ only the sections it asked for spends that inference on the work.
172
+
173
+ \section{Skills You Can Check}
174
+ \label{sec:check}
175
+
176
+ Because the index is data, a script can verify the relationship between index
177
+ and corpus.
178
+
179
+ Every row is an executable command. After any edit to a skill, a script can run
180
+ every listed command and confirm that each resolves to a section. A row that
181
+ fails is a defect, and the most common cause is a renamed heading the index did
182
+ not follow. Renaming a heading is a schema change under this pattern, and the
183
+ check treats it as one.
184
+
185
+ The reverse check is more interesting. A script can list every section in the
186
+ corpus and count how many index rows reach each one. A section reached zero
187
+ times is a finding, and the count says which questions to ask. The index may
188
+ have fallen behind the corpus and be missing a row. The section may be content
189
+ nobody routes to, and belong trimmed or folded into a neighbor. The corpus may
190
+ have outgrown one index and need a second. Or the file may already be
191
+ structured for navigation by intent, so that an agent told to surf its heading
192
+ tree directly needs no row for it at all. Skills written as prose cannot be
193
+ audited this way, because there is nothing to count.
194
+
195
+ Two conventions make the checks reliable. The index quotes headings literally
196
+ from \texttt{surf -{}-list}, macros and punctuation included, so it never guesses
197
+ at an address. And structural divisions use real headings rather than bold text,
198
+ because surf sees the first and cannot see the second.
199
+
200
+ \section{Indexes over Indexes}
201
+
202
+ An index lists what a corpus contains. A procedure, a numbered list of steps,
203
+ says what to do with it. Most of what a skill carries is the first kind, and a
204
+ thin index hands the agent that inventory without ordering it. The order comes
205
+ from the task.
206
+
207
+ Indexes compose, and composing them is cheaper than enlarging one. A family of
208
+ twenty skills with forty sections each is eight hundred rows in a single index,
209
+ all of them loaded on every invocation to use one. Split into a top index that
210
+ routes by broad intent to member skills and a member index in each that routes
211
+ by narrower intent to its sections, the same destination costs twenty rows and
212
+ then forty, and the agent sees no others. A row in one skill's index may also
213
+ point at a section in another skill's corpus, so shared reference material lives
214
+ in one place and every skill that needs it routes there. The agent descends as
215
+ far as the task requires and at each level pays for one small table, never for
216
+ the corpora beneath it. A family grows by adding a member index and one row
217
+ above it.
218
+
219
+ \section{Markdown, TeX, and PDF}
220
+
221
+ surf addresses a markdown file by its ATX headings, a TeX file by its sectioning
222
+ commands and its \texttt{abstract} environment, and a PDF by its outline. The
223
+ agent's two moves are the same in each case: list the section titles, then
224
+ select the one to read.
225
+
226
+ \begin{Verbatim}
227
+ surf paper.tex --list --level 2
228
+ surf paper.tex "abstract"
229
+ surf paper.pdf "Experiments#Programming"
230
+ \end{Verbatim}
231
+
232
+ A markdown wiki adds a second kind of structure. In an Obsidian vault, a
233
+ section's body names other sections by link, \texttt{[[note\#Heading]]} or
234
+ \texttt{[[note\#Parent\#Child]]}, and surf accepts that link text as a target.
235
+ An agent reading one section follows a link with the same command it used to
236
+ arrive, so the vault is traversed one section at a time along the links its
237
+ authors wrote, and no note is loaded whole.
238
+
239
+ \begin{Verbatim}
240
+ surf "[[agents/memory#Episodic buffer]]"
241
+ \end{Verbatim}
242
+
243
+ A paper already has the structure the pattern needs, maintained by its authors.
244
+ One skill can therefore index a family of related papers at subsection
245
+ granularity, with rows that resolve to one experiment section of one paper. An
246
+ agent working the topic reads the sections its question touches across the
247
+ family and nothing else.
248
+
249
+ Shared structure across a corpus goes further. Most papers have an abstract, so
250
+ a loop over a directory of papers that runs \texttt{surf \$f "abstract"}
251
+ projects that one section out of most of the collection. If every skill in a
252
+ library keeps a \texttt{references/about.md} with the headings
253
+ \texttt{Overview} and \texttt{When to use}, two commands over the library return
254
+ a digest of every skill for a few hundred tokens each. Mine does, and the digest
255
+ is how I orient an agent to some eighty skills without loading one of them.
256
+ Frontmatter, read with \texttt{surf -f}, filters the collection to the files
257
+ that share a structure before the projection runs. A heading convention, seen
258
+ this way, is a schema declaration, and a corpus that follows one can be queried
259
+ like a table. In every format the address is the text that
260
+ \texttt{surf -{}-list} prints for the heading.
261
+
262
+ \section{This Paper}
263
+
264
+ The surf skill needs a TeX file and a PDF that it may ship, and this paper is
265
+ both. The commands below run against it as distributed.
266
+
267
+ \begin{Verbatim}
268
+ surf surf.tex --list
269
+ surf surf.pdf --list
270
+ surf surf.tex "abstract"
271
+ surf surf.pdf "Skills You Can Check"
272
+ \end{Verbatim}
273
+
274
+ The two listings return the same tree. The skill's build checks that claim
275
+ mechanically, in the way Section~\ref{sec:check} describes, each time this file
276
+ changes.
277
+
278
+ \begin{thebibliography}{9}
279
+
280
+ \bibitem{ceri1989}
281
+ S.~Ceri, G.~Gottlob, and L.~Tanca.
282
+ What you always wanted to know about Datalog (and never dared to ask).
283
+ \emph{IEEE Transactions on Knowledge and Data Engineering}, 1(1):146--166, 1989.
284
+
285
+ \bibitem{liu2024}
286
+ N.~F. Liu, K.~Lin, J.~Hewitt, A.~Paranjape, M.~Bevilacqua, F.~Petroni, and
287
+ P.~Liang.
288
+ Lost in the middle: How language models use long contexts.
289
+ \emph{Transactions of the Association for Computational Linguistics}, 12:157--173, 2024.
290
+
291
+ \bibitem{hong2025}
292
+ K.~Hong, A.~Troynikov, and J.~Huber.
293
+ Context rot: How increasing input tokens impacts LLM performance.
294
+ Chroma technical report, July 2025.
295
+
296
+ \end{thebibliography}
297
+
298
+ \end{document}
@@ -0,0 +1,136 @@
1
+ ---
2
+ metadata:
3
+ author:
4
+ name: Alexander R. Saint Croix
5
+ github_username: saintx
6
+ email: alex@saintx.us
7
+ twitter: alexsaintx
8
+ surf-version: "0.7.0"
9
+ ---
10
+ # Surf Usage
11
+
12
+ `surf` extracts a markdown heading, a TeX section, or a PDF outline item.
13
+
14
+ Name a heading to get that section. `--full` with a named heading prints YAML frontmatter, then that section. `--full` without a heading is the file map. Neither form prints the whole file.
15
+
16
+ ## Default: file map
17
+
18
+ ```bash
19
+ surf ~/path/to/file.md
20
+ ```
21
+
22
+ No heading. This is the file map: YAML frontmatter if present, then the heading list. It does not print the body.
23
+
24
+ If there is no frontmatter, you get the heading list only. If there are no headings and no YAML, you get `no structural index` and a size line. Markdown and TeX print lines and bytes. PDF prints pages and bytes. The same command on a `.tex` or `.pdf` file prints that file's heading or outline list, not the body.
25
+
26
+ `surf` with no file path exits 2. It prints `Error: no target specified. Use surf --help for usage.`
27
+
28
+ ## Frontmatter only
29
+
30
+ ```bash
31
+ surf -f ~/path/to/file.md
32
+ ```
33
+
34
+ Returns YAML frontmatter. No body. No heading list. Use this to read YAML metadata. PDF has no YAML, so this prints nothing.
35
+
36
+ ## Heading tree only
37
+
38
+ ```bash
39
+ surf --list ~/path/to/file.md
40
+ ```
41
+
42
+ Returns the heading hierarchy. No YAML. Use this when you want headings and not frontmatter.
43
+
44
+ ## Targeted section
45
+
46
+ ```bash
47
+ surf ~/path/to/file.md "<heading text>"
48
+ ```
49
+
50
+ Returns content under the matched heading, down to the next same-or-higher-level heading. Nested paths (`Parent#Child`) select a child inside that parent.
51
+
52
+ On markdown, the address is the heading text after the `#` marks. On TeX, the address is the title in braces. The TeX `abstract` environment is addressed as `abstract`. On PDF, the address is the outline bookmark title.
53
+
54
+ Heading match ignores case.
55
+
56
+ ## TeX
57
+
58
+ ```bash
59
+ surf ~/path/to/file.tex
60
+ surf --list ~/path/to/file.tex
61
+ surf ~/path/to/file.tex "Heading"
62
+ ```
63
+
64
+ Address is the brace title. The `abstract` environment is addressed as `abstract`. `\input` and `\include` expand `.tex` files relative to the file you named. They do not expand graphics, comments, shell pipes, or paths built by macros. They also skip absolute paths, `..` path segments, and files that are not `.tex`.
65
+
66
+ ## PDF
67
+
68
+ ```bash
69
+ surf ~/path/to/file.pdf
70
+ surf --list ~/path/to/file.pdf
71
+ surf ~/path/to/file.pdf "Heading"
72
+ ```
73
+
74
+ Address is the outline bookmark title. The extract is the page text from this bookmark to the next bookmark. If the next bookmark is on the same page, that page is included. Listing the outline does not print page text.
75
+
76
+ ## Level
77
+
78
+ `--level 1` is the top rank. On markdown, rank 1 is `#`. A file whose first heading is `##` needs `--level 2` to list it. On TeX, rank 1 is the shallowest command in that file. In an article that starts at `\section`, `--level 1` is `\section`. On PDF, rank 1 is the top outline rank.
79
+
80
+ `--list --level N` prints ranks 1 through N. A named heading with `--level N` matches only a heading at rank N.
81
+
82
+ ## Wikilink and link input
83
+
84
+ `surf` accepts wikilinks and markdown links as targets:
85
+
86
+ ```bash
87
+ surf "[[path/to/file]]"
88
+ surf "[[path/to/file#Heading]]"
89
+ ```
90
+
91
+ ```bash
92
+ surf "[Text](path/to/file.md#Heading)"
93
+ ```
94
+
95
+ A wikilink with no `#Heading` is the file map, not the full page. A wikilink with `#Heading` extracts that section. A markdown link with a `#Heading` extracts that section. A markdown link with no `#Heading` is the file map.
96
+
97
+ The `.md` extension is implicit. `surf ~/path/to/file` resolves to `~/path/to/file.md`. `.tex` and `.pdf` need the suffix.
98
+
99
+ ## Batch scanning
100
+
101
+ Scan YAML across a directory without loading any body:
102
+
103
+ ```bash
104
+ ls ~/path/to/*.md | xargs -I {} surf -f {}
105
+ ```
106
+
107
+ Same pattern on nested trees:
108
+
109
+ ```bash
110
+ ls ~/path/to/*/file.md | xargs -I {} surf -f {}
111
+ ```
112
+
113
+ ## Platform aggregation (batch section extraction)
114
+
115
+ Print the path, then one heading, for each matching file:
116
+
117
+ ```bash
118
+ find ~/path/to -path '*/subdir/file.md' | sort \
119
+ | xargs -I{} sh -c 'echo "### {}" && surf {} "Some Heading" && echo'
120
+ ```
121
+
122
+ Change the `find` root, the path glob, and the heading text to match the files you have.
123
+
124
+ ## Additional options
125
+
126
+ | Flag | Effect |
127
+ |------|--------|
128
+ | no heading | File map (YAML if any, then headings or outline). Not the body. |
129
+ | heading, no flag | Section content without frontmatter |
130
+ | `--list` | Heading tree only |
131
+ | `-f` / `--frontmatter-only` | YAML only |
132
+ | `--full` | With a named heading: YAML frontmatter, then that section. Without a heading: the file map. |
133
+ | `--content-only` / `--body-only` | Same as a named heading with no flag. This is the default extract. |
134
+ | `--level N` | List: ranks 1 through N. Extract: exact rank N. |
135
+ | `--no-heading` | Omit the heading line from section output |
136
+ | `-o <file>` | Write output to a file. |
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "surf-cli",
3
+ "owner": { "name": "Alexander R. Saint Croix", "email": "alex@saintx.us" },
4
+ "metadata": {
5
+ "description": "Plugins shipped with the surf CLI.",
6
+ "version": "0.7.0"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "surf",
11
+ "source": "./plugins/surf",
12
+ "description": "Extract a markdown heading, TeX section, or PDF outline item without loading the rest of the file.",
13
+ "version": "0.7.0",
14
+ "category": "tooling",
15
+ "keywords": ["surf", "markdown", "tex", "pdf", "context", "skills"]
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,47 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "*.*.*"
7
+
8
+ jobs:
9
+ build:
10
+ name: Build
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+ with:
17
+ persist-credentials: false
18
+ - uses: astral-sh/setup-uv@v6
19
+ with:
20
+ python-version: "3.12"
21
+ enable-cache: true
22
+ - name: Test
23
+ run: uv run --frozen pytest src/surf -q
24
+ - name: Build
25
+ run: uv build --no-sources
26
+ - uses: actions/upload-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+
31
+ publish:
32
+ name: Publish
33
+ needs: build
34
+ if: github.ref_type == 'tag' && !startsWith(github.ref_name, 'v')
35
+ runs-on: ubuntu-latest
36
+ environment:
37
+ name: pypi
38
+ url: https://pypi.org/p/surf-cli
39
+ permissions:
40
+ id-token: write
41
+ contents: read
42
+ steps:
43
+ - uses: actions/download-artifact@v4
44
+ with:
45
+ name: dist
46
+ path: dist/
47
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,16 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ .venv/
5
+ .pytest_cache/
6
+ dist/
7
+
8
+ # Nix
9
+ result
10
+
11
+ # IDE
12
+ .idea/
13
+ .vscode/
14
+
15
+ # OS
16
+ .DS_Store
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "surf-cli",
3
+ "description": "Plugins shipped with the surf CLI.",
4
+ "owner": { "name": "Alexander R. Saint Croix" },
5
+ "plugins": [
6
+ {
7
+ "name": "surf",
8
+ "description": "Extract a markdown heading, TeX section, or PDF outline item without loading the rest of the file.",
9
+ "category": "tooling",
10
+ "source": "./plugins/surf",
11
+ "homepage": "https://github.com/saintx/surf-cli",
12
+ "keywords": ["surf", "markdown", "tex", "pdf", "context", "skills"]
13
+ }
14
+ ]
15
+ }
surf_cli-0.7.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexander R. Saint Croix
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.