ghostfox 0.2.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.
- ghostfox-0.2.0/PKG-INFO +35 -0
- ghostfox-0.2.0/README.md +22 -0
- ghostfox-0.2.0/ghostfox/__init__.py +16 -0
- ghostfox-0.2.0/ghostfox/engine.py +110 -0
- ghostfox-0.2.0/ghostfox/mcp.py +158 -0
- ghostfox-0.2.0/ghostfox.egg-info/PKG-INFO +35 -0
- ghostfox-0.2.0/ghostfox.egg-info/SOURCES.txt +9 -0
- ghostfox-0.2.0/ghostfox.egg-info/dependency_links.txt +1 -0
- ghostfox-0.2.0/ghostfox.egg-info/top_level.txt +1 -0
- ghostfox-0.2.0/pyproject.toml +23 -0
- ghostfox-0.2.0/setup.cfg +4 -0
ghostfox-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ghostfox
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Ghostfox — the agent-native stealth browser you can own. Python client + engine installer for the Ghostfox MCP runtime.
|
|
5
|
+
Author: autokeren
|
|
6
|
+
License: MIT OR Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/autokeren/ghostfox
|
|
8
|
+
Keywords: browser,stealth,anti-detect,mcp,ai-agents,scraping
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# ghostfox (Python)
|
|
15
|
+
|
|
16
|
+
Python surface for the [Ghostfox](https://github.com/autokeren/ghostfox)
|
|
17
|
+
agent-native stealth browser.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import ghostfox
|
|
21
|
+
|
|
22
|
+
# one-time: download the prebuilt engine + runtime (Linux x86_64)
|
|
23
|
+
ghostfox.install_engine()
|
|
24
|
+
ghostfox.install_runtime()
|
|
25
|
+
|
|
26
|
+
fox = ghostfox.GhostfoxMCP()
|
|
27
|
+
sid = fox.session_create(platform="android") # coherent mobile persona
|
|
28
|
+
page = fox.page_open(sid, "https://example.com")
|
|
29
|
+
print(fox.page_snapshot(sid, page)["content"])
|
|
30
|
+
fox.page_screenshot(sid, page) # evidence + live view
|
|
31
|
+
fox.close()
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Zero Python dependencies — it drives the Rust MCP runtime over stdio, the
|
|
35
|
+
same server AI agents use.
|
ghostfox-0.2.0/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ghostfox (Python)
|
|
2
|
+
|
|
3
|
+
Python surface for the [Ghostfox](https://github.com/autokeren/ghostfox)
|
|
4
|
+
agent-native stealth browser.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
import ghostfox
|
|
8
|
+
|
|
9
|
+
# one-time: download the prebuilt engine + runtime (Linux x86_64)
|
|
10
|
+
ghostfox.install_engine()
|
|
11
|
+
ghostfox.install_runtime()
|
|
12
|
+
|
|
13
|
+
fox = ghostfox.GhostfoxMCP()
|
|
14
|
+
sid = fox.session_create(platform="android") # coherent mobile persona
|
|
15
|
+
page = fox.page_open(sid, "https://example.com")
|
|
16
|
+
print(fox.page_snapshot(sid, page)["content"])
|
|
17
|
+
fox.page_screenshot(sid, page) # evidence + live view
|
|
18
|
+
fox.close()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Zero Python dependencies — it drives the Rust MCP runtime over stdio, the
|
|
22
|
+
same server AI agents use.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Ghostfox — the agent-native stealth browser you can own.
|
|
2
|
+
|
|
3
|
+
Python surface for the Ghostfox stack:
|
|
4
|
+
- ``ghostfox.install_engine()`` — download + unpack the prebuilt engine
|
|
5
|
+
- ``ghostfox.GhostfoxMCP`` — drive the runtime (the same MCP server agents
|
|
6
|
+
use) from plain Python
|
|
7
|
+
|
|
8
|
+
The runtime is a Rust binary (``ghostcloak-mcp``); this package wraps it so
|
|
9
|
+
the Python world gets the same one-command story as ``pip install browser-use``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .mcp import GhostfoxMCP, McpError
|
|
13
|
+
from .engine import install_engine, engine_home
|
|
14
|
+
|
|
15
|
+
__version__ = "0.2.0"
|
|
16
|
+
__all__ = ["GhostfoxMCP", "McpError", "install_engine", "engine_home"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Engine installer: fetch the prebuilt Ghostfox engine from GitHub Releases.
|
|
2
|
+
|
|
3
|
+
Layout after install: ``~/.ghostfox/engine`` (override the root with
|
|
4
|
+
``GHOSTFOX_HOME_ROOT``). Point ``GHOSTFOX_HOME`` at the returned path when
|
|
5
|
+
launching the runtime.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import sys
|
|
15
|
+
import urllib.request
|
|
16
|
+
import zipfile
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
REPO = "autokeren/ghostfox"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _latest_release() -> dict:
|
|
23
|
+
url = f"https://api.github.com/repos/{REPO}//releases/latest"
|
|
24
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ghostfox-py"})
|
|
25
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
26
|
+
return json.load(r)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def engine_home() -> Path:
|
|
30
|
+
"""Where the engine is (or will be) installed."""
|
|
31
|
+
root = Path(os.environ.get("GHOSTFOX_HOME_ROOT", Path.home() / ".ghostfox"))
|
|
32
|
+
return root / "engine"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def install_engine(dest: Path | None = None, quiet: bool = False) -> Path:
|
|
36
|
+
"""Download and unpack the latest prebuilt engine. Returns its path."""
|
|
37
|
+
dest = dest or engine_home()
|
|
38
|
+
if (dest / "ghostfox-bin").exists():
|
|
39
|
+
if not quiet:
|
|
40
|
+
print(f"engine already installed at {dest}", file=sys.stderr)
|
|
41
|
+
return dest
|
|
42
|
+
|
|
43
|
+
rel = _latest_release()
|
|
44
|
+
assets = {
|
|
45
|
+
a["name"]: a["browser_download_url"] for a in rel.get("assets", [])
|
|
46
|
+
}
|
|
47
|
+
url = next(
|
|
48
|
+
(u for n, u in assets.items() if "lin.x86_64" in n and n.endswith(".zip")),
|
|
49
|
+
None,
|
|
50
|
+
)
|
|
51
|
+
if not url:
|
|
52
|
+
raise RuntimeError(f"no Linux x86_64 engine asset in release {rel.get('tag_name')}")
|
|
53
|
+
|
|
54
|
+
if not quiet:
|
|
55
|
+
print(f"downloading {url.split('/')[-1]} ...", file=sys.stderr)
|
|
56
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ghostfox-py"})
|
|
57
|
+
data = urllib.request.urlopen(req, timeout=600).read()
|
|
58
|
+
|
|
59
|
+
tmp = dest.parent / ".engine-download"
|
|
60
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
61
|
+
tmp.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
with zipfile.ZipFile(io.BytesIO(data)) as zf:
|
|
63
|
+
zf.extractall(tmp)
|
|
64
|
+
|
|
65
|
+
src = tmp / "ghostfox" if (tmp / "ghostfox").is_dir() else tmp
|
|
66
|
+
shutil.rmtree(dest, ignore_errors=True)
|
|
67
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
shutil.move(str(src), dest)
|
|
69
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
70
|
+
if not quiet:
|
|
71
|
+
print(f"engine installed at {dest}", file=sys.stderr)
|
|
72
|
+
return dest
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def install_runtime(dest: Path | None = None, quiet: bool = False) -> Path:
|
|
76
|
+
"""Download the prebuilt ``ghostcloak-mcp`` runtime binary (Linux x86_64)."""
|
|
77
|
+
dest = dest or engine_home().parent / "mcp" / "ghostcloak-mcp"
|
|
78
|
+
if dest.exists():
|
|
79
|
+
return dest
|
|
80
|
+
rel = _latest_release()
|
|
81
|
+
url = next(
|
|
82
|
+
(a["browser_download_url"] for a in rel.get("assets", []) if a["name"] == "ghostcloak-mcp"),
|
|
83
|
+
None,
|
|
84
|
+
)
|
|
85
|
+
if not url:
|
|
86
|
+
raise RuntimeError("no prebuilt runtime asset — build it with cargo (see README)")
|
|
87
|
+
if not quiet:
|
|
88
|
+
print("downloading ghostcloak-mcp ...", file=sys.stderr)
|
|
89
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ghostfox-py"})
|
|
90
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
dest.write_bytes(urllib.request.urlopen(req, timeout=120).read())
|
|
92
|
+
dest.chmod(0o755)
|
|
93
|
+
return dest
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def default_runtime() -> Path:
|
|
97
|
+
"""Find a runtime: $GHOSTFOX_MCP, the installed copy, or one on PATH."""
|
|
98
|
+
if v := os.environ.get("GHOSTFOX_MCP"):
|
|
99
|
+
p = Path(v)
|
|
100
|
+
if p.exists():
|
|
101
|
+
return p
|
|
102
|
+
installed = engine_home().parent / "mcp" / "ghostcloak-mcp"
|
|
103
|
+
if installed.exists():
|
|
104
|
+
return installed
|
|
105
|
+
if shutil.which("ghostcloak-mcp"):
|
|
106
|
+
return Path(shutil.which("ghostcloak-mcp"))
|
|
107
|
+
raise RuntimeError(
|
|
108
|
+
"runtime not found: set GHOSTFOX_MCP, or call ghostfox.install_runtime()"
|
|
109
|
+
)
|
|
110
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Synchronous Python client for the Ghostfox MCP runtime (ghostcloak-mcp).
|
|
2
|
+
|
|
3
|
+
The runtime is the same MCP server AI agents use; this client gives plain
|
|
4
|
+
Python the same surface:
|
|
5
|
+
|
|
6
|
+
from ghostfox import GhostfoxMCP
|
|
7
|
+
|
|
8
|
+
fox = GhostfoxMCP() # finds/downloads nothing; requires runtime+engine
|
|
9
|
+
sid = fox.session_create(platform="android")
|
|
10
|
+
page = fox.page_open(sid, "https://example.com")
|
|
11
|
+
print(fox.page_snapshot(sid, page)["content"])
|
|
12
|
+
fox.close()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from .engine import default_runtime
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class McpError(RuntimeError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GhostfoxMCP:
|
|
32
|
+
"""Drive the Ghostfox runtime over MCP stdio from Python."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
runtime: Path | str | None = None,
|
|
37
|
+
engine_home: Path | str | None = None,
|
|
38
|
+
env: dict[str, str] | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.runtime = Path(runtime) if runtime else default_runtime()
|
|
41
|
+
self.env = dict(os.environ)
|
|
42
|
+
self.env["GHOSTFOX_HOME"] = str(engine_home or os.environ.get("GHOSTFOX_HOME") or Path.home() / ".ghostfox" / "engine")
|
|
43
|
+
if env:
|
|
44
|
+
self.env.update(env)
|
|
45
|
+
self.env.setdefault("RUST_LOG", "warn")
|
|
46
|
+
|
|
47
|
+
self.proc = subprocess.Popen(
|
|
48
|
+
[str(self.runtime)],
|
|
49
|
+
stdin=subprocess.PIPE,
|
|
50
|
+
stdout=subprocess.PIPE,
|
|
51
|
+
stderr=subprocess.DEVNULL,
|
|
52
|
+
text=True,
|
|
53
|
+
bufsize=1,
|
|
54
|
+
env=self.env,
|
|
55
|
+
)
|
|
56
|
+
self._id = 0
|
|
57
|
+
self._call("initialize", {
|
|
58
|
+
"protocolVersion": "2024-11-05",
|
|
59
|
+
"capabilities": {},
|
|
60
|
+
"clientInfo": {"name": "ghostfox-py", "version": "0.1.0"},
|
|
61
|
+
})
|
|
62
|
+
self._notify("notifications/initialized")
|
|
63
|
+
|
|
64
|
+
# -- protocol plumbing ---------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def _send(self, method: str, params: dict | None = None, notify: bool = False) -> None:
|
|
67
|
+
msg: dict[str, Any] = {"jsonrpc": "2.0", "method": method}
|
|
68
|
+
if params is not None:
|
|
69
|
+
msg["params"] = params
|
|
70
|
+
if not notify:
|
|
71
|
+
self._id += 1
|
|
72
|
+
msg["id"] = self._id
|
|
73
|
+
self.proc.stdin.write(json.dumps(msg) + "\n")
|
|
74
|
+
self.proc.stdin.flush()
|
|
75
|
+
|
|
76
|
+
def _recv(self, want: int, timeout: float = 120.0) -> dict:
|
|
77
|
+
deadline = time.time() + timeout
|
|
78
|
+
while time.time() < deadline:
|
|
79
|
+
line = self.proc.stdout.readline()
|
|
80
|
+
if not line:
|
|
81
|
+
raise McpError("runtime closed the connection")
|
|
82
|
+
line = line.strip()
|
|
83
|
+
if not line:
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
resp = json.loads(line)
|
|
87
|
+
except json.JSONDecodeError:
|
|
88
|
+
continue
|
|
89
|
+
if resp.get("id") == want:
|
|
90
|
+
return resp
|
|
91
|
+
raise McpError(f"timeout waiting for response {want}")
|
|
92
|
+
|
|
93
|
+
def _call(self, method: str, params: dict | None = None, timeout: float = 120.0) -> Any:
|
|
94
|
+
self._send(method, params)
|
|
95
|
+
resp = self._recv(self._id, timeout)
|
|
96
|
+
if "error" in resp:
|
|
97
|
+
raise McpError(f"{method}: {resp['error'].get('message')}")
|
|
98
|
+
return resp.get("result")
|
|
99
|
+
|
|
100
|
+
def _notify(self, method: str) -> None:
|
|
101
|
+
self._send(method, notify=True)
|
|
102
|
+
|
|
103
|
+
def _tool(self, name: str, args: dict, timeout: float = 180.0) -> str:
|
|
104
|
+
result = self._call("tools/call", {"name": name, "arguments": args}, timeout)
|
|
105
|
+
if result.get("isError"):
|
|
106
|
+
texts = [c.get("text", "") for c in result.get("content", [])]
|
|
107
|
+
raise McpError(f"{name}: {' '.join(texts)}")
|
|
108
|
+
return result["content"][0]["text"]
|
|
109
|
+
|
|
110
|
+
# -- the browser surface ---------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def session_create(self, platform: str | None = None, profile_dir: str | None = None, proxy: str | None = None) -> str:
|
|
113
|
+
args: dict[str, Any] = {}
|
|
114
|
+
if platform:
|
|
115
|
+
args["platform"] = platform
|
|
116
|
+
if profile_dir:
|
|
117
|
+
args["profile_dir"] = profile_dir
|
|
118
|
+
if proxy:
|
|
119
|
+
args["proxy"] = proxy
|
|
120
|
+
return self._tool("session_create", args)
|
|
121
|
+
|
|
122
|
+
def page_open(self, session_id: str, url: str) -> str:
|
|
123
|
+
return self._tool("page_open", {"session_id": session_id, "url": url})
|
|
124
|
+
|
|
125
|
+
def page_snapshot(self, session_id: str, page_id: str) -> dict:
|
|
126
|
+
return json.loads(self._tool("page_snapshot", {"session_id": session_id, "page_id": page_id}))
|
|
127
|
+
|
|
128
|
+
def page_screenshot(self, session_id: str, page_id: str, full_page: bool = False) -> dict:
|
|
129
|
+
return json.loads(self._tool("page_screenshot", {"session_id": session_id, "page_id": page_id, "full_page": full_page}))
|
|
130
|
+
|
|
131
|
+
def page_click(self, session_id: str, page_id: str, selector: str) -> None:
|
|
132
|
+
self._tool("page_click", {"session_id": session_id, "page_id": page_id, "selector": selector})
|
|
133
|
+
|
|
134
|
+
def page_type(self, session_id: str, page_id: str, selector: str, text: str) -> None:
|
|
135
|
+
self._tool("page_type", {"session_id": session_id, "page_id": page_id, "selector": selector, "text": text})
|
|
136
|
+
|
|
137
|
+
def page_fill(self, session_id: str, page_id: str, selector: str, text: str) -> None:
|
|
138
|
+
self._tool("page_fill", {"session_id": session_id, "page_id": page_id, "selector": selector, "text": text})
|
|
139
|
+
|
|
140
|
+
def page_press(self, session_id: str, page_id: str, key: str) -> None:
|
|
141
|
+
self._tool("page_press", {"session_id": session_id, "page_id": page_id, "key": key})
|
|
142
|
+
|
|
143
|
+
def identity_generate(self) -> str:
|
|
144
|
+
return self._tool("identity_generate", {})
|
|
145
|
+
|
|
146
|
+
def identity_audit(self, identity_toml: str) -> str:
|
|
147
|
+
return self._tool("identity_audit", {"identity_toml": identity_toml})
|
|
148
|
+
|
|
149
|
+
def session_evidence(self, session_id: str) -> dict:
|
|
150
|
+
return json.loads(self._tool("session_evidence", {"session_id": session_id}))
|
|
151
|
+
|
|
152
|
+
def close(self) -> None:
|
|
153
|
+
if self.proc.poll() is None:
|
|
154
|
+
self.proc.terminate()
|
|
155
|
+
try:
|
|
156
|
+
self.proc.wait(timeout=10)
|
|
157
|
+
except subprocess.TimeoutExpired:
|
|
158
|
+
self.proc.kill()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ghostfox
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Ghostfox — the agent-native stealth browser you can own. Python client + engine installer for the Ghostfox MCP runtime.
|
|
5
|
+
Author: autokeren
|
|
6
|
+
License: MIT OR Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/autokeren/ghostfox
|
|
8
|
+
Keywords: browser,stealth,anti-detect,mcp,ai-agents,scraping
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# ghostfox (Python)
|
|
15
|
+
|
|
16
|
+
Python surface for the [Ghostfox](https://github.com/autokeren/ghostfox)
|
|
17
|
+
agent-native stealth browser.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import ghostfox
|
|
21
|
+
|
|
22
|
+
# one-time: download the prebuilt engine + runtime (Linux x86_64)
|
|
23
|
+
ghostfox.install_engine()
|
|
24
|
+
ghostfox.install_runtime()
|
|
25
|
+
|
|
26
|
+
fox = ghostfox.GhostfoxMCP()
|
|
27
|
+
sid = fox.session_create(platform="android") # coherent mobile persona
|
|
28
|
+
page = fox.page_open(sid, "https://example.com")
|
|
29
|
+
print(fox.page_snapshot(sid, page)["content"])
|
|
30
|
+
fox.page_screenshot(sid, page) # evidence + live view
|
|
31
|
+
fox.close()
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Zero Python dependencies — it drives the Rust MCP runtime over stdio, the
|
|
35
|
+
same server AI agents use.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ghostfox
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ghostfox"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Ghostfox — the agent-native stealth browser you can own. Python client + engine installer for the Ghostfox MCP runtime."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT OR Apache-2.0" }
|
|
12
|
+
authors = [{ name = "autokeren" }]
|
|
13
|
+
keywords = ["browser", "stealth", "anti-detect", "mcp", "ai-agents", "scraping"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Repository = "https://github.com/autokeren/ghostfox"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
include = ["ghostfox*"]
|
ghostfox-0.2.0/setup.cfg
ADDED