zjcode 0.0.1__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. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. zjcode-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,559 @@
1
+ """The `dcode doctor` command: report install health and diagnostics.
2
+
3
+ Inspired by `claude doctor`, this prints a grouped, tree-style summary of the
4
+ running install, update status, and configuration locations so the output is
5
+ safe to paste into a bug report. It stays offline: the update section reads
6
+ only the local cache and never contacts PyPI.
7
+
8
+ Help rendering for `dcode doctor -h` is served by `ui.show_doctor_help`, which
9
+ does not import this module, so the help path stays light.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import platform
16
+ import sys
17
+ from dataclasses import dataclass, field
18
+ from typing import TYPE_CHECKING
19
+ from urllib.parse import urlsplit
20
+
21
+ from deepagents_code.output import write_json
22
+
23
+ if TYPE_CHECKING:
24
+ import argparse
25
+
26
+ from deepagents_code.config import TracingStatus
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ @dataclass
32
+ class DiagnosticItem:
33
+ """A single labeled diagnostic fact.
34
+
35
+ `ok` is `False` only for genuine problems (e.g. a missing dependency), not
36
+ for informational states such as an available update.
37
+ """
38
+
39
+ label: str
40
+ value: str
41
+ ok: bool = True
42
+
43
+
44
+ @dataclass
45
+ class DiagnosticSection:
46
+ """A named group of related diagnostic items."""
47
+
48
+ title: str
49
+ items: list[DiagnosticItem] = field(default_factory=list)
50
+
51
+ @property
52
+ def ok(self) -> bool:
53
+ """Whether every item in the section is healthy."""
54
+ return all(item.ok for item in self.items)
55
+
56
+
57
+ def _platform_tag() -> str:
58
+ """Return a compact `<os>-<arch>` platform tag (e.g. `darwin-arm64`)."""
59
+ return f"{platform.system()}-{platform.machine()}".lower()
60
+
61
+
62
+ def _sdk_version() -> tuple[str, bool]:
63
+ """Return the installed `deepagents` SDK version and whether it resolved.
64
+
65
+ Maps the shared `resolve_sdk_version` outcome onto doctor's display: a
66
+ genuinely missing package reads as `not installed`, an unexpected lookup
67
+ failure as `unknown`, and only a resolved version is marked healthy.
68
+ """
69
+ from deepagents_code.extras_info import resolve_sdk_version
70
+
71
+ sdk_version, status = resolve_sdk_version()
72
+ if status == "resolved":
73
+ # `sdk_version` is a real string when the status is "resolved".
74
+ return sdk_version or "unknown", True
75
+ if status == "not_installed":
76
+ return "not installed", False
77
+ return "unknown", False
78
+
79
+
80
+ def _build_commit() -> str | None:
81
+ """Return the commit stamped into the package at build time, if present.
82
+
83
+ Released wheels carry a generated `_build_info.py` (see `hatch_build.py`);
84
+ editable and local installs do not, so this returns `None` for them.
85
+ """
86
+ try:
87
+ from deepagents_code._build_info import ( # ty: ignore[unresolved-import] # generated at build time
88
+ BUILD_COMMIT,
89
+ )
90
+ except ImportError:
91
+ return None
92
+ except Exception: # a corrupt stamp must never crash `doctor`
93
+ logger.debug("Build-info module present but failed to import", exc_info=True)
94
+ return None
95
+ commit = (BUILD_COMMIT or "").strip()
96
+ return commit or None
97
+
98
+
99
+ def _commit_hash(path: str) -> str:
100
+ """Return the short git commit hash for the install, if available.
101
+
102
+ Prefers the commit stamped into a released wheel at build time, but only for
103
+ non-editable installs: an editable install may carry a stale stamp from a
104
+ prior local build (the generated file is gitignored and survives a failed
105
+ build), so it always probes the live git working tree, which reflects local
106
+ changes.
107
+
108
+ Args:
109
+ path: Directory used as the git command working directory.
110
+
111
+ Returns:
112
+ The short commit hash, or `unknown` when no commit can be determined.
113
+ """
114
+ baked = _build_commit()
115
+ if baked:
116
+ from deepagents_code.config import _is_editable_install
117
+
118
+ # A baked commit only describes a built wheel; ignore it for editable
119
+ # installs so a stale stamp can't mask the live working-tree commit.
120
+ if not _is_editable_install():
121
+ return baked
122
+
123
+ import shutil
124
+ import subprocess # noqa: S404 # fixed-argv git metadata probe
125
+ from pathlib import Path
126
+
127
+ cwd = Path(path).expanduser()
128
+ git = shutil.which("git")
129
+ if git is None:
130
+ return "unknown"
131
+ try:
132
+ git_path = Path(git).expanduser().resolve(strict=True)
133
+ result = subprocess.run( # noqa: S603 # fixed argv with absolute Git path
134
+ [str(git_path), "rev-parse", "--short", "HEAD"],
135
+ capture_output=True,
136
+ text=True,
137
+ timeout=2,
138
+ check=False,
139
+ cwd=cwd,
140
+ )
141
+ except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
142
+ logger.debug("Git commit hash detection failed", exc_info=True)
143
+ return "unknown"
144
+ if result.returncode != 0:
145
+ return "unknown"
146
+ return result.stdout.strip() or "unknown"
147
+
148
+
149
+ def _collect_diagnostics() -> DiagnosticSection:
150
+ """Collect core version, platform, and install-location facts.
151
+
152
+ Returns:
153
+ The `Diagnostics` section.
154
+ """
155
+ from deepagents_code._version import __version__
156
+ from deepagents_code.config import (
157
+ _get_editable_install_path,
158
+ _is_editable_install,
159
+ )
160
+ from deepagents_code.update_check import detect_install_method
161
+
162
+ sdk_version, sdk_ok = _sdk_version()
163
+ editable = _is_editable_install()
164
+ if editable:
165
+ method = "editable"
166
+ path = _get_editable_install_path() or sys.prefix
167
+ else:
168
+ method = detect_install_method()
169
+ path = sys.prefix
170
+
171
+ return DiagnosticSection(
172
+ title="Diagnostics",
173
+ items=[
174
+ DiagnosticItem("deepagents-code", __version__),
175
+ DiagnosticItem("deepagents (SDK)", sdk_version, ok=sdk_ok),
176
+ DiagnosticItem("Commit hash", _commit_hash(path)),
177
+ DiagnosticItem("Python", platform.python_version()),
178
+ DiagnosticItem("Platform", _platform_tag()),
179
+ DiagnosticItem("Install method", method),
180
+ DiagnosticItem("Path", path),
181
+ ],
182
+ )
183
+
184
+
185
+ def _collect_updates() -> DiagnosticSection:
186
+ """Collect update-channel status from local config and the offline cache.
187
+
188
+ Returns:
189
+ The `Updates` section.
190
+ """
191
+ from deepagents_code.config import _is_editable_install
192
+ from deepagents_code.update_check import (
193
+ get_cached_update_available,
194
+ is_auto_update_enabled,
195
+ is_update_check_enabled,
196
+ )
197
+
198
+ items = [
199
+ DiagnosticItem(
200
+ "Update checks",
201
+ "enabled" if is_update_check_enabled() else "disabled",
202
+ ),
203
+ ]
204
+ if _is_editable_install():
205
+ items.append(DiagnosticItem("Auto-updates", "disabled (editable install)"))
206
+ else:
207
+ items.append(
208
+ DiagnosticItem(
209
+ "Auto-updates",
210
+ "enabled" if is_auto_update_enabled() else "disabled",
211
+ )
212
+ )
213
+
214
+ available, latest = get_cached_update_available()
215
+ if latest is None:
216
+ update_status = "unknown (no recent check)"
217
+ elif available:
218
+ update_status = f"v{latest} available"
219
+ else:
220
+ update_status = "up to date"
221
+ items.extend(
222
+ (
223
+ DiagnosticItem("Latest version", update_status),
224
+ DiagnosticItem("Last checked", _format_last_checked()),
225
+ )
226
+ )
227
+
228
+ return DiagnosticSection(title="Updates", items=items)
229
+
230
+
231
+ def _format_last_checked() -> str:
232
+ """Return a relative description of the last update check, or `never`.
233
+
234
+ `never` covers both the no-check-recorded case and, defensively, a stamp
235
+ that cannot be formatted. `get_last_update_check_time` only returns finite,
236
+ in-range epochs, so the formatting path does not raise here.
237
+ """
238
+ from datetime import UTC, datetime
239
+
240
+ from deepagents_code.sessions import format_relative_timestamp
241
+ from deepagents_code.update_check import get_last_update_check_time
242
+
243
+ checked_at = get_last_update_check_time()
244
+ if checked_at is None:
245
+ return "never"
246
+ iso = datetime.fromtimestamp(checked_at, tz=UTC).isoformat()
247
+ return format_relative_timestamp(iso) or "never"
248
+
249
+
250
+ def _sanitize_endpoint(endpoint: str) -> str:
251
+ """Return a paste-safe custom endpoint identifier.
252
+
253
+ Args:
254
+ endpoint: Configured endpoint URL.
255
+
256
+ Returns:
257
+ The endpoint origin when parseable, otherwise a generic configured
258
+ marker that does not include user-controlled URL contents.
259
+ """
260
+ parsed = urlsplit(endpoint.strip())
261
+ if not parsed.scheme or not parsed.hostname:
262
+ return "(custom endpoint configured)"
263
+
264
+ host = parsed.hostname
265
+ if ":" in host and not host.startswith("["):
266
+ host = f"[{host}]"
267
+
268
+ try:
269
+ port = parsed.port
270
+ except ValueError:
271
+ port = None
272
+
273
+ netloc = f"{host}:{port}" if port is not None else host
274
+ return f"{parsed.scheme}://{netloc}"
275
+
276
+
277
+ _LANGSMITH_GATEWAY_HOST = "smith.langchain.com"
278
+ """Host identifying LangSmith's managed (SaaS) tracing gateway.
279
+
280
+ Traces sent to an endpoint whose host is `smith.langchain.com` (or a subdomain
281
+ of it) route through the managed gateway; any other host is a self-hosted or
282
+ dev/staging target. `app.py` keeps the same constant for its model-gateway
283
+ key-mismatch check, but matches a raw-URL substring; this module matches the
284
+ parsed hostname exactly or by subdomain suffix so lookalike hosts such as
285
+ `smith.langchain.com.evil.example` are not treated as the gateway.
286
+ """
287
+
288
+
289
+ def _endpoint_gateway_state(endpoint: str) -> str:
290
+ """Classify a single tracing endpoint as gateway, non-gateway, or unknown.
291
+
292
+ Args:
293
+ endpoint: A configured tracing endpoint URL.
294
+
295
+ Returns:
296
+ `"yes"` when the endpoint's host is the LangSmith managed gateway (an
297
+ exact host or a subdomain), `"no"` for any other resolvable host,
298
+ and `"unknown"` when the endpoint cannot be parsed into a host — so
299
+ a typo'd or malformed URL is never silently reported as `"no"`.
300
+ """
301
+ try:
302
+ host = urlsplit(endpoint.strip()).hostname or ""
303
+ except ValueError:
304
+ # urlsplit raises on bracket-malformed IPv6 (e.g. `http://[::1`); a
305
+ # diagnostic must degrade to "unknown" rather than crash `dcode doctor`.
306
+ return "unknown"
307
+ if not host:
308
+ return "unknown"
309
+ if host == _LANGSMITH_GATEWAY_HOST or host.endswith(f".{_LANGSMITH_GATEWAY_HOST}"):
310
+ return "yes"
311
+ return "no"
312
+
313
+
314
+ def _tracing_gateway_state(status: TracingStatus) -> str:
315
+ """Report whether all trace ingestion targets are the managed gateway.
316
+
317
+ Considers both the primary endpoint and any replica ingestion URLs
318
+ (`LANGSMITH_RUNS_ENDPOINTS`), since a self-hosted replica means traces leave
319
+ for a custom target even when the primary endpoint is unset. With no target
320
+ configured, tracing falls back to the LangSmith SDK default
321
+ (`https://api.smith.langchain.com`), which is the managed gateway.
322
+
323
+ Args:
324
+ status: The resolved tracing status.
325
+
326
+ Returns:
327
+ `"yes"` when every configured target is the managed gateway (or none is
328
+ configured, i.e. the SDK default), `"no"` when any target is a
329
+ self-hosted or dev/staging host, and `"unknown"` when a target
330
+ cannot be parsed and none is a definite non-gateway host.
331
+ """
332
+ states = [
333
+ _endpoint_gateway_state(target)
334
+ for target in (status.endpoint, *status.runs_endpoints)
335
+ if target
336
+ ]
337
+ if not states:
338
+ return "yes"
339
+ if "no" in states:
340
+ return "no"
341
+ if "unknown" in states:
342
+ return "unknown"
343
+ return "yes"
344
+
345
+
346
+ def _format_tracing_project(status: TracingStatus) -> str:
347
+ """Render the tracing project, marking the unconfigured default.
348
+
349
+ Returns:
350
+ The project name with a `(default)` suffix when it is the built-in
351
+ fallback rather than an explicit setting, or `(unset)` when absent.
352
+ """
353
+ if not status.project:
354
+ return "(unset)"
355
+ if status.project_is_default:
356
+ return f"{status.project} (default)"
357
+ return status.project
358
+
359
+
360
+ def _collect_tracing() -> DiagnosticSection:
361
+ """Collect LangSmith tracing status from env and profile (offline).
362
+
363
+ Tracing reads `enabled` when a flag is truthy, `disabled` only when a flag
364
+ is explicitly set to a falsy value, and `not configured` when no flag is set.
365
+ Credentials are reported as configured/not set only — the API key
366
+ value is never read or printed. The `Credentials` item is flagged as a
367
+ problem only when tracing is enabled without a key and without a custom
368
+ endpoint, mirroring the runtime's orphaned-tracing guard (a keyless
369
+ self-hosted endpoint is a valid, healthy setup). When tracing is enabled, a
370
+ `Gateway` item reports whether traces route through LangSmith's managed
371
+ (SaaS) gateway (`yes`), a custom self-hosted/dev/staging endpoint (`no`), or
372
+ an endpoint that could not be parsed (`unknown`), accounting for both the
373
+ primary endpoint and any replica ingestion targets.
374
+
375
+ Returns:
376
+ The `Tracing` section.
377
+ """
378
+ from deepagents_code.config import get_tracing_status
379
+
380
+ status = get_tracing_status()
381
+ creds_required = status.enabled and status.endpoint is None
382
+ if status.enabled:
383
+ tracing_value = "enabled"
384
+ elif status.explicitly_disabled:
385
+ tracing_value = "disabled"
386
+ else:
387
+ tracing_value = "not configured"
388
+ items = [
389
+ DiagnosticItem("Tracing", tracing_value),
390
+ DiagnosticItem(
391
+ "Credentials",
392
+ "configured" if status.has_credentials else "not set",
393
+ ok=status.has_credentials or not creds_required,
394
+ ),
395
+ DiagnosticItem("Project", _format_tracing_project(status)),
396
+ ]
397
+ if status.endpoint:
398
+ items.append(DiagnosticItem("Endpoint", _sanitize_endpoint(status.endpoint)))
399
+ if status.enabled:
400
+ items.append(DiagnosticItem("Gateway", _tracing_gateway_state(status)))
401
+ if status.replica_project:
402
+ items.append(DiagnosticItem("Replica project", status.replica_project))
403
+ return DiagnosticSection(title="Tracing", items=items)
404
+
405
+
406
+ def _path_status(label: str, path: object) -> DiagnosticItem:
407
+ """Build an item reporting a path and whether it exists on disk.
408
+
409
+ An unreadable path (e.g. a parent directory that denies traversal) is
410
+ flagged as a genuine problem (`ok=False`) so it surfaces in the section
411
+ health and exit code, rather than being mistaken for a not-yet-created one.
412
+
413
+ Args:
414
+ label: Human-readable name for the path.
415
+ path: Filesystem path to probe.
416
+
417
+ Returns:
418
+ A diagnostic item describing the path and its existence.
419
+ """
420
+ from pathlib import Path
421
+
422
+ from deepagents_code._paths import PathState, classify_path
423
+
424
+ resolved = Path(str(path))
425
+ state = classify_path(resolved)
426
+ suffix = {
427
+ PathState.EXISTS: "exists",
428
+ PathState.MISSING: "not created",
429
+ PathState.UNREADABLE: "unreadable",
430
+ }[state]
431
+ return DiagnosticItem(
432
+ label, f"{resolved} ({suffix})", ok=state is not PathState.UNREADABLE
433
+ )
434
+
435
+
436
+ def _collect_configuration() -> DiagnosticSection:
437
+ """Collect on-disk configuration and data locations.
438
+
439
+ Returns:
440
+ The `Configuration` section.
441
+ """
442
+ from deepagents_code.model_config import (
443
+ DEFAULT_CONFIG_DIR,
444
+ DEFAULT_CONFIG_PATH,
445
+ )
446
+
447
+ return DiagnosticSection(
448
+ title="Configuration",
449
+ items=[
450
+ _path_status("Data directory", DEFAULT_CONFIG_DIR),
451
+ _path_status("Config file", DEFAULT_CONFIG_PATH),
452
+ ],
453
+ )
454
+
455
+
456
+ def collect_sections() -> list[DiagnosticSection]:
457
+ """Gather every diagnostic section in display order.
458
+
459
+ Returns:
460
+ The diagnostic sections, in render order.
461
+ """
462
+ return [
463
+ _collect_diagnostics(),
464
+ _collect_updates(),
465
+ _collect_tracing(),
466
+ _collect_configuration(),
467
+ ]
468
+
469
+
470
+ def _tree_connectors() -> tuple[str, str]:
471
+ """Return the `(tee, corner)` tree connectors for the active charset."""
472
+ from deepagents_code.config import is_ascii_mode
473
+
474
+ if is_ascii_mode():
475
+ return "|-", "`-"
476
+ return "\u251c", "\u2514" # ├ └
477
+
478
+
479
+ def _render_text(sections: list[DiagnosticSection]) -> None:
480
+ """Print the diagnostic sections as a styled tree to the console."""
481
+ from rich.markup import escape
482
+
483
+ from deepagents_code import theme
484
+ from deepagents_code.config import console, get_glyphs
485
+
486
+ glyphs = get_glyphs()
487
+ tee, corner = _tree_connectors()
488
+
489
+ console.print()
490
+ for section in sections:
491
+ status_glyph = glyphs.checkmark if section.ok else glyphs.warning
492
+ status_color = theme.SUCCESS if section.ok else theme.WARNING
493
+ console.print(
494
+ f" [bold]{escape(section.title)}[/bold] "
495
+ f"[{status_color}]{status_glyph}[/{status_color}]"
496
+ )
497
+ for index, item in enumerate(section.items):
498
+ connector = corner if index == len(section.items) - 1 else tee
499
+ value_color = theme.MUTED if item.ok else "red"
500
+ console.print(
501
+ f" {connector} {escape(item.label)}: "
502
+ f"[{value_color}]{escape(item.value)}[/{value_color}]",
503
+ highlight=False,
504
+ )
505
+ console.print()
506
+
507
+ console.print(
508
+ " Tip: Run `dcode config show` or `dcode config get <key>` "
509
+ "to drill into config details.",
510
+ style=theme.MUTED,
511
+ highlight=False,
512
+ )
513
+ console.print(
514
+ " Run `dcode --version` (or `dcode -v`) for dependency versions.",
515
+ style=theme.MUTED,
516
+ highlight=False,
517
+ )
518
+ console.print()
519
+
520
+
521
+ def run_doctor_command(args: argparse.Namespace) -> int:
522
+ """Run `dcode doctor`, printing diagnostics as text or JSON.
523
+
524
+ Args:
525
+ args: Parsed CLI namespace. Only `output_format` is read.
526
+
527
+ Returns:
528
+ Process exit code: `0` when all sections are healthy, `1` otherwise.
529
+ """
530
+ sections = collect_sections()
531
+ healthy = all(section.ok for section in sections)
532
+ output_format = getattr(args, "output_format", "text")
533
+
534
+ if output_format == "json":
535
+ write_json(
536
+ "doctor",
537
+ {
538
+ "healthy": healthy,
539
+ "sections": [
540
+ {
541
+ "title": section.title,
542
+ "ok": section.ok,
543
+ "items": [
544
+ {
545
+ "label": item.label,
546
+ "value": item.value,
547
+ "ok": item.ok,
548
+ }
549
+ for item in section.items
550
+ ],
551
+ }
552
+ for section in sections
553
+ ],
554
+ },
555
+ )
556
+ else:
557
+ _render_text(sections)
558
+
559
+ return 0 if healthy else 1
@@ -0,0 +1,142 @@
1
+ """External editor support for composing prompts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import logging
7
+ import os
8
+ import shlex
9
+ import subprocess # noqa: S404
10
+ import sys
11
+ import tempfile
12
+ from pathlib import Path
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ GUI_WAIT_FLAG: dict[str, str] = {
17
+ "code": "--wait",
18
+ "cursor": "--wait",
19
+ "zed": "--wait",
20
+ "atom": "--wait",
21
+ "subl": "-w",
22
+ "windsurf": "--wait",
23
+ }
24
+ """Mapping of GUI editor base names to their blocking flag."""
25
+
26
+ VIM_EDITORS = {"vi", "vim", "nvim"}
27
+ """Set of vim-family editor base names that receive the `-i NONE` flag."""
28
+
29
+
30
+ def resolve_editor() -> list[str] | None:
31
+ """Resolve editor command from environment.
32
+
33
+ Checks $VISUAL, then $EDITOR, then falls back to platform default.
34
+
35
+ Returns:
36
+ Tokenized command list, or `None` if the env var was set but empty after
37
+ tokenization.
38
+ """
39
+ editor = os.environ.get("VISUAL") or os.environ.get("EDITOR")
40
+ if not editor:
41
+ if sys.platform == "win32":
42
+ return ["notepad"]
43
+ return ["vi"]
44
+ tokens = shlex.split(editor)
45
+ return tokens or None
46
+
47
+
48
+ def _prepare_command(cmd: list[str], filepath: str) -> list[str]:
49
+ """Build the full command list with appropriate flags.
50
+
51
+ Adds --wait/-w for GUI editors and `-i NONE` for vim-family editors.
52
+
53
+ Returns:
54
+ The complete command list with flags and filepath appended.
55
+ """
56
+ cmd = list(cmd) # copy
57
+ exe = Path(cmd[0]).stem.lower()
58
+
59
+ # Auto-inject wait flag for GUI editors
60
+ if exe in GUI_WAIT_FLAG:
61
+ flag = GUI_WAIT_FLAG[exe]
62
+ if flag not in cmd:
63
+ cmd.insert(1, flag)
64
+
65
+ # Vim workaround: avoid viminfo errors in temp environments
66
+ if exe in VIM_EDITORS and "-i" not in cmd:
67
+ cmd.extend(["-i", "NONE"])
68
+
69
+ cmd.append(filepath)
70
+ return cmd
71
+
72
+
73
+ def open_in_editor(current_text: str) -> str | None:
74
+ """Open current_text in an external editor.
75
+
76
+ Creates a temp .md file, launches the editor, and reads back the result.
77
+
78
+ Args:
79
+ current_text: The text to pre-populate in the editor.
80
+
81
+ Returns:
82
+ The edited text with normalized line endings, or `None` if the editor
83
+ exited with a non-zero status, was not found, or the result was
84
+ empty/whitespace-only.
85
+ """
86
+ cmd = resolve_editor()
87
+ if cmd is None:
88
+ return None
89
+
90
+ tmp_path: str | None = None
91
+ try:
92
+ with tempfile.NamedTemporaryFile(
93
+ suffix=".md",
94
+ prefix="deepagents-edit-",
95
+ delete=False,
96
+ mode="w",
97
+ encoding="utf-8",
98
+ ) as tmp:
99
+ tmp_path = tmp.name
100
+ tmp.write(current_text)
101
+
102
+ full_cmd = _prepare_command(cmd, tmp_path)
103
+
104
+ # S603: editor command comes from user's own $EDITOR env var
105
+ result = subprocess.run( # noqa: S603
106
+ full_cmd,
107
+ stdin=sys.stdin,
108
+ stdout=sys.stdout,
109
+ stderr=sys.stderr,
110
+ check=False,
111
+ )
112
+ if result.returncode != 0:
113
+ logger.warning(
114
+ "Editor exited with code %d: %s", result.returncode, full_cmd
115
+ )
116
+ return None
117
+
118
+ edited = Path(tmp_path).read_text(encoding="utf-8")
119
+
120
+ # Normalize line endings
121
+ edited = edited.replace("\r\n", "\n").replace("\r", "\n")
122
+
123
+ # Most editors append a final newline on save (POSIX convention).
124
+ # Strip exactly one so the cursor lands on content, not a blank line,
125
+ # while preserving any intentional trailing newlines the user added.
126
+ edited = edited.removesuffix("\n")
127
+
128
+ # Treat empty result as cancellation
129
+ if not edited.strip():
130
+ return None
131
+
132
+ except FileNotFoundError:
133
+ return None
134
+ except Exception:
135
+ logger.warning("Editor failed", exc_info=True)
136
+ return None
137
+ else:
138
+ return edited
139
+ finally:
140
+ if tmp_path is not None:
141
+ with contextlib.suppress(OSError):
142
+ Path(tmp_path).unlink(missing_ok=True)