ccx-native 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +241 -2
- package/bin/ccx +56 -0
- package/ccx/__init__.py +163 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +762 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +250 -0
- package/ccx/claude/host/session.py +5184 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2281 -0
- package/ccx/claude/usage.py +837 -0
- package/ccx/claude/usage_control.py +152 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/read.py +1020 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +301 -0
- package/ccx/cli/parser.py +1324 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1900 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +732 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1556 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2396 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1412 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +289 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1540 -0
- package/ccx/ops/conversation_paging.py +1338 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +579 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +770 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +306 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +636 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +254 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +804 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +705 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2081 -0
- package/ccx.py +9 -0
- package/package.json +40 -4
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +155 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
|
File without changes
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Local Codex TUI PTY with the existing CCX terminal status painter.
|
|
2
|
+
|
|
3
|
+
Codex owns its native input, including /quit, Ctrl-C and Ctrl-D. CCX
|
|
4
|
+
filters only the shared Claude detach keys before forwarding to the local
|
|
5
|
+
Codex TUI client. Codexd retains the native thread independently.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import fcntl
|
|
10
|
+
import os
|
|
11
|
+
import pty
|
|
12
|
+
import select
|
|
13
|
+
import signal
|
|
14
|
+
import struct
|
|
15
|
+
import termios
|
|
16
|
+
import time
|
|
17
|
+
import tty
|
|
18
|
+
|
|
19
|
+
from ccx.claude.attach import DetachFilter, MAX_PENDING_OUTPUT, StatusBar, _tty_size, _write_tty
|
|
20
|
+
from ccx.codex import runtime as codex_runtime
|
|
21
|
+
|
|
22
|
+
_MAX_INPUT = 64 * 1024
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _codex_status(worker_id: str) -> dict | None:
|
|
26
|
+
"""One cheap read of the *existing* worker; never start a daemon or turn."""
|
|
27
|
+
observation = codex_runtime.observe_worker(worker_id, timeout=0.8)
|
|
28
|
+
reply = observation.get("reply") or {}
|
|
29
|
+
if observation.get("presence") != "served" or reply.get("ok") is not True:
|
|
30
|
+
return None
|
|
31
|
+
state = reply.get("state")
|
|
32
|
+
if state == "waiting" and reply.get("pendingApprovals"):
|
|
33
|
+
state = "waiting for approval"
|
|
34
|
+
elif state == "busy":
|
|
35
|
+
# Compact owns the busy state too. Only inspect its existing marker,
|
|
36
|
+
# never call the native provider or acquire an operation lock.
|
|
37
|
+
compact = codex_runtime.daemon_rpc(
|
|
38
|
+
{"op": "compact-observe", "short": worker_id}, timeout=0.8,
|
|
39
|
+
)
|
|
40
|
+
if compact.get("ok") is True and isinstance(compact.get("activeCompact"), dict):
|
|
41
|
+
state = "compacting"
|
|
42
|
+
return {"ok": True, "state": state}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _spawn_tui(argv: list[str], cwd: str, rows: int, cols: int) -> tuple[int, int]:
|
|
46
|
+
"""Start only our own controlling PTY child, with its size set before exec."""
|
|
47
|
+
gate_r, gate_w = os.pipe()
|
|
48
|
+
try:
|
|
49
|
+
pid, master = pty.fork()
|
|
50
|
+
except BaseException:
|
|
51
|
+
os.close(gate_r)
|
|
52
|
+
os.close(gate_w)
|
|
53
|
+
raise
|
|
54
|
+
if pid == 0:
|
|
55
|
+
os.close(gate_w)
|
|
56
|
+
try:
|
|
57
|
+
if os.read(gate_r, 1) != b"1":
|
|
58
|
+
os._exit(125)
|
|
59
|
+
os.close(gate_r)
|
|
60
|
+
os.chdir(cwd)
|
|
61
|
+
os.execvp(argv[0], argv)
|
|
62
|
+
except BaseException as error:
|
|
63
|
+
os.write(2, f"ccx attach: cannot start Codex: {error}\n".encode()[:512])
|
|
64
|
+
os._exit(127)
|
|
65
|
+
os.close(gate_r)
|
|
66
|
+
try:
|
|
67
|
+
fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
|
|
68
|
+
os.write(gate_w, b"1")
|
|
69
|
+
return pid, master
|
|
70
|
+
except BaseException:
|
|
71
|
+
try:
|
|
72
|
+
os.killpg(pid, signal.SIGKILL)
|
|
73
|
+
except ProcessLookupError:
|
|
74
|
+
pass
|
|
75
|
+
os.waitpid(pid, 0)
|
|
76
|
+
os.close(master)
|
|
77
|
+
raise
|
|
78
|
+
finally:
|
|
79
|
+
os.close(gate_w)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _reap_owned_tui(pid: int) -> None:
|
|
83
|
+
"""Detach only the local TUI group, never the codexd-owned worker.
|
|
84
|
+
|
|
85
|
+
SIGHUP lets the Codex remote client exit cleanly; escalation is bounded
|
|
86
|
+
even when the client or its launcher ignores hangup/termination.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
if os.getpgid(pid) != pid:
|
|
90
|
+
return # do not signal a group that cannot be proven ours
|
|
91
|
+
except ProcessLookupError:
|
|
92
|
+
return
|
|
93
|
+
for sig in (signal.SIGHUP, signal.SIGTERM):
|
|
94
|
+
try:
|
|
95
|
+
os.killpg(pid, sig)
|
|
96
|
+
except ProcessLookupError:
|
|
97
|
+
pass
|
|
98
|
+
deadline = time.monotonic() + .6
|
|
99
|
+
while time.monotonic() < deadline:
|
|
100
|
+
try:
|
|
101
|
+
exited, _ = os.waitpid(pid, os.WNOHANG)
|
|
102
|
+
if exited:
|
|
103
|
+
return
|
|
104
|
+
except ChildProcessError:
|
|
105
|
+
return
|
|
106
|
+
time.sleep(.03)
|
|
107
|
+
try:
|
|
108
|
+
os.killpg(pid, signal.SIGKILL)
|
|
109
|
+
except ProcessLookupError:
|
|
110
|
+
pass
|
|
111
|
+
try:
|
|
112
|
+
os.waitpid(pid, 0)
|
|
113
|
+
except ChildProcessError:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def run_status_tui(
|
|
118
|
+
argv: list[str], *, cwd: str, worker_id: str, input_fd: int = 0,
|
|
119
|
+
output_fd: int = 1, status_fetch=None, status=True,
|
|
120
|
+
) -> int:
|
|
121
|
+
"""Proxy Codex through a PTY, with optional CCX status bar and detach keys."""
|
|
122
|
+
bar = (StatusBar(
|
|
123
|
+
worker_id[:8],
|
|
124
|
+
fetch=status_fetch if status_fetch is not None
|
|
125
|
+
else lambda: _codex_status(worker_id),
|
|
126
|
+
) if status else None)
|
|
127
|
+
old_attrs = termios.tcgetattr(input_fd)
|
|
128
|
+
old_flags = fcntl.fcntl(output_fd, fcntl.F_GETFL)
|
|
129
|
+
rows, cols = _tty_size(output_fd)
|
|
130
|
+
pid, master = _spawn_tui(argv, cwd, bar.resize(rows, cols) if bar else rows, cols)
|
|
131
|
+
os.set_blocking(master, False)
|
|
132
|
+
handlers = {}
|
|
133
|
+
resized = [False]
|
|
134
|
+
stopped = [None]
|
|
135
|
+
child_exit = None
|
|
136
|
+
child_exited_at = None
|
|
137
|
+
|
|
138
|
+
def on_resize(_sig, _frame):
|
|
139
|
+
resized[0] = True
|
|
140
|
+
|
|
141
|
+
def on_stop(sig, _frame):
|
|
142
|
+
stopped[0] = sig
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
for sig, fn in ((signal.SIGWINCH, on_resize),
|
|
146
|
+
(signal.SIGTERM, on_stop), (signal.SIGHUP, on_stop)):
|
|
147
|
+
handlers[sig] = signal.getsignal(sig)
|
|
148
|
+
signal.signal(sig, fn)
|
|
149
|
+
fcntl.fcntl(output_fd, fcntl.F_SETFL, old_flags | os.O_NONBLOCK)
|
|
150
|
+
tty.setraw(input_fd)
|
|
151
|
+
if bar is not None:
|
|
152
|
+
bar.start()
|
|
153
|
+
output = bytearray()
|
|
154
|
+
pending_input = bytearray()
|
|
155
|
+
filter_ = DetachFilter()
|
|
156
|
+
escape_at = None
|
|
157
|
+
master_ended = False
|
|
158
|
+
|
|
159
|
+
while stopped[0] is None:
|
|
160
|
+
if resized[0]:
|
|
161
|
+
resized[0] = False
|
|
162
|
+
rows, cols = _tty_size(output_fd)
|
|
163
|
+
child_rows = bar.resize(rows, cols) if bar else rows
|
|
164
|
+
fcntl.ioctl(master, termios.TIOCSWINSZ,
|
|
165
|
+
struct.pack("HHHH", child_rows, cols, 0, 0))
|
|
166
|
+
if bar is not None:
|
|
167
|
+
bar.update()
|
|
168
|
+
output.extend(bar.poll())
|
|
169
|
+
if child_exit is None:
|
|
170
|
+
exited, status = os.waitpid(pid, os.WNOHANG)
|
|
171
|
+
if exited:
|
|
172
|
+
child_exit = os.waitstatus_to_exitcode(status)
|
|
173
|
+
child_exited_at = time.monotonic()
|
|
174
|
+
if child_exit is not None and master_ended and not output:
|
|
175
|
+
return child_exit
|
|
176
|
+
# A stopped terminal reader must never pin termios restoration
|
|
177
|
+
# after the native child has exited.
|
|
178
|
+
if child_exited_at is not None and time.monotonic() - child_exited_at > 2:
|
|
179
|
+
return child_exit
|
|
180
|
+
|
|
181
|
+
readers = []
|
|
182
|
+
if not master_ended and len(output) < MAX_PENDING_OUTPUT - 32768:
|
|
183
|
+
readers.append(master)
|
|
184
|
+
if child_exit is None and len(pending_input) < _MAX_INPUT:
|
|
185
|
+
readers.append(input_fd)
|
|
186
|
+
writers = []
|
|
187
|
+
if output:
|
|
188
|
+
writers.append(output_fd)
|
|
189
|
+
if pending_input and not master_ended:
|
|
190
|
+
writers.append(master)
|
|
191
|
+
try:
|
|
192
|
+
ready, writable, _ = select.select(readers, writers, [], .05)
|
|
193
|
+
except InterruptedError:
|
|
194
|
+
continue
|
|
195
|
+
if output_fd in writable:
|
|
196
|
+
try:
|
|
197
|
+
n = os.write(output_fd, output[:16384])
|
|
198
|
+
if not n:
|
|
199
|
+
raise OSError("terminal output unavailable")
|
|
200
|
+
del output[:n]
|
|
201
|
+
except (BlockingIOError, InterruptedError):
|
|
202
|
+
pass
|
|
203
|
+
if master in writable:
|
|
204
|
+
try:
|
|
205
|
+
n = os.write(master, pending_input[:16384])
|
|
206
|
+
if not n:
|
|
207
|
+
raise OSError("Codex PTY no longer receives input")
|
|
208
|
+
del pending_input[:n]
|
|
209
|
+
except (BlockingIOError, InterruptedError):
|
|
210
|
+
pass
|
|
211
|
+
except OSError:
|
|
212
|
+
pending_input.clear()
|
|
213
|
+
master_ended = True
|
|
214
|
+
if input_fd in ready:
|
|
215
|
+
data = os.read(input_fd, 16384)
|
|
216
|
+
if not data:
|
|
217
|
+
break
|
|
218
|
+
filtered = filter_.feed(data)
|
|
219
|
+
if filter_.detached:
|
|
220
|
+
# CCX detaches only its local remote TUI; the codexd
|
|
221
|
+
# subscription and any running turn keep their lifecycle.
|
|
222
|
+
return 0
|
|
223
|
+
pending_input.extend(filtered)
|
|
224
|
+
escape_at = time.monotonic() if filter_.escape else None
|
|
225
|
+
if (filter_.escape and escape_at is not None
|
|
226
|
+
and time.monotonic() - escape_at >= .2):
|
|
227
|
+
# Same lone-ESC flush rule as shared Claude attach.
|
|
228
|
+
pending_input.extend(filter_.flush_escape())
|
|
229
|
+
escape_at = None
|
|
230
|
+
if master in ready:
|
|
231
|
+
try:
|
|
232
|
+
data = os.read(master, 16384)
|
|
233
|
+
except (BlockingIOError, InterruptedError):
|
|
234
|
+
data = None
|
|
235
|
+
except OSError: # EIO is normal PTY EOF on Linux.
|
|
236
|
+
data = b""
|
|
237
|
+
if data == b"":
|
|
238
|
+
master_ended = True
|
|
239
|
+
elif data:
|
|
240
|
+
output.extend(bar.feed(data) if bar is not None else data)
|
|
241
|
+
|
|
242
|
+
return 128 + stopped[0] if stopped[0] is not None else 1
|
|
243
|
+
finally:
|
|
244
|
+
if bar is not None:
|
|
245
|
+
bar.stop()
|
|
246
|
+
if child_exit is None:
|
|
247
|
+
_reap_owned_tui(pid)
|
|
248
|
+
os.close(master)
|
|
249
|
+
try:
|
|
250
|
+
try:
|
|
251
|
+
_write_tty(
|
|
252
|
+
output_fd, (bar.restore() if bar is not None else b"") +
|
|
253
|
+
b"\x1b[?2004l\x1b[?1000l\x1b[?1002l\x1b[?1006l"
|
|
254
|
+
b"\x1b[?1004l\x1b[?25h\x1b[?1049l",
|
|
255
|
+
timeout_s=0.1,
|
|
256
|
+
)
|
|
257
|
+
except (OSError, TimeoutError, InterruptedError):
|
|
258
|
+
pass
|
|
259
|
+
finally:
|
|
260
|
+
try:
|
|
261
|
+
termios.tcsetattr(input_fd, termios.TCSANOW, old_attrs)
|
|
262
|
+
finally:
|
|
263
|
+
try:
|
|
264
|
+
fcntl.fcntl(output_fd, fcntl.F_SETFL, old_flags)
|
|
265
|
+
finally:
|
|
266
|
+
for sig, fn in handlers.items():
|
|
267
|
+
signal.signal(sig, fn)
|