zigpeek 0.3.0__py3-none-any.whl
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.
- zigpeek/__init__.py +0 -0
- zigpeek/_vendor/main.wasm +0 -0
- zigpeek/builtins.py +148 -0
- zigpeek/cli.py +320 -0
- zigpeek/fetch.py +103 -0
- zigpeek/stdlib.py +324 -0
- zigpeek/version.py +15 -0
- zigpeek/wasm.py +210 -0
- zigpeek-0.3.0.dist-info/METADATA +10 -0
- zigpeek-0.3.0.dist-info/RECORD +13 -0
- zigpeek-0.3.0.dist-info/WHEEL +4 -0
- zigpeek-0.3.0.dist-info/entry_points.txt +2 -0
- zigpeek-0.3.0.dist-info/licenses/LICENSE +28 -0
zigpeek/__init__.py
ADDED
|
File without changes
|
|
Binary file
|
zigpeek/builtins.py
ADDED
|
@@ -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]
|
zigpeek/cli.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""argparse entrypoint for `zigpeek`. Five subcommands matching the MCP tools
|
|
2
|
+
plus a `prefetch` helper for offline-first workflows and a `batch` runner
|
|
3
|
+
that amortizes Python+wasmtime startup across many lookups.
|
|
4
|
+
|
|
5
|
+
Exit codes:
|
|
6
|
+
0 — success (markdown on stdout)
|
|
7
|
+
1 — bad input or "not found" (message on stderr)
|
|
8
|
+
2 — network or cache failure (message on stderr)
|
|
9
|
+
|
|
10
|
+
For `batch`, the exit code is the maximum exit code seen across the
|
|
11
|
+
input lines, so a single not-found does not mask a later cache error.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import functools
|
|
16
|
+
import io
|
|
17
|
+
import shlex
|
|
18
|
+
import sys
|
|
19
|
+
from importlib.resources import files
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
import wasmtime
|
|
24
|
+
|
|
25
|
+
from zigpeek import builtins as builtins_mod
|
|
26
|
+
from zigpeek.fetch import (
|
|
27
|
+
fetch_langref_html,
|
|
28
|
+
fetch_sources_tar,
|
|
29
|
+
langref_url,
|
|
30
|
+
prefetch as fetch_prefetch,
|
|
31
|
+
)
|
|
32
|
+
from zigpeek.stdlib import render_get_item, render_search
|
|
33
|
+
from zigpeek.version import resolve_version
|
|
34
|
+
from zigpeek.wasm import WasmStd
|
|
35
|
+
|
|
36
|
+
_BATCH_DISALLOWED = frozenset({"batch", "prefetch"})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _err(msg: str, code: int) -> int:
|
|
40
|
+
print(msg, file=sys.stderr)
|
|
41
|
+
return code
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _wrap_errors(fn):
|
|
45
|
+
"""Translate the two exception families every command shares into the
|
|
46
|
+
documented exit codes. Keeps the per-command bodies focused on the
|
|
47
|
+
happy path."""
|
|
48
|
+
|
|
49
|
+
@functools.wraps(fn)
|
|
50
|
+
def inner(args: argparse.Namespace) -> int:
|
|
51
|
+
try:
|
|
52
|
+
return fn(args)
|
|
53
|
+
except (httpx.HTTPError, OSError) as e:
|
|
54
|
+
return _err(f"network/cache error: {e}", 2)
|
|
55
|
+
except (wasmtime.WasmtimeError, RuntimeError) as e:
|
|
56
|
+
return _err(
|
|
57
|
+
f"data error: {e}\n"
|
|
58
|
+
"The cached or bundled sources.tar may be corrupt. "
|
|
59
|
+
"Try `zigpeek prefetch --refresh`.",
|
|
60
|
+
2,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return inner
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _vendor_wasm_bytes() -> bytes:
|
|
67
|
+
wasm = files("zigpeek").joinpath("_vendor", "main.wasm")
|
|
68
|
+
if not wasm.is_file():
|
|
69
|
+
raise FileNotFoundError(
|
|
70
|
+
"main.wasm not found inside the zigpeek package "
|
|
71
|
+
"(expected at zigpeek/_vendor/main.wasm). "
|
|
72
|
+
"See vendor/PROVENANCE.md for build instructions."
|
|
73
|
+
)
|
|
74
|
+
return wasm.read_bytes()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@functools.lru_cache(maxsize=4)
|
|
78
|
+
def _load_std(version: str, refresh: bool, cache_dir: str | None) -> WasmStd:
|
|
79
|
+
sources = fetch_sources_tar(version, refresh=refresh, cache_dir=cache_dir)
|
|
80
|
+
return WasmStd(_vendor_wasm_bytes(), sources)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@functools.lru_cache(maxsize=4)
|
|
84
|
+
def _load_builtins(
|
|
85
|
+
version: str, refresh: bool, cache_dir: str | None
|
|
86
|
+
) -> tuple[list[builtins_mod.BuiltinFunction], str]:
|
|
87
|
+
html = fetch_langref_html(version, refresh=refresh, cache_dir=cache_dir)
|
|
88
|
+
base = langref_url(version)
|
|
89
|
+
return builtins_mod.parse_builtin_functions_html(html, link_base_url=base), base
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@_wrap_errors
|
|
93
|
+
def _cmd_search(args: argparse.Namespace) -> int:
|
|
94
|
+
if not args.query:
|
|
95
|
+
return _err("query cannot be empty", 1)
|
|
96
|
+
version = resolve_version(args.version)
|
|
97
|
+
std = _load_std(version, args.refresh, args.cache_dir)
|
|
98
|
+
sys.stdout.write(render_search(std, args.query, limit=args.limit))
|
|
99
|
+
sys.stdout.write("\n")
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@_wrap_errors
|
|
104
|
+
def _cmd_get(args: argparse.Namespace) -> int:
|
|
105
|
+
if not args.fqn:
|
|
106
|
+
return _err("fully-qualified name cannot be empty", 1)
|
|
107
|
+
version = resolve_version(args.version)
|
|
108
|
+
std = _load_std(version, args.refresh, args.cache_dir)
|
|
109
|
+
md = render_get_item(std, args.fqn, get_source_file=args.source_file)
|
|
110
|
+
if md.startswith("# Error"):
|
|
111
|
+
print(md, file=sys.stderr)
|
|
112
|
+
return 1
|
|
113
|
+
sys.stdout.write(md)
|
|
114
|
+
sys.stdout.write("\n")
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@_wrap_errors
|
|
119
|
+
def _cmd_builtins_list(args: argparse.Namespace) -> int:
|
|
120
|
+
version = resolve_version(args.version)
|
|
121
|
+
fns, base = _load_builtins(version, args.refresh, args.cache_dir)
|
|
122
|
+
lines = "\n".join(f"- {fn.signature}" for fn in fns)
|
|
123
|
+
sys.stdout.write(
|
|
124
|
+
f"Available {len(fns)} builtin functions "
|
|
125
|
+
f"(full docs: {base}):\n\n{lines}\n"
|
|
126
|
+
)
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@_wrap_errors
|
|
131
|
+
def _cmd_builtins_get(args: argparse.Namespace) -> int:
|
|
132
|
+
if not args.query:
|
|
133
|
+
return _err("query cannot be empty", 1)
|
|
134
|
+
version = resolve_version(args.version)
|
|
135
|
+
fns, _ = _load_builtins(version, args.refresh, args.cache_dir)
|
|
136
|
+
ranked = builtins_mod.rank_builtin_functions(fns, args.query)
|
|
137
|
+
if not ranked:
|
|
138
|
+
return _err(
|
|
139
|
+
f'No builtin functions found matching "{args.query}". '
|
|
140
|
+
"Try `zigpeek builtins list` to see all functions.",
|
|
141
|
+
1,
|
|
142
|
+
)
|
|
143
|
+
chunks = [
|
|
144
|
+
f"**{fn.func}**\n```zig\n{fn.signature}\n```\n\n{fn.docs}"
|
|
145
|
+
for fn in ranked
|
|
146
|
+
]
|
|
147
|
+
body = "\n\n---\n\n".join(chunks)
|
|
148
|
+
if len(ranked) == 1:
|
|
149
|
+
sys.stdout.write(body + "\n")
|
|
150
|
+
else:
|
|
151
|
+
sys.stdout.write(f"Found {len(ranked)} matching functions:\n\n{body}\n")
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@_wrap_errors
|
|
156
|
+
def _cmd_prefetch(args: argparse.Namespace) -> int:
|
|
157
|
+
version = resolve_version(args.version)
|
|
158
|
+
paths = fetch_prefetch(version, refresh=args.refresh, cache_dir=args.cache_dir)
|
|
159
|
+
sys.stdout.write(
|
|
160
|
+
f"Prefetched docs for Zig {version}:\n"
|
|
161
|
+
f" sources.tar → {paths['sources.tar']}\n"
|
|
162
|
+
f" langref.html → {paths['langref.html']}\n"
|
|
163
|
+
)
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _read_batch_lines(args: argparse.Namespace) -> list[str]:
|
|
168
|
+
if args.file:
|
|
169
|
+
text = args.file.read_text(encoding="utf-8")
|
|
170
|
+
else:
|
|
171
|
+
text = sys.stdin.read()
|
|
172
|
+
return text.splitlines()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _run_captured(parser: argparse.ArgumentParser, tokens: list[str]) -> tuple[
|
|
176
|
+
int, str, str
|
|
177
|
+
]:
|
|
178
|
+
"""Parse + dispatch a batch line, capturing its stdout/stderr so the
|
|
179
|
+
parent batch loop can frame each command's output."""
|
|
180
|
+
out, err = io.StringIO(), io.StringIO()
|
|
181
|
+
old_out, old_err = sys.stdout, sys.stderr
|
|
182
|
+
sys.stdout, sys.stderr = out, err
|
|
183
|
+
try:
|
|
184
|
+
try:
|
|
185
|
+
sub_args = parser.parse_args(tokens)
|
|
186
|
+
except SystemExit as se:
|
|
187
|
+
# argparse exits 2 for usage errors; map to 1 (bad input) so the
|
|
188
|
+
# batch exit code reserves 2 for real cache/network failures.
|
|
189
|
+
code = 0 if se.code == 0 else 1
|
|
190
|
+
return code, out.getvalue(), err.getvalue()
|
|
191
|
+
code = sub_args.func(sub_args)
|
|
192
|
+
finally:
|
|
193
|
+
sys.stdout, sys.stderr = old_out, old_err
|
|
194
|
+
return code, out.getvalue(), err.getvalue()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _cmd_batch(args: argparse.Namespace) -> int:
|
|
198
|
+
parser = build_parser()
|
|
199
|
+
worst = 0
|
|
200
|
+
first = True
|
|
201
|
+
for raw in _read_batch_lines(args):
|
|
202
|
+
line = raw.strip()
|
|
203
|
+
if not line or line.startswith("#"):
|
|
204
|
+
continue
|
|
205
|
+
try:
|
|
206
|
+
tokens = shlex.split(line)
|
|
207
|
+
except ValueError as e:
|
|
208
|
+
if not first:
|
|
209
|
+
sys.stdout.write("\n")
|
|
210
|
+
sys.stdout.write(f"===> {line}\n# Error\nunparseable line: {e}\n")
|
|
211
|
+
worst = max(worst, 1)
|
|
212
|
+
first = False
|
|
213
|
+
continue
|
|
214
|
+
if not tokens:
|
|
215
|
+
continue
|
|
216
|
+
if tokens[0] in _BATCH_DISALLOWED:
|
|
217
|
+
if not first:
|
|
218
|
+
sys.stdout.write("\n")
|
|
219
|
+
sys.stdout.write(
|
|
220
|
+
f"===> {line}\n# Error\n"
|
|
221
|
+
f"`{tokens[0]}` is not allowed inside batch.\n"
|
|
222
|
+
)
|
|
223
|
+
worst = max(worst, 1)
|
|
224
|
+
first = False
|
|
225
|
+
continue
|
|
226
|
+
code, out, err = _run_captured(parser, tokens)
|
|
227
|
+
if not first:
|
|
228
|
+
sys.stdout.write("\n")
|
|
229
|
+
sys.stdout.write(f"===> {line}\n")
|
|
230
|
+
sys.stdout.write(out)
|
|
231
|
+
if err:
|
|
232
|
+
sys.stdout.write(err)
|
|
233
|
+
worst = max(worst, code)
|
|
234
|
+
first = False
|
|
235
|
+
return worst
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _add_common(p: argparse.ArgumentParser) -> None:
|
|
239
|
+
p.add_argument("--version", default=None, help="Zig version (default: 0.16.0)")
|
|
240
|
+
p.add_argument(
|
|
241
|
+
"--refresh",
|
|
242
|
+
action="store_true",
|
|
243
|
+
help="Force re-download of cached resources",
|
|
244
|
+
)
|
|
245
|
+
p.add_argument(
|
|
246
|
+
"--cache-dir",
|
|
247
|
+
default=None,
|
|
248
|
+
help=(
|
|
249
|
+
"Cache directory root (overrides ZIGPEEK_CACHE_DIR; "
|
|
250
|
+
"default: /tmp/zigpeek-cache)"
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
256
|
+
parser = argparse.ArgumentParser(prog="zigpeek", description="Zig 0.16 docs CLI")
|
|
257
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
258
|
+
|
|
259
|
+
p_search = sub.add_parser("search", help="Search the standard library")
|
|
260
|
+
p_search.add_argument("query")
|
|
261
|
+
p_search.add_argument("--limit", type=int, default=20)
|
|
262
|
+
_add_common(p_search)
|
|
263
|
+
p_search.set_defaults(func=_cmd_search)
|
|
264
|
+
|
|
265
|
+
p_get = sub.add_parser("get", help="Get docs for a fully-qualified stdlib name")
|
|
266
|
+
p_get.add_argument("fqn")
|
|
267
|
+
p_get.add_argument(
|
|
268
|
+
"--source-file",
|
|
269
|
+
action="store_true",
|
|
270
|
+
help="Return the entire source file for the item",
|
|
271
|
+
)
|
|
272
|
+
_add_common(p_get)
|
|
273
|
+
p_get.set_defaults(func=_cmd_get)
|
|
274
|
+
|
|
275
|
+
p_builtins = sub.add_parser("builtins", help="Builtin function lookups")
|
|
276
|
+
builtins_sub = p_builtins.add_subparsers(dest="builtins_cmd", required=True)
|
|
277
|
+
|
|
278
|
+
p_blist = builtins_sub.add_parser("list", help="List all builtin functions")
|
|
279
|
+
_add_common(p_blist)
|
|
280
|
+
p_blist.set_defaults(func=_cmd_builtins_list)
|
|
281
|
+
|
|
282
|
+
p_bget = builtins_sub.add_parser("get", help="Look up a builtin by name/keyword")
|
|
283
|
+
p_bget.add_argument("query")
|
|
284
|
+
_add_common(p_bget)
|
|
285
|
+
p_bget.set_defaults(func=_cmd_builtins_get)
|
|
286
|
+
|
|
287
|
+
p_pre = sub.add_parser(
|
|
288
|
+
"prefetch",
|
|
289
|
+
help="Download sources.tar + langref.html so other commands run offline",
|
|
290
|
+
)
|
|
291
|
+
_add_common(p_pre)
|
|
292
|
+
p_pre.set_defaults(func=_cmd_prefetch)
|
|
293
|
+
|
|
294
|
+
p_batch = sub.add_parser(
|
|
295
|
+
"batch",
|
|
296
|
+
help=(
|
|
297
|
+
"Run many lookups in one process; reads one command per line "
|
|
298
|
+
"from stdin (or -f FILE) and frames each output with `===> <cmd>`"
|
|
299
|
+
),
|
|
300
|
+
)
|
|
301
|
+
p_batch.add_argument(
|
|
302
|
+
"-f",
|
|
303
|
+
"--file",
|
|
304
|
+
type=Path,
|
|
305
|
+
default=None,
|
|
306
|
+
help="Read commands from FILE instead of stdin",
|
|
307
|
+
)
|
|
308
|
+
p_batch.set_defaults(func=_cmd_batch)
|
|
309
|
+
|
|
310
|
+
return parser
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def main(argv: list[str] | None = None) -> int:
|
|
314
|
+
parser = build_parser()
|
|
315
|
+
args = parser.parse_args(argv)
|
|
316
|
+
return args.func(args)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
sys.exit(main())
|
zigpeek/fetch.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from importlib.resources import files
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
_CACHE_ENV = "ZIGPEEK_CACHE_DIR"
|
|
8
|
+
_DEFAULT_CACHE_ROOT = Path("/tmp/zigpeek-cache")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def sources_tar_url(zig_version: str) -> str:
|
|
12
|
+
return f"https://ziglang.org/documentation/{zig_version}/std/sources.tar"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def langref_url(zig_version: str) -> str:
|
|
16
|
+
return f"https://ziglang.org/documentation/{zig_version}/"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cache_dir_for(zig_version: str, override: Path | str | None = None) -> Path:
|
|
20
|
+
if override is not None:
|
|
21
|
+
return Path(override) / zig_version
|
|
22
|
+
root = os.environ.get(_CACHE_ENV)
|
|
23
|
+
base = Path(root) if root else _DEFAULT_CACHE_ROOT
|
|
24
|
+
return base / zig_version
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def bundled_path_for(zig_version: str, filename: str) -> Path:
|
|
28
|
+
"""Where a pre-bundled snapshot would live for a given version."""
|
|
29
|
+
return Path(str(files("zigpeek").joinpath("_data", zig_version, filename)))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _http_get_bytes(url: str) -> bytes:
|
|
33
|
+
with httpx.Client(follow_redirects=True, timeout=60.0) as client:
|
|
34
|
+
r = client.get(url)
|
|
35
|
+
r.raise_for_status()
|
|
36
|
+
return r.content
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _read_or_fetch(
|
|
40
|
+
url: str,
|
|
41
|
+
cache_path: Path,
|
|
42
|
+
refresh: bool,
|
|
43
|
+
bundled: Path | None = None,
|
|
44
|
+
) -> bytes:
|
|
45
|
+
if not refresh and bundled is not None and bundled.exists():
|
|
46
|
+
return bundled.read_bytes()
|
|
47
|
+
if not refresh and cache_path.exists():
|
|
48
|
+
return cache_path.read_bytes()
|
|
49
|
+
data = _http_get_bytes(url)
|
|
50
|
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
cache_path.write_bytes(data)
|
|
52
|
+
return data
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def fetch_sources_tar(
|
|
56
|
+
zig_version: str,
|
|
57
|
+
refresh: bool = False,
|
|
58
|
+
cache_dir: Path | str | None = None,
|
|
59
|
+
) -> bytes:
|
|
60
|
+
return _read_or_fetch(
|
|
61
|
+
sources_tar_url(zig_version),
|
|
62
|
+
cache_dir_for(zig_version, cache_dir) / "sources.tar",
|
|
63
|
+
refresh,
|
|
64
|
+
bundled=bundled_path_for(zig_version, "sources.tar"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def fetch_langref_html(
|
|
69
|
+
zig_version: str,
|
|
70
|
+
refresh: bool = False,
|
|
71
|
+
cache_dir: Path | str | None = None,
|
|
72
|
+
) -> str:
|
|
73
|
+
data = _read_or_fetch(
|
|
74
|
+
langref_url(zig_version),
|
|
75
|
+
cache_dir_for(zig_version, cache_dir) / "langref.html",
|
|
76
|
+
refresh,
|
|
77
|
+
bundled=bundled_path_for(zig_version, "langref.html"),
|
|
78
|
+
)
|
|
79
|
+
return data.decode("utf-8")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def prefetch(
|
|
83
|
+
zig_version: str,
|
|
84
|
+
refresh: bool = False,
|
|
85
|
+
cache_dir: Path | str | None = None,
|
|
86
|
+
) -> dict[str, Path]:
|
|
87
|
+
"""Populate the cache so subsequent reads are network-free.
|
|
88
|
+
|
|
89
|
+
Returns a mapping of {"sources.tar": path, "langref.html": path}
|
|
90
|
+
pointing at whichever location the read path will resolve to next
|
|
91
|
+
(bundled snapshot if present, otherwise cache).
|
|
92
|
+
"""
|
|
93
|
+
fetch_sources_tar(zig_version, refresh=refresh, cache_dir=cache_dir)
|
|
94
|
+
fetch_langref_html(zig_version, refresh=refresh, cache_dir=cache_dir)
|
|
95
|
+
bundled_src = bundled_path_for(zig_version, "sources.tar")
|
|
96
|
+
bundled_lang = bundled_path_for(zig_version, "langref.html")
|
|
97
|
+
cache = cache_dir_for(zig_version, cache_dir)
|
|
98
|
+
return {
|
|
99
|
+
"sources.tar": bundled_src if bundled_src.exists() else cache / "sources.tar",
|
|
100
|
+
"langref.html": (
|
|
101
|
+
bundled_lang if bundled_lang.exists() else cache / "langref.html"
|
|
102
|
+
),
|
|
103
|
+
}
|
zigpeek/stdlib.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Markdown rendering — port of ~/Documents/GitHub/zig-mcp/mcp/std.ts.
|
|
2
|
+
|
|
3
|
+
Each function below mirrors a renderer in std.ts. Smoke tests catch
|
|
4
|
+
structural drift; cosmetic whitespace differences are acceptable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from zigpeek.wasm import WasmStd
|
|
8
|
+
|
|
9
|
+
CAT_NAMESPACE = 0
|
|
10
|
+
CAT_CONTAINER = 1
|
|
11
|
+
CAT_GLOBAL_VARIABLE = 2
|
|
12
|
+
CAT_FUNCTION = 3
|
|
13
|
+
CAT_PRIMITIVE = 4
|
|
14
|
+
CAT_ERROR_SET = 5
|
|
15
|
+
CAT_GLOBAL_CONST = 6
|
|
16
|
+
CAT_ALIAS = 7
|
|
17
|
+
CAT_TYPE = 8
|
|
18
|
+
CAT_TYPE_TYPE = 9
|
|
19
|
+
CAT_TYPE_FUNCTION = 10
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def render_search(std: WasmStd, query: str, limit: int = 20) -> str:
|
|
23
|
+
ignore_case = query.lower() == query
|
|
24
|
+
results = std.execute_query(query, ignore_case)
|
|
25
|
+
|
|
26
|
+
md = f'# Search Results\n\nQuery: "{query}"\n\n'
|
|
27
|
+
if results:
|
|
28
|
+
limited = results[:limit]
|
|
29
|
+
md += f"Found {len(results)} results (showing {len(limited)}):\n\n"
|
|
30
|
+
for match in limited:
|
|
31
|
+
md += f"- {std.fully_qualified_name(match)}\n"
|
|
32
|
+
else:
|
|
33
|
+
md += "No results found."
|
|
34
|
+
return md
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def render_get_item(std: WasmStd, name: str, get_source_file: bool = False) -> str:
|
|
38
|
+
decl_index = std.find_decl(name)
|
|
39
|
+
if decl_index is None:
|
|
40
|
+
return f'# Error\n\nDeclaration "{name}" not found.'
|
|
41
|
+
|
|
42
|
+
if get_source_file:
|
|
43
|
+
cur = decl_index
|
|
44
|
+
seen: set[int] = set()
|
|
45
|
+
while True:
|
|
46
|
+
cat = std.categorize_decl(cur, 0)
|
|
47
|
+
if cat != CAT_ALIAS or cur in seen:
|
|
48
|
+
break
|
|
49
|
+
seen.add(cur)
|
|
50
|
+
nxt = std.get_aliasee()
|
|
51
|
+
if nxt is None or nxt == cur:
|
|
52
|
+
break
|
|
53
|
+
cur = nxt
|
|
54
|
+
|
|
55
|
+
file_path = std.decl_file_path(cur)
|
|
56
|
+
if file_path:
|
|
57
|
+
file_decl = std.find_file_root(file_path)
|
|
58
|
+
if file_decl is not None:
|
|
59
|
+
return f"# {file_path}\n\n{std.decl_source_html(file_decl)}"
|
|
60
|
+
return f'# Error\n\nCould not find source file for "{name}".'
|
|
61
|
+
|
|
62
|
+
return _render_decl(std, decl_index)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _render_decl(std: WasmStd, decl_index: int) -> str:
|
|
66
|
+
current = decl_index
|
|
67
|
+
seen: set[int] = set()
|
|
68
|
+
while True:
|
|
69
|
+
category = std.categorize_decl(current, 0)
|
|
70
|
+
if category in (CAT_NAMESPACE, CAT_CONTAINER):
|
|
71
|
+
return _render_namespace(std, current)
|
|
72
|
+
if category in (
|
|
73
|
+
CAT_GLOBAL_VARIABLE,
|
|
74
|
+
CAT_PRIMITIVE,
|
|
75
|
+
CAT_GLOBAL_CONST,
|
|
76
|
+
CAT_TYPE,
|
|
77
|
+
CAT_TYPE_TYPE,
|
|
78
|
+
):
|
|
79
|
+
return _render_global(std, current)
|
|
80
|
+
if category == CAT_FUNCTION:
|
|
81
|
+
return _render_function(std, current)
|
|
82
|
+
if category == CAT_TYPE_FUNCTION:
|
|
83
|
+
return _render_type_function(std, current)
|
|
84
|
+
if category == CAT_ERROR_SET:
|
|
85
|
+
return _render_error_set(std, current)
|
|
86
|
+
if category == CAT_ALIAS:
|
|
87
|
+
if current in seen:
|
|
88
|
+
return _render_not_found()
|
|
89
|
+
seen.add(current)
|
|
90
|
+
aliasee = std.get_aliasee()
|
|
91
|
+
if aliasee is None:
|
|
92
|
+
return _render_not_found()
|
|
93
|
+
current = aliasee
|
|
94
|
+
continue
|
|
95
|
+
raise RuntimeError(f"unrecognized category {category}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _render_namespace(std: WasmStd, decl_index: int) -> str:
|
|
99
|
+
name = std.decl_category_name(decl_index)
|
|
100
|
+
md = f"# {name}\n\n"
|
|
101
|
+
|
|
102
|
+
docs = std.decl_docs_html(decl_index, False)
|
|
103
|
+
if docs:
|
|
104
|
+
md += docs + "\n\n"
|
|
105
|
+
|
|
106
|
+
members = std.namespace_members(decl_index, False)
|
|
107
|
+
fields = std.decl_fields(decl_index)
|
|
108
|
+
md += _render_namespace_md(std, decl_index, members, fields)
|
|
109
|
+
return md
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _render_function(std: WasmStd, decl_index: int) -> str:
|
|
113
|
+
name = std.decl_category_name(decl_index)
|
|
114
|
+
md = f"# {name}\n"
|
|
115
|
+
|
|
116
|
+
docs = std.decl_docs_html(decl_index, False)
|
|
117
|
+
if docs:
|
|
118
|
+
md += "\n" + docs
|
|
119
|
+
|
|
120
|
+
proto = std.decl_fn_proto_html(decl_index, False)
|
|
121
|
+
if proto:
|
|
122
|
+
md += "\n\n## Function Signature\n\n" + proto
|
|
123
|
+
|
|
124
|
+
params = std.decl_params(decl_index)
|
|
125
|
+
if params:
|
|
126
|
+
md += "\n\n## Parameters\n"
|
|
127
|
+
for p in params:
|
|
128
|
+
md += "\n" + std.decl_param_html(decl_index, p)
|
|
129
|
+
|
|
130
|
+
err_set_node = std.fn_error_set(decl_index)
|
|
131
|
+
if err_set_node is not None:
|
|
132
|
+
base_decl = std.fn_error_set_decl(decl_index, err_set_node)
|
|
133
|
+
error_list = std.error_set_node_list(decl_index, err_set_node)
|
|
134
|
+
if error_list:
|
|
135
|
+
md += "\n\n## Errors\n"
|
|
136
|
+
for e in error_list:
|
|
137
|
+
md += "\n" + std.error_html(base_decl, e)
|
|
138
|
+
|
|
139
|
+
doctest = std.decl_doctest_html(decl_index)
|
|
140
|
+
if doctest:
|
|
141
|
+
md += "\n\n## Example Usage\n\n" + doctest
|
|
142
|
+
|
|
143
|
+
source = std.decl_source_html(decl_index)
|
|
144
|
+
if source:
|
|
145
|
+
md += "\n\n## Source Code\n\n" + source
|
|
146
|
+
|
|
147
|
+
return md
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _render_global(std: WasmStd, decl_index: int) -> str:
|
|
151
|
+
name = std.decl_category_name(decl_index)
|
|
152
|
+
md = f"# {name}\n\n"
|
|
153
|
+
|
|
154
|
+
docs = std.decl_docs_html(decl_index, True)
|
|
155
|
+
if docs:
|
|
156
|
+
md += docs + "\n\n"
|
|
157
|
+
|
|
158
|
+
source = std.decl_source_html(decl_index)
|
|
159
|
+
if source:
|
|
160
|
+
md += "## Source Code\n\n" + source + "\n\n"
|
|
161
|
+
|
|
162
|
+
return md
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _render_type_function(std: WasmStd, decl_index: int) -> str:
|
|
166
|
+
name = std.decl_category_name(decl_index)
|
|
167
|
+
md = f"# {name}\n\n"
|
|
168
|
+
|
|
169
|
+
docs = std.decl_docs_html(decl_index, False)
|
|
170
|
+
if docs:
|
|
171
|
+
md += docs + "\n\n"
|
|
172
|
+
|
|
173
|
+
params = std.decl_params(decl_index)
|
|
174
|
+
if params:
|
|
175
|
+
md += "## Parameters\n\n"
|
|
176
|
+
for p in params:
|
|
177
|
+
md += std.decl_param_html(decl_index, p) + "\n\n"
|
|
178
|
+
|
|
179
|
+
doctest = std.decl_doctest_html(decl_index)
|
|
180
|
+
if doctest:
|
|
181
|
+
md += "## Example Usage\n\n" + doctest + "\n\n"
|
|
182
|
+
|
|
183
|
+
members = std.type_fn_members(decl_index, False)
|
|
184
|
+
fields = std.type_fn_fields(decl_index)
|
|
185
|
+
if members or fields:
|
|
186
|
+
md += _render_namespace_md(std, decl_index, members, fields)
|
|
187
|
+
else:
|
|
188
|
+
source = std.decl_source_html(decl_index)
|
|
189
|
+
if source:
|
|
190
|
+
md += "## Source Code\n\n" + source + "\n\n"
|
|
191
|
+
|
|
192
|
+
return md
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _render_error_set(std: WasmStd, decl_index: int) -> str:
|
|
196
|
+
name = std.decl_category_name(decl_index)
|
|
197
|
+
md = f"# {name}\n\n"
|
|
198
|
+
|
|
199
|
+
docs = std.decl_docs_html(decl_index, False)
|
|
200
|
+
if docs:
|
|
201
|
+
md += docs + "\n\n"
|
|
202
|
+
|
|
203
|
+
error_list = std.decl_error_set(decl_index)
|
|
204
|
+
if error_list:
|
|
205
|
+
md += "## Errors\n\n"
|
|
206
|
+
for e in error_list:
|
|
207
|
+
md += std.error_html(decl_index, e) + "\n\n"
|
|
208
|
+
|
|
209
|
+
return md
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _render_not_found() -> str:
|
|
213
|
+
return "# Error\n\nDeclaration not found."
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _render_namespace_md(
|
|
217
|
+
std: WasmStd,
|
|
218
|
+
base_decl: int,
|
|
219
|
+
members: list[int],
|
|
220
|
+
fields: list[int],
|
|
221
|
+
) -> str:
|
|
222
|
+
types_list: list[tuple[int, int]] = []
|
|
223
|
+
namespaces_list: list[tuple[int, int]] = []
|
|
224
|
+
err_sets_list: list[tuple[int, int]] = []
|
|
225
|
+
fns_list: list[int] = []
|
|
226
|
+
vars_list: list[int] = []
|
|
227
|
+
vals_list: list[tuple[int, int]] = []
|
|
228
|
+
|
|
229
|
+
for original in members:
|
|
230
|
+
member = original
|
|
231
|
+
seen: set[int] = set()
|
|
232
|
+
while True:
|
|
233
|
+
cat = std.categorize_decl(member, 0)
|
|
234
|
+
if cat == CAT_NAMESPACE:
|
|
235
|
+
namespaces_list.append((original, member))
|
|
236
|
+
elif cat == CAT_CONTAINER:
|
|
237
|
+
types_list.append((original, member))
|
|
238
|
+
elif cat == CAT_GLOBAL_VARIABLE:
|
|
239
|
+
vars_list.append(member)
|
|
240
|
+
elif cat == CAT_FUNCTION:
|
|
241
|
+
fns_list.append(member)
|
|
242
|
+
elif cat in (CAT_TYPE, CAT_TYPE_TYPE, CAT_TYPE_FUNCTION):
|
|
243
|
+
types_list.append((original, member))
|
|
244
|
+
elif cat == CAT_ERROR_SET:
|
|
245
|
+
err_sets_list.append((original, member))
|
|
246
|
+
elif cat in (CAT_GLOBAL_CONST, CAT_PRIMITIVE):
|
|
247
|
+
vals_list.append((original, member))
|
|
248
|
+
elif cat == CAT_ALIAS:
|
|
249
|
+
if member in seen:
|
|
250
|
+
vals_list.append((original, member))
|
|
251
|
+
break
|
|
252
|
+
seen.add(member)
|
|
253
|
+
nxt = std.get_aliasee()
|
|
254
|
+
if nxt is None:
|
|
255
|
+
vals_list.append((original, member))
|
|
256
|
+
break
|
|
257
|
+
member = nxt
|
|
258
|
+
continue
|
|
259
|
+
else:
|
|
260
|
+
raise RuntimeError(f"unknown category: {cat}")
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
md = ""
|
|
264
|
+
|
|
265
|
+
if types_list:
|
|
266
|
+
md += "## Types\n\n"
|
|
267
|
+
for original, _ in types_list:
|
|
268
|
+
md += f"- {std.decl_name(original)}\n"
|
|
269
|
+
md += "\n"
|
|
270
|
+
|
|
271
|
+
if namespaces_list:
|
|
272
|
+
md += "## Namespaces\n\n"
|
|
273
|
+
for original, _ in namespaces_list:
|
|
274
|
+
md += f"- {std.decl_name(original)}\n"
|
|
275
|
+
md += "\n"
|
|
276
|
+
|
|
277
|
+
if err_sets_list:
|
|
278
|
+
md += "## Error Sets\n\n"
|
|
279
|
+
for original, _ in err_sets_list:
|
|
280
|
+
md += f"- {std.decl_name(original)}\n"
|
|
281
|
+
md += "\n"
|
|
282
|
+
|
|
283
|
+
if fns_list:
|
|
284
|
+
md += "## Functions\n\n"
|
|
285
|
+
for decl in fns_list:
|
|
286
|
+
name = std.decl_name(decl)
|
|
287
|
+
proto = std.decl_fn_proto_html(decl, True)
|
|
288
|
+
docs = std.decl_docs_html(decl, True)
|
|
289
|
+
md += f"### {name}\n\n"
|
|
290
|
+
if proto:
|
|
291
|
+
md += proto + "\n\n"
|
|
292
|
+
if docs:
|
|
293
|
+
md += docs + "\n\n"
|
|
294
|
+
|
|
295
|
+
if fields:
|
|
296
|
+
md += "## Fields\n\n"
|
|
297
|
+
for f in fields:
|
|
298
|
+
md += std.decl_field_html(base_decl, f) + "\n\n"
|
|
299
|
+
|
|
300
|
+
if vars_list:
|
|
301
|
+
md += "## Global Variables\n\n"
|
|
302
|
+
for decl in vars_list:
|
|
303
|
+
name = std.decl_name(decl)
|
|
304
|
+
type_html = std.decl_type_html(decl)
|
|
305
|
+
docs = std.decl_docs_html(decl, True)
|
|
306
|
+
md += f"### {name}\n\n"
|
|
307
|
+
if type_html:
|
|
308
|
+
md += f"Type: {type_html}\n\n"
|
|
309
|
+
if docs:
|
|
310
|
+
md += docs + "\n\n"
|
|
311
|
+
|
|
312
|
+
if vals_list:
|
|
313
|
+
md += "## Values\n\n"
|
|
314
|
+
for original, member in vals_list:
|
|
315
|
+
name = std.decl_name(original)
|
|
316
|
+
type_html = std.decl_type_html(member)
|
|
317
|
+
docs = std.decl_docs_html(member, True)
|
|
318
|
+
md += f"### {name}\n\n"
|
|
319
|
+
if type_html:
|
|
320
|
+
md += f"Type: {type_html}\n\n"
|
|
321
|
+
if docs:
|
|
322
|
+
md += docs + "\n\n"
|
|
323
|
+
|
|
324
|
+
return md
|
zigpeek/version.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
DEFAULT_ZIG_VERSION = "0.16.0"
|
|
4
|
+
_ENV_VAR = "ZIGPEEK_VERSION"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def resolve_version(cli_value: str | None) -> str:
|
|
8
|
+
if cli_value is not None:
|
|
9
|
+
if cli_value == "":
|
|
10
|
+
raise ValueError("--version cannot be empty")
|
|
11
|
+
return cli_value
|
|
12
|
+
env_value = os.environ.get(_ENV_VAR)
|
|
13
|
+
if env_value:
|
|
14
|
+
return env_value
|
|
15
|
+
return DEFAULT_ZIG_VERSION
|
zigpeek/wasm.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
|
|
3
|
+
import wasmtime
|
|
4
|
+
|
|
5
|
+
_INVALID_INDEX = 0xFFFF_FFFF
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def unpack_string(memory: bytes, ptr: int, length: int) -> str:
|
|
9
|
+
if length == 0:
|
|
10
|
+
return ""
|
|
11
|
+
return memory[ptr : ptr + length].decode("utf-8", errors="replace")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def unpack_slice32(memory: bytes, ptr: int, length: int) -> list[int]:
|
|
15
|
+
if length == 0:
|
|
16
|
+
return []
|
|
17
|
+
return list(struct.unpack_from(f"<{length}I", memory, ptr))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def unpack_slice64(memory: bytes, ptr: int, length: int) -> list[int]:
|
|
21
|
+
if length == 0:
|
|
22
|
+
return []
|
|
23
|
+
return list(struct.unpack_from(f"<{length}Q", memory, ptr))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def split_packed(packed: int) -> tuple[int, int]:
|
|
27
|
+
"""Decode the JS BigInt packing: low32 = ptr, high32 = length."""
|
|
28
|
+
return packed & 0xFFFFFFFF, packed >> 32
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WasmStd:
|
|
32
|
+
"""Drive the autodoc WASM module loaded from sources.tar.
|
|
33
|
+
|
|
34
|
+
Mirrors the JS interface in ~/Documents/GitHub/zig-mcp/mcp/std.ts.
|
|
35
|
+
The WASM imports a single js.log function (level, ptr, len) — we
|
|
36
|
+
forward errors as exceptions and discard the rest.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, wasm_bytes: bytes, sources_tar: bytes) -> None:
|
|
40
|
+
self._engine = wasmtime.Engine()
|
|
41
|
+
self._store = wasmtime.Store(self._engine)
|
|
42
|
+
module = wasmtime.Module(self._engine, wasm_bytes)
|
|
43
|
+
|
|
44
|
+
log_type = wasmtime.FuncType(
|
|
45
|
+
[wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32()],
|
|
46
|
+
[],
|
|
47
|
+
)
|
|
48
|
+
log_func = wasmtime.Func(self._store, log_type, self._on_log)
|
|
49
|
+
instance = wasmtime.Instance(self._store, module, [log_func])
|
|
50
|
+
|
|
51
|
+
self._exports = instance.exports(self._store)
|
|
52
|
+
self._memory: wasmtime.Memory = self._exports["memory"]
|
|
53
|
+
|
|
54
|
+
ptr = self._call("alloc", len(sources_tar))
|
|
55
|
+
self._memory.write(self._store, sources_tar, ptr)
|
|
56
|
+
self._call("unpack", ptr, len(sources_tar))
|
|
57
|
+
|
|
58
|
+
def _on_log(self, level: int, ptr: int, length: int) -> None:
|
|
59
|
+
if level == 0:
|
|
60
|
+
raise RuntimeError(f"WASM log error: {self._read_string(ptr, length)}")
|
|
61
|
+
|
|
62
|
+
def _call(self, name: str, *args: int) -> int:
|
|
63
|
+
result = self._exports[name](self._store, *args)
|
|
64
|
+
return int(result) if result is not None else 0
|
|
65
|
+
|
|
66
|
+
def _call_optional(self, name: str, *args: int) -> int | None:
|
|
67
|
+
result = self._call(name, *args)
|
|
68
|
+
return None if result in (-1, _INVALID_INDEX) else result
|
|
69
|
+
|
|
70
|
+
def _read_string(self, ptr: int, length: int) -> str:
|
|
71
|
+
if length == 0:
|
|
72
|
+
return ""
|
|
73
|
+
return self._memory.read(self._store, ptr, ptr + length).decode(
|
|
74
|
+
"utf-8", errors="replace"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _read_slice32(self, ptr: int, length: int) -> list[int]:
|
|
78
|
+
if length == 0:
|
|
79
|
+
return []
|
|
80
|
+
raw = self._memory.read(self._store, ptr, ptr + length * 4)
|
|
81
|
+
return list(struct.unpack_from(f"<{length}I", raw))
|
|
82
|
+
|
|
83
|
+
def _read_slice64(self, ptr: int, length: int) -> list[int]:
|
|
84
|
+
if length == 0:
|
|
85
|
+
return []
|
|
86
|
+
raw = self._memory.read(self._store, ptr, ptr + length * 8)
|
|
87
|
+
return list(struct.unpack_from(f"<{length}Q", raw))
|
|
88
|
+
|
|
89
|
+
def _packed_string(self, packed: int) -> str:
|
|
90
|
+
return self._read_string(*split_packed(packed))
|
|
91
|
+
|
|
92
|
+
def _packed_slice32(self, packed: int) -> list[int]:
|
|
93
|
+
return self._read_slice32(*split_packed(packed))
|
|
94
|
+
|
|
95
|
+
def _packed_slice64(self, packed: int) -> list[int]:
|
|
96
|
+
return self._read_slice64(*split_packed(packed))
|
|
97
|
+
|
|
98
|
+
def _set_string(self, export: str, s: str) -> None:
|
|
99
|
+
encoded = s.encode("utf-8")
|
|
100
|
+
ptr = self._call(export, len(encoded))
|
|
101
|
+
self._memory.write(self._store, encoded, ptr)
|
|
102
|
+
|
|
103
|
+
def list_modules(self) -> list[str]:
|
|
104
|
+
out: list[str] = []
|
|
105
|
+
i = 0
|
|
106
|
+
while True:
|
|
107
|
+
name = self._packed_string(self._call("module_name", i))
|
|
108
|
+
if not name:
|
|
109
|
+
break
|
|
110
|
+
out.append(name)
|
|
111
|
+
i += 1
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
def find_decl(self, fqn: str) -> int | None:
|
|
115
|
+
self._set_string("set_input_string", fqn)
|
|
116
|
+
return self._call_optional("find_decl")
|
|
117
|
+
|
|
118
|
+
def find_file_root(self, path: str) -> int | None:
|
|
119
|
+
self._set_string("set_input_string", path)
|
|
120
|
+
return self._call_optional("find_file_root")
|
|
121
|
+
|
|
122
|
+
def find_module_root(self, pkg_index: int) -> int:
|
|
123
|
+
return self._call("find_module_root", pkg_index)
|
|
124
|
+
|
|
125
|
+
def categorize_decl(self, decl_index: int, resolve_alias_to: int = 0) -> int:
|
|
126
|
+
return self._call("categorize_decl", decl_index, resolve_alias_to)
|
|
127
|
+
|
|
128
|
+
def get_aliasee(self) -> int | None:
|
|
129
|
+
return self._call_optional("get_aliasee")
|
|
130
|
+
|
|
131
|
+
def decl_parent(self, decl_index: int) -> int | None:
|
|
132
|
+
return self._call_optional("decl_parent", decl_index)
|
|
133
|
+
|
|
134
|
+
def fully_qualified_name(self, decl_index: int) -> str:
|
|
135
|
+
return self._packed_string(self._call("decl_fqn", decl_index))
|
|
136
|
+
|
|
137
|
+
def decl_name(self, decl_index: int) -> str:
|
|
138
|
+
return self._packed_string(self._call("decl_name", decl_index))
|
|
139
|
+
|
|
140
|
+
def decl_category_name(self, decl_index: int) -> str:
|
|
141
|
+
return self._packed_string(self._call("decl_category_name", decl_index))
|
|
142
|
+
|
|
143
|
+
def decl_docs_html(self, decl_index: int, short: bool) -> str:
|
|
144
|
+
return self._packed_string(self._call("decl_docs_html", decl_index, int(short)))
|
|
145
|
+
|
|
146
|
+
def decl_fn_proto_html(self, decl_index: int, linkify: bool) -> str:
|
|
147
|
+
return self._packed_string(
|
|
148
|
+
self._call("decl_fn_proto_html", decl_index, int(linkify))
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def decl_param_html(self, decl_index: int, param: int) -> str:
|
|
152
|
+
return self._packed_string(self._call("decl_param_html", decl_index, param))
|
|
153
|
+
|
|
154
|
+
def decl_doctest_html(self, decl_index: int) -> str:
|
|
155
|
+
return self._packed_string(self._call("decl_doctest_html", decl_index))
|
|
156
|
+
|
|
157
|
+
def decl_source_html(self, decl_index: int) -> str:
|
|
158
|
+
return self._packed_string(self._call("decl_source_html", decl_index))
|
|
159
|
+
|
|
160
|
+
def decl_field_html(self, base_decl: int, field: int) -> str:
|
|
161
|
+
return self._packed_string(self._call("decl_field_html", base_decl, field))
|
|
162
|
+
|
|
163
|
+
def decl_type_html(self, decl_index: int) -> str:
|
|
164
|
+
return self._packed_string(self._call("decl_type_html", decl_index))
|
|
165
|
+
|
|
166
|
+
def decl_file_path(self, decl_index: int) -> str:
|
|
167
|
+
return self._packed_string(self._call("decl_file_path", decl_index))
|
|
168
|
+
|
|
169
|
+
def decl_params(self, decl_index: int) -> list[int]:
|
|
170
|
+
return self._packed_slice32(self._call("decl_params", decl_index))
|
|
171
|
+
|
|
172
|
+
def decl_fields(self, decl_index: int) -> list[int]:
|
|
173
|
+
return self._packed_slice32(self._call("decl_fields", decl_index))
|
|
174
|
+
|
|
175
|
+
def decl_error_set(self, decl_index: int) -> list[int]:
|
|
176
|
+
return self._packed_slice64(self._call("decl_error_set", decl_index))
|
|
177
|
+
|
|
178
|
+
def namespace_members(self, decl_index: int, include_private: bool) -> list[int]:
|
|
179
|
+
return self._packed_slice32(
|
|
180
|
+
self._call("namespace_members", decl_index, int(include_private))
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def type_fn_members(self, decl_index: int, include_private: bool) -> list[int]:
|
|
184
|
+
return self._packed_slice32(
|
|
185
|
+
self._call("type_fn_members", decl_index, int(include_private))
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def type_fn_fields(self, decl_index: int) -> list[int]:
|
|
189
|
+
return self._packed_slice32(self._call("type_fn_fields", decl_index))
|
|
190
|
+
|
|
191
|
+
def fn_error_set(self, decl_index: int) -> int | None:
|
|
192
|
+
result = self._call("fn_error_set", decl_index)
|
|
193
|
+
return None if result == 0 else result
|
|
194
|
+
|
|
195
|
+
def fn_error_set_decl(self, decl_index: int, err_set_node: int) -> int:
|
|
196
|
+
return self._call("fn_error_set_decl", decl_index, err_set_node)
|
|
197
|
+
|
|
198
|
+
def error_set_node_list(self, base_decl: int, err_set_node: int) -> list[int]:
|
|
199
|
+
return self._packed_slice64(
|
|
200
|
+
self._call("error_set_node_list", base_decl, err_set_node)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def error_html(self, base_decl: int, err: int) -> str:
|
|
204
|
+
return self._packed_string(self._call("error_html", base_decl, err))
|
|
205
|
+
|
|
206
|
+
def execute_query(self, query: str, ignore_case: bool) -> list[int]:
|
|
207
|
+
self._set_string("query_begin", query)
|
|
208
|
+
ptr = self._call("query_exec", int(ignore_case))
|
|
209
|
+
length = self._read_slice32(ptr, 1)[0]
|
|
210
|
+
return self._read_slice32(ptr + 4, length)
|
|
@@ -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,13 @@
|
|
|
1
|
+
zigpeek/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
zigpeek/builtins.py,sha256=bObngeM-ECoxYy6W_GK-gK0XddftRULZVoSyv5u9xvw,5209
|
|
3
|
+
zigpeek/cli.py,sha256=d54AG3Zoc05ZT8K5G2Ov5biRSnyAruqtETWdCkpkaxg,10139
|
|
4
|
+
zigpeek/fetch.py,sha256=NEv5Ro0y3rMebjsDUoLobFxRrcjkRLt3bIcwinmaQcg,3183
|
|
5
|
+
zigpeek/stdlib.py,sha256=8--XHGvI-lt4aFQpEYMohUcW_siyZ1njIe9lzvjiZxw,9805
|
|
6
|
+
zigpeek/version.py,sha256=Jgu1ea8iDxoaOuzFiloUfyKZyBZE-jXBSKyBNVALfCU,380
|
|
7
|
+
zigpeek/wasm.py,sha256=YpdZqiXcbUCzfwarjXQ1hSPxl5C3Epe9ojL5YDpYvSo,8141
|
|
8
|
+
zigpeek/_vendor/main.wasm,sha256=p0uEHkPeJKd_SaGAsxSObqX1HvOvg-zIRtqCV8xGBbU,198076
|
|
9
|
+
zigpeek-0.3.0.dist-info/METADATA,sha256=yMS1-Xb_YoaXgH6IIzTvhDfNwhb7hnAPGLaZJW7CtWc,287
|
|
10
|
+
zigpeek-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
zigpeek-0.3.0.dist-info/entry_points.txt,sha256=pgEKNRrTppa0T5zXRA9VYU_wKK1pe_hvCTdmQWf3Ivo,45
|
|
12
|
+
zigpeek-0.3.0.dist-info/licenses/LICENSE,sha256=uNuWcR4sJ7681Zgk8CdW_6u2WLeeVqScvIOLuQoh8z8,1298
|
|
13
|
+
zigpeek-0.3.0.dist-info/RECORD,,
|
|
@@ -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.
|