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,223 @@
|
|
|
1
|
+
"""First-run onboarding state for the interactive TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from deepagents_code._env_vars import DEBUG_ONBOARDING, is_env_truthy
|
|
10
|
+
from deepagents_code.model_config import DEFAULT_STATE_DIR
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
ONBOARDING_MARKER_FILENAME = "onboarding_complete"
|
|
18
|
+
"""Marker filename under `~/.deepagents/.state` after onboarding has completed."""
|
|
19
|
+
|
|
20
|
+
ONBOARDING_NAME_MEMORY_START = "<!-- deepagents:onboarding-name:start -->"
|
|
21
|
+
"""Start marker for the managed onboarding name memory block."""
|
|
22
|
+
|
|
23
|
+
ONBOARDING_NAME_MEMORY_END = "<!-- deepagents:onboarding-name:end -->"
|
|
24
|
+
"""End marker for the managed onboarding name memory block."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def onboarding_marker_path(state_dir: Path | None = None) -> Path:
|
|
28
|
+
"""Return the first-run onboarding marker path.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
state_dir: Optional state directory override for tests.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Path to the onboarding completion marker.
|
|
35
|
+
"""
|
|
36
|
+
return (state_dir or DEFAULT_STATE_DIR) / ONBOARDING_MARKER_FILENAME
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def has_completed_onboarding(state_dir: Path | None = None) -> bool:
|
|
40
|
+
"""Return whether the user has completed onboarding.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
state_dir: Optional state directory override for tests.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
`True` when the onboarding marker exists, otherwise `False`.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
return onboarding_marker_path(state_dir).exists()
|
|
50
|
+
except OSError:
|
|
51
|
+
logger.warning("Could not inspect onboarding marker", exc_info=True)
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def mark_onboarding_complete(state_dir: Path | None = None) -> bool:
|
|
56
|
+
"""Persist that onboarding has completed.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
state_dir: Optional state directory override for tests.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
`True` when the marker was written, otherwise `False`.
|
|
63
|
+
"""
|
|
64
|
+
path = onboarding_marker_path(state_dir)
|
|
65
|
+
try:
|
|
66
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
path.write_text("1\n", encoding="utf-8")
|
|
68
|
+
except OSError:
|
|
69
|
+
logger.warning("Could not write onboarding marker at %s", path, exc_info=True)
|
|
70
|
+
return False
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def write_onboarding_name_memory(
|
|
75
|
+
name: str,
|
|
76
|
+
assistant_id: str,
|
|
77
|
+
*,
|
|
78
|
+
memory_path: Path | None = None,
|
|
79
|
+
) -> bool:
|
|
80
|
+
"""Persist the optional onboarding name into user agent memory.
|
|
81
|
+
|
|
82
|
+
Empty or whitespace-only names are skipped (no file is written).
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
name: Submitted user name.
|
|
86
|
+
assistant_id: Agent identifier whose user memory should be updated.
|
|
87
|
+
memory_path: Optional memory file override for tests.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
`True` when memory was written, otherwise `False`.
|
|
91
|
+
"""
|
|
92
|
+
clean = _normalize_memory_name(name)
|
|
93
|
+
if not clean:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
if memory_path is None:
|
|
97
|
+
from deepagents_code.config import settings
|
|
98
|
+
|
|
99
|
+
path = settings.get_user_agent_md_path(assistant_id)
|
|
100
|
+
else:
|
|
101
|
+
path = memory_path
|
|
102
|
+
|
|
103
|
+
block = _onboarding_name_memory_block(clean)
|
|
104
|
+
try:
|
|
105
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
try:
|
|
107
|
+
existing = path.read_text(encoding="utf-8")
|
|
108
|
+
except FileNotFoundError:
|
|
109
|
+
existing = ""
|
|
110
|
+
except UnicodeDecodeError:
|
|
111
|
+
# Existing memory file is not valid UTF-8. Overwriting would clobber
|
|
112
|
+
# whatever the user has there, so abort and let them resolve it.
|
|
113
|
+
logger.warning(
|
|
114
|
+
"Existing memory file %s is not valid UTF-8; skipping onboarding "
|
|
115
|
+
"name memory write to avoid clobbering user content",
|
|
116
|
+
path,
|
|
117
|
+
exc_info=True,
|
|
118
|
+
)
|
|
119
|
+
return False
|
|
120
|
+
path.write_text(
|
|
121
|
+
_upsert_onboarding_name_memory(existing, block),
|
|
122
|
+
encoding="utf-8",
|
|
123
|
+
)
|
|
124
|
+
except OSError:
|
|
125
|
+
logger.warning(
|
|
126
|
+
"Could not write onboarding name memory at %s",
|
|
127
|
+
path,
|
|
128
|
+
exc_info=True,
|
|
129
|
+
)
|
|
130
|
+
return False
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _normalize_memory_name(name: str) -> str:
|
|
135
|
+
"""Normalize whitespace in a name before writing it to memory.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Name with leading/trailing whitespace stripped and internal runs
|
|
139
|
+
collapsed to single spaces.
|
|
140
|
+
"""
|
|
141
|
+
return " ".join(name.split())
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _onboarding_name_memory_block(name: str) -> str:
|
|
145
|
+
"""Return the managed memory block for an onboarding name."""
|
|
146
|
+
quoted = json.dumps(name)
|
|
147
|
+
return (
|
|
148
|
+
f"{ONBOARDING_NAME_MEMORY_START}\n"
|
|
149
|
+
f"- The user's preferred name is {quoted}.\n"
|
|
150
|
+
f"{ONBOARDING_NAME_MEMORY_END}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _upsert_onboarding_name_memory(existing: str, block: str) -> str:
|
|
155
|
+
"""Insert or replace the managed onboarding name memory block.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Updated memory file content.
|
|
159
|
+
"""
|
|
160
|
+
start = existing.find(ONBOARDING_NAME_MEMORY_START)
|
|
161
|
+
end = existing.find(ONBOARDING_NAME_MEMORY_END)
|
|
162
|
+
if start != -1 and end != -1 and start < end:
|
|
163
|
+
end += len(ONBOARDING_NAME_MEMORY_END)
|
|
164
|
+
prefix = existing[:start].rstrip()
|
|
165
|
+
suffix = existing[end:].strip()
|
|
166
|
+
parts = [part for part in (prefix, block, suffix) if part]
|
|
167
|
+
return "\n\n".join(parts).rstrip() + "\n"
|
|
168
|
+
|
|
169
|
+
base = existing.rstrip()
|
|
170
|
+
if not base:
|
|
171
|
+
return f"## User Preferences\n\n{block}\n"
|
|
172
|
+
if "## User Preferences" in base:
|
|
173
|
+
return f"{base}\n\n{block}\n"
|
|
174
|
+
return f"{base}\n\n## User Preferences\n\n{block}\n"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def extract_onboarding_name_block(text: str) -> str | None:
|
|
178
|
+
"""Return the managed onboarding name block (markers included) if present.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
text: Memory file content to inspect.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
The substring from the start marker through the end marker, or `None`
|
|
185
|
+
when a well-formed block is absent.
|
|
186
|
+
"""
|
|
187
|
+
start = text.find(ONBOARDING_NAME_MEMORY_START)
|
|
188
|
+
end = text.find(ONBOARDING_NAME_MEMORY_END)
|
|
189
|
+
if start == -1 or end == -1 or start >= end:
|
|
190
|
+
return None
|
|
191
|
+
return text[start : end + len(ONBOARDING_NAME_MEMORY_END)]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def strip_onboarding_name_markers(text: str) -> str:
|
|
195
|
+
"""Remove every onboarding-name marker occurrence from `text`.
|
|
196
|
+
|
|
197
|
+
A partial edit can leave a lone start or end marker behind. Stripping all
|
|
198
|
+
marker strings before re-inserting the managed block keeps re-insertion from
|
|
199
|
+
producing orphaned markers that would confuse `extract_onboarding_name_block`.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
text: Memory file content to sanitize.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
`text` with all start and end marker strings removed.
|
|
206
|
+
"""
|
|
207
|
+
return text.replace(ONBOARDING_NAME_MEMORY_START, "").replace(
|
|
208
|
+
ONBOARDING_NAME_MEMORY_END, ""
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def should_run_onboarding(state_dir: Path | None = None) -> bool:
|
|
213
|
+
"""Return whether onboarding should open at interactive startup.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
state_dir: Optional state directory override for tests.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
`True` when the debug override is enabled or no completion marker exists.
|
|
220
|
+
"""
|
|
221
|
+
if is_env_truthy(DEBUG_ONBOARDING):
|
|
222
|
+
return True
|
|
223
|
+
return not has_completed_onboarding(state_dir)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Machine-readable JSON output helpers for CLI subcommands.
|
|
2
|
+
|
|
3
|
+
This module deliberately stays stdlib-only so it can be imported from CLI
|
|
4
|
+
startup paths without pulling in unnecessary dependency trees.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
OutputFormat = Literal["text", "json"]
|
|
15
|
+
"""Accepted internal output modes for CLI subcommands."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def add_json_output_arg(
|
|
19
|
+
parser: argparse.ArgumentParser, *, default: OutputFormat | None = None
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Add a `--json` flag to an argparse parser.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
parser: Parser to update.
|
|
25
|
+
default: Default output format for this parser.
|
|
26
|
+
|
|
27
|
+
Pass `None` for subparsers so parent parser values are preserved.
|
|
28
|
+
"""
|
|
29
|
+
if default is None:
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--json",
|
|
32
|
+
dest="output_format",
|
|
33
|
+
action="store_const",
|
|
34
|
+
const="json",
|
|
35
|
+
default=argparse.SUPPRESS,
|
|
36
|
+
help="Emit machine-readable JSON for this command",
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--json",
|
|
41
|
+
dest="output_format",
|
|
42
|
+
action="store_const",
|
|
43
|
+
const="json",
|
|
44
|
+
default=default,
|
|
45
|
+
help="Emit machine-readable JSON for this command",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def write_json(command: str, data: list | dict) -> None:
|
|
50
|
+
"""Write a JSON envelope to stdout and flush.
|
|
51
|
+
|
|
52
|
+
The envelope is a single-line JSON object with a stable schema:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{"schema_version": 1, "command": "...", "data": ...}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
command: Self-documenting command name (e.g. `'list'`,
|
|
60
|
+
`'threads list'`).
|
|
61
|
+
data: Payload — typically a list for listing commands or a dict
|
|
62
|
+
for action/info commands.
|
|
63
|
+
|
|
64
|
+
`default=str` is used so that `Path` and `datetime` objects
|
|
65
|
+
serialize without error.
|
|
66
|
+
"""
|
|
67
|
+
envelope = {"schema_version": 1, "command": command, "data": data}
|
|
68
|
+
sys.stdout.write(json.dumps(envelope, default=str) + "\n")
|
|
69
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
r"""Large paste collapsing for the chat input.
|
|
2
|
+
|
|
3
|
+
When the user pastes text exceeding a size or line threshold, the full text
|
|
4
|
+
is stored off-screen and a compact `[Pasted text #N +M lines]` placeholder
|
|
5
|
+
is inserted into the input box instead. At submission time the placeholder
|
|
6
|
+
is expanded back to the original content so the agent receives the full text.
|
|
7
|
+
|
|
8
|
+
This mirrors the behavior of Claude Code's paste-collapsing system.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
PASTE_THRESHOLD_CHARS = 800
|
|
17
|
+
"""Minimum character count for a paste to be collapsed into a placeholder."""
|
|
18
|
+
|
|
19
|
+
PASTE_THRESHOLD_LINES = 2
|
|
20
|
+
"""Minimum line count (newline-separated) for a paste to be collapsed."""
|
|
21
|
+
|
|
22
|
+
PASTE_PLACEHOLDER_PATTERN = re.compile(r"\[Pasted text #(\d+)(?: \+(\d+) lines)?\]")
|
|
23
|
+
"""Regex matching `[Pasted text #N]` or `[Pasted text #N +M lines]`."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class PastedContent:
|
|
28
|
+
"""Stored content for a collapsed paste.
|
|
29
|
+
|
|
30
|
+
The paste's numeric identifier is the key under which this record is
|
|
31
|
+
stored in the input's paste map, so it is not duplicated on the record.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
content: The full pasted text.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
content: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def count_lines(text: str) -> int:
|
|
41
|
+
r"""Return the number of newline characters in `text`.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
text: The text to count newlines in.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The number of `\n` occurrences (0 for single-line text).
|
|
48
|
+
"""
|
|
49
|
+
return text.count("\n")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def should_collapse_paste(text: str) -> bool:
|
|
53
|
+
"""Return whether `text` should be collapsed into a placeholder.
|
|
54
|
+
|
|
55
|
+
Collapses when the text exceeds the character threshold or the line
|
|
56
|
+
threshold.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
text: The pasted text to evaluate.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
`True` if the paste should be collapsed.
|
|
63
|
+
"""
|
|
64
|
+
return (
|
|
65
|
+
len(text) > PASTE_THRESHOLD_CHARS or count_lines(text) > PASTE_THRESHOLD_LINES
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_paste_ref(paste_id: int, num_lines: int) -> str:
|
|
70
|
+
"""Format a paste placeholder reference string.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
paste_id: The numeric paste identifier.
|
|
74
|
+
num_lines: The number of extra lines (newlines) in the pasted content.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
`[Pasted text #N]` when `num_lines` is 0, otherwise
|
|
78
|
+
`[Pasted text #N +M lines]`.
|
|
79
|
+
"""
|
|
80
|
+
if num_lines == 0:
|
|
81
|
+
return f"[Pasted text #{paste_id}]"
|
|
82
|
+
return f"[Pasted text #{paste_id} +{num_lines} lines]"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def expand_paste_refs(text: str, pasted_contents: dict[int, PastedContent]) -> str:
|
|
86
|
+
"""Replace all paste placeholders in `text` with their full content.
|
|
87
|
+
|
|
88
|
+
Placeholders whose IDs are not in `pasted_contents` are left unchanged.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
text: The text containing placeholders.
|
|
92
|
+
pasted_contents: Mapping of paste IDs to stored content.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The text with all known placeholders expanded.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def _replace(match: re.Match[str]) -> str:
|
|
99
|
+
content = pasted_contents.get(int(match.group(1)))
|
|
100
|
+
# Return the stored text literally; unknown IDs keep their placeholder.
|
|
101
|
+
return content.content if content is not None else match.group(0)
|
|
102
|
+
|
|
103
|
+
return PASTE_PLACEHOLDER_PATTERN.sub(_replace, text)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Utilities for project root detection and project-specific configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from deepagents_code._env_vars import SERVER_ENV_PREFIX
|
|
11
|
+
from deepagents_code._git import find_git_root
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ProjectContext:
|
|
23
|
+
"""Explicit user/project path context for project-sensitive behavior.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
user_cwd: Authoritative working directory from the app invocation.
|
|
27
|
+
project_root: Resolved project root for `user_cwd`, if one exists.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
user_cwd: Path
|
|
31
|
+
project_root: Path | None = None
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
"""Validate that path fields are absolute.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If `user_cwd` or `project_root` is not absolute.
|
|
38
|
+
"""
|
|
39
|
+
if not self.user_cwd.is_absolute():
|
|
40
|
+
msg = f"user_cwd must be absolute, got {self.user_cwd!r}"
|
|
41
|
+
raise ValueError(msg)
|
|
42
|
+
if self.project_root is not None and not self.project_root.is_absolute():
|
|
43
|
+
msg = f"project_root must be absolute, got {self.project_root!r}"
|
|
44
|
+
raise ValueError(msg)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_user_cwd(cls, user_cwd: str | Path) -> ProjectContext:
|
|
48
|
+
"""Build a project context from an explicit user working directory.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
user_cwd: User invocation directory.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Resolved project context.
|
|
55
|
+
"""
|
|
56
|
+
resolved_cwd = Path(user_cwd).expanduser().resolve()
|
|
57
|
+
return cls(
|
|
58
|
+
user_cwd=resolved_cwd,
|
|
59
|
+
project_root=find_project_root(resolved_cwd),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def resolve_user_path(self, path: str | Path) -> Path:
|
|
63
|
+
"""Resolve a path relative to the explicit user working directory.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
path: Absolute or relative user-facing path.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Absolute resolved path.
|
|
70
|
+
"""
|
|
71
|
+
candidate = Path(path).expanduser()
|
|
72
|
+
if candidate.is_absolute():
|
|
73
|
+
return candidate.resolve()
|
|
74
|
+
return (self.user_cwd / candidate).resolve()
|
|
75
|
+
|
|
76
|
+
def project_agent_md_paths(self) -> list[Path]:
|
|
77
|
+
"""Return project-level `AGENTS.md` files for this context."""
|
|
78
|
+
if self.project_root is None:
|
|
79
|
+
return []
|
|
80
|
+
return find_project_agent_md(self.project_root)
|
|
81
|
+
|
|
82
|
+
def project_skills_dir(self) -> Path | None:
|
|
83
|
+
"""Return the project `.deepagents/skills` directory, if any."""
|
|
84
|
+
if self.project_root is None:
|
|
85
|
+
return None
|
|
86
|
+
return self.project_root / ".deepagents" / "skills"
|
|
87
|
+
|
|
88
|
+
def project_agents_dir(self) -> Path | None:
|
|
89
|
+
"""Return the project `.deepagents/agents` directory, if any."""
|
|
90
|
+
if self.project_root is None:
|
|
91
|
+
return None
|
|
92
|
+
return self.project_root / ".deepagents" / "agents"
|
|
93
|
+
|
|
94
|
+
def project_agent_skills_dir(self) -> Path | None:
|
|
95
|
+
"""Return the project `.agents/skills` directory, if any."""
|
|
96
|
+
if self.project_root is None:
|
|
97
|
+
return None
|
|
98
|
+
return self.project_root / ".agents" / "skills"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_server_project_context(
|
|
102
|
+
env: Mapping[str, str] | None = None,
|
|
103
|
+
) -> ProjectContext | None:
|
|
104
|
+
"""Read the server project context from environment transport data.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
env: Environment mapping to read from.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Reconstructed project context, or `None` if no server context exists.
|
|
111
|
+
"""
|
|
112
|
+
environment = os.environ if env is None else env
|
|
113
|
+
raw_cwd = environment.get(f"{SERVER_ENV_PREFIX}CWD")
|
|
114
|
+
if not raw_cwd:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
user_cwd = Path(raw_cwd).expanduser().resolve()
|
|
119
|
+
raw_project_root = environment.get(f"{SERVER_ENV_PREFIX}PROJECT_ROOT")
|
|
120
|
+
project_root = (
|
|
121
|
+
Path(raw_project_root).expanduser().resolve()
|
|
122
|
+
if raw_project_root
|
|
123
|
+
else find_project_root(user_cwd)
|
|
124
|
+
)
|
|
125
|
+
except OSError:
|
|
126
|
+
logger.warning(
|
|
127
|
+
"Could not resolve server project context from CWD=%s",
|
|
128
|
+
raw_cwd,
|
|
129
|
+
exc_info=True,
|
|
130
|
+
)
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
return ProjectContext(user_cwd=user_cwd, project_root=project_root)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def find_project_root(start_path: str | Path | None = None) -> Path | None:
|
|
137
|
+
"""Find the project root by looking for git metadata.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
start_path: Directory to start searching from.
|
|
141
|
+
Defaults to current working directory.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Path to the project root if found, None otherwise.
|
|
145
|
+
"""
|
|
146
|
+
current = Path(start_path or Path.cwd()).expanduser().resolve()
|
|
147
|
+
return find_git_root(current)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def find_project_agent_md(project_root: Path) -> list[Path]:
|
|
151
|
+
"""Find project-specific AGENTS.md file(s).
|
|
152
|
+
|
|
153
|
+
Checks two locations and returns ALL that exist:
|
|
154
|
+
1. project_root/.deepagents/AGENTS.md
|
|
155
|
+
2. project_root/AGENTS.md
|
|
156
|
+
|
|
157
|
+
Both files will be loaded and combined if both exist.
|
|
158
|
+
|
|
159
|
+
Candidates with symlinked path components are followed only when the
|
|
160
|
+
resolved target stays inside `project_root`. The returned `Path` is the
|
|
161
|
+
resolved target when any symlink component was traversed, so
|
|
162
|
+
`FilesystemBackend.download_files` opens a regular file rather than
|
|
163
|
+
tripping `O_NOFOLLOW` on the link itself. Symlinks pointing outside the
|
|
164
|
+
project root, symlink loops, and unreadable parents are skipped with a
|
|
165
|
+
warning. Broken symlinks are treated as missing files (no warning),
|
|
166
|
+
matching the pre-existing behavior for absent candidates.
|
|
167
|
+
|
|
168
|
+
Why: project AGENTS.md is auto-discovered and loaded into the system
|
|
169
|
+
prompt before the first model call. Without the in-tree check, a
|
|
170
|
+
malicious clone could ship `AGENTS.md -> ~/.ssh/config` (or any other
|
|
171
|
+
locally-readable file) and have its contents injected as agent
|
|
172
|
+
instructions on first run.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
project_root: Path to the project root directory.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Existing AGENTS.md paths, with in-tree symlinked path components
|
|
179
|
+
pre-resolved to their targets. Empty if neither file exists, one
|
|
180
|
+
entry if only one is present, or two entries if both locations
|
|
181
|
+
have the file.
|
|
182
|
+
"""
|
|
183
|
+
# Resolve the root once so the candidate-equality check below works even
|
|
184
|
+
# when the caller passes a non-canonical `project_root` (e.g., macOS
|
|
185
|
+
# `/var` -> `/private/var`, or a path with symlinked ancestors).
|
|
186
|
+
project_root_resolved = project_root.resolve()
|
|
187
|
+
candidates = [
|
|
188
|
+
project_root_resolved / ".deepagents" / "AGENTS.md",
|
|
189
|
+
project_root_resolved / "AGENTS.md",
|
|
190
|
+
]
|
|
191
|
+
paths: list[Path] = []
|
|
192
|
+
for candidate in candidates:
|
|
193
|
+
try:
|
|
194
|
+
# Single syscall handles existence, broken symlinks, loops, and
|
|
195
|
+
# canonicalization. `strict=True` raises rather than returning
|
|
196
|
+
# the unresolved path.
|
|
197
|
+
resolved = candidate.resolve(strict=True)
|
|
198
|
+
except FileNotFoundError:
|
|
199
|
+
# Absent file or broken symlink — matches the pre-existing
|
|
200
|
+
# silent-skip behavior for missing candidates. `Path.exists()`
|
|
201
|
+
# also returns False for symlink loops on some Python versions,
|
|
202
|
+
# so loops do NOT come through here; see the OSError branch.
|
|
203
|
+
continue
|
|
204
|
+
except (OSError, RuntimeError) as exc:
|
|
205
|
+
# `OSError(ELOOP)` on Python 3.13+, `RuntimeError("Symlink loop
|
|
206
|
+
# ...")` on 3.11-3.12; bare `OSError` for permission/unreadable
|
|
207
|
+
# parent. Security-relevant — warn and skip.
|
|
208
|
+
logger.warning(
|
|
209
|
+
"Skipping AGENTS.md candidate %s: %s",
|
|
210
|
+
candidate,
|
|
211
|
+
exc,
|
|
212
|
+
)
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
resolved.relative_to(project_root_resolved)
|
|
217
|
+
except ValueError:
|
|
218
|
+
logger.warning(
|
|
219
|
+
"Skipping AGENTS.md symlink %s: target %s is outside "
|
|
220
|
+
"the project root %s",
|
|
221
|
+
candidate,
|
|
222
|
+
resolved,
|
|
223
|
+
project_root_resolved,
|
|
224
|
+
)
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
if candidate.absolute() == resolved:
|
|
228
|
+
paths.append(candidate)
|
|
229
|
+
else:
|
|
230
|
+
paths.append(resolved)
|
|
231
|
+
return paths
|
deepagents_code/py.typed
ADDED
|
File without changes
|