scamscript 0.2.1__tar.gz → 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.
- {scamscript-0.2.1 → scamscript-0.3.0}/CHANGELOG.md +7 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/PKG-INFO +14 -10
- {scamscript-0.2.1 → scamscript-0.3.0}/README.md +13 -9
- {scamscript-0.2.1 → scamscript-0.3.0}/pyproject.toml +1 -1
- {scamscript-0.2.1 → scamscript-0.3.0}/scripts/calibrate.py +5 -3
- {scamscript-0.2.1 → scamscript-0.3.0}/scripts/injection_eval.py +5 -3
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/__init__.py +1 -1
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/cli.py +11 -8
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/client.py +3 -5
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/config.py +19 -7
- {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_cli.py +31 -1
- {scamscript-0.2.1 → scamscript-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/.github/SECURITY.md +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/.github/pull_request_template.md +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/.github/workflows/ci.yml +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/.github/workflows/release.yml +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/.gitignore +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/CITATION.cff +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/CONTRIBUTING.md +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/LICENSE +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/docs/banner.svg +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/docs/calibration.md +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/docs/demo.gif +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/docs/sample-report.md +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/docs/social-preview.png +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/fake_output_json.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/fence_escape.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/fixed_attack_type.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/non_json_demand.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/print_system_prompt.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/role_switch.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/score_override.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/crude_robocall.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/gov_impersonation_vishing.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/prompt_injection_script.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/romance_scam_sms.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/sophisticated_wire_fraud.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/examples/tech_support_scam.txt +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/__main__.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/analysis.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/ingest.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/models.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/prompts.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/redact.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/render.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/report.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/store.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/verify.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_analysis.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_client.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_ingest.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_models.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_prompts.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_redact.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_render.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_report.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_store.py +0 -0
- {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_verify.py +0 -0
|
@@ -5,6 +5,13 @@ schema are the things that count as public API.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.3.0 - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Breaking: no default provider or model. `--base-url` and `--model` (or `SCAMSCRIPT_BASE_URL` /
|
|
13
|
+
`SCAMSCRIPT_MODEL`) are required for API calls; `--dry-run` works without them.
|
|
14
|
+
|
|
8
15
|
## 0.2.1 - 2026-11-02
|
|
9
16
|
|
|
10
17
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: scamscript
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Reverse-engineer scam scripts and vishing transcripts into structured counter-playbooks
|
|
5
5
|
Project-URL: Homepage, https://github.com/sjdsjadjajdjsa/scamscript
|
|
6
6
|
Project-URL: Repository, https://github.com/sjdsjadjajdjsa/scamscript
|
|
@@ -76,18 +76,22 @@ mechanics instead of general advice.
|
|
|
76
76
|
pip install scamscript # or, for development:
|
|
77
77
|
# git clone https://github.com/sjdsjadjajdjsa/scamscript && cd scamscript && pip install -e ".[dev]"
|
|
78
78
|
|
|
79
|
-
export API_KEY=...
|
|
79
|
+
export API_KEY=...
|
|
80
|
+
export SCAMSCRIPT_BASE_URL=https://openrouter.ai/api/v1
|
|
81
|
+
export SCAMSCRIPT_MODEL=deepseek/deepseek-v4.1-flash-1017
|
|
80
82
|
|
|
81
83
|
scamscript --dry-run analyze --file examples/tech_support_scam.txt # inspect the prompt, spend nothing
|
|
82
84
|
scamscript analyze --file examples/tech_support_scam.txt
|
|
83
85
|
```
|
|
84
86
|
|
|
85
|
-
Any OpenAI-compatible `/chat/completions` endpoint works.
|
|
86
|
-
`
|
|
87
|
+
Any OpenAI-compatible `/chat/completions` endpoint works. There is no baked-in provider or model:
|
|
88
|
+
pass `--base-url` and `--model`, or set `SCAMSCRIPT_BASE_URL` and `SCAMSCRIPT_MODEL`.
|
|
87
89
|
|
|
88
90
|
```bash
|
|
89
91
|
scamscript --base-url https://openrouter.ai/api/v1 \
|
|
90
92
|
--model deepseek/deepseek-v4.1-flash-1017 analyze --file script.txt
|
|
93
|
+
|
|
94
|
+
scamscript --base-url http://localhost:11434/v1 --model llama3.2 analyze --file script.txt
|
|
91
95
|
```
|
|
92
96
|
|
|
93
97
|
## What it produces
|
|
@@ -182,8 +186,8 @@ Release history is in [CHANGELOG.md](https://github.com/sjdsjadjajdjsa/scamscrip
|
|
|
182
186
|
| Setting | Flag | Env | Default |
|
|
183
187
|
| --- | --- | --- | --- |
|
|
184
188
|
| API key | `--api-key` | `API_KEY` | none, required unless `--dry-run` |
|
|
185
|
-
| Base URL | `--base-url` | `SCAMSCRIPT_BASE_URL` |
|
|
186
|
-
| Model | `--model` | `SCAMSCRIPT_MODEL` |
|
|
189
|
+
| Base URL | `--base-url` | `SCAMSCRIPT_BASE_URL` | none, required |
|
|
190
|
+
| Model | `--model` | `SCAMSCRIPT_MODEL` | none, required |
|
|
187
191
|
| State dir | `--home` | `SCAMSCRIPT_HOME` | `~/.scamscript` |
|
|
188
192
|
|
|
189
193
|
## Tests, CI and calibration
|
|
@@ -199,9 +203,9 @@ Calibration is evidence, not vibes. `scripts/calibrate.py` measures first-attemp
|
|
|
199
203
|
adherence, repair rate, latency, score spread and billed cost against a live endpoint:
|
|
200
204
|
|
|
201
205
|
```bash
|
|
202
|
-
API_KEY=...
|
|
203
|
-
|
|
204
|
-
--
|
|
206
|
+
API_KEY=... SCAMSCRIPT_BASE_URL=https://openrouter.ai/api/v1 \
|
|
207
|
+
python scripts/calibrate.py --dir examples --repeat 3 \
|
|
208
|
+
--model deepseek/deepseek-v4.1-flash-1017
|
|
205
209
|
```
|
|
206
210
|
|
|
207
211
|
Recorded passes with their numbers are in [`docs/calibration.md`](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/docs/calibration.md): three rubric
|
|
@@ -212,7 +216,7 @@ take as evidence.
|
|
|
212
216
|
The adversarial set lives in `examples/adversarial/` and has its own harness:
|
|
213
217
|
|
|
214
218
|
```bash
|
|
215
|
-
API_KEY=... python scripts/injection_eval.py --model ...
|
|
219
|
+
API_KEY=... SCAMSCRIPT_BASE_URL=... python scripts/injection_eval.py --model ...
|
|
216
220
|
```
|
|
217
221
|
|
|
218
222
|
Format held in 7/7 cases; embedded demands were obeyed in 1 of 6. The fence protects the schema, not
|
|
@@ -40,18 +40,22 @@ mechanics instead of general advice.
|
|
|
40
40
|
pip install scamscript # or, for development:
|
|
41
41
|
# git clone https://github.com/sjdsjadjajdjsa/scamscript && cd scamscript && pip install -e ".[dev]"
|
|
42
42
|
|
|
43
|
-
export API_KEY=...
|
|
43
|
+
export API_KEY=...
|
|
44
|
+
export SCAMSCRIPT_BASE_URL=https://openrouter.ai/api/v1
|
|
45
|
+
export SCAMSCRIPT_MODEL=deepseek/deepseek-v4.1-flash-1017
|
|
44
46
|
|
|
45
47
|
scamscript --dry-run analyze --file examples/tech_support_scam.txt # inspect the prompt, spend nothing
|
|
46
48
|
scamscript analyze --file examples/tech_support_scam.txt
|
|
47
49
|
```
|
|
48
50
|
|
|
49
|
-
Any OpenAI-compatible `/chat/completions` endpoint works.
|
|
50
|
-
`
|
|
51
|
+
Any OpenAI-compatible `/chat/completions` endpoint works. There is no baked-in provider or model:
|
|
52
|
+
pass `--base-url` and `--model`, or set `SCAMSCRIPT_BASE_URL` and `SCAMSCRIPT_MODEL`.
|
|
51
53
|
|
|
52
54
|
```bash
|
|
53
55
|
scamscript --base-url https://openrouter.ai/api/v1 \
|
|
54
56
|
--model deepseek/deepseek-v4.1-flash-1017 analyze --file script.txt
|
|
57
|
+
|
|
58
|
+
scamscript --base-url http://localhost:11434/v1 --model llama3.2 analyze --file script.txt
|
|
55
59
|
```
|
|
56
60
|
|
|
57
61
|
## What it produces
|
|
@@ -146,8 +150,8 @@ Release history is in [CHANGELOG.md](https://github.com/sjdsjadjajdjsa/scamscrip
|
|
|
146
150
|
| Setting | Flag | Env | Default |
|
|
147
151
|
| --- | --- | --- | --- |
|
|
148
152
|
| API key | `--api-key` | `API_KEY` | none, required unless `--dry-run` |
|
|
149
|
-
| Base URL | `--base-url` | `SCAMSCRIPT_BASE_URL` |
|
|
150
|
-
| Model | `--model` | `SCAMSCRIPT_MODEL` |
|
|
153
|
+
| Base URL | `--base-url` | `SCAMSCRIPT_BASE_URL` | none, required |
|
|
154
|
+
| Model | `--model` | `SCAMSCRIPT_MODEL` | none, required |
|
|
151
155
|
| State dir | `--home` | `SCAMSCRIPT_HOME` | `~/.scamscript` |
|
|
152
156
|
|
|
153
157
|
## Tests, CI and calibration
|
|
@@ -163,9 +167,9 @@ Calibration is evidence, not vibes. `scripts/calibrate.py` measures first-attemp
|
|
|
163
167
|
adherence, repair rate, latency, score spread and billed cost against a live endpoint:
|
|
164
168
|
|
|
165
169
|
```bash
|
|
166
|
-
API_KEY=...
|
|
167
|
-
|
|
168
|
-
--
|
|
170
|
+
API_KEY=... SCAMSCRIPT_BASE_URL=https://openrouter.ai/api/v1 \
|
|
171
|
+
python scripts/calibrate.py --dir examples --repeat 3 \
|
|
172
|
+
--model deepseek/deepseek-v4.1-flash-1017
|
|
169
173
|
```
|
|
170
174
|
|
|
171
175
|
Recorded passes with their numbers are in [`docs/calibration.md`](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/docs/calibration.md): three rubric
|
|
@@ -176,7 +180,7 @@ take as evidence.
|
|
|
176
180
|
The adversarial set lives in `examples/adversarial/` and has its own harness:
|
|
177
181
|
|
|
178
182
|
```bash
|
|
179
|
-
API_KEY=... python scripts/injection_eval.py --model ...
|
|
183
|
+
API_KEY=... SCAMSCRIPT_BASE_URL=... python scripts/injection_eval.py --model ...
|
|
180
184
|
```
|
|
181
185
|
|
|
182
186
|
Format held in 7/7 cases; embedded demands were obeyed in 1 of 6. The fence protects the schema, not
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scamscript"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Reverse-engineer scam scripts and vishing transcripts into structured counter-playbooks"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -21,7 +21,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
21
21
|
|
|
22
22
|
from scamscript.analysis import analyze_script # noqa: E402
|
|
23
23
|
from scamscript.client import ChatClient # noqa: E402
|
|
24
|
-
from scamscript.config import
|
|
24
|
+
from scamscript.config import Settings # noqa: E402
|
|
25
25
|
from scamscript.ingest import collect_scripts # noqa: E402
|
|
26
26
|
from scamscript.store import RunLogger # noqa: E402
|
|
27
27
|
|
|
@@ -29,13 +29,15 @@ from scamscript.store import RunLogger # noqa: E402
|
|
|
29
29
|
def main(
|
|
30
30
|
directory: Path = typer.Option(Path("examples"), "--dir", "-d"),
|
|
31
31
|
repeat: int = typer.Option(2, "--repeat", "-n", min=1),
|
|
32
|
-
model: str = typer.Option(
|
|
33
|
-
base_url: str = typer.Option(
|
|
32
|
+
model: str = typer.Option("", "--model", "-m", envvar="SCAMSCRIPT_MODEL"),
|
|
33
|
+
base_url: str = typer.Option("", "--base-url", envvar="SCAMSCRIPT_BASE_URL"),
|
|
34
34
|
prompt_price: float = typer.Option(0.035, help="USD per million prompt tokens."),
|
|
35
35
|
completion_price: float = typer.Option(0.5, help="USD per million completion tokens."),
|
|
36
36
|
max_tokens: int = typer.Option(4096, "--max-tokens"),
|
|
37
37
|
) -> None:
|
|
38
38
|
settings = Settings(base_url=base_url, model=model, max_tokens=max_tokens)
|
|
39
|
+
settings.require_base_url()
|
|
40
|
+
settings.require_model()
|
|
39
41
|
api_key = settings.require_api_key()
|
|
40
42
|
scripts = collect_scripts(directory)
|
|
41
43
|
runs = [script for _ in range(repeat) for script in scripts]
|
|
@@ -22,7 +22,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
22
22
|
|
|
23
23
|
from scamscript.analysis import analyze_script # noqa: E402
|
|
24
24
|
from scamscript.client import ChatClient # noqa: E402
|
|
25
|
-
from scamscript.config import
|
|
25
|
+
from scamscript.config import Settings # noqa: E402
|
|
26
26
|
from scamscript.ingest import ScriptFile # noqa: E402
|
|
27
27
|
from scamscript.store import RunLogger # noqa: E402
|
|
28
28
|
|
|
@@ -82,10 +82,12 @@ CASES: dict[str, dict[str, object]] = {
|
|
|
82
82
|
|
|
83
83
|
def main(
|
|
84
84
|
directory: Path = typer.Option(Path("examples/adversarial"), "--dir", "-d"),
|
|
85
|
-
model: str = typer.Option(
|
|
86
|
-
base_url: str = typer.Option(
|
|
85
|
+
model: str = typer.Option("", "--model", "-m", envvar="SCAMSCRIPT_MODEL"),
|
|
86
|
+
base_url: str = typer.Option("", "--base-url", envvar="SCAMSCRIPT_BASE_URL"),
|
|
87
87
|
) -> None:
|
|
88
88
|
settings = Settings(base_url=base_url, model=model)
|
|
89
|
+
settings.require_base_url()
|
|
90
|
+
settings.require_model()
|
|
89
91
|
api_key = settings.require_api_key()
|
|
90
92
|
cases = [(name, spec) for name, spec in CASES.items() if (directory / f"{name}.txt").is_file()]
|
|
91
93
|
|
|
@@ -15,9 +15,7 @@ from scamscript import report as reporting
|
|
|
15
15
|
from scamscript.analysis import Outcome, analyze_script
|
|
16
16
|
from scamscript.client import ChatClient
|
|
17
17
|
from scamscript.config import (
|
|
18
|
-
DEFAULT_BASE_URL,
|
|
19
18
|
DEFAULT_MAX_TOKENS,
|
|
20
|
-
DEFAULT_MODEL,
|
|
21
19
|
DEFAULT_RETRIES,
|
|
22
20
|
DEFAULT_TIMEOUT,
|
|
23
21
|
ConfigError,
|
|
@@ -66,19 +64,17 @@ def version_callback(value: bool) -> None:
|
|
|
66
64
|
def main(
|
|
67
65
|
ctx: typer.Context,
|
|
68
66
|
base_url: str = typer.Option(
|
|
69
|
-
|
|
67
|
+
"",
|
|
70
68
|
"--base-url",
|
|
71
69
|
envvar="SCAMSCRIPT_BASE_URL",
|
|
72
|
-
help="OpenAI-compatible API base URL.",
|
|
73
|
-
show_default=True,
|
|
70
|
+
help="OpenAI-compatible API base URL. Falls back to SCAMSCRIPT_BASE_URL.",
|
|
74
71
|
),
|
|
75
72
|
model: str = typer.Option(
|
|
76
|
-
|
|
73
|
+
"",
|
|
77
74
|
"--model",
|
|
78
75
|
"-m",
|
|
79
76
|
envvar="SCAMSCRIPT_MODEL",
|
|
80
|
-
help="Model identifier sent to the API.",
|
|
81
|
-
show_default=True,
|
|
77
|
+
help="Model identifier sent to the API. Falls back to SCAMSCRIPT_MODEL.",
|
|
82
78
|
),
|
|
83
79
|
api_key: str | None = typer.Option(
|
|
84
80
|
None,
|
|
@@ -219,6 +215,11 @@ def _run(
|
|
|
219
215
|
)
|
|
220
216
|
|
|
221
217
|
if state.dry_run:
|
|
218
|
+
if not settings.base_url or not settings.model:
|
|
219
|
+
console.print(
|
|
220
|
+
"[yellow]note[/yellow] --base-url and --model are unset; "
|
|
221
|
+
"the prompt below is complete but unaddressed"
|
|
222
|
+
)
|
|
222
223
|
for script in scripts:
|
|
223
224
|
text, counts = _maybe_redact(script.text, settings.redact)
|
|
224
225
|
messages = prompts.build_messages(script.name, text)
|
|
@@ -246,6 +247,8 @@ def _run(
|
|
|
246
247
|
return
|
|
247
248
|
|
|
248
249
|
try:
|
|
250
|
+
settings.require_base_url()
|
|
251
|
+
settings.require_model()
|
|
249
252
|
api_key = settings.require_api_key()
|
|
250
253
|
except ConfigError as exc:
|
|
251
254
|
_fail(console, str(exc))
|
|
@@ -8,9 +8,7 @@ from typing import Any, Callable
|
|
|
8
8
|
import httpx
|
|
9
9
|
|
|
10
10
|
from scamscript.config import (
|
|
11
|
-
DEFAULT_BASE_URL,
|
|
12
11
|
DEFAULT_MAX_TOKENS,
|
|
13
|
-
DEFAULT_MODEL,
|
|
14
12
|
DEFAULT_RETRIES,
|
|
15
13
|
DEFAULT_TIMEOUT,
|
|
16
14
|
)
|
|
@@ -42,8 +40,8 @@ class ChatClient:
|
|
|
42
40
|
def __init__(
|
|
43
41
|
self,
|
|
44
42
|
api_key: str,
|
|
45
|
-
base_url: str =
|
|
46
|
-
model: str =
|
|
43
|
+
base_url: str = "",
|
|
44
|
+
model: str = "",
|
|
47
45
|
timeout: float = DEFAULT_TIMEOUT,
|
|
48
46
|
retries: int = DEFAULT_RETRIES,
|
|
49
47
|
max_tokens: int = DEFAULT_MAX_TOKENS,
|
|
@@ -64,7 +62,7 @@ class ChatClient:
|
|
|
64
62
|
"Authorization": f"Bearer {api_key}",
|
|
65
63
|
"Content-Type": "application/json",
|
|
66
64
|
"Accept": "application/json",
|
|
67
|
-
"User-Agent": "scamscript/0.
|
|
65
|
+
"User-Agent": "scamscript/0.3.0",
|
|
68
66
|
},
|
|
69
67
|
)
|
|
70
68
|
|
|
@@ -5,8 +5,6 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
|
|
8
|
-
DEFAULT_BASE_URL = "https://api.venice.ai/api/v1"
|
|
9
|
-
DEFAULT_MODEL = "deepseek-v4-1-flash-1017-uncensored"
|
|
10
8
|
DEFAULT_TIMEOUT = 120.0
|
|
11
9
|
DEFAULT_RETRIES = 2
|
|
12
10
|
DEFAULT_MAX_TOKENS = 4096
|
|
@@ -20,8 +18,8 @@ class ConfigError(Exception):
|
|
|
20
18
|
|
|
21
19
|
@dataclass(slots=True)
|
|
22
20
|
class Settings:
|
|
23
|
-
base_url: str =
|
|
24
|
-
model: str =
|
|
21
|
+
base_url: str = ""
|
|
22
|
+
model: str = ""
|
|
25
23
|
api_key: str | None = None
|
|
26
24
|
timeout: float = DEFAULT_TIMEOUT
|
|
27
25
|
retries: int = DEFAULT_RETRIES
|
|
@@ -30,9 +28,8 @@ class Settings:
|
|
|
30
28
|
home: str = field(default_factory=lambda: os.environ.get(ENV_HOME, ""))
|
|
31
29
|
|
|
32
30
|
def __post_init__(self) -> None:
|
|
33
|
-
self.base_url = (self.base_url or
|
|
34
|
-
|
|
35
|
-
self.model = DEFAULT_MODEL
|
|
31
|
+
self.base_url = (self.base_url or "").strip().rstrip("/")
|
|
32
|
+
self.model = (self.model or "").strip()
|
|
36
33
|
home = self.home or os.path.join(os.path.expanduser("~"), ".scamscript")
|
|
37
34
|
self.home = home
|
|
38
35
|
|
|
@@ -42,6 +39,21 @@ class Settings:
|
|
|
42
39
|
key = key.strip()
|
|
43
40
|
return key or None
|
|
44
41
|
|
|
42
|
+
def require_base_url(self) -> str:
|
|
43
|
+
if not self.base_url:
|
|
44
|
+
raise ConfigError(
|
|
45
|
+
"missing API base URL: pass --base-url or set the SCAMSCRIPT_BASE_URL "
|
|
46
|
+
"environment variable"
|
|
47
|
+
)
|
|
48
|
+
return self.base_url
|
|
49
|
+
|
|
50
|
+
def require_model(self) -> str:
|
|
51
|
+
if not self.model:
|
|
52
|
+
raise ConfigError(
|
|
53
|
+
"missing model: pass --model or set the SCAMSCRIPT_MODEL environment variable"
|
|
54
|
+
)
|
|
55
|
+
return self.model
|
|
56
|
+
|
|
45
57
|
def require_api_key(self) -> str:
|
|
46
58
|
key = self.resolve_api_key()
|
|
47
59
|
if key is None:
|
|
@@ -50,6 +50,8 @@ class FakeClient:
|
|
|
50
50
|
@pytest.fixture()
|
|
51
51
|
def home(tmp_path, monkeypatch):
|
|
52
52
|
monkeypatch.setenv("SCAMSCRIPT_HOME", str(tmp_path))
|
|
53
|
+
monkeypatch.setenv("SCAMSCRIPT_BASE_URL", "https://example.test/v1")
|
|
54
|
+
monkeypatch.setenv("SCAMSCRIPT_MODEL", "test-model")
|
|
53
55
|
monkeypatch.delenv("API_KEY", raising=False)
|
|
54
56
|
return tmp_path
|
|
55
57
|
|
|
@@ -194,10 +196,38 @@ def test_history_empty(home):
|
|
|
194
196
|
assert "no runs logged" in result.stdout
|
|
195
197
|
|
|
196
198
|
|
|
199
|
+
def test_missing_model(home, tmp_path, monkeypatch):
|
|
200
|
+
monkeypatch.delenv("SCAMSCRIPT_MODEL")
|
|
201
|
+
script = tmp_path / "a.txt"
|
|
202
|
+
script.write_text("CALLER: hello", encoding="utf-8")
|
|
203
|
+
result = runner.invoke(cli.app, ["--api-key", "k", "analyze", "--file", str(script)])
|
|
204
|
+
assert result.exit_code == 1
|
|
205
|
+
assert "missing model" in result.stdout
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_missing_base_url(home, tmp_path, monkeypatch):
|
|
209
|
+
monkeypatch.delenv("SCAMSCRIPT_BASE_URL")
|
|
210
|
+
script = tmp_path / "a.txt"
|
|
211
|
+
script.write_text("CALLER: hello", encoding="utf-8")
|
|
212
|
+
result = runner.invoke(cli.app, ["--api-key", "k", "analyze", "--file", str(script)])
|
|
213
|
+
assert result.exit_code == 1
|
|
214
|
+
assert "missing API base URL" in result.stdout
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_dry_run_without_endpoint(home, tmp_path, monkeypatch):
|
|
218
|
+
monkeypatch.delenv("SCAMSCRIPT_MODEL")
|
|
219
|
+
monkeypatch.delenv("SCAMSCRIPT_BASE_URL")
|
|
220
|
+
script = tmp_path / "a.txt"
|
|
221
|
+
script.write_text("CALLER: hello", encoding="utf-8")
|
|
222
|
+
result = runner.invoke(cli.app, ["--dry-run", "analyze", "--file", str(script)])
|
|
223
|
+
assert result.exit_code == 0
|
|
224
|
+
assert "unaddressed" in result.stdout
|
|
225
|
+
|
|
226
|
+
|
|
197
227
|
def test_version(home):
|
|
198
228
|
result = runner.invoke(cli.app, ["--version"])
|
|
199
229
|
assert result.exit_code == 0
|
|
200
|
-
assert result.stdout.strip() == "scamscript 0.
|
|
230
|
+
assert result.stdout.strip() == "scamscript 0.3.0"
|
|
201
231
|
|
|
202
232
|
|
|
203
233
|
def test_json_output_is_one_object_per_script(home, tmp_path, fake):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|