wrencode 0.1.4.4__tar.gz → 0.1.4.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wrencode
3
- Version: 0.1.4.4
3
+ Version: 0.1.4.6
4
4
  Summary: A minimal agentic coding assistant in a single Python file
5
5
  Project-URL: Homepage, https://github.com/almostly/wrencode
6
6
  Project-URL: Repository, https://github.com/almostly/wrencode
@@ -53,6 +53,19 @@ import urllib.error
53
53
  import urllib.request
54
54
  from typing import Any, Callable, Optional
55
55
 
56
+ # The PyInstaller single-file binary ships no system CA trust store, so urllib's
57
+ # TLS verification fails out of the box ("CERTIFICATE_VERIFY_FAILED") on a clean
58
+ # machine. If certifi is bundled (it is in the binary build), point the SSL
59
+ # defaults at it before any request. Soft import keeps pip/uvx installs
60
+ # dependency-free; setdefault preserves any user-provided override.
61
+ try:
62
+ import certifi
63
+
64
+ os.environ.setdefault("SSL_CERT_FILE", certifi.where())
65
+ os.environ.setdefault("REQUESTS_CA_BUNDLE", certifi.where())
66
+ except ImportError:
67
+ pass
68
+
56
69
  # Load .env (next to this script, then the current directory); real env vars win.
57
70
  for _dir in (os.path.dirname(os.path.abspath(__file__)), os.getcwd()):
58
71
  _env_path = os.path.join(_dir, ".env")
@@ -72,9 +85,9 @@ for _dir in (os.path.dirname(os.path.abspath(__file__)), os.getcwd()):
72
85
  os.environ.setdefault(_k.strip(), _v)
73
86
 
74
87
  # -----------------------------------------------------------------------------------------------
75
- # Backend Configuration
88
+ # Backend configuration
76
89
  # -----------------------------------------------------------------------------------------------
77
- WRENCODE_VERSION = "0.1.4.4"
90
+ WRENCODE_VERSION = "0.1.4.6"
78
91
 
79
92
  # Per-backend defaults. "kind" controls how a backend is treated:
80
93
  # api - hosted HTTP API, needs an API key
@@ -220,7 +233,7 @@ def apply_backend(backend: str, model: str = "", api_key: str = "") -> None:
220
233
 
221
234
 
222
235
  # -----------------------------------------------------------------------------------------------
223
- # Constants & Environment Variables
236
+ # Constants & environment variables
224
237
  # -----------------------------------------------------------------------------------------------
225
238
  MAX_TOKENS = int(os.environ.get("MAX_TOKENS", "8192"))
226
239
  MAX_READ_BYTES = int(os.environ.get("MAX_READ_BYTES", str(4 * 1024 * 1024)))
@@ -239,7 +252,7 @@ _GLOB_SKIP: set[str] = {
239
252
  }
240
253
 
241
254
  # -----------------------------------------------------------------------------------------------
242
- # Terminal Colors
255
+ # Terminal colors
243
256
  # -----------------------------------------------------------------------------------------------
244
257
  RESET, BOLD, DIM = "\033[0m", "\033[1m", "\033[2m"
245
258
  BLUE, CYAN, GREEN, YELLOW, RED = (
@@ -505,7 +518,7 @@ WREN_BANNER = f"""{BRIGHT_CYAN}\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\
505
518
 
506
519
 
507
520
  # -----------------------------------------------------------------------------------------------
508
- # Path Helpers
521
+ # Path helpers
509
522
  # -----------------------------------------------------------------------------------------------
510
523
  def workspace_root() -> pathlib.Path:
511
524
  """Return the resolved workspace root path from env or cwd."""
@@ -533,7 +546,7 @@ def resolve_tool_path(raw: Any) -> pathlib.Path:
533
546
 
534
547
 
535
548
  # -----------------------------------------------------------------------------------------------
536
- # Input Validation
549
+ # Input validation
537
550
  # -----------------------------------------------------------------------------------------------
538
551
  def _require_str(args: dict[str, Any], key: str) -> str:
539
552
  """Require a non-empty string value from args dict by key."""
@@ -1031,7 +1044,7 @@ def run_tool(name: str, args: dict[str, Any]) -> str:
1031
1044
 
1032
1045
 
1033
1046
  # -----------------------------------------------------------------------------------------------
1034
- # Message Formatting
1047
+ # Message formatting
1035
1048
  # -----------------------------------------------------------------------------------------------
1036
1049
  def flatten_content(content: Any) -> str:
1037
1050
  """Flatten Anthropic-style content list to plain string."""
@@ -1143,7 +1156,7 @@ def thinking_spinner() -> Any:
1143
1156
 
1144
1157
 
1145
1158
  # -----------------------------------------------------------------------------------------------
1146
- # Token & Output Cleaning
1159
+ # Token & output cleaning
1147
1160
  # -----------------------------------------------------------------------------------------------
1148
1161
  def strip_gptoss_tokens(text: str) -> str:
1149
1162
  """Strip GPT-OSS special tokens and channel markers from model output."""
@@ -1244,7 +1257,7 @@ def parse_tool_calls(text: str) -> list[dict[str, Any]]:
1244
1257
 
1245
1258
 
1246
1259
  # -----------------------------------------------------------------------------------------------
1247
- # Anthropic Native Tools
1260
+ # Anthropic native tools
1248
1261
  # -----------------------------------------------------------------------------------------------
1249
1262
  _TYPE_MAP: dict[str, str] = {
1250
1263
  "string": "string",
@@ -1300,9 +1313,32 @@ def _warn_if_truncated(data: dict[str, Any]) -> None:
1300
1313
  )
1301
1314
 
1302
1315
 
1316
+ def _log_usage_debug(data: dict[str, Any]) -> None:
1317
+ """When WRENCODE_DEBUG=1, print one stderr line per turn with token usage.
1318
+ Surfaces cache_creation_input_tokens / cache_read_input_tokens so prompt-
1319
+ cache hit rate is observable without external tooling.
1320
+ """
1321
+ if not os.environ.get("WRENCODE_DEBUG"):
1322
+ return
1323
+ u = data.get("usage", {})
1324
+ if BACKEND == "anthropic":
1325
+ print(
1326
+ f"{DIM}usage: in={u.get('input_tokens')} out={u.get('output_tokens')} "
1327
+ f"cache_write={u.get('cache_creation_input_tokens', 0)} "
1328
+ f"cache_read={u.get('cache_read_input_tokens', 0)}{RESET}",
1329
+ file=sys.stderr,
1330
+ )
1331
+ else:
1332
+ print(
1333
+ f"{DIM}usage: in={u.get('prompt_tokens')} out={u.get('completion_tokens')}{RESET}",
1334
+ file=sys.stderr,
1335
+ )
1336
+
1337
+
1303
1338
  def _parse_native_response(data: dict[str, Any]) -> tuple[str, list["ToolCall"]]:
1304
1339
  """Parse a native (Anthropic / OpenAI) API response into display text and tool calls."""
1305
1340
  _warn_if_truncated(data)
1341
+ _log_usage_debug(data)
1306
1342
  if BACKEND == "anthropic":
1307
1343
  blocks = data.get("content", [])
1308
1344
  text = "\n".join(b["text"] for b in blocks if b.get("type") == "text").strip()
@@ -1386,7 +1422,7 @@ def _append_tool_results(
1386
1422
 
1387
1423
 
1388
1424
  # -----------------------------------------------------------------------------------------------
1389
- # HTTP Helper
1425
+ # HTTP helper
1390
1426
  # -----------------------------------------------------------------------------------------------
1391
1427
  def _http_post(
1392
1428
  url: str, payload: dict[str, Any], headers: dict[str, str]
@@ -1415,7 +1451,7 @@ def get_response(
1415
1451
  for m in messages
1416
1452
  ]
1417
1453
 
1418
- # OpenAI — native function calling
1454
+ # OpenAI - native function calling
1419
1455
  if BACKEND == "openai":
1420
1456
  data = _http_post(
1421
1457
  API_BASE,
@@ -1432,7 +1468,7 @@ def get_response(
1432
1468
  )
1433
1469
  return json.dumps(data) # return raw for agent loop to parse natively
1434
1470
 
1435
- # OpenRouter / Ollama — OpenAI-compatible chat completions (no native tools)
1471
+ # OpenRouter / Ollama - OpenAI-compatible chat completions (no native tools)
1436
1472
  if BACKEND in {"openrouter", "ollama"}:
1437
1473
  data = _http_post(
1438
1474
  API_BASE,
@@ -1447,22 +1483,30 @@ def get_response(
1447
1483
  )
1448
1484
  return str(data["choices"][0]["message"]["content"])
1449
1485
 
1450
- # Anthropic — native tool use API
1486
+ # Anthropic - native tool use API
1451
1487
  if BACKEND == "anthropic":
1488
+ # Prompt caching: mark the (large, static) system block + tool schemas as
1489
+ # ephemeral. Anthropic caches everything up to and including each marker
1490
+ # for ~5 minutes; subsequent turns in the same session read at ~10% of
1491
+ # normal input cost. Two breakpoints out of the four allowed per request.
1492
+ tools = _build_tool_schemas("anthropic")
1493
+ if tools:
1494
+ tools[-1] = {**tools[-1], "cache_control": {"type": "ephemeral"}}
1452
1495
  data = _http_post(
1453
1496
  API_BASE,
1454
1497
  {
1455
1498
  "model": MODEL,
1456
- "system": system_prompt,
1499
+ "system": [{"type": "text", "text": system_prompt,
1500
+ "cache_control": {"type": "ephemeral"}}],
1457
1501
  "messages": messages,
1458
1502
  "max_tokens": MAX_TOKENS,
1459
- "tools": _build_tool_schemas("anthropic"),
1503
+ "tools": tools,
1460
1504
  },
1461
1505
  _anthropic_headers(),
1462
1506
  )
1463
1507
  return json.dumps(data) # return raw for agent loop to parse natively
1464
1508
 
1465
- # Local proxy — Anthropic messages API; tool calls returned as XML <tool_call> tags in text
1509
+ # Local proxy - Anthropic messages API; tool calls returned as XML <tool_call> tags in text
1466
1510
  if BACKEND == "local":
1467
1511
  data = _http_post(
1468
1512
  API_BASE,
@@ -1534,7 +1578,7 @@ def get_response(
1534
1578
 
1535
1579
 
1536
1580
  # -----------------------------------------------------------------------------------------------
1537
- # History Management
1581
+ # History management
1538
1582
  # -----------------------------------------------------------------------------------------------
1539
1583
  def history_file_path() -> pathlib.Path:
1540
1584
  """Return the history file path from env override or user-level default."""
@@ -1634,7 +1678,7 @@ def compact_messages(
1634
1678
 
1635
1679
 
1636
1680
  # -----------------------------------------------------------------------------------------------
1637
- # Workspace & System Prompt
1681
+ # Workspace & system prompt
1638
1682
  # -----------------------------------------------------------------------------------------------
1639
1683
  def git_context() -> str:
1640
1684
  """Return a formatted git status string if inside a git repository."""
@@ -1686,7 +1730,7 @@ CRITICAL: You MUST use tools for file operations. Never say you can't access fil
1686
1730
 
1687
1731
 
1688
1732
  # -----------------------------------------------------------------------------------------------
1689
- # Agent Loop
1733
+ # Agentic loop
1690
1734
  # -----------------------------------------------------------------------------------------------
1691
1735
  def _track_error(
1692
1736
  result: str, last: Optional[str], count: int
@@ -1755,7 +1799,7 @@ def run_agent_turn(
1755
1799
 
1756
1800
 
1757
1801
  # -----------------------------------------------------------------------------------------------
1758
- # Slash Commands
1802
+ # Slash commands
1759
1803
  # -----------------------------------------------------------------------------------------------
1760
1804
  def handle_slash_command(
1761
1805
  cmd: str,
@@ -1806,7 +1850,7 @@ def handle_slash_command(
1806
1850
 
1807
1851
 
1808
1852
  # -----------------------------------------------------------------------------------------------
1809
- # Backend Selection & Config Persistence
1853
+ # Backend selection & config persistence
1810
1854
  # -----------------------------------------------------------------------------------------------
1811
1855
  def is_frozen() -> bool:
1812
1856
  """Set true when running as a PyInstaller standalone binary."""
@@ -2159,7 +2203,7 @@ def resolve_configuration(force_chooser: bool = False) -> None:
2159
2203
 
2160
2204
 
2161
2205
  # -----------------------------------------------------------------------------------------------
2162
- # Model Loading
2206
+ # Model loading
2163
2207
  # -----------------------------------------------------------------------------------------------
2164
2208
  def load_model() -> Optional[tuple[Any, Any]]:
2165
2209
  """Load model for the current backend and return mlx_state (or None for API backends)."""
@@ -2247,7 +2291,7 @@ def load_model() -> Optional[tuple[Any, Any]]:
2247
2291
 
2248
2292
 
2249
2293
  # -----------------------------------------------------------------------------------------------
2250
- # Entry Point
2294
+ # Entrypoint
2251
2295
  # -----------------------------------------------------------------------------------------------
2252
2296
  def print_help() -> None:
2253
2297
  """Print CLI usage."""
File without changes
File without changes
File without changes