rpr-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 (97) hide show
  1. rpr/__init__.py +1 -0
  2. rpr/agent/__init__.py +29 -0
  3. rpr/agent/approval.py +19 -0
  4. rpr/agent/bootstrap.py +60 -0
  5. rpr/agent/client.py +115 -0
  6. rpr/agent/control.py +16 -0
  7. rpr/agent/mock.py +158 -0
  8. rpr/agent/runtime.py +291 -0
  9. rpr/agent/session.py +91 -0
  10. rpr/agent/tools/__init__.py +10 -0
  11. rpr/agent/tools/base.py +62 -0
  12. rpr/agent/tools/mutating.py +47 -0
  13. rpr/agent/tools/readonly.py +147 -0
  14. rpr/agent/tools/registry.py +30 -0
  15. rpr/application/__init__.py +1 -0
  16. rpr/application/catalog.py +427 -0
  17. rpr/application/chat_service.py +413 -0
  18. rpr/application/checks.py +49 -0
  19. rpr/application/cli_adapter.py +36 -0
  20. rpr/application/completer.py +163 -0
  21. rpr/application/conversation_service.py +92 -0
  22. rpr/application/prompt_service.py +85 -0
  23. rpr/application/selector.py +240 -0
  24. rpr/application/shell.py +543 -0
  25. rpr/checks/__init__.py +0 -0
  26. rpr/checks/base.py +13 -0
  27. rpr/checks/instructions.py +89 -0
  28. rpr/checks/packages.py +619 -0
  29. rpr/checks/workspace.py +170 -0
  30. rpr/cli.py +78 -0
  31. rpr/commands/__init__.py +0 -0
  32. rpr/commands/add.py +166 -0
  33. rpr/commands/chat.py +48 -0
  34. rpr/commands/check.py +53 -0
  35. rpr/commands/generate/__init__.py +0 -0
  36. rpr/commands/generate/api.py +228 -0
  37. rpr/commands/generate/domain.py +383 -0
  38. rpr/commands/generate/engine.py +148 -0
  39. rpr/commands/generate/storybook.py +442 -0
  40. rpr/commands/generate/ui.py +414 -0
  41. rpr/commands/init.py +822 -0
  42. rpr/commands/map.py +113 -0
  43. rpr/commands/settings.py +102 -0
  44. rpr/commands/sync.py +97 -0
  45. rpr/context.py +203 -0
  46. rpr/generators/__init__.py +0 -0
  47. rpr/generators/base.py +110 -0
  48. rpr/generators/claude.py +33 -0
  49. rpr/generators/copilot.py +36 -0
  50. rpr/generators/cursor.py +38 -0
  51. rpr/generators/gemini.py +33 -0
  52. rpr/map/__init__.py +0 -0
  53. rpr/map/architecture.py +495 -0
  54. rpr/map/chains.py +317 -0
  55. rpr/map/classifier.py +170 -0
  56. rpr/map/coverage.py +200 -0
  57. rpr/map/dependencies.py +243 -0
  58. rpr/map/extractor.py +223 -0
  59. rpr/map/graph.py +318 -0
  60. rpr/map/output.py +1030 -0
  61. rpr/map/responsibility.py +345 -0
  62. rpr/map/topology.py +327 -0
  63. rpr/map/walker.py +151 -0
  64. rpr/scaffolds/domain/base_entity.md +30 -0
  65. rpr/scaffolds/domain/base_repo.md +48 -0
  66. rpr/scaffolds/domain/container.md +76 -0
  67. rpr/scaffolds/domain/settings.md +57 -0
  68. rpr/scaffolds/instructions/all.instructions.md +50 -0
  69. rpr/scaffolds/instructions/api.instructions.md +42 -0
  70. rpr/scaffolds/instructions/domain.instructions.md +93 -0
  71. rpr/scaffolds/instructions/frontend.instructions.md +97 -0
  72. rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
  73. rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
  74. rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
  75. rpr/scaffolds/instructions/tooling.instructions.md +42 -0
  76. rpr/scaffolds/js_special_files/fetch.service.md +222 -0
  77. rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
  78. rpr/scaffolds/special_files/domain_container.md +76 -0
  79. rpr/scaffolds/special_files/domain_settings.md +57 -0
  80. rpr/scaffolds/special_files/dto_util.md +62 -0
  81. rpr/scaffolds/special_files/encrypted_column.md +98 -0
  82. rpr/scaffolds/special_files/mapper_util.md +159 -0
  83. rpr/scaffolds/special_files/partial_update.md +61 -0
  84. rpr/templates/__init__.py +0 -0
  85. rpr/templates/registry.py +81 -0
  86. rpr/ui/__init__.py +0 -0
  87. rpr/ui/console.py +32 -0
  88. rpr/ui/markdown.py +59 -0
  89. rpr/ui/prompt_session.py +430 -0
  90. rpr/ui/renderers.py +167 -0
  91. rpr/ui/theme.py +286 -0
  92. rpr/workspace.py +131 -0
  93. rpr_cli-0.1.1.dist-info/METADATA +201 -0
  94. rpr_cli-0.1.1.dist-info/RECORD +97 -0
  95. rpr_cli-0.1.1.dist-info/WHEEL +4 -0
  96. rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
  97. rpr_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Protocol
7
+
8
+ from rpr.agent.approval import ApprovalMode
9
+ from rpr.agent.session import ChatSession
10
+
11
+
12
+ @dataclass(slots=True, frozen=True)
13
+ class ConversationSummary:
14
+ session_id: str
15
+ title: str
16
+ project_dir: Path
17
+ created_at: datetime
18
+ updated_at: datetime
19
+
20
+
21
+ class ConversationRepository(Protocol):
22
+ def save(self, session: ChatSession) -> None: ...
23
+ def find(self, session_id: str) -> ChatSession | None: ...
24
+ def all(self) -> list[ChatSession]: ...
25
+
26
+
27
+ class InMemoryConversationRepository:
28
+ def __init__(self) -> None:
29
+ self._store: dict[str, ChatSession] = {}
30
+
31
+ def save(self, session: ChatSession) -> None:
32
+ self._store[session.session_id] = session
33
+
34
+ def find(self, session_id: str) -> ChatSession | None:
35
+ return self._store.get(session_id)
36
+
37
+ def all(self) -> list[ChatSession]:
38
+ return list(self._store.values())
39
+
40
+
41
+ class ConversationService:
42
+ def __init__(self, repository: ConversationRepository | None = None) -> None:
43
+ self._repository = repository or InMemoryConversationRepository()
44
+ self._current_session_id: str | None = None
45
+
46
+ def list_conversations(self) -> list[ConversationSummary]:
47
+ sessions = sorted(
48
+ self._repository.all(),
49
+ key=lambda session: session.updated_at,
50
+ reverse=True,
51
+ )
52
+ return [
53
+ ConversationSummary(
54
+ session_id=session.session_id,
55
+ title=session.title,
56
+ project_dir=session.project_dir,
57
+ created_at=session.created_at,
58
+ updated_at=session.updated_at,
59
+ )
60
+ for session in sessions
61
+ ]
62
+
63
+ def create_conversation(
64
+ self, *, project_dir: Path, approval_mode: ApprovalMode
65
+ ) -> ChatSession:
66
+ conversation = ChatSession(project_dir=project_dir, approval_mode=approval_mode)
67
+ self._repository.save(conversation)
68
+ self._current_session_id = conversation.session_id
69
+ return conversation
70
+
71
+ def load_conversation(self, session_id: str) -> ChatSession:
72
+ conversation = self._repository.find(session_id)
73
+ if conversation is None:
74
+ raise ValueError(f"Unknown conversation: {session_id}")
75
+ conversation.touch()
76
+ self._current_session_id = session_id
77
+ return conversation
78
+
79
+ def save_conversation(self, session: ChatSession | None = None) -> None:
80
+ if session is None:
81
+ if self._current_session_id is None:
82
+ return
83
+ session = self._repository.find(self._current_session_id)
84
+ if session is None:
85
+ return
86
+ session.touch()
87
+ self._repository.save(session)
88
+ self._current_session_id = session.session_id
89
+
90
+ def reset(self, repository: ConversationRepository | None = None) -> None:
91
+ self._repository = repository or InMemoryConversationRepository()
92
+ self._current_session_id = None
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import ClassVar
6
+
7
+ from prompt_toolkit.completion import Completer
8
+
9
+ from rpr.agent.approval import ApprovalMode
10
+ from rpr.agent.control import TurnControl
11
+ from rpr.agent.session import ChatSession
12
+ from rpr.application.conversation_service import (
13
+ ConversationService,
14
+ ConversationSummary,
15
+ InMemoryConversationRepository,
16
+ )
17
+ from rpr.ui.prompt_session import PromptSessionAdapter
18
+
19
+
20
+ class PromptService:
21
+ _instance: ClassVar[PromptService | None] = None
22
+
23
+ def __init__(self) -> None:
24
+ self._conversations = ConversationService()
25
+ self._session = PromptSessionAdapter()
26
+
27
+ @classmethod
28
+ def instance(cls) -> PromptService:
29
+ if cls._instance is None:
30
+ cls._instance = cls()
31
+ return cls._instance
32
+
33
+ def set_active_task(self, task: asyncio.Task | None) -> None:
34
+ self._session.set_active_task(task)
35
+
36
+ def set_active_control(self, control: TurnControl | None) -> None:
37
+ self._session.set_active_control(control)
38
+
39
+ def set_queue_info(self, count: int, preview: str | None) -> None:
40
+ self._session.set_queue_info(count, preview)
41
+
42
+ def set_completer(self, completer: Completer) -> None:
43
+ self._session.set_completer(completer)
44
+
45
+ def has_modal_input(self) -> bool:
46
+ return self._session.has_modal_input()
47
+
48
+ def get_prompt_session(self):
49
+ return self._session.get_prompt_session()
50
+
51
+ def list_conversations(self) -> list[ConversationSummary]:
52
+ return self._conversations.list_conversations()
53
+
54
+ def create_conversation(
55
+ self, *, project_dir: Path, approval_mode: ApprovalMode
56
+ ) -> ChatSession:
57
+ return self._conversations.create_conversation(
58
+ project_dir=project_dir,
59
+ approval_mode=approval_mode,
60
+ )
61
+
62
+ def load_conversation(self, session_id: str) -> ChatSession:
63
+ return self._conversations.load_conversation(session_id)
64
+
65
+ def save_conversation(self, session: ChatSession | None = None) -> None:
66
+ self._conversations.save_conversation(session)
67
+
68
+ async def read_input(
69
+ self,
70
+ message: str = "> ",
71
+ *,
72
+ multiline: bool = False,
73
+ modal: bool = False,
74
+ ) -> str:
75
+ return await self._session.read_input(message, multiline=multiline, modal=modal)
76
+
77
+ async def prompt(self, label: str, *, default: str | None = None) -> str:
78
+ return await self._session.prompt(label, default=default)
79
+
80
+ async def confirm(self, label: str, *, default: bool = False) -> bool:
81
+ return await self._session.confirm(label, default=default)
82
+
83
+ def reset(self) -> None:
84
+ self._session.reset()
85
+ self._conversations.reset()
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass(slots=True, frozen=True)
8
+ class TriggerSelectorCandidate:
9
+ value: str
10
+ label: str
11
+ description: str
12
+ insert_text: str
13
+ detail: str | None = None
14
+ keep_open_on_exact_match: bool = False
15
+
16
+
17
+ @dataclass(slots=True, frozen=True)
18
+ class TriggerSelectorSource:
19
+ trigger: str
20
+ candidates: tuple[TriggerSelectorCandidate, ...]
21
+ anchored_to_start: bool = True
22
+ hide_on_exact_match: bool = True
23
+ case_sensitive: bool = True
24
+
25
+
26
+ @dataclass(slots=True, frozen=True)
27
+ class TriggerSelectorApplication:
28
+ text: str
29
+ cursor_position: int
30
+ candidate: TriggerSelectorCandidate
31
+
32
+
33
+ @dataclass(slots=True, frozen=True)
34
+ class TriggerSelectorSnapshot:
35
+ trigger: str | None
36
+ query: str
37
+ replacement_start: int
38
+ replacement_end: int
39
+ matches: tuple[TriggerSelectorCandidate, ...]
40
+ visible_matches: tuple[TriggerSelectorCandidate, ...]
41
+ selected_index: int
42
+ visible_start: int
43
+ dismissed: bool = False
44
+
45
+ @property
46
+ def selected_candidate(self) -> TriggerSelectorCandidate | None:
47
+ if not self.matches:
48
+ return None
49
+ return self.matches[self.selected_index]
50
+
51
+ @property
52
+ def visible(self) -> bool:
53
+ return self.trigger is not None and bool(self.matches) and not self.dismissed
54
+
55
+
56
+ class TriggerSelector:
57
+ def __init__(
58
+ self,
59
+ sources: Sequence[TriggerSelectorSource] = (),
60
+ *,
61
+ max_visible_matches: int = 6,
62
+ ) -> None:
63
+ self._sources: tuple[TriggerSelectorSource, ...] = tuple(sources)
64
+ self._max_visible_matches = max_visible_matches
65
+ self._selected_index = 0
66
+ self._active_key: tuple[str, int, int, str] | None = None
67
+ self._dismissed_key: tuple[str, int, int, str] | None = None
68
+
69
+ def set_sources(self, sources: Sequence[TriggerSelectorSource]) -> None:
70
+ self._sources = tuple(sources)
71
+ self.reset()
72
+
73
+ def reset(self) -> None:
74
+ self._selected_index = 0
75
+ self._active_key = None
76
+ self._dismissed_key = None
77
+
78
+ def snapshot(
79
+ self, text: str, cursor_position: int | None = None
80
+ ) -> TriggerSelectorSnapshot:
81
+ resolved_cursor_position = (
82
+ len(text) if cursor_position is None else cursor_position
83
+ )
84
+ source, query, replacement_start, replacement_end = self._find_active_source(
85
+ text=text,
86
+ cursor_position=resolved_cursor_position,
87
+ )
88
+ if source is None:
89
+ self._active_key = None
90
+ return TriggerSelectorSnapshot(
91
+ trigger=None,
92
+ query="",
93
+ replacement_start=resolved_cursor_position,
94
+ replacement_end=resolved_cursor_position,
95
+ matches=(),
96
+ visible_matches=(),
97
+ selected_index=0,
98
+ visible_start=0,
99
+ dismissed=False,
100
+ )
101
+
102
+ normalized_query = query if source.case_sensitive else query.casefold()
103
+ matches = tuple(
104
+ candidate
105
+ for candidate in source.candidates
106
+ if self._normalize_value(
107
+ candidate.value, case_sensitive=source.case_sensitive
108
+ ).startswith(normalized_query)
109
+ )
110
+ snapshot_key = (source.trigger, replacement_start, replacement_end, query)
111
+ if snapshot_key != self._active_key:
112
+ self._selected_index = 0
113
+ self._dismissed_key = None
114
+ self._active_key = snapshot_key
115
+ exact_matches = tuple(
116
+ candidate
117
+ for candidate in matches
118
+ if self._normalize_value(
119
+ candidate.value, case_sensitive=source.case_sensitive
120
+ )
121
+ == normalized_query
122
+ )
123
+ has_exact_match = (
124
+ source.hide_on_exact_match
125
+ and bool(exact_matches)
126
+ and all(
127
+ not candidate.keep_open_on_exact_match for candidate in exact_matches
128
+ )
129
+ )
130
+
131
+ if matches:
132
+ self._selected_index = min(self._selected_index, len(matches) - 1)
133
+ else:
134
+ self._selected_index = 0
135
+
136
+ visible_start = self._visible_start(self._selected_index, len(matches))
137
+ visible_end = min(len(matches), visible_start + self._max_visible_matches)
138
+ return TriggerSelectorSnapshot(
139
+ trigger=source.trigger,
140
+ query=query,
141
+ replacement_start=replacement_start,
142
+ replacement_end=replacement_end,
143
+ matches=matches,
144
+ visible_matches=matches[visible_start:visible_end],
145
+ selected_index=self._selected_index,
146
+ visible_start=visible_start,
147
+ dismissed=snapshot_key == self._dismissed_key or has_exact_match,
148
+ )
149
+
150
+ def is_visible(self, text: str, cursor_position: int | None = None) -> bool:
151
+ return self.snapshot(text, cursor_position).visible
152
+
153
+ def move_selection(
154
+ self,
155
+ text: str,
156
+ cursor_position: int | None = None,
157
+ *,
158
+ step: int,
159
+ ) -> TriggerSelectorSnapshot:
160
+ snapshot = self.snapshot(text, cursor_position)
161
+ if not snapshot.visible or not snapshot.matches:
162
+ return snapshot
163
+ self._selected_index = (snapshot.selected_index + step) % len(snapshot.matches)
164
+ return self.snapshot(text, cursor_position)
165
+
166
+ def dismiss(self, text: str, cursor_position: int | None = None) -> None:
167
+ snapshot = self.snapshot(text, cursor_position)
168
+ if snapshot.trigger is None:
169
+ return
170
+ self._dismissed_key = (
171
+ snapshot.trigger,
172
+ snapshot.replacement_start,
173
+ snapshot.replacement_end,
174
+ snapshot.query,
175
+ )
176
+
177
+ def apply_selection(
178
+ self,
179
+ text: str,
180
+ cursor_position: int | None = None,
181
+ ) -> TriggerSelectorApplication | None:
182
+ snapshot = self.snapshot(text, cursor_position)
183
+ candidate = snapshot.selected_candidate
184
+ if not snapshot.visible or candidate is None:
185
+ return None
186
+
187
+ new_text = (
188
+ text[: snapshot.replacement_start]
189
+ + candidate.insert_text
190
+ + text[snapshot.replacement_end :]
191
+ )
192
+ new_cursor_position = snapshot.replacement_start + len(candidate.insert_text)
193
+ self.reset()
194
+ return TriggerSelectorApplication(
195
+ text=new_text,
196
+ cursor_position=new_cursor_position,
197
+ candidate=candidate,
198
+ )
199
+
200
+ def _find_active_source(
201
+ self,
202
+ *,
203
+ text: str,
204
+ cursor_position: int,
205
+ ) -> tuple[TriggerSelectorSource | None, str, int, int]:
206
+ text_before_cursor = text[:cursor_position]
207
+ for source in self._sources:
208
+ if source.anchored_to_start:
209
+ if not text_before_cursor.startswith(source.trigger):
210
+ continue
211
+ query = text_before_cursor[len(source.trigger) :]
212
+ if any(character.isspace() for character in query):
213
+ continue
214
+ return source, query, 0, cursor_position
215
+
216
+ trigger_index = text_before_cursor.rfind(source.trigger)
217
+ if trigger_index < 0:
218
+ continue
219
+ if (
220
+ trigger_index > 0
221
+ and not text_before_cursor[trigger_index - 1].isspace()
222
+ ):
223
+ continue
224
+ query = text_before_cursor[trigger_index + len(source.trigger) :]
225
+ if any(character.isspace() for character in query):
226
+ continue
227
+ return source, query, trigger_index, cursor_position
228
+
229
+ return None, "", cursor_position, cursor_position
230
+
231
+ def _visible_start(self, selected_index: int, match_count: int) -> int:
232
+ if match_count <= self._max_visible_matches:
233
+ return 0
234
+ max_start = match_count - self._max_visible_matches
235
+ centered_start = selected_index - (self._max_visible_matches // 2)
236
+ return max(0, min(centered_start, max_start))
237
+
238
+ @staticmethod
239
+ def _normalize_value(value: str, *, case_sensitive: bool) -> str:
240
+ return value if case_sensitive else value.casefold()