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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- 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
|