zigpeek 0.3.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.
@@ -0,0 +1,72 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ name: build distributions
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Install uv
15
+ uses: astral-sh/setup-uv@v5
16
+ with:
17
+ python-version: "3.12"
18
+
19
+ - name: Build sdist + wheel
20
+ run: uv build
21
+
22
+ - name: Verify wheel ships main.wasm
23
+ run: |
24
+ set -euo pipefail
25
+ wheel=$(ls dist/zigpeek-*.whl)
26
+ python -m zipfile -l "$wheel" | grep -q "_vendor/main.wasm"
27
+
28
+ - uses: actions/upload-artifact@v4
29
+ with:
30
+ name: dist
31
+ path: dist/
32
+ if-no-files-found: error
33
+
34
+ publish-pypi:
35
+ name: publish to PyPI
36
+ needs: build
37
+ runs-on: ubuntu-latest
38
+ environment:
39
+ name: pypi
40
+ url: https://pypi.org/p/zigpeek
41
+ permissions:
42
+ id-token: write
43
+ steps:
44
+ - uses: actions/download-artifact@v4
45
+ with:
46
+ name: dist
47
+ path: dist/
48
+
49
+ - uses: pypa/gh-action-pypi-publish@release/v1
50
+
51
+ github-release:
52
+ name: attach artifacts to GitHub Release
53
+ needs: publish-pypi
54
+ runs-on: ubuntu-latest
55
+ permissions:
56
+ contents: write
57
+ steps:
58
+ - uses: actions/download-artifact@v4
59
+ with:
60
+ name: dist
61
+ path: dist/
62
+
63
+ - name: Create GitHub Release
64
+ env:
65
+ GH_TOKEN: ${{ github.token }}
66
+ GH_REPO: ${{ github.repository }}
67
+ TAG: ${{ github.ref_name }}
68
+ run: |
69
+ gh release create "$TAG" \
70
+ --title "$TAG" \
71
+ --generate-notes \
72
+ dist/*
@@ -0,0 +1,34 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: test-${{ github.workflow }}-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ pytest:
14
+ name: pytest (${{ matrix.os }} / py${{ matrix.python }})
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ os: [ubuntu-latest, macos-latest]
19
+ python: ["3.12", "3.13"]
20
+ runs-on: ${{ matrix.os }}
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v5
26
+ with:
27
+ python-version: ${{ matrix.python }}
28
+ enable-cache: true
29
+
30
+ - name: Sync dependencies
31
+ run: uv sync --dev
32
+
33
+ - name: Run unit tests
34
+ run: uv run pytest -q
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ .venv/
5
+ .DS_Store
@@ -0,0 +1 @@
1
+ 3.12
zigpeek-0.3.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tanuj Vasudeva
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.
22
+
23
+ ---
24
+
25
+ This project bundles `src/zigpeek/_vendor/main.wasm`, built from the
26
+ `zig-mcp` project (https://github.com/loonghao/zig-mcp), which is also
27
+ distributed under the MIT License. See `vendor/PROVENANCE.md` for build
28
+ details.
zigpeek-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: zigpeek
3
+ Version: 0.3.0
4
+ Summary: Fast CLI for Zig 0.16 stdlib + Skill for coding agents
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: beautifulsoup4>=4.12.0
8
+ Requires-Dist: httpx>=0.27.0
9
+ Requires-Dist: lxml>=5.0.0
10
+ Requires-Dist: wasmtime>=25.0.0
@@ -0,0 +1,97 @@
1
+ # zigpeek
2
+
3
+ Fast CLI for Zig 0.16 stdlib + builtin docs lookups. Replaces the
4
+ [`zig-docs` MCP server](https://github.com/loonghao/zig-mcp) in
5
+ environments without MCP support — typically cloud agents.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pipx install git+https://github.com/TanGentleman/zigpeek
11
+ # or
12
+ uv tool install git+https://github.com/TanGentleman/zigpeek
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```sh
18
+ zigpeek prefetch # warm cache (once per session)
19
+ zigpeek search ArrayList --limit 10 # fuzzy stdlib search
20
+ zigpeek get std.ArrayList # full docs for an FQN
21
+ zigpeek get std.ArrayList --source-file # source file containing it
22
+ zigpeek builtins list # all @-builtins
23
+ zigpeek builtins get atomic # specific builtin
24
+ zigpeek batch <<EOF # amortize startup
25
+ search ArrayList
26
+ get std.ArrayList
27
+ EOF
28
+ ```
29
+
30
+ Full agent-facing docs live in [`skills/zigpeek/SKILL.md`](skills/zigpeek/SKILL.md).
31
+
32
+ ## Use as a Claude Code skill
33
+
34
+ Drop the `skills/zigpeek/` directory into your skills folder so Claude
35
+ Code can discover it:
36
+
37
+ ```sh
38
+ cp -r skills/zigpeek ~/.claude/skills/
39
+ ```
40
+
41
+ The skill assumes `zigpeek` is on `$PATH` (see install above).
42
+
43
+ ## Architecture
44
+
45
+ | File | Role |
46
+ | --------------------------------- | --------------------------------------------------- |
47
+ | `src/zigpeek/cli.py` | argparse entrypoint, exit-code contract |
48
+ | `src/zigpeek/stdlib.py` | markdown rendering (port of `zig-mcp/mcp/std.ts`) |
49
+ | `src/zigpeek/wasm.py` | wasmtime driver + typed wrapper around WASM exports |
50
+ | `src/zigpeek/builtins.py` | langref HTML parser + ranking |
51
+ | `src/zigpeek/fetch.py` | sources.tar / langref download + `/tmp` cache |
52
+ | `src/zigpeek/version.py` | default Zig version + override resolution |
53
+ | `src/zigpeek/_vendor/main.wasm` | autodoc WASM, shipped inside the package |
54
+ | `vendor/PROVENANCE.md` | build instructions + SHA256 + upstream commit |
55
+ | `vendor/patches/` | local patches applied before rebuilding the WASM |
56
+ | `skills/zigpeek/SKILL.md` | skill metadata for Claude Code |
57
+
58
+ ## Updating the vendored WASM
59
+
60
+ Bumping the Zig version may need a fresh `main.wasm`. Build steps live in
61
+ [`vendor/PROVENANCE.md`](vendor/PROVENANCE.md). Summary:
62
+
63
+ ```sh
64
+ cd ~/Documents/GitHub/zig-mcp
65
+ git pull
66
+ zig build
67
+ cp zig-out/main.wasm <repo>/src/zigpeek/_vendor/main.wasm
68
+ shasum -a 256 <repo>/src/zigpeek/_vendor/main.wasm
69
+ # update SHA256 + commit + date in vendor/PROVENANCE.md
70
+ ```
71
+
72
+ Run smoke tests after updating:
73
+
74
+ ```sh
75
+ ZIGPEEK_SMOKE=1 uv run pytest -v
76
+ ```
77
+
78
+ ## Testing
79
+
80
+ ```sh
81
+ uv sync # install deps
82
+ uv run pytest -q # unit tests (no network)
83
+ ZIGPEEK_SMOKE=1 uv run pytest # adds smoke tests (needs network + WASM)
84
+ ```
85
+
86
+ ## Why a port and not a wrapper?
87
+
88
+ The autodoc renderer lives inside the WASM as HTML-emitting exports.
89
+ Wrapping the upstream MCP would mean shipping Node.js to every cloud
90
+ agent. Porting the ~700 lines of TS that drive the WASM gets us the same
91
+ output with only Python + a vendored binary.
92
+
93
+ ## License
94
+
95
+ MIT — see [`LICENSE`](LICENSE). Bundled `main.wasm` is also MIT (from
96
+ [`zig-mcp`](https://github.com/loonghao/zig-mcp)); see
97
+ [`vendor/PROVENANCE.md`](vendor/PROVENANCE.md).
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "zigpeek"
3
+ version = "0.3.0"
4
+ description = "Fast CLI for Zig 0.16 stdlib + Skill for coding agents"
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "wasmtime>=25.0.0",
8
+ "httpx>=0.27.0",
9
+ "beautifulsoup4>=4.12.0",
10
+ "lxml>=5.0.0",
11
+ ]
12
+
13
+ [project.scripts]
14
+ zigpeek = "zigpeek.cli:main"
15
+
16
+ [dependency-groups]
17
+ dev = [
18
+ "pytest>=8.0.0",
19
+ ]
20
+
21
+ [build-system]
22
+ requires = ["hatchling"]
23
+ build-backend = "hatchling.build"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/zigpeek"]
27
+
28
+ # Be explicit: the vendored WASM is part of the package, not just incidental
29
+ # files swept in by hatchling defaults. Keeps editable installs (`uv sync`),
30
+ # wheel builds (`uv build`), and future tool installs in lockstep.
31
+ [tool.hatch.build.targets.wheel.force-include]
32
+ "src/zigpeek/_vendor/main.wasm" = "zigpeek/_vendor/main.wasm"
@@ -0,0 +1,161 @@
1
+ ---
2
+ name: zigpeek
3
+ description: Look up Zig 0.16 standard library APIs and builtin functions via a local CLI (replaces the zig-docs MCP server in environments without MCP support, e.g. cloud agents). Use before writing or reviewing Zig code that touches stdlib — critical for std.Io filesystem APIs (std.Io.Dir, std.Io.File), Reader/Writer interfaces, and std.process.Init. Triggers when answering "how do I X in Zig" or writing Zig that touches files, dirs, env, or process state. If the zig-docs MCP server is already connected, prefer it over this CLI.
4
+ ---
5
+
6
+ # zigpeek
7
+
8
+ A Python+wasmtime port of the four `zig-docs` MCP tools. Loads the same
9
+ autodoc WASM module the official Zig docs use, against the same
10
+ `sources.tar` from `ziglang.org`. Output is markdown, byte-equivalent
11
+ (modulo whitespace) to what the MCP returns.
12
+
13
+ ## Setup (run once per agent session/sandbox)
14
+
15
+ Install the `zigpeek` CLI globally with either tool:
16
+
17
+ ```sh
18
+ pipx install git+https://github.com/TanGentleman/zigpeek
19
+ # or
20
+ uv tool install git+https://github.com/TanGentleman/zigpeek
21
+ ```
22
+
23
+ Then warm the cache so subsequent lookups are offline:
24
+
25
+ ```sh
26
+ zigpeek prefetch
27
+ ```
28
+
29
+ Requires outbound network access to `ziglang.org` on first use. Caches
30
+ downloads under `/tmp/zigpeek-cache/<version>/`.
31
+
32
+ ## Usage
33
+
34
+ ```sh
35
+ # Search the stdlib
36
+ zigpeek search ArrayList --limit 10
37
+
38
+ # Get full docs for a stdlib item
39
+ zigpeek get std.ArrayList
40
+
41
+ # Get the source file containing an item
42
+ zigpeek get std.ArrayList --source-file
43
+
44
+ # List all builtin functions
45
+ zigpeek builtins list
46
+
47
+ # Look up a builtin (by name or keyword)
48
+ zigpeek builtins get atomic
49
+
50
+ # Pre-populate the cache (so later commands don't need network)
51
+ zigpeek prefetch
52
+ ```
53
+
54
+ ## When to use which command
55
+
56
+ | Need | Command |
57
+ | ------------------------------------------------- | --------------------------------- |
58
+ | Discover stdlib symbols matching a keyword | `zigpeek search <q>` |
59
+ | Read full docs + signature for a known FQN | `zigpeek get <fqn>` |
60
+ | Read the full source file (terse docstring; want invariants, internals, or per-field implementation) | `zigpeek get <fqn> --source-file` |
61
+ | Browse all `@`-builtins | `zigpeek builtins list` |
62
+ | Look up a specific `@builtin` (accepts `atomic` or `@atomic`) | `zigpeek builtins get <q>` |
63
+ | Warm cache before going offline | `zigpeek prefetch` |
64
+ | Run several lookups in one process (cheap) | `zigpeek batch` |
65
+
66
+ ## Batching multiple lookups
67
+
68
+ Each `zigpeek` invocation pays ~1 s of Python+wasmtime startup. If you
69
+ plan more than two lookups, pipe them through `zigpeek batch` to share
70
+ that cost across the whole sequence — the WASM instance and parsed
71
+ sources are reused between commands.
72
+
73
+ ```sh
74
+ zigpeek batch <<'EOF'
75
+ search ArrayList --limit 5
76
+ get std.ArrayList
77
+ get std.ArrayList --source-file
78
+ builtins get atomic
79
+ EOF
80
+ ```
81
+
82
+ Each command's output is framed with a `===> <command>` separator on its
83
+ own line. Per-line failures (not-found, bad input) are reported inline
84
+ and **do not abort the batch**; the process exit code is the worst code
85
+ seen across all lines (`0` if every command succeeded). `prefetch` and
86
+ nested `batch` are rejected — run them outside.
87
+
88
+ Use `zigpeek batch -f commands.txt` to read from a file instead of
89
+ stdin. Blank lines and lines starting with `#` are ignored.
90
+
91
+ **Reach for `--source-file` early** when:
92
+
93
+ - The docstring is one line or missing.
94
+ - The page lists a method signature but elides the body (e.g.
95
+ `MultiArrayList.items` shows the prototype but the per-field pointer
96
+ math from `ptrs[@intFromEnum(field)]` only lives in the source).
97
+ - You need invariants, error sets, or how a private field is computed.
98
+
99
+ ## Finding nested types
100
+
101
+ Inner types live under the **defining module's path**, not the
102
+ re-export. `std.MultiArrayList` is a re-export from
103
+ `std.multi_array_list`; its inner `Slice` type only resolves at the
104
+ defining path:
105
+
106
+ ```sh
107
+ zigpeek get std.multi_array_list.MultiArrayList.Slice # works
108
+ zigpeek get std.MultiArrayList.Slice # not found
109
+ ```
110
+
111
+ If `search` only surfaces a re-export and `get` 404s on the inner type,
112
+ re-run `get` with the module path.
113
+
114
+ ## Version override
115
+
116
+ Defaults to Zig `0.16.0`. Override with:
117
+
118
+ ```sh
119
+ zigpeek search ArrayList --version 0.15.1
120
+ ZIGPEEK_VERSION=master zigpeek search ArrayList
121
+ ```
122
+
123
+ ## Offline mode
124
+
125
+ If the agent will run without internet, prefetch first while you still
126
+ have network:
127
+
128
+ ```sh
129
+ # Default cache (/tmp/zigpeek-cache/<version>/)
130
+ zigpeek prefetch
131
+
132
+ # Custom cache directory (persists outside /tmp)
133
+ zigpeek prefetch --cache-dir ~/.cache/zigpeek
134
+ ```
135
+
136
+ After prefetch, every `search` / `get` / `builtins` call reads from disk
137
+ and never touches the network. Pass the same `--cache-dir` (or set
138
+ `ZIGPEEK_CACHE_DIR`) on subsequent commands if you used a non-default
139
+ location.
140
+
141
+ If a bundled snapshot ships inside the package (`src/zigpeek/_data/<version>/`),
142
+ the read path uses it automatically and prefetch becomes a no-op.
143
+
144
+ ## Exit codes
145
+
146
+ - `0` — success (markdown on stdout)
147
+ - `1` — bad input or "not found" (message on stderr)
148
+ - `2` — network/cache failure (message on stderr)
149
+
150
+ ## Troubleshooting
151
+
152
+ - **`zigpeek: command not found`** — install via `pipx install git+https://github.com/TanGentleman/zigpeek`
153
+ (or `uv tool install ...`). If neither tool is available, install uv:
154
+ `curl -LsSf https://astral.sh/uv/install.sh | sh`.
155
+ - **`network/cache error`** — `ziglang.org` is blocked or unreachable.
156
+ Run `zigpeek prefetch` from a network-enabled host first, or check
157
+ your sandbox network policy.
158
+ - **`Declaration "..." not found`** — the FQN is wrong. Two things to
159
+ try: (1) `zigpeek search` to discover the canonical name; (2) if you
160
+ searched a re-export (e.g. `std.MultiArrayList`), retry `get` against
161
+ the defining module path (e.g. `std.multi_array_list.MultiArrayList`).
File without changes
@@ -0,0 +1,148 @@
1
+ """Port of mcp/extract-builtin-functions.ts and the ranking from mcp/tools.ts.
2
+
3
+ Parses Zig's langref HTML into a list of BuiltinFunction records, then ranks
4
+ those records by relevance to a query string.
5
+ """
6
+
7
+ import re
8
+ from dataclasses import dataclass
9
+
10
+ from bs4 import BeautifulSoup, NavigableString, Tag
11
+
12
+
13
+ @dataclass
14
+ class BuiltinFunction:
15
+ func: str
16
+ signature: str
17
+ docs: str
18
+
19
+
20
+ _WS_RE = re.compile(r"\s+")
21
+ _BLANK_LINES_RE = re.compile(r"\n{2,}")
22
+ _TRAILING_NEWLINES_RE = re.compile(r"\n+$")
23
+
24
+
25
+ def _rewrite_link(text: str, href: str, link_base_url: str | None) -> str:
26
+ if href.startswith("#") and link_base_url:
27
+ return f"[{text}]({link_base_url}{href})"
28
+ return f"[{text}]({href})"
29
+
30
+
31
+ def _inline_text(tag: Tag, link_base_url: str | None) -> str:
32
+ cloned = BeautifulSoup(str(tag), "lxml").find()
33
+ if cloned is None:
34
+ return ""
35
+
36
+ for a in list(cloned.find_all("a")):
37
+ href = a.get("href", "")
38
+ text = a.get_text()
39
+ a.replace_with(NavigableString(_rewrite_link(text, href, link_base_url)))
40
+
41
+ for code in list(cloned.find_all("code")):
42
+ code.replace_with(NavigableString(f"`{code.get_text()}`"))
43
+
44
+ return _WS_RE.sub(" ", cloned.get_text()).strip()
45
+
46
+
47
+ def _next_sibling_tag(tag: Tag) -> Tag | None:
48
+ sib = tag.next_sibling
49
+ while sib is not None and not isinstance(sib, Tag):
50
+ sib = sib.next_sibling
51
+ return sib
52
+
53
+
54
+ def parse_builtin_functions_html(
55
+ html: str,
56
+ link_base_url: str | None,
57
+ ) -> list[BuiltinFunction]:
58
+ soup = BeautifulSoup(html, "lxml")
59
+ section = soup.find("h2", id="Builtin-Functions")
60
+ if section is None:
61
+ raise ValueError("Could not find Builtin Functions section in HTML")
62
+
63
+ builtins: list[BuiltinFunction] = []
64
+ current = _next_sibling_tag(section)
65
+
66
+ while current is not None and current.name != "h2":
67
+ if current.name == "h3" and current.has_attr("id"):
68
+ first_a = current.find("a")
69
+ func = first_a.get_text() if first_a is not None else ""
70
+ if func.startswith("@"):
71
+ pre = _next_sibling_tag(current)
72
+ signature = ""
73
+ desc_start = pre
74
+ if pre is not None and pre.name == "pre":
75
+ signature = pre.get_text().strip()
76
+ desc_start = _next_sibling_tag(pre)
77
+
78
+ description_parts: list[str] = []
79
+ desc_current = desc_start
80
+ while desc_current is not None and desc_current.name not in ("h2", "h3"):
81
+ if desc_current.name == "p":
82
+ description_parts.append(
83
+ _inline_text(desc_current, link_base_url)
84
+ )
85
+ elif desc_current.name == "ul":
86
+ for li in desc_current.find_all("li", recursive=False):
87
+ li_text = _inline_text(li, link_base_url)
88
+ if li_text:
89
+ description_parts.append(f"* {li_text}")
90
+ elif desc_current.name == "figure":
91
+ figcaption = ""
92
+ cap_el = desc_current.find("figcaption")
93
+ if cap_el is not None:
94
+ figcaption = cap_el.get_text().strip()
95
+ pre_el = desc_current.find("pre")
96
+ code = pre_el.get_text() if pre_el is not None else ""
97
+ lang = ""
98
+ label = ""
99
+ if figcaption:
100
+ label = f"**{figcaption}**\n"
101
+ if figcaption.endswith(".zig"):
102
+ lang = "zig"
103
+ elif "shell" in figcaption.lower():
104
+ lang = "sh"
105
+ if code:
106
+ block = f"{label}\n```{lang}\n{code.strip()}\n```"
107
+ description_parts.append(block.strip())
108
+ desc_current = _next_sibling_tag(desc_current)
109
+
110
+ docs = "\n".join(description_parts)
111
+ docs = _BLANK_LINES_RE.sub("\n", docs)
112
+ docs = _TRAILING_NEWLINES_RE.sub("", docs)
113
+ if docs.lower().endswith("see also:"):
114
+ docs = docs[: -len("see also:")].strip()
115
+
116
+ builtins.append(
117
+ BuiltinFunction(func=func, signature=signature, docs=docs)
118
+ )
119
+
120
+ current = _next_sibling_tag(current)
121
+
122
+ return builtins
123
+
124
+
125
+ def rank_builtin_functions(
126
+ functions: list[BuiltinFunction],
127
+ query: str,
128
+ ) -> list[BuiltinFunction]:
129
+ q = query.lower().strip()
130
+ if not q:
131
+ return []
132
+
133
+ scored: list[tuple[int, BuiltinFunction]] = []
134
+ for fn in functions:
135
+ f_lower = fn.func.lower()
136
+ score = 0
137
+ if f_lower == q:
138
+ score += 1000
139
+ elif f_lower.startswith(q):
140
+ score += 500
141
+ elif q in f_lower:
142
+ score += 300
143
+ if score > 0:
144
+ score += max(0, 50 - len(fn.func))
145
+ scored.append((score, fn))
146
+
147
+ scored.sort(key=lambda pair: pair[0], reverse=True)
148
+ return [fn for _, fn in scored]