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,87 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Mapping
5
+
6
+ from deepy.usage import format_usage_line, normalize_usage
7
+
8
+
9
+ STALL_THRESHOLD_MS = 3_000
10
+
11
+
12
+ def build_loading_text(
13
+ *,
14
+ progress: Any | None,
15
+ now_ms: int | float,
16
+ processes: Mapping[str, Any] | None = None,
17
+ usage: Any | None = None,
18
+ ) -> str:
19
+ usage_suffix = _usage_suffix(usage)
20
+ process_text = build_process_loading_text(processes, now_ms=now_ms)
21
+ if process_text:
22
+ return f"{process_text}{usage_suffix}"
23
+
24
+ if progress is None:
25
+ return f"Thinking...{usage_suffix}"
26
+
27
+ started_at = parse_timestamp_ms(_field(progress, "startedAt"))
28
+ if started_at is None:
29
+ return f"Thinking...{usage_suffix}"
30
+
31
+ elapsed_ms = max(0, int(now_ms - started_at))
32
+ if elapsed_ms < STALL_THRESHOLD_MS:
33
+ return f"Thinking...{usage_suffix}"
34
+
35
+ elapsed_seconds = elapsed_ms // 1_000
36
+ tokens = _field(progress, "formattedTokens") or "0"
37
+ return f"Thinking... ({elapsed_seconds}s) · ↓ {tokens} tokens{usage_suffix}"
38
+
39
+
40
+ def build_process_loading_text(
41
+ processes: Mapping[str, Any] | None,
42
+ *,
43
+ now_ms: int | float,
44
+ ) -> str | None:
45
+ if not processes:
46
+ return None
47
+ first = next(iter(processes.values()), None)
48
+ if first is None:
49
+ return None
50
+ start_time = _field(first, "startTime") or ""
51
+ command = _field(first, "command") or "Running process..."
52
+ return f"({format_elapsed_time(start_time, now_ms=now_ms)}) {command}"
53
+
54
+
55
+ def format_elapsed_time(start_time_iso: str, *, now_ms: int | float) -> str:
56
+ start_time = parse_timestamp_ms(start_time_iso)
57
+ elapsed_ms = 0 if start_time is None else max(0, int(now_ms - start_time))
58
+ elapsed_seconds = elapsed_ms // 1_000
59
+ minutes = elapsed_seconds // 60
60
+ seconds = elapsed_seconds % 60
61
+ if minutes > 0:
62
+ return f"{minutes}m{seconds}s"
63
+ return f"{seconds}s"
64
+
65
+
66
+ def parse_timestamp_ms(value: Any) -> int | None:
67
+ if not isinstance(value, str):
68
+ return None
69
+ try:
70
+ normalized = value.replace("Z", "+00:00")
71
+ parsed = datetime.fromisoformat(normalized)
72
+ except ValueError:
73
+ return None
74
+ return int(parsed.timestamp() * 1_000)
75
+
76
+
77
+ def _field(value: Any, name: str) -> Any:
78
+ if isinstance(value, Mapping):
79
+ return value.get(name)
80
+ return getattr(value, name, None)
81
+
82
+
83
+ def _usage_suffix(usage: Any | None) -> str:
84
+ normalized = normalize_usage(usage)
85
+ if not normalized.known:
86
+ return ""
87
+ return f" · {format_usage_line(normalized)}"
deepy/ui/markdown.py ADDED
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from typing import Literal
6
+
7
+ from rich.text import Text
8
+
9
+
10
+ SegmentKind = Literal["text", "code"]
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class MarkdownSegment:
15
+ kind: SegmentKind
16
+ body: str
17
+ lang: str = ""
18
+
19
+
20
+ def render_markdown(text: str) -> Text:
21
+ output = Text()
22
+ if not text:
23
+ return output
24
+
25
+ for segment in split_by_fences(text):
26
+ if segment.kind == "code":
27
+ if segment.lang:
28
+ output.append(f"code {segment.lang}\n", style="dim")
29
+ for index, line in enumerate(segment.body.splitlines() or [""]):
30
+ if index:
31
+ output.append("\n")
32
+ output.append(f" {line}", style="bright_white on #1f2430")
33
+ else:
34
+ output.append(render_inline_block(segment.body))
35
+ return output
36
+
37
+
38
+ def split_by_fences(text: str) -> list[MarkdownSegment]:
39
+ segments: list[MarkdownSegment] = []
40
+ lines = text.splitlines()
41
+ buffer: list[str] = []
42
+ in_fence = False
43
+ fence_lang = ""
44
+ fence_body: list[str] = []
45
+
46
+ def flush_text() -> None:
47
+ nonlocal buffer
48
+ if buffer:
49
+ segments.append(MarkdownSegment(kind="text", body="\n".join(buffer)))
50
+ buffer = []
51
+
52
+ for line in lines:
53
+ fence_match = re.match(r"^\s*```(\w*)\s*$", line)
54
+ if fence_match:
55
+ if not in_fence:
56
+ flush_text()
57
+ in_fence = True
58
+ fence_lang = fence_match.group(1) or ""
59
+ fence_body = []
60
+ else:
61
+ segments.append(
62
+ MarkdownSegment(kind="code", lang=fence_lang, body="\n".join(fence_body))
63
+ )
64
+ in_fence = False
65
+ fence_lang = ""
66
+ fence_body = []
67
+ continue
68
+
69
+ if in_fence:
70
+ fence_body.append(line)
71
+ else:
72
+ buffer.append(line)
73
+
74
+ if in_fence:
75
+ segments.append(MarkdownSegment(kind="code", lang=fence_lang, body="\n".join(fence_body)))
76
+ else:
77
+ flush_text()
78
+ return segments
79
+
80
+
81
+ def render_inline_block(text: str) -> Text:
82
+ rendered = Text()
83
+ lines = text.split("\n")
84
+ for index, line in enumerate(lines):
85
+ if index:
86
+ rendered.append("\n")
87
+ rendered.append(render_inline_line(line))
88
+ return rendered
89
+
90
+
91
+ def render_inline_line(line: str) -> Text:
92
+ heading = re.match(r"^(\s*)(#{1,6})\s+(.*)$", line)
93
+ if heading:
94
+ lead, hashes, content = heading.groups()
95
+ rendered = Text(lead)
96
+ rendered.append(content, style="bold cyan" if len(hashes) > 2 else "bold bright_cyan")
97
+ return rendered
98
+
99
+ list_match = re.match(r"^(\s*)([-*+])\s+(.*)$", line)
100
+ if list_match:
101
+ lead, _bullet, content = list_match.groups()
102
+ rendered = Text(lead)
103
+ rendered.append("•", style="bright_blue")
104
+ rendered.append(" ")
105
+ rendered.append(render_inline_spans(content))
106
+ return rendered
107
+
108
+ number_match = re.match(r"^(\s*)(\d+\.)\s+(.*)$", line)
109
+ if number_match:
110
+ lead, marker, content = number_match.groups()
111
+ rendered = Text(lead)
112
+ rendered.append(marker, style="yellow")
113
+ rendered.append(" ")
114
+ rendered.append(render_inline_spans(content))
115
+ return rendered
116
+
117
+ quote = re.match(r"^(\s*)>\s?(.*)$", line)
118
+ if quote:
119
+ lead, content = quote.groups()
120
+ rendered = Text(lead)
121
+ rendered.append("| ", style="dim")
122
+ start = len(rendered)
123
+ rendered.append(render_inline_spans(content))
124
+ rendered.stylize("italic", start, len(rendered))
125
+ return rendered
126
+
127
+ return render_inline_spans(line)
128
+
129
+
130
+ def render_inline_spans(text: str) -> Text:
131
+ rendered = Text()
132
+ pattern = re.compile(
133
+ r"`([^`]+)`"
134
+ r"|\*\*([^*]+)\*\*"
135
+ r"|(?<!\*)\*([^*]+)\*(?!\*)"
136
+ r"|_([^_\n]+)_"
137
+ )
138
+ position = 0
139
+ for match in pattern.finditer(text):
140
+ if match.start() > position:
141
+ rendered.append(text[position : match.start()])
142
+ code, bold, star_italic, underscore_italic = match.groups()
143
+ if code is not None:
144
+ rendered.append(code, style="bold bright_yellow")
145
+ elif bold is not None:
146
+ rendered.append(bold, style="bold bright_white")
147
+ else:
148
+ rendered.append(star_italic or underscore_italic or "", style="italic")
149
+ position = match.end()
150
+ if position < len(text):
151
+ rendered.append(text[position:])
152
+ return rendered