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,250 @@
1
+ """CLI commands of the MCP module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ import argparse
10
+ from collections.abc import Callable
11
+
12
+ from deepagents_code.mcp_login_service import ConfigResolutionError
13
+
14
+
15
+ def _lazy_ui_help(fn_name: str) -> Callable[[], None]:
16
+ """Return a callable that lazily imports and invokes a `ui` help function."""
17
+
18
+ def _show() -> None:
19
+ from deepagents_code import ui
20
+
21
+ getattr(ui, fn_name)()
22
+
23
+ return _show
24
+
25
+
26
+ def setup_mcp_parsers(
27
+ subparsers: Any, # noqa: ANN401
28
+ *,
29
+ make_help_action: Callable[[Callable[[], None]], type[argparse.Action]],
30
+ ) -> None:
31
+ """Register the `dcode mcp` command group.
32
+
33
+ Args:
34
+ subparsers: The `argparse` subparsers object from the top-level CLI
35
+ parser, onto which the `mcp` command group is attached.
36
+ make_help_action: Factory that wraps a `show_*` callable into an
37
+ `argparse.Action` so `-h/--help` renders the hand-maintained
38
+ help screens from `deepagents_code.ui` instead of argparse's
39
+ auto-generated text.
40
+ """
41
+ mcp_parser = subparsers.add_parser(
42
+ "mcp",
43
+ help="Manage MCP servers",
44
+ add_help=False,
45
+ )
46
+ mcp_parser.add_argument(
47
+ "-h",
48
+ "--help",
49
+ action=make_help_action(_lazy_ui_help("show_mcp_help")),
50
+ )
51
+ mcp_sub = mcp_parser.add_subparsers(dest="mcp_command")
52
+
53
+ login_parser = mcp_sub.add_parser(
54
+ "login",
55
+ help="Run the OAuth login flow for an MCP server",
56
+ add_help=False,
57
+ )
58
+ login_parser.add_argument("server", help="Server name from mcpServers config")
59
+ login_parser.add_argument(
60
+ "--mcp-config",
61
+ dest="config_path",
62
+ default=None,
63
+ help="Path to an MCP config JSON file. Falls back to the top-level "
64
+ "`--mcp-config`, then to auto-discovered configs.",
65
+ )
66
+ login_parser.add_argument(
67
+ "-h",
68
+ "--help",
69
+ action=make_help_action(_lazy_ui_help("show_mcp_login_help")),
70
+ )
71
+
72
+ config_parser = mcp_sub.add_parser(
73
+ "config",
74
+ help="Show MCP config discovery paths",
75
+ add_help=False,
76
+ )
77
+ config_parser.add_argument(
78
+ "-h",
79
+ "--help",
80
+ action=make_help_action(_lazy_ui_help("show_mcp_config_help")),
81
+ )
82
+
83
+
84
+ # Maintainer note: `deepagents-talon` dynamically imports `run_mcp_login` from
85
+ # this module for its `talon mcp login` command. Keep the function name,
86
+ # keyword-only signature, async behavior, and integer exit-code contract stable
87
+ # unless `deepagents-talon` is migrated in the same change.
88
+ async def run_mcp_login(*, server: str, config_path: str | None) -> int:
89
+ """Handle `dcode mcp login <server>`.
90
+
91
+ When `config_path` is omitted, auto-discovered MCP configs are merged in
92
+ the same precedence order as the runtime loader, with matching trust
93
+ gating: user-level configs are always included, but project-level configs
94
+ are only included when the trust store has a fingerprint match. An
95
+ untrusted project-level config (for example, a `.mcp.json` in a cloned
96
+ repo) is skipped so attacker-controlled `headers` entries cannot exfiltrate
97
+ local secrets during the OAuth handshake. When `config_path` is set, that
98
+ file alone is loaded and treated as explicitly trusted.
99
+
100
+ Args:
101
+ server: Target server name from `mcpServers`.
102
+ config_path: Optional explicit MCP config path.
103
+
104
+ Returns:
105
+ Process exit code: 0 on success, 1 on config or login failure,
106
+ 2 if no config file could be found.
107
+ """
108
+ from deepagents_code.mcp_auth import login
109
+ from deepagents_code.mcp_login_service import (
110
+ ConfigErrorKind,
111
+ ConfigResolution,
112
+ ConfigResolutionError,
113
+ format_untrusted_project_notice,
114
+ resolve_mcp_config,
115
+ select_server,
116
+ )
117
+ from deepagents_code.mcp_oauth_ui import CliOAuthInteraction
118
+
119
+ resolution = resolve_mcp_config(config_path)
120
+ if isinstance(resolution, ConfigResolutionError):
121
+ _print_resolution_error(resolution)
122
+ return 2 if resolution.kind is ConfigErrorKind.NO_CONFIG_FOUND else 1
123
+
124
+ if not isinstance(resolution, ConfigResolution): # pragma: no cover - safety
125
+ print( # noqa: T201
126
+ "Internal error: unexpected result from resolve_mcp_config. "
127
+ "Please report this bug.",
128
+ file=sys.stderr,
129
+ )
130
+ return 1
131
+
132
+ notice = format_untrusted_project_notice(resolution.untrusted_project_paths)
133
+ if notice:
134
+ print(notice, file=sys.stderr) # noqa: T201
135
+
136
+ selection = select_server(resolution, server)
137
+ if isinstance(selection, ConfigResolutionError):
138
+ print(selection.message, file=sys.stderr) # noqa: T201
139
+ return 1
140
+
141
+ import httpx
142
+ from pydantic import ValidationError
143
+
144
+ from deepagents_code.mcp_auth import format_login_failure
145
+
146
+ try:
147
+ await login(
148
+ server_name=selection.server_name,
149
+ server_config=selection.server_config,
150
+ ui=CliOAuthInteraction(),
151
+ )
152
+ except PermissionError as exc:
153
+ # PermissionError typically means the user's home dir or the
154
+ # ~/.deepagents/.state/mcp-tokens/ tree isn't writable. Retrying
155
+ # without a hint sends users in circles.
156
+ print( # noqa: T201
157
+ f"Login failed: cannot write to the MCP tokens store ({exc}). "
158
+ f"Check permissions on ~/.deepagents/.state/mcp-tokens/ and "
159
+ f"retry `dcode mcp login {selection.server_name}`.",
160
+ file=sys.stderr,
161
+ )
162
+ return 1
163
+ except (
164
+ ValueError,
165
+ RuntimeError,
166
+ httpx.HTTPError,
167
+ ValidationError,
168
+ KeyError,
169
+ OSError,
170
+ ) as exc:
171
+ print( # noqa: T201
172
+ f"Login failed: {format_login_failure(exc)}",
173
+ file=sys.stderr,
174
+ )
175
+ return 1
176
+ return 0
177
+
178
+
179
+ def run_mcp_config() -> int:
180
+ """Handle `dcode mcp config`.
181
+
182
+ Prints the MCP config discovery paths in precedence order with a
183
+ marker showing which exist on disk. Stat-only; never opens config
184
+ files, so config-trust prompts are not triggered.
185
+
186
+ Returns:
187
+ Process exit code: always 0.
188
+ """
189
+ from pathlib import Path
190
+
191
+ from deepagents_code.mcp_tools import (
192
+ _resolve_project_config_base,
193
+ discover_mcp_configs,
194
+ )
195
+ from deepagents_code.ui import console
196
+
197
+ found = {str(p.resolve()) for p in discover_mcp_configs()}
198
+ user_dir = Path.home() / ".zjcode"
199
+ project_root = _resolve_project_config_base(None)
200
+
201
+ rows: list[tuple[str, str, bool]] = []
202
+ for display, label, resolved in (
203
+ ("~/.zjcode/.mcp.json", "user-level", user_dir / ".mcp.json"),
204
+ (
205
+ "<project-root>/.deepagents/.mcp.json",
206
+ "project subdir",
207
+ project_root / ".deepagents" / ".mcp.json",
208
+ ),
209
+ ("<project-root>/.mcp.json", "project root", project_root / ".mcp.json"),
210
+ ):
211
+ exists = str(resolved.resolve()) in found or resolved.is_file()
212
+ rows.append((display, label, exists))
213
+
214
+ width = max(len(p) for p, _, _ in rows)
215
+ console.print(
216
+ "MCP config discovery paths (lowest to highest precedence):",
217
+ highlight=False,
218
+ )
219
+ for display, label, exists in rows:
220
+ marker = "found" if exists else "missing"
221
+ console.print(
222
+ f" [{marker:>7}] {display:<{width}} ({label})",
223
+ highlight=False,
224
+ markup=False,
225
+ )
226
+ console.print()
227
+ console.print(
228
+ "<project-root> = nearest ancestor with `.git`, else current directory.",
229
+ highlight=False,
230
+ )
231
+ console.print(
232
+ "Override via `--mcp-config <path>` at the top level or on "
233
+ "`dcode mcp login <server>`.",
234
+ highlight=False,
235
+ )
236
+ return 0
237
+
238
+
239
+ def _print_resolution_error(error: ConfigResolutionError) -> None:
240
+ """Print the untrusted-paths notice (if any) then `error.message` to stderr.
241
+
242
+ Note: the untrusted-paths notice is also surfaced independently for
243
+ successful resolutions in `run_mcp_login`.
244
+ """
245
+ from deepagents_code.mcp_login_service import format_untrusted_project_notice
246
+
247
+ notice = format_untrusted_project_notice(error.untrusted_project_paths)
248
+ if notice:
249
+ print(notice, file=sys.stderr) # noqa: T201
250
+ print(error.message, file=sys.stderr) # noqa: T201
@@ -0,0 +1,416 @@
1
+ """The `dcode tools` command group: provision managed external tools.
2
+
3
+ `dcode tools install` fetches the pinned, SHA-256-verified ripgrep binary into
4
+ `~/.deepagents/bin` (the same managed path used on first run) and is also handy
5
+ for repairing a missing or stale `rg`. The install script calls this verb
6
+ instead of re-encoding the pinned version + checksum table in bash.
7
+
8
+ `dcode tools list` prints the tools available to the agent, grouped by source
9
+ (built-in tools, then per-server MCP tools), enumerated from the real tool
10
+ objects the agent binds so the output never drifts from what the model sees.
11
+
12
+ Help rendering for `dcode tools -h` / `dcode tools install -h` /
13
+ `dcode tools list -h` is served by `ui.show_tools_help` /
14
+ `ui.show_tools_install_help` / `ui.show_tools_list_help`, which do not import
15
+ this module, so the help path stays light.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import logging
22
+ from typing import TYPE_CHECKING, Literal
23
+
24
+ from deepagents_code.output import write_json
25
+
26
+ if TYPE_CHECKING:
27
+ import argparse
28
+
29
+ from deepagents_code.output import OutputFormat
30
+ from deepagents_code.tool_catalog import ToolCatalog, UnavailableServer
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ InstallStatus = Literal["ok", "skipped", "error"]
35
+ """Stable machine token for the `tools install` outcome, surfaced via `--json`.
36
+
37
+ `ok` (installed or already current), `skipped` (intentional opt-out), and
38
+ `error` (an install was expected but failed). Only `error` is unhealthy, so it
39
+ alone drives a non-zero exit code."""
40
+
41
+
42
+ def run_tools_command(args: argparse.Namespace) -> int:
43
+ """Dispatch a `dcode tools` subcommand.
44
+
45
+ Args:
46
+ args: Parsed CLI namespace.
47
+
48
+ Returns:
49
+ Process exit code.
50
+ """
51
+ subcommand = getattr(args, "tools_command", None)
52
+ if subcommand == "install":
53
+ return _run_tools_install(args)
54
+ if subcommand == "list":
55
+ return _run_tools_list(args)
56
+
57
+ # `cli_main`'s bare-group help fast path handles `dcode tools` with no
58
+ # subcommand, so this is only reached for an unexpected value.
59
+ from deepagents_code import ui
60
+
61
+ ui.show_tools_help()
62
+ return 0
63
+
64
+
65
+ def _run_tools_list(args: argparse.Namespace) -> int:
66
+ """List the tools available to the agent, grouped by source.
67
+
68
+ Enumerates the real tool objects the agent binds (see
69
+ `tool_catalog.collect_catalog`) so names and descriptions never drift from
70
+ what the model sees. The same runtime options that shape the agent's tool
71
+ set are honored: the resolved interpreter setting controls whether `js_eval`
72
+ is listed, and the MCP options (`--no-mcp`, `--mcp-config`,
73
+ `--trust-project-mcp`) control MCP discovery. Those are top-level flags, so
74
+ they must precede the subcommand (e.g. `dcode --no-mcp tools list`).
75
+
76
+ MCP discovery is best-effort: the built-in tools always render. Servers that
77
+ errored, need login, or are disabled are still reported (not hidden) so a
78
+ user debugging a missing tool can see why it is absent. When discovery fails
79
+ outright while an explicit `--mcp-config` was supplied, the command exits
80
+ non-zero because the user's explicit request could not be satisfied.
81
+
82
+ The exit code is not a complete health signal: only a discovery *failure*
83
+ (missing/unparseable config) sets a non-zero code, and only for an explicit
84
+ `--mcp-config`. An explicit config that parses but whose server is merely
85
+ unreachable is surfaced as an `unavailable` entry with exit `0`. Scripts
86
+ that need per-server health must inspect `unavailable`/`mcp_error` in the
87
+ `--json` output, not the exit code alone.
88
+
89
+ Args:
90
+ args: Parsed CLI namespace. Reads `output_format`, `agent`,
91
+ `interpreter`, `sandbox`, `no_mcp`, `mcp_config`, and
92
+ `trust_project_mcp`.
93
+
94
+ Returns:
95
+ `0` on success (including best-effort MCP degradation); `1` when an
96
+ explicit `--mcp-config` was given but MCP discovery failed.
97
+ """
98
+ from deepagents_code._constants import DEFAULT_AGENT_NAME
99
+ from deepagents_code._server_config import _resolve_enable_interpreter
100
+ from deepagents_code.main import _resolve_agent_arg
101
+ from deepagents_code.tool_catalog import collect_catalog
102
+
103
+ output_format: OutputFormat = getattr(args, "output_format", "text")
104
+ mcp_config_path: str | None = getattr(args, "mcp_config", None)
105
+ assistant_id = (
106
+ _resolve_agent_arg(args) if hasattr(args, "agent") else DEFAULT_AGENT_NAME
107
+ )
108
+ enable_interpreter = _resolve_enable_interpreter(
109
+ getattr(args, "interpreter", None), getattr(args, "sandbox", None)
110
+ )
111
+ catalog = collect_catalog(
112
+ assistant_id=assistant_id,
113
+ enable_interpreter=enable_interpreter,
114
+ include_mcp=not getattr(args, "no_mcp", False),
115
+ mcp_config_path=mcp_config_path,
116
+ trust_project_mcp=_tools_list_project_mcp_trust(args),
117
+ )
118
+
119
+ # A failed *explicit* --mcp-config is a failed user request → non-zero exit;
120
+ # best-effort auto-discovery failures stay exit 0 (built-ins still render).
121
+ exit_code = 1 if catalog.mcp_error and mcp_config_path else 0
122
+
123
+ if output_format == "json":
124
+ tools_payload = [
125
+ {
126
+ "name": entry.name,
127
+ "description": entry.description,
128
+ "group": group.label,
129
+ "source": group.source,
130
+ }
131
+ for group in catalog.groups
132
+ for entry in group.tools
133
+ ]
134
+ write_json(
135
+ "tools list",
136
+ {
137
+ "tools": tools_payload,
138
+ "count": len(tools_payload),
139
+ "unavailable": [
140
+ {
141
+ "name": server.name,
142
+ "status": server.status,
143
+ "detail": server.detail,
144
+ }
145
+ for server in catalog.unavailable
146
+ ],
147
+ "mcp_error": catalog.mcp_error,
148
+ },
149
+ )
150
+ return exit_code
151
+
152
+ _print_catalog(catalog)
153
+ return exit_code
154
+
155
+
156
+ def _tools_list_project_mcp_trust(args: argparse.Namespace) -> bool | None:
157
+ """Resolve project MCP trust behavior for `dcode tools list`.
158
+
159
+ Args:
160
+ args: Parsed CLI namespace.
161
+
162
+ Returns:
163
+ `True` when project MCP trust was explicitly requested, otherwise
164
+ `None` so MCP discovery can consult persisted trust.
165
+ """
166
+ if getattr(args, "trust_project_mcp", False):
167
+ return True
168
+ return None
169
+
170
+
171
+ def _print_catalog(catalog: ToolCatalog) -> None:
172
+ """Render a tool catalog to the console.
173
+
174
+ Prints the count header, the tool groups, then any unavailable MCP servers
175
+ and a discovery-failure notice.
176
+
177
+ Args:
178
+ catalog: Collected groups, unavailable servers, and discovery status.
179
+ """
180
+ from deepagents_code.config import console, get_glyphs
181
+
182
+ ellipsis = get_glyphs().ellipsis
183
+ total = sum(len(group.tools) for group in catalog.groups)
184
+ noun = "tool" if total == 1 else "tools"
185
+
186
+ console.print()
187
+ console.print(f"{total} {noun} available", highlight=False)
188
+
189
+ for group in catalog.groups:
190
+ if not group.tools:
191
+ continue
192
+ name_width = max(len(entry.name) for entry in group.tools)
193
+ # Indent (2) + name column + gap (2) precede the description; keep each
194
+ # row on one line by truncating the description to the terminal width.
195
+ desc_width = console.width - 2 - name_width - 2
196
+ console.print()
197
+ console.print(group.label, style="bold", markup=False, highlight=False)
198
+ for entry in group.tools:
199
+ padded = entry.name.ljust(name_width)
200
+ description = _truncate(entry.description, desc_width, ellipsis)
201
+ # `markup=False`/`highlight=False`: tool names and descriptions are
202
+ # sourced from tool objects and may contain brackets or numbers.
203
+ console.print(
204
+ f" {padded} {description}".rstrip(),
205
+ markup=False,
206
+ highlight=False,
207
+ no_wrap=True,
208
+ crop=True,
209
+ )
210
+
211
+ _print_unavailable_servers(catalog.unavailable)
212
+
213
+ if catalog.mcp_error:
214
+ console.print()
215
+ console.print(f"Note: {catalog.mcp_error}", style="yellow", highlight=False)
216
+
217
+ console.print()
218
+
219
+
220
+ def _print_unavailable_servers(servers: tuple[UnavailableServer, ...]) -> None:
221
+ """Render MCP servers that were discovered but expose no tools.
222
+
223
+ Args:
224
+ servers: Unavailable servers (errored, needing login, or disabled).
225
+ """
226
+ if not servers:
227
+ return
228
+ from deepagents_code.config import console
229
+ from deepagents_code.tool_catalog import unavailable_server_display
230
+
231
+ name_width = max(len(server.name) for server in servers)
232
+ console.print()
233
+ console.print(
234
+ "Unavailable MCP servers", style="bold", markup=False, highlight=False
235
+ )
236
+ for server in servers:
237
+ padded = server.name.ljust(name_width)
238
+ # `status: detail`, where detail is discovery's own curated reason string
239
+ # (ASCII on this CLI path, so legacy consoles don't hit an encoding
240
+ # error). `unavailable_server_display` collapses a disabled server to
241
+ # "disabled by user" with no detail; other statuses keep discovery's
242
+ # reason string. (The TUI's em-dash reconnect guidance is never produced
243
+ # by CLI discovery, so it cannot reach this column here.)
244
+ status, detail_text = unavailable_server_display(server)
245
+ detail = f": {detail_text}" if detail_text else ""
246
+ console.print(
247
+ f" {padded} {status}{detail}".rstrip(),
248
+ style="dim",
249
+ markup=False,
250
+ highlight=False,
251
+ no_wrap=True,
252
+ crop=True,
253
+ )
254
+
255
+
256
+ def _truncate(text: str, width: int, ellipsis: str) -> str:
257
+ """Truncate `text` to `width` columns, appending `ellipsis` when clipped.
258
+
259
+ Args:
260
+ text: Description text to truncate.
261
+ width: Maximum column width for the description.
262
+ ellipsis: Marker appended when `text` is clipped.
263
+
264
+ Returns:
265
+ `text` unchanged when it fits, otherwise a clipped string ending in
266
+ `ellipsis`.
267
+ """
268
+ if width <= 0 or len(text) <= width:
269
+ return text
270
+ if width <= len(ellipsis):
271
+ return text[:width]
272
+ return text[: width - len(ellipsis)].rstrip() + ellipsis
273
+
274
+
275
+ def _run_tools_install(args: argparse.Namespace) -> int:
276
+ """Install or repair the managed ripgrep binary.
277
+
278
+ Honors the same opt-outs as first-run startup (`DEEPAGENTS_CODE_OFFLINE`
279
+ and `DEEPAGENTS_CODE_RIPGREP_INSTALLER=system`) so behavior stays
280
+ consistent across entry points.
281
+
282
+ Args:
283
+ args: Parsed CLI namespace. Only `output_format` is read.
284
+
285
+ Returns:
286
+ `0` when a usable `rg` is available (installed, already current, or an
287
+ intentional opt-out), `1` when an install was expected but failed.
288
+ """
289
+ from deepagents_code.managed_tools import (
290
+ RIPGREP_VERSION,
291
+ ChecksumMismatchError,
292
+ ManagedToolUnavailableError,
293
+ ensure_ripgrep,
294
+ is_offline,
295
+ managed_rg_path,
296
+ prefers_system_ripgrep,
297
+ prepend_managed_bin_to_path,
298
+ )
299
+
300
+ output_format: OutputFormat = getattr(args, "output_format", "text")
301
+ managed_target = managed_rg_path()
302
+
303
+ try:
304
+ installed = asyncio.run(ensure_ripgrep())
305
+ except ChecksumMismatchError:
306
+ logger.exception(
307
+ "ripgrep install aborted: SHA-256 mismatch on downloaded archive"
308
+ )
309
+ return _emit_install_result(
310
+ output_format,
311
+ status="error",
312
+ message=(
313
+ "ripgrep install aborted: the downloaded archive failed SHA-256 "
314
+ "verification. Refusing to install."
315
+ ),
316
+ )
317
+ except ManagedToolUnavailableError as exc:
318
+ logger.info("ripgrep install unavailable: %s", exc.reason)
319
+ return _emit_install_result(
320
+ output_format,
321
+ status="error",
322
+ message=exc.message,
323
+ )
324
+ except Exception:
325
+ # Backstop for a clean exit instead of a raw traceback.
326
+ # `ensure_ripgrep` is defensive internally, but this is the only
327
+ # `ensure_ripgrep` caller wired into `scripts/install.sh`, so an
328
+ # unexpected escape must degrade to a structured error + exit 1
329
+ # (matching the broad backstops in `app.py` / `main.py`) rather than
330
+ # dumping a traceback and breaking the `--json` envelope.
331
+ logger.warning("ripgrep install failed unexpectedly", exc_info=True)
332
+ return _emit_install_result(
333
+ output_format,
334
+ status="error",
335
+ message="ripgrep install failed unexpectedly. See logs for details.",
336
+ )
337
+
338
+ if installed is not None:
339
+ if installed == managed_target:
340
+ prepend_managed_bin_to_path()
341
+ message = f"Managed ripgrep {RIPGREP_VERSION} ready at {installed}"
342
+ else:
343
+ message = f"Using ripgrep already on PATH at {installed}"
344
+ return _emit_install_result(
345
+ output_format,
346
+ status="ok",
347
+ message=message,
348
+ path=str(installed),
349
+ )
350
+
351
+ # `ensure_ripgrep` returned `None`: an intentional opt-out is a success
352
+ # (nothing to do), while an unexpected failure is reported as an error.
353
+ if prefers_system_ripgrep():
354
+ return _emit_install_result(
355
+ output_format,
356
+ status="skipped",
357
+ message=(
358
+ "Skipped managed ripgrep install: DEEPAGENTS_CODE_RIPGREP_INSTALLER"
359
+ "=system. Install ripgrep with your package manager, or unset the "
360
+ "variable to use the managed binary."
361
+ ),
362
+ )
363
+ if is_offline():
364
+ return _emit_install_result(
365
+ output_format,
366
+ status="skipped",
367
+ message=(
368
+ "Skipped managed ripgrep install: DEEPAGENTS_CODE_OFFLINE is set. "
369
+ "Unset it to download the managed binary."
370
+ ),
371
+ )
372
+ return _emit_install_result(
373
+ output_format,
374
+ status="error",
375
+ message=(
376
+ "Could not install ripgrep (download failure). See logs, or install "
377
+ "ripgrep manually."
378
+ ),
379
+ )
380
+
381
+
382
+ def _emit_install_result(
383
+ output_format: OutputFormat,
384
+ *,
385
+ status: InstallStatus,
386
+ message: str,
387
+ path: str | None = None,
388
+ ) -> int:
389
+ """Print the install outcome as text or JSON and return its exit code.
390
+
391
+ `ok` is derived from `status` (only `"error"` is unhealthy) so the JSON
392
+ envelope and exit code cannot disagree and no illegal `(status, ok)` pair
393
+ is representable.
394
+
395
+ Args:
396
+ output_format: `"json"` for machine-readable output, else text.
397
+ status: Stable machine token (`"ok"`, `"skipped"`, or `"error"`).
398
+ message: Human-readable summary line.
399
+ path: Resolved `rg` path when one is available.
400
+
401
+ Returns:
402
+ `0` for `"ok"`/`"skipped"`, `1` for `"error"`.
403
+ """
404
+ ok = status != "error"
405
+ if output_format == "json":
406
+ payload: dict[str, object] = {"status": status, "ok": ok, "message": message}
407
+ if path is not None:
408
+ payload["path"] = path
409
+ write_json("tools install", payload)
410
+ else:
411
+ from deepagents_code.config import console
412
+
413
+ style = "green" if ok else "bold red"
414
+ console.print(message, style=style, markup=False)
415
+
416
+ return 0 if ok else 1
@@ -0,0 +1 @@
1
+ """Local LangGraph server launch helpers for Deep Agents Code clients."""