claxcode 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.
- claxcode/__init__.py +3 -0
- claxcode/api/__init__.py +1 -0
- claxcode/api/chat.py +51 -0
- claxcode/api/session.py +66 -0
- claxcode/api/subagents.py +82 -0
- claxcode/api/tools.py +38 -0
- claxcode/cli.py +1214 -0
- claxcode/cli_completion.py +174 -0
- claxcode/config.py +54 -0
- claxcode/core/__init__.py +1 -0
- claxcode/core/agent.py +327 -0
- claxcode/core/checkpoint.py +100 -0
- claxcode/core/context.py +114 -0
- claxcode/core/models.py +446 -0
- claxcode/extensions/__init__.py +1 -0
- claxcode/extensions/hooks.py +131 -0
- claxcode/extensions/mcp.py +13 -0
- claxcode/extensions/plugins.py +10 -0
- claxcode/extensions/skills.py +205 -0
- claxcode/main.py +55 -0
- claxcode/memory/__init__.py +23 -0
- claxcode/memory/auto_memory.py +68 -0
- claxcode/memory/long_term.py +196 -0
- claxcode/memory/project_context.py +20 -0
- claxcode/memory/project_memory.py +194 -0
- claxcode/memory/session_store.py +206 -0
- claxcode/models/__init__.py +1 -0
- claxcode/models/session.py +25 -0
- claxcode/models/subagent.py +25 -0
- claxcode/models/tool.py +24 -0
- claxcode/security/__init__.py +1 -0
- claxcode/security/audit.py +54 -0
- claxcode/security/permissions.py +29 -0
- claxcode/skills/bug-fix/SKILL.md +33 -0
- claxcode/skills/code-review/SKILL.md +35 -0
- claxcode/skills/python-refactor/SKILL.md +22 -0
- claxcode/skills/task-workflow/SKILL.md +38 -0
- claxcode/subagents/__init__.py +1 -0
- claxcode/subagents/base.py +108 -0
- claxcode/subagents/executor.py +16 -0
- claxcode/subagents/explore.py +25 -0
- claxcode/subagents/planner.py +25 -0
- claxcode/tools/__init__.py +14 -0
- claxcode/tools/admin.py +170 -0
- claxcode/tools/base.py +19 -0
- claxcode/tools/browser.py +499 -0
- claxcode/tools/code_intelligence.py +68 -0
- claxcode/tools/custom_example.py +76 -0
- claxcode/tools/execution.py +114 -0
- claxcode/tools/file_operations.py +287 -0
- claxcode/tools/memory_tool.py +29 -0
- claxcode/tools/registry.py +35 -0
- claxcode/tools/search.py +157 -0
- claxcode/tools/ssh.py +534 -0
- claxcode/tools/web.py +83 -0
- claxcode-0.1.1.dist-info/METADATA +359 -0
- claxcode-0.1.1.dist-info/RECORD +60 -0
- claxcode-0.1.1.dist-info/WHEEL +4 -0
- claxcode-0.1.1.dist-info/entry_points.txt +3 -0
- claxcode-0.1.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""项目记忆管理器 - 项目级别存储."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import select
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
|
+
|
|
12
|
+
from claxcode.memory.session_store import ProjectMemoryORM, get_db_session
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ProjectMemory:
|
|
16
|
+
"""管理项目级别记忆."""
|
|
17
|
+
|
|
18
|
+
async def init_project(
|
|
19
|
+
self,
|
|
20
|
+
project_path: str,
|
|
21
|
+
config: dict[str, Any],
|
|
22
|
+
) -> str:
|
|
23
|
+
"""初始化项目记忆.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
project_path: 项目路径
|
|
27
|
+
config: 项目配置信息
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
项目记忆ID
|
|
31
|
+
"""
|
|
32
|
+
# 确保路径标准化
|
|
33
|
+
project_path = str(Path(project_path).resolve())
|
|
34
|
+
|
|
35
|
+
async for db in get_db_session():
|
|
36
|
+
db: AsyncSession
|
|
37
|
+
# 检查是否已存在
|
|
38
|
+
result = await db.execute(
|
|
39
|
+
select(ProjectMemoryORM).where(ProjectMemoryORM.project_path == project_path)
|
|
40
|
+
)
|
|
41
|
+
existing = result.scalar_one_or_none()
|
|
42
|
+
|
|
43
|
+
if existing:
|
|
44
|
+
# 更新现有配置
|
|
45
|
+
existing.config = json.dumps(config, ensure_ascii=False)
|
|
46
|
+
existing.updated_at = datetime.now(UTC)
|
|
47
|
+
await db.commit()
|
|
48
|
+
return existing.id
|
|
49
|
+
else:
|
|
50
|
+
# 创建新的项目记忆
|
|
51
|
+
memory_id = str(uuid4())
|
|
52
|
+
memory = ProjectMemoryORM(
|
|
53
|
+
id=memory_id,
|
|
54
|
+
project_path=project_path,
|
|
55
|
+
initialized_at=datetime.now(UTC),
|
|
56
|
+
config=json.dumps(config, ensure_ascii=False),
|
|
57
|
+
context_summary="",
|
|
58
|
+
updated_at=datetime.now(UTC),
|
|
59
|
+
)
|
|
60
|
+
db.add(memory)
|
|
61
|
+
await db.commit()
|
|
62
|
+
return memory_id
|
|
63
|
+
|
|
64
|
+
async def get_project(self, project_path: str) -> dict[str, Any] | None:
|
|
65
|
+
"""获取项目记忆.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
project_path: 项目路径
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
项目记忆信息,不存在则返回 None
|
|
72
|
+
"""
|
|
73
|
+
project_path = str(Path(project_path).resolve())
|
|
74
|
+
|
|
75
|
+
async for db in get_db_session():
|
|
76
|
+
db: AsyncSession
|
|
77
|
+
result = await db.execute(
|
|
78
|
+
select(ProjectMemoryORM).where(ProjectMemoryORM.project_path == project_path)
|
|
79
|
+
)
|
|
80
|
+
memory = result.scalar_one_or_none()
|
|
81
|
+
|
|
82
|
+
if memory:
|
|
83
|
+
return {
|
|
84
|
+
"id": memory.id,
|
|
85
|
+
"project_path": memory.project_path,
|
|
86
|
+
"initialized_at": memory.initialized_at.isoformat(),
|
|
87
|
+
"config": json.loads(memory.config) if memory.config else {},
|
|
88
|
+
"context_summary": memory.context_summary,
|
|
89
|
+
"updated_at": memory.updated_at.isoformat(),
|
|
90
|
+
}
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
async def update_summary(self, project_path: str, summary: str) -> bool:
|
|
94
|
+
"""更新项目上下文摘要.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
project_path: 项目路径
|
|
98
|
+
summary: 摘要内容
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
是否成功更新
|
|
102
|
+
"""
|
|
103
|
+
project_path = str(Path(project_path).resolve())
|
|
104
|
+
|
|
105
|
+
async for db in get_db_session():
|
|
106
|
+
db: AsyncSession
|
|
107
|
+
result = await db.execute(
|
|
108
|
+
select(ProjectMemoryORM).where(ProjectMemoryORM.project_path == project_path)
|
|
109
|
+
)
|
|
110
|
+
memory = result.scalar_one_or_none()
|
|
111
|
+
|
|
112
|
+
now = datetime.now(UTC)
|
|
113
|
+
if memory is None:
|
|
114
|
+
memory = ProjectMemoryORM(
|
|
115
|
+
id=str(uuid4()),
|
|
116
|
+
project_path=project_path,
|
|
117
|
+
initialized_at=now,
|
|
118
|
+
config="",
|
|
119
|
+
context_summary=summary,
|
|
120
|
+
updated_at=now,
|
|
121
|
+
)
|
|
122
|
+
db.add(memory)
|
|
123
|
+
else:
|
|
124
|
+
memory.context_summary = summary
|
|
125
|
+
memory.updated_at = now
|
|
126
|
+
await db.commit()
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
async def list_all_projects(self, limit: int = 100) -> list[dict[str, Any]]:
|
|
130
|
+
"""列出所有项目.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
limit: 返回数量限制
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
项目列表
|
|
137
|
+
"""
|
|
138
|
+
async for db in get_db_session():
|
|
139
|
+
db: AsyncSession
|
|
140
|
+
result = await db.execute(
|
|
141
|
+
select(ProjectMemoryORM)
|
|
142
|
+
.order_by(ProjectMemoryORM.updated_at.desc())
|
|
143
|
+
.limit(limit)
|
|
144
|
+
)
|
|
145
|
+
memories = result.scalars().all()
|
|
146
|
+
return [
|
|
147
|
+
{
|
|
148
|
+
"id": m.id,
|
|
149
|
+
"project_path": m.project_path,
|
|
150
|
+
"name": Path(m.project_path).name,
|
|
151
|
+
"initialized_at": m.initialized_at.isoformat(),
|
|
152
|
+
"updated_at": m.updated_at.isoformat(),
|
|
153
|
+
"has_summary": bool(m.context_summary),
|
|
154
|
+
}
|
|
155
|
+
for m in memories
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
def generate_context_md(self, config: dict[str, Any]) -> str:
|
|
159
|
+
"""生成 CLAUDE.md 文件内容.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
config: 项目配置
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Markdown 格式的项目上下文
|
|
166
|
+
"""
|
|
167
|
+
lines = [
|
|
168
|
+
f"# {config.get('name', '未命名项目')}",
|
|
169
|
+
"",
|
|
170
|
+
"## 项目描述",
|
|
171
|
+
config.get('description', ''),
|
|
172
|
+
"",
|
|
173
|
+
"## 技术栈",
|
|
174
|
+
config.get('tech_stack', ''),
|
|
175
|
+
"",
|
|
176
|
+
"## 常用命令",
|
|
177
|
+
"",
|
|
178
|
+
"### 构建",
|
|
179
|
+
f"```bash\n{config.get('build_command', '')}\n```",
|
|
180
|
+
"",
|
|
181
|
+
"### 测试",
|
|
182
|
+
f"```bash\n{config.get('test_command', '')}\n```",
|
|
183
|
+
"",
|
|
184
|
+
"## 代码风格",
|
|
185
|
+
config.get('code_style', ''),
|
|
186
|
+
"",
|
|
187
|
+
"## 其他说明",
|
|
188
|
+
config.get('notes', ''),
|
|
189
|
+
]
|
|
190
|
+
return "\n".join(lines)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# 全局实例
|
|
194
|
+
project_memory = ProjectMemory()
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""SQLite persistence for sessions using SQLAlchemy async."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from sqlalchemy import DateTime, String, Text, select
|
|
11
|
+
from sqlalchemy.ext.asyncio import (
|
|
12
|
+
AsyncEngine,
|
|
13
|
+
AsyncSession,
|
|
14
|
+
async_sessionmaker,
|
|
15
|
+
create_async_engine,
|
|
16
|
+
)
|
|
17
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
18
|
+
|
|
19
|
+
from claxcode.config import settings
|
|
20
|
+
from claxcode.models.subagent import SubagentStatus, SubagentTask
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Base(DeclarativeBase):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SessionORM(Base):
|
|
28
|
+
__tablename__ = "sessions"
|
|
29
|
+
|
|
30
|
+
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
|
31
|
+
name: Mapped[str] = mapped_column(String(255), default="新会话")
|
|
32
|
+
project_path: Mapped[str] = mapped_column(String(512), default=".")
|
|
33
|
+
permission_mode: Mapped[str] = mapped_column(String(32), default="ask")
|
|
34
|
+
messages: Mapped[str] = mapped_column(Text, default="[]")
|
|
35
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC))
|
|
36
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
37
|
+
DateTime,
|
|
38
|
+
default=lambda: datetime.now(UTC),
|
|
39
|
+
onupdate=lambda: datetime.now(UTC),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SubagentTaskORM(Base):
|
|
44
|
+
__tablename__ = "subagent_tasks"
|
|
45
|
+
|
|
46
|
+
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
|
47
|
+
session_id: Mapped[str] = mapped_column(String(36), default="")
|
|
48
|
+
agent_type: Mapped[str] = mapped_column(String(32), default="explore")
|
|
49
|
+
instruction: Mapped[str] = mapped_column(Text, default="")
|
|
50
|
+
status: Mapped[str] = mapped_column(String(32), default="pending")
|
|
51
|
+
result: Mapped[str] = mapped_column(Text, default="")
|
|
52
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC))
|
|
53
|
+
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LongTermMemoryORM(Base):
|
|
57
|
+
"""长期记忆表 - 全局记忆存储"""
|
|
58
|
+
__tablename__ = "long_term_memories"
|
|
59
|
+
|
|
60
|
+
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
|
61
|
+
session_id: Mapped[str] = mapped_column(String(36), default="")
|
|
62
|
+
timestamp: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC))
|
|
63
|
+
keywords: Mapped[str] = mapped_column(Text, default="") # JSON 格式存储关键词列表
|
|
64
|
+
compressed_content: Mapped[str] = mapped_column(Text, default="") # 压缩后的核心内容
|
|
65
|
+
project_path: Mapped[str] = mapped_column(String(512), default="")
|
|
66
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ProjectMemoryORM(Base):
|
|
70
|
+
"""项目记忆表 - 项目级别存储"""
|
|
71
|
+
__tablename__ = "project_memories"
|
|
72
|
+
|
|
73
|
+
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
|
74
|
+
project_path: Mapped[str] = mapped_column(String(512), unique=True, index=True)
|
|
75
|
+
initialized_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC))
|
|
76
|
+
config: Mapped[str] = mapped_column(Text, default="") # JSON 格式存储项目配置
|
|
77
|
+
context_summary: Mapped[str] = mapped_column(Text, default="") # 项目上下文摘要
|
|
78
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
79
|
+
DateTime,
|
|
80
|
+
default=lambda: datetime.now(UTC),
|
|
81
|
+
onupdate=lambda: datetime.now(UTC),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_engine = None
|
|
86
|
+
_session_maker = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_engine() -> AsyncEngine:
|
|
90
|
+
global _engine
|
|
91
|
+
if _engine is None:
|
|
92
|
+
_engine = create_async_engine(settings.database_url, echo=settings.debug)
|
|
93
|
+
return _engine
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _get_session_maker() -> async_sessionmaker[AsyncSession]:
|
|
97
|
+
global _session_maker
|
|
98
|
+
if _session_maker is None:
|
|
99
|
+
_session_maker = async_sessionmaker(_get_engine(), expire_on_commit=False)
|
|
100
|
+
return _session_maker
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def init_db() -> None:
|
|
104
|
+
"""Initialize database tables."""
|
|
105
|
+
# Ensure parent directory exists for SQLite file
|
|
106
|
+
if settings.database_url.startswith("sqlite"):
|
|
107
|
+
db_path = settings.database_url.replace("sqlite+aiosqlite:///", "").replace(
|
|
108
|
+
"sqlite:///", ""
|
|
109
|
+
)
|
|
110
|
+
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
engine = _get_engine()
|
|
112
|
+
async with engine.begin() as conn:
|
|
113
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def get_db_session() -> AsyncIterator[AsyncSession]:
|
|
117
|
+
"""Yield an async DB session."""
|
|
118
|
+
async with _get_session_maker()() as session:
|
|
119
|
+
yield session
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _orm_to_subagent_task(row: SubagentTaskORM) -> SubagentTask:
|
|
123
|
+
return SubagentTask(
|
|
124
|
+
id=UUID(row.id),
|
|
125
|
+
session_id=UUID(row.session_id),
|
|
126
|
+
agent_type=row.agent_type,
|
|
127
|
+
instruction=row.instruction,
|
|
128
|
+
status=SubagentStatus(row.status),
|
|
129
|
+
result=row.result,
|
|
130
|
+
created_at=row.created_at,
|
|
131
|
+
completed_at=row.completed_at,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def create_subagent_task(task: SubagentTask) -> None:
|
|
136
|
+
"""Persist a new subagent task."""
|
|
137
|
+
async with _get_session_maker()() as db:
|
|
138
|
+
db.add(
|
|
139
|
+
SubagentTaskORM(
|
|
140
|
+
id=str(task.id),
|
|
141
|
+
session_id=str(task.session_id),
|
|
142
|
+
agent_type=task.agent_type,
|
|
143
|
+
instruction=task.instruction,
|
|
144
|
+
status=task.status.value,
|
|
145
|
+
result=task.result,
|
|
146
|
+
created_at=task.created_at,
|
|
147
|
+
completed_at=task.completed_at,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
await db.commit()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def get_subagent_task(task_id: UUID) -> SubagentTask | None:
|
|
154
|
+
"""Load a subagent task by id, or None if it does not exist."""
|
|
155
|
+
async with _get_session_maker()() as db:
|
|
156
|
+
result = await db.execute(
|
|
157
|
+
select(SubagentTaskORM).where(SubagentTaskORM.id == str(task_id))
|
|
158
|
+
)
|
|
159
|
+
row = result.scalar_one_or_none()
|
|
160
|
+
return _orm_to_subagent_task(row) if row is not None else None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async def finish_subagent_task(task_id: UUID, status: SubagentStatus, result_text: str) -> None:
|
|
164
|
+
"""Mark a subagent task as completed/failed and store its result."""
|
|
165
|
+
async with _get_session_maker()() as db:
|
|
166
|
+
result = await db.execute(
|
|
167
|
+
select(SubagentTaskORM).where(SubagentTaskORM.id == str(task_id))
|
|
168
|
+
)
|
|
169
|
+
row = result.scalar_one_or_none()
|
|
170
|
+
if row is None:
|
|
171
|
+
return
|
|
172
|
+
row.status = status.value
|
|
173
|
+
row.result = result_text
|
|
174
|
+
row.completed_at = datetime.now(UTC)
|
|
175
|
+
await db.commit()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def save_session_messages(session_id: UUID, messages: list[dict[str, Any]]) -> None:
|
|
179
|
+
"""Persist conversation messages (excluding the system prompt) for a session.
|
|
180
|
+
|
|
181
|
+
Creates the session row when it does not exist yet (e.g. CLI sessions).
|
|
182
|
+
"""
|
|
183
|
+
async with _get_session_maker()() as db:
|
|
184
|
+
result = await db.execute(select(SessionORM).where(SessionORM.id == str(session_id)))
|
|
185
|
+
row = result.scalar_one_or_none()
|
|
186
|
+
if row is None:
|
|
187
|
+
row = SessionORM(id=str(session_id))
|
|
188
|
+
db.add(row)
|
|
189
|
+
row.messages = json.dumps(messages, ensure_ascii=False)
|
|
190
|
+
await db.commit()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def get_session_messages(session_id: UUID) -> list[dict[str, Any]]:
|
|
194
|
+
"""Load previously persisted conversation messages for a session."""
|
|
195
|
+
async with _get_session_maker()() as db:
|
|
196
|
+
result = await db.execute(
|
|
197
|
+
select(SessionORM.messages).where(SessionORM.id == str(session_id))
|
|
198
|
+
)
|
|
199
|
+
raw = result.scalar_one_or_none()
|
|
200
|
+
if not raw:
|
|
201
|
+
return []
|
|
202
|
+
try:
|
|
203
|
+
data = json.loads(raw)
|
|
204
|
+
except json.JSONDecodeError:
|
|
205
|
+
return []
|
|
206
|
+
return data if isinstance(data, list) else []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pydantic models for ClaxCode."""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Session and chat request models."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from uuid import UUID, uuid4
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Session(BaseModel):
|
|
10
|
+
id: UUID = Field(default_factory=uuid4)
|
|
11
|
+
name: str = "New Session"
|
|
12
|
+
project_path: str = "."
|
|
13
|
+
permission_mode: str = "ask"
|
|
14
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
15
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SessionCreate(BaseModel):
|
|
19
|
+
name: str = "New Session"
|
|
20
|
+
project_path: str = "."
|
|
21
|
+
permission_mode: str = "ask"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChatRequest(BaseModel):
|
|
25
|
+
message: str
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Subagent models."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from uuid import UUID, uuid4
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SubagentStatus(str, Enum):
|
|
11
|
+
PENDING = "pending"
|
|
12
|
+
RUNNING = "running"
|
|
13
|
+
COMPLETED = "completed"
|
|
14
|
+
FAILED = "failed"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SubagentTask(BaseModel):
|
|
18
|
+
id: UUID = Field(default_factory=uuid4)
|
|
19
|
+
session_id: UUID
|
|
20
|
+
agent_type: str = "explore" # explore | planner | executor
|
|
21
|
+
instruction: str
|
|
22
|
+
status: SubagentStatus = SubagentStatus.PENDING
|
|
23
|
+
result: str = ""
|
|
24
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
25
|
+
completed_at: datetime | None = None
|
claxcode/models/tool.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Tool-related models."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ToolInfo(BaseModel):
|
|
9
|
+
name: str
|
|
10
|
+
description: str
|
|
11
|
+
input_schema: dict[str, Any]
|
|
12
|
+
category: str
|
|
13
|
+
required_permissions: list[str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolInvokeRequest(BaseModel):
|
|
17
|
+
name: str
|
|
18
|
+
arguments: dict[str, Any] = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ToolInvokeResponse(BaseModel):
|
|
22
|
+
name: str
|
|
23
|
+
output: str
|
|
24
|
+
error: bool = False
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Security and permissions for ClaxCode."""
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Durable audit logging — JSONL audit trail under .claxcode/."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from threading import Lock
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuditLogger:
|
|
12
|
+
"""Logs tool calls and permission decisions to a JSONL file."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, log_path: Path | None = None):
|
|
15
|
+
self._log_path = log_path or Path(".claxcode") / "audit.jsonl"
|
|
16
|
+
self._lock = Lock()
|
|
17
|
+
|
|
18
|
+
def log_tool_call(
|
|
19
|
+
self,
|
|
20
|
+
session_id: UUID,
|
|
21
|
+
tool_name: str,
|
|
22
|
+
arguments: dict[str, Any],
|
|
23
|
+
result: str,
|
|
24
|
+
allowed: bool,
|
|
25
|
+
) -> None:
|
|
26
|
+
entry = {
|
|
27
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
28
|
+
"session_id": str(session_id),
|
|
29
|
+
"tool_name": tool_name,
|
|
30
|
+
"arguments": arguments,
|
|
31
|
+
"result_preview": result[:500],
|
|
32
|
+
"allowed": allowed,
|
|
33
|
+
}
|
|
34
|
+
self._append(entry)
|
|
35
|
+
|
|
36
|
+
def read_recent(self, limit: int = 100) -> list[dict[str, Any]]:
|
|
37
|
+
"""Return the most recent entries (newest last)."""
|
|
38
|
+
if limit < 1:
|
|
39
|
+
return []
|
|
40
|
+
if not self._log_path.exists():
|
|
41
|
+
return []
|
|
42
|
+
lines = self._log_path.read_text(encoding="utf-8").splitlines()
|
|
43
|
+
return [json.loads(line) for line in lines[-limit:] if line.strip()]
|
|
44
|
+
|
|
45
|
+
def _append(self, entry: dict[str, Any]) -> None:
|
|
46
|
+
try:
|
|
47
|
+
self._log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
with self._lock, self._log_path.open("a", encoding="utf-8") as f:
|
|
49
|
+
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
50
|
+
except OSError:
|
|
51
|
+
pass # 审计写入失败不应阻塞主流程
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
audit_logger = AuditLogger()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Permission modes."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PermissionMode(str, Enum):
|
|
7
|
+
ASK = "ask"
|
|
8
|
+
AUTO_EDIT = "auto_edit"
|
|
9
|
+
PLAN = "plan"
|
|
10
|
+
AUTO_SAFE = "auto_safe"
|
|
11
|
+
YOLO = "yolo"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check_permission(
|
|
15
|
+
mode: PermissionMode, tool_name: str, required_perms: list[str]
|
|
16
|
+
) -> tuple[bool, str]:
|
|
17
|
+
"""Return (allowed, reason)."""
|
|
18
|
+
needs_write = "write" in required_perms or "execute" in required_perms
|
|
19
|
+
if mode == PermissionMode.PLAN and needs_write:
|
|
20
|
+
return False, "计划模式为只读模式"
|
|
21
|
+
if mode == PermissionMode.ASK:
|
|
22
|
+
# Read-only tools run freely; anything else requires user approval
|
|
23
|
+
if set(required_perms) <= {"read"}:
|
|
24
|
+
return True, ""
|
|
25
|
+
return False, "需要批准"
|
|
26
|
+
if mode == PermissionMode.AUTO_SAFE and "execute" in required_perms:
|
|
27
|
+
return False, "在自动安全模式下执行操作需要批准"
|
|
28
|
+
# AUTO_EDIT and YOLO allow everything
|
|
29
|
+
return True, ""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Bug Fix
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
缺陷修复方法论:先复现、找根因、最小修复、回归防复发。不猜原因,不修表象。
|
|
6
|
+
|
|
7
|
+
## Instructions
|
|
8
|
+
|
|
9
|
+
修复 bug 时严格按以下顺序:
|
|
10
|
+
|
|
11
|
+
**1. 复现(一切的前提)**
|
|
12
|
+
- 先让 bug 可重复出现:运行失败测试、最小复现脚本,或按用户描述重走一遍。
|
|
13
|
+
- 无法复现时,先向用户索要复现步骤/日志,不要直接开改。
|
|
14
|
+
|
|
15
|
+
**2. 定位根因**
|
|
16
|
+
- 从错误信息/堆栈出发,读相关代码;怀疑多处时逐一带假设验证(加打印或写小测试)。
|
|
17
|
+
- 区分"出错的位置"和"出错的原因"——异常抛出点往往不是根因。
|
|
18
|
+
- 若与近期改动有关,优先检查那些改动。
|
|
19
|
+
|
|
20
|
+
**3. 最小修复**
|
|
21
|
+
- 只修根因,用最少的改动;不顺手清理周边代码、不改变无关行为。
|
|
22
|
+
- 修复方案有多个时,选最简单、对现有逻辑侵入最小的那个。
|
|
23
|
+
|
|
24
|
+
**4. 回归验证(缺一不可)**
|
|
25
|
+
- 先证明修复有效:复现步骤不再出错。
|
|
26
|
+
- 再证明没有破坏:跑相关测试;最好补一个针对该 bug 的回归测试,防止复发。
|
|
27
|
+
|
|
28
|
+
**5. 汇报**
|
|
29
|
+
- 一句话说清根因、修复方式、验证证据;若只缓解了表象,必须明说。
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
- `fix`: 按此方法论修复一个 bug(用法:/fix <bug 描述或错误信息>)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Code Review
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
代码审查方法论:按"正确性 > 安全 > 简洁 > 规范"的优先级审查,输出按严重度分级、可直接执行的结论。
|
|
6
|
+
|
|
7
|
+
## Instructions
|
|
8
|
+
|
|
9
|
+
审查代码(diff、文件或目录)时按以下顺序检查,发现即记录:
|
|
10
|
+
|
|
11
|
+
**1. 正确性(最高优先级)**
|
|
12
|
+
- 逻辑错误、边界条件(空值/越界/并发)、异常路径、资源泄漏(连接/文件未关闭)。
|
|
13
|
+
- 行为是否与注释、文档、测试描述一致。
|
|
14
|
+
|
|
15
|
+
**2. 安全**
|
|
16
|
+
- 注入风险(命令/SQL/路径穿越)、密钥硬编码、敏感信息进日志、不安全的反序列化。
|
|
17
|
+
- 写操作是否有权限校验和备份/回滚手段。
|
|
18
|
+
|
|
19
|
+
**3. 简洁**
|
|
20
|
+
- 死代码、重复代码、可以压平的嵌套、过度设计(为用不到的需求做的抽象和配置)。
|
|
21
|
+
|
|
22
|
+
**4. 规范(最低优先级)**
|
|
23
|
+
- 命名、类型标注、docstring、与周边代码风格一致性;lint/类型检查是否引入新违规。
|
|
24
|
+
|
|
25
|
+
**输出格式(严格遵守)**
|
|
26
|
+
- 先给一行总体结论(可合并/需修改/不可合并)。
|
|
27
|
+
- 问题按三级分类列出,每条注明 `文件:行号`、问题、建议改法:
|
|
28
|
+
- 🔴 阻断:必须修复(正确性/安全问题)
|
|
29
|
+
- 🟡 建议:应该修复(简洁/明显改进)
|
|
30
|
+
- ⚪ 可选:风格偏好,可不改
|
|
31
|
+
- 没有问题就直说"未发现问题",不要硬凑。
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
- `review`: 审查指定文件、目录或当前改动(用法:/review [路径];无参数时审查 git 未提交改动)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Python Refactoring
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Expert-level Python refactoring assistant. Prefer type hints, dataclasses, and modern Python idioms (3.11+).
|
|
6
|
+
|
|
7
|
+
## Instructions
|
|
8
|
+
|
|
9
|
+
When asked to refactor Python code:
|
|
10
|
+
|
|
11
|
+
1. Use `pathlib.Path` instead of `os.path`.
|
|
12
|
+
2. Prefer `dataclasses` over plain classes with `__init__`.
|
|
13
|
+
3. Use structural pattern matching (`match`/`case`) where appropriate.
|
|
14
|
+
4. Add type hints to all public functions.
|
|
15
|
+
5. Keep docstrings in Google style.
|
|
16
|
+
6. Use f-strings for formatting, never `%` formatting.
|
|
17
|
+
7. Prefer list/dict comprehensions over explicit loops when readable.
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
- `refactor_file`: Refactor an entire Python file in place
|
|
22
|
+
- `add_types`: Add type hints to a function or module
|