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.
Files changed (163) hide show
  1. pcode/__init__.py +7 -0
  2. pcode/_job_supervisor.py +65 -0
  3. pcode/agent.py +592 -0
  4. pcode/anthropic_oauth.py +469 -0
  5. pcode/app.py +3165 -0
  6. pcode/appearance.py +181 -0
  7. pcode/aside.py +541 -0
  8. pcode/aside_guard.py +39 -0
  9. pcode/aside_ui.py +683 -0
  10. pcode/auth.py +113 -0
  11. pcode/block.py +39 -0
  12. pcode/browser.py +393 -0
  13. pcode/cache_diagnostics.py +336 -0
  14. pcode/cache_settings.py +58 -0
  15. pcode/cache_warnings.py +139 -0
  16. pcode/claude_sdk/__init__.py +96 -0
  17. pcode/claude_sdk/cli.py +52 -0
  18. pcode/claude_sdk/errors.py +61 -0
  19. pcode/claude_sdk/messages.py +154 -0
  20. pcode/claude_sdk/model.py +179 -0
  21. pcode/claude_sdk/resume.py +114 -0
  22. pcode/claude_sdk/session.py +547 -0
  23. pcode/claude_sdk/session_pool.py +251 -0
  24. pcode/claude_sdk/workspace.py +34 -0
  25. pcode/cli.py +73 -0
  26. pcode/clipboard.py +76 -0
  27. pcode/code_mode.py +43 -0
  28. pcode/codex_login.py +189 -0
  29. pcode/command_transcript.py +71 -0
  30. pcode/commands.py +145 -0
  31. pcode/compaction.py +457 -0
  32. pcode/completion.py +263 -0
  33. pcode/config.py +223 -0
  34. pcode/context_breakdown.py +178 -0
  35. pcode/context_usage.py +52 -0
  36. pcode/controller.py +3496 -0
  37. pcode/conversation_ids.py +27 -0
  38. pcode/conversation_tree.py +160 -0
  39. pcode/copy_ui.py +172 -0
  40. pcode/delegation.py +236 -0
  41. pcode/delta.py +295 -0
  42. pcode/diagnostics.py +226 -0
  43. pcode/edit_preview.py +158 -0
  44. pcode/edit_transcript.py +287 -0
  45. pcode/edit_ui.py +850 -0
  46. pcode/edits.py +162 -0
  47. pcode/email_remote/__init__.py +14 -0
  48. pcode/email_remote/cli.py +133 -0
  49. pcode/email_remote/listener.py +695 -0
  50. pcode/email_remote/mailbox.py +336 -0
  51. pcode/email_remote/outbound.py +75 -0
  52. pcode/email_remote/parsing.py +226 -0
  53. pcode/email_remote/render.py +175 -0
  54. pcode/email_remote/state.py +79 -0
  55. pcode/error_report.py +38 -0
  56. pcode/ext.py +602 -0
  57. pcode/extension_guide.md +311 -0
  58. pcode/extensions/browser.py +284 -0
  59. pcode/extensions/ponytail.py +217 -0
  60. pcode/extensions/sandbox.py +138 -0
  61. pcode/extensions/session_history.py +170 -0
  62. pcode/extensions/web_research.py +89 -0
  63. pcode/file_refs.py +265 -0
  64. pcode/filesystem.py +138 -0
  65. pcode/frame.py +149 -0
  66. pcode/gateway.py +465 -0
  67. pcode/git_diff.py +497 -0
  68. pcode/history.py +589 -0
  69. pcode/history_cursor.py +147 -0
  70. pcode/history_embeddings.py +102 -0
  71. pcode/host.py +941 -0
  72. pcode/host_protocol.py +291 -0
  73. pcode/host_ui.py +225 -0
  74. pcode/input_keys.py +35 -0
  75. pcode/inspection.py +289 -0
  76. pcode/inspector_ui.py +448 -0
  77. pcode/isolated_delegation.py +425 -0
  78. pcode/job_notices.py +54 -0
  79. pcode/jobs.py +584 -0
  80. pcode/jobs_ui.py +265 -0
  81. pcode/keymap.py +215 -0
  82. pcode/layout_speed.py +103 -0
  83. pcode/links.py +90 -0
  84. pcode/links_ui.py +200 -0
  85. pcode/live.py +1301 -0
  86. pcode/llm_proxy.py +44 -0
  87. pcode/logins.py +233 -0
  88. pcode/mcp.py +683 -0
  89. pcode/mcp_notice.py +91 -0
  90. pcode/mcp_oauth.py +381 -0
  91. pcode/meridian.py +233 -0
  92. pcode/meridian_process.py +337 -0
  93. pcode/meridian_reminders.py +107 -0
  94. pcode/meridian_setup.py +356 -0
  95. pcode/model_metadata.py +451 -0
  96. pcode/model_ui.py +168 -0
  97. pcode/models.py +209 -0
  98. pcode/native_results.py +119 -0
  99. pcode/oauth_pages.py +66 -0
  100. pcode/output_limits.py +41 -0
  101. pcode/paste.py +45 -0
  102. pcode/path_completion.py +66 -0
  103. pcode/plan_preview.py +131 -0
  104. pcode/plan_sizes.py +35 -0
  105. pcode/planning.py +161 -0
  106. pcode/popup_ui.py +873 -0
  107. pcode/preferences.py +1176 -0
  108. pcode/prefix_keys.py +490 -0
  109. pcode/profile_benchmark.py +332 -0
  110. pcode/profiling.py +566 -0
  111. pcode/project_trust.py +94 -0
  112. pcode/prompt_keys.py +286 -0
  113. pcode/remote.py +717 -0
  114. pcode/remote_print.py +125 -0
  115. pcode/remote_profile.py +237 -0
  116. pcode/replay_benchmark.py +319 -0
  117. pcode/repo_context.py +158 -0
  118. pcode/retries.py +28 -0
  119. pcode/rpc.py +280 -0
  120. pcode/runtime.py +244 -0
  121. pcode/sandbox.py +454 -0
  122. pcode/session_naming.py +82 -0
  123. pcode/session_ui.py +550 -0
  124. pcode/sessions.py +892 -0
  125. pcode/shell.py +113 -0
  126. pcode/shell_mode.py +172 -0
  127. pcode/shell_tools.py +459 -0
  128. pcode/skills.py +161 -0
  129. pcode/stall_log.py +187 -0
  130. pcode/startup_input.py +96 -0
  131. pcode/steering.py +27 -0
  132. pcode/stream_display.py +188 -0
  133. pcode/stream_events.py +437 -0
  134. pcode/strict_tools.py +130 -0
  135. pcode/syntax.py +43 -0
  136. pcode/syntax_colors.py +206 -0
  137. pcode/task_prompt.py +28 -0
  138. pcode/task_worktrees.py +482 -0
  139. pcode/terminal_notify.py +383 -0
  140. pcode/theme.py +135 -0
  141. pcode/theme_gallery.py +88 -0
  142. pcode/thinking_markdown.py +41 -0
  143. pcode/token_accounting.py +67 -0
  144. pcode/tool_display.py +784 -0
  145. pcode/tool_output_limits.py +108 -0
  146. pcode/tool_panel.py +377 -0
  147. pcode/transcript_log.py +127 -0
  148. pcode/transcript_notice.py +103 -0
  149. pcode/tree_ui.py +288 -0
  150. pcode/turn.py +44 -0
  151. pcode/ui.py +3745 -0
  152. pcode/usage.py +293 -0
  153. pcode/word_wrap.py +107 -0
  154. pcode/worker_ui.py +235 -0
  155. pcode/workers.py +136 -0
  156. pcode/workspace.py +57 -0
  157. pcode/workspace_filesystem.py +72 -0
  158. pcode/worktree.py +650 -0
  159. pcode-0.1.0.dist-info/METADATA +145 -0
  160. pcode-0.1.0.dist-info/RECORD +163 -0
  161. pcode-0.1.0.dist-info/WHEEL +4 -0
  162. pcode-0.1.0.dist-info/entry_points.txt +3 -0
  163. 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.")