scamscript 0.2.0__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.0 → scamscript-0.3.0}/CHANGELOG.md +15 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/PKG-INFO +22 -38
- {scamscript-0.2.0 → scamscript-0.3.0}/README.md +20 -16
- {scamscript-0.2.0 → scamscript-0.3.0}/pyproject.toml +3 -2
- {scamscript-0.2.0 → scamscript-0.3.0}/scripts/calibrate.py +5 -3
- {scamscript-0.2.0 → scamscript-0.3.0}/scripts/injection_eval.py +5 -3
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/__init__.py +1 -1
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/cli.py +11 -8
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/client.py +3 -5
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/config.py +19 -7
- {scamscript-0.2.0 → scamscript-0.3.0}/tests/test_cli.py +31 -1
- {scamscript-0.2.0 → scamscript-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/.github/SECURITY.md +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/.github/pull_request_template.md +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/.github/workflows/ci.yml +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/.github/workflows/release.yml +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/.gitignore +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/CITATION.cff +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/CONTRIBUTING.md +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/LICENSE +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/docs/banner.svg +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/docs/calibration.md +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/docs/demo.gif +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/docs/sample-report.md +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/docs/social-preview.png +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/adversarial/fake_output_json.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/adversarial/fence_escape.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/adversarial/fixed_attack_type.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/adversarial/non_json_demand.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/adversarial/print_system_prompt.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/adversarial/role_switch.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/adversarial/score_override.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/crude_robocall.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/gov_impersonation_vishing.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/prompt_injection_script.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/romance_scam_sms.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/sophisticated_wire_fraud.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/examples/tech_support_scam.txt +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/__main__.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/analysis.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/ingest.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/models.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/prompts.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/redact.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/render.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/report.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/store.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/src/scamscript/verify.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/tests/test_analysis.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/tests/test_client.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/tests/test_ingest.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/tests/test_models.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/tests/test_prompts.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/tests/test_redact.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/tests/test_render.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/tests/test_report.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/tests/test_store.py +0 -0
- {scamscript-0.2.0 → scamscript-0.3.0}/tests/test_verify.py +0 -0
|
@@ -5,6 +5,21 @@ 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
|
+
|
|
15
|
+
## 0.2.1 - 2026-11-02
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- PyPI project page: absolute image and link URLs so the banner, demo and docs links resolve.
|
|
20
|
+
- License metadata now uses the SPDX expression `MIT` plus `license-files` instead of embedding the
|
|
21
|
+
full license text in the sidebar.
|
|
22
|
+
|
|
8
23
|
### Added
|
|
9
24
|
|
|
10
25
|
- Terminal demo GIF in the README, recorded against a local stub endpoint.
|
|
@@ -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
|
|
@@ -8,27 +8,7 @@ Project-URL: Issues, https://github.com/sjdsjadjajdjsa/scamscript/issues
|
|
|
8
8
|
Project-URL: Changelog, https://github.com/sjdsjadjajdjsa/scamscript/blob/master/CHANGELOG.md
|
|
9
9
|
Author-email: scamscript contributors <313729361+sjdsjadjajdjsa@users.noreply.github.com>
|
|
10
10
|
Maintainer-email: scamscript contributors <313729361+sjdsjadjajdjsa@users.noreply.github.com>
|
|
11
|
-
License: MIT
|
|
12
|
-
|
|
13
|
-
Copyright (c) 2026 scamscript contributors
|
|
14
|
-
|
|
15
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
-
in the Software without restriction, including without limitation the rights
|
|
18
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
-
furnished to do so, subject to the following conditions:
|
|
21
|
-
|
|
22
|
-
The above copyright notice and this permission notice shall be included in all
|
|
23
|
-
copies or substantial portions of the Software.
|
|
24
|
-
|
|
25
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
-
SOFTWARE.
|
|
11
|
+
License-Expression: MIT
|
|
32
12
|
License-File: LICENSE
|
|
33
13
|
Keywords: cli,fraud,osint,phishing,scam,security,smishing,social-engineering,transcripts,vishing
|
|
34
14
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -55,11 +35,11 @@ Requires-Dist: twine==6.1.0; extra == 'dev'
|
|
|
55
35
|
Description-Content-Type: text/markdown
|
|
56
36
|
|
|
57
37
|
<p align="center">
|
|
58
|
-
<img src="docs/banner.svg" alt="scamscript: reverse-engineer the playbook, not the story" width="100%">
|
|
38
|
+
<img src="https://raw.githubusercontent.com/sjdsjadjajdjsa/scamscript/master/docs/banner.svg" alt="scamscript: reverse-engineer the playbook, not the story" width="100%">
|
|
59
39
|
</p>
|
|
60
40
|
|
|
61
41
|
<p align="center">
|
|
62
|
-
<img src="docs/demo.gif" alt="terminal demo: analyzing a script, then ranking a directory" width="760">
|
|
42
|
+
<img src="https://raw.githubusercontent.com/sjdsjadjajdjsa/scamscript/master/docs/demo.gif" alt="terminal demo: analyzing a script, then ranking a directory" width="760">
|
|
63
43
|
<br>
|
|
64
44
|
<sub>Recorded against a local stub endpoint, so no API spend.</sub>
|
|
65
45
|
</p>
|
|
@@ -68,7 +48,7 @@ Description-Content-Type: text/markdown
|
|
|
68
48
|
|
|
69
49
|
[](https://github.com/sjdsjadjajdjsa/scamscript/actions/workflows/ci.yml)
|
|
70
50
|
[](pyproject.toml)
|
|
71
|
-
[](LICENSE)
|
|
51
|
+
[](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/LICENSE)
|
|
72
52
|
[](#scope-and-disclaimer)
|
|
73
53
|
|
|
74
54
|
A reverse-engineering analyzer for leaked or public scam call scripts and vishing transcripts.
|
|
@@ -96,18 +76,22 @@ mechanics instead of general advice.
|
|
|
96
76
|
pip install scamscript # or, for development:
|
|
97
77
|
# git clone https://github.com/sjdsjadjajdjsa/scamscript && cd scamscript && pip install -e ".[dev]"
|
|
98
78
|
|
|
99
|
-
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
|
|
100
82
|
|
|
101
83
|
scamscript --dry-run analyze --file examples/tech_support_scam.txt # inspect the prompt, spend nothing
|
|
102
84
|
scamscript analyze --file examples/tech_support_scam.txt
|
|
103
85
|
```
|
|
104
86
|
|
|
105
|
-
Any OpenAI-compatible `/chat/completions` endpoint works.
|
|
106
|
-
`
|
|
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`.
|
|
107
89
|
|
|
108
90
|
```bash
|
|
109
91
|
scamscript --base-url https://openrouter.ai/api/v1 \
|
|
110
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
|
|
111
95
|
```
|
|
112
96
|
|
|
113
97
|
## What it produces
|
|
@@ -135,7 +119,7 @@ compared against the source, and lines that are paraphrased or simply not there
|
|
|
135
119
|
`--strict-quotes` to make a missing line fail the run, and `--no-verify` to skip it.
|
|
136
120
|
|
|
137
121
|
Batch mode ranks a whole directory into one markdown report. A redacted report from the sample
|
|
138
|
-
corpus is at [`docs/sample-report.md`](docs/sample-report.md).
|
|
122
|
+
corpus is at [`docs/sample-report.md`](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/docs/sample-report.md).
|
|
139
123
|
|
|
140
124
|
## Commands
|
|
141
125
|
|
|
@@ -197,13 +181,13 @@ bounded.
|
|
|
197
181
|
## Install and configure
|
|
198
182
|
|
|
199
183
|
Python 3.12+. `pip install scamscript` for use, `pip install -e ".[dev]"` from a clone for tests.
|
|
200
|
-
Release history is in [CHANGELOG.md](CHANGELOG.md).
|
|
184
|
+
Release history is in [CHANGELOG.md](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/CHANGELOG.md).
|
|
201
185
|
|
|
202
186
|
| Setting | Flag | Env | Default |
|
|
203
187
|
| --- | --- | --- | --- |
|
|
204
188
|
| API key | `--api-key` | `API_KEY` | none, required unless `--dry-run` |
|
|
205
|
-
| Base URL | `--base-url` | `SCAMSCRIPT_BASE_URL` |
|
|
206
|
-
| Model | `--model` | `SCAMSCRIPT_MODEL` |
|
|
189
|
+
| Base URL | `--base-url` | `SCAMSCRIPT_BASE_URL` | none, required |
|
|
190
|
+
| Model | `--model` | `SCAMSCRIPT_MODEL` | none, required |
|
|
207
191
|
| State dir | `--home` | `SCAMSCRIPT_HOME` | `~/.scamscript` |
|
|
208
192
|
|
|
209
193
|
## Tests, CI and calibration
|
|
@@ -219,12 +203,12 @@ Calibration is evidence, not vibes. `scripts/calibrate.py` measures first-attemp
|
|
|
219
203
|
adherence, repair rate, latency, score spread and billed cost against a live endpoint:
|
|
220
204
|
|
|
221
205
|
```bash
|
|
222
|
-
API_KEY=...
|
|
223
|
-
|
|
224
|
-
--
|
|
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
|
|
225
209
|
```
|
|
226
210
|
|
|
227
|
-
Recorded passes with their numbers are in [`docs/calibration.md`](docs/calibration.md): three rubric
|
|
211
|
+
Recorded passes with their numbers are in [`docs/calibration.md`](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/docs/calibration.md): three rubric
|
|
228
212
|
repeats, a three-model comparison, an adversarial set and end-case stability runs. The current rubric
|
|
229
213
|
reaches both ends of the scale, and quote checking flags the invented lines a plain reading would
|
|
230
214
|
take as evidence.
|
|
@@ -232,7 +216,7 @@ take as evidence.
|
|
|
232
216
|
The adversarial set lives in `examples/adversarial/` and has its own harness:
|
|
233
217
|
|
|
234
218
|
```bash
|
|
235
|
-
API_KEY=... python scripts/injection_eval.py --model ...
|
|
219
|
+
API_KEY=... SCAMSCRIPT_BASE_URL=... python scripts/injection_eval.py --model ...
|
|
236
220
|
```
|
|
237
221
|
|
|
238
222
|
Format held in 7/7 cases; embedded demands were obeyed in 1 of 6. The fence protects the schema, not
|
|
@@ -274,5 +258,5 @@ docs/ calibration record, sample report, banner, demo
|
|
|
274
258
|
For security research and fraud-awareness training only. Not legal advice, not an attribution
|
|
275
259
|
tool, no warranty. Keep API keys out of commits; the key is read from the environment.
|
|
276
260
|
|
|
277
|
-
MIT licensed, see [LICENSE](LICENSE). Contribution notes in [CONTRIBUTING.md](CONTRIBUTING.md),
|
|
261
|
+
MIT licensed, see [LICENSE](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/LICENSE). Contribution notes in [CONTRIBUTING.md](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/CONTRIBUTING.md),
|
|
278
262
|
vulnerability reports in [SECURITY.md](.github/SECURITY.md).
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/banner.svg" alt="scamscript: reverse-engineer the playbook, not the story" width="100%">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/sjdsjadjajdjsa/scamscript/master/docs/banner.svg" alt="scamscript: reverse-engineer the playbook, not the story" width="100%">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<img src="docs/demo.gif" alt="terminal demo: analyzing a script, then ranking a directory" width="760">
|
|
6
|
+
<img src="https://raw.githubusercontent.com/sjdsjadjajdjsa/scamscript/master/docs/demo.gif" alt="terminal demo: analyzing a script, then ranking a directory" width="760">
|
|
7
7
|
<br>
|
|
8
8
|
<sub>Recorded against a local stub endpoint, so no API spend.</sub>
|
|
9
9
|
</p>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
[](https://github.com/sjdsjadjajdjsa/scamscript/actions/workflows/ci.yml)
|
|
14
14
|
[](pyproject.toml)
|
|
15
|
-
[](LICENSE)
|
|
15
|
+
[](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/LICENSE)
|
|
16
16
|
[](#scope-and-disclaimer)
|
|
17
17
|
|
|
18
18
|
A reverse-engineering analyzer for leaked or public scam call scripts and vishing transcripts.
|
|
@@ -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
|
|
@@ -79,7 +83,7 @@ compared against the source, and lines that are paraphrased or simply not there
|
|
|
79
83
|
`--strict-quotes` to make a missing line fail the run, and `--no-verify` to skip it.
|
|
80
84
|
|
|
81
85
|
Batch mode ranks a whole directory into one markdown report. A redacted report from the sample
|
|
82
|
-
corpus is at [`docs/sample-report.md`](docs/sample-report.md).
|
|
86
|
+
corpus is at [`docs/sample-report.md`](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/docs/sample-report.md).
|
|
83
87
|
|
|
84
88
|
## Commands
|
|
85
89
|
|
|
@@ -141,13 +145,13 @@ bounded.
|
|
|
141
145
|
## Install and configure
|
|
142
146
|
|
|
143
147
|
Python 3.12+. `pip install scamscript` for use, `pip install -e ".[dev]"` from a clone for tests.
|
|
144
|
-
Release history is in [CHANGELOG.md](CHANGELOG.md).
|
|
148
|
+
Release history is in [CHANGELOG.md](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/CHANGELOG.md).
|
|
145
149
|
|
|
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,12 +167,12 @@ 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
|
-
Recorded passes with their numbers are in [`docs/calibration.md`](docs/calibration.md): three rubric
|
|
175
|
+
Recorded passes with their numbers are in [`docs/calibration.md`](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/docs/calibration.md): three rubric
|
|
172
176
|
repeats, a three-model comparison, an adversarial set and end-case stability runs. The current rubric
|
|
173
177
|
reaches both ends of the scale, and quote checking flags the invented lines a plain reading would
|
|
174
178
|
take as evidence.
|
|
@@ -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
|
|
@@ -218,5 +222,5 @@ docs/ calibration record, sample report, banner, demo
|
|
|
218
222
|
For security research and fraud-awareness training only. Not legal advice, not an attribution
|
|
219
223
|
tool, no warranty. Keep API keys out of commits; the key is read from the environment.
|
|
220
224
|
|
|
221
|
-
MIT licensed, see [LICENSE](LICENSE). Contribution notes in [CONTRIBUTING.md](CONTRIBUTING.md),
|
|
225
|
+
MIT licensed, see [LICENSE](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/LICENSE). Contribution notes in [CONTRIBUTING.md](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/CONTRIBUTING.md),
|
|
222
226
|
vulnerability reports in [SECURITY.md](.github/SECURITY.md).
|
|
@@ -4,11 +4,12 @@ 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"
|
|
11
|
-
license =
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
12
13
|
authors = [
|
|
13
14
|
{ name = "scamscript contributors", email = "313729361+sjdsjadjajdjsa@users.noreply.github.com" },
|
|
14
15
|
]
|
|
@@ -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
|