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.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- 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)
|