vesk-cli 0.1.2__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.
vesk/report/model.py ADDED
@@ -0,0 +1,43 @@
1
+ """Schema of the results JSON consumed by `vesk report`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ Status = Literal["pass", "fail", "error"]
10
+
11
+
12
+ class Case(BaseModel):
13
+ id: str
14
+ passed: bool
15
+ prompt: str = ""
16
+ response: str = ""
17
+ evidence: str = ""
18
+
19
+
20
+ class ProbeResult(BaseModel):
21
+ id: str
22
+ name: str = ""
23
+ category: str = ""
24
+ status: Status
25
+ description: str = ""
26
+ error: str = ""
27
+ cases: list[Case] = Field(default_factory=list)
28
+
29
+ @property
30
+ def failed_cases(self) -> list[Case]:
31
+ return [c for c in self.cases if not c.passed]
32
+
33
+
34
+ class Results(BaseModel):
35
+ vesk_version: str = ""
36
+ target: str = ""
37
+ model: str = ""
38
+ started_at: str = ""
39
+ finished_at: str = ""
40
+ probes: list[ProbeResult] = Field(default_factory=list)
41
+
42
+ def count(self, status: Status) -> int:
43
+ return sum(1 for p in self.probes if p.status == status)
vesk/report/render.py ADDED
@@ -0,0 +1,52 @@
1
+ """Render a Results object to Markdown or HTML."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from jinja2 import Environment, FileSystemLoader
9
+ from pydantic import ValidationError
10
+
11
+ from vesk.report.model import Results
12
+
13
+ DISCLAIMER = (
14
+ "This report is generated automatically from automated test results. It is not legal "
15
+ "or compliance advice, and it does not certify compliance with any law, regulation or "
16
+ "standard. Consult qualified professionals before relying on it."
17
+ )
18
+
19
+ FORMATS = ("md", "html")
20
+
21
+
22
+ class ReportError(Exception):
23
+ """Results file could not be read or rendered."""
24
+
25
+
26
+ def load_results(path: Path) -> Results:
27
+ try:
28
+ raw = path.read_text(encoding="utf-8")
29
+ except OSError as e:
30
+ raise ReportError(f"cannot read {path}: {e.strerror or e}") from e
31
+ try:
32
+ return Results.model_validate(json.loads(raw))
33
+ except json.JSONDecodeError as e:
34
+ raise ReportError(f"{path} is not valid JSON: {e}") from e
35
+ except ValidationError as e:
36
+ raise ReportError(f"{path} is not a valid results file:\n{e}") from e
37
+
38
+
39
+ def _env(fmt: str) -> Environment:
40
+ return Environment(
41
+ loader=FileSystemLoader(Path(__file__).parent / "templates"),
42
+ autoescape=fmt == "html",
43
+ trim_blocks=True,
44
+ lstrip_blocks=True,
45
+ keep_trailing_newline=True,
46
+ )
47
+
48
+
49
+ def render(results: Results, fmt: str = "md") -> str:
50
+ if fmt not in FORMATS:
51
+ raise ReportError(f"unknown format {fmt!r}; choose one of: {', '.join(FORMATS)}")
52
+ return _env(fmt).get_template(f"report.{fmt}.j2").render(r=results, disclaimer=DISCLAIMER)
@@ -0,0 +1,48 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Vesk Verify report</title>
6
+ <style>
7
+ body{font-family:sans-serif;max-width:60rem;margin:2rem auto;padding:0 1rem}
8
+ table{border-collapse:collapse;width:100%}td,th{border:1px solid #ccc;padding:.3rem .5rem;text-align:left}
9
+ .pass{color:#0a6b2d}.fail,.error{color:#b00020}
10
+ .disclaimer{border:1px solid #c90;background:#fff8e1;padding:.6rem}
11
+ pre{white-space:pre-wrap}
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <h1>Vesk Verify report</h1>
16
+ <p class="disclaimer"><strong>Disclaimer:</strong> {{ disclaimer }}</p>
17
+ <ul>
18
+ <li>Target: {{ r.target or "n/a" }}</li>
19
+ <li>Model: {{ r.model or "n/a" }}</li>
20
+ <li>Run: {{ r.started_at or "n/a" }} to {{ r.finished_at or "n/a" }}</li>
21
+ <li>Vesk version: {{ r.vesk_version or "n/a" }}</li>
22
+ </ul>
23
+ <h2>Summary</h2>
24
+ <p>{{ r.count("pass") }} passed, {{ r.count("fail") }} failed, {{ r.count("error") }} errored ({{ r.probes|length }} probes).</p>
25
+ <table>
26
+ <tr><th>Probe</th><th>Category</th><th>Status</th><th>Failed cases</th></tr>
27
+ {% for p in r.probes %}
28
+ <tr><td>{{ p.name or p.id }}</td><td>{{ p.category }}</td><td class="{{ p.status }}">{{ p.status|upper }}</td><td>{{ p.failed_cases|length }}/{{ p.cases|length }}</td></tr>
29
+ {% endfor %}
30
+ </table>
31
+ <h2>Details</h2>
32
+ {% for p in r.probes if p.status != "pass" %}
33
+ <h3>{{ p.name or p.id }} (<span class="{{ p.status }}">{{ p.status|upper }}</span>)</h3>
34
+ {% if p.description %}<p>{{ p.description }}</p>{% endif %}
35
+ {% if p.error %}<p>Error: {{ p.error }}</p>{% endif %}
36
+ {% for c in p.failed_cases %}
37
+ <h4>Case {{ c.id }}</h4>
38
+ <p>Prompt:</p><pre>{{ c.prompt }}</pre>
39
+ <p>Response:</p><pre>{{ c.response }}</pre>
40
+ {% if c.evidence %}<p>Evidence: {{ c.evidence }}</p>{% endif %}
41
+ {% endfor %}
42
+ {% else %}
43
+ <p>No failures or errors.</p>
44
+ {% endfor %}
45
+ <hr>
46
+ <p class="disclaimer">{{ disclaimer }}</p>
47
+ </body>
48
+ </html>
@@ -0,0 +1,48 @@
1
+ # Vesk Verify report
2
+
3
+ > **Disclaimer:** {{ disclaimer }}
4
+
5
+ - Target: {{ r.target or "n/a" }}
6
+ - Model: {{ r.model or "n/a" }}
7
+ - Run: {{ r.started_at or "n/a" }} to {{ r.finished_at or "n/a" }}
8
+ - Vesk version: {{ r.vesk_version or "n/a" }}
9
+
10
+ ## Summary
11
+
12
+ {{ r.count("pass") }} passed, {{ r.count("fail") }} failed, {{ r.count("error") }} errored ({{ r.probes|length }} probes).
13
+
14
+ | Probe | Category | Status | Failed cases |
15
+ |---|---|---|---|
16
+ {% for p in r.probes %}
17
+ | {{ (p.name or p.id)|replace("|", "\\|") }} | {{ p.category|replace("|", "\\|") }} | {{ p.status|upper }} | {{ p.failed_cases|length }}/{{ p.cases|length }} |
18
+ {% endfor %}
19
+
20
+ ## Details
21
+ {% for p in r.probes if p.status != "pass" %}
22
+
23
+ ### {{ p.name or p.id }} ({{ p.status|upper }})
24
+ {% if p.description %}
25
+
26
+ {{ p.description }}
27
+ {% endif %}
28
+ {% if p.error %}
29
+
30
+ Error: {{ p.error }}
31
+ {% endif %}
32
+ {% for c in p.failed_cases %}
33
+
34
+ - Case `{{ c.id }}`
35
+ - Prompt: `{{ c.prompt|replace("`", "'")|replace("\n", " ") }}`
36
+ - Response: `{{ c.response|replace("`", "'")|replace("\n", " ") }}`
37
+ {% if c.evidence %}
38
+ - Evidence: {{ c.evidence|replace("\n", " ") }}
39
+ {% endif %}
40
+ {% endfor %}
41
+ {% else %}
42
+
43
+ No failures or errors.
44
+ {% endfor %}
45
+
46
+ ---
47
+
48
+ {{ disclaimer }}
vesk/runner.py ADDED
@@ -0,0 +1,134 @@
1
+ """Runs probes against a chat client and builds the results consumed by `vesk report`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+ from vesk import __version__
12
+ from vesk.client import ChatClient, EndpointError
13
+ from vesk.config import SEVERITY_RANK, Config, ConfigError
14
+ from vesk.probes import REGISTRY, Probe
15
+
16
+ RESPONSE_LIMIT = 500
17
+
18
+
19
+ class CaseResult(BaseModel):
20
+ id: str
21
+ passed: bool
22
+ prompt: str = ""
23
+ response: str = ""
24
+ evidence: str = ""
25
+
26
+
27
+ class ProbeRun(BaseModel):
28
+ id: str
29
+ name: str
30
+ category: str
31
+ severity: str
32
+ status: str # pass | fail | error
33
+ description: str = ""
34
+ error: str = ""
35
+ cases: list[CaseResult] = Field(default_factory=list)
36
+
37
+
38
+ class ScanResults(BaseModel):
39
+ schema_version: int = 1
40
+ vesk_version: str = __version__
41
+ target: str
42
+ model: str
43
+ system_name: str = ""
44
+ started_at: str
45
+ finished_at: str
46
+ probes: list[ProbeRun]
47
+
48
+ def exit_code(self, fail_on: str) -> int:
49
+ """3 = incomplete scan (endpoint errors), 1 = findings at/above fail_on, else 0."""
50
+ if any(p.error for p in self.probes):
51
+ return 3
52
+ if fail_on == "none":
53
+ return 0
54
+ floor = SEVERITY_RANK[fail_on]
55
+ failing = (p for p in self.probes if p.status == "fail")
56
+ return 1 if any(SEVERITY_RANK[p.severity] >= floor for p in failing) else 0
57
+
58
+ def write(self, path: Path) -> None:
59
+ path.parent.mkdir(parents=True, exist_ok=True)
60
+ path.write_text(self.model_dump_json(indent=2) + "\n", encoding="utf-8")
61
+
62
+
63
+ def select_probes(cfg: Config, override: list[str] | None) -> list[Probe]:
64
+ names = override or cfg.scan.probes or list(REGISTRY)
65
+ unknown = [n for n in names if n not in REGISTRY]
66
+ if unknown:
67
+ raise ConfigError(
68
+ f"unknown probe(s): {', '.join(unknown)}. Available: {', '.join(REGISTRY)}"
69
+ )
70
+ return [REGISTRY[n]() for n in dict.fromkeys(names)]
71
+
72
+
73
+ def _now() -> str:
74
+ return datetime.now(timezone.utc).isoformat(timespec="seconds")
75
+
76
+
77
+ def _excerpt(text: str) -> str:
78
+ return text if len(text) <= RESPONSE_LIMIT else text[:RESPONSE_LIMIT] + "…"
79
+
80
+
81
+ async def _run_probe(probe: Probe, cfg: Config, client: ChatClient, sem: asyncio.Semaphore):
82
+ cases = probe.cases(cfg.target.system_prompt, cfg.scan.samples_per_probe, cfg.scan.seed)
83
+ errors: list[str] = []
84
+
85
+ async def one(case) -> CaseResult | None:
86
+ async with sem:
87
+ try:
88
+ response = await client.complete(case.messages)
89
+ except EndpointError as e:
90
+ errors.append(str(e))
91
+ return None
92
+ verdict = probe.evaluate(case, response)
93
+ return CaseResult(
94
+ id=case.id,
95
+ passed=not verdict.failed,
96
+ prompt=probe.redact(case, case.prompt),
97
+ response=_excerpt(probe.redact(case, response)),
98
+ evidence=verdict.evidence,
99
+ )
100
+
101
+ results = [r for r in await asyncio.gather(*(one(c) for c in cases)) if r is not None]
102
+ if not results:
103
+ status = "error"
104
+ else:
105
+ status = "fail" if any(not r.passed for r in results) else "pass"
106
+ error = ""
107
+ if errors:
108
+ error = f"{len(errors)} of {len(cases)} requests failed: {errors[0]}"
109
+ return ProbeRun(
110
+ id=probe.name,
111
+ name=probe.name.replace("_", " ").title(),
112
+ category=probe.category,
113
+ severity=probe.severity,
114
+ status=status,
115
+ description=probe.description,
116
+ error=error,
117
+ cases=results,
118
+ )
119
+
120
+
121
+ async def run_scan(
122
+ cfg: Config, probes: list[Probe], client: ChatClient
123
+ ) -> ScanResults:
124
+ started = _now()
125
+ sem = asyncio.Semaphore(cfg.scan.max_concurrency)
126
+ runs = [await _run_probe(p, cfg, client, sem) for p in probes]
127
+ return ScanResults(
128
+ target=cfg.target.base_url,
129
+ model=cfg.target.model,
130
+ system_name=cfg.report.system_name,
131
+ started_at=started,
132
+ finished_at=_now(),
133
+ probes=runs,
134
+ )
vesk/scaffold.py ADDED
@@ -0,0 +1,48 @@
1
+ """Starter config written by `vesk init`."""
2
+
3
+ from pathlib import Path
4
+
5
+ STARTER_CONFIG = """\
6
+ # Vesk Verify configuration. Docs: https://github.com/vesk-ai/vesk-cli
7
+ version: 1
8
+
9
+ target:
10
+ # Any OpenAI-compatible chat completions endpoint.
11
+ base_url: https://api.example.com/v1
12
+ model: my-model
13
+ # Name of the environment variable holding the API key.
14
+ # The key itself must never be written in this file.
15
+ api_key_env: VESK_API_KEY
16
+ # Optional system prompt your application uses (helps injection/leak probes).
17
+ # system_prompt: "You are a support bot."
18
+ timeout_s: 30
19
+ max_retries: 2
20
+
21
+ scan:
22
+ # Probes to run. Available: prompt_injection, jailbreak, pii_leakage.
23
+ probes: [prompt_injection, jailbreak, pii_leakage]
24
+ samples_per_probe: 20
25
+ max_concurrency: 4
26
+ seed: 0
27
+ # Exit with code 1 if any finding is at or above this severity: none|low|medium|high
28
+ fail_on: high
29
+
30
+ report:
31
+ frameworks: [eu_ai_act, dpdp, nist_ai_rmf]
32
+ system_name: "My AI system"
33
+ """
34
+
35
+
36
+ class ConfigExistsError(FileExistsError):
37
+ """Raised when the target file exists and overwrite was not requested."""
38
+
39
+
40
+ def write_starter_config(path: Path, force: bool = False) -> Path:
41
+ """Write the starter config to `path`, creating parent directories."""
42
+ if path.exists() and not force:
43
+ raise ConfigExistsError(f"{path} already exists (use --force to overwrite)")
44
+ if path.is_dir():
45
+ raise IsADirectoryError(f"{path} is a directory")
46
+ path.parent.mkdir(parents=True, exist_ok=True)
47
+ path.write_text(STARTER_CONFIG, encoding="utf-8")
48
+ return path
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.5
2
+ Name: vesk-cli
3
+ Version: 0.1.2
4
+ Summary: Vesk Verify: test LLM endpoints and produce compliance-mapped reports.
5
+ Author: Vesk
6
+ License-Expression: Apache-2.0
7
+ License-File: LICENSE
8
+ License-File: NOTICE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: httpx>=0.27
18
+ Requires-Dist: jinja2>=3.1
19
+ Requires-Dist: pydantic>=2
20
+ Requires-Dist: pyyaml>=6
21
+ Requires-Dist: typer>=0.12
22
+ Provides-Extra: dev
23
+ Requires-Dist: build; extra == 'dev'
24
+ Requires-Dist: mypy; extra == 'dev'
25
+ Requires-Dist: pytest; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio; extra == 'dev'
27
+ Requires-Dist: respx; extra == 'dev'
28
+ Requires-Dist: ruff; extra == 'dev'
29
+ Requires-Dist: types-pyyaml; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # vesk
33
+
34
+ **Vesk Verify** tests an OpenAI-compatible LLM endpoint with prompt-injection, jailbreak and
35
+ PII-leakage probes, then renders a report mapped to EU AI Act, DPDP and NIST AI RMF.
36
+
37
+ > Status: alpha (0.1.x). Interfaces may change between minor versions.
38
+
39
+ ## Install
40
+
41
+ ```
42
+ pip install vesk-cli
43
+ ```
44
+
45
+ Requires Python 3.10 or newer.
46
+
47
+ ## Quick start
48
+
49
+ ```
50
+ vesk init # write a commented vesk.yaml
51
+ export VESK_API_KEY=... # the env var named by target.api_key_env
52
+ vesk scan # run probes, write results.json
53
+ vesk report results.json -o report.md
54
+ ```
55
+
56
+ ## Commands
57
+
58
+ ### `vesk init`
59
+
60
+ Writes a starter `vesk.yaml`.
61
+
62
+ - `--path PATH`: where to write it (default `vesk.yaml`).
63
+ - `--force`: overwrite an existing file.
64
+
65
+ ### `vesk scan`
66
+
67
+ Runs the configured probes against the target and writes a results JSON file.
68
+
69
+ - `-c, --config PATH`: config file (default `vesk.yaml`).
70
+ - `--probe NAME`: run only this probe; repeatable.
71
+ - `--out PATH`: results file (default `results.json`).
72
+
73
+ Exit codes:
74
+
75
+ | Code | Meaning |
76
+ |------|---------|
77
+ | 0 | Scan completed; no finding at or above `scan.fail_on`. |
78
+ | 1 | At least one failing probe at or above `scan.fail_on`. |
79
+ | 2 | Configuration or file error. |
80
+ | 3 | Incomplete scan: a probe hit endpoint errors. |
81
+
82
+ This makes `vesk scan` usable as a CI gate.
83
+
84
+ ### `vesk report`
85
+
86
+ Renders a report from a results file.
87
+
88
+ ```
89
+ vesk report results.json --format html -o report.html
90
+ ```
91
+
92
+ - `--format md|html`: output format (default `md`).
93
+ - `-o, --output PATH`: write to a file instead of stdout.
94
+
95
+ ## Configuration
96
+
97
+ `vesk init` writes a fully commented file. In short:
98
+
99
+ ```yaml
100
+ version: 1
101
+ target:
102
+ base_url: https://api.example.com/v1 # OpenAI-compatible chat completions API
103
+ model: my-model
104
+ api_key_env: VESK_API_KEY # name of the env var; never put the key in the file
105
+ # system_prompt: "You are a support bot."
106
+ timeout_s: 30
107
+ max_retries: 2
108
+ scan:
109
+ probes: [prompt_injection, jailbreak, pii_leakage]
110
+ samples_per_probe: 20
111
+ max_concurrency: 4
112
+ seed: 0
113
+ fail_on: high # none | low | medium | high
114
+ report:
115
+ frameworks: [eu_ai_act, dpdp, nist_ai_rmf]
116
+ system_name: "My AI system"
117
+ ```
118
+
119
+ ## Probes
120
+
121
+ | Probe | Checks |
122
+ |-------|--------|
123
+ | `prompt_injection` | Whether text in the user turn or in quoted data overrides the instructions. |
124
+ | `jailbreak` | Whether persona and role-play framings make the model drop its rules. Probes ask only for a harmless marker, never for harmful content. |
125
+ | `pii_leakage` | Whether the model repeats synthetic personal data planted in its own context. Training-data regurgitation is out of scope for 0.1. |
126
+
127
+ ## Privacy
128
+
129
+ The API key is read from an environment variable and is not stored in the config. Probe
130
+ requests go only to the endpoint you configure. PII probes use synthetic values, masked in
131
+ stored results.
132
+
133
+ ## Not legal advice
134
+
135
+ Reports are generated automatically from test results. They are not legal or compliance advice
136
+ and do not certify compliance with any law, regulation or standard.
137
+
138
+ ## Development
139
+
140
+ ```
141
+ pip install -e ".[dev]"
142
+ pytest
143
+ ruff check .
144
+ ```
145
+
146
+ ## License
147
+
148
+ Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
@@ -0,0 +1,27 @@
1
+ vesk/__init__.py,sha256=YPFawP1xoc0x_YlmM9OEuRhMw9NeZQtmeZFNd5sIpJ8,95
2
+ vesk/cli.py,sha256=1fPQLOv94vDUB0vmhQolG-88JfcdozkMqEU0C-ieEWM,3757
3
+ vesk/client.py,sha256=bgc2uDGVcOr12Y4n7A-DCgmpWC3r1XMH3kXSHrEWHFY,2700
4
+ vesk/config.py,sha256=ZxOGFErGjFNO-2_ZIorZOPV7ekk0YrZcKIbSiTAkb-8,2544
5
+ vesk/runner.py,sha256=dTxsg_QKa8lZcJ1oU4jrfRMDoDYN8GxJ0aETxajN8rc,4077
6
+ vesk/scaffold.py,sha256=djeLAEY72fKwh3mFvQzErqtckdo9S_rgwrIVN696CO4,1559
7
+ vesk/data/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ vesk/probes/__init__.py,sha256=Kn5etDCHFh56k77SszloaWxcyOzP1aAuIKt9zb5SRHU,426
9
+ vesk/probes/base.py,sha256=0twz3F9Gt6eFLSwksqTz5dvRGPwBN2OeG-NOCUDNvDA,2063
10
+ vesk/probes/jailbreak.py,sha256=dOVA5H-nGWM51S41oL7fRUXLMRma9CuNelcwJgjCQow,1735
11
+ vesk/probes/pii_leakage.py,sha256=_7qDS7am-zcav8_4uA-CGZSqPJ3mO02xQtr7MaXq--U,3146
12
+ vesk/probes/prompt_injection.py,sha256=IbDRd2FNzp2vCAcp-PNVnvWwYuPzXHJHowdGyk0-K8Y,1605
13
+ vesk/probes/data/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ vesk/probes/data/jailbreak.yaml,sha256=1lu0CcWewfoN7qUtB-1W3Amyn_oB0rMA2CpVA6XJF6s,1793
15
+ vesk/probes/data/pii_leakage.yaml,sha256=jlAZv1Nx4PfOKi_77EkuywsSeMX83KekA1G2qdJUBSg,1205
16
+ vesk/probes/data/prompt_injection.yaml,sha256=NBKvaGDUnGhmz3ofJUEymQu8DKHwTvjuvUZNz88nYeI,1685
17
+ vesk/report/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
18
+ vesk/report/model.py,sha256=7LbZdPI2Dh55LBWxYotBbD0Zml4xXeUA5Xd1mxGjuW4,959
19
+ vesk/report/render.py,sha256=0tuhLPmIbr8tglsD4FsBlhELcR5J7APNnog3BN-9Po0,1642
20
+ vesk/report/templates/report.html.j2,sha256=H1bbbLg4QWDn0DBTaCkwWssQllvUvVt3ytfUFI7Gerw,1825
21
+ vesk/report/templates/report.md.j2,sha256=9dT-9pahBwDIqkk7Xk13GvJWguQE6i5o00n4B8UPhOo,1201
22
+ vesk_cli-0.1.2.dist-info/METADATA,sha256=izqBoazvJj2xBb3UzFUz-HCZEB-OUCjNTg2cDfVZucA,4233
23
+ vesk_cli-0.1.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
24
+ vesk_cli-0.1.2.dist-info/entry_points.txt,sha256=0Tf_RxRyGYLVCZFhshQ8b1MV30xqSPFaE3f4EXjE-HE,38
25
+ vesk_cli-0.1.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
26
+ vesk_cli-0.1.2.dist-info/licenses/NOTICE,sha256=rfzyf7DuE9d7-OA76HB_sIxIFAPLr3D3DNE1rHLsoQU,138
27
+ vesk_cli-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vesk = vesk.cli:app