ccx-native 0.0.1 → 0.2.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 +240 -4
- package/bin/ccx +55 -2
- package/ccx/__init__.py +167 -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 +807 -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 +269 -0
- package/ccx/claude/host/session.py +5244 -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 +2535 -0
- package/ccx/claude/usage.py +796 -0
- package/ccx/claude/usage_control.py +204 -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/models.py +53 -0
- package/ccx/cli/commands/read.py +1104 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +302 -0
- package/ccx/cli/parser.py +1439 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1904 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +738 -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 +1558 -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 +2424 -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/history_store.py +131 -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 +1454 -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/models.py +225 -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 +296 -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 +1593 -0
- package/ccx/ops/conversation_paging.py +1339 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +575 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/history.py +1041 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +773 -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 +348 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +640 -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 +250 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +789 -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/private_json.py +54 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +809 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2093 -0
- package/ccx.py +9 -0
- package/package.json +39 -7
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +166 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/history.md +50 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Promptless Claude Code usage and model control protocol; no credential reads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import select
|
|
8
|
+
import subprocess
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
from ccx.constants import CLAUDE_BIN
|
|
13
|
+
|
|
14
|
+
# None of these flags is --bare: --bare disables native OAuth/Keychain auth.
|
|
15
|
+
_PROBE_ARGS = (
|
|
16
|
+
"-p", "--input-format", "stream-json", "--output-format", "stream-json",
|
|
17
|
+
"--verbose", "--safe-mode", "--strict-mcp-config",
|
|
18
|
+
"--no-session-persistence", "--disable-slash-commands", "--no-chrome",
|
|
19
|
+
"--tools", "",
|
|
20
|
+
)
|
|
21
|
+
_MAX_OUTPUT_BYTES = 2 * 1024 * 1024
|
|
22
|
+
_MAX_LINE_BYTES = 1 * 1024 * 1024
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _control_exchange(requests: list[dict], timeout: float, cwd: str | None = None) -> dict:
|
|
26
|
+
"""Send a list of requests through one bounded promptless child.
|
|
27
|
+
|
|
28
|
+
Initialize replies are projected immediately: the full response can also
|
|
29
|
+
contain personal account data, which must never leave this transport.
|
|
30
|
+
"""
|
|
31
|
+
timeout = max(0.1, float(timeout))
|
|
32
|
+
deadline = time.monotonic() + timeout
|
|
33
|
+
ids = [(uuid.uuid4().hex, request) for request in requests]
|
|
34
|
+
target_id = ids[-1][0]
|
|
35
|
+
subtypes = {rid: request["subtype"] for rid, request in ids}
|
|
36
|
+
child = None
|
|
37
|
+
replies: dict[str, dict] = {}
|
|
38
|
+
seen_target = False
|
|
39
|
+
invalid_line = None
|
|
40
|
+
total_bytes = 0
|
|
41
|
+
buffer = b""
|
|
42
|
+
problem = None
|
|
43
|
+
try:
|
|
44
|
+
child = subprocess.Popen(
|
|
45
|
+
[CLAUDE_BIN, *_PROBE_ARGS], stdin=subprocess.PIPE,
|
|
46
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, cwd=cwd,
|
|
47
|
+
)
|
|
48
|
+
for rid, request in ids:
|
|
49
|
+
message = {"type": "control_request", "request_id": rid, "request": request}
|
|
50
|
+
child.stdin.write((json.dumps(message, separators=(",", ":")) + "\n").encode())
|
|
51
|
+
child.stdin.flush()
|
|
52
|
+
|
|
53
|
+
while time.monotonic() < deadline and not seen_target and problem is None:
|
|
54
|
+
readable, _, _ = select.select(
|
|
55
|
+
[child.stdout], [], [], min(0.2, max(0, deadline - time.monotonic()))
|
|
56
|
+
)
|
|
57
|
+
if not readable:
|
|
58
|
+
if child.poll() is not None:
|
|
59
|
+
break
|
|
60
|
+
continue
|
|
61
|
+
chunk = os.read(child.stdout.fileno(), 32768)
|
|
62
|
+
if not chunk:
|
|
63
|
+
break
|
|
64
|
+
total_bytes += len(chunk)
|
|
65
|
+
if total_bytes > _MAX_OUTPUT_BYTES:
|
|
66
|
+
problem = "Claude usage control output too large"
|
|
67
|
+
break
|
|
68
|
+
buffer += chunk
|
|
69
|
+
if len(buffer) > _MAX_LINE_BYTES and b"\n" not in buffer:
|
|
70
|
+
problem = "Claude usage control line too large"
|
|
71
|
+
break
|
|
72
|
+
while b"\n" in buffer:
|
|
73
|
+
line, buffer = buffer.split(b"\n", 1)
|
|
74
|
+
if len(line) > _MAX_LINE_BYTES:
|
|
75
|
+
problem = "Claude usage control line too large"
|
|
76
|
+
break
|
|
77
|
+
try:
|
|
78
|
+
message = json.loads(line)
|
|
79
|
+
except (ValueError, UnicodeDecodeError):
|
|
80
|
+
invalid_line = line[:1000].decode("utf-8", "replace")
|
|
81
|
+
continue
|
|
82
|
+
if not isinstance(message, dict) or message.get("type") != "control_response":
|
|
83
|
+
continue
|
|
84
|
+
response = message.get("response")
|
|
85
|
+
if not isinstance(response, dict):
|
|
86
|
+
continue
|
|
87
|
+
rid = response.get("request_id")
|
|
88
|
+
if rid not in subtypes:
|
|
89
|
+
continue
|
|
90
|
+
subtype = subtypes[rid]
|
|
91
|
+
if subtype == "initialize" and response.get("subtype") == "error":
|
|
92
|
+
problem = "Claude usage initialize control request failed"
|
|
93
|
+
break
|
|
94
|
+
if subtype == "initialize":
|
|
95
|
+
body = response.get("response")
|
|
96
|
+
# Do not retain the rest of initialize (notably 'account').
|
|
97
|
+
safe_body = ({
|
|
98
|
+
"models": body.get("models"),
|
|
99
|
+
"claude_code_version": body.get("claude_code_version"),
|
|
100
|
+
} if isinstance(body, dict) else None)
|
|
101
|
+
message = {
|
|
102
|
+
"type": "control_response",
|
|
103
|
+
"response": {
|
|
104
|
+
"request_id": rid,
|
|
105
|
+
"subtype": response.get("subtype"),
|
|
106
|
+
"response": safe_body,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
replies[subtype] = message
|
|
110
|
+
if rid == target_id:
|
|
111
|
+
seen_target = True
|
|
112
|
+
break
|
|
113
|
+
# Always close stdin after the matching response, or on a bounded failure.
|
|
114
|
+
child.stdin.close()
|
|
115
|
+
child.stdin = None
|
|
116
|
+
remaining = max(0.0, deadline - time.monotonic())
|
|
117
|
+
try:
|
|
118
|
+
exit_code = child.wait(timeout=remaining)
|
|
119
|
+
except subprocess.TimeoutExpired:
|
|
120
|
+
problem = problem or "Claude usage control timed out"
|
|
121
|
+
exit_code = None
|
|
122
|
+
if not seen_target:
|
|
123
|
+
if problem is None:
|
|
124
|
+
problem = "Claude usage control returned no matching response"
|
|
125
|
+
if invalid_line is not None:
|
|
126
|
+
return {"kind": "unparsed", "raw": {"stdout_text": invalid_line},
|
|
127
|
+
"error": problem}
|
|
128
|
+
return {"kind": "error", "error": problem}
|
|
129
|
+
if problem is not None or exit_code not in (0, None):
|
|
130
|
+
return {"kind": "error", "error": problem or "Claude usage control exited unsuccessfully"}
|
|
131
|
+
return {"kind": "success", "responses": replies}
|
|
132
|
+
except (OSError, ValueError, BrokenPipeError) as exc:
|
|
133
|
+
return {"kind": "error", "error": f"Claude usage control unavailable: {type(exc).__name__}"}
|
|
134
|
+
finally:
|
|
135
|
+
if child is not None:
|
|
136
|
+
if child.stdin is not None and not child.stdin.closed:
|
|
137
|
+
child.stdin.close()
|
|
138
|
+
if child.poll() is None:
|
|
139
|
+
# No early SIGKILL: wait for the bounded deadline to expire first.
|
|
140
|
+
try:
|
|
141
|
+
child.wait(timeout=max(0.0, deadline - time.monotonic()))
|
|
142
|
+
except subprocess.TimeoutExpired:
|
|
143
|
+
child.terminate()
|
|
144
|
+
try:
|
|
145
|
+
child.wait(timeout=1.0)
|
|
146
|
+
except subprocess.TimeoutExpired:
|
|
147
|
+
child.kill()
|
|
148
|
+
child.wait(timeout=1.0)
|
|
149
|
+
if child.stdout is not None:
|
|
150
|
+
child.stdout.close()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def request_usage(timeout: float = 8.0) -> dict:
|
|
154
|
+
"""Return {kind, body?, raw?, error?} from one bounded get_usage exchange.
|
|
155
|
+
|
|
156
|
+
The native CLI owns auth. The only input consists of initialize/get_usage
|
|
157
|
+
control requests; no model prompt, hooks, MCP server or transcript scan.
|
|
158
|
+
"""
|
|
159
|
+
result = _control_exchange([
|
|
160
|
+
{"subtype": "initialize"},
|
|
161
|
+
{"subtype": "get_usage", "skip_behaviors": True},
|
|
162
|
+
], timeout)
|
|
163
|
+
if result["kind"] != "success":
|
|
164
|
+
return result
|
|
165
|
+
reply = result["responses"]["get_usage"]
|
|
166
|
+
response = reply["response"]
|
|
167
|
+
if response.get("subtype") == "error":
|
|
168
|
+
return {"kind": "error", "error": "Claude usage control response was an error"}
|
|
169
|
+
if response.get("subtype") != "success":
|
|
170
|
+
return {"kind": "unparsed", "raw": reply,
|
|
171
|
+
"error": "Claude usage control returned an unknown response subtype"}
|
|
172
|
+
body = response.get("response")
|
|
173
|
+
if not isinstance(body, dict):
|
|
174
|
+
return {"kind": "unparsed", "raw": reply,
|
|
175
|
+
"error": "Claude usage control returned an unknown JSON body"}
|
|
176
|
+
if body.get("rate_limits_available") is False and body.get("rate_limits") is None:
|
|
177
|
+
return {"kind": "unavailable", "error": "Claude plan rate limits unavailable"}
|
|
178
|
+
if body.get("rate_limits_available") is True and body.get("rate_limits") is None:
|
|
179
|
+
return {"kind": "held", "error": "Claude plan usage is held by the provider"}
|
|
180
|
+
rate_limits = body.get("rate_limits")
|
|
181
|
+
if isinstance(rate_limits, dict) and any(k in rate_limits for k in (
|
|
182
|
+
"five_hour", "seven_day", "limits", "model_scoped", "extra_usage",
|
|
183
|
+
"seven_day_opus", "seven_day_sonnet",
|
|
184
|
+
)):
|
|
185
|
+
return {"kind": "success", "body": rate_limits}
|
|
186
|
+
return {"kind": "unparsed", "raw": reply,
|
|
187
|
+
"error": "Claude usage control returned an unrecognized rate_limits field"}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def request_models(timeout: float = 8.0) -> dict:
|
|
191
|
+
"""Return only Claude's model entries and version from initialize."""
|
|
192
|
+
# From home, so one project's settings can't shape the shared catalog.
|
|
193
|
+
result = _control_exchange([{"subtype": "initialize"}], timeout,
|
|
194
|
+
cwd=os.path.expanduser("~"))
|
|
195
|
+
if result["kind"] != "success":
|
|
196
|
+
# Do not return raw stdout; initialize may include private account data.
|
|
197
|
+
return {"kind": "error", "error": result.get("error") or "Claude model probe failed"}
|
|
198
|
+
response = result["responses"]["initialize"]["response"]
|
|
199
|
+
if response.get("subtype") != "success":
|
|
200
|
+
return {"kind": "error", "error": "Claude model initialize failed"}
|
|
201
|
+
body = response.get("response")
|
|
202
|
+
if not isinstance(body, dict) or not isinstance(body.get("models"), list):
|
|
203
|
+
return {"kind": "error", "error": "Claude model initialize returned no models"}
|
|
204
|
+
return {"kind": "success", "body": body}
|
|
File without changes
|