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.
Files changed (69) hide show
  1. deepy/__init__.py +9 -0
  2. deepy/__main__.py +7 -0
  3. deepy/cli.py +413 -0
  4. deepy/config/__init__.py +21 -0
  5. deepy/config/settings.py +237 -0
  6. deepy/data/__init__.py +1 -0
  7. deepy/data/tools/AskUserQuestion.md +10 -0
  8. deepy/data/tools/WebFetch.md +9 -0
  9. deepy/data/tools/WebSearch.md +9 -0
  10. deepy/data/tools/__init__.py +1 -0
  11. deepy/data/tools/bash.md +7 -0
  12. deepy/data/tools/edit.md +13 -0
  13. deepy/data/tools/modify.md +17 -0
  14. deepy/data/tools/read.md +8 -0
  15. deepy/data/tools/write.md +12 -0
  16. deepy/errors.py +63 -0
  17. deepy/llm/__init__.py +13 -0
  18. deepy/llm/agent.py +31 -0
  19. deepy/llm/context.py +109 -0
  20. deepy/llm/events.py +187 -0
  21. deepy/llm/model_capabilities.py +7 -0
  22. deepy/llm/provider.py +81 -0
  23. deepy/llm/replay.py +120 -0
  24. deepy/llm/runner.py +412 -0
  25. deepy/llm/thinking.py +30 -0
  26. deepy/prompts/__init__.py +6 -0
  27. deepy/prompts/compact.py +100 -0
  28. deepy/prompts/rules.py +24 -0
  29. deepy/prompts/runtime_context.py +98 -0
  30. deepy/prompts/system.py +72 -0
  31. deepy/prompts/tool_docs.py +21 -0
  32. deepy/sessions/__init__.py +17 -0
  33. deepy/sessions/jsonl.py +306 -0
  34. deepy/sessions/manager.py +202 -0
  35. deepy/skills.py +202 -0
  36. deepy/status.py +65 -0
  37. deepy/tools/__init__.py +6 -0
  38. deepy/tools/agents.py +343 -0
  39. deepy/tools/builtin.py +2113 -0
  40. deepy/tools/file_state.py +85 -0
  41. deepy/tools/result.py +54 -0
  42. deepy/tools/shell_utils.py +83 -0
  43. deepy/ui/__init__.py +5 -0
  44. deepy/ui/app.py +118 -0
  45. deepy/ui/ask_user_question.py +182 -0
  46. deepy/ui/exit_summary.py +142 -0
  47. deepy/ui/loading_text.py +87 -0
  48. deepy/ui/markdown.py +152 -0
  49. deepy/ui/message_view.py +546 -0
  50. deepy/ui/prompt_buffer.py +176 -0
  51. deepy/ui/prompt_input.py +286 -0
  52. deepy/ui/session_list.py +140 -0
  53. deepy/ui/session_picker.py +179 -0
  54. deepy/ui/slash_commands.py +67 -0
  55. deepy/ui/styles.py +21 -0
  56. deepy/ui/terminal.py +959 -0
  57. deepy/ui/thinking_state.py +29 -0
  58. deepy/ui/welcome.py +195 -0
  59. deepy/update_check.py +195 -0
  60. deepy/usage.py +192 -0
  61. deepy/utils/__init__.py +15 -0
  62. deepy/utils/debug_logger.py +62 -0
  63. deepy/utils/error_logger.py +107 -0
  64. deepy/utils/json.py +29 -0
  65. deepy/utils/notify.py +66 -0
  66. deepy_cli-0.1.1.dist-info/METADATA +205 -0
  67. deepy_cli-0.1.1.dist-info/RECORD +69 -0
  68. deepy_cli-0.1.1.dist-info/WHEEL +4 -0
  69. 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