nabla-cli 0.4.0__tar.gz → 0.4.1__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.
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/PKG-INFO +1 -1
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/__init__.py +1 -1
- nabla_cli-0.4.1/nabla/branding.py +74 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/cli.py +48 -10
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/recorder.py +5 -1
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/ui.py +2 -1
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla_cli.egg-info/PKG-INFO +1 -1
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/pyproject.toml +1 -1
- nabla_cli-0.4.0/nabla/branding.py +0 -56
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/README.md +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/__main__.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/config.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/contract.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/induce.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/profile.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/prompt_recon.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/samplegen.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/scanner.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/schema_resolver.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/schemas/site_profile.schema.json +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla/term.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla_cli.egg-info/SOURCES.txt +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla_cli.egg-info/dependency_links.txt +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla_cli.egg-info/entry_points.txt +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla_cli.egg-info/requires.txt +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/nabla_cli.egg-info/top_level.txt +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/setup.cfg +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/tests/test_cli_ux.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/tests/test_config.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/tests/test_e2e_profile.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/tests/test_induce.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/tests/test_prompt_recon.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/tests/test_recorder.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/tests/test_samplegen.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/tests/test_scanner.py +0 -0
- {nabla_cli-0.4.0 → nabla_cli-0.4.1}/tests/test_schema_resolver.py +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""nabla terminal branding: the ∇ logo and startup banner.
|
|
2
|
+
|
|
3
|
+
The banner prints only on a real terminal — piped output, tests, and
|
|
4
|
+
--json consumers never see it. Block glyphs render via the Windows
|
|
5
|
+
console's UTF-16 writer, so no legacy-codepage fallback is needed on an
|
|
6
|
+
interactive console.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
|
|
16
|
+
# Uniform thick strokes like the wordmark: solid bar, solid diagonals,
|
|
17
|
+
# single-block apex. Terminal cells are ~2:1 tall, so 6 rows x 11 cols
|
|
18
|
+
# reads close to the mark's near-equilateral proportions.
|
|
19
|
+
_LOGO = [
|
|
20
|
+
"█▀▀▀▀▀▀▀▀▀█",
|
|
21
|
+
" █ █",
|
|
22
|
+
" █ █",
|
|
23
|
+
" █ █",
|
|
24
|
+
" █ █",
|
|
25
|
+
" █",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
_BRAND_STYLE = "bold cyan"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def banner_lines() -> list[tuple[str, str, str]]:
|
|
32
|
+
"""(logo_line, tagline, tagline_style) triples; outer rows have no tagline."""
|
|
33
|
+
taglines = [
|
|
34
|
+
("", ""),
|
|
35
|
+
("nabla", "bold"),
|
|
36
|
+
(f"v{__version__} · OpenAI call-site harvester", ""),
|
|
37
|
+
("scan · record · profile", "cyan"),
|
|
38
|
+
("", ""),
|
|
39
|
+
("", ""),
|
|
40
|
+
]
|
|
41
|
+
return [(logo, text, style) for logo, (text, style) in zip(_LOGO, taglines)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _compose(reveal: int | None = None, with_taglines: bool = True) -> Text:
|
|
45
|
+
frame = Text()
|
|
46
|
+
for logo_line, tagline, tag_style in banner_lines():
|
|
47
|
+
segment = logo_line if reveal is None else logo_line[:reveal]
|
|
48
|
+
frame.append(f" {segment:<14}", style=_BRAND_STYLE)
|
|
49
|
+
if with_taglines and tagline:
|
|
50
|
+
frame.append(tagline, style=tag_style)
|
|
51
|
+
frame.append("\n")
|
|
52
|
+
return frame
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def print_banner(console: Console | None = None, animate: bool = False) -> None:
|
|
56
|
+
console = console or Console()
|
|
57
|
+
if not console.is_terminal:
|
|
58
|
+
return
|
|
59
|
+
console.print()
|
|
60
|
+
if animate:
|
|
61
|
+
import time
|
|
62
|
+
|
|
63
|
+
from rich.live import Live
|
|
64
|
+
|
|
65
|
+
width = max(len(line) for line, _, _ in banner_lines())
|
|
66
|
+
with Live(console=console, refresh_per_second=60, transient=False) as live:
|
|
67
|
+
# The mark draws itself in, left to right; the words arrive last.
|
|
68
|
+
for cut in range(width + 1):
|
|
69
|
+
live.update(_compose(reveal=cut, with_taglines=False))
|
|
70
|
+
time.sleep(0.028)
|
|
71
|
+
live.update(_compose())
|
|
72
|
+
else:
|
|
73
|
+
console.print(_compose(), end="")
|
|
74
|
+
console.print()
|
|
@@ -191,15 +191,41 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
191
191
|
return commands[args.command](args)
|
|
192
192
|
|
|
193
193
|
|
|
194
|
+
def _clipboard_text() -> str:
|
|
195
|
+
"""Windows clipboard contents (CF_UNICODETEXT), or "" on any failure."""
|
|
196
|
+
try:
|
|
197
|
+
import ctypes
|
|
198
|
+
|
|
199
|
+
CF_UNICODETEXT = 13
|
|
200
|
+
user32, kernel32 = ctypes.windll.user32, ctypes.windll.kernel32
|
|
201
|
+
if not user32.OpenClipboard(0):
|
|
202
|
+
return ""
|
|
203
|
+
try:
|
|
204
|
+
handle = user32.GetClipboardData(CF_UNICODETEXT)
|
|
205
|
+
if not handle:
|
|
206
|
+
return ""
|
|
207
|
+
locked = kernel32.GlobalLock(handle)
|
|
208
|
+
if not locked:
|
|
209
|
+
return ""
|
|
210
|
+
try:
|
|
211
|
+
return ctypes.c_wchar_p(locked).value or ""
|
|
212
|
+
finally:
|
|
213
|
+
kernel32.GlobalUnlock(handle)
|
|
214
|
+
finally:
|
|
215
|
+
user32.CloseClipboard()
|
|
216
|
+
except Exception:
|
|
217
|
+
return ""
|
|
218
|
+
|
|
219
|
+
|
|
194
220
|
def _read_api_key(prompt: str) -> str:
|
|
195
221
|
"""Read a secret with visible masking: one `*` per character, backspace
|
|
196
|
-
supported,
|
|
197
|
-
|
|
198
|
-
fully hidden input where character
|
|
222
|
+
supported, and Ctrl+V pastes from the clipboard (classic consoles don't
|
|
223
|
+
translate Ctrl+V into keystrokes, which silently swallowed pastes).
|
|
224
|
+
Falls back to fully hidden input where character reading isn't available."""
|
|
199
225
|
if sys.platform == "win32" and sys.stdin.isatty():
|
|
200
226
|
import msvcrt
|
|
201
227
|
|
|
202
|
-
print(prompt, end="", flush=True)
|
|
228
|
+
print(f"{prompt}(paste with Ctrl+V or right-click) ", end="", flush=True)
|
|
203
229
|
chars: list[str] = []
|
|
204
230
|
while True:
|
|
205
231
|
ch = msvcrt.getwch()
|
|
@@ -208,7 +234,13 @@ def _read_api_key(prompt: str) -> str:
|
|
|
208
234
|
break
|
|
209
235
|
if ch == "\x03": # Ctrl+C
|
|
210
236
|
raise KeyboardInterrupt
|
|
211
|
-
if ch == "\
|
|
237
|
+
if ch == "\x16": # Ctrl+V — classic console gives us the raw control char
|
|
238
|
+
pasted = _clipboard_text().strip()
|
|
239
|
+
for c in pasted:
|
|
240
|
+
if c >= " ":
|
|
241
|
+
chars.append(c)
|
|
242
|
+
print("*" * len(pasted), end="", flush=True)
|
|
243
|
+
elif ch == "\b":
|
|
212
244
|
if chars:
|
|
213
245
|
chars.pop()
|
|
214
246
|
print("\b \b", end="", flush=True)
|
|
@@ -222,7 +254,7 @@ def _read_api_key(prompt: str) -> str:
|
|
|
222
254
|
def _cmd_init(args) -> int:
|
|
223
255
|
from .ui import RAIL, rail, rail_end, rail_ok, rail_start, select
|
|
224
256
|
|
|
225
|
-
print_banner(_out)
|
|
257
|
+
print_banner(_out, animate=True)
|
|
226
258
|
rail_start(_out, "nabla init")
|
|
227
259
|
rail(_out)
|
|
228
260
|
|
|
@@ -395,7 +427,7 @@ def _resolve_site(args, sites):
|
|
|
395
427
|
if len(supported) == 1:
|
|
396
428
|
site = supported[0]
|
|
397
429
|
from .ui import rail
|
|
398
|
-
rail(_out, f"using site {site.symbol} (the only supported call site)"
|
|
430
|
+
rail(_out, f"using site {site.symbol} (the only supported call site)")
|
|
399
431
|
return site, None
|
|
400
432
|
|
|
401
433
|
candidates = supported or sites
|
|
@@ -442,8 +474,7 @@ def _cmd_record(args) -> int:
|
|
|
442
474
|
else:
|
|
443
475
|
conventional = Path(f"{site.symbol}.samples.jsonl")
|
|
444
476
|
if conventional.exists():
|
|
445
|
-
rail(_out, f"using {conventional} (found by convention; pass --samples to override)"
|
|
446
|
-
style="dim")
|
|
477
|
+
rail(_out, f"using {conventional} (found by convention; pass --samples to override)")
|
|
447
478
|
source = RecordSource(kind="samples", samples_file=str(conventional))
|
|
448
479
|
else:
|
|
449
480
|
source, err = _generate_samples_source(args, site, conventional)
|
|
@@ -471,6 +502,13 @@ def _cmd_record(args) -> int:
|
|
|
471
502
|
except ZeroTrafficError as exc:
|
|
472
503
|
_fail(f"nabla record: {exc}")
|
|
473
504
|
return 1
|
|
505
|
+
except RuntimeError as exc:
|
|
506
|
+
message = str(exc)
|
|
507
|
+
_fail(f"nabla record: {message[:400]}")
|
|
508
|
+
if "invalid_api_key" in message or "Invalid API Key" in message or "401" in message:
|
|
509
|
+
_warn("the configured provider rejected your API key — rerun `nabla init` "
|
|
510
|
+
"and paste it again (the input shows one * per character, including pastes)")
|
|
511
|
+
return 1
|
|
474
512
|
out = args.out or f"{site.symbol}.recorded.jsonl"
|
|
475
513
|
with open(out, "w", encoding="utf-8") as f:
|
|
476
514
|
for ex in examples:
|
|
@@ -545,7 +583,7 @@ def _cmd_profile(args) -> int:
|
|
|
545
583
|
print(f"nabla profile: no recorded examples found ({conventional} or recorded.jsonl). "
|
|
546
584
|
f"Run first: {record_cmd}", file=sys.stderr)
|
|
547
585
|
return 2
|
|
548
|
-
rail(_out, f"using {examples_path}"
|
|
586
|
+
rail(_out, f"using {examples_path}")
|
|
549
587
|
|
|
550
588
|
examples, recorded_from = [], "samples"
|
|
551
589
|
for line in Path(examples_path).read_text(encoding="utf-8").splitlines():
|
|
@@ -415,9 +415,13 @@ def _run_site_in_subprocess(site: RawSite, repo_path: str, input_slots: dict, en
|
|
|
415
415
|
timeout = int(os.environ.get("NABLA_SAMPLE_TIMEOUT", "300"))
|
|
416
416
|
proc = subprocess.run(args, cwd=repo_path, env=env, capture_output=True, text=True, timeout=timeout)
|
|
417
417
|
if proc.returncode != 0:
|
|
418
|
+
# The last stderr lines carry the actual exception; the rest is
|
|
419
|
+
# traceback scaffolding that buries it.
|
|
420
|
+
lines = [l for l in (proc.stderr or proc.stdout or "").splitlines() if l.strip()]
|
|
421
|
+
detail = " | ".join(lines[-2:]) if lines else "no output"
|
|
418
422
|
raise RuntimeError(
|
|
419
423
|
f"Sample invocation of {site.symbol!r} in {site.file!r} failed "
|
|
420
|
-
f"(exit {proc.returncode}): {
|
|
424
|
+
f"(exit {proc.returncode}): {detail}"
|
|
421
425
|
)
|
|
422
426
|
|
|
423
427
|
|
|
@@ -110,8 +110,9 @@ def select(console: Console, label: str, options: list[tuple[str, str]],
|
|
|
110
110
|
row.append(f" {_POINTER} {_ON} ", style="cyan")
|
|
111
111
|
row.append(f"{name:<{name_width}}", style="bold cyan")
|
|
112
112
|
else:
|
|
113
|
+
# Unselected is quieter, never grey — grey reads as disabled.
|
|
113
114
|
row.append(f" {_OFF} ", style="dim")
|
|
114
|
-
row.append(f"{name:<{name_width}}"
|
|
115
|
+
row.append(f"{name:<{name_width}}")
|
|
115
116
|
row.append(f" {desc}", style="dim")
|
|
116
117
|
console.print(row)
|
|
117
118
|
lines_drawn = len(options) + 1
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
"""nabla terminal branding: the ∇ logo and startup banner.
|
|
2
|
-
|
|
3
|
-
The banner prints only on a real terminal — piped output, tests, and
|
|
4
|
-
--json consumers never see it. Block glyphs render via the Windows
|
|
5
|
-
console's UTF-16 writer, so no legacy-codepage fallback is needed on an
|
|
6
|
-
interactive console.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from rich.console import Console
|
|
12
|
-
from rich.text import Text
|
|
13
|
-
|
|
14
|
-
from . import __version__
|
|
15
|
-
|
|
16
|
-
# Proportioned after the wordmark: tall triangle, heavy top bar,
|
|
17
|
-
# strokes converging to a centered apex.
|
|
18
|
-
_LOGO = [
|
|
19
|
-
"▛▀▀▀▀▀▀▀▀▀▜",
|
|
20
|
-
"▚ ▞",
|
|
21
|
-
" ▚ ▞",
|
|
22
|
-
" ▚ ▞",
|
|
23
|
-
" ▚ ▞",
|
|
24
|
-
" ▚ ▞",
|
|
25
|
-
" ▀",
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
_BRAND_STYLE = "bold cyan"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def banner_lines() -> list[tuple[str, str]]:
|
|
32
|
-
"""(logo_line, tagline) pairs; taglines are empty on the outer rows."""
|
|
33
|
-
taglines = [
|
|
34
|
-
"",
|
|
35
|
-
"",
|
|
36
|
-
"nabla",
|
|
37
|
-
f"v{__version__} · OpenAI call-site harvester",
|
|
38
|
-
"scan · record · profile",
|
|
39
|
-
"",
|
|
40
|
-
"",
|
|
41
|
-
]
|
|
42
|
-
return list(zip(_LOGO, taglines))
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def print_banner(console: Console | None = None) -> None:
|
|
46
|
-
console = console or Console()
|
|
47
|
-
if not console.is_terminal:
|
|
48
|
-
return
|
|
49
|
-
console.print()
|
|
50
|
-
for logo_line, tagline in banner_lines():
|
|
51
|
-
row = Text()
|
|
52
|
-
row.append(f" {logo_line:<14}", style=_BRAND_STYLE)
|
|
53
|
-
if tagline:
|
|
54
|
-
row.append(tagline, style="bold" if tagline == "nabla" else "dim")
|
|
55
|
-
console.print(row)
|
|
56
|
-
console.print()
|
|
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
|