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.
Files changed (61) hide show
  1. {scamscript-0.2.1 → scamscript-0.3.0}/CHANGELOG.md +7 -0
  2. {scamscript-0.2.1 → scamscript-0.3.0}/PKG-INFO +14 -10
  3. {scamscript-0.2.1 → scamscript-0.3.0}/README.md +13 -9
  4. {scamscript-0.2.1 → scamscript-0.3.0}/pyproject.toml +1 -1
  5. {scamscript-0.2.1 → scamscript-0.3.0}/scripts/calibrate.py +5 -3
  6. {scamscript-0.2.1 → scamscript-0.3.0}/scripts/injection_eval.py +5 -3
  7. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/__init__.py +1 -1
  8. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/cli.py +11 -8
  9. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/client.py +3 -5
  10. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/config.py +19 -7
  11. {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_cli.py +31 -1
  12. {scamscript-0.2.1 → scamscript-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  13. {scamscript-0.2.1 → scamscript-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  14. {scamscript-0.2.1 → scamscript-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  15. {scamscript-0.2.1 → scamscript-0.3.0}/.github/SECURITY.md +0 -0
  16. {scamscript-0.2.1 → scamscript-0.3.0}/.github/pull_request_template.md +0 -0
  17. {scamscript-0.2.1 → scamscript-0.3.0}/.github/workflows/ci.yml +0 -0
  18. {scamscript-0.2.1 → scamscript-0.3.0}/.github/workflows/release.yml +0 -0
  19. {scamscript-0.2.1 → scamscript-0.3.0}/.gitignore +0 -0
  20. {scamscript-0.2.1 → scamscript-0.3.0}/CITATION.cff +0 -0
  21. {scamscript-0.2.1 → scamscript-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  22. {scamscript-0.2.1 → scamscript-0.3.0}/CONTRIBUTING.md +0 -0
  23. {scamscript-0.2.1 → scamscript-0.3.0}/LICENSE +0 -0
  24. {scamscript-0.2.1 → scamscript-0.3.0}/docs/banner.svg +0 -0
  25. {scamscript-0.2.1 → scamscript-0.3.0}/docs/calibration.md +0 -0
  26. {scamscript-0.2.1 → scamscript-0.3.0}/docs/demo.gif +0 -0
  27. {scamscript-0.2.1 → scamscript-0.3.0}/docs/sample-report.md +0 -0
  28. {scamscript-0.2.1 → scamscript-0.3.0}/docs/social-preview.png +0 -0
  29. {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/fake_output_json.txt +0 -0
  30. {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/fence_escape.txt +0 -0
  31. {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/fixed_attack_type.txt +0 -0
  32. {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/non_json_demand.txt +0 -0
  33. {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/print_system_prompt.txt +0 -0
  34. {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/role_switch.txt +0 -0
  35. {scamscript-0.2.1 → scamscript-0.3.0}/examples/adversarial/score_override.txt +0 -0
  36. {scamscript-0.2.1 → scamscript-0.3.0}/examples/crude_robocall.txt +0 -0
  37. {scamscript-0.2.1 → scamscript-0.3.0}/examples/gov_impersonation_vishing.txt +0 -0
  38. {scamscript-0.2.1 → scamscript-0.3.0}/examples/prompt_injection_script.txt +0 -0
  39. {scamscript-0.2.1 → scamscript-0.3.0}/examples/romance_scam_sms.txt +0 -0
  40. {scamscript-0.2.1 → scamscript-0.3.0}/examples/sophisticated_wire_fraud.txt +0 -0
  41. {scamscript-0.2.1 → scamscript-0.3.0}/examples/tech_support_scam.txt +0 -0
  42. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/__main__.py +0 -0
  43. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/analysis.py +0 -0
  44. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/ingest.py +0 -0
  45. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/models.py +0 -0
  46. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/prompts.py +0 -0
  47. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/redact.py +0 -0
  48. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/render.py +0 -0
  49. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/report.py +0 -0
  50. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/store.py +0 -0
  51. {scamscript-0.2.1 → scamscript-0.3.0}/src/scamscript/verify.py +0 -0
  52. {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_analysis.py +0 -0
  53. {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_client.py +0 -0
  54. {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_ingest.py +0 -0
  55. {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_models.py +0 -0
  56. {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_prompts.py +0 -0
  57. {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_redact.py +0 -0
  58. {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_render.py +0 -0
  59. {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_report.py +0 -0
  60. {scamscript-0.2.1 → scamscript-0.3.0}/tests/test_store.py +0 -0
  61. {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.2.1
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=... # never committed, never logged
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. Defaults are Venice with
86
- `deepseek-v4-1-flash-1017-uncensored`; OpenRouter needs only the model slug:
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` | `https://api.venice.ai/api/v1` |
186
- | Model | `--model` | `SCAMSCRIPT_MODEL` | `deepseek-v4-1-flash-1017-uncensored` |
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=... python scripts/calibrate.py --dir examples --repeat 3 \
203
- --model deepseek/deepseek-v4.1-flash-1017 \
204
- --base-url https://openrouter.ai/api/v1
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 ... --base-url ...
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=... # never committed, never logged
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. Defaults are Venice with
50
- `deepseek-v4-1-flash-1017-uncensored`; OpenRouter needs only the model slug:
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` | `https://api.venice.ai/api/v1` |
150
- | Model | `--model` | `SCAMSCRIPT_MODEL` | `deepseek-v4-1-flash-1017-uncensored` |
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=... python scripts/calibrate.py --dir examples --repeat 3 \
167
- --model deepseek/deepseek-v4.1-flash-1017 \
168
- --base-url https://openrouter.ai/api/v1
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 ... --base-url ...
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.2.1"
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 DEFAULT_BASE_URL, DEFAULT_MODEL, Settings # noqa: E402
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(DEFAULT_MODEL, "--model", "-m"),
33
- base_url: str = typer.Option(DEFAULT_BASE_URL, "--base-url"),
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 DEFAULT_BASE_URL, DEFAULT_MODEL, Settings # noqa: E402
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(DEFAULT_MODEL, "--model", "-m"),
86
- base_url: str = typer.Option(DEFAULT_BASE_URL, "--base-url"),
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
 
@@ -1,3 +1,3 @@
1
1
  """scamscript: reverse-engineering analyzer for scam call scripts."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.3.0"
@@ -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
- DEFAULT_BASE_URL,
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
- DEFAULT_MODEL,
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 = DEFAULT_BASE_URL,
46
- model: str = DEFAULT_MODEL,
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.2.1",
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 = DEFAULT_BASE_URL
24
- model: str = DEFAULT_MODEL
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 DEFAULT_BASE_URL).rstrip("/")
34
- if not self.model:
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.2.1"
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