pcode 0.1.0__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.
- pcode/__init__.py +7 -0
- pcode/_job_supervisor.py +65 -0
- pcode/agent.py +592 -0
- pcode/anthropic_oauth.py +469 -0
- pcode/app.py +3165 -0
- pcode/appearance.py +181 -0
- pcode/aside.py +541 -0
- pcode/aside_guard.py +39 -0
- pcode/aside_ui.py +683 -0
- pcode/auth.py +113 -0
- pcode/block.py +39 -0
- pcode/browser.py +393 -0
- pcode/cache_diagnostics.py +336 -0
- pcode/cache_settings.py +58 -0
- pcode/cache_warnings.py +139 -0
- pcode/claude_sdk/__init__.py +96 -0
- pcode/claude_sdk/cli.py +52 -0
- pcode/claude_sdk/errors.py +61 -0
- pcode/claude_sdk/messages.py +154 -0
- pcode/claude_sdk/model.py +179 -0
- pcode/claude_sdk/resume.py +114 -0
- pcode/claude_sdk/session.py +547 -0
- pcode/claude_sdk/session_pool.py +251 -0
- pcode/claude_sdk/workspace.py +34 -0
- pcode/cli.py +73 -0
- pcode/clipboard.py +76 -0
- pcode/code_mode.py +43 -0
- pcode/codex_login.py +189 -0
- pcode/command_transcript.py +71 -0
- pcode/commands.py +145 -0
- pcode/compaction.py +457 -0
- pcode/completion.py +263 -0
- pcode/config.py +223 -0
- pcode/context_breakdown.py +178 -0
- pcode/context_usage.py +52 -0
- pcode/controller.py +3496 -0
- pcode/conversation_ids.py +27 -0
- pcode/conversation_tree.py +160 -0
- pcode/copy_ui.py +172 -0
- pcode/delegation.py +236 -0
- pcode/delta.py +295 -0
- pcode/diagnostics.py +226 -0
- pcode/edit_preview.py +158 -0
- pcode/edit_transcript.py +287 -0
- pcode/edit_ui.py +850 -0
- pcode/edits.py +162 -0
- pcode/email_remote/__init__.py +14 -0
- pcode/email_remote/cli.py +133 -0
- pcode/email_remote/listener.py +695 -0
- pcode/email_remote/mailbox.py +336 -0
- pcode/email_remote/outbound.py +75 -0
- pcode/email_remote/parsing.py +226 -0
- pcode/email_remote/render.py +175 -0
- pcode/email_remote/state.py +79 -0
- pcode/error_report.py +38 -0
- pcode/ext.py +602 -0
- pcode/extension_guide.md +311 -0
- pcode/extensions/browser.py +284 -0
- pcode/extensions/ponytail.py +217 -0
- pcode/extensions/sandbox.py +138 -0
- pcode/extensions/session_history.py +170 -0
- pcode/extensions/web_research.py +89 -0
- pcode/file_refs.py +265 -0
- pcode/filesystem.py +138 -0
- pcode/frame.py +149 -0
- pcode/gateway.py +465 -0
- pcode/git_diff.py +497 -0
- pcode/history.py +589 -0
- pcode/history_cursor.py +147 -0
- pcode/history_embeddings.py +102 -0
- pcode/host.py +941 -0
- pcode/host_protocol.py +291 -0
- pcode/host_ui.py +225 -0
- pcode/input_keys.py +35 -0
- pcode/inspection.py +289 -0
- pcode/inspector_ui.py +448 -0
- pcode/isolated_delegation.py +425 -0
- pcode/job_notices.py +54 -0
- pcode/jobs.py +584 -0
- pcode/jobs_ui.py +265 -0
- pcode/keymap.py +215 -0
- pcode/layout_speed.py +103 -0
- pcode/links.py +90 -0
- pcode/links_ui.py +200 -0
- pcode/live.py +1301 -0
- pcode/llm_proxy.py +44 -0
- pcode/logins.py +233 -0
- pcode/mcp.py +683 -0
- pcode/mcp_notice.py +91 -0
- pcode/mcp_oauth.py +381 -0
- pcode/meridian.py +233 -0
- pcode/meridian_process.py +337 -0
- pcode/meridian_reminders.py +107 -0
- pcode/meridian_setup.py +356 -0
- pcode/model_metadata.py +451 -0
- pcode/model_ui.py +168 -0
- pcode/models.py +209 -0
- pcode/native_results.py +119 -0
- pcode/oauth_pages.py +66 -0
- pcode/output_limits.py +41 -0
- pcode/paste.py +45 -0
- pcode/path_completion.py +66 -0
- pcode/plan_preview.py +131 -0
- pcode/plan_sizes.py +35 -0
- pcode/planning.py +161 -0
- pcode/popup_ui.py +873 -0
- pcode/preferences.py +1176 -0
- pcode/prefix_keys.py +490 -0
- pcode/profile_benchmark.py +332 -0
- pcode/profiling.py +566 -0
- pcode/project_trust.py +94 -0
- pcode/prompt_keys.py +286 -0
- pcode/remote.py +717 -0
- pcode/remote_print.py +125 -0
- pcode/remote_profile.py +237 -0
- pcode/replay_benchmark.py +319 -0
- pcode/repo_context.py +158 -0
- pcode/retries.py +28 -0
- pcode/rpc.py +280 -0
- pcode/runtime.py +244 -0
- pcode/sandbox.py +454 -0
- pcode/session_naming.py +82 -0
- pcode/session_ui.py +550 -0
- pcode/sessions.py +892 -0
- pcode/shell.py +113 -0
- pcode/shell_mode.py +172 -0
- pcode/shell_tools.py +459 -0
- pcode/skills.py +161 -0
- pcode/stall_log.py +187 -0
- pcode/startup_input.py +96 -0
- pcode/steering.py +27 -0
- pcode/stream_display.py +188 -0
- pcode/stream_events.py +437 -0
- pcode/strict_tools.py +130 -0
- pcode/syntax.py +43 -0
- pcode/syntax_colors.py +206 -0
- pcode/task_prompt.py +28 -0
- pcode/task_worktrees.py +482 -0
- pcode/terminal_notify.py +383 -0
- pcode/theme.py +135 -0
- pcode/theme_gallery.py +88 -0
- pcode/thinking_markdown.py +41 -0
- pcode/token_accounting.py +67 -0
- pcode/tool_display.py +784 -0
- pcode/tool_output_limits.py +108 -0
- pcode/tool_panel.py +377 -0
- pcode/transcript_log.py +127 -0
- pcode/transcript_notice.py +103 -0
- pcode/tree_ui.py +288 -0
- pcode/turn.py +44 -0
- pcode/ui.py +3745 -0
- pcode/usage.py +293 -0
- pcode/word_wrap.py +107 -0
- pcode/worker_ui.py +235 -0
- pcode/workers.py +136 -0
- pcode/workspace.py +57 -0
- pcode/workspace_filesystem.py +72 -0
- pcode/worktree.py +650 -0
- pcode-0.1.0.dist-info/METADATA +145 -0
- pcode-0.1.0.dist-info/RECORD +163 -0
- pcode-0.1.0.dist-info/WHEEL +4 -0
- pcode-0.1.0.dist-info/entry_points.txt +3 -0
- pcode-0.1.0.dist-info/licenses/LICENSE +21 -0
pcode/llm_proxy.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Codex-only proxy transport; never change process-wide HTTP settings."""
|
|
2
|
+
|
|
3
|
+
import httpx2
|
|
4
|
+
from pydantic_ai.models import get_user_agent
|
|
5
|
+
from pydantic_ai.providers.openai_codex import OpenAICodexCredentialSource, OpenAICodexProvider
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProxiedCodexProvider(OpenAICodexProvider):
|
|
9
|
+
"""Own a dedicated proxied client, including across repeated agent runs."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self, proxy: str, *, credential_source: OpenAICodexCredentialSource | None = None
|
|
13
|
+
) -> None:
|
|
14
|
+
# Validate without echoing a URL that may contain proxy credentials.
|
|
15
|
+
try:
|
|
16
|
+
url = httpx2.URL(proxy)
|
|
17
|
+
if url.scheme not in {"http", "https"} or not url.host:
|
|
18
|
+
raise ValueError
|
|
19
|
+
except (ValueError, httpx2.InvalidURL):
|
|
20
|
+
raise ValueError(
|
|
21
|
+
"PCODE_LLM_PROXY must be a valid http:// or https:// proxy URL"
|
|
22
|
+
) from None
|
|
23
|
+
self._proxy_url = proxy
|
|
24
|
+
client = self._new_http_client()
|
|
25
|
+
super().__init__(credential_source=credential_source, http_client=client)
|
|
26
|
+
# Pydantic AI 2.43's Provider context manager closes owned clients and
|
|
27
|
+
# recreates them on re-entry. Injected clients are otherwise caller-owned.
|
|
28
|
+
# These private ownership hooks are covered by lifecycle regression tests.
|
|
29
|
+
self._own_http_client = client
|
|
30
|
+
self._http_client_factory = self._create_http_client
|
|
31
|
+
|
|
32
|
+
def _new_http_client(self) -> httpx2.AsyncClient:
|
|
33
|
+
return httpx2.AsyncClient(
|
|
34
|
+
proxy=self._proxy_url,
|
|
35
|
+
trust_env=False,
|
|
36
|
+
timeout=httpx2.Timeout(600, connect=5),
|
|
37
|
+
headers={"User-Agent": get_user_agent()},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def _create_http_client(self) -> httpx2.AsyncClient:
|
|
41
|
+
client = self._new_http_client()
|
|
42
|
+
client.auth = self._auth
|
|
43
|
+
self._http_client = client
|
|
44
|
+
return client
|
pcode/logins.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Provider sign-in and sign-out for a session: `/login` and `/logout`.
|
|
2
|
+
|
|
3
|
+
`Logins` is a collaborator of `SessionController`, which holds one as
|
|
4
|
+
`controller.logins`. The commands only record what was asked for
|
|
5
|
+
(`controller.login_requested` / `logout_requested`); the controller's command
|
|
6
|
+
loop runs the browser flow once the handler returns.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import os
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from pcode.preferences import load_preferences
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pcode.controller import SessionController
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Logins:
|
|
22
|
+
"""Signs a session in to, and out of, the providers pcode can store logins for."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, controller: SessionController) -> None:
|
|
25
|
+
self.controller = controller
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def view(self):
|
|
29
|
+
return self.controller.view
|
|
30
|
+
|
|
31
|
+
def login(self, argument: str) -> None:
|
|
32
|
+
# Signing in stores a credential; it does not require the conversation to
|
|
33
|
+
# already be on Anthropic. A non-Anthropic session keeps its own model.
|
|
34
|
+
from pcode.models import login_sources
|
|
35
|
+
|
|
36
|
+
sources = login_sources()
|
|
37
|
+
source = argument.strip() or sources[0]
|
|
38
|
+
if source not in sources:
|
|
39
|
+
self.view.note(f"Usage: /login [{'|'.join(sources)}]")
|
|
40
|
+
return
|
|
41
|
+
self.controller.login_requested = source
|
|
42
|
+
|
|
43
|
+
def logout(self, argument: str) -> None:
|
|
44
|
+
source = argument.strip() or "anthropic"
|
|
45
|
+
if source == "openai-codex":
|
|
46
|
+
self.controller.logout_requested = source
|
|
47
|
+
return
|
|
48
|
+
if source != "anthropic":
|
|
49
|
+
self.view.note("Usage: /logout [anthropic|openai-codex]")
|
|
50
|
+
return
|
|
51
|
+
self.logout_anthropic()
|
|
52
|
+
|
|
53
|
+
def logout_anthropic(self) -> None:
|
|
54
|
+
from pcode.anthropic_oauth import credentials_path, delete_tokens
|
|
55
|
+
from pcode.auth import LoginError
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
removed = delete_tokens(credentials_path())
|
|
59
|
+
except LoginError as error:
|
|
60
|
+
self.view.error(str(error))
|
|
61
|
+
return
|
|
62
|
+
if os.environ.get("PCODE_ANTHROPIC_AUTH", "").strip() == "oauth":
|
|
63
|
+
del os.environ["PCODE_ANTHROPIC_AUTH"]
|
|
64
|
+
# The stored sign-in is gone; a saved "oauth" choice would now resolve
|
|
65
|
+
# to a credential that no longer exists.
|
|
66
|
+
if load_preferences().get("anthropic_auth") == "oauth":
|
|
67
|
+
self.controller.forget_defaults("anthropic_auth")
|
|
68
|
+
if not removed:
|
|
69
|
+
self.view.note("No stored Anthropic login to remove.")
|
|
70
|
+
return
|
|
71
|
+
self.view.note(
|
|
72
|
+
"Removed pcode's stored Anthropic login. This conversation keeps its current "
|
|
73
|
+
"model until the token expires; use /login again or set ANTHROPIC_API_KEY."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
async def perform_login(self, source: str | None) -> None:
|
|
77
|
+
if source == "openai-codex":
|
|
78
|
+
await self.login_codex()
|
|
79
|
+
elif source == "meridian":
|
|
80
|
+
await self.login_meridian()
|
|
81
|
+
elif source == "claude":
|
|
82
|
+
await self.login_claude()
|
|
83
|
+
else:
|
|
84
|
+
await self.login_anthropic()
|
|
85
|
+
|
|
86
|
+
async def login_meridian(self) -> None:
|
|
87
|
+
"""Run Claude Code's own sign-in for the Meridian this session uses."""
|
|
88
|
+
from pcode.auth import LoginError
|
|
89
|
+
from pcode.meridian_setup import claude_login, login_target
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
target = await asyncio.to_thread(login_target)
|
|
93
|
+
self.view.note(
|
|
94
|
+
f"Signing in to Claude for Meridian ({target.label}) with `claude auth login`. "
|
|
95
|
+
"Finish in the browser (Ctrl+C cancels)."
|
|
96
|
+
)
|
|
97
|
+
status = await claude_login(self.view.note, target)
|
|
98
|
+
plan = status.get("subscriptionType")
|
|
99
|
+
self.view.note(
|
|
100
|
+
f"Signed in to Claude ({target.label}"
|
|
101
|
+
+ (f", {plan} plan" if plan else "")
|
|
102
|
+
+ "). Meridian uses it from its next request; pcode stores nothing."
|
|
103
|
+
)
|
|
104
|
+
except asyncio.CancelledError:
|
|
105
|
+
self.view.note("Claude sign-in cancelled.")
|
|
106
|
+
raise
|
|
107
|
+
except LoginError as error:
|
|
108
|
+
self.view.error(str(error))
|
|
109
|
+
except Exception:
|
|
110
|
+
self.view.error("Claude sign-in failed. No credential details were logged.")
|
|
111
|
+
|
|
112
|
+
async def login_claude(self) -> None:
|
|
113
|
+
"""Run Claude Code's own sign-in for `claude:` models, with the CLI they run."""
|
|
114
|
+
from pcode.auth import LoginError
|
|
115
|
+
from pcode.claude_sdk import LOGIN_ENV, MISSING_SDK, cli_path
|
|
116
|
+
from pcode.meridian_setup import LoginTarget, claude_login
|
|
117
|
+
from pcode.models import claude_sdk_installed
|
|
118
|
+
|
|
119
|
+
if not claude_sdk_installed():
|
|
120
|
+
self.view.error(MISSING_SDK)
|
|
121
|
+
return
|
|
122
|
+
target = LoginTarget(os.environ.get("CLAUDE_CONFIG_DIR") or None, "Claude Code's login")
|
|
123
|
+
try:
|
|
124
|
+
self.view.note(
|
|
125
|
+
"Signing in to Claude Code with `claude auth login`. "
|
|
126
|
+
"Finish in the browser (Ctrl+C cancels)."
|
|
127
|
+
)
|
|
128
|
+
# Scrubbed as the requests are, so an API key cannot pass for the login.
|
|
129
|
+
status = await claude_login(
|
|
130
|
+
self.view.note,
|
|
131
|
+
target,
|
|
132
|
+
executable=cli_path(),
|
|
133
|
+
retry="/login claude",
|
|
134
|
+
extra_env=LOGIN_ENV,
|
|
135
|
+
for_meridian=False,
|
|
136
|
+
)
|
|
137
|
+
plan = status.get("subscriptionType")
|
|
138
|
+
self.view.note(
|
|
139
|
+
"Signed in to Claude Code"
|
|
140
|
+
+ (f" ({plan} plan)" if plan else "")
|
|
141
|
+
+ ". claude: models use it from their next request; pcode stores nothing."
|
|
142
|
+
)
|
|
143
|
+
except asyncio.CancelledError:
|
|
144
|
+
self.view.note("Claude sign-in cancelled.")
|
|
145
|
+
raise
|
|
146
|
+
except LoginError as error:
|
|
147
|
+
self.view.error(str(error))
|
|
148
|
+
except Exception:
|
|
149
|
+
self.view.error("Claude sign-in failed. No credential details were logged.")
|
|
150
|
+
|
|
151
|
+
async def login_codex(self) -> None:
|
|
152
|
+
from pcode.agent import codex_model
|
|
153
|
+
from pcode.auth import LoginError
|
|
154
|
+
from pcode.codex_login import credentials_path, login
|
|
155
|
+
|
|
156
|
+
controller = self.controller
|
|
157
|
+
self.view.note(
|
|
158
|
+
"Sign in with your ChatGPT account in the browser. "
|
|
159
|
+
"If no browser opens, visit this URL (Ctrl+C cancels):"
|
|
160
|
+
)
|
|
161
|
+
try:
|
|
162
|
+
await login(notify=self.view.note)
|
|
163
|
+
# Codex credentials are read when the model is built, so a Codex
|
|
164
|
+
# conversation must rebuild its model to adopt the new sign-in.
|
|
165
|
+
codex = (controller.model or "").startswith("openai-codex:")
|
|
166
|
+
if codex and hasattr(controller.runtime, "agent"):
|
|
167
|
+
controller.runtime.agent.model = await asyncio.to_thread(
|
|
168
|
+
codex_model, controller.model
|
|
169
|
+
)
|
|
170
|
+
self.view.note(
|
|
171
|
+
f"Signed in to OpenAI Codex. Credentials are stored in {credentials_path()} "
|
|
172
|
+
"(owner-only) and refreshed automatically; /logout openai-codex removes them."
|
|
173
|
+
)
|
|
174
|
+
except asyncio.CancelledError:
|
|
175
|
+
self.view.note("OpenAI Codex sign-in cancelled.")
|
|
176
|
+
raise
|
|
177
|
+
except LoginError as error:
|
|
178
|
+
self.view.error(str(error))
|
|
179
|
+
except Exception:
|
|
180
|
+
self.view.error("OpenAI Codex sign-in failed. No credential details were logged.")
|
|
181
|
+
|
|
182
|
+
async def perform_logout(self) -> None:
|
|
183
|
+
from pcode.auth import LoginError
|
|
184
|
+
from pcode.codex_login import credentials_path, delete_credentials
|
|
185
|
+
|
|
186
|
+
self.controller.logout_requested = None
|
|
187
|
+
try:
|
|
188
|
+
removed = await asyncio.to_thread(delete_credentials, credentials_path())
|
|
189
|
+
except LoginError as error:
|
|
190
|
+
self.view.error(str(error))
|
|
191
|
+
return
|
|
192
|
+
self.view.note(
|
|
193
|
+
"Removed pcode's stored OpenAI Codex login. "
|
|
194
|
+
"New models fall back to the CLI login, if present; that login was not removed. "
|
|
195
|
+
"The current model retains its in-memory token until it expires."
|
|
196
|
+
if removed
|
|
197
|
+
else "No stored pcode OpenAI Codex login to remove. CLI login is unchanged."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
async def login_anthropic(self) -> None:
|
|
201
|
+
from pcode.anthropic_oauth import AnthropicOAuthModel, credentials_path, login
|
|
202
|
+
from pcode.auth import LoginError
|
|
203
|
+
|
|
204
|
+
controller = self.controller
|
|
205
|
+
self.view.note(
|
|
206
|
+
"Opening claude.ai to sign in with your Anthropic account. "
|
|
207
|
+
"If no browser opens, visit this URL (Ctrl+C cancels):"
|
|
208
|
+
)
|
|
209
|
+
try:
|
|
210
|
+
await login(notify=self.view.note)
|
|
211
|
+
# Only an Anthropic conversation adopts the new credential; a Codex
|
|
212
|
+
# or Meridian session keeps its own model and provider.
|
|
213
|
+
if controller.model and controller.model.startswith("anthropic:"):
|
|
214
|
+
controller.runtime.agent.model = await asyncio.to_thread(
|
|
215
|
+
AnthropicOAuthModel, controller.model
|
|
216
|
+
)
|
|
217
|
+
os.environ["PCODE_ANTHROPIC_AUTH"] = "oauth"
|
|
218
|
+
controller.persist_defaults(anthropic_auth="oauth")
|
|
219
|
+
self.view.note(
|
|
220
|
+
f"Signed in to Anthropic. Credentials are stored in {credentials_path()} "
|
|
221
|
+
"(owner-only) and refreshed automatically; /logout removes them."
|
|
222
|
+
)
|
|
223
|
+
self.view.note(
|
|
224
|
+
"Future launches use this login automatically. "
|
|
225
|
+
"Set PCODE_ANTHROPIC_AUTH=api-key to use ANTHROPIC_API_KEY instead."
|
|
226
|
+
)
|
|
227
|
+
except asyncio.CancelledError:
|
|
228
|
+
self.view.note("Anthropic sign-in cancelled.")
|
|
229
|
+
raise
|
|
230
|
+
except LoginError as error:
|
|
231
|
+
self.view.error(str(error))
|
|
232
|
+
except Exception:
|
|
233
|
+
self.view.error("Anthropic sign-in failed. No credential details were logged.")
|