lingling 2.0.0__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.
lingling-2.0.0/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Lingling License
2
+
3
+ Copyright (c) 2026 huranth
4
+
5
+ You are free to use and run this software, in whole or in part, for any
6
+ purpose, at no cost.
7
+
8
+ You may NOT:
9
+ - sell this software, or offer it (modified or not) as a paid product or
10
+ paid service;
11
+ - distribute modified versions of this software, or publish derivative
12
+ works based on it.
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies of the software.
16
+
17
+ All rights not expressly granted above are reserved by the copyright holder.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
23
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: lingling
3
+ Version: 2.0.0
4
+ Summary: Official OpenCode, but your requests ride rotating Tor exit lanes.
5
+ Author: huranth
6
+ License: LicenseRef-Lingling
7
+ Project-URL: Homepage, https://github.com/huranth/lingling
8
+ Project-URL: Repository, https://github.com/huranth/lingling
9
+ Project-URL: Issues, https://github.com/huranth/lingling/issues
10
+ Keywords: opencode,tor,proxy,llm,rate-limit
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Internet :: Proxy Servers
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: stem==1.8.2
23
+ Requires-Dist: cryptography>=41
24
+ Dynamic: license-file
25
+
26
+ # Lingling
27
+
28
+ ### What even is this?
29
+
30
+ OpenCode gives away genuinely good models for free. The catch: they throttle
31
+ you by IP, so right when the conversation gets good — bam, wall. Very rude.
32
+
33
+ Lingling fixes this the fun way. It spins up a little kitchen of Tor exit
34
+ lanes on your machine, each one a different country, and puts your requests
35
+ on whichever lane is hot. One exit gets throttled? Cute. Your traffic is
36
+ already on another one. You never see the limit. You just keep typing.
37
+
38
+ ```
39
+ pip install lingling
40
+ lingling # = opencode, but the requests ride Tor
41
+ lingling --help # anything after `lingling` goes to opencode untouched
42
+ ```
43
+
44
+ First run downloads the Tor Expert Bundle (~30 MB) into Lingling's per-user
45
+ data directory (`%LOCALAPPDATA%\lingling` on Windows, `~/.local/share/lingling`
46
+ on Linux, `~/Library/Application Support/lingling` on macOS; override with
47
+ `LINGLING_DATA_DIR`). After that, launching looks like this — a few seconds
48
+ of kitchen noises while the first lane cooks, then opencode opens:
49
+
50
+ ```
51
+ ⠋ glazing the tunnel
52
+ served! lanes are hot -- proof is in the other window.
53
+ ```
54
+
55
+ ### "Sure, but how do I know it's not lying?"
56
+
57
+ Fair. That's what the proof window is for. A second console pops up next to
58
+ your editor and shows every single model call, live: which lane it rode,
59
+ which country, which exit IP, what came back, how big, how fast.
60
+
61
+ ```
62
+ 13:52:55 #3.1 lane 2 {de} 185.220.100.243 -> muse-spark-1.2-contributor-free
63
+ 13:53:00 | #3.1 200 81.4 KB in 2.7s
64
+ 13:53:01 #4.1 lane 3 {nl} 192.42.116.113 -> muse-spark-1.2-contributor-free
65
+ 13:53:03 | #4.1 200 79.6 KB in 2.6s
66
+ 13:53:07 * lane 4 hit a hidden limit -- your traffic moved to a fresh lane
67
+ ```
68
+
69
+ No gaslighting. If the pane says bytes are flowing through a German exit,
70
+ your tokens are flowing through a German exit. `--no-proof` if you trust us.
71
+ (You shouldn't trust anyone. That's the point of the window.)
72
+
73
+ ### How a limit dies without you noticing
74
+
75
+ A Tor tunnel is end-to-end TLS, so a 429 can't be spotted in flight — instead,
76
+ every 45 seconds each lane fires a real probe at the upstream and rats itself
77
+ out. The moment a lane comes back throttled:
78
+
79
+ 1. it's yanked from rotation **before** your next request would have touched
80
+ it — your traffic just... continues, elsewhere;
81
+ 2. the throttled lane is taken out back and re-cooked from scratch: fresh
82
+ tor process, fresh keys, fresh guards, fresh exit IP. We don't negotiate
83
+ with rate limits;
84
+ 3. if the same lane keeps getting burned, it gets a new country too
85
+ (`us → de → nl → fr → ro → …`). A new personality, basically.
86
+
87
+ Lanes that just fall over get poked, restarted, re-cooked, and — if they're
88
+ truly hopeless — benched with a note in the proof window. You'll see the whole
89
+ drama live, which is honestly half the fun.
90
+
91
+ ### "Wait, how can it see each request? Isn't TLS encrypted?"
92
+
93
+ Very observant. opencode holds one TLS connection open for your whole
94
+ session, so a normal proxy sees one opaque pipe, not individual calls. So
95
+ Lingling does a magic trick: it mints a throwaway certificate authority **on
96
+ your own machine**, hands it to opencode (and only opencode), and unwraps
97
+ `opencode.ai` traffic just long enough to read the model name off each
98
+ request before re-encrypting it through a lane to the real server.
99
+
100
+ Everything stays end-to-end TLS over Tor. The only new thing that can see
101
+ your traffic is... your own computer, which could already see your traffic.
102
+ `LINGLING_NO_MITM=1` turns the trick off if it makes you itchy (you'll get
103
+ per-tunnel proof instead of per-request).
104
+
105
+ ### Fresh every time
106
+
107
+ Every launch wipes lane keys, guards, and circuit state — yesterday's exits
108
+ are dead to us. The only thing kept is tor's cached copy of the public relay
109
+ directory, because re-downloading that every launch is how you turn a
110
+ five-second boot into a two-minute boot. Orphaned `tor.exe` processes from a
111
+ crashed run are swept up before new lanes take their ports.
112
+
113
+ ### Flags Lingling keeps for itself
114
+
115
+ | Flag | What it does |
116
+ |---|---|
117
+ | `--lanes N` | how many lanes to keep warm (default 5, env `LINGLING_TOR_COUNT`) |
118
+ | `--no-tor` | skip the lanes, run opencode on your own IP like a civilian |
119
+ | `--no-proof` | no proof window (coward's mode) |
120
+ | `--demo [question]` | fire one real Muse Spark request through a lane, show receipts |
121
+
122
+ Everything else is handed to opencode byte-for-byte. We don't touch it,
123
+ we don't parse it, we don't want to know.
124
+
125
+ ### Under the hood
126
+
127
+ ```
128
+ lingling/cli.py entry point, loader, opencode launch
129
+ lingling/lanes.py Tor lane lifecycle: boot, regenerate, country rotation
130
+ lingling/relay.py local CONNECT relay, least-busy lane picking, MITM handoff
131
+ lingling/mitm.py local CA + per-request interception for opencode.ai
132
+ lingling/health.py probe daemon: burns, deaths, dramatic recoveries
133
+ lingling/proof.py the proof window (event log + live tail)
134
+ lingling/netutil.py raw SOCKS5 / HTTPS-over-SOCKS primitives
135
+ lingling/demo.py lingling --demo
136
+ lingling/winjob.py Windows Job Object so tor.exe dies with us
137
+ ```
138
+
139
+ Runtime state (tor, lanes, proof log, local CA) lives in the per-user data
140
+ directory — never in the package, never in your project folder.
141
+
142
+ Requirements: Python 3.11+, Windows/macOS/Linux, and `opencode` on your PATH.
143
+ Runs entirely on your machine. Nothing calls home. The kitchen is local.
@@ -0,0 +1,118 @@
1
+ # Lingling
2
+
3
+ ### What even is this?
4
+
5
+ OpenCode gives away genuinely good models for free. The catch: they throttle
6
+ you by IP, so right when the conversation gets good — bam, wall. Very rude.
7
+
8
+ Lingling fixes this the fun way. It spins up a little kitchen of Tor exit
9
+ lanes on your machine, each one a different country, and puts your requests
10
+ on whichever lane is hot. One exit gets throttled? Cute. Your traffic is
11
+ already on another one. You never see the limit. You just keep typing.
12
+
13
+ ```
14
+ pip install lingling
15
+ lingling # = opencode, but the requests ride Tor
16
+ lingling --help # anything after `lingling` goes to opencode untouched
17
+ ```
18
+
19
+ First run downloads the Tor Expert Bundle (~30 MB) into Lingling's per-user
20
+ data directory (`%LOCALAPPDATA%\lingling` on Windows, `~/.local/share/lingling`
21
+ on Linux, `~/Library/Application Support/lingling` on macOS; override with
22
+ `LINGLING_DATA_DIR`). After that, launching looks like this — a few seconds
23
+ of kitchen noises while the first lane cooks, then opencode opens:
24
+
25
+ ```
26
+ ⠋ glazing the tunnel
27
+ served! lanes are hot -- proof is in the other window.
28
+ ```
29
+
30
+ ### "Sure, but how do I know it's not lying?"
31
+
32
+ Fair. That's what the proof window is for. A second console pops up next to
33
+ your editor and shows every single model call, live: which lane it rode,
34
+ which country, which exit IP, what came back, how big, how fast.
35
+
36
+ ```
37
+ 13:52:55 #3.1 lane 2 {de} 185.220.100.243 -> muse-spark-1.2-contributor-free
38
+ 13:53:00 | #3.1 200 81.4 KB in 2.7s
39
+ 13:53:01 #4.1 lane 3 {nl} 192.42.116.113 -> muse-spark-1.2-contributor-free
40
+ 13:53:03 | #4.1 200 79.6 KB in 2.6s
41
+ 13:53:07 * lane 4 hit a hidden limit -- your traffic moved to a fresh lane
42
+ ```
43
+
44
+ No gaslighting. If the pane says bytes are flowing through a German exit,
45
+ your tokens are flowing through a German exit. `--no-proof` if you trust us.
46
+ (You shouldn't trust anyone. That's the point of the window.)
47
+
48
+ ### How a limit dies without you noticing
49
+
50
+ A Tor tunnel is end-to-end TLS, so a 429 can't be spotted in flight — instead,
51
+ every 45 seconds each lane fires a real probe at the upstream and rats itself
52
+ out. The moment a lane comes back throttled:
53
+
54
+ 1. it's yanked from rotation **before** your next request would have touched
55
+ it — your traffic just... continues, elsewhere;
56
+ 2. the throttled lane is taken out back and re-cooked from scratch: fresh
57
+ tor process, fresh keys, fresh guards, fresh exit IP. We don't negotiate
58
+ with rate limits;
59
+ 3. if the same lane keeps getting burned, it gets a new country too
60
+ (`us → de → nl → fr → ro → …`). A new personality, basically.
61
+
62
+ Lanes that just fall over get poked, restarted, re-cooked, and — if they're
63
+ truly hopeless — benched with a note in the proof window. You'll see the whole
64
+ drama live, which is honestly half the fun.
65
+
66
+ ### "Wait, how can it see each request? Isn't TLS encrypted?"
67
+
68
+ Very observant. opencode holds one TLS connection open for your whole
69
+ session, so a normal proxy sees one opaque pipe, not individual calls. So
70
+ Lingling does a magic trick: it mints a throwaway certificate authority **on
71
+ your own machine**, hands it to opencode (and only opencode), and unwraps
72
+ `opencode.ai` traffic just long enough to read the model name off each
73
+ request before re-encrypting it through a lane to the real server.
74
+
75
+ Everything stays end-to-end TLS over Tor. The only new thing that can see
76
+ your traffic is... your own computer, which could already see your traffic.
77
+ `LINGLING_NO_MITM=1` turns the trick off if it makes you itchy (you'll get
78
+ per-tunnel proof instead of per-request).
79
+
80
+ ### Fresh every time
81
+
82
+ Every launch wipes lane keys, guards, and circuit state — yesterday's exits
83
+ are dead to us. The only thing kept is tor's cached copy of the public relay
84
+ directory, because re-downloading that every launch is how you turn a
85
+ five-second boot into a two-minute boot. Orphaned `tor.exe` processes from a
86
+ crashed run are swept up before new lanes take their ports.
87
+
88
+ ### Flags Lingling keeps for itself
89
+
90
+ | Flag | What it does |
91
+ |---|---|
92
+ | `--lanes N` | how many lanes to keep warm (default 5, env `LINGLING_TOR_COUNT`) |
93
+ | `--no-tor` | skip the lanes, run opencode on your own IP like a civilian |
94
+ | `--no-proof` | no proof window (coward's mode) |
95
+ | `--demo [question]` | fire one real Muse Spark request through a lane, show receipts |
96
+
97
+ Everything else is handed to opencode byte-for-byte. We don't touch it,
98
+ we don't parse it, we don't want to know.
99
+
100
+ ### Under the hood
101
+
102
+ ```
103
+ lingling/cli.py entry point, loader, opencode launch
104
+ lingling/lanes.py Tor lane lifecycle: boot, regenerate, country rotation
105
+ lingling/relay.py local CONNECT relay, least-busy lane picking, MITM handoff
106
+ lingling/mitm.py local CA + per-request interception for opencode.ai
107
+ lingling/health.py probe daemon: burns, deaths, dramatic recoveries
108
+ lingling/proof.py the proof window (event log + live tail)
109
+ lingling/netutil.py raw SOCKS5 / HTTPS-over-SOCKS primitives
110
+ lingling/demo.py lingling --demo
111
+ lingling/winjob.py Windows Job Object so tor.exe dies with us
112
+ ```
113
+
114
+ Runtime state (tor, lanes, proof log, local CA) lives in the per-user data
115
+ directory — never in the package, never in your project folder.
116
+
117
+ Requirements: Python 3.11+, Windows/macOS/Linux, and `opencode` on your PATH.
118
+ Runs entirely on your machine. Nothing calls home. The kitchen is local.
@@ -0,0 +1,21 @@
1
+ """Lingling -- official OpenCode, but your requests ride rotating Tor lanes."""
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ __version__ = "2.0.0"
8
+
9
+
10
+ def data_dir() -> Path:
11
+ """Per-user runtime state (tor, lanes, proof log). Lives outside the
12
+ package -- a pip install must never write into site-packages."""
13
+ override = os.environ.get("LINGLING_DATA_DIR")
14
+ if override:
15
+ return Path(override)
16
+ if sys.platform == "win32":
17
+ base = os.environ.get("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
18
+ return Path(base) / "lingling"
19
+ if sys.platform == "darwin":
20
+ return Path.home() / "Library" / "Application Support" / "lingling"
21
+ return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "lingling"
@@ -0,0 +1,4 @@
1
+ from .cli import entry
2
+
3
+ if __name__ == "__main__":
4
+ entry()
@@ -0,0 +1,277 @@
1
+ """lingling -- official OpenCode, riding rotating Tor lanes.
2
+
3
+ CLI entrypoint: boots Tor lanes, starts the local relay, then execs opencode
4
+ with HTTPS_PROXY pointed at it; all other args pass through untouched.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import itertools
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ import threading
15
+ import time
16
+ from pathlib import Path
17
+
18
+ from . import __version__, data_dir, proof
19
+ from .health import HealthDaemon
20
+ from .lanes import TorManager
21
+ from .relay import Relay
22
+
23
+ DATA_DIR = data_dir()
24
+ PROOF_LOG = DATA_DIR / "proof.log"
25
+
26
+ DEFAULT_COUNTRIES = ["us", "de", "nl", "fr", "ro", "gb", "ca", "se", "pl", "ch"]
27
+
28
+ _KITCHEN_LINES = [
29
+ "cooking the lanes", "baking it", "warming the exits",
30
+ "glazing the tunnel", "seasoning the circuits", "proofing the dough",
31
+ "preheating the relays", "tasting the traffic",
32
+ ]
33
+
34
+ _SPIN = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
35
+
36
+ _PHRASE_COLORS = ["38;5;215", "38;5;222", "38;5;180", "38;5;173",
37
+ "38;5;114", "38;5;109", "38;5;139", "38;5;175"]
38
+
39
+
40
+ def _c(text: str, code: str) -> str:
41
+ if os.environ.get("NO_COLOR") or not sys.stdout.isatty():
42
+ return text
43
+ return f"\x1b[{code}m{text}\x1b[0m"
44
+
45
+
46
+ class _Loader:
47
+ """Self-rewriting status line; deliberately vibe-only, no lane counts or progress bar."""
48
+
49
+ def __init__(self) -> None:
50
+ self._stop = threading.Event()
51
+ self._thread: threading.Thread | None = None
52
+
53
+ def set(self, detail: str = "") -> None:
54
+ # Kept for API compatibility; details are intentionally not shown.
55
+ pass
56
+
57
+ def start(self) -> None:
58
+ if not sys.stdout.isatty():
59
+ return
60
+ self._thread = threading.Thread(target=self._run, daemon=True)
61
+ self._thread.start()
62
+
63
+ def _run(self) -> None:
64
+ tick = itertools.count()
65
+ while not self._stop.is_set():
66
+ t = next(tick)
67
+ spin = _c(_SPIN[t % len(_SPIN)], "1;38;5;220")
68
+ idx = (t // 24) % len(_KITCHEN_LINES)
69
+ msg = _c(_KITCHEN_LINES[idx], _PHRASE_COLORS[idx])
70
+ sys.stdout.write(f"\r\x1b[K {spin} {msg}")
71
+ sys.stdout.flush()
72
+ time.sleep(0.09)
73
+
74
+ def stop(self, final: str = "") -> None:
75
+ self._stop.set()
76
+ if self._thread:
77
+ self._thread.join(timeout=2)
78
+ self._thread = None
79
+ if sys.stdout.isatty():
80
+ sys.stdout.write("\r\x1b[K")
81
+ if final:
82
+ sys.stdout.write(final + "\n")
83
+ sys.stdout.flush()
84
+
85
+
86
+ def _parse_args(argv: list[str]) -> dict:
87
+ """Split lingling's own flags from args passed through to opencode."""
88
+ opts = {
89
+ "lanes": int(os.environ.get("LINGLING_TOR_COUNT", "5") or 5),
90
+ "no_tor": False,
91
+ "no_proof": False,
92
+ "proof_tail": None,
93
+ "demo": False,
94
+ "passthrough": [],
95
+ }
96
+ skip = False
97
+ for i, a in enumerate(argv):
98
+ if skip:
99
+ skip = False
100
+ continue
101
+ if a == "--proof":
102
+ opts["proof_tail"] = argv[i + 1] if i + 1 < len(argv) else ""
103
+ skip = True
104
+ elif a == "--demo":
105
+ opts["demo"] = True
106
+ elif a == "--lanes":
107
+ try:
108
+ opts["lanes"] = max(1, int(argv[i + 1]))
109
+ opts["lanes_explicit"] = True
110
+ except (IndexError, ValueError):
111
+ pass
112
+ skip = True
113
+ elif a == "--no-tor":
114
+ opts["no_tor"] = True
115
+ elif a == "--no-proof":
116
+ opts["no_proof"] = True
117
+ else:
118
+ opts["passthrough"].append(a)
119
+ return opts
120
+
121
+
122
+ def main(argv: list[str]) -> int:
123
+ opts = _parse_args(argv)
124
+
125
+ if opts["proof_tail"] is not None:
126
+ return proof.tail(Path(opts["proof_tail"] or str(PROOF_LOG)))
127
+
128
+ if opts["demo"]:
129
+ from .demo import run_demo
130
+ question = " ".join(opts["passthrough"]).strip() or (
131
+ "In one sentence: who are you, and what exit node do you think "
132
+ "this request came from?")
133
+ lanes = opts["lanes"] if opts.get("lanes_explicit") else 2
134
+ return run_demo(question, lanes=lanes)
135
+
136
+ if "--version" in opts["passthrough"] or "-v" in opts["passthrough"]:
137
+ print(f"lingling {__version__} (wraps opencode)")
138
+ return 0
139
+
140
+ opencode = shutil.which("opencode")
141
+ if opencode is None:
142
+ print("lingling: couldn't find `opencode` on your PATH.")
143
+ print("Install it first (https://opencode.ai) and re-run.")
144
+ return 1
145
+
146
+ loader = _Loader()
147
+ loader.start()
148
+ manager: TorManager | None = None
149
+ daemon: HealthDaemon | None = None
150
+ relay: Relay | None = None
151
+ direct = opts["no_tor"]
152
+
153
+ try:
154
+ if not direct:
155
+ manager = TorManager(
156
+ DATA_DIR, count=opts["lanes"],
157
+ exit_countries=DEFAULT_COUNTRIES,
158
+ tor_exe=os.environ.get("LINGLING_TOR_EXE", ""),
159
+ log=lambda *a: None,
160
+ )
161
+ loader.set()
162
+ err = manager.setup_lanes()
163
+ if err:
164
+ loader.stop(_c(f" !! tor unavailable ({err}) -- going direct",
165
+ "33"))
166
+ direct = True
167
+ else:
168
+ emit = proof.make_emitter(PROOF_LOG)
169
+ daemon = HealthDaemon(manager, event=emit,
170
+ log=lambda *a: None)
171
+
172
+ # Boot order: only lane 1 cooks in the foreground; the rest follow in background.
173
+ first = manager.lanes[0]
174
+ manager.start_lanes([first])
175
+
176
+ # Block until lane 1 provably carries traffic, else the user's first prompt dies.
177
+ deadline = time.time() + 150
178
+ while time.time() < deadline:
179
+ verdict = daemon.probe_lane(first)
180
+ if verdict == "healthy":
181
+ first.healthy = True
182
+ first.unhealthy_cycles = 0
183
+ break
184
+ if verdict == "burned":
185
+ first.burned_cycles += 1
186
+ daemon._heal_burn(first)
187
+ time.sleep(2)
188
+ if first.healthy is not True:
189
+ loader.stop(_c(" !! the kitchen stayed cold -- "
190
+ "going direct", "31"))
191
+ direct = True
192
+ manager.stop_all()
193
+ daemon.start()
194
+
195
+ if direct:
196
+ loader.stop()
197
+ print(_c("lingling: no lanes -- opencode rides your own IP.\n",
198
+ "33"))
199
+ return _run_opencode(opencode, opts["passthrough"], None)
200
+
201
+ emit = proof.make_emitter(PROOF_LOG)
202
+ relay = Relay(manager, event=emit)
203
+ port = relay.start()
204
+
205
+ # Per-request proof via local TLS termination; best-effort, falls back to blind tunnels.
206
+ ca_pem = None
207
+ if os.environ.get("LINGLING_NO_MITM", "").lower() not in ("1", "true"):
208
+ try:
209
+ from . import mitm
210
+ relay.cert_shop = mitm.CertShop(DATA_DIR / "mitm")
211
+ ca_pem = relay.cert_shop.ca_pem_path
212
+ except Exception: # noqa: BLE001
213
+ pass
214
+
215
+ loader.stop(_c(" served! lanes are hot -- proof is in the other "
216
+ "window.", "1;32"))
217
+ if not opts["no_proof"]:
218
+ proof.spawn_proof_window(PROOF_LOG)
219
+
220
+ # Remaining lanes bootstrap in background; health probes join them to rotation as they come up.
221
+ rest = manager.lanes[1:]
222
+ if rest:
223
+ def _cook_rest() -> None:
224
+ for lane in rest:
225
+ emit({"type": "lane", "kind": "heal", "t": time.time(),
226
+ "lane": lane.index, "cc": lane.exit_country,
227
+ "ip": "",
228
+ "msg": f"lane {lane.index} {{{lane.exit_country}}} "
229
+ f"registering in the background ..."})
230
+ manager.start_lanes(rest)
231
+
232
+ threading.Thread(target=_cook_rest, name="lane-cook",
233
+ daemon=True).start()
234
+
235
+ env = dict(os.environ)
236
+ for var in ("HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"):
237
+ env[var] = f"http://127.0.0.1:{port}"
238
+ env["NO_PROXY"] = env["no_proxy"] = "localhost,127.0.0.1"
239
+ if ca_pem:
240
+ # Let opencode trust our local CA so we can log each model call.
241
+ env["NODE_EXTRA_CA_CERTS"] = str(ca_pem)
242
+ return _run_opencode(opencode, opts["passthrough"], env)
243
+ finally:
244
+ if daemon:
245
+ daemon.stop()
246
+ if relay:
247
+ relay.stop()
248
+ if manager and not direct:
249
+ manager.stop_all()
250
+ if not direct:
251
+ try:
252
+ proof.make_emitter(PROOF_LOG)(proof.DONE)
253
+ except Exception: # noqa: BLE001
254
+ pass
255
+
256
+
257
+ def _run_opencode(binary: str, args: list[str],
258
+ env: dict | None) -> int:
259
+ """Exec opencode with stdio inherited so it owns the terminal."""
260
+ try:
261
+ proc = subprocess.Popen([binary, *args], env=env)
262
+ except OSError as exc:
263
+ print(f"lingling: couldn't launch opencode: {exc}")
264
+ return 1
265
+ try:
266
+ return proc.wait()
267
+ except KeyboardInterrupt:
268
+ try:
269
+ proc.terminate()
270
+ proc.wait(timeout=5)
271
+ except Exception: # noqa: BLE001
272
+ pass
273
+ return 130
274
+
275
+
276
+ def entry() -> None:
277
+ sys.exit(main(sys.argv[1:]))