wrencode 0.1.4.6__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
|
|
@@ -87,7 +87,7 @@ for _dir in (os.path.dirname(os.path.abspath(__file__)), os.getcwd()):
|
|
|
87
87
|
# -----------------------------------------------------------------------------------------------
|
|
88
88
|
# Backend configuration
|
|
89
89
|
# -----------------------------------------------------------------------------------------------
|
|
90
|
-
WRENCODE_VERSION = "0.1.4.
|
|
90
|
+
WRENCODE_VERSION = "0.1.4.7"
|
|
91
91
|
|
|
92
92
|
# Per-backend defaults. "kind" controls how a backend is treated:
|
|
93
93
|
# api - hosted HTTP API, needs an API key
|
|
@@ -280,6 +280,7 @@ def _loader_context_text() -> str:
|
|
|
280
280
|
|
|
281
281
|
|
|
282
282
|
def loader_display(step: int) -> str:
|
|
283
|
+
"""Render one animated loader frame for the given step."""
|
|
283
284
|
sym = _COMPOSE_FRAMES[step % len(_COMPOSE_FRAMES)]
|
|
284
285
|
text = _loader_context_text()
|
|
285
286
|
if not colors_enabled():
|
|
@@ -288,7 +289,7 @@ def loader_display(step: int) -> str:
|
|
|
288
289
|
|
|
289
290
|
|
|
290
291
|
def colors_enabled() -> bool:
|
|
291
|
-
"""True when ANSI styling should be applied."""
|
|
292
|
+
"""Return True when ANSI styling should be applied."""
|
|
292
293
|
if os.environ.get("NO_COLOR") is not None:
|
|
293
294
|
return False
|
|
294
295
|
if os.environ.get("FORCE_COLOR"):
|
|
@@ -1293,6 +1294,7 @@ def _openai_headers() -> dict[str, str]:
|
|
|
1293
1294
|
|
|
1294
1295
|
def _warn_if_truncated(data: dict[str, Any]) -> None:
|
|
1295
1296
|
"""Print a stderr warning if the API truncated the response at max_tokens.
|
|
1297
|
+
|
|
1296
1298
|
A truncated `tool_use` may be missing required fields (e.g. `content` for
|
|
1297
1299
|
`write`), so callers need to know to raise MAX_TOKENS rather than silently
|
|
1298
1300
|
treating an empty operation as success.
|
|
@@ -1315,6 +1317,7 @@ def _warn_if_truncated(data: dict[str, Any]) -> None:
|
|
|
1315
1317
|
|
|
1316
1318
|
def _log_usage_debug(data: dict[str, Any]) -> None:
|
|
1317
1319
|
"""When WRENCODE_DEBUG=1, print one stderr line per turn with token usage.
|
|
1320
|
+
|
|
1318
1321
|
Surfaces cache_creation_input_tokens / cache_read_input_tokens so prompt-
|
|
1319
1322
|
cache hit rate is observable without external tooling.
|
|
1320
1323
|
"""
|
|
@@ -1360,6 +1363,8 @@ def _parse_native_response(data: dict[str, Any]) -> tuple[str, list["ToolCall"]]
|
|
|
1360
1363
|
|
|
1361
1364
|
@dataclass
|
|
1362
1365
|
class ToolCall:
|
|
1366
|
+
"""A single tool invocation parsed from a model response."""
|
|
1367
|
+
|
|
1363
1368
|
id: str
|
|
1364
1369
|
name: str
|
|
1365
1370
|
input: dict[str, Any]
|
|
@@ -2039,6 +2044,39 @@ def persist_backend_choice(cfg: dict[str, str]) -> None:
|
|
|
2039
2044
|
)
|
|
2040
2045
|
|
|
2041
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
|
+
|
|
2042
2080
|
def pick_model_interactive(backend: str) -> Optional[str]:
|
|
2043
2081
|
"""Arrow-key model picker for a backend; returns model id or None."""
|
|
2044
2082
|
models = list_models_for_backend(backend)
|
|
@@ -2153,21 +2191,36 @@ def choose_backend_interactive() -> None:
|
|
|
2153
2191
|
raise SystemExit(1)
|
|
2154
2192
|
|
|
2155
2193
|
cfg: dict[str, str] = {"backend": choice, "model": model}
|
|
2194
|
+
spec = BACKEND_SPECS[choice]
|
|
2156
2195
|
_prompt_api_key_if_needed(choice, existing, cfg)
|
|
2157
2196
|
persist_backend_choice(cfg)
|
|
2158
2197
|
print_system(f"✓ Saved backend choice to {CONFIG_FILE}")
|
|
2159
|
-
print()
|
|
2160
|
-
|
|
2161
2198
|
|
|
2162
|
-
|
|
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:
|
|
2163
2223
|
"""Decide which backend to use: env override > saved config > interactive > error."""
|
|
2164
|
-
if force_chooser:
|
|
2165
|
-
if not sys.stdin.isatty():
|
|
2166
|
-
print(f"{RED}--configure needs an interactive terminal.{RESET}")
|
|
2167
|
-
raise SystemExit(1)
|
|
2168
|
-
choose_backend_interactive()
|
|
2169
|
-
return
|
|
2170
|
-
|
|
2171
2224
|
# 1. Explicit BACKEND env var — power users / CI. Unchanged from prior behaviour.
|
|
2172
2225
|
env_backend = os.environ.get("BACKEND")
|
|
2173
2226
|
if env_backend:
|
|
@@ -2183,9 +2236,26 @@ def resolve_configuration(force_chooser: bool = False) -> None:
|
|
|
2183
2236
|
# 2. A choice saved from a previous run.
|
|
2184
2237
|
cfg = load_config()
|
|
2185
2238
|
if cfg.get("backend") in BACKEND_SPECS:
|
|
2186
|
-
|
|
2187
|
-
|
|
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")
|
|
2188
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", ""))
|
|
2189
2259
|
return
|
|
2190
2260
|
|
|
2191
2261
|
# 3. First run with a real terminal — ask the user.
|
|
@@ -2197,7 +2267,7 @@ def resolve_configuration(force_chooser: bool = False) -> None:
|
|
|
2197
2267
|
print(f"{RED}No backend configured.{RESET}")
|
|
2198
2268
|
print(
|
|
2199
2269
|
"Set BACKEND=<name> (plus the matching API key), "
|
|
2200
|
-
"or run `wrencode
|
|
2270
|
+
"or run `wrencode` in a terminal to choose one."
|
|
2201
2271
|
)
|
|
2202
2272
|
raise SystemExit(1)
|
|
2203
2273
|
|
|
@@ -2216,7 +2286,7 @@ def load_model() -> Optional[tuple[Any, Any]]:
|
|
|
2216
2286
|
except ImportError:
|
|
2217
2287
|
print(f"{RED}MLX backend needs mlx-lm:{RESET} pip install mlx-lm")
|
|
2218
2288
|
print(
|
|
2219
|
-
f"{DIM}Or run `wrencode
|
|
2289
|
+
f"{DIM}Or run `wrencode` to pick a hosted backend.{RESET}"
|
|
2220
2290
|
)
|
|
2221
2291
|
raise SystemExit(1)
|
|
2222
2292
|
print(f"{YELLOW}Loading model...{RESET}")
|
|
@@ -2238,7 +2308,7 @@ def load_model() -> Optional[tuple[Any, Any]]:
|
|
|
2238
2308
|
"pip install transformers torch"
|
|
2239
2309
|
)
|
|
2240
2310
|
print(
|
|
2241
|
-
f"{DIM}Or run `wrencode
|
|
2311
|
+
f"{DIM}Or run `wrencode` to pick a hosted backend.{RESET}"
|
|
2242
2312
|
)
|
|
2243
2313
|
raise SystemExit(1)
|
|
2244
2314
|
print(f"{YELLOW}Loading model via transformers...{RESET}")
|
|
@@ -2283,7 +2353,7 @@ def load_model() -> Optional[tuple[Any, Any]]:
|
|
|
2283
2353
|
key_env = BACKEND_SPECS[BACKEND]["key_env"]
|
|
2284
2354
|
print(f"{RED}{key_env} not set.{RESET}")
|
|
2285
2355
|
print(
|
|
2286
|
-
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}"
|
|
2287
2357
|
)
|
|
2288
2358
|
raise SystemExit(1)
|
|
2289
2359
|
print_system(f"{BACKEND} ({MODEL})")
|
|
@@ -2293,15 +2363,36 @@ def load_model() -> Optional[tuple[Any, Any]]:
|
|
|
2293
2363
|
# -----------------------------------------------------------------------------------------------
|
|
2294
2364
|
# Entrypoint
|
|
2295
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
|
+
|
|
2296
2387
|
def print_help() -> None:
|
|
2297
2388
|
"""Print CLI usage."""
|
|
2298
2389
|
print("wrencode — a minimal agentic coding assistant\n")
|
|
2299
2390
|
print("Usage: wrencode [options]\n")
|
|
2300
2391
|
print("Options:")
|
|
2301
|
-
print("
|
|
2302
|
-
print("
|
|
2303
|
-
print("
|
|
2304
|
-
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")
|
|
2305
2396
|
print("Slash commands: /backend /model /c /compact /q (see /help in session)")
|
|
2306
2397
|
print(
|
|
2307
2398
|
"Environment overrides: BACKEND, MODEL, and the backend's API key "
|
|
@@ -2323,14 +2414,16 @@ def main() -> None:
|
|
|
2323
2414
|
if "--version" in args or "-V" in args:
|
|
2324
2415
|
print(f"wrencode {WRENCODE_VERSION}")
|
|
2325
2416
|
return
|
|
2326
|
-
|
|
2417
|
+
if "--uninstall" in args or (bool(args) and args[0] == "uninstall"):
|
|
2418
|
+
uninstall()
|
|
2419
|
+
return
|
|
2327
2420
|
if "--yes" in args or "--auto-approve" in args:
|
|
2328
2421
|
os.environ["WRENCODE_AUTO_APPROVE"] = "1"
|
|
2329
2422
|
|
|
2330
2423
|
os.environ.setdefault(
|
|
2331
2424
|
"WRENCODE_WORKSPACE", str(pathlib.Path.cwd().resolve())
|
|
2332
2425
|
)
|
|
2333
|
-
resolve_configuration(
|
|
2426
|
+
resolve_configuration()
|
|
2334
2427
|
|
|
2335
2428
|
sys.stdout.write("\033]0;wrencode\007") # set terminal tab/window title
|
|
2336
2429
|
print(WREN_BANNER)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|