wrencode 0.1.4.5__tar.gz → 0.1.4.7__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.
|
|
3
|
+
Version: 0.1.4.7
|
|
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")
|
|
@@ -74,7 +87,7 @@ for _dir in (os.path.dirname(os.path.abspath(__file__)), os.getcwd()):
|
|
|
74
87
|
# -----------------------------------------------------------------------------------------------
|
|
75
88
|
# Backend configuration
|
|
76
89
|
# -----------------------------------------------------------------------------------------------
|
|
77
|
-
WRENCODE_VERSION = "0.1.4.
|
|
90
|
+
WRENCODE_VERSION = "0.1.4.7"
|
|
78
91
|
|
|
79
92
|
# Per-backend defaults. "kind" controls how a backend is treated:
|
|
80
93
|
# api - hosted HTTP API, needs an API key
|
|
@@ -267,6 +280,7 @@ def _loader_context_text() -> str:
|
|
|
267
280
|
|
|
268
281
|
|
|
269
282
|
def loader_display(step: int) -> str:
|
|
283
|
+
"""Render one animated loader frame for the given step."""
|
|
270
284
|
sym = _COMPOSE_FRAMES[step % len(_COMPOSE_FRAMES)]
|
|
271
285
|
text = _loader_context_text()
|
|
272
286
|
if not colors_enabled():
|
|
@@ -275,7 +289,7 @@ def loader_display(step: int) -> str:
|
|
|
275
289
|
|
|
276
290
|
|
|
277
291
|
def colors_enabled() -> bool:
|
|
278
|
-
"""True when ANSI styling should be applied."""
|
|
292
|
+
"""Return True when ANSI styling should be applied."""
|
|
279
293
|
if os.environ.get("NO_COLOR") is not None:
|
|
280
294
|
return False
|
|
281
295
|
if os.environ.get("FORCE_COLOR"):
|
|
@@ -1280,6 +1294,7 @@ def _openai_headers() -> dict[str, str]:
|
|
|
1280
1294
|
|
|
1281
1295
|
def _warn_if_truncated(data: dict[str, Any]) -> None:
|
|
1282
1296
|
"""Print a stderr warning if the API truncated the response at max_tokens.
|
|
1297
|
+
|
|
1283
1298
|
A truncated `tool_use` may be missing required fields (e.g. `content` for
|
|
1284
1299
|
`write`), so callers need to know to raise MAX_TOKENS rather than silently
|
|
1285
1300
|
treating an empty operation as success.
|
|
@@ -1302,6 +1317,7 @@ def _warn_if_truncated(data: dict[str, Any]) -> None:
|
|
|
1302
1317
|
|
|
1303
1318
|
def _log_usage_debug(data: dict[str, Any]) -> None:
|
|
1304
1319
|
"""When WRENCODE_DEBUG=1, print one stderr line per turn with token usage.
|
|
1320
|
+
|
|
1305
1321
|
Surfaces cache_creation_input_tokens / cache_read_input_tokens so prompt-
|
|
1306
1322
|
cache hit rate is observable without external tooling.
|
|
1307
1323
|
"""
|
|
@@ -1347,6 +1363,8 @@ def _parse_native_response(data: dict[str, Any]) -> tuple[str, list["ToolCall"]]
|
|
|
1347
1363
|
|
|
1348
1364
|
@dataclass
|
|
1349
1365
|
class ToolCall:
|
|
1366
|
+
"""A single tool invocation parsed from a model response."""
|
|
1367
|
+
|
|
1350
1368
|
id: str
|
|
1351
1369
|
name: str
|
|
1352
1370
|
input: dict[str, Any]
|
|
@@ -2026,6 +2044,39 @@ def persist_backend_choice(cfg: dict[str, str]) -> None:
|
|
|
2026
2044
|
)
|
|
2027
2045
|
|
|
2028
2046
|
|
|
2047
|
+
def verify_api_key() -> tuple[str, str]:
|
|
2048
|
+
"""Probe the current backend with API_KEY to see if the key actually works.
|
|
2049
|
+
|
|
2050
|
+
Returns (state, detail): "ok" (accepted), "invalid" (rejected by the
|
|
2051
|
+
service), or "unknown" (couldn't reach it — network/timeout). Local
|
|
2052
|
+
backends and missing keys short-circuit to "ok"/"invalid" without a call.
|
|
2053
|
+
"""
|
|
2054
|
+
spec = BACKEND_SPECS[BACKEND]
|
|
2055
|
+
if spec["kind"] != "api":
|
|
2056
|
+
return ("ok", "")
|
|
2057
|
+
if not API_KEY:
|
|
2058
|
+
return ("invalid", "no key")
|
|
2059
|
+
probes = {
|
|
2060
|
+
"anthropic": ("https://api.anthropic.com/v1/models", _anthropic_headers()),
|
|
2061
|
+
"openai": ("https://api.openai.com/v1/models", _openai_headers()),
|
|
2062
|
+
"openrouter": ("https://openrouter.ai/api/v1/key", _openai_headers()),
|
|
2063
|
+
}
|
|
2064
|
+
if BACKEND not in probes:
|
|
2065
|
+
return ("unknown", "")
|
|
2066
|
+
url, headers = probes[BACKEND]
|
|
2067
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
2068
|
+
try:
|
|
2069
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
2070
|
+
resp.read(1)
|
|
2071
|
+
return ("ok", "")
|
|
2072
|
+
except urllib.error.HTTPError as err:
|
|
2073
|
+
if err.code in (401, 403):
|
|
2074
|
+
return ("invalid", f"HTTP {err.code}")
|
|
2075
|
+
return ("unknown", f"HTTP {err.code}")
|
|
2076
|
+
except Exception as err:
|
|
2077
|
+
return ("unknown", str(err))
|
|
2078
|
+
|
|
2079
|
+
|
|
2029
2080
|
def pick_model_interactive(backend: str) -> Optional[str]:
|
|
2030
2081
|
"""Arrow-key model picker for a backend; returns model id or None."""
|
|
2031
2082
|
models = list_models_for_backend(backend)
|
|
@@ -2140,21 +2191,36 @@ def choose_backend_interactive() -> None:
|
|
|
2140
2191
|
raise SystemExit(1)
|
|
2141
2192
|
|
|
2142
2193
|
cfg: dict[str, str] = {"backend": choice, "model": model}
|
|
2194
|
+
spec = BACKEND_SPECS[choice]
|
|
2143
2195
|
_prompt_api_key_if_needed(choice, existing, cfg)
|
|
2144
2196
|
persist_backend_choice(cfg)
|
|
2145
2197
|
print_system(f"✓ Saved backend choice to {CONFIG_FILE}")
|
|
2146
|
-
print()
|
|
2147
|
-
|
|
2148
2198
|
|
|
2149
|
-
|
|
2199
|
+
# Verify the key actually works before declaring the backend ready, so a
|
|
2200
|
+
# typo or revoked key surfaces here instead of mid-chat. Re-prompt on a
|
|
2201
|
+
# hard rejection; don't block on a transient network failure.
|
|
2202
|
+
for attempt in range(3):
|
|
2203
|
+
state, detail = verify_api_key()
|
|
2204
|
+
if state == "ok":
|
|
2205
|
+
print_system(f"✓ {choice}:{MODEL} ready")
|
|
2206
|
+
return
|
|
2207
|
+
if state == "unknown":
|
|
2208
|
+
print(f"{YELLOW}⚠ Couldn't verify {spec['key_env']} ({detail}).{RESET}")
|
|
2209
|
+
return
|
|
2210
|
+
print(f"{RED}✗ {spec['key_env']} was rejected ({detail}).{RESET}")
|
|
2211
|
+
if not sys.stdin.isatty() or attempt == 2:
|
|
2212
|
+
return
|
|
2213
|
+
newkey = getpass.getpass(
|
|
2214
|
+
f"{BLUE}❯{RESET} re-enter {spec['key_env']} (input hidden): "
|
|
2215
|
+
).strip()
|
|
2216
|
+
if not newkey:
|
|
2217
|
+
return
|
|
2218
|
+
cfg["api_key"] = newkey
|
|
2219
|
+
persist_backend_choice(cfg)
|
|
2220
|
+
|
|
2221
|
+
|
|
2222
|
+
def resolve_configuration() -> None:
|
|
2150
2223
|
"""Decide which backend to use: env override > saved config > interactive > error."""
|
|
2151
|
-
if force_chooser:
|
|
2152
|
-
if not sys.stdin.isatty():
|
|
2153
|
-
print(f"{RED}--configure needs an interactive terminal.{RESET}")
|
|
2154
|
-
raise SystemExit(1)
|
|
2155
|
-
choose_backend_interactive()
|
|
2156
|
-
return
|
|
2157
|
-
|
|
2158
2224
|
# 1. Explicit BACKEND env var — power users / CI. Unchanged from prior behaviour.
|
|
2159
2225
|
env_backend = os.environ.get("BACKEND")
|
|
2160
2226
|
if env_backend:
|
|
@@ -2170,9 +2236,26 @@ def resolve_configuration(force_chooser: bool = False) -> None:
|
|
|
2170
2236
|
# 2. A choice saved from a previous run.
|
|
2171
2237
|
cfg = load_config()
|
|
2172
2238
|
if cfg.get("backend") in BACKEND_SPECS:
|
|
2173
|
-
|
|
2174
|
-
|
|
2239
|
+
backend = cfg["backend"]
|
|
2240
|
+
spec = BACKEND_SPECS[backend]
|
|
2241
|
+
# A hosted backend with no usable key — env var unset and nothing
|
|
2242
|
+
# saved (e.g. configured in a dir whose .env supplied the key, so it
|
|
2243
|
+
# was never persisted) — would dead-end in load_model() with a
|
|
2244
|
+
# SystemExit. In a terminal, re-run the chooser so the user can pick a
|
|
2245
|
+
# backend and enter a key instead of the tool exiting immediately.
|
|
2246
|
+
key_missing = (
|
|
2247
|
+
spec["kind"] == "api"
|
|
2248
|
+
and not os.environ.get(spec["key_env"])
|
|
2249
|
+
and not cfg.get("api_key")
|
|
2175
2250
|
)
|
|
2251
|
+
if key_missing and sys.stdin.isatty():
|
|
2252
|
+
print(
|
|
2253
|
+
f"{YELLOW}Saved backend '{backend}' has no API key "
|
|
2254
|
+
f"({spec['key_env']} is unset and none was saved).{RESET}"
|
|
2255
|
+
)
|
|
2256
|
+
choose_backend_interactive()
|
|
2257
|
+
return
|
|
2258
|
+
apply_backend(backend, cfg.get("model", ""), cfg.get("api_key", ""))
|
|
2176
2259
|
return
|
|
2177
2260
|
|
|
2178
2261
|
# 3. First run with a real terminal — ask the user.
|
|
@@ -2184,7 +2267,7 @@ def resolve_configuration(force_chooser: bool = False) -> None:
|
|
|
2184
2267
|
print(f"{RED}No backend configured.{RESET}")
|
|
2185
2268
|
print(
|
|
2186
2269
|
"Set BACKEND=<name> (plus the matching API key), "
|
|
2187
|
-
"or run `wrencode
|
|
2270
|
+
"or run `wrencode` in a terminal to choose one."
|
|
2188
2271
|
)
|
|
2189
2272
|
raise SystemExit(1)
|
|
2190
2273
|
|
|
@@ -2203,7 +2286,7 @@ def load_model() -> Optional[tuple[Any, Any]]:
|
|
|
2203
2286
|
except ImportError:
|
|
2204
2287
|
print(f"{RED}MLX backend needs mlx-lm:{RESET} pip install mlx-lm")
|
|
2205
2288
|
print(
|
|
2206
|
-
f"{DIM}Or run `wrencode
|
|
2289
|
+
f"{DIM}Or run `wrencode` to pick a hosted backend.{RESET}"
|
|
2207
2290
|
)
|
|
2208
2291
|
raise SystemExit(1)
|
|
2209
2292
|
print(f"{YELLOW}Loading model...{RESET}")
|
|
@@ -2225,7 +2308,7 @@ def load_model() -> Optional[tuple[Any, Any]]:
|
|
|
2225
2308
|
"pip install transformers torch"
|
|
2226
2309
|
)
|
|
2227
2310
|
print(
|
|
2228
|
-
f"{DIM}Or run `wrencode
|
|
2311
|
+
f"{DIM}Or run `wrencode` to pick a hosted backend.{RESET}"
|
|
2229
2312
|
)
|
|
2230
2313
|
raise SystemExit(1)
|
|
2231
2314
|
print(f"{YELLOW}Loading model via transformers...{RESET}")
|
|
@@ -2270,7 +2353,7 @@ def load_model() -> Optional[tuple[Any, Any]]:
|
|
|
2270
2353
|
key_env = BACKEND_SPECS[BACKEND]["key_env"]
|
|
2271
2354
|
print(f"{RED}{key_env} not set.{RESET}")
|
|
2272
2355
|
print(
|
|
2273
|
-
f"{DIM}Set {key_env}, or run `wrencode
|
|
2356
|
+
f"{DIM}Set {key_env}, or run `wrencode` in a terminal to enter a key.{RESET}"
|
|
2274
2357
|
)
|
|
2275
2358
|
raise SystemExit(1)
|
|
2276
2359
|
print_system(f"{BACKEND} ({MODEL})")
|
|
@@ -2280,15 +2363,36 @@ def load_model() -> Optional[tuple[Any, Any]]:
|
|
|
2280
2363
|
# -----------------------------------------------------------------------------------------------
|
|
2281
2364
|
# Entrypoint
|
|
2282
2365
|
# -----------------------------------------------------------------------------------------------
|
|
2366
|
+
def uninstall() -> None:
|
|
2367
|
+
"""Delete saved state and print how to remove the program for this install."""
|
|
2368
|
+
if CONFIG_DIR.exists():
|
|
2369
|
+
try:
|
|
2370
|
+
shutil.rmtree(CONFIG_DIR)
|
|
2371
|
+
print_system(f"✓ Removed saved config {CONFIG_DIR}")
|
|
2372
|
+
except OSError as err:
|
|
2373
|
+
print(f"{YELLOW}Could not remove {CONFIG_DIR}: {err}{RESET}")
|
|
2374
|
+
else:
|
|
2375
|
+
print_system(f"No saved config at {CONFIG_DIR}")
|
|
2376
|
+
|
|
2377
|
+
print_system("To remove the program itself:")
|
|
2378
|
+
if is_frozen():
|
|
2379
|
+
# install.sh drops a standalone binary; sys.executable is that file.
|
|
2380
|
+
print(f"rm {sys.executable}")
|
|
2381
|
+
else:
|
|
2382
|
+
print(f"{DIM}uv tool install:{RESET} uv tool uninstall wrencode")
|
|
2383
|
+
print(f"{DIM}pip install:{RESET} pip uninstall wrencode")
|
|
2384
|
+
print(f"{DIM}uvx cache:{RESET} uv cache clean wrencode")
|
|
2385
|
+
|
|
2386
|
+
|
|
2283
2387
|
def print_help() -> None:
|
|
2284
2388
|
"""Print CLI usage."""
|
|
2285
2389
|
print("wrencode — a minimal agentic coding assistant\n")
|
|
2286
2390
|
print("Usage: wrencode [options]\n")
|
|
2287
2391
|
print("Options:")
|
|
2288
|
-
print("
|
|
2289
|
-
print("
|
|
2290
|
-
print("
|
|
2291
|
-
print("
|
|
2392
|
+
print("--yes auto-approve all writes/commands (WRENCODE_AUTO_APPROVE)")
|
|
2393
|
+
print("--uninstall remove saved config and show how to delete wrencode")
|
|
2394
|
+
print("--version, -V print version and exit")
|
|
2395
|
+
print("--help, -h show this help\n")
|
|
2292
2396
|
print("Slash commands: /backend /model /c /compact /q (see /help in session)")
|
|
2293
2397
|
print(
|
|
2294
2398
|
"Environment overrides: BACKEND, MODEL, and the backend's API key "
|
|
@@ -2310,14 +2414,16 @@ def main() -> None:
|
|
|
2310
2414
|
if "--version" in args or "-V" in args:
|
|
2311
2415
|
print(f"wrencode {WRENCODE_VERSION}")
|
|
2312
2416
|
return
|
|
2313
|
-
|
|
2417
|
+
if "--uninstall" in args or (bool(args) and args[0] == "uninstall"):
|
|
2418
|
+
uninstall()
|
|
2419
|
+
return
|
|
2314
2420
|
if "--yes" in args or "--auto-approve" in args:
|
|
2315
2421
|
os.environ["WRENCODE_AUTO_APPROVE"] = "1"
|
|
2316
2422
|
|
|
2317
2423
|
os.environ.setdefault(
|
|
2318
2424
|
"WRENCODE_WORKSPACE", str(pathlib.Path.cwd().resolve())
|
|
2319
2425
|
)
|
|
2320
|
-
resolve_configuration(
|
|
2426
|
+
resolve_configuration()
|
|
2321
2427
|
|
|
2322
2428
|
sys.stdout.write("\033]0;wrencode\007") # set terminal tab/window title
|
|
2323
2429
|
print(WREN_BANNER)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|