nibs 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nibs-0.1.0/.coderabbit.yaml +8 -0
- nibs-0.1.0/.github/dependabot.yml +18 -0
- nibs-0.1.0/.github/release-please-config.json +11 -0
- nibs-0.1.0/.github/release-please-manifest.json +1 -0
- nibs-0.1.0/.github/workflows/release.yml +148 -0
- nibs-0.1.0/.github/workflows/test.yml +54 -0
- nibs-0.1.0/.gitignore +8 -0
- nibs-0.1.0/CHANGELOG.md +17 -0
- nibs-0.1.0/CONTRIBUTING.md +29 -0
- nibs-0.1.0/LICENSE +21 -0
- nibs-0.1.0/PKG-INFO +93 -0
- nibs-0.1.0/README.md +76 -0
- nibs-0.1.0/pyproject.toml +38 -0
- nibs-0.1.0/src/nibs/__init__.py +6 -0
- nibs-0.1.0/src/nibs/__main__.py +5 -0
- nibs-0.1.0/src/nibs/cache.py +48 -0
- nibs-0.1.0/src/nibs/cli.py +125 -0
- nibs-0.1.0/src/nibs/page.py +223 -0
- nibs-0.1.0/src/nibs/paths.py +14 -0
- nibs-0.1.0/src/nibs/usage.py +48 -0
- nibs-0.1.0/tests/conftest.py +26 -0
- nibs-0.1.0/tests/test_cli.py +223 -0
- nibs-0.1.0/tests/test_encoding.py +34 -0
- nibs-0.1.0/tests/test_fetch.py +84 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://www.coderabbit.ai/integrations/schema.v2.json
|
|
2
|
+
# Settings not listed here keep CodeRabbit's defaults (reviews.profile: chill).
|
|
3
|
+
reviews:
|
|
4
|
+
auto_review:
|
|
5
|
+
# The release pull request that release.yml opens changes only CHANGELOG.md
|
|
6
|
+
# and the release manifest, both generated.
|
|
7
|
+
ignore_title_keywords:
|
|
8
|
+
- 'chore(main): release'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# nibs has no dependencies to install; only the actions in the workflows.
|
|
4
|
+
- package-ecosystem: github-actions
|
|
5
|
+
directory: /
|
|
6
|
+
schedule:
|
|
7
|
+
interval: weekly
|
|
8
|
+
# Wait three days after a release: compromised releases are usually found
|
|
9
|
+
# and removed within hours or days. Three days is also Dependabot's default.
|
|
10
|
+
cooldown:
|
|
11
|
+
default-days: 3
|
|
12
|
+
# Majors come one by one: they can break the workflow.
|
|
13
|
+
groups:
|
|
14
|
+
actions:
|
|
15
|
+
patterns: ['*']
|
|
16
|
+
update-types: [minor, patch]
|
|
17
|
+
commit-message:
|
|
18
|
+
prefix: ci
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
|
+
"packages": {
|
|
4
|
+
".": {
|
|
5
|
+
"release-type": "python",
|
|
6
|
+
"include-component-in-tag": false,
|
|
7
|
+
"bump-minor-pre-major": true,
|
|
8
|
+
"initial-version": "0.1.0"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{".":"0.1.0"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Merging a pull request that bumps the version (feat:, fix:, a breaking change;
|
|
4
|
+
# see CONTRIBUTING.md) makes release-please open or update a release pull
|
|
5
|
+
# request, which changes CHANGELOG.md and .github/release-please-manifest.json.
|
|
6
|
+
# Merging that tags v<version> and creates the GitHub release; the jobs below
|
|
7
|
+
# then build, test and publish it to PyPI. The version of the package comes
|
|
8
|
+
# from the tag. Nothing is published from a laptop.
|
|
9
|
+
#
|
|
10
|
+
# Run by hand (Run workflow on main, with the tag) to publish a tag again after
|
|
11
|
+
# a failure outside the tagged code, such as the PyPI settings. If the tagged
|
|
12
|
+
# code is at fault, release a new version instead.
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
branches: [main]
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
inputs:
|
|
18
|
+
tag:
|
|
19
|
+
description: Release tag to publish, e.g. v1.2.3
|
|
20
|
+
required: true
|
|
21
|
+
|
|
22
|
+
permissions: {}
|
|
23
|
+
|
|
24
|
+
# One release at a time, and never cancel one halfway.
|
|
25
|
+
concurrency:
|
|
26
|
+
group: release
|
|
27
|
+
cancel-in-progress: false
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
release-please:
|
|
31
|
+
if: github.event_name == 'push'
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
timeout-minutes: 10
|
|
34
|
+
permissions:
|
|
35
|
+
contents: write
|
|
36
|
+
pull-requests: write
|
|
37
|
+
issues: write
|
|
38
|
+
outputs:
|
|
39
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
40
|
+
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
41
|
+
steps:
|
|
42
|
+
- id: release
|
|
43
|
+
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
|
|
44
|
+
with:
|
|
45
|
+
config-file: .github/release-please-config.json
|
|
46
|
+
manifest-file: .github/release-please-manifest.json
|
|
47
|
+
|
|
48
|
+
# The build tool runs only here, and cannot publish. publish installs and runs
|
|
49
|
+
# nothing from the repository, and publishes only the files build made: their
|
|
50
|
+
# SHA-256 sums are build's outputs, which later jobs cannot change.
|
|
51
|
+
build:
|
|
52
|
+
needs: release-please
|
|
53
|
+
if: >-
|
|
54
|
+
!cancelled() && needs.release-please.result != 'failure' &&
|
|
55
|
+
(github.event_name == 'workflow_dispatch' || needs.release-please.outputs.release_created == 'true')
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
timeout-minutes: 10
|
|
58
|
+
permissions:
|
|
59
|
+
contents: read
|
|
60
|
+
env:
|
|
61
|
+
TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || needs.release-please.outputs.tag_name }}
|
|
62
|
+
outputs:
|
|
63
|
+
sha: ${{ steps.commit.outputs.sha }}
|
|
64
|
+
sums: ${{ steps.build.outputs.sums }}
|
|
65
|
+
steps:
|
|
66
|
+
- name: Check the tag
|
|
67
|
+
run: '[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "not a release tag: $TAG"; exit 1; }'
|
|
68
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
69
|
+
with:
|
|
70
|
+
ref: refs/tags/${{ env.TAG }}
|
|
71
|
+
# The version is read from the tag, which needs the history.
|
|
72
|
+
fetch-depth: 0
|
|
73
|
+
persist-credentials: false
|
|
74
|
+
- id: commit
|
|
75
|
+
name: Check the commit
|
|
76
|
+
run: |
|
|
77
|
+
# By hand, only from main's own workflow, and only for a commit on main.
|
|
78
|
+
if [[ "$GITHUB_EVENT_NAME" == workflow_dispatch ]]; then
|
|
79
|
+
[[ "$GITHUB_REF" == refs/heads/main ]] || { echo "run it on main, not $GITHUB_REF"; exit 1; }
|
|
80
|
+
git merge-base --is-ancestor HEAD origin/main || { echo "$TAG is not on main"; exit 1; }
|
|
81
|
+
fi
|
|
82
|
+
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
83
|
+
- name: Check the version against PyPI
|
|
84
|
+
run: |
|
|
85
|
+
status=$(curl -s -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/nibs/${TAG#v}/json")
|
|
86
|
+
[[ "$status" == 404 ]] || { echo "${TAG#v} is already on PyPI (HTTP $status)"; exit 1; }
|
|
87
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
88
|
+
with:
|
|
89
|
+
python-version: "3.15"
|
|
90
|
+
- id: build
|
|
91
|
+
run: |
|
|
92
|
+
pip install build
|
|
93
|
+
python -m build --outdir "$RUNNER_TEMP/dist"
|
|
94
|
+
cd "$RUNNER_TEMP/dist"
|
|
95
|
+
version=${TAG#v}
|
|
96
|
+
[[ -f "nibs-$version-py3-none-any.whl" && -f "nibs-$version.tar.gz" ]] || { ls; echo "the files are not version $version"; exit 1; }
|
|
97
|
+
{ echo 'sums<<EOF'; sha256sum ./*; echo EOF; } >> "$GITHUB_OUTPUT"
|
|
98
|
+
# The wheel as it will be published installs, and reports the version.
|
|
99
|
+
- run: |
|
|
100
|
+
python -m venv "$RUNNER_TEMP/venv"
|
|
101
|
+
"$RUNNER_TEMP/venv/bin/pip" install "$RUNNER_TEMP"/dist/*.whl
|
|
102
|
+
[[ "$("$RUNNER_TEMP/venv/bin/nibs" --version)" == "nibs ${TAG#v}" ]]
|
|
103
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
104
|
+
with:
|
|
105
|
+
name: dist
|
|
106
|
+
path: ${{ runner.temp }}/dist
|
|
107
|
+
if-no-files-found: error
|
|
108
|
+
retention-days: 7
|
|
109
|
+
|
|
110
|
+
# Separate from build, so that the test tools never touch what is published.
|
|
111
|
+
test:
|
|
112
|
+
needs: build
|
|
113
|
+
if: ${{ !cancelled() && needs.build.result == 'success' }}
|
|
114
|
+
runs-on: ubuntu-latest
|
|
115
|
+
timeout-minutes: 10
|
|
116
|
+
permissions:
|
|
117
|
+
contents: read
|
|
118
|
+
steps:
|
|
119
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
120
|
+
with:
|
|
121
|
+
ref: ${{ needs.build.outputs.sha }}
|
|
122
|
+
persist-credentials: false
|
|
123
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
124
|
+
with:
|
|
125
|
+
python-version: "3.15"
|
|
126
|
+
- run: pip install -e . pytest
|
|
127
|
+
- run: pytest -q
|
|
128
|
+
|
|
129
|
+
publish:
|
|
130
|
+
needs: [build, test]
|
|
131
|
+
if: ${{ !cancelled() && needs.build.result == 'success' && needs.test.result == 'success' }}
|
|
132
|
+
runs-on: ubuntu-latest
|
|
133
|
+
timeout-minutes: 10
|
|
134
|
+
# PyPI's trusted publisher accepts this environment only.
|
|
135
|
+
environment: pypi
|
|
136
|
+
permissions:
|
|
137
|
+
contents: read
|
|
138
|
+
id-token: write # PyPI trusted publishing
|
|
139
|
+
steps:
|
|
140
|
+
# No checkout: only the files build made are used.
|
|
141
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
142
|
+
with:
|
|
143
|
+
name: dist
|
|
144
|
+
path: dist
|
|
145
|
+
- run: cd dist && sha256sum --check --strict <<< "$SUMS"
|
|
146
|
+
env:
|
|
147
|
+
SUMS: ${{ needs.build.outputs.sums }}
|
|
148
|
+
- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
test:
|
|
17
|
+
name: test (${{ matrix.os }}, Python ${{ matrix.python }})
|
|
18
|
+
strategy:
|
|
19
|
+
fail-fast: false
|
|
20
|
+
matrix:
|
|
21
|
+
os: [ubuntu-latest]
|
|
22
|
+
python: ["3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
23
|
+
# Every supported Python on one OS; the others only at the oldest and
|
|
24
|
+
# newest, since what differs by OS (file paths, replacing the cache
|
|
25
|
+
# file) does not depend on the Python version.
|
|
26
|
+
include:
|
|
27
|
+
- os: macos-latest
|
|
28
|
+
python: "3.11"
|
|
29
|
+
- os: macos-latest
|
|
30
|
+
python: "3.15"
|
|
31
|
+
- os: windows-latest
|
|
32
|
+
python: "3.11"
|
|
33
|
+
- os: windows-latest
|
|
34
|
+
python: "3.15"
|
|
35
|
+
runs-on: ${{ matrix.os }}
|
|
36
|
+
timeout-minutes: 10
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
39
|
+
with:
|
|
40
|
+
persist-credentials: false
|
|
41
|
+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
42
|
+
with:
|
|
43
|
+
python-version: ${{ matrix.python }}
|
|
44
|
+
- run: pip install -e . pytest
|
|
45
|
+
- run: pytest -q
|
|
46
|
+
# The wheel as it would be published installs, and its command starts.
|
|
47
|
+
- name: Install the wheel in a new environment
|
|
48
|
+
shell: bash
|
|
49
|
+
run: |
|
|
50
|
+
pip wheel --no-deps -w "$RUNNER_TEMP/wheel" .
|
|
51
|
+
python -m venv "$RUNNER_TEMP/venv"
|
|
52
|
+
if [[ -d "$RUNNER_TEMP/venv/Scripts" ]]; then bin="$RUNNER_TEMP/venv/Scripts"; else bin="$RUNNER_TEMP/venv/bin"; fi
|
|
53
|
+
"$bin/python" -m pip install "$RUNNER_TEMP"/wheel/nibs-*.whl
|
|
54
|
+
"$bin/nibs" --help
|
nibs-0.1.0/.gitignore
ADDED
nibs-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-11-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* read a web page by its headings ([6e42fc1](https://github.com/nhinata/nibs/commit/6e42fc1571dc3a17e93febf0caed0db9d289857d))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* close a code fence only with one at least as long ([#10](https://github.com/nhinata/nibs/issues/10)) ([e4c363d](https://github.com/nhinata/nibs/commit/e4c363d66e23bc12209a5d2cf74925f532b180b4))
|
|
14
|
+
* keep article headers, forms and code blocks when reading HTML ([#4](https://github.com/nhinata/nibs/issues/4)) ([7ff6281](https://github.com/nhinata/nibs/commit/7ff62811e90ba1530804212c997fb0eea10782ad))
|
|
15
|
+
* read only http and https URLs ([#2](https://github.com/nhinata/nibs/issues/2)) ([11dd070](https://github.com/nhinata/nibs/commit/11dd070561fb2a1fb682ecadaeab9d1224b24aed))
|
|
16
|
+
* read the charset a page declares in a meta tag ([#5](https://github.com/nhinata/nibs/issues/5)) ([478c52e](https://github.com/nhinata/nibs/commit/478c52ead8fb284a0b1d8cdfb5c4bf145122b28c))
|
|
17
|
+
* show a message instead of a traceback on broken input ([#6](https://github.com/nhinata/nibs/issues/6)) ([6d603c2](https://github.com/nhinata/nibs/commit/6d603c29036f55f68e8328f5e82c5cb74ea12495))
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Development
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
python -m venv .venv
|
|
7
|
+
.venv/bin/pip install -e . pytest
|
|
8
|
+
.venv/bin/pytest -q
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The tests do not reach the internet: they serve pages from a fake site (`tests/conftest.py`) or from a web server on localhost (`tests/test_fetch.py`).
|
|
12
|
+
|
|
13
|
+
## Pull requests
|
|
14
|
+
|
|
15
|
+
- **One topic per pull request.** Name the branch `<type>/<topic>`, such as `fix/sphinx-headings`.
|
|
16
|
+
- **Titles follow [Conventional Commits](https://www.conventionalcommits.org/).** Pull requests are squash-merged, so the title becomes the commit on `main`, and the release tooling reads it to pick the next version:
|
|
17
|
+
|
|
18
|
+
| Prefix | Use for | Version bump |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `feat:` | a new command, option or output | minor |
|
|
21
|
+
| `fix:` | wrong output or a crash | patch |
|
|
22
|
+
| `feat!:` / `fix!:` | a change to the output or to a command that breaks instructions written for the old one | major (minor while 0.x) |
|
|
23
|
+
| `docs:`, `test:`, `refactor:`, `ci:`, `chore:` | everything else | none |
|
|
24
|
+
|
|
25
|
+
- `main` is protected: every change goes through a pull request, and the tests must pass.
|
|
26
|
+
|
|
27
|
+
## Releases
|
|
28
|
+
|
|
29
|
+
Merging a pull request that bumps the version makes release-please open a release pull request. Merging that publishes the version to PyPI ([release.yml](.github/workflows/release.yml)).
|
nibs-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nhinata
|
|
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.
|
nibs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nibs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read a web page by its headings: list them, then print only the section you need
|
|
5
|
+
Project-URL: Repository, https://github.com/nhinata/nibs
|
|
6
|
+
Author: nhinata
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agent,cli,llm,markdown,tokens,web
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# nibs
|
|
19
|
+
|
|
20
|
+
Read a web page by its headings: list them, then print only the section you need.
|
|
21
|
+
|
|
22
|
+
An AI agent that reads a whole documentation page to answer one question puts tens of thousands of characters into its context, most of which it never uses. nibs lets it look at the page's headings first and then take just one section, in the page's own words.
|
|
23
|
+
|
|
24
|
+
```console
|
|
25
|
+
$ nibs toc https://docs.python.org/3/library/argparse.html
|
|
26
|
+
82,019 chars, 57 headings
|
|
27
|
+
1. Navigation (254)
|
|
28
|
+
2. argparse — Parser for command-line options, arguments and subcommands¶ (81,665)
|
|
29
|
+
3. ArgumentParser objects¶ (19,217)
|
|
30
|
+
4. prog¶ (1,483)
|
|
31
|
+
5. usage¶ (893)
|
|
32
|
+
...
|
|
33
|
+
$ nibs find https://docs.python.org/3/library/argparse.html allow_abbrev --limit 2
|
|
34
|
+
## ArgumentParser objects¶
|
|
35
|
+
class argparse.ArgumentParser(prog=None, usage=None, description=None, epilog=None, ...
|
|
36
|
+
allow_abbrev - Allows long options to be abbreviated if the abbreviation is unambiguous (default: True)
|
|
37
|
+
[nibs: stopped at 2 matches; pass --limit]
|
|
38
|
+
$ nibs section https://docs.python.org/3/library/argparse.html 4
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pipx install nibs # or: uv tool install nibs
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Python 3.11 or later. No other dependencies.
|
|
48
|
+
|
|
49
|
+
## Commands
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
nibs toc <url> # headings, numbered, with each section's size in characters
|
|
53
|
+
nibs section <url> <n|heading> # one section, by its number in toc or by heading text
|
|
54
|
+
nibs find <url> <regex> # matching lines, each under the heading it belongs to
|
|
55
|
+
nibs stats # characters printed so far, against reading each page whole
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `section` prints at most 8,000 characters; `--max` changes that. A heading given as text matches exactly first, then as a case-insensitive substring.
|
|
59
|
+
- `find` prints at most 30 lines; `--limit` changes that.
|
|
60
|
+
|
|
61
|
+
## How a page is read
|
|
62
|
+
|
|
63
|
+
- Only `http` and `https` URLs are read, also when a page redirects.
|
|
64
|
+
- If the site serves Markdown, either at the URL itself or at the URL with `.md` added, nibs uses it. Many documentation sites do.
|
|
65
|
+
- Otherwise nibs turns the HTML into headings, text and code blocks (from `<pre>`). It leaves out scripts, styles, navigation, sidebars, and the page's own header and footer; a header or footer inside an article is kept. Only heading tags become headings, and only `<pre>` becomes a code block: a line of text that would read as either (see below) gets a backslash in front.
|
|
66
|
+
- Control characters in the page are removed before printing.
|
|
67
|
+
- nibs does not run JavaScript. A page that builds its content in the browser comes back with little or no text; when no headings are found, nibs says so.
|
|
68
|
+
- A fetched page is kept for 15 minutes in `~/.cache/nibs` (or `$XDG_CACHE_HOME/nibs`), so `toc`, `section` and `find` on the same page fetch it once.
|
|
69
|
+
|
|
70
|
+
### Headings and code blocks
|
|
71
|
+
|
|
72
|
+
nibs splits a page by its Markdown headings. It reads only what it needs for that, by this rule rather than all of CommonMark:
|
|
73
|
+
|
|
74
|
+
- A heading is a line that starts with 1 to 6 `#` and a space.
|
|
75
|
+
- A line that starts with three or more `` ` `` or `~` opens a code block, after any spaces. The block ends at a line of the same character, at least as long, with nothing after it. Headings inside a code block are not headings.
|
|
76
|
+
- Nothing else is read as structure: not headings underlined with `===` or `---`, not code indented by four spaces.
|
|
77
|
+
|
|
78
|
+
## Using it from an agent
|
|
79
|
+
|
|
80
|
+
Tell the agent in its instructions, for example in `CLAUDE.md` or `AGENTS.md`:
|
|
81
|
+
|
|
82
|
+
```markdown
|
|
83
|
+
To read documentation on the web, run `nibs toc <url>` to see the headings,
|
|
84
|
+
then `nibs section <url> <n>` for the part you need, or `nibs find <url> <regex>`.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## The usage record
|
|
88
|
+
|
|
89
|
+
Each run adds one line to `~/.local/state/nibs/usage.jsonl` (or `$XDG_STATE_HOME/nibs/usage.jsonl`): the time, the command, the URL, the page's size and how many characters were printed. `nibs stats` reads it. Set `NIBS_LOG` to another path to keep the record there. Nothing is sent anywhere.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
nibs-0.1.0/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# nibs
|
|
2
|
+
|
|
3
|
+
Read a web page by its headings: list them, then print only the section you need.
|
|
4
|
+
|
|
5
|
+
An AI agent that reads a whole documentation page to answer one question puts tens of thousands of characters into its context, most of which it never uses. nibs lets it look at the page's headings first and then take just one section, in the page's own words.
|
|
6
|
+
|
|
7
|
+
```console
|
|
8
|
+
$ nibs toc https://docs.python.org/3/library/argparse.html
|
|
9
|
+
82,019 chars, 57 headings
|
|
10
|
+
1. Navigation (254)
|
|
11
|
+
2. argparse — Parser for command-line options, arguments and subcommands¶ (81,665)
|
|
12
|
+
3. ArgumentParser objects¶ (19,217)
|
|
13
|
+
4. prog¶ (1,483)
|
|
14
|
+
5. usage¶ (893)
|
|
15
|
+
...
|
|
16
|
+
$ nibs find https://docs.python.org/3/library/argparse.html allow_abbrev --limit 2
|
|
17
|
+
## ArgumentParser objects¶
|
|
18
|
+
class argparse.ArgumentParser(prog=None, usage=None, description=None, epilog=None, ...
|
|
19
|
+
allow_abbrev - Allows long options to be abbreviated if the abbreviation is unambiguous (default: True)
|
|
20
|
+
[nibs: stopped at 2 matches; pass --limit]
|
|
21
|
+
$ nibs section https://docs.python.org/3/library/argparse.html 4
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pipx install nibs # or: uv tool install nibs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Python 3.11 or later. No other dependencies.
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
nibs toc <url> # headings, numbered, with each section's size in characters
|
|
36
|
+
nibs section <url> <n|heading> # one section, by its number in toc or by heading text
|
|
37
|
+
nibs find <url> <regex> # matching lines, each under the heading it belongs to
|
|
38
|
+
nibs stats # characters printed so far, against reading each page whole
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `section` prints at most 8,000 characters; `--max` changes that. A heading given as text matches exactly first, then as a case-insensitive substring.
|
|
42
|
+
- `find` prints at most 30 lines; `--limit` changes that.
|
|
43
|
+
|
|
44
|
+
## How a page is read
|
|
45
|
+
|
|
46
|
+
- Only `http` and `https` URLs are read, also when a page redirects.
|
|
47
|
+
- If the site serves Markdown, either at the URL itself or at the URL with `.md` added, nibs uses it. Many documentation sites do.
|
|
48
|
+
- Otherwise nibs turns the HTML into headings, text and code blocks (from `<pre>`). It leaves out scripts, styles, navigation, sidebars, and the page's own header and footer; a header or footer inside an article is kept. Only heading tags become headings, and only `<pre>` becomes a code block: a line of text that would read as either (see below) gets a backslash in front.
|
|
49
|
+
- Control characters in the page are removed before printing.
|
|
50
|
+
- nibs does not run JavaScript. A page that builds its content in the browser comes back with little or no text; when no headings are found, nibs says so.
|
|
51
|
+
- A fetched page is kept for 15 minutes in `~/.cache/nibs` (or `$XDG_CACHE_HOME/nibs`), so `toc`, `section` and `find` on the same page fetch it once.
|
|
52
|
+
|
|
53
|
+
### Headings and code blocks
|
|
54
|
+
|
|
55
|
+
nibs splits a page by its Markdown headings. It reads only what it needs for that, by this rule rather than all of CommonMark:
|
|
56
|
+
|
|
57
|
+
- A heading is a line that starts with 1 to 6 `#` and a space.
|
|
58
|
+
- A line that starts with three or more `` ` `` or `~` opens a code block, after any spaces. The block ends at a line of the same character, at least as long, with nothing after it. Headings inside a code block are not headings.
|
|
59
|
+
- Nothing else is read as structure: not headings underlined with `===` or `---`, not code indented by four spaces.
|
|
60
|
+
|
|
61
|
+
## Using it from an agent
|
|
62
|
+
|
|
63
|
+
Tell the agent in its instructions, for example in `CLAUDE.md` or `AGENTS.md`:
|
|
64
|
+
|
|
65
|
+
```markdown
|
|
66
|
+
To read documentation on the web, run `nibs toc <url>` to see the headings,
|
|
67
|
+
then `nibs section <url> <n>` for the part you need, or `nibs find <url> <regex>`.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## The usage record
|
|
71
|
+
|
|
72
|
+
Each run adds one line to `~/.local/state/nibs/usage.jsonl` (or `$XDG_STATE_HOME/nibs/usage.jsonl`): the time, the command, the URL, the page's size and how many characters were printed. `nibs stats` reads it. Set `NIBS_LOG` to another path to keep the record there. Nothing is sent anywhere.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nibs"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Read a web page by its headings: list them, then print only the section you need"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{ name = "nhinata" }]
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
dependencies = []
|
|
15
|
+
keywords = ["llm", "agent", "tokens", "web", "markdown", "cli"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
21
|
+
"Topic :: Utilities",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://github.com/nhinata/nibs"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
nibs = "nibs.cli:main"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.version]
|
|
31
|
+
source = "vcs"
|
|
32
|
+
fallback-version = "0.0.0"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/nibs"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Pages already read, so that toc, section and find on one page fetch it once."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from .paths import cache_dir
|
|
9
|
+
|
|
10
|
+
# How long a fetched page is reused; the same default as Claude Code's WebFetch.
|
|
11
|
+
TTL_SECONDS = 15 * 60
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _path(url):
|
|
15
|
+
return cache_dir() / (hashlib.sha256(url.encode()).hexdigest() + ".json")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get(url):
|
|
19
|
+
try:
|
|
20
|
+
with open(_path(url), encoding="utf-8") as f:
|
|
21
|
+
entry = json.load(f)
|
|
22
|
+
except (OSError, ValueError):
|
|
23
|
+
return None
|
|
24
|
+
if entry.get("url") != url or time.time() - entry.get("time", 0) > TTL_SECONDS:
|
|
25
|
+
return None
|
|
26
|
+
return entry.get("text")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def put(url, text):
|
|
30
|
+
try:
|
|
31
|
+
cache_dir().mkdir(parents=True, exist_ok=True)
|
|
32
|
+
_prune()
|
|
33
|
+
tmp = _path(url).with_suffix(".tmp")
|
|
34
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
35
|
+
json.dump({"url": url, "time": time.time(), "text": text}, f)
|
|
36
|
+
os.replace(tmp, _path(url))
|
|
37
|
+
except OSError:
|
|
38
|
+
pass # A page that cannot be cached is fetched again next time.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _prune():
|
|
42
|
+
now = time.time()
|
|
43
|
+
for p in cache_dir().glob("*.json"):
|
|
44
|
+
try:
|
|
45
|
+
if now - p.stat().st_mtime > TTL_SECONDS:
|
|
46
|
+
p.unlink()
|
|
47
|
+
except OSError:
|
|
48
|
+
pass
|