crabcode 0.1.6__py3-none-win_amd64.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 (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,169 @@
1
+ """Data structures for structured execution plans."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class PlanStep:
11
+ """A single step in an execution plan."""
12
+
13
+ id: str
14
+ title: str
15
+ description: str
16
+ files: list[str] = field(default_factory=list)
17
+ depends_on: list[str] = field(default_factory=list)
18
+ subagent_type: str = "generalPurpose"
19
+ status: str = "pending" # pending | running | completed | failed | cancelled
20
+ agent_id: str | None = None
21
+ result: str = ""
22
+ error: str = ""
23
+
24
+ def to_dict(self) -> dict[str, Any]:
25
+ return {
26
+ "id": self.id,
27
+ "title": self.title,
28
+ "description": self.description,
29
+ "files": list(self.files),
30
+ "depends_on": list(self.depends_on),
31
+ "subagent_type": self.subagent_type,
32
+ "status": self.status,
33
+ "agent_id": self.agent_id,
34
+ "result": self.result,
35
+ "error": self.error,
36
+ }
37
+
38
+ @staticmethod
39
+ def from_dict(data: dict[str, Any]) -> PlanStep:
40
+ return PlanStep(
41
+ id=data["id"],
42
+ title=data.get("title", ""),
43
+ description=data.get("description", ""),
44
+ files=data.get("files", []),
45
+ depends_on=data.get("depends_on", []),
46
+ subagent_type=data.get("subagent_type", "generalPurpose"),
47
+ status=data.get("status", "pending"),
48
+ agent_id=data.get("agent_id"),
49
+ result=data.get("result", ""),
50
+ error=data.get("error", ""),
51
+ )
52
+
53
+
54
+ @dataclass
55
+ class ExecutionPlan:
56
+ """A structured execution plan with steps and dependency DAG."""
57
+
58
+ title: str
59
+ steps: list[PlanStep] = field(default_factory=list)
60
+ summary: str = ""
61
+ status: str = "pending" # pending | running | completed | failed
62
+
63
+ def to_dict(self) -> dict[str, Any]:
64
+ return {
65
+ "title": self.title,
66
+ "summary": self.summary,
67
+ "status": self.status,
68
+ "steps": [s.to_dict() for s in self.steps],
69
+ }
70
+
71
+ @staticmethod
72
+ def from_dict(data: dict[str, Any]) -> ExecutionPlan:
73
+ steps = [PlanStep.from_dict(s) for s in data.get("steps", [])]
74
+ return ExecutionPlan(
75
+ title=data.get("title", ""),
76
+ summary=data.get("summary", ""),
77
+ status=data.get("status", "pending"),
78
+ steps=steps,
79
+ )
80
+
81
+ def validate_dag(self) -> list[str]:
82
+ """Validate the DAG has no cycles and all dependencies exist.
83
+
84
+ Returns a list of error messages (empty if valid).
85
+ """
86
+ step_ids = {s.id for s in self.steps}
87
+ errors: list[str] = []
88
+
89
+ for step in self.steps:
90
+ for dep in step.depends_on:
91
+ if dep not in step_ids:
92
+ errors.append(
93
+ f"Step '{step.id}' depends on unknown step '{dep}'"
94
+ )
95
+
96
+ # Cycle detection via topological sort (Kahn's algorithm)
97
+ in_degree: dict[str, int] = {s.id: 0 for s in self.steps}
98
+ adjacency: dict[str, list[str]] = {s.id: [] for s in self.steps}
99
+ for step in self.steps:
100
+ for dep in step.depends_on:
101
+ if dep in step_ids:
102
+ adjacency[dep].append(step.id)
103
+ in_degree[step.id] += 1
104
+
105
+ queue = [sid for sid, deg in in_degree.items() if deg == 0]
106
+ visited = 0
107
+ while queue:
108
+ node = queue.pop(0)
109
+ visited += 1
110
+ for neighbor in adjacency[node]:
111
+ in_degree[neighbor] -= 1
112
+ if in_degree[neighbor] == 0:
113
+ queue.append(neighbor)
114
+
115
+ if visited != len(self.steps):
116
+ errors.append("Dependency graph contains a cycle")
117
+
118
+ return errors
119
+
120
+ def get_ready_steps(self) -> list[PlanStep]:
121
+ """Return steps whose dependencies are all completed."""
122
+ completed = {s.id for s in self.steps if s.status == "completed"}
123
+ return [
124
+ s
125
+ for s in self.steps
126
+ if s.status == "pending"
127
+ and all(dep in completed for dep in s.depends_on)
128
+ ]
129
+
130
+ def prepare_for_execution(self) -> tuple[int, int]:
131
+ """Reset stale step statuses before DAG run.
132
+
133
+ Returns ``(total_steps, reset_count)``. Steps already tied to a sub-agent
134
+ (``agent_id`` set) are left unchanged for resume scenarios.
135
+ """
136
+ reset = 0
137
+ for step in self.steps:
138
+ if not step.agent_id and step.status != "pending":
139
+ step.status = "pending"
140
+ step.error = ""
141
+ step.result = ""
142
+ reset += 1
143
+ return len(self.steps), reset
144
+
145
+ def render(self) -> str:
146
+ """Render a human-readable view of the plan."""
147
+ status_icons = {
148
+ "pending": "○",
149
+ "running": "◉",
150
+ "completed": "●",
151
+ "failed": "✗",
152
+ "cancelled": "⊘",
153
+ }
154
+ lines = [f"Plan: {self.title}"]
155
+ if self.summary:
156
+ lines.append(f" {self.summary}")
157
+ lines.append("")
158
+ for step in self.steps:
159
+ icon = status_icons.get(step.status, "?")
160
+ deps = ""
161
+ if step.depends_on:
162
+ deps = f" (after: {', '.join(step.depends_on)})"
163
+ lines.append(f" {icon} [{step.id}] {step.title}{deps}")
164
+ if step.files:
165
+ lines.append(f" files: {', '.join(step.files)}")
166
+ done = sum(1 for s in self.steps if s.status == "completed")
167
+ total = len(self.steps)
168
+ lines.append(f"\n Progress: {done}/{total} steps executed")
169
+ return "\n".join(lines)
File without changes
@@ -0,0 +1,10 @@
1
+ """System sections with cache metadata that never becomes model-visible text."""
2
+
3
+
4
+ class SystemPrompt(list[str]):
5
+ def __init__(self, sections=(), *, static_count: int = 0):
6
+ super().__init__(sections)
7
+ self.static_count = min(max(static_count, 0), len(self))
8
+
9
+ def with_dynamic(self, *sections: str) -> "SystemPrompt":
10
+ return SystemPrompt([*self, *(s for s in sections if s)], static_count=self.static_count)
@@ -0,0 +1,229 @@
1
+ """Context assembly — ported from src/context.ts.
2
+
3
+ Provides getSystemContext (git status etc.) and getUserContext (CLAUDE.md, date).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import os
10
+ import subprocess
11
+ from datetime import date
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from crabcode_core.logging_utils import get_logger
16
+ from crabcode_core.paths import get_config_home
17
+ from crabcode_core.subprocess_utils import decode_subprocess_output, subprocess_group_options
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ async def _run_git(*args: str, cwd: str = ".") -> str:
23
+ """Run a git command and return stdout, or empty string on failure."""
24
+ try:
25
+ proc = await asyncio.create_subprocess_exec(
26
+ "git", *args,
27
+ stdout=asyncio.subprocess.PIPE,
28
+ stderr=asyncio.subprocess.PIPE,
29
+ cwd=cwd,
30
+ **subprocess_group_options(),
31
+ )
32
+ stdout, _ = await proc.communicate()
33
+ return decode_subprocess_output(stdout).strip()
34
+ except Exception:
35
+ logger.debug("Git command failed: git %s", " ".join(args), exc_info=True)
36
+ return ""
37
+
38
+
39
+ async def _is_git_repo(cwd: str = ".") -> bool:
40
+ result = await _run_git("rev-parse", "--is-inside-work-tree", cwd=cwd)
41
+ return result == "true"
42
+
43
+
44
+ async def get_git_status(cwd: str = ".") -> str | None:
45
+ """Build the git status context block."""
46
+ is_git = await _is_git_repo(cwd)
47
+ if not is_git:
48
+ return None
49
+
50
+ try:
51
+ branch, main_branch, status, log, user_name = await asyncio.gather(
52
+ _run_git("branch", "--show-current", cwd=cwd),
53
+ _run_git("rev-parse", "--abbrev-ref", "origin/HEAD", cwd=cwd),
54
+ _run_git("--no-optional-locks", "status", "--short", cwd=cwd),
55
+ _run_git("--no-optional-locks", "log", "--oneline", "-n", "5", cwd=cwd),
56
+ _run_git("config", "user.name", cwd=cwd),
57
+ )
58
+
59
+ main_branch = main_branch.replace("origin/", "") if main_branch else "main"
60
+
61
+ max_chars = 2000
62
+ if len(status) > max_chars:
63
+ status = (
64
+ status[:max_chars]
65
+ + '\n... (truncated because it exceeds 2k characters. '
66
+ 'If you need more information, run "git status" using BashTool)'
67
+ )
68
+
69
+ parts = [
70
+ "This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.",
71
+ f"Current branch: {branch}",
72
+ f"Main branch (you will usually use this for PRs): {main_branch}",
73
+ ]
74
+ if user_name:
75
+ parts.append(f"Git user: {user_name}")
76
+ parts.append(f"Status:\n{status or '(clean)'}")
77
+ parts.append(f"Recent commits:\n{log}")
78
+
79
+ return "\n\n".join(parts)
80
+ except Exception:
81
+ logger.debug("Failed to build async git status context for %s", cwd, exc_info=True)
82
+ return None
83
+
84
+
85
+ def get_system_context(cwd: str = ".") -> dict[str, str]:
86
+ """Build system context (appended to system prompt).
87
+
88
+ Synchronous wrapper — runs git commands in event loop if available.
89
+ For the initial version, we do a simplified synchronous git check.
90
+ """
91
+ context: dict[str, str] = {}
92
+
93
+ try:
94
+ result = subprocess.run(
95
+ ["git", "rev-parse", "--is-inside-work-tree"],
96
+ capture_output=True, text=True, encoding="utf-8", errors="replace", cwd=cwd, timeout=5,
97
+ **subprocess_group_options(),
98
+ )
99
+ is_git = result.stdout.strip() == "true"
100
+ except Exception:
101
+ logger.debug("Failed to detect git repository for %s", cwd, exc_info=True)
102
+ is_git = False
103
+
104
+ if is_git:
105
+ try:
106
+ parts = []
107
+ parts.append(
108
+ "This is the git status at the start of the conversation. "
109
+ "Note that this status is a snapshot in time, and will not "
110
+ "update during the conversation."
111
+ )
112
+
113
+ branch = subprocess.run(
114
+ ["git", "branch", "--show-current"],
115
+ capture_output=True, text=True, encoding="utf-8", errors="replace", cwd=cwd, timeout=5,
116
+ **subprocess_group_options(),
117
+ ).stdout.strip()
118
+ parts.append(f"Current branch: {branch}")
119
+
120
+ status = subprocess.run(
121
+ ["git", "--no-optional-locks", "status", "--short"],
122
+ capture_output=True, text=True, encoding="utf-8", errors="replace", cwd=cwd, timeout=5,
123
+ **subprocess_group_options(),
124
+ ).stdout.strip()
125
+ parts.append(f"Status:\n{status or '(clean)'}")
126
+
127
+ log = subprocess.run(
128
+ ["git", "--no-optional-locks", "log", "--oneline", "-n", "5"],
129
+ capture_output=True, text=True, encoding="utf-8", errors="replace", cwd=cwd, timeout=5,
130
+ **subprocess_group_options(),
131
+ ).stdout.strip()
132
+ parts.append(f"Recent commits:\n{log}")
133
+
134
+ context["gitStatus"] = "\n\n".join(parts)
135
+ except Exception:
136
+ logger.debug("Failed to build git status system context for %s", cwd, exc_info=True)
137
+
138
+ return context
139
+
140
+
141
+ def get_user_context(cwd: str = ".") -> dict[str, str]:
142
+ """Build user context (prepended as first user message).
143
+
144
+ Contains CLAUDE.md content, persistent memories, and current date.
145
+ """
146
+ context: dict[str, str] = {}
147
+
148
+ context["currentDate"] = f"Today's date is {date.today().isoformat()}."
149
+
150
+ claude_md = _load_claude_md(cwd)
151
+ if claude_md:
152
+ context["claudeMd"] = claude_md
153
+
154
+ memories_text = _load_memories_context(cwd)
155
+ if memories_text:
156
+ context["memories"] = memories_text
157
+
158
+ return context
159
+
160
+
161
+ def _load_claude_md(cwd: str) -> str | None:
162
+ """Discover and load CLAUDE.md files from project hierarchy."""
163
+ contents: list[str] = []
164
+
165
+ search_dir = Path(cwd).resolve()
166
+ home = Path.home()
167
+
168
+ home_claude_md = home / ".claude" / "CLAUDE.md"
169
+ if home_claude_md.exists():
170
+ try:
171
+ contents.append(home_claude_md.read_text(encoding="utf-8", errors="replace"))
172
+ except Exception:
173
+ logger.warning("Failed to read %s", home_claude_md, exc_info=True)
174
+
175
+ crabcode_md = get_config_home() / "CLAUDE.md"
176
+ if crabcode_md.exists():
177
+ try:
178
+ contents.append(crabcode_md.read_text(encoding="utf-8", errors="replace"))
179
+ except Exception:
180
+ logger.warning("Failed to read %s", crabcode_md, exc_info=True)
181
+
182
+ current = search_dir
183
+ project_files: list[str] = []
184
+ while current != current.parent:
185
+ for name in ["CLAUDE.md", ".claude/CLAUDE.md"]:
186
+ candidate = current / name
187
+ if candidate.exists():
188
+ try:
189
+ project_files.append(
190
+ candidate.read_text(encoding="utf-8", errors="replace")
191
+ )
192
+ except Exception:
193
+ logger.warning("Failed to read %s", candidate, exc_info=True)
194
+ if (current / ".git").exists():
195
+ break
196
+ current = current.parent
197
+
198
+ contents.extend(reversed(project_files))
199
+
200
+ if not contents:
201
+ return None
202
+ return "\n\n---\n\n".join(contents)
203
+
204
+
205
+ def _load_memories_context(cwd: str) -> str | None:
206
+ """Load a compact memory directory for context injection."""
207
+ try:
208
+ from crabcode_core.tools.memory import (
209
+ format_memory_directory,
210
+ load_all_memories,
211
+ )
212
+ except ImportError:
213
+ return None
214
+
215
+ memories = load_all_memories(cwd)
216
+ if not memories:
217
+ return None
218
+
219
+ directory = format_memory_directory(memories)
220
+ if not directory:
221
+ return None
222
+
223
+ return (
224
+ "The following compact directory lists memories saved from previous "
225
+ "conversations. Titles are summaries, not the full memory content. "
226
+ "They may or may not be relevant to the current task. Use the Memory "
227
+ "tool's search and read actions when details are needed.\n\n"
228
+ + directory
229
+ )
@@ -0,0 +1,92 @@
1
+ """Named prompt templates and prompts appended to user input.
2
+
3
+ A template section that is missing or blank keeps the built-in default.
4
+ Selecting no template leaves the legacy ``prompt_profile`` in effect, and
5
+ otherwise uses the built-in prompt.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TypeVar
11
+
12
+ from crabcode_core.prompts.profile import PromptProfile
13
+ from crabcode_core.types.config import (
14
+ CrabCodeSettings,
15
+ PromptTemplateConfig,
16
+ UserAppendPromptConfig,
17
+ )
18
+
19
+ PROMPT_SECTIONS: tuple[tuple[str, str], ...] = (
20
+ ("prefix", "身份前缀"),
21
+ ("intro", "介绍"),
22
+ ("system", "系统规则"),
23
+ ("doing_tasks", "任务执行"),
24
+ ("actions", "授权与安全"),
25
+ ("git_safety", "Git 安全"),
26
+ ("using_tools", "工具使用"),
27
+ ("tone_and_style", "语气与风格"),
28
+ ("output_efficiency", "输出效率"),
29
+ ("session_guidance", "会话指引"),
30
+ ("agent_prompt", "子代理提示词"),
31
+ ("compact_prompt", "上下文压缩提示词"),
32
+ ("extra", "额外段落"),
33
+ )
34
+
35
+ PROMPT_SECTION_KEYS = frozenset(key for key, _label in PROMPT_SECTIONS)
36
+ _PROFILE_KEYS = PROMPT_SECTION_KEYS - {"extra"}
37
+
38
+ _T = TypeVar("_T", PromptTemplateConfig, UserAppendPromptConfig)
39
+
40
+
41
+ def last_by_id(items: list[_T]) -> list[_T]:
42
+ """Keep one item per id. A later settings layer overrides an earlier one."""
43
+ order: list[str] = []
44
+ by_id: dict[str, _T] = {}
45
+ for item in items:
46
+ if item.id not in by_id:
47
+ order.append(item.id)
48
+ by_id[item.id] = item
49
+ return [by_id[item_id] for item_id in order]
50
+
51
+
52
+ def profile_from_sections(sections: dict[str, str]) -> PromptProfile:
53
+ """Build a profile. Blank text is omitted so that section stays default."""
54
+ kwargs: dict[str, object] = {}
55
+ extra: list[str] = []
56
+ for key, raw in sections.items():
57
+ if not isinstance(raw, str):
58
+ continue
59
+ text = raw.strip()
60
+ if not text or key not in PROMPT_SECTION_KEYS:
61
+ continue
62
+ if key == "extra":
63
+ extra.append(text)
64
+ elif key in _PROFILE_KEYS:
65
+ kwargs[key] = text
66
+ if extra:
67
+ kwargs["extra_sections"] = extra
68
+ return PromptProfile(**kwargs)
69
+
70
+
71
+ def resolve_prompt_profile(settings: CrabCodeSettings) -> PromptProfile | None:
72
+ """Return the active template, otherwise a legacy prompt_profile."""
73
+ active = settings.active_prompt_template
74
+ if active:
75
+ match = next(
76
+ (item for item in last_by_id(settings.prompt_templates) if item.id == active),
77
+ None,
78
+ )
79
+ if match is not None:
80
+ return profile_from_sections(match.sections)
81
+ if settings.prompt_profile:
82
+ return PromptProfile(**settings.prompt_profile)
83
+ return None
84
+
85
+
86
+ def enabled_user_append_texts(settings: CrabCodeSettings) -> list[str]:
87
+ """Return checked prompts in list order, after later layers override ids."""
88
+ texts: list[str] = []
89
+ for item in last_by_id(settings.user_append_prompts):
90
+ if item.enabled and item.text.strip():
91
+ texts.append(item.text.strip())
92
+ return texts
@@ -0,0 +1,79 @@
1
+ """Prompt profile — configurable prompt templates for different agent personas.
2
+
3
+ Each section field controls one part of the system prompt:
4
+ - None → use the built-in default for that section
5
+ - "" → disable (skip) the section entirely
6
+ - "..." → override with custom text
7
+
8
+ The compaction prompt is separate from the system prompt; blank text uses its
9
+ built-in default rather than disabling checkpoint instructions.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+ from crabcode_core.prompts.templates import DEFAULT_AGENT_PROMPT, DEFAULT_COMPACT_PROMPT, DEFAULT_PREFIX
17
+
18
+
19
+ class PromptProfile(BaseModel):
20
+ """Configurable prompt profile that separates identity/behavior from engine logic."""
21
+
22
+ # Agent identity line — used to build the default intro section.
23
+ prefix: str = DEFAULT_PREFIX
24
+
25
+ # ── Behavioral sections (domain-specific, the main things to swap) ──
26
+
27
+ intro: str | None = None
28
+ system: str | None = None
29
+ doing_tasks: str | None = None
30
+ actions: str | None = None
31
+ git_safety: str | None = None
32
+ using_tools: str | None = None
33
+ tone_and_style: str | None = None
34
+ output_efficiency: str | None = None
35
+ session_guidance: str | None = None
36
+
37
+ # Sub-agent system prompt.
38
+ agent_prompt: str | None = None
39
+
40
+ # Checkpoint instructions for manual and automatic context compaction.
41
+ compact_prompt: str | None = None
42
+
43
+ # Extra sections appended after all built-in sections
44
+ # (before the dynamic env / language / mcp blocks).
45
+ extra_sections: list[str] = Field(default_factory=list)
46
+
47
+ model_config = {"extra": "allow"}
48
+
49
+
50
+ def default_profile() -> PromptProfile:
51
+ """The default coding-assistant profile (all built-in defaults)."""
52
+ return PromptProfile()
53
+
54
+
55
+ def minimal_profile() -> PromptProfile:
56
+ """A minimal profile with coding-specific sections stripped out.
57
+
58
+ Useful as a starting point for non-coding agents.
59
+ """
60
+ return PromptProfile(
61
+ prefix="You are a helpful AI assistant.",
62
+ doing_tasks="",
63
+ actions="",
64
+ git_safety="",
65
+ )
66
+
67
+
68
+ def resolve_agent_prompt(profile: PromptProfile | None) -> str:
69
+ """Return the sub-agent system prompt, respecting profile overrides."""
70
+ if profile and profile.agent_prompt is not None:
71
+ return profile.agent_prompt
72
+ return DEFAULT_AGENT_PROMPT
73
+
74
+
75
+ def resolve_compact_prompt(profile: PromptProfile | None) -> str:
76
+ """Return checkpoint instructions, falling back for missing or blank text."""
77
+ if profile and profile.compact_prompt and profile.compact_prompt.strip():
78
+ return profile.compact_prompt.strip()
79
+ return DEFAULT_COMPACT_PROMPT