witbitz-code 1.3.0__py3-none-any.whl → 1.3.2__py3-none-any.whl

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.
witbitz_code/__init__.py CHANGED
@@ -4,4 +4,4 @@ The Python implementation of tools/witbitz-code.mjs (docs/opencode-relay.md phas
4
4
  vectors and cross-implementation tests.
5
5
  """
6
6
 
7
- __version__ = "1.3.0"
7
+ __version__ = "1.3.2"
witbitz_code/account.py CHANGED
@@ -73,13 +73,16 @@ async def publish_computer(*, call: ApiCall, idx: dict, computer_id: str, name:
73
73
 
74
74
 
75
75
  async def unpublish_computer(*, call: ApiCall, idx: dict, computer_id: str, now: float | None = None) -> dict:
76
- """Tombstone this computer in the account's registry (its secret leaves the account). Nothing there → nothing written."""
76
+ """Tombstone this computer in the account's registry (its secret leaves the account). Written even when this doc does not
77
+ list it: an app that seals the list keeps it in 'xcomputers', which this tool cannot read (it holds no account key), and
78
+ an older app emptied this doc — so "not listed" here says nothing about the list the apps show (tools/opencode-pair.mjs
79
+ unpublishComputer). Only a tombstone already here is left alone."""
77
80
  now = _now_ms() if now is None else now
78
81
  rd = await read_doc(call, idx, COMPUTERS_DOC)
79
82
  if not rd["ok"]:
80
83
  return rd
81
84
  cur = norm_registry(rd["state"])["computers"].get(computer_id)
82
- if not cur or cur.get("removed"):
85
+ if cur and cur.get("removed"):
83
86
  return {"ok": True, "noop": True}
84
87
  nxt = without_computer(rd["state"], computer_id, now)
85
88
  wr = await call(_write_body(idx, COMPUTERS_DOC, wrap_index_state({**nxt, "at": now}), "computer unpaired"))
witbitz_code/cli.py CHANGED
@@ -25,7 +25,7 @@ import httpx
25
25
  from . import __version__
26
26
  from . import pair as pair_cmd
27
27
  from . import pairings as pp
28
- from . import codekeys, codetools, confidential, keys, models, plugins, policy, uninstall as uninstall_mod, wizard
28
+ from . import codekeys, codetools, confidential, keys, models, plugins, policy, uninstall as uninstall_mod, winconsole, wizard
29
29
  from ._js import to_number, truthy
30
30
  from .connector import pairings_for_port, start_connector
31
31
  from .payers import SessionPayers
@@ -263,13 +263,31 @@ async def _check_keys_every(live_keys: codekeys.LiveKeys, http: httpx.AsyncClien
263
263
  async def setup(args: list[str]) -> int:
264
264
  port_n = to_number(_flag(args, "--port", "4096"))
265
265
  port = int(port_n) if truthy(port_n) and port_n == port_n and float(port_n).is_integer() else 4096
266
+ from . import net # noqa: PLC0415
267
+
268
+ client = net.new_client()
269
+
270
+ async def keys_state(p: dict) -> str:
271
+ entry = codekeys.for_computer(p.get("computerId") or "")
272
+ if not entry:
273
+ return "none"
274
+ try:
275
+ st = await codekeys.status(client, entry)
276
+ except Exception: # noqa: BLE001 — offline: unknown, never a removal
277
+ return "unknown"
278
+ return "live" if st.get("live") is True else st.get("why") if st.get("why") in ("revoked", "paused") else "unknown"
279
+
266
280
  d = wizard.Deps(
267
281
  say=_err, ask=lambda q: keys.read_line(q), secret=lambda q: keys.read_line(q, hidden=True),
268
282
  port=port, port_explicit="--port" in args,
269
283
  pair=lambda p: pair_cmd.main(["--port", str(p)]),
284
+ keys_state=keys_state,
270
285
  is_listening=is_listening, serve_here=lambda p: serve(["--port", str(p)]),
271
286
  )
272
- r = await wizard.run_setup(d)
287
+ try:
288
+ r = await wizard.run_setup(d)
289
+ finally:
290
+ await client.aclose()
273
291
  return r.get("served", 0) if not r.get("stopped") else 1
274
292
 
275
293
 
@@ -352,6 +370,7 @@ async def _main(argv: list[str]) -> int:
352
370
 
353
371
 
354
372
  def main(argv: list[str] | None = None) -> Any:
373
+ winconsole.prepare() # Windows: UTF-8 when not a console, ANSI colours for the QR when it is (winconsole.py)
355
374
  try:
356
375
  code = asyncio.run(_main(sys.argv[1:] if argv is None else argv))
357
376
  except KeyboardInterrupt:
witbitz_code/pair.py CHANGED
@@ -205,7 +205,7 @@ async def _run(args: Args, call: Any, link_fn: LinkFn | None, out: Callable, err
205
205
  ok = False
206
206
  err(f"witbitz-code: ✖ {p.get('account')}: {r['why']} — kept the local pairing so you can retry")
207
207
  continue
208
- note = " (it was not listed)" if r.get("noop") else ""
208
+ note = " (it was already removed)" if r.get("noop") else ""
209
209
  out(f"witbitz-code: ✓ removed \"{p.get('name')}\" from {p.get('account')}{note} — every device drops it on its next sync")
210
210
  pp.write_pairings(pp.remove_pairings(pp.read_pairings(), p.get("account") or "")["doc"])
211
211
  out(f"witbitz-code: restart the connector ({START_HINT}) so it stops answering on the old channel.")
witbitz_code/uninstall.py CHANGED
@@ -323,7 +323,7 @@ async def run_uninstall(d: Deps) -> dict:
323
323
  except Exception as e: # noqa: BLE001 — one account failing must not stop the rest
324
324
  r = {"ok": False, "why": str(e)}
325
325
  if r.get("ok"):
326
- d.say(f"✓ Removed \"{p.get('name')}\" from {p.get('account') or 'your account'}{' (it was not listed)' if r.get('noop') else ''}")
326
+ d.say(f"✓ Removed \"{p.get('name')}\" from {p.get('account') or 'your account'}{' (it was already removed)' if r.get('noop') else ''}")
327
327
  else:
328
328
  d.say(f"✖ Could not remove \"{p.get('name')}\" from {p.get('account') or 'your account'} ({r.get('why')})")
329
329
  left.append(p)
@@ -0,0 +1,60 @@
1
+ """Windows output, set up once before anything is printed (tests/test_winconsole.py).
2
+
3
+ - Not a console (piped, redirected, captured by another program — an installer, a service log): Windows Python writes the
4
+ locale's code page, cp1252, and the first arrow or QR block crashed witbitz-code ("'charmap' codec can't encode
5
+ character '\\u2192'", measured on Windows 11 with 1.3.0). Such a stream is switched to UTF-8.
6
+ - A console: the QR is drawn with ANSI colours (qr.render_ansi), which the console shows only once its virtual terminal
7
+ mode is on — Windows Terminal has it, an older console window prints the codes as text. It is turned on here.
8
+
9
+ Off Windows, and wherever the console cannot say what it is, nothing changes.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import sys
15
+ from typing import Any
16
+
17
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
18
+ STD_HANDLES = (-11, -12) # stdout, stderr
19
+
20
+
21
+ def _kernel32() -> Any:
22
+ import ctypes # noqa: PLC0415 — Windows only
23
+
24
+ return ctypes.windll.kernel32 # type: ignore[attr-defined]
25
+
26
+
27
+ def prepare(*, platform: str = sys.platform, streams: tuple = (), kernel32: Any = "auto") -> None:
28
+ if platform != "win32":
29
+ return
30
+ streams = streams or (sys.stdout, sys.stderr)
31
+ console = []
32
+ for i, s in enumerate(streams):
33
+ try:
34
+ tty = bool(s.isatty())
35
+ except (AttributeError, ValueError):
36
+ tty = False
37
+ if tty:
38
+ console.append(STD_HANDLES[i] if i < len(STD_HANDLES) else None)
39
+ continue
40
+ try:
41
+ s.reconfigure(encoding="utf-8", errors="replace")
42
+ except (AttributeError, ValueError):
43
+ pass
44
+ if not any(h is not None for h in console):
45
+ return
46
+ try:
47
+ import ctypes # noqa: PLC0415
48
+
49
+ k = _kernel32() if kernel32 == "auto" else kernel32
50
+ if k is None:
51
+ return
52
+ for n in console:
53
+ if n is None:
54
+ continue
55
+ handle = k.GetStdHandle(n)
56
+ mode = ctypes.c_ulong()
57
+ if k.GetConsoleMode(handle, ctypes.byref(mode)):
58
+ k.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
59
+ except Exception: # noqa: BLE001 — an old console keeps the raw code line below the QR
60
+ pass
witbitz_code/wizard.py CHANGED
@@ -69,6 +69,10 @@ class Deps:
69
69
  pair: Callable[[int], Awaitable[Any]] = None # type: ignore[assignment]
70
70
  move_pairing: Callable[[dict, int], None] = move_pairing
71
71
  code_keys: Callable[[dict], dict | None] = lambda p: codekeys.for_computer(p.get("computerId") or "")
72
+ # What the ACCOUNT says about a pairing's keys: "live" | "revoked" (the computer was removed — Remove in the app, an
73
+ # unpair) | "paused" | "unknown" (offline) | "none" (no keys here). The file on disk outlives a removal.
74
+ keys_state: Callable[[dict], Awaitable[str]] | None = None
75
+ forget_keys: Callable[[dict], Any] = lambda p: codekeys.forget(p.get("computerId") or "")
72
76
  is_listening: Callable[[int], Awaitable[bool]] = None # type: ignore[assignment]
73
77
  serve_here: Callable[[int], Awaitable[int]] = None # type: ignore[assignment]
74
78
  result: dict = field(default_factory=lambda: {"opencode": False, "paired": False, "keys": False, "running": ""})
@@ -120,6 +124,14 @@ async def run_setup(d: Deps) -> dict:
120
124
  d.move_pairing(p, port)
121
125
  everything = d.all_pairings()
122
126
  mine = on(everything, port)
127
+ # A pairing the account has let go of (the owner, 2026-10-08: reinstalled after a removal and "it does not present the
128
+ # QR code"): its keys come back revoked, so it is not a pairing any more — say so and scan again. Offline is not that.
129
+ gone = [p for p in mine if d.keys_state and d.code_keys(p) and await d.keys_state(p) == "revoked"]
130
+ for p in gone:
131
+ d.forget_keys(p)
132
+ if mine and len(gone) == len(mine):
133
+ d.say(f" {named(gone)} was removed from your Witbitz account, so this computer pairs again.")
134
+ mine = []
123
135
  if not mine:
124
136
  d.say(" On your phone, open Spaces → Settings → Back up & recovery → Add a device, and scan the code below.\n")
125
137
  await d.pair(port)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -1,12 +1,12 @@
1
- witbitz_code/__init__.py,sha256=1DVDBN_h6k6evWPdXIAumnBUic5RgCrQScZFB_02CwQ,290
1
+ witbitz_code/__init__.py,sha256=nSbrwMc3vKJUmM8eFTRh1MhmY14_4BdYS2cGhoyaWTs,290
2
2
  witbitz_code/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
3
3
  witbitz_code/_js.py,sha256=56cKC97SfM1UZAcOGzGnL-SgIB4SfdAlqVDXX3Vi-N0,11458
4
- witbitz_code/account.py,sha256=JoCbG1H-qK0hrh2PyNTFYoPpaHukuB8zfD_xaE6lsjA,6133
4
+ witbitz_code/account.py,sha256=m_tEqkN31oEaFJQ5r1fAtl6_IE1D5IApOIJreoEGvxo,6451
5
5
  witbitz_code/asks.py,sha256=PuuM_WItEim4gKyn4tY9uEo3u_ejmcmwcJAYk2cziYs,5703
6
6
  witbitz_code/attachments.py,sha256=1IxpI0YOTl-8x8AtPXwmXYMuxU5qip0ggoBo1I7rAS4,12409
7
7
  witbitz_code/auto.py,sha256=lHwCzGOgQd42WUQTCCF4a-otEXaGQ9H4QDdPOJh1wtI,27609
8
8
  witbitz_code/auto_runner.py,sha256=PU3HP6CGFoOU4RcHVWBL5BNH1dUErKCTA8x5XAVeO1w,26521
9
- witbitz_code/cli.py,sha256=u-oqfcyK_9CFublNJNAdliE5AanT3woDudlMRSVQbww,18289
9
+ witbitz_code/cli.py,sha256=6dPQ1VegsFY9RhZo6hIHt7gi9jI8UQIFhKlkDKRAJvs,19029
10
10
  witbitz_code/codekeys.py,sha256=N-U9TJkXl318yNH8qvu1JLZmFruFSqCuxvDhVjyu2ck,13842
11
11
  witbitz_code/codetools.py,sha256=MAZ3CkeHhHgaNa1Mi_nmL7eIC-uOroskdnSSVgomkCI,6758
12
12
  witbitz_code/compress.py,sha256=ghZf_HTmYyHOwJDxDgJumr-7hIeohwio_SIMrzs1jtM,759
@@ -23,7 +23,7 @@ witbitz_code/models.py,sha256=v9RdoNj0iBXI7vstQ-M6tjRakPV4_S867OPtcZTI974,4046
23
23
  witbitz_code/net.py,sha256=YnqhwZPer4RADTtNIGdp02NXzI5v45fq9XsWOr-yLns,2062
24
24
  witbitz_code/offered_models.json,sha256=YJVQQfNOrrIDU5KQwzb5jMwrgmH_MD2sH7lJaMsubtg,838
25
25
  witbitz_code/outputs.py,sha256=Sibj4Gb6yNEgBQdM7DDlOXbZS93Wo0CxHz7Llzgmyvs,5808
26
- witbitz_code/pair.py,sha256=2FPTRV-SdCk7r0V_FA8iSduR7crwkFRv7aaMZd328WQ,13401
26
+ witbitz_code/pair.py,sha256=OoXLpnVy_M0bAcAmmVmzAqcik9_rlby2HO84uFgiAxE,13406
27
27
  witbitz_code/pairings.py,sha256=tb9A3jfa4AHmJUsuApJGUPNlI4T8oX7oQry17BP0xcQ,9808
28
28
  witbitz_code/payers.py,sha256=0bxyAY6kjWBWnUEcZT0b-14oDTrAfYnJoTDJjfamK5c,4948
29
29
  witbitz_code/plugins.py,sha256=c4soyUM-VSwcLSmvey2aEYNfuu8J6jrsZtAAv4YzS6I,6679
@@ -34,15 +34,16 @@ witbitz_code/relay.py,sha256=9pHVrqQj0c4kGGPmckHZLsIchdLtexC5zGUjm3IwWWg,34626
34
34
  witbitz_code/seen.py,sha256=ltMM6RgOu9b2mdpGOgTKWGWME_gLHUs1UxJiY7n_AyI,2182
35
35
  witbitz_code/tinfoil_read.py,sha256=xfgKhdYMGt5GW2GFqjR2ffnMXN57JKz9Nm5wGP2qDbA,6519
36
36
  witbitz_code/tools_probe.py,sha256=fELe69rzJwpB0omek9twRHN3OOolky3wrKVXFk28VFg,3404
37
- witbitz_code/uninstall.py,sha256=0rwH57Z2s0iymvZU2gVPTFQE9kBsldJuRFln9hNQRtQ,22240
38
- witbitz_code/wizard.py,sha256=3fKRHxQ3MnQq0WreMaHSX4I5NDfHGOsfxIRBj4VFSmg,8296
37
+ witbitz_code/uninstall.py,sha256=sv8bOfnnsiaDafeVKYZh9J5Rhcobr_Ut5LBg5ClunoQ,22245
38
+ witbitz_code/winconsole.py,sha256=Yr5ikHj7OS4O9W_PMFzSMdC7lXXmb5Yr532oeL1wkdI,2278
39
+ witbitz_code/wizard.py,sha256=EV4CnGLMJHnMEBQwRUjzIWdWc0iaITA8MWmTA017MC0,9242
39
40
  witbitz_code/opencode_plugins/notes-init.md,sha256=7SkhvxSFhKY0W1EZv4DIk34RzbpxoQbqub3f7DJs9c0,768
40
41
  witbitz_code/opencode_plugins/witbitz-media-server.mjs,sha256=M-wEUBlDASM89txnCyYLMUYci6Ss5du14fmEx27TZvo,75529
41
42
  witbitz_code/opencode_plugins/witbitz-notes.js,sha256=M8V0WZiJdPE78hcjHT3oo1aybbD513rMWlxbcDBOdzg,26661
42
43
  witbitz_code/opencode_plugins/witbitz-progress.js,sha256=6J9B71eZqhXpWoCLm-hSBSrlTfflCmHnWHp9yhEItbQ,10349
43
- witbitz_code-1.3.0.dist-info/licenses/LICENSE,sha256=tAdpEnopN3C7_Mtwt4FpOt54tdJcytanMIPmfjWQyX8,1064
44
- witbitz_code-1.3.0.dist-info/METADATA,sha256=S0HnydkjR8T8m1kuPk-l3uEbZNFdlGC64l1Ttp01CmM,11710
45
- witbitz_code-1.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
46
- witbitz_code-1.3.0.dist-info/entry_points.txt,sha256=rjZTJptXZoB2fSJowQBgbsNiiTfsmJGXEwJNIYJHbu4,55
47
- witbitz_code-1.3.0.dist-info/top_level.txt,sha256=_Q9iBQXeIhv110W9Do9iSt_V4RRTbWIFBIjEhN_hq94,13
48
- witbitz_code-1.3.0.dist-info/RECORD,,
44
+ witbitz_code-1.3.2.dist-info/licenses/LICENSE,sha256=tAdpEnopN3C7_Mtwt4FpOt54tdJcytanMIPmfjWQyX8,1064
45
+ witbitz_code-1.3.2.dist-info/METADATA,sha256=9GNh26oNkaM-5Hi9nWSljHcNaaM_JW3ihIqVMOUoOxA,11710
46
+ witbitz_code-1.3.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
47
+ witbitz_code-1.3.2.dist-info/entry_points.txt,sha256=rjZTJptXZoB2fSJowQBgbsNiiTfsmJGXEwJNIYJHbu4,55
48
+ witbitz_code-1.3.2.dist-info/top_level.txt,sha256=_Q9iBQXeIhv110W9Do9iSt_V4RRTbWIFBIjEhN_hq94,13
49
+ witbitz_code-1.3.2.dist-info/RECORD,,