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
rpr/agent/session.py ADDED
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import UTC, datetime
5
+ from enum import StrEnum
6
+ from pathlib import Path
7
+ from uuid import uuid4
8
+
9
+ from rpr.agent.approval import ApprovalMode
10
+ from rpr.agent.client import ConversationMessage, MessageContent, ToolArguments
11
+
12
+
13
+ class ToolCallStatus(StrEnum):
14
+ PENDING = "pending"
15
+ APPROVED = "approved"
16
+ REJECTED = "rejected"
17
+ CANCELLED = "cancelled"
18
+ COMPLETED = "completed"
19
+ FAILED = "failed"
20
+
21
+
22
+ @dataclass(slots=True)
23
+ class ToolCallRecord:
24
+ call_id: str
25
+ tool_name: str
26
+ arguments: ToolArguments
27
+ is_mutating: bool
28
+ status: ToolCallStatus = ToolCallStatus.PENDING
29
+ output: str | None = None
30
+ error: str | None = None
31
+
32
+
33
+ @dataclass(slots=True)
34
+ class ChatSession:
35
+ project_dir: Path
36
+ approval_mode: ApprovalMode = ApprovalMode.ASK
37
+ session_id: str = field(default_factory=lambda: uuid4().hex)
38
+ title: str = "New chat"
39
+ created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
40
+ updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
41
+ messages: list[ConversationMessage] = field(default_factory=list)
42
+ tool_calls: list[ToolCallRecord] = field(default_factory=list)
43
+
44
+ def add_user_message(self, content: MessageContent) -> None:
45
+ stripped = self._message_title_text(content).strip()
46
+ if stripped and self.title == "New chat":
47
+ self.title = stripped.splitlines()[0][:60]
48
+ self.touch()
49
+ self.messages.append(ConversationMessage(role="user", content=content))
50
+
51
+ def add_assistant_message(self, content: str) -> None:
52
+ if content:
53
+ self.touch()
54
+ self.messages.append(ConversationMessage(role="assistant", content=content))
55
+
56
+ def add_tool_message(self, tool_name: str, content: str) -> None:
57
+ self.touch()
58
+ self.messages.append(
59
+ ConversationMessage(role="tool", content=content, name=tool_name)
60
+ )
61
+
62
+ def create_tool_call(
63
+ self,
64
+ *,
65
+ call_id: str,
66
+ tool_name: str,
67
+ arguments: ToolArguments,
68
+ is_mutating: bool,
69
+ ) -> ToolCallRecord:
70
+ record = ToolCallRecord(
71
+ call_id=call_id,
72
+ tool_name=tool_name,
73
+ arguments=arguments,
74
+ is_mutating=is_mutating,
75
+ )
76
+ self.tool_calls.append(record)
77
+ self.touch()
78
+ return record
79
+
80
+ def enable_auto_all(self) -> None:
81
+ self.approval_mode = ApprovalMode.AUTO_ALL
82
+ self.touch()
83
+
84
+ def touch(self) -> None:
85
+ self.updated_at = datetime.now(UTC)
86
+
87
+ @staticmethod
88
+ def _message_title_text(content: MessageContent) -> str:
89
+ if isinstance(content, str):
90
+ return content
91
+ return "".join(part.text for part in content if hasattr(part, "text"))
@@ -0,0 +1,10 @@
1
+ from rpr.agent.tools.base import AgentTool, ToolResult, ToolSpec
2
+ from rpr.agent.tools.registry import ToolRegistry, build_default_tool_registry
3
+
4
+ __all__ = [
5
+ "AgentTool",
6
+ "ToolRegistry",
7
+ "ToolResult",
8
+ "ToolSpec",
9
+ "build_default_tool_registry",
10
+ ]
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from rpr.agent.client import ToolArguments
8
+ from rpr.agent.control import TurnControl
9
+ from rpr.context import RunContext
10
+
11
+
12
+ @dataclass(slots=True, frozen=True)
13
+ class ToolSpec:
14
+ name: str
15
+ description: str
16
+ is_mutating: bool = False
17
+
18
+
19
+ @dataclass(slots=True, frozen=True)
20
+ class ToolResult:
21
+ tool_name: str
22
+ success: bool
23
+ output: str
24
+ cancelled: bool = False
25
+
26
+ def as_message_content(self) -> str:
27
+ if self.cancelled:
28
+ status = "cancelled"
29
+ else:
30
+ status = "succeeded" if self.success else "failed"
31
+ return f"Tool {self.tool_name} {status}.\n\n{self.output}".strip()
32
+
33
+
34
+ class AgentTool(ABC):
35
+ @property
36
+ @abstractmethod
37
+ def spec(self) -> ToolSpec: ...
38
+
39
+ @abstractmethod
40
+ def execute(
41
+ self,
42
+ arguments: ToolArguments,
43
+ ctx: RunContext,
44
+ *,
45
+ control: TurnControl | None = None,
46
+ ) -> ToolResult: ...
47
+
48
+
49
+ def resolve_project_path(project_dir: Path, raw_path: object) -> Path:
50
+ if not isinstance(raw_path, str) or not raw_path.strip():
51
+ raise ValueError("Tool argument 'path' must be a non-empty string.")
52
+
53
+ candidate = Path(raw_path)
54
+ resolved = (
55
+ candidate.resolve()
56
+ if candidate.is_absolute()
57
+ else (project_dir / candidate).resolve()
58
+ )
59
+ project_root = project_dir.resolve()
60
+ if not resolved.is_relative_to(project_root):
61
+ raise ValueError(f"Path must stay within the project root: {project_root}")
62
+ return resolved
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from rpr.agent.client import ToolArguments
4
+ from rpr.agent.control import TurnControl
5
+ from rpr.agent.tools.base import AgentTool, ToolResult, ToolSpec, resolve_project_path
6
+ from rpr.context import RunContext
7
+
8
+
9
+ class WriteFileTool(AgentTool):
10
+ @property
11
+ def spec(self) -> ToolSpec:
12
+ return ToolSpec(
13
+ name="write_file",
14
+ description="Create or update a file in the repository.",
15
+ is_mutating=True,
16
+ )
17
+
18
+ def execute(
19
+ self,
20
+ arguments: ToolArguments,
21
+ ctx: RunContext,
22
+ *,
23
+ control: TurnControl | None = None,
24
+ ) -> ToolResult:
25
+ if control is not None and control.is_cancelled:
26
+ return ToolResult(
27
+ tool_name=self.spec.name,
28
+ success=False,
29
+ output="Tool execution cancelled.",
30
+ cancelled=True,
31
+ )
32
+ path = resolve_project_path(ctx.project_dir, arguments.get("path"))
33
+ content = arguments.get("content")
34
+ if not isinstance(content, str):
35
+ raise ValueError("Tool argument 'content' must be a string.")
36
+
37
+ if path.exists():
38
+ ctx.update_file(path, content)
39
+ else:
40
+ ctx.write_file(path, content)
41
+
42
+ relative_path = path.relative_to(ctx.project_dir)
43
+ return ToolResult(
44
+ tool_name=self.spec.name,
45
+ success=True,
46
+ output=f"Wrote {relative_path}",
47
+ )
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ from rpr.agent.client import ToolArguments
4
+ from rpr.agent.control import TurnControl
5
+ from rpr.agent.tools.base import AgentTool, ToolResult, ToolSpec, resolve_project_path
6
+ from rpr.context import RunContext
7
+ from rpr.map.walker import _IGNORED_DIRS
8
+
9
+
10
+ class ReadFileTool(AgentTool):
11
+ @property
12
+ def spec(self) -> ToolSpec:
13
+ return ToolSpec(
14
+ name="read_file", description="Read a file from the repository."
15
+ )
16
+
17
+ def execute(
18
+ self,
19
+ arguments: ToolArguments,
20
+ ctx: RunContext,
21
+ *,
22
+ control: TurnControl | None = None,
23
+ ) -> ToolResult:
24
+ if control is not None and control.is_cancelled:
25
+ return ToolResult(
26
+ tool_name=self.spec.name,
27
+ success=False,
28
+ output="Tool execution cancelled.",
29
+ cancelled=True,
30
+ )
31
+ path = resolve_project_path(ctx.project_dir, arguments.get("path"))
32
+ start_line = int(arguments.get("start_line", 1))
33
+ end_line = arguments.get("end_line")
34
+ content = path.read_text()
35
+ lines = content.splitlines()
36
+ if end_line is None:
37
+ end_line = len(lines)
38
+ end_line = int(end_line)
39
+ selected = lines[max(start_line - 1, 0) : max(end_line, 0)]
40
+ output = "\n".join(
41
+ f"{index}: {line}" for index, line in enumerate(selected, start=start_line)
42
+ )
43
+ return ToolResult(
44
+ tool_name=self.spec.name,
45
+ success=True,
46
+ output=output or "(file is empty)",
47
+ )
48
+
49
+
50
+ class ListDirectoryTool(AgentTool):
51
+ @property
52
+ def spec(self) -> ToolSpec:
53
+ return ToolSpec(
54
+ name="list_directory", description="List files under a directory."
55
+ )
56
+
57
+ def execute(
58
+ self,
59
+ arguments: ToolArguments,
60
+ ctx: RunContext,
61
+ *,
62
+ control: TurnControl | None = None,
63
+ ) -> ToolResult:
64
+ if control is not None and control.is_cancelled:
65
+ return ToolResult(
66
+ tool_name=self.spec.name,
67
+ success=False,
68
+ output="Tool execution cancelled.",
69
+ cancelled=True,
70
+ )
71
+ path = resolve_project_path(ctx.project_dir, arguments.get("path", "."))
72
+ entries = sorted(
73
+ path.iterdir(), key=lambda entry: (not entry.is_dir(), entry.name.lower())
74
+ )
75
+ output = "\n".join(
76
+ f"{entry.name}/" if entry.is_dir() else entry.name for entry in entries
77
+ )
78
+ return ToolResult(
79
+ tool_name=self.spec.name,
80
+ success=True,
81
+ output=output or "(directory is empty)",
82
+ )
83
+
84
+
85
+ class SearchTextTool(AgentTool):
86
+ @property
87
+ def spec(self) -> ToolSpec:
88
+ return ToolSpec(
89
+ name="search_text", description="Search repository text for a query."
90
+ )
91
+
92
+ def execute(
93
+ self,
94
+ arguments: ToolArguments,
95
+ ctx: RunContext,
96
+ *,
97
+ control: TurnControl | None = None,
98
+ ) -> ToolResult:
99
+ query = arguments.get("query")
100
+ if not isinstance(query, str) or not query.strip():
101
+ raise ValueError("Tool argument 'query' must be a non-empty string.")
102
+
103
+ max_results = int(arguments.get("max_results", 20))
104
+ normalized_query = query.casefold()
105
+ matches: list[str] = []
106
+ for current_root, dir_names, file_names in ctx.project_dir.walk():
107
+ if control is not None and control.is_cancelled:
108
+ return ToolResult(
109
+ tool_name=self.spec.name,
110
+ success=False,
111
+ output="Tool execution cancelled.",
112
+ cancelled=True,
113
+ )
114
+ dir_names[:] = [name for name in dir_names if name not in _IGNORED_DIRS]
115
+ for file_name in sorted(file_names):
116
+ if control is not None and control.is_cancelled:
117
+ return ToolResult(
118
+ tool_name=self.spec.name,
119
+ success=False,
120
+ output="Tool execution cancelled.",
121
+ cancelled=True,
122
+ )
123
+ path = current_root / file_name
124
+ try:
125
+ content = path.read_text()
126
+ except OSError, UnicodeDecodeError:
127
+ continue
128
+
129
+ for line_number, line in enumerate(content.splitlines(), start=1):
130
+ if control is not None and control.is_cancelled:
131
+ return ToolResult(
132
+ tool_name=self.spec.name,
133
+ success=False,
134
+ output="Tool execution cancelled.",
135
+ cancelled=True,
136
+ )
137
+ if normalized_query in line.casefold():
138
+ relative_path = path.relative_to(ctx.project_dir)
139
+ matches.append(f"{relative_path}:{line_number}: {line.strip()}")
140
+ if len(matches) >= max_results:
141
+ output = "\n".join(matches)
142
+ return ToolResult(
143
+ tool_name=self.spec.name, success=True, output=output
144
+ )
145
+
146
+ output = "\n".join(matches) if matches else f"No matches found for: {query}"
147
+ return ToolResult(tool_name=self.spec.name, success=True, output=output)
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from rpr.agent.tools.base import AgentTool
6
+ from rpr.agent.tools.mutating import WriteFileTool
7
+ from rpr.agent.tools.readonly import ListDirectoryTool, ReadFileTool, SearchTextTool
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class ToolRegistry:
12
+ tools: dict[str, AgentTool] = field(default_factory=dict)
13
+
14
+ def register(self, tool: AgentTool) -> None:
15
+ self.tools[tool.spec.name] = tool
16
+
17
+ def get(self, name: str) -> AgentTool | None:
18
+ return self.tools.get(name)
19
+
20
+
21
+ def build_default_tool_registry() -> ToolRegistry:
22
+ registry = ToolRegistry()
23
+ for tool in (
24
+ ReadFileTool(),
25
+ ListDirectoryTool(),
26
+ SearchTextTool(),
27
+ WriteFileTool(),
28
+ ):
29
+ registry.register(tool)
30
+ return registry
@@ -0,0 +1 @@
1
+ from __future__ import annotations