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.
Files changed (60) hide show
  1. claxcode/__init__.py +3 -0
  2. claxcode/api/__init__.py +1 -0
  3. claxcode/api/chat.py +51 -0
  4. claxcode/api/session.py +66 -0
  5. claxcode/api/subagents.py +82 -0
  6. claxcode/api/tools.py +38 -0
  7. claxcode/cli.py +1214 -0
  8. claxcode/cli_completion.py +174 -0
  9. claxcode/config.py +54 -0
  10. claxcode/core/__init__.py +1 -0
  11. claxcode/core/agent.py +327 -0
  12. claxcode/core/checkpoint.py +100 -0
  13. claxcode/core/context.py +114 -0
  14. claxcode/core/models.py +446 -0
  15. claxcode/extensions/__init__.py +1 -0
  16. claxcode/extensions/hooks.py +131 -0
  17. claxcode/extensions/mcp.py +13 -0
  18. claxcode/extensions/plugins.py +10 -0
  19. claxcode/extensions/skills.py +205 -0
  20. claxcode/main.py +55 -0
  21. claxcode/memory/__init__.py +23 -0
  22. claxcode/memory/auto_memory.py +68 -0
  23. claxcode/memory/long_term.py +196 -0
  24. claxcode/memory/project_context.py +20 -0
  25. claxcode/memory/project_memory.py +194 -0
  26. claxcode/memory/session_store.py +206 -0
  27. claxcode/models/__init__.py +1 -0
  28. claxcode/models/session.py +25 -0
  29. claxcode/models/subagent.py +25 -0
  30. claxcode/models/tool.py +24 -0
  31. claxcode/security/__init__.py +1 -0
  32. claxcode/security/audit.py +54 -0
  33. claxcode/security/permissions.py +29 -0
  34. claxcode/skills/bug-fix/SKILL.md +33 -0
  35. claxcode/skills/code-review/SKILL.md +35 -0
  36. claxcode/skills/python-refactor/SKILL.md +22 -0
  37. claxcode/skills/task-workflow/SKILL.md +38 -0
  38. claxcode/subagents/__init__.py +1 -0
  39. claxcode/subagents/base.py +108 -0
  40. claxcode/subagents/executor.py +16 -0
  41. claxcode/subagents/explore.py +25 -0
  42. claxcode/subagents/planner.py +25 -0
  43. claxcode/tools/__init__.py +14 -0
  44. claxcode/tools/admin.py +170 -0
  45. claxcode/tools/base.py +19 -0
  46. claxcode/tools/browser.py +499 -0
  47. claxcode/tools/code_intelligence.py +68 -0
  48. claxcode/tools/custom_example.py +76 -0
  49. claxcode/tools/execution.py +114 -0
  50. claxcode/tools/file_operations.py +287 -0
  51. claxcode/tools/memory_tool.py +29 -0
  52. claxcode/tools/registry.py +35 -0
  53. claxcode/tools/search.py +157 -0
  54. claxcode/tools/ssh.py +534 -0
  55. claxcode/tools/web.py +83 -0
  56. claxcode-0.1.1.dist-info/METADATA +359 -0
  57. claxcode-0.1.1.dist-info/RECORD +60 -0
  58. claxcode-0.1.1.dist-info/WHEEL +4 -0
  59. claxcode-0.1.1.dist-info/entry_points.txt +3 -0
  60. claxcode-0.1.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,13 @@
1
+ """MCP (Model Context Protocol) connector (stub for Phase 5)."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class MCPClient:
7
+ """Client for connecting to MCP servers."""
8
+
9
+ def __init__(self, server_url: str):
10
+ self.server_url = server_url
11
+
12
+ async def list_tools(self) -> list[dict[str, Any]]:
13
+ return []
@@ -0,0 +1,10 @@
1
+ """Plugin system (stub for Phase 5)."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ class PluginManager:
7
+ """Load and manage cross-project plugins."""
8
+
9
+ def load_plugin(self, path: Path) -> None:
10
+ pass
@@ -0,0 +1,205 @@
1
+ """Skills system — load and inject skill packages into agent context.
2
+
3
+ Inspired by Claude Code's Skills. A skill is a directory containing a SKILL.md
4
+ file that describes capabilities, conventions, and commands. The SKILL.md content
5
+ is injected into the agent's system prompt.
6
+
7
+ Skill directory layout:
8
+ my-skill/
9
+ SKILL.md # Required: name, description, instructions
10
+ tools.py # Optional: skill-specific tools (auto-registered)
11
+ (optional files) # Templates, scripts, or reference docs
12
+
13
+ SKILL.md format:
14
+ # Skill Name
15
+
16
+ ## Description
17
+ Brief explanation of what this skill does.
18
+
19
+ ## Instructions
20
+ Detailed instructions for the agent on how to use this skill.
21
+ Can include examples, conventions, file patterns, etc.
22
+
23
+ ## Commands (optional)
24
+ - `cmd_name`: Description of what this command does
25
+
26
+ Skill sources (later overrides earlier on name conflict):
27
+ builtin — shipped inside the package at claxcode/skills/
28
+ user — ~/.claxcode/skills/ and ~/.agent/skills/
29
+ project — <project>/.claxcode/skills/, <project>/.agent/skills/ and <project>/skills/
30
+ """
31
+
32
+ from dataclasses import dataclass, field
33
+ from pathlib import Path
34
+
35
+ from claxcode.tools.registry import registry
36
+
37
+ BUILTIN_SKILLS_DIR = Path(__file__).resolve().parent.parent / "skills"
38
+
39
+
40
+ @dataclass
41
+ class Skill:
42
+ """A loaded skill package."""
43
+
44
+ name: str
45
+ description: str
46
+ content: str # Full SKILL.md text
47
+ commands: list[dict[str, str]] # Optional custom commands
48
+ path: Path # Directory containing the skill
49
+ source: str = "project" # builtin | user | project
50
+ registered_tools: list[str] = field(default_factory=list)
51
+
52
+
53
+ class SkillManager:
54
+ """Load and manage skill packages."""
55
+
56
+ def __init__(self) -> None:
57
+ self.skills: list[Skill] = []
58
+ self._skill_dirs: list[tuple[Path, str]] = []
59
+
60
+ def load_defaults(self, project_path: Path) -> None:
61
+ """Load built-in, user-global, then project skills.
62
+
63
+ Later sources override earlier ones when skill names collide.
64
+ """
65
+ self.load_skill_dir(BUILTIN_SKILLS_DIR, source="builtin")
66
+ self.load_skill_dir(Path.home() / ".claxcode" / "skills", source="user")
67
+ self.load_skill_dir(Path.home() / ".agent" / "skills", source="user")
68
+ self.load_skill_dir(project_path / ".claxcode" / "skills", source="project")
69
+ self.load_skill_dir(project_path / ".agent" / "skills", source="project")
70
+ self.load_skill_dir(project_path / "skills", source="project")
71
+
72
+ def load_skill_dir(self, path: Path, source: str = "project") -> None:
73
+ """Scan a directory for skill packages and load them."""
74
+ if not path.exists():
75
+ return
76
+ if (path, source) not in self._skill_dirs:
77
+ self._skill_dirs.append((path, source))
78
+ for entry in sorted(path.iterdir()):
79
+ if entry.is_dir() and (entry / "SKILL.md").exists():
80
+ self._load_skill(entry, source)
81
+
82
+ def _load_skill(self, skill_dir: Path, source: str) -> None:
83
+ """Parse and register a single skill, replacing any same-name skill."""
84
+ skill = self._parse_skill_md(skill_dir, source)
85
+ self._remove_skill(skill.name) # unregister tools of the overridden skill
86
+ self._load_skill_tools(skill_dir, skill)
87
+ self.skills.append(skill)
88
+
89
+ def _parse_skill_md(self, skill_dir: Path, source: str) -> Skill:
90
+ """Parse SKILL.md into a Skill (no side effects)."""
91
+ raw = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
92
+
93
+ name = skill_dir.name
94
+ description = ""
95
+ commands: list[dict[str, str]] = []
96
+
97
+ # Simple parsing: extract first H1 as name, first paragraph as description
98
+ lines = raw.splitlines()
99
+ for i, line in enumerate(lines):
100
+ if line.startswith("# ") and name == skill_dir.name:
101
+ name = line[2:].strip()
102
+ if line.lower().startswith("## description"):
103
+ # Collect following paragraph
104
+ for j in range(i + 1, len(lines)):
105
+ if lines[j].strip() and not lines[j].startswith("##"):
106
+ description = lines[j].strip()
107
+ break
108
+ if line.lower().startswith("## commands"):
109
+ # Parse command list
110
+ for j in range(i + 1, len(lines)):
111
+ cmd_line = lines[j].strip()
112
+ if cmd_line.startswith("##"):
113
+ break
114
+ if cmd_line.startswith("- "):
115
+ cmd_text = cmd_line[2:].strip()
116
+ if ":" in cmd_text:
117
+ cmd_name, cmd_desc = cmd_text.split(":", 1)
118
+ cmd_label = cmd_name.strip().strip("`")
119
+ commands.append({"name": cmd_label, "description": cmd_desc.strip()})
120
+
121
+ if not description:
122
+ # Fallback: first non-empty, non-header line
123
+ for line in lines:
124
+ stripped = line.strip()
125
+ if stripped and not stripped.startswith("#"):
126
+ description = stripped
127
+ break
128
+
129
+ return Skill(
130
+ name=name,
131
+ description=description,
132
+ content=raw,
133
+ commands=commands,
134
+ path=skill_dir,
135
+ source=source,
136
+ )
137
+
138
+ def _load_skill_tools(self, skill_dir: Path, skill: Skill) -> None:
139
+ """Dynamically load tools.py from a skill directory."""
140
+ tools_file = skill_dir / "tools.py"
141
+ if not tools_file.exists():
142
+ return
143
+
144
+ before = set(registry._tools.keys())
145
+ code = tools_file.read_text(encoding="utf-8")
146
+ namespace = {
147
+ "__name__": f"claxcode_skill_{skill.name}",
148
+ "__file__": str(tools_file),
149
+ }
150
+ exec(compile(code, str(tools_file), "exec"), namespace) # noqa: S102
151
+ after = set(registry._tools.keys())
152
+ skill.registered_tools = sorted(after - before)
153
+
154
+ def _remove_skill(self, name: str) -> bool:
155
+ """Remove a skill by name and unregister its tools."""
156
+ for i, skill in enumerate(self.skills):
157
+ if skill.name == name:
158
+ for tool_name in skill.registered_tools:
159
+ registry.unregister(tool_name)
160
+ self.skills.pop(i)
161
+ return True
162
+ return False
163
+
164
+ def unload_skill(self, name: str) -> bool:
165
+ """Unload a skill and unregister its tools."""
166
+ return self._remove_skill(name)
167
+
168
+ def reload_all(self) -> tuple[int, int]:
169
+ """Unload all skills and reload from all known directories.
170
+
171
+ Returns (skills_loaded, tools_registered).
172
+ """
173
+ for skill in self.skills:
174
+ for tool_name in skill.registered_tools:
175
+ registry.unregister(tool_name)
176
+ self.skills.clear()
177
+
178
+ for path, source in self._skill_dirs:
179
+ self.load_skill_dir(path, source)
180
+ return len(self.skills), sum(len(s.registered_tools) for s in self.skills)
181
+
182
+ def to_prompt_fragment(self) -> str:
183
+ """Convert all loaded skills into a system-prompt fragment."""
184
+ if not self.skills:
185
+ return ""
186
+ lines = ["# Skills"]
187
+ for skill in self.skills:
188
+ lines.append(f"\n## {skill.name}")
189
+ lines.append(skill.content)
190
+ return "\n".join(lines)
191
+
192
+ def get_skill_commands(self) -> list[dict[str, str]]:
193
+ """Return all custom commands defined across skills (with source skill)."""
194
+ cmds = []
195
+ for skill in self.skills:
196
+ for cmd in skill.commands:
197
+ cmds.append({**cmd, "skill": skill.name})
198
+ return cmds
199
+
200
+ def find_command(self, name: str) -> dict[str, str] | None:
201
+ """Find a skill command by name (without the leading slash)."""
202
+ for cmd in self.get_skill_commands():
203
+ if cmd["name"] == name:
204
+ return cmd
205
+ return None
claxcode/main.py ADDED
@@ -0,0 +1,55 @@
1
+ """FastAPI application entry point."""
2
+
3
+ from contextlib import asynccontextmanager
4
+
5
+ from fastapi import FastAPI
6
+ from fastapi.responses import PlainTextResponse
7
+
8
+ # Ensure all built-in tools are registered before API starts
9
+ import claxcode.tools # noqa: F401
10
+ from claxcode.api import chat, session, subagents, tools
11
+ from claxcode.config import settings
12
+ from claxcode.memory.session_store import init_db
13
+
14
+
15
+ @asynccontextmanager
16
+ async def lifespan(app: FastAPI):
17
+ """Application lifespan events."""
18
+ # Startup
19
+ await init_db()
20
+ yield
21
+ # Shutdown
22
+
23
+
24
+ app = FastAPI(
25
+ title=settings.app_name,
26
+ description="Agentic coding assistant API",
27
+ version="0.1.0",
28
+ lifespan=lifespan,
29
+ )
30
+
31
+ app.include_router(session.router, prefix="/sessions", tags=["sessions"])
32
+ app.include_router(chat.router, prefix="/sessions", tags=["chat"])
33
+ app.include_router(tools.router, prefix="/tools", tags=["tools"])
34
+ app.include_router(subagents.router, prefix="/subagents", tags=["subagents"])
35
+
36
+
37
+ @app.get("/health", response_class=PlainTextResponse)
38
+ async def health():
39
+ return "ok"
40
+
41
+
42
+ def run() -> None:
43
+ """CLI entry point to start the server."""
44
+ import uvicorn
45
+
46
+ uvicorn.run(
47
+ "claxcode.main:app",
48
+ host=settings.host,
49
+ port=settings.port,
50
+ reload=settings.debug,
51
+ )
52
+
53
+
54
+ if __name__ == "__main__":
55
+ run()
@@ -0,0 +1,23 @@
1
+ """Memory system for ClaxCode."""
2
+
3
+ from claxcode.memory.long_term import LongTermMemory, long_term_memory
4
+ from claxcode.memory.project_memory import ProjectMemory, project_memory
5
+ from claxcode.memory.session_store import (
6
+ LongTermMemoryORM,
7
+ ProjectMemoryORM,
8
+ SessionORM,
9
+ get_db_session,
10
+ init_db,
11
+ )
12
+
13
+ __all__ = [
14
+ "long_term_memory",
15
+ "LongTermMemory",
16
+ "project_memory",
17
+ "ProjectMemory",
18
+ "init_db",
19
+ "get_db_session",
20
+ "SessionORM",
21
+ "LongTermMemoryORM",
22
+ "ProjectMemoryORM",
23
+ ]
@@ -0,0 +1,68 @@
1
+ """Auto-memory for learned facts and preferences (durable JSON store)."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ DEFAULT_STORE_PATH = Path.home() / ".claxcode" / "auto_memory.json"
7
+
8
+
9
+ class AutoMemory:
10
+ """Stores stable global user preferences and facts in a JSON file."""
11
+
12
+ def __init__(self, store_path: Path | None = None, limit: int = 50):
13
+ self._store_path = store_path or DEFAULT_STORE_PATH
14
+ self._limit = limit
15
+ self._memories: dict[str, str] = self._load()
16
+
17
+ def remember(self, key: str, value: str) -> str:
18
+ """Record or update a fact. Returns a human-readable status."""
19
+ if key not in self._memories and len(self._memories) >= self._limit:
20
+ return f"记忆已满({self._limit} 条上限),请先清理其他条目"
21
+ self._memories[key] = value
22
+ self._save()
23
+ return f"已记住: {key}"
24
+
25
+ def recall(self, key: str) -> str | None:
26
+ return self._memories.get(key)
27
+
28
+ def forget(self, key: str) -> bool:
29
+ """Delete a fact. Returns True if it existed."""
30
+ if key not in self._memories:
31
+ return False
32
+ del self._memories[key]
33
+ self._save()
34
+ return True
35
+
36
+ def items(self) -> dict[str, str]:
37
+ return dict(self._memories)
38
+
39
+ def to_prompt_fragment(self) -> str:
40
+ if not self._memories:
41
+ return ""
42
+ lines = ["# 全局用户偏好与事实(自动记忆)"]
43
+ for k, v in self._memories.items():
44
+ lines.append(f"- {k}: {v}")
45
+ return "\n".join(lines)
46
+
47
+ def _load(self) -> dict[str, str]:
48
+ if not self._store_path.exists():
49
+ return {}
50
+ try:
51
+ data = json.loads(self._store_path.read_text(encoding="utf-8"))
52
+ except (json.JSONDecodeError, OSError):
53
+ return {}
54
+ return {str(k): str(v) for k, v in data.items()} if isinstance(data, dict) else {}
55
+
56
+ def _save(self) -> None:
57
+ try:
58
+ self._store_path.parent.mkdir(parents=True, exist_ok=True)
59
+ self._store_path.write_text(
60
+ json.dumps(self._memories, ensure_ascii=False, indent=2),
61
+ encoding="utf-8",
62
+ )
63
+ except OSError:
64
+ pass # 记忆写入失败不应阻塞主流程
65
+
66
+
67
+ # Global singleton shared by the agent prompt, the remember_fact tool and the CLI.
68
+ auto_memory = AutoMemory()
@@ -0,0 +1,196 @@
1
+ """Searchable long-term memories with project and global scopes."""
2
+
3
+ import json
4
+ import re
5
+ from datetime import UTC, datetime
6
+ from pathlib import Path
7
+ from typing import Any
8
+ from uuid import uuid4
9
+
10
+ from sqlalchemy import desc, or_, select
11
+
12
+ from claxcode.memory.session_store import LongTermMemoryORM, get_db_session
13
+
14
+ _STOP_TERMS = {
15
+ "我们", "你们", "他们", "这个", "那个", "可以", "怎么", "什么", "一下",
16
+ "帮我", "谢谢", "现在", "还是", "就是", "因为", "所以", "如果", "已经", "继续",
17
+ }
18
+
19
+
20
+ def _normalize_project_path(project_path: str) -> str:
21
+ return str(Path(project_path).resolve()) if project_path else ""
22
+
23
+
24
+ def _search_terms(keywords: list[str] | None) -> list[str]:
25
+ """Build searchable words and Chinese bigrams without external tokenizers."""
26
+ if not keywords:
27
+ return []
28
+ terms: set[str] = set()
29
+ for token in re.findall(r"[a-z0-9_]+|[\u4e00-\u9fff]+", " ".join(keywords).lower()):
30
+ if re.fullmatch(r"[a-z0-9_]+", token):
31
+ if len(token) >= 2:
32
+ terms.add(token)
33
+ continue
34
+ if len(token) <= 6 and token not in _STOP_TERMS:
35
+ terms.add(token)
36
+ terms.update(
37
+ token[i:i + 2]
38
+ for i in range(len(token) - 1)
39
+ if token[i:i + 2] not in _STOP_TERMS
40
+ )
41
+ return sorted(terms, key=lambda term: (-len(term), term))[:60]
42
+
43
+
44
+ def _relevance(memory: LongTermMemoryORM, terms: list[str]) -> int:
45
+ text = f"{memory.keywords}\n{memory.compressed_content}".lower()
46
+ return sum(len(term) if len(term) > 2 else 1 for term in terms if term in text)
47
+
48
+
49
+ def _to_dict(memory: LongTermMemoryORM, truncate: bool = False) -> dict[str, Any]:
50
+ content = memory.compressed_content
51
+ if truncate and len(content) > 200:
52
+ content = content[:200] + "..."
53
+ return {
54
+ "id": memory.id,
55
+ "session_id": memory.session_id,
56
+ "timestamp": memory.timestamp.isoformat(),
57
+ "keywords": json.loads(memory.keywords) if memory.keywords else [],
58
+ "compressed_content": content,
59
+ "project_path": memory.project_path,
60
+ }
61
+
62
+
63
+ class LongTermMemory:
64
+ """Manage searchable project memories and cross-project global memories."""
65
+
66
+ async def save(
67
+ self,
68
+ session_id: str,
69
+ compressed_content: str,
70
+ keywords: list[str],
71
+ project_path: str = "",
72
+ ) -> str:
73
+ """Save a memory; an empty project_path means global scope."""
74
+ content = compressed_content.strip()
75
+ if not content:
76
+ raise ValueError("记忆内容不能为空")
77
+ memory_id = str(uuid4())
78
+ normalized_keywords = list(dict.fromkeys(k.strip() for k in keywords if k.strip()))
79
+ async for db in get_db_session():
80
+ memory = LongTermMemoryORM(
81
+ id=memory_id,
82
+ session_id=session_id,
83
+ timestamp=datetime.now(UTC),
84
+ keywords=json.dumps(normalized_keywords, ensure_ascii=False),
85
+ compressed_content=content,
86
+ project_path=_normalize_project_path(project_path),
87
+ )
88
+ db.add(memory)
89
+ await db.commit()
90
+ return memory_id
91
+ return memory_id
92
+
93
+ async def search(
94
+ self,
95
+ keywords: list[str] | None = None,
96
+ project_path: str | None = None,
97
+ limit: int = 10,
98
+ include_global: bool = False,
99
+ ) -> list[dict[str, Any]]:
100
+ """Search by relevance within a project, optionally including global memories."""
101
+ terms = _search_terms(keywords)
102
+ normalized_path = _normalize_project_path(project_path or "")
103
+ async for db in get_db_session():
104
+ query = select(LongTermMemoryORM).order_by(desc(LongTermMemoryORM.timestamp))
105
+ if normalized_path:
106
+ if include_global:
107
+ query = query.where(
108
+ or_(
109
+ LongTermMemoryORM.project_path == normalized_path,
110
+ LongTermMemoryORM.project_path == "",
111
+ )
112
+ )
113
+ else:
114
+ query = query.where(LongTermMemoryORM.project_path == normalized_path)
115
+
116
+ result = await db.execute(query.limit(200 if terms else limit))
117
+ memories = list(result.scalars().all())
118
+ if terms:
119
+ memories = [m for m in memories if _relevance(m, terms) > 0]
120
+ memories.sort(key=lambda m: _relevance(m, terms), reverse=True)
121
+ return [_to_dict(m) for m in memories[:limit]]
122
+ return []
123
+
124
+ async def list_all(
125
+ self,
126
+ limit: int = 50,
127
+ project_path: str | None = None,
128
+ include_global: bool = False,
129
+ ) -> list[dict[str, Any]]:
130
+ """List recent memories, optionally restricted to project and global scopes."""
131
+ normalized_path = _normalize_project_path(project_path or "")
132
+ async for db in get_db_session():
133
+ query = select(LongTermMemoryORM).order_by(desc(LongTermMemoryORM.timestamp))
134
+ if normalized_path:
135
+ if include_global:
136
+ query = query.where(
137
+ or_(
138
+ LongTermMemoryORM.project_path == normalized_path,
139
+ LongTermMemoryORM.project_path == "",
140
+ )
141
+ )
142
+ else:
143
+ query = query.where(LongTermMemoryORM.project_path == normalized_path)
144
+ result = await db.execute(query.limit(limit))
145
+ return [_to_dict(m, truncate=True) for m in result.scalars().all()]
146
+ return []
147
+
148
+ async def delete(self, memory_id: str) -> bool:
149
+ """Delete a memory by ID."""
150
+ async for db in get_db_session():
151
+ result = await db.execute(
152
+ select(LongTermMemoryORM).where(LongTermMemoryORM.id == memory_id)
153
+ )
154
+ memory = result.scalar_one_or_none()
155
+ if memory:
156
+ await db.delete(memory)
157
+ await db.commit()
158
+ return True
159
+ return False
160
+ return False
161
+
162
+ async def export_to_file(self, filepath: str, project_path: str | None = None) -> int:
163
+ """Export memories to JSON."""
164
+ memories = await self.search(project_path=project_path, limit=1000)
165
+ export_data = {
166
+ "export_time": datetime.now(UTC).isoformat(),
167
+ "memories": memories,
168
+ }
169
+ Path(filepath).write_text(
170
+ json.dumps(export_data, ensure_ascii=False, indent=2),
171
+ encoding="utf-8",
172
+ )
173
+ return len(memories)
174
+
175
+ async def import_from_file(self, filepath: str) -> int:
176
+ """Import memories from JSON without reusing their original IDs."""
177
+ data = json.loads(Path(filepath).read_text(encoding="utf-8"))
178
+ memories = data.get("memories", [])
179
+ count = 0
180
+ async for db in get_db_session():
181
+ for item in memories:
182
+ memory = LongTermMemoryORM(
183
+ id=str(uuid4()),
184
+ session_id=item.get("session_id", ""),
185
+ timestamp=datetime.fromisoformat(item["timestamp"]),
186
+ keywords=json.dumps(item.get("keywords", []), ensure_ascii=False),
187
+ compressed_content=item.get("compressed_content", ""),
188
+ project_path=_normalize_project_path(item.get("project_path", "")),
189
+ )
190
+ db.add(memory)
191
+ count += 1
192
+ await db.commit()
193
+ return count
194
+
195
+
196
+ long_term_memory = LongTermMemory()
@@ -0,0 +1,20 @@
1
+ """Project-level context loading."""
2
+
3
+ from pathlib import Path
4
+
5
+ from claxcode.config import settings
6
+
7
+
8
+ def load_project_context(project_path: Path) -> str:
9
+ """Load CLAUDE.md or .claxcode/context.md if present."""
10
+ candidates = [
11
+ project_path / settings.project_context_file,
12
+ project_path / ".claxcode" / "context.md",
13
+ ]
14
+ for candidate in candidates:
15
+ if candidate.exists():
16
+ try:
17
+ return candidate.read_text(encoding="utf-8")
18
+ except Exception:
19
+ continue
20
+ return ""