deepy-cli 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/PKG-INFO +3 -3
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/README.md +2 -2
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/pyproject.toml +2 -9
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/__init__.py +1 -1
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/cli.py +3 -3
- deepy_cli-0.2.2/src/deepy/data/tools/modify.md +26 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/llm/agent.py +5 -1
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/llm/context.py +8 -8
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/llm/provider.py +3 -2
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/llm/replay.py +2 -2
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/llm/runner.py +2 -2
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/mcp.py +8 -3
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/prompts/system.py +1 -1
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/skills.py +1 -1
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/tools/agents.py +7 -3
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/tools/builtin.py +63 -43
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/tools/file_state.py +16 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/tools/result.py +1 -1
- deepy_cli-0.2.2/src/deepy/types/__init__.py +2 -0
- deepy_cli-0.2.2/src/deepy/types/sdk.py +12 -0
- deepy_cli-0.2.2/src/deepy/types/tool_payloads.py +15 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/exit_summary.py +2 -1
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/local_command.py +9 -3
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/message_view.py +27 -15
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/prompt_input.py +7 -6
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/skill_picker.py +150 -1
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/terminal.py +76 -23
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/welcome.py +2 -1
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/utils/json.py +5 -2
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/utils/notify.py +2 -2
- deepy_cli-0.2.0/src/deepy/data/tools/modify.md +0 -22
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/config/__init__.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/config/settings.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/data/tools/AskUserQuestion.md +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/data/tools/WebFetch.md +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/data/tools/edit.md +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/data/tools/read.md +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/data/tools/shell.md +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/data/tools/write.md +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/errors.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/prompts/init_agents.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/prompts/tool_docs.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/sessions/jsonl.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/skill_market.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/status.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/tools/shell_output.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/ask_user_question.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/file_mentions.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/slash_commands.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/styles.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/usage.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.2.0 → deepy_cli-0.2.2}/src/deepy/utils/error_logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: deepy-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Deepy - Vibe coding for DeepSeek models in your terminal
|
|
5
5
|
Keywords: deepseek,coding-agent,terminal,cli,agents
|
|
6
6
|
Author: kirineko
|
|
@@ -342,7 +342,7 @@ A concise `AGENTS.md` works best:
|
|
|
342
342
|
## Commands
|
|
343
343
|
- Test: `uv run pytest`
|
|
344
344
|
- Lint: `uv run ruff check`
|
|
345
|
-
- Type check: `uv run
|
|
345
|
+
- Type check: `uv run ty check src`
|
|
346
346
|
|
|
347
347
|
## Architecture
|
|
348
348
|
- Keep CLI entry points thin; put reusable behavior under `src/`.
|
|
@@ -369,7 +369,7 @@ create or refresh the project root `AGENTS.md`.
|
|
|
369
369
|
uv sync --group dev
|
|
370
370
|
uv run pytest
|
|
371
371
|
uv run ruff check
|
|
372
|
-
uv run
|
|
372
|
+
uv run ty check src
|
|
373
373
|
uv build
|
|
374
374
|
```
|
|
375
375
|
|
|
@@ -313,7 +313,7 @@ A concise `AGENTS.md` works best:
|
|
|
313
313
|
## Commands
|
|
314
314
|
- Test: `uv run pytest`
|
|
315
315
|
- Lint: `uv run ruff check`
|
|
316
|
-
- Type check: `uv run
|
|
316
|
+
- Type check: `uv run ty check src`
|
|
317
317
|
|
|
318
318
|
## Architecture
|
|
319
319
|
- Keep CLI entry points thin; put reusable behavior under `src/`.
|
|
@@ -340,7 +340,7 @@ create or refresh the project root `AGENTS.md`.
|
|
|
340
340
|
uv sync --group dev
|
|
341
341
|
uv run pytest
|
|
342
342
|
uv run ruff check
|
|
343
|
-
uv run
|
|
343
|
+
uv run ty check src
|
|
344
344
|
uv build
|
|
345
345
|
```
|
|
346
346
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deepy-cli"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Deepy - Vibe coding for DeepSeek models in your terminal"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -39,10 +39,10 @@ Issues = "https://github.com/kirineko/deepy/issues"
|
|
|
39
39
|
|
|
40
40
|
[dependency-groups]
|
|
41
41
|
dev = [
|
|
42
|
-
"pyright>=1.1.407",
|
|
43
42
|
"pytest>=8.0",
|
|
44
43
|
"pytest-asyncio>=0.24",
|
|
45
44
|
"ruff>=0.14",
|
|
45
|
+
"ty>=0.0.32",
|
|
46
46
|
]
|
|
47
47
|
|
|
48
48
|
[tool.pytest.ini_options]
|
|
@@ -54,13 +54,6 @@ target-version = "py312"
|
|
|
54
54
|
src = ["src", "tests"]
|
|
55
55
|
exclude = ["dist", "reference", "spec"]
|
|
56
56
|
|
|
57
|
-
[tool.pyright]
|
|
58
|
-
pythonVersion = "3.12"
|
|
59
|
-
typeCheckingMode = "off"
|
|
60
|
-
include = ["src", "tests"]
|
|
61
|
-
exclude = ["dist", "reference", "spec"]
|
|
62
|
-
reportMissingTypeStubs = false
|
|
63
|
-
|
|
64
57
|
[tool.uv.build-backend]
|
|
65
58
|
module-name = "deepy"
|
|
66
59
|
|
|
@@ -4,7 +4,7 @@ import argparse
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Sequence
|
|
7
|
+
from typing import Any, Sequence
|
|
8
8
|
|
|
9
9
|
import tomli_w
|
|
10
10
|
|
|
@@ -198,7 +198,7 @@ def _cmd_config_theme(args: argparse.Namespace) -> int:
|
|
|
198
198
|
return 0
|
|
199
199
|
|
|
200
200
|
|
|
201
|
-
def _doctor(args: argparse.Namespace) -> tuple[int, dict[str,
|
|
201
|
+
def _doctor(args: argparse.Namespace) -> tuple[int, dict[str, Any]]:
|
|
202
202
|
settings = load_settings(args.config)
|
|
203
203
|
checks: list[dict[str, object]] = []
|
|
204
204
|
|
|
@@ -251,7 +251,7 @@ def _doctor(args: argparse.Namespace) -> tuple[int, dict[str, object]]:
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
|
|
254
|
-
async def _doctor_live(settings: Settings) -> dict[str,
|
|
254
|
+
async def _doctor_live(settings: Settings) -> dict[str, Any]:
|
|
255
255
|
from agents import Agent, RunConfig, Runner
|
|
256
256
|
|
|
257
257
|
provider = build_provider_bundle(settings)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
## modify
|
|
2
|
+
|
|
3
|
+
Create new files or edit existing files.
|
|
4
|
+
|
|
5
|
+
Use `content` only when the target file does not exist. For existing files, use
|
|
6
|
+
`old_string` and `new_string` for the smallest reliable replacement. You may
|
|
7
|
+
attempt an exact replacement directly when you already know the current text;
|
|
8
|
+
Deepy records the required file snapshot internally when no prior snapshot
|
|
9
|
+
exists. Read first when you need to inspect context. Do not rewrite an existing
|
|
10
|
+
scaffolded file with full content; replace the specific generated block instead.
|
|
11
|
+
|
|
12
|
+
Args for new files: `file_path`, `content`.
|
|
13
|
+
|
|
14
|
+
Args for existing files: `file_path`, `old_string`, `new_string`, optional
|
|
15
|
+
`replace_all`, optional `snippet_id`.
|
|
16
|
+
|
|
17
|
+
Existing-file edits must have a managed snapshot before Deepy commits changes;
|
|
18
|
+
`modify` can create that snapshot internally for direct exact replacements. Stale
|
|
19
|
+
edits are rejected. Repeated matches are rejected unless `replace_all` is true;
|
|
20
|
+
candidate snippets can be reused with `snippet_id`. Success includes diff
|
|
21
|
+
metadata.
|
|
22
|
+
|
|
23
|
+
If several `old_string` attempts fail and you know the complete desired file content,
|
|
24
|
+
re-read the file and use the managed whole-file replacement path. Do not delete the file
|
|
25
|
+
and recreate it with shell commands or here-strings; that bypasses Deepy's encoding,
|
|
26
|
+
newline, and stale-write protections, especially on Windows.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from deepy.config import Settings
|
|
6
7
|
from deepy.prompts import build_system_prompt
|
|
@@ -10,6 +11,9 @@ from deepy.tools.agents import build_function_tools
|
|
|
10
11
|
|
|
11
12
|
from .provider import ProviderBundle, build_provider_bundle
|
|
12
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from agents.mcp import MCPServer
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
def build_deepy_agent(
|
|
15
19
|
settings: Settings,
|
|
@@ -18,7 +22,7 @@ def build_deepy_agent(
|
|
|
18
22
|
project_root: Path,
|
|
19
23
|
provider: ProviderBundle | None = None,
|
|
20
24
|
loaded_skills: list[SkillInfo] | None = None,
|
|
21
|
-
mcp_servers: list[
|
|
25
|
+
mcp_servers: list[MCPServer] | None = None,
|
|
22
26
|
preferred_mcp_web_search_tools: list[str] | None = None,
|
|
23
27
|
):
|
|
24
28
|
from agents import Agent
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Callable
|
|
4
3
|
from math import ceil
|
|
5
4
|
from typing import Any
|
|
6
5
|
|
|
7
6
|
from deepy.config import Settings
|
|
7
|
+
from deepy.types.sdk import SessionInputCallback
|
|
8
8
|
from deepy.utils import json as json_utils
|
|
9
9
|
|
|
10
|
+
tiktoken: Any | None
|
|
10
11
|
try:
|
|
11
|
-
import tiktoken
|
|
12
|
+
import tiktoken as _tiktoken
|
|
12
13
|
except Exception: # pragma: no cover - optional dependency fallback.
|
|
13
|
-
tiktoken = None
|
|
14
|
+
tiktoken = None
|
|
15
|
+
else:
|
|
16
|
+
tiktoken = _tiktoken
|
|
14
17
|
|
|
15
18
|
_ENCODING = None
|
|
16
19
|
|
|
@@ -38,11 +41,8 @@ def estimate_tokens_for_items(items: list[dict[str, Any]]) -> int:
|
|
|
38
41
|
return sum(estimate_tokens_for_item(item) for item in items)
|
|
39
42
|
|
|
40
43
|
|
|
41
|
-
def build_session_input_callback(settings: Settings) ->
|
|
42
|
-
|
|
43
|
-
list[dict[str, Any]],
|
|
44
|
-
]:
|
|
45
|
-
def callback(history: list[dict[str, Any]], new_input: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
44
|
+
def build_session_input_callback(settings: Settings) -> SessionInputCallback:
|
|
45
|
+
def callback(history: list[Any], new_input: list[Any]) -> list[Any]:
|
|
46
46
|
return [*history, *new_input]
|
|
47
47
|
|
|
48
48
|
return callback
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from agents import Model, ModelSettings
|
|
6
7
|
from agents import OpenAIChatCompletionsModel
|
|
7
8
|
|
|
8
9
|
from deepy.config import Settings
|
|
@@ -17,8 +18,8 @@ from .replay import (
|
|
|
17
18
|
@dataclass(frozen=True)
|
|
18
19
|
class ProviderBundle:
|
|
19
20
|
client: object
|
|
20
|
-
model:
|
|
21
|
-
model_settings:
|
|
21
|
+
model: Model
|
|
22
|
+
model_settings: ModelSettings
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class DeepyOpenAIChatCompletionsModel(OpenAIChatCompletionsModel):
|
|
@@ -105,9 +105,9 @@ def sanitize_chat_completion_stream_event(event: Any) -> Any | None:
|
|
|
105
105
|
if getattr(event, "type", None) == "response.completed":
|
|
106
106
|
response = getattr(event, "response", None)
|
|
107
107
|
output = getattr(response, "output", None)
|
|
108
|
-
if isinstance(output, list):
|
|
108
|
+
if response is not None and isinstance(output, list):
|
|
109
109
|
try:
|
|
110
|
-
response.output = sanitize_model_response_output(output)
|
|
110
|
+
cast(Any, response).output = sanitize_model_response_output(output)
|
|
111
111
|
except Exception:
|
|
112
112
|
pass
|
|
113
113
|
return event
|
|
@@ -130,7 +130,7 @@ async def run_prompt_once(
|
|
|
130
130
|
input=prompt,
|
|
131
131
|
max_turns=max_turns,
|
|
132
132
|
run_config=run_config,
|
|
133
|
-
session=session,
|
|
133
|
+
session=session, # ty: ignore[invalid-argument-type] - DeepyJsonlSession matches the SDK Session protocol at runtime.
|
|
134
134
|
)
|
|
135
135
|
if should_interrupt is not None:
|
|
136
136
|
interrupt_task = asyncio.create_task(
|
|
@@ -349,7 +349,7 @@ def _max_turns_output(chunks: list[str], *, max_turns: int) -> str:
|
|
|
349
349
|
|
|
350
350
|
def format_deepseek_api_error(error: Any) -> str:
|
|
351
351
|
status_code = _safe_int(getattr(error, "status_code", None))
|
|
352
|
-
status = DEEPSEEK_ERROR_CODES.get(status_code)
|
|
352
|
+
status = DEEPSEEK_ERROR_CODES.get(status_code) if status_code is not None else None
|
|
353
353
|
title = f"DeepSeek API error {status_code}" if status_code is not None else "DeepSeek API error"
|
|
354
354
|
if status is not None:
|
|
355
355
|
title = f"{title}: {status.title}"
|
|
@@ -155,7 +155,12 @@ class DeepyMcpRuntime:
|
|
|
155
155
|
await self._manager.cleanup_all()
|
|
156
156
|
|
|
157
157
|
def _build_sdk_servers(self) -> list[Any]:
|
|
158
|
-
from agents.mcp import
|
|
158
|
+
from agents.mcp import (
|
|
159
|
+
MCPServerStdio,
|
|
160
|
+
MCPServerStdioParams,
|
|
161
|
+
MCPServerStreamableHttp,
|
|
162
|
+
MCPServerStreamableHttpParams,
|
|
163
|
+
)
|
|
159
164
|
|
|
160
165
|
servers: list[Any] = []
|
|
161
166
|
for definition in self.definitions:
|
|
@@ -173,7 +178,7 @@ class DeepyMcpRuntime:
|
|
|
173
178
|
)
|
|
174
179
|
continue
|
|
175
180
|
if definition.transport == "stdio":
|
|
176
|
-
params:
|
|
181
|
+
params: MCPServerStdioParams = {"command": definition.command or ""}
|
|
177
182
|
if definition.args:
|
|
178
183
|
params["args"] = list(definition.args)
|
|
179
184
|
if definition.env:
|
|
@@ -189,7 +194,7 @@ class DeepyMcpRuntime:
|
|
|
189
194
|
),
|
|
190
195
|
)
|
|
191
196
|
else:
|
|
192
|
-
params = {"url": definition.url or ""}
|
|
197
|
+
params: MCPServerStreamableHttpParams = {"url": definition.url or ""}
|
|
193
198
|
if definition.headers:
|
|
194
199
|
params["headers"] = dict(definition.headers)
|
|
195
200
|
server = MCPServerStreamableHttp(
|
|
@@ -45,7 +45,7 @@ def build_system_prompt(
|
|
|
45
45
|
Core rules:
|
|
46
46
|
- Work in the repo with tools: inspect, modify, test, verify.
|
|
47
47
|
- Preserve user changes. Prefer small, verifiable edits.
|
|
48
|
-
- Read
|
|
48
|
+
- Read existing files when you need context; exact `modify` edits can establish the managed snapshot internally.
|
|
49
49
|
- Use `modify` for file changes: `content` only creates new files; existing files use `old_string`/`new_string`.
|
|
50
50
|
- After project generators create scaffold files, read and edit the generated block instead of replacing the file.
|
|
51
51
|
- Run shell commands using the Runtime context's command dialect and path style: `powershell` -> PowerShell with Windows paths; `cmd` -> cmd; `posix` -> POSIX shell.
|
|
@@ -116,7 +116,7 @@ def _format_skills(skills: Iterable[SkillInfo], *, include_paths: bool) -> str:
|
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
def _builtin_skills_root() -> Path:
|
|
119
|
-
return Path(resources.files("deepy.data").joinpath("skills"))
|
|
119
|
+
return Path(str(resources.files("deepy.data").joinpath("skills")))
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
def _discover_skills_root(root: Path, *, scope: str) -> list[SkillInfo]:
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
from deepy.utils import json as json_utils
|
|
6
6
|
|
|
7
7
|
from .builtin import ToolRuntime
|
|
8
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from agents import Tool
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
def build_function_tools(
|
|
11
14
|
runtime: ToolRuntime,
|
|
12
15
|
*,
|
|
13
16
|
preferred_mcp_web_search_tools: list[str] | None = None,
|
|
14
|
-
) -> list[
|
|
17
|
+
) -> list[Tool]:
|
|
15
18
|
from agents.tool import FunctionTool
|
|
16
19
|
|
|
17
20
|
async def invoke_shell(_context: object, raw_input: str) -> str:
|
|
@@ -104,7 +107,8 @@ def build_function_tools(
|
|
|
104
107
|
name="modify",
|
|
105
108
|
description=(
|
|
106
109
|
"Create new files or edit existing files. Use content only for files that do not "
|
|
107
|
-
"exist. For existing files, read first
|
|
110
|
+
"exist. For existing files, use old_string/new_string; read first when you need "
|
|
111
|
+
"to inspect context."
|
|
108
112
|
),
|
|
109
113
|
params_json_schema=MODIFY_SCHEMA,
|
|
110
114
|
on_invoke_tool=invoke_modify,
|
|
@@ -22,6 +22,7 @@ from html.parser import HTMLParser
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
|
|
24
24
|
from deepy.config import DEFAULT_WEB_SEARCH_SEARXNG_URL, Settings, mask_secret
|
|
25
|
+
from deepy.types.tool_payloads import AskUserOption, AskUserQuestion
|
|
25
26
|
from deepy.utils import json as json_utils
|
|
26
27
|
|
|
27
28
|
from .file_state import FileSnippet, FileState
|
|
@@ -728,7 +729,7 @@ def _decide_search_language_with_llm(query: str, settings: Settings) -> dict[str
|
|
|
728
729
|
)
|
|
729
730
|
parsed = _parse_json_response(_web_search_chat(settings, prompt))
|
|
730
731
|
dominant_language = parsed.get("dominant_language")
|
|
731
|
-
if dominant_language not in {"en", "zh"}:
|
|
732
|
+
if not isinstance(dominant_language, str) or dominant_language not in {"en", "zh"}:
|
|
732
733
|
raise ValueError(f"Unexpected dominant language: {dominant_language}")
|
|
733
734
|
reason = parsed.get("reason")
|
|
734
735
|
return {
|
|
@@ -1249,7 +1250,7 @@ class ToolRuntime:
|
|
|
1249
1250
|
)
|
|
1250
1251
|
self.file_state.mark_read(target, full=False)
|
|
1251
1252
|
snippet_metadata = _snippet_metadata(snippet)
|
|
1252
|
-
metadata = {
|
|
1253
|
+
metadata: dict[str, object] = {
|
|
1253
1254
|
"path": str(target),
|
|
1254
1255
|
"kind": "file",
|
|
1255
1256
|
"startLine": start + 1,
|
|
@@ -1301,7 +1302,14 @@ class ToolRuntime:
|
|
|
1301
1302
|
"modify",
|
|
1302
1303
|
"Provide content for a new file, or both old_string and new_string for an existing file.",
|
|
1303
1304
|
).to_json()
|
|
1304
|
-
return self.edit(
|
|
1305
|
+
return self.edit(
|
|
1306
|
+
path,
|
|
1307
|
+
old,
|
|
1308
|
+
new,
|
|
1309
|
+
replace_all=replace_all,
|
|
1310
|
+
snippet_id=snippet_id,
|
|
1311
|
+
auto_read_if_missing_snapshot=True,
|
|
1312
|
+
)
|
|
1305
1313
|
|
|
1306
1314
|
def write(self, path: str, content: object) -> str:
|
|
1307
1315
|
name = "write"
|
|
@@ -1322,7 +1330,7 @@ class ToolRuntime:
|
|
|
1322
1330
|
encoding = (
|
|
1323
1331
|
existing_metadata.encoding
|
|
1324
1332
|
if existing_metadata is not None
|
|
1325
|
-
else _default_new_text_encoding(
|
|
1333
|
+
else _default_new_text_encoding()
|
|
1326
1334
|
)
|
|
1327
1335
|
line_endings = _detect_line_endings(old_content or text_content)
|
|
1328
1336
|
normalized_content = _normalize_line_endings(text_content, line_endings)
|
|
@@ -1350,6 +1358,7 @@ class ToolRuntime:
|
|
|
1350
1358
|
new: str,
|
|
1351
1359
|
replace_all: bool = False,
|
|
1352
1360
|
snippet_id: str | None = None,
|
|
1361
|
+
auto_read_if_missing_snapshot: bool = False,
|
|
1353
1362
|
) -> str:
|
|
1354
1363
|
name = "edit"
|
|
1355
1364
|
if not old:
|
|
@@ -1376,6 +1385,14 @@ class ToolRuntime:
|
|
|
1376
1385
|
target = _resolve_in_cwd(self.cwd, path)
|
|
1377
1386
|
if not target.exists():
|
|
1378
1387
|
return ToolResult.error_result(name, f"File does not exist: {target}").to_json()
|
|
1388
|
+
auto_read_before_modify = False
|
|
1389
|
+
if (
|
|
1390
|
+
auto_read_if_missing_snapshot
|
|
1391
|
+
and snippet is None
|
|
1392
|
+
and self.file_state.snapshot_status(target) == "missing"
|
|
1393
|
+
):
|
|
1394
|
+
self.file_state.mark_read(target)
|
|
1395
|
+
auto_read_before_modify = True
|
|
1379
1396
|
ok, error = self.file_state.check_writable(
|
|
1380
1397
|
target,
|
|
1381
1398
|
require_read=True,
|
|
@@ -1453,7 +1470,7 @@ class ToolRuntime:
|
|
|
1453
1470
|
_write_text_with_encoding(target, updated, text_metadata.encoding)
|
|
1454
1471
|
self.file_state.mark_written(target)
|
|
1455
1472
|
diff = _unified_diff(text, updated, path=str(target))
|
|
1456
|
-
metadata = {
|
|
1473
|
+
metadata: dict[str, object] = {
|
|
1457
1474
|
"path": str(target),
|
|
1458
1475
|
"file_path": str(target),
|
|
1459
1476
|
"occurrences": occurrences if replace_all else 1,
|
|
@@ -1464,6 +1481,8 @@ class ToolRuntime:
|
|
|
1464
1481
|
"diff": diff,
|
|
1465
1482
|
"diff_preview": diff,
|
|
1466
1483
|
}
|
|
1484
|
+
if auto_read_before_modify:
|
|
1485
|
+
metadata["autoReadBeforeModify"] = True
|
|
1467
1486
|
if snippet is not None:
|
|
1468
1487
|
metadata["scope"] = _format_scope_metadata(target, snippet, scope, text)
|
|
1469
1488
|
return ToolResult.ok_result(name, f"Edited {target}", metadata=metadata).to_json()
|
|
@@ -1542,7 +1561,6 @@ class ToolRuntime:
|
|
|
1542
1561
|
self.cwd = final_cwd
|
|
1543
1562
|
returncode = exit_code if exit_code is not None else process.returncode
|
|
1544
1563
|
output, output_truncated = _truncate_output(stdout + (stderr or ""))
|
|
1545
|
-
result = ToolResult.ok_result if returncode == 0 else ToolResult.error_result
|
|
1546
1564
|
metadata = _shell_metadata(
|
|
1547
1565
|
self.cwd,
|
|
1548
1566
|
process_id,
|
|
@@ -1558,12 +1576,12 @@ class ToolRuntime:
|
|
|
1558
1576
|
}
|
|
1559
1577
|
)
|
|
1560
1578
|
if returncode == 0:
|
|
1561
|
-
return
|
|
1579
|
+
return ToolResult.ok_result(
|
|
1562
1580
|
name,
|
|
1563
1581
|
output,
|
|
1564
1582
|
metadata=metadata,
|
|
1565
1583
|
).to_json()
|
|
1566
|
-
return
|
|
1584
|
+
return ToolResult.error_result(
|
|
1567
1585
|
name,
|
|
1568
1586
|
f"Command exited with code {returncode}.",
|
|
1569
1587
|
output=output,
|
|
@@ -1916,17 +1934,10 @@ def _python_text_encoding(encoding: str) -> str:
|
|
|
1916
1934
|
return "utf8"
|
|
1917
1935
|
|
|
1918
1936
|
|
|
1919
|
-
def _default_new_text_encoding(
|
|
1920
|
-
resolved_platform = platform_name or sys.platform
|
|
1921
|
-
if resolved_platform.startswith("win") and _contains_non_ascii(content):
|
|
1922
|
-
return "utf8-sig"
|
|
1937
|
+
def _default_new_text_encoding() -> str:
|
|
1923
1938
|
return "utf8"
|
|
1924
1939
|
|
|
1925
1940
|
|
|
1926
|
-
def _contains_non_ascii(text: str) -> bool:
|
|
1927
|
-
return any(ord(char) > 0x7F for char in text)
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
1941
|
def _write_text_with_encoding(path: Path, content: str, encoding: str) -> None:
|
|
1931
1942
|
path.write_bytes(content.encode(_python_text_encoding(encoding)))
|
|
1932
1943
|
|
|
@@ -1974,24 +1985,24 @@ def _format_notebook(path: Path) -> tuple[str, str | None]:
|
|
|
1974
1985
|
cells = parsed.get("cells")
|
|
1975
1986
|
lines: list[str] = []
|
|
1976
1987
|
if isinstance(cells, list):
|
|
1977
|
-
for index,
|
|
1978
|
-
|
|
1988
|
+
for index, raw_cell in enumerate(cells):
|
|
1989
|
+
cell = _string_key_dict(raw_cell)
|
|
1990
|
+
if cell is None:
|
|
1979
1991
|
continue
|
|
1980
|
-
|
|
1992
|
+
raw_cell_type = cell.get("cell_type")
|
|
1993
|
+
cell_type = raw_cell_type if isinstance(raw_cell_type, str) else "unknown"
|
|
1981
1994
|
lines.append(f"# Cell {index + 1} ({cell_type})")
|
|
1982
1995
|
lines.extend(_normalize_notebook_field(cell.get("source")))
|
|
1983
1996
|
|
|
1984
1997
|
outputs = cell.get("outputs")
|
|
1985
1998
|
if not isinstance(outputs, list):
|
|
1986
1999
|
continue
|
|
1987
|
-
for output_index,
|
|
1988
|
-
|
|
2000
|
+
for output_index, raw_output in enumerate(outputs):
|
|
2001
|
+
output = _string_key_dict(raw_output)
|
|
2002
|
+
if output is None:
|
|
1989
2003
|
continue
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
if isinstance(output.get("output_type"), str)
|
|
1993
|
-
else "output"
|
|
1994
|
-
)
|
|
2004
|
+
raw_output_type = output.get("output_type")
|
|
2005
|
+
output_type = raw_output_type if isinstance(raw_output_type, str) else "output"
|
|
1995
2006
|
lines.append(f"# Output {output_index + 1} ({output_type})")
|
|
1996
2007
|
lines.extend(_format_notebook_output(output))
|
|
1997
2008
|
|
|
@@ -2011,12 +2022,13 @@ def _normalize_notebook_field(value: object) -> list[str]:
|
|
|
2011
2022
|
def _format_notebook_output(output: dict[str, object]) -> list[str]:
|
|
2012
2023
|
lines = _normalize_notebook_field(output.get("text"))
|
|
2013
2024
|
data = output.get("data")
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2025
|
+
data_dict = _string_key_dict(data)
|
|
2026
|
+
if data_dict is not None:
|
|
2027
|
+
lines.extend(_normalize_notebook_field(data_dict.get("text/plain")))
|
|
2028
|
+
image_png = data_dict.get("image/png")
|
|
2017
2029
|
if isinstance(image_png, str):
|
|
2018
2030
|
lines.append(f"[image/png {len(image_png)} chars]")
|
|
2019
|
-
image_jpeg =
|
|
2031
|
+
image_jpeg = data_dict.get("image/jpeg")
|
|
2020
2032
|
if isinstance(image_jpeg, str):
|
|
2021
2033
|
lines.append(f"[image/jpeg {len(image_jpeg)} chars]")
|
|
2022
2034
|
traceback = output.get("traceback")
|
|
@@ -2025,6 +2037,14 @@ def _format_notebook_output(output: dict[str, object]) -> list[str]:
|
|
|
2025
2037
|
return lines or ["[output omitted]"]
|
|
2026
2038
|
|
|
2027
2039
|
|
|
2040
|
+
def _string_key_dict(value: object) -> dict[str, object] | None:
|
|
2041
|
+
if not isinstance(value, dict):
|
|
2042
|
+
return None
|
|
2043
|
+
if not all(isinstance(key, str) for key in value):
|
|
2044
|
+
return None
|
|
2045
|
+
return {key: item for key, item in value.items() if isinstance(key, str)}
|
|
2046
|
+
|
|
2047
|
+
|
|
2028
2048
|
@dataclass(frozen=True)
|
|
2029
2049
|
class PageRange:
|
|
2030
2050
|
start: int
|
|
@@ -2360,7 +2380,7 @@ def _now_iso() -> str:
|
|
|
2360
2380
|
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
2361
2381
|
|
|
2362
2382
|
|
|
2363
|
-
def _terminate_process(process: subprocess.Popen[
|
|
2383
|
+
def _terminate_process(process: subprocess.Popen[bytes]) -> None:
|
|
2364
2384
|
try:
|
|
2365
2385
|
if os.name != "nt":
|
|
2366
2386
|
os.killpg(process.pid, signal.SIGKILL)
|
|
@@ -2483,13 +2503,14 @@ def _gitignore_pattern_matches(pattern: str, relative_path: str, is_dir: bool) -
|
|
|
2483
2503
|
return any(fnmatch(part, normalized_pattern) for part in parts)
|
|
2484
2504
|
|
|
2485
2505
|
|
|
2486
|
-
def _parse_ask_user_questions(value: object) -> tuple[list[
|
|
2506
|
+
def _parse_ask_user_questions(value: object) -> tuple[list[AskUserQuestion], str | None]:
|
|
2487
2507
|
if not isinstance(value, list) or not value:
|
|
2488
2508
|
return [], '"questions" must be a non-empty array.'
|
|
2489
2509
|
|
|
2490
|
-
questions: list[
|
|
2491
|
-
for index,
|
|
2492
|
-
|
|
2510
|
+
questions: list[AskUserQuestion] = []
|
|
2511
|
+
for index, raw_item in enumerate(value):
|
|
2512
|
+
item = _string_key_dict(raw_item)
|
|
2513
|
+
if item is None:
|
|
2493
2514
|
return [], f"Question at index {index} must be an object."
|
|
2494
2515
|
|
|
2495
2516
|
question = _trimmed_string(item.get("question"))
|
|
@@ -2500,9 +2521,10 @@ def _parse_ask_user_questions(value: object) -> tuple[list[dict[str, object]], s
|
|
|
2500
2521
|
if not isinstance(raw_options, list) or not raw_options:
|
|
2501
2522
|
return [], f'Question at index {index} must include a non-empty "options" array.'
|
|
2502
2523
|
|
|
2503
|
-
options: list[
|
|
2504
|
-
for option_index,
|
|
2505
|
-
|
|
2524
|
+
options: list[AskUserOption] = []
|
|
2525
|
+
for option_index, raw_option in enumerate(raw_options):
|
|
2526
|
+
option = _string_key_dict(raw_option)
|
|
2527
|
+
if option is None:
|
|
2506
2528
|
return [], f"Option {option_index} for question {index} must be an object."
|
|
2507
2529
|
|
|
2508
2530
|
label = _trimmed_string(option.get("label"))
|
|
@@ -2512,13 +2534,13 @@ def _parse_ask_user_questions(value: object) -> tuple[list[dict[str, object]], s
|
|
|
2512
2534
|
f'Option {option_index} for question {index} is missing a non-empty "label" string.',
|
|
2513
2535
|
)
|
|
2514
2536
|
|
|
2515
|
-
parsed_option = {"label": label}
|
|
2537
|
+
parsed_option: AskUserOption = {"label": label}
|
|
2516
2538
|
description = _trimmed_string(option.get("description"))
|
|
2517
2539
|
if description:
|
|
2518
2540
|
parsed_option["description"] = description
|
|
2519
2541
|
options.append(parsed_option)
|
|
2520
2542
|
|
|
2521
|
-
parsed_question:
|
|
2543
|
+
parsed_question: AskUserQuestion = {
|
|
2522
2544
|
"question": question,
|
|
2523
2545
|
"options": options,
|
|
2524
2546
|
}
|
|
@@ -2530,15 +2552,13 @@ def _parse_ask_user_questions(value: object) -> tuple[list[dict[str, object]], s
|
|
|
2530
2552
|
return questions, None
|
|
2531
2553
|
|
|
2532
2554
|
|
|
2533
|
-
def _build_question_summary(questions: list[
|
|
2555
|
+
def _build_question_summary(questions: list[AskUserQuestion]) -> str:
|
|
2534
2556
|
lines = ["Waiting for user input."]
|
|
2535
2557
|
for index, item in enumerate(questions):
|
|
2536
2558
|
lines.append("")
|
|
2537
2559
|
lines.append(f"{index + 1}. {item['question']}")
|
|
2538
2560
|
lines.append(f" Mode: {'multi-select' if item.get('multiSelect') else 'single-select'}")
|
|
2539
2561
|
for option in item["options"]:
|
|
2540
|
-
if not isinstance(option, dict):
|
|
2541
|
-
continue
|
|
2542
2562
|
lines.append(f" - {option['label']}")
|
|
2543
2563
|
if option.get("description"):
|
|
2544
2564
|
lines.append(f" {option['description']}")
|
|
@@ -2,6 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
SnapshotStatus = Literal["missing", "full", "partial", "deleted", "stale"]
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
@dataclass
|
|
@@ -58,6 +62,18 @@ class FileState:
|
|
|
58
62
|
return False, "File changed since it was read; read it again before editing."
|
|
59
63
|
return True, None
|
|
60
64
|
|
|
65
|
+
def snapshot_status(self, path: Path) -> SnapshotStatus:
|
|
66
|
+
resolved = path.resolve()
|
|
67
|
+
snapshot = self._snapshots.get(resolved)
|
|
68
|
+
if snapshot is None:
|
|
69
|
+
return "missing"
|
|
70
|
+
if not resolved.exists():
|
|
71
|
+
return "deleted"
|
|
72
|
+
stat = resolved.stat()
|
|
73
|
+
if stat.st_mtime_ns != snapshot.mtime_ns or stat.st_size != snapshot.size:
|
|
74
|
+
return "stale"
|
|
75
|
+
return "full" if snapshot.full_read else "partial"
|
|
76
|
+
|
|
61
77
|
def mark_written(self, path: Path) -> None:
|
|
62
78
|
if path.exists():
|
|
63
79
|
self.mark_read(path)
|