deepy-cli 0.1.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.
- deepy/__init__.py +9 -0
- deepy/__main__.py +7 -0
- deepy/cli.py +413 -0
- deepy/config/__init__.py +21 -0
- deepy/config/settings.py +237 -0
- deepy/data/__init__.py +1 -0
- deepy/data/tools/AskUserQuestion.md +10 -0
- deepy/data/tools/WebFetch.md +9 -0
- deepy/data/tools/WebSearch.md +9 -0
- deepy/data/tools/__init__.py +1 -0
- deepy/data/tools/bash.md +7 -0
- deepy/data/tools/edit.md +13 -0
- deepy/data/tools/modify.md +17 -0
- deepy/data/tools/read.md +8 -0
- deepy/data/tools/write.md +12 -0
- deepy/errors.py +63 -0
- deepy/llm/__init__.py +13 -0
- deepy/llm/agent.py +31 -0
- deepy/llm/context.py +109 -0
- deepy/llm/events.py +187 -0
- deepy/llm/model_capabilities.py +7 -0
- deepy/llm/provider.py +81 -0
- deepy/llm/replay.py +120 -0
- deepy/llm/runner.py +412 -0
- deepy/llm/thinking.py +30 -0
- deepy/prompts/__init__.py +6 -0
- deepy/prompts/compact.py +100 -0
- deepy/prompts/rules.py +24 -0
- deepy/prompts/runtime_context.py +98 -0
- deepy/prompts/system.py +72 -0
- deepy/prompts/tool_docs.py +21 -0
- deepy/sessions/__init__.py +17 -0
- deepy/sessions/jsonl.py +306 -0
- deepy/sessions/manager.py +202 -0
- deepy/skills.py +202 -0
- deepy/status.py +65 -0
- deepy/tools/__init__.py +6 -0
- deepy/tools/agents.py +343 -0
- deepy/tools/builtin.py +2113 -0
- deepy/tools/file_state.py +85 -0
- deepy/tools/result.py +54 -0
- deepy/tools/shell_utils.py +83 -0
- deepy/ui/__init__.py +5 -0
- deepy/ui/app.py +118 -0
- deepy/ui/ask_user_question.py +182 -0
- deepy/ui/exit_summary.py +142 -0
- deepy/ui/loading_text.py +87 -0
- deepy/ui/markdown.py +152 -0
- deepy/ui/message_view.py +546 -0
- deepy/ui/prompt_buffer.py +176 -0
- deepy/ui/prompt_input.py +286 -0
- deepy/ui/session_list.py +140 -0
- deepy/ui/session_picker.py +179 -0
- deepy/ui/slash_commands.py +67 -0
- deepy/ui/styles.py +21 -0
- deepy/ui/terminal.py +959 -0
- deepy/ui/thinking_state.py +29 -0
- deepy/ui/welcome.py +195 -0
- deepy/update_check.py +195 -0
- deepy/usage.py +192 -0
- deepy/utils/__init__.py +15 -0
- deepy/utils/debug_logger.py +62 -0
- deepy/utils/error_logger.py +107 -0
- deepy/utils/json.py +29 -0
- deepy/utils/notify.py +66 -0
- deepy_cli-0.1.1.dist-info/METADATA +205 -0
- deepy_cli-0.1.1.dist-info/RECORD +69 -0
- deepy_cli-0.1.1.dist-info/WHEEL +4 -0
- deepy_cli-0.1.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit.application import Application
|
|
8
|
+
from prompt_toolkit.formatted_text import StyleAndTextTuples
|
|
9
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
10
|
+
from prompt_toolkit.key_binding import KeyPressEvent
|
|
11
|
+
from prompt_toolkit.layout import HSplit
|
|
12
|
+
from prompt_toolkit.layout import Layout
|
|
13
|
+
from prompt_toolkit.layout import Window
|
|
14
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
15
|
+
from prompt_toolkit.styles import Style
|
|
16
|
+
from prompt_toolkit.widgets import Box
|
|
17
|
+
from prompt_toolkit.widgets import Frame
|
|
18
|
+
from prompt_toolkit.widgets import RadioList
|
|
19
|
+
|
|
20
|
+
from deepy.ui.session_list import format_session_title
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_EMPTY_SESSION_ID = "__empty__"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ResumeSessionPreview:
|
|
28
|
+
id: str
|
|
29
|
+
title: str
|
|
30
|
+
status: str
|
|
31
|
+
updated_at: int
|
|
32
|
+
active_tokens: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def pick_resume_session(previews: Sequence[ResumeSessionPreview]) -> str | None:
|
|
36
|
+
if not previews:
|
|
37
|
+
return None
|
|
38
|
+
return ResumeSessionPicker(previews).run()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_session_time(timestamp: int) -> str:
|
|
42
|
+
if timestamp <= 0:
|
|
43
|
+
return "unknown time"
|
|
44
|
+
seconds = timestamp / 1000 if timestamp > 10_000_000_000 else timestamp
|
|
45
|
+
return datetime.fromtimestamp(seconds).strftime("%Y-%m-%d %H:%M:%S")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def format_resume_session_label(preview: ResumeSessionPreview) -> str:
|
|
49
|
+
title = format_session_title(preview.title, max_chars=70)
|
|
50
|
+
meta = (
|
|
51
|
+
f" {format_session_time(preview.updated_at)}"
|
|
52
|
+
f" · {preview.status}"
|
|
53
|
+
f" · history {preview.active_tokens:,} est"
|
|
54
|
+
f" · {preview.id[:8]}"
|
|
55
|
+
)
|
|
56
|
+
return f"{title}\n{meta}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def format_resume_session_choices(
|
|
60
|
+
previews: Sequence[ResumeSessionPreview],
|
|
61
|
+
*,
|
|
62
|
+
max_entries: int = 10,
|
|
63
|
+
) -> str:
|
|
64
|
+
if not previews:
|
|
65
|
+
return "No sessions found."
|
|
66
|
+
lines = [f"Resume a session ({len(previews)} total)"]
|
|
67
|
+
for index, preview in enumerate(previews[:max_entries], 1):
|
|
68
|
+
lines.append(f"{index}. {format_resume_session_label(preview)}")
|
|
69
|
+
remaining = len(previews) - min(len(previews), max_entries)
|
|
70
|
+
if remaining > 0:
|
|
71
|
+
lines.append(f"...and {remaining} more.")
|
|
72
|
+
return "\n".join(lines)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ResumeSessionPicker:
|
|
76
|
+
def __init__(self, previews: Sequence[ResumeSessionPreview]) -> None:
|
|
77
|
+
self._previews = list(previews)
|
|
78
|
+
self._radio_list = RadioList[str](
|
|
79
|
+
values=self._build_values(),
|
|
80
|
+
default=self._previews[0].id if self._previews else _EMPTY_SESSION_ID,
|
|
81
|
+
show_numbers=False,
|
|
82
|
+
select_on_focus=True,
|
|
83
|
+
open_character="",
|
|
84
|
+
select_character="›",
|
|
85
|
+
close_character="",
|
|
86
|
+
show_cursor=False,
|
|
87
|
+
show_scrollbar=False,
|
|
88
|
+
container_style="class:session-list",
|
|
89
|
+
checked_style="class:session-list.checked",
|
|
90
|
+
)
|
|
91
|
+
self._app = self._build_app()
|
|
92
|
+
|
|
93
|
+
def run(self) -> str | None:
|
|
94
|
+
result = self._app.run()
|
|
95
|
+
if result in {None, _EMPTY_SESSION_ID}:
|
|
96
|
+
return None
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
def _build_values(self) -> list[tuple[str, str]]:
|
|
100
|
+
if not self._previews:
|
|
101
|
+
return [(_EMPTY_SESSION_ID, "No sessions found.")]
|
|
102
|
+
return [(preview.id, format_resume_session_label(preview)) for preview in self._previews]
|
|
103
|
+
|
|
104
|
+
def _header_fragments(self) -> StyleAndTextTuples:
|
|
105
|
+
total = len(self._previews)
|
|
106
|
+
if total <= 0:
|
|
107
|
+
selected = 0
|
|
108
|
+
else:
|
|
109
|
+
selected = min(self._radio_list._selected_index + 1, total) # pyright: ignore[reportPrivateUsage]
|
|
110
|
+
return [
|
|
111
|
+
("class:header.title", f" Resume a session ({total} total) "),
|
|
112
|
+
("class:header.meta", f" {selected}/{total} "),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
def _footer_fragments(self) -> StyleAndTextTuples:
|
|
116
|
+
return [
|
|
117
|
+
("class:footer.text", " ↑/↓ navigate"),
|
|
118
|
+
("class:footer.text", " · "),
|
|
119
|
+
("class:footer.text", "PgUp/PgDn page"),
|
|
120
|
+
("class:footer.text", " · "),
|
|
121
|
+
("class:footer.text", "Enter select"),
|
|
122
|
+
("class:footer.text", " · "),
|
|
123
|
+
("class:footer.text", "Esc cancel "),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
def _build_app(self) -> Application[str | None]:
|
|
127
|
+
kb = KeyBindings()
|
|
128
|
+
|
|
129
|
+
@kb.add("escape")
|
|
130
|
+
@kb.add("c-c")
|
|
131
|
+
def _cancel(event: KeyPressEvent) -> None:
|
|
132
|
+
event.app.exit(result=None)
|
|
133
|
+
|
|
134
|
+
@kb.add("enter", eager=True)
|
|
135
|
+
def _select(event: KeyPressEvent) -> None:
|
|
136
|
+
event.app.exit(result=self._radio_list.current_value)
|
|
137
|
+
|
|
138
|
+
_ = (_cancel, _select)
|
|
139
|
+
|
|
140
|
+
header = Window(
|
|
141
|
+
FormattedTextControl(self._header_fragments),
|
|
142
|
+
height=1,
|
|
143
|
+
style="class:header",
|
|
144
|
+
)
|
|
145
|
+
body = Frame(
|
|
146
|
+
Box(self._radio_list, padding=1),
|
|
147
|
+
title=lambda: " Sessions ",
|
|
148
|
+
)
|
|
149
|
+
footer = Window(
|
|
150
|
+
FormattedTextControl(self._footer_fragments),
|
|
151
|
+
height=1,
|
|
152
|
+
style="class:footer",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return Application(
|
|
156
|
+
layout=Layout(HSplit([header, body, footer]), focused_element=self._radio_list),
|
|
157
|
+
key_bindings=kb,
|
|
158
|
+
full_screen=True,
|
|
159
|
+
erase_when_done=True,
|
|
160
|
+
mouse_support=True,
|
|
161
|
+
style=_session_picker_style(),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _session_picker_style() -> Style:
|
|
166
|
+
return Style.from_dict(
|
|
167
|
+
{
|
|
168
|
+
"header": "bg:#1f2333 #8be9fd",
|
|
169
|
+
"header.title": "bold",
|
|
170
|
+
"header.meta": "#8a90aa",
|
|
171
|
+
"frame.border": "#5f6688",
|
|
172
|
+
"frame.label": "#8be9fd bold",
|
|
173
|
+
"session-list": "#c6d0f5",
|
|
174
|
+
"session-list.checked": "#8be9fd bold",
|
|
175
|
+
"radio-selected": "#8be9fd bold",
|
|
176
|
+
"footer": "bg:#1f2333 #8a90aa",
|
|
177
|
+
"footer.text": "#8a90aa",
|
|
178
|
+
}
|
|
179
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from deepy.skills import SkillInfo
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class SlashCommandItem:
|
|
11
|
+
kind: str
|
|
12
|
+
name: str
|
|
13
|
+
label: str
|
|
14
|
+
description: str
|
|
15
|
+
skill: SkillInfo | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
BUILTIN_SLASH_COMMANDS = (
|
|
19
|
+
SlashCommandItem("skills", "skills", "/skills", "List available skills"),
|
|
20
|
+
SlashCommandItem("new", "new", "/new", "Start a fresh conversation"),
|
|
21
|
+
SlashCommandItem("resume", "resume", "/resume", "Pick a previous conversation to continue"),
|
|
22
|
+
SlashCommandItem("exit", "exit", "/exit", "Quit Deepy"),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_slash_commands(skills: list[SkillInfo]) -> list[SlashCommandItem]:
|
|
27
|
+
skill_items = [
|
|
28
|
+
SlashCommandItem(
|
|
29
|
+
kind="skill",
|
|
30
|
+
name=skill.name,
|
|
31
|
+
label=f"/{skill.name}",
|
|
32
|
+
description=skill.description or "(no description)",
|
|
33
|
+
skill=skill,
|
|
34
|
+
)
|
|
35
|
+
for skill in skills
|
|
36
|
+
]
|
|
37
|
+
return [*skill_items, *BUILTIN_SLASH_COMMANDS]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def filter_slash_commands(items: list[SlashCommandItem], token: str) -> list[SlashCommandItem]:
|
|
41
|
+
if not token.startswith("/"):
|
|
42
|
+
return []
|
|
43
|
+
query = token[1:].lower()
|
|
44
|
+
if not query:
|
|
45
|
+
return items
|
|
46
|
+
return [item for item in items if query in item.name.lower()]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_exact_slash_command(
|
|
50
|
+
items: list[SlashCommandItem],
|
|
51
|
+
token: str,
|
|
52
|
+
) -> SlashCommandItem | None:
|
|
53
|
+
if not token.startswith("/"):
|
|
54
|
+
return None
|
|
55
|
+
query = token[1:]
|
|
56
|
+
matches = [item for item in items if item.name == query]
|
|
57
|
+
builtin = next((item for item in matches if item.kind != "skill"), None)
|
|
58
|
+
return builtin or (matches[0] if matches else None)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def format_slash_command_description(description: str) -> str:
|
|
62
|
+
return re.sub(r"\s+", " ", description or "(no description)").strip()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def format_slash_command_label(item: SlashCommandItem) -> str:
|
|
66
|
+
loaded = bool(item.skill and item.skill.is_loaded)
|
|
67
|
+
return f"{item.label} *" if item.kind == "skill" and loaded else item.label
|
deepy/ui/styles.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
STYLE_MUTED = "dim"
|
|
5
|
+
STYLE_ACCENT = "bright_cyan"
|
|
6
|
+
STYLE_SUCCESS = "green"
|
|
7
|
+
STYLE_WARNING = "yellow"
|
|
8
|
+
STYLE_ERROR = "bold red"
|
|
9
|
+
STYLE_INFO = "bright_blue"
|
|
10
|
+
STYLE_ASSISTANT = "green"
|
|
11
|
+
STYLE_USER = "cyan"
|
|
12
|
+
STYLE_SYSTEM = "magenta"
|
|
13
|
+
STYLE_TOOL = "yellow"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def status_style(ok: bool | None) -> str:
|
|
17
|
+
if ok is True:
|
|
18
|
+
return STYLE_SUCCESS
|
|
19
|
+
if ok is False:
|
|
20
|
+
return STYLE_ERROR
|
|
21
|
+
return STYLE_WARNING
|