pythonclaw 0.3.0__tar.gz → 0.3.2__tar.gz

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 (106) hide show
  1. {pythonclaw-0.3.0/pythonclaw.egg-info → pythonclaw-0.3.2}/PKG-INFO +3 -8
  2. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pyproject.toml +3 -9
  3. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/__init__.py +1 -1
  4. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/config.py +18 -17
  5. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/agent.py +19 -12
  6. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/compaction.py +9 -3
  7. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/memory/storage.py +4 -1
  8. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/session_store.py +8 -3
  9. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/skillhub.py +2 -1
  10. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/tools.py +2 -1
  11. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/daemon.py +5 -2
  12. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/init.py +9 -4
  13. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/onboard.py +3 -2
  14. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/scheduler/cron.py +18 -8
  15. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/scheduler/heartbeat.py +13 -3
  16. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/server.py +0 -3
  17. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/system/change_setting/update_config.py +12 -5
  18. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/system/onboarding/write_identity.py +4 -2
  19. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/web/app.py +10 -10
  20. {pythonclaw-0.3.0 → pythonclaw-0.3.2/pythonclaw.egg-info}/PKG-INFO +3 -8
  21. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw.egg-info/requires.txt +2 -10
  22. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/tests/test_persistence.py +5 -5
  23. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/LICENSE +0 -0
  24. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/MANIFEST.in +0 -0
  25. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/README.md +0 -0
  26. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/__main__.py +0 -0
  27. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/channels/discord_bot.py +0 -0
  28. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/channels/telegram_bot.py +0 -0
  29. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/__init__.py +0 -0
  30. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/knowledge/rag.py +0 -0
  31. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/llm/anthropic_client.py +0 -0
  32. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/llm/base.py +0 -0
  33. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/llm/gemini_client.py +0 -0
  34. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/llm/openai_compatible.py +0 -0
  35. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/llm/response.py +0 -0
  36. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/memory/manager.py +0 -0
  37. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/persistent_agent.py +0 -0
  38. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/retrieval/__init__.py +0 -0
  39. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/retrieval/chunker.py +0 -0
  40. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/retrieval/dense.py +0 -0
  41. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/retrieval/fusion.py +0 -0
  42. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/retrieval/reranker.py +0 -0
  43. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/retrieval/retriever.py +0 -0
  44. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/retrieval/sparse.py +0 -0
  45. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/skill_loader.py +0 -0
  46. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/core/utils.py +0 -0
  47. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/main.py +0 -0
  48. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/session_manager.py +0 -0
  49. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/persona/demo_persona.md +0 -0
  50. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/communication/CATEGORY.md +0 -0
  51. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/communication/email/SKILL.md +0 -0
  52. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/communication/email/send_email.py +0 -0
  53. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/CATEGORY.md +0 -0
  54. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/csv_analyzer/SKILL.md +0 -0
  55. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/csv_analyzer/analyze.py +0 -0
  56. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/finance/SKILL.md +0 -0
  57. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/finance/fetch_quote.py +0 -0
  58. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/news/SKILL.md +0 -0
  59. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/news/search_news.py +0 -0
  60. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/pdf_reader/SKILL.md +0 -0
  61. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/pdf_reader/read_pdf.py +0 -0
  62. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/scraper/SKILL.md +0 -0
  63. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/scraper/scrape.py +0 -0
  64. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/weather/SKILL.md +0 -0
  65. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/weather/weather.py +0 -0
  66. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/youtube/SKILL.md +0 -0
  67. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/data/youtube/youtube_info.py +0 -0
  68. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/dev/CATEGORY.md +0 -0
  69. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/dev/code_runner/SKILL.md +0 -0
  70. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/dev/code_runner/run_code.py +0 -0
  71. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/dev/github/SKILL.md +0 -0
  72. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/dev/github/gh.py +0 -0
  73. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/dev/http_request/SKILL.md +0 -0
  74. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/dev/http_request/request.py +0 -0
  75. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/google/CATEGORY.md +0 -0
  76. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/google/workspace/SKILL.md +0 -0
  77. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/meta/CATEGORY.md +0 -0
  78. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/meta/skill_creator/SKILL.md +0 -0
  79. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/system/CATEGORY.md +0 -0
  80. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/system/change_persona/SKILL.md +0 -0
  81. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/system/change_setting/SKILL.md +0 -0
  82. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/system/change_soul/SKILL.md +0 -0
  83. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/system/onboarding/SKILL.md +0 -0
  84. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/system/random/SKILL.md +0 -0
  85. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/system/random/random_util.py +0 -0
  86. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/system/time/SKILL.md +0 -0
  87. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/system/time/time_util.py +0 -0
  88. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/text/CATEGORY.md +0 -0
  89. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/text/translator/SKILL.md +0 -0
  90. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/text/translator/translate.py +0 -0
  91. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/web/CATEGORY.md +0 -0
  92. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/skills/web/tavily/SKILL.md +0 -0
  93. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/templates/soul/SOUL.md +0 -0
  94. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/web/__init__.py +0 -0
  95. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/web/static/favicon.png +0 -0
  96. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/web/static/index.html +0 -0
  97. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw/web/static/logo.png +0 -0
  98. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw.egg-info/SOURCES.txt +0 -0
  99. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw.egg-info/dependency_links.txt +0 -0
  100. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw.egg-info/entry_points.txt +0 -0
  101. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/pythonclaw.egg-info/top_level.txt +0 -0
  102. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/setup.cfg +0 -0
  103. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/tests/test_compaction.py +0 -0
  104. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/tests/test_rag_hybrid.py +0 -0
  105. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/tests/test_skills.py +0 -0
  106. {pythonclaw-0.3.0 → pythonclaw-0.3.2}/tests/test_soul.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonclaw
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: OpenClaw reimagined in pure Python — autonomous AI agent with memory, RAG, skills, web dashboard, and multi-channel support.
5
5
  Author-email: Eric Wang <wangchen2007915@gmail.com>
6
6
  License: MIT
@@ -23,6 +23,8 @@ Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: openai
26
+ Requires-Dist: anthropic
27
+ Requires-Dist: google-generativeai
26
28
  Requires-Dist: python-telegram-bot[job-queue]>=20.0
27
29
  Requires-Dist: discord.py>=2.3
28
30
  Requires-Dist: apscheduler>=3.10
@@ -35,13 +37,6 @@ Requires-Dist: numpy>=1.24
35
37
  Requires-Dist: scikit-learn>=1.3
36
38
  Requires-Dist: beautifulsoup4
37
39
  Requires-Dist: requests
38
- Provides-Extra: all
39
- Requires-Dist: anthropic; extra == "all"
40
- Requires-Dist: google-generativeai; extra == "all"
41
- Provides-Extra: anthropic
42
- Requires-Dist: anthropic; extra == "anthropic"
43
- Provides-Extra: gemini
44
- Requires-Dist: google-generativeai; extra == "gemini"
45
40
  Dynamic: license-file
46
41
 
47
42
  <p align="center">
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythonclaw"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "OpenClaw reimagined in pure Python — autonomous AI agent with memory, RAG, skills, web dashboard, and multi-channel support."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -27,6 +27,8 @@ classifiers = [
27
27
  ]
28
28
  dependencies = [
29
29
  "openai",
30
+ "anthropic",
31
+ "google-generativeai",
30
32
  "python-telegram-bot[job-queue]>=20.0",
31
33
  "discord.py>=2.3",
32
34
  "apscheduler>=3.10",
@@ -41,14 +43,6 @@ dependencies = [
41
43
  "requests",
42
44
  ]
43
45
 
44
- [project.optional-dependencies]
45
- all = [
46
- "anthropic",
47
- "google-generativeai",
48
- ]
49
- anthropic = ["anthropic"]
50
- gemini = ["google-generativeai"]
51
-
52
46
  [project.scripts]
53
47
  pythonclaw = "pythonclaw.main:main"
54
48
 
@@ -6,7 +6,7 @@ from .core.llm.base import LLMProvider
6
6
  from .core.llm.openai_compatible import OpenAICompatibleProvider
7
7
  from .init import init
8
8
 
9
- __version__ = "0.3.0"
9
+ __version__ = "0.3.2"
10
10
  __all__ = [
11
11
  "Agent",
12
12
  "LLMProvider",
@@ -1,23 +1,17 @@
1
1
  """
2
2
  Centralised configuration for PythonClaw.
3
3
 
4
+ All runtime data lives under ``~/.pythonclaw/`` (the *home* directory):
5
+
6
+ ~/.pythonclaw/
7
+ pythonclaw.json ← config file
8
+ context/ ← sessions, logs, memory, skills, …
9
+ daemon.log ← daemon output
10
+ pythonclaw.pid ← daemon PID
11
+
4
12
  Load order (later sources override earlier ones):
5
- 1. pythonclaw.json in current working directory
6
- 2. ~/.pythonclaw/pythonclaw.json
7
- 3. Environment variables (highest priority)
8
-
9
- The JSON file supports // line comments and trailing commas for convenience
10
- (a subset of JSON5 that covers the most common needs).
11
-
12
- Usage
13
- -----
14
- from pythonclaw import config
15
-
16
- config.load() # call once at startup
17
- provider = config.get("llm", "provider", env="LLM_PROVIDER", default="deepseek")
18
- token = config.get("channels", "telegram", "token", env="TELEGRAM_BOT_TOKEN")
19
- users = config.get_int_list("channels", "telegram", "allowedUsers",
20
- env="TELEGRAM_ALLOWED_USERS")
13
+ 1. ~/.pythonclaw/pythonclaw.json
14
+ 2. Environment variables (highest priority)
21
15
  """
22
16
 
23
17
  from __future__ import annotations
@@ -28,12 +22,19 @@ import re
28
22
  from pathlib import Path
29
23
  from typing import Any
30
24
 
25
+ PYTHONCLAW_HOME = Path(os.environ.get("PYTHONCLAW_HOME", Path.home() / ".pythonclaw"))
26
+
31
27
  _TRAILING_COMMA_RE = re.compile(r",\s*([}\]])")
32
28
 
33
29
  _config: dict | None = None
34
30
  _config_path: Path | None = None
35
31
 
36
32
 
33
+ def home() -> Path:
34
+ """Return the PythonClaw home directory (``~/.pythonclaw`` by default)."""
35
+ return PYTHONCLAW_HOME
36
+
37
+
37
38
  def _strip_json5(text: str) -> str:
38
39
  """Strip // comments and trailing commas so standard json.loads works.
39
40
 
@@ -71,8 +72,8 @@ def _strip_json5(text: str) -> str:
71
72
 
72
73
  def _find_config_file() -> Path | None:
73
74
  candidates = [
75
+ PYTHONCLAW_HOME / "pythonclaw.json",
74
76
  Path.cwd() / "pythonclaw.json",
75
- Path.home() / ".pythonclaw" / "pythonclaw.json",
76
77
  ]
77
78
  for p in candidates:
78
79
  if p.is_file():
@@ -73,16 +73,22 @@ def _load_text_dir_or_file(path: str | None, label: str = "File") -> str:
73
73
  return ""
74
74
 
75
75
 
76
- _DETAIL_LOG_DIR = os.path.join("context", "logs")
77
- _DETAIL_LOG_FILE = os.path.join(_DETAIL_LOG_DIR, "history_detail.jsonl")
76
+ def _detail_log_dir() -> str:
77
+ from .. import config as _cfg
78
+ return os.path.join(str(_cfg.PYTHONCLAW_HOME), "context", "logs")
79
+
80
+
81
+ def _detail_log_file() -> str:
82
+ return os.path.join(_detail_log_dir(), "history_detail.jsonl")
78
83
 
79
84
 
80
85
  def _log_detail(entry: dict) -> None:
81
86
  """Append a JSON line to the detailed interaction log."""
82
87
  try:
83
- os.makedirs(_DETAIL_LOG_DIR, exist_ok=True)
88
+ log_dir = _detail_log_dir()
89
+ os.makedirs(log_dir, exist_ok=True)
84
90
  entry["ts"] = datetime.now().isoformat(timespec="milliseconds")
85
- with open(_DETAIL_LOG_FILE, "a", encoding="utf-8") as f:
91
+ with open(_detail_log_file(), "a", encoding="utf-8") as f:
86
92
  f.write(json.dumps(entry, ensure_ascii=False) + "\n")
87
93
  except OSError:
88
94
  pass
@@ -129,18 +135,19 @@ class Agent:
129
135
  cron_manager=None,
130
136
  ) -> None:
131
137
  if memory_dir is None and skills_dirs is None and knowledge_path is None and persona_path is None:
132
- cwd = os.getcwd()
133
- context_dir = os.path.join(cwd, "context")
138
+ from .. import config as _cfg
139
+ home = str(_cfg.PYTHONCLAW_HOME)
140
+ context_dir = os.path.join(home, "context")
134
141
  if not os.path.exists(context_dir):
135
142
  if verbose:
136
143
  print(f"[Agent] Context not found. Initialising default context in {context_dir}...")
137
144
  try:
138
- from ...init import init
139
- init(cwd)
145
+ from ..init import init
146
+ init(home)
140
147
  except ImportError:
141
148
  try:
142
149
  from pythonclaw.init import init
143
- init(cwd)
150
+ init(home)
144
151
  except ImportError:
145
152
  print("[Agent] Warning: Could not auto-initialise context.")
146
153
  if verbose:
@@ -152,9 +159,9 @@ class Agent:
152
159
  if soul_path is None:
153
160
  soul_path = os.path.join(context_dir, "soul")
154
161
 
155
- # Sandbox: restrict file-write tools to the project working tree
156
- sandbox_root = os.getcwd()
157
- set_sandbox([sandbox_root])
162
+ # Sandbox: restrict file-write tools to the home directory
163
+ sandbox_root = str(_cfg.PYTHONCLAW_HOME) if '_cfg' in dir() else os.getcwd()
164
+ set_sandbox([sandbox_root, os.path.expanduser("~")])
158
165
  if verbose:
159
166
  print(f"[Agent] Sandbox root: {sandbox_root}")
160
167
 
@@ -45,7 +45,12 @@ logger = logging.getLogger(__name__)
45
45
  CHARS_PER_TOKEN = 4
46
46
  DEFAULT_AUTO_THRESHOLD_TOKENS = 6000 # trigger auto-compaction at ~6k tokens
47
47
  DEFAULT_RECENT_KEEP = 6 # keep last N chat messages verbatim
48
- COMPACTION_LOG_FILE = os.path.join("context", "compaction", "history.jsonl")
48
+ def _compaction_log_file() -> str:
49
+ from .. import config as _cfg
50
+ return os.path.join(str(_cfg.PYTHONCLAW_HOME), "context", "compaction", "history.jsonl")
51
+
52
+
53
+ COMPACTION_LOG_FILE = None # resolved lazily
49
54
 
50
55
 
51
56
  # ── Token estimation ──────────────────────────────────────────────────────────
@@ -58,8 +63,9 @@ def estimate_tokens(messages: list[dict]) -> int:
58
63
 
59
64
  # ── JSONL persistence ─────────────────────────────────────────────────────────
60
65
 
61
- def persist_compaction(summary: str, message_count: int, log_path: str = COMPACTION_LOG_FILE) -> None:
66
+ def persist_compaction(summary: str, message_count: int, log_path: str | None = None) -> None:
62
67
  """Append one compaction entry to the JSONL audit log."""
68
+ log_path = log_path or _compaction_log_file()
63
69
  os.makedirs(os.path.dirname(log_path), exist_ok=True)
64
70
  entry = {
65
71
  "ts": datetime.now(timezone.utc).isoformat(),
@@ -150,7 +156,7 @@ def compact(
150
156
  memory: "MemoryManager | None" = None,
151
157
  recent_keep: int = DEFAULT_RECENT_KEEP,
152
158
  instruction: str | None = None,
153
- log_path: str = COMPACTION_LOG_FILE,
159
+ log_path: str | None = None,
154
160
  ) -> tuple[list[dict], str]:
155
161
  """
156
162
  Compact conversation history.
@@ -36,7 +36,10 @@ _UPDATED_LINE = re.compile(r"^> Updated: (.+)$", re.MULTILINE)
36
36
  class MemoryStorage:
37
37
  """Markdown-backed key-value memory with daily logs."""
38
38
 
39
- def __init__(self, memory_dir: str = "context/memory") -> None:
39
+ def __init__(self, memory_dir: str | None = None) -> None:
40
+ if memory_dir is None:
41
+ from ... import config as _cfg
42
+ memory_dir = os.path.join(str(_cfg.PYTHONCLAW_HOME), "context", "memory")
40
43
  self.memory_dir = memory_dir
41
44
  os.makedirs(memory_dir, exist_ok=True)
42
45
  self._memory_file = os.path.join(memory_dir, "MEMORY.md")
@@ -37,7 +37,12 @@ from datetime import datetime
37
37
 
38
38
  logger = logging.getLogger(__name__)
39
39
 
40
- DEFAULT_STORE_DIR = os.path.join("context", "sessions")
40
+ def _default_store_dir() -> str:
41
+ from .. import config as _cfg
42
+ return os.path.join(str(_cfg.PYTHONCLAW_HOME), "context", "sessions")
43
+
44
+
45
+ DEFAULT_STORE_DIR = None # resolved lazily
41
46
  DEFAULT_MAX_MESSAGES = 200
42
47
 
43
48
  _META_PATTERN = re.compile(r"<!-- msg:(.*?) -->")
@@ -54,10 +59,10 @@ class SessionStore:
54
59
 
55
60
  def __init__(
56
61
  self,
57
- base_dir: str = DEFAULT_STORE_DIR,
62
+ base_dir: str | None = None,
58
63
  max_messages: int = DEFAULT_MAX_MESSAGES,
59
64
  ) -> None:
60
- self.base_dir = base_dir
65
+ self.base_dir = base_dir or _default_store_dir()
61
66
  self.max_messages = max_messages
62
67
  os.makedirs(base_dir, exist_ok=True)
63
68
 
@@ -213,7 +213,8 @@ def install_skill(
213
213
  Returns the path to the installed skill directory.
214
214
  """
215
215
  if target_dir is None:
216
- target_dir = os.path.join("context", "skills")
216
+ from .. import config as _cfg
217
+ target_dir = os.path.join(str(_cfg.PYTHONCLAW_HOME), "context", "skills")
217
218
 
218
219
  detail = None
219
220
  if not skill_md_override:
@@ -460,7 +460,8 @@ def create_skill(
460
460
  All paths are validated against the sandbox. Resource filenames are
461
461
  sanitized to prevent directory traversal.
462
462
  """
463
- skills_dir = os.path.join("context", "skills")
463
+ from .. import config as _cfg
464
+ skills_dir = os.path.join(str(_cfg.PYTHONCLAW_HOME), "context", "skills")
464
465
  _resolve_in_sandbox(skills_dir)
465
466
  os.makedirs(skills_dir, exist_ok=True)
466
467
 
@@ -62,12 +62,15 @@ def start_daemon(
62
62
  log_handle.write(f"{'='*60}\n")
63
63
  log_handle.flush()
64
64
 
65
+ home = str(config.PYTHONCLAW_HOME)
66
+ os.makedirs(home, exist_ok=True)
67
+
65
68
  proc = subprocess.Popen(
66
69
  cmd,
67
70
  stdout=log_handle,
68
71
  stderr=subprocess.STDOUT,
69
72
  start_new_session=True,
70
- cwd=os.getcwd(),
73
+ cwd=home,
71
74
  )
72
75
 
73
76
  _write_pid(proc.pid)
@@ -76,7 +79,7 @@ def start_daemon(
76
79
  _write_meta({
77
80
  "pid": proc.pid,
78
81
  "port": port,
79
- "cwd": os.getcwd(),
82
+ "cwd": home,
80
83
  "started_at": time.strftime("%Y-%m-%d %H:%M:%S"),
81
84
  "channels": channels or [],
82
85
  "config_path": config_path,
@@ -19,14 +19,19 @@ import os
19
19
  import shutil
20
20
 
21
21
 
22
- def init(project_path: str = ".") -> None:
22
+ def init(project_path: str | None = None) -> None:
23
23
  """
24
- Initialise a new PythonClaw project at *project_path*.
24
+ Initialise a new PythonClaw project.
25
25
 
26
- Copies template files into `<project_path>/context/` only for
26
+ Copies template files into ``<project_path>/context/`` only for
27
27
  directories that do not already exist (safe to re-run).
28
+ Defaults to ``~/.pythonclaw``.
28
29
  """
29
- application_dir = os.path.abspath(project_path)
30
+ if project_path is None:
31
+ from . import config
32
+ application_dir = str(config.PYTHONCLAW_HOME)
33
+ else:
34
+ application_dir = os.path.abspath(project_path)
30
35
  context_dir = os.path.join(application_dir, "context")
31
36
  pkg_dir = os.path.dirname(os.path.abspath(__file__))
32
37
  templates_dir = os.path.join(pkg_dir, "templates")
@@ -300,11 +300,12 @@ def _validate_key(cfg: dict, provider: dict) -> None:
300
300
 
301
301
 
302
302
  def _save_config(cfg: dict, config_path: str | None) -> Path:
303
- """Write config to disk."""
303
+ """Write config to disk (defaults to ~/.pythonclaw/pythonclaw.json)."""
304
304
  if config_path:
305
305
  out = Path(config_path)
306
306
  else:
307
- out = Path.cwd() / "pythonclaw.json"
307
+ out = config.PYTHONCLAW_HOME / "pythonclaw.json"
308
+ out.parent.mkdir(parents=True, exist_ok=True)
308
309
 
309
310
  # Ensure default sections exist
310
311
  cfg.setdefault("channels", {"telegram": {"token": "", "allowedUsers": []}, "discord": {"token": "", "allowedUsers": [], "allowedChannels": []}})
@@ -52,9 +52,17 @@ if TYPE_CHECKING:
52
52
 
53
53
  logger = logging.getLogger(__name__)
54
54
 
55
- DYNAMIC_JOBS_FILE = os.path.join("context", "cron", "dynamic_jobs.json")
55
+ def _cron_dir() -> str:
56
+ from .. import config as _cfg
57
+ return os.path.join(str(_cfg.PYTHONCLAW_HOME), "context", "cron")
56
58
 
57
- DEFAULT_JOBS_PATH = os.path.join("context", "cron", "jobs.yaml")
59
+
60
+ def _dynamic_jobs_file() -> str:
61
+ return os.path.join(_cron_dir(), "dynamic_jobs.json")
62
+
63
+
64
+ def _default_jobs_path() -> str:
65
+ return os.path.join(_cron_dir(), "jobs.yaml")
58
66
 
59
67
 
60
68
  class CronScheduler:
@@ -68,11 +76,11 @@ class CronScheduler:
68
76
  def __init__(
69
77
  self,
70
78
  session_manager: "SessionManager",
71
- jobs_path: str = DEFAULT_JOBS_PATH,
79
+ jobs_path: str | None = None,
72
80
  telegram_bot: "TelegramBot | None" = None,
73
81
  ) -> None:
74
82
  self._sm = session_manager
75
- self._jobs_path = jobs_path
83
+ self._jobs_path = jobs_path or _default_jobs_path()
76
84
  self._telegram_bot = telegram_bot
77
85
  self._scheduler = AsyncIOScheduler()
78
86
 
@@ -186,18 +194,20 @@ class CronScheduler:
186
194
 
187
195
  def _load_dynamic_jobs(self) -> dict[str, dict]:
188
196
  """Load persisted dynamic jobs from JSON. Returns {job_id: job_dict}."""
189
- if not os.path.exists(DYNAMIC_JOBS_FILE):
197
+ djf = _dynamic_jobs_file()
198
+ if not os.path.exists(djf):
190
199
  return {}
191
200
  try:
192
- with open(DYNAMIC_JOBS_FILE, "r", encoding="utf-8") as f:
201
+ with open(djf, "r", encoding="utf-8") as f:
193
202
  return json.load(f)
194
203
  except (OSError, json.JSONDecodeError) as exc:
195
204
  logger.error("[CronScheduler] Failed to load dynamic jobs: %s", exc)
196
205
  return {}
197
206
 
198
207
  def _save_dynamic_jobs(self, jobs: dict[str, dict]) -> None:
199
- os.makedirs(os.path.dirname(DYNAMIC_JOBS_FILE), exist_ok=True)
200
- with open(DYNAMIC_JOBS_FILE, "w", encoding="utf-8") as f:
208
+ djf = _dynamic_jobs_file()
209
+ os.makedirs(os.path.dirname(djf), exist_ok=True)
210
+ with open(djf, "w", encoding="utf-8") as f:
201
211
  json.dump(jobs, f, indent=2, ensure_ascii=False)
202
212
 
203
213
  def _register_dynamic_jobs(self) -> int:
@@ -33,8 +33,13 @@ if TYPE_CHECKING:
33
33
  logger = logging.getLogger(__name__)
34
34
 
35
35
  DEFAULT_INTERVAL = 60
36
- LOG_DIR = os.path.join("context", "logs")
37
- LOG_FILE = os.path.join(LOG_DIR, "heartbeat.log")
36
+ def _log_dir() -> str:
37
+ from .. import config as _cfg
38
+ return os.path.join(str(_cfg.PYTHONCLAW_HOME), "context", "logs")
39
+
40
+
41
+ def _log_file() -> str:
42
+ return os.path.join(_log_dir(), "heartbeat.log")
38
43
 
39
44
  # Minimal probe message sent to the LLM
40
45
  _PROBE_MESSAGES = [{"role": "user", "content": "ping"}]
@@ -49,12 +54,17 @@ class HeartbeatMonitor:
49
54
  interval_sec: int = DEFAULT_INTERVAL,
50
55
  telegram_bot: "TelegramBot | None" = None,
51
56
  alert_chat_id: int | None = None,
52
- log_path: str = LOG_FILE,
57
+ log_path: str | None = None,
53
58
  ) -> None:
54
59
  self._provider = provider
55
60
  self._interval = interval_sec
56
61
  self._telegram_bot = telegram_bot
57
62
  self._alert_chat_id = alert_chat_id
63
+ if log_path is None:
64
+ try:
65
+ log_path = _log_file()
66
+ except Exception:
67
+ log_path = os.path.join(os.path.expanduser("~/.pythonclaw"), "context", "logs", "heartbeat.log")
58
68
  self._log_path = log_path
59
69
  self._running = False
60
70
  self._task: asyncio.Task | None = None
@@ -9,7 +9,6 @@ from __future__ import annotations
9
9
 
10
10
  import asyncio
11
11
  import logging
12
- import os
13
12
  import signal
14
13
 
15
14
  from .core.llm.base import LLMProvider
@@ -33,10 +32,8 @@ async def start_channels(
33
32
  store = SessionStore()
34
33
  session_manager = SessionManager(agent_factory=lambda sid: None, store=store)
35
34
 
36
- jobs_path = os.path.join("context", "cron", "jobs.yaml")
37
35
  scheduler = CronScheduler(
38
36
  session_manager=session_manager,
39
- jobs_path=jobs_path,
40
37
  )
41
38
 
42
39
  def agent_factory(session_id: str) -> PersistentAgent:
@@ -7,7 +7,13 @@ import os
7
7
  import re
8
8
  import sys
9
9
 
10
- CONFIG_FILE = "pythonclaw.json"
10
+ def _config_file() -> str:
11
+ home = os.path.expanduser("~/.pythonclaw")
12
+ for p in [os.path.join(home, "pythonclaw.json"), "pythonclaw.json"]:
13
+ if os.path.exists(p):
14
+ return p
15
+ return os.path.join(home, "pythonclaw.json")
16
+
11
17
 
12
18
  SENSITIVE_PATTERNS = re.compile(
13
19
  r"(apikey|api_key|token|password|secret)", re.IGNORECASE
@@ -15,15 +21,16 @@ SENSITIVE_PATTERNS = re.compile(
15
21
 
16
22
 
17
23
  def _load_config() -> dict:
18
- if not os.path.exists(CONFIG_FILE):
19
- print(f"Error: {CONFIG_FILE} not found in {os.getcwd()}", file=sys.stderr)
24
+ cfg_file = _config_file()
25
+ if not os.path.exists(cfg_file):
26
+ print(f"Error: {cfg_file} not found", file=sys.stderr)
20
27
  sys.exit(1)
21
- with open(CONFIG_FILE, "r", encoding="utf-8") as f:
28
+ with open(cfg_file, "r", encoding="utf-8") as f:
22
29
  return json.load(f)
23
30
 
24
31
 
25
32
  def _save_config(cfg: dict) -> None:
26
- with open(CONFIG_FILE, "w", encoding="utf-8") as f:
33
+ with open(_config_file(), "w", encoding="utf-8") as f:
27
34
  json.dump(cfg, f, indent=2, ensure_ascii=False)
28
35
  f.write("\n")
29
36
 
@@ -174,7 +174,8 @@ def write_soul(user_name: str, personality: str, focus: str, language: str) -> s
174
174
  language=language,
175
175
  personality_description=_personality_description(personality),
176
176
  )
177
- path = os.path.join("context", "soul", "SOUL.md")
177
+ home = os.path.expanduser("~/.pythonclaw")
178
+ path = os.path.join(home, "context", "soul", "SOUL.md")
178
179
  os.makedirs(os.path.dirname(path), exist_ok=True)
179
180
  with open(path, "w", encoding="utf-8") as f:
180
181
  f.write(content.strip() + "\n")
@@ -190,7 +191,8 @@ def write_persona(user_name: str, personality: str, focus: str, language: str) -
190
191
  style_notes=_style_notes(personality),
191
192
  focus_guidelines=_focus_guidelines(focus),
192
193
  )
193
- path = os.path.join("context", "persona", "persona.md")
194
+ home = os.path.expanduser("~/.pythonclaw")
195
+ path = os.path.join(home, "context", "persona", "persona.md")
194
196
  os.makedirs(os.path.dirname(path), exist_ok=True)
195
197
  with open(path, "w", encoding="utf-8") as f:
196
198
  f.write(content.strip() + "\n")
@@ -209,7 +209,7 @@ async def _api_config_save(request: Request):
209
209
 
210
210
  cfg_path = config.config_path()
211
211
  if cfg_path is None:
212
- cfg_path = Path.cwd() / "pythonclaw.json"
212
+ cfg_path = config.PYTHONCLAW_HOME / "pythonclaw.json"
213
213
 
214
214
  try:
215
215
  json_text = json.dumps(new_config, indent=2, ensure_ascii=False)
@@ -247,7 +247,7 @@ async def _api_skills():
247
247
  os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
248
248
  "templates", "skills",
249
249
  )
250
- skills_dirs = [pkg_templates, os.path.join("context", "skills")]
250
+ skills_dirs = [pkg_templates, os.path.join(str(config.PYTHONCLAW_HOME), "context", "skills")]
251
251
  skills_dirs = [d for d in skills_dirs if os.path.isdir(d)]
252
252
  registry = SkillRegistry(skills_dirs=skills_dirs)
253
253
  skills_meta = registry.discover()
@@ -331,9 +331,9 @@ async def _api_identity():
331
331
  return f.read_text(encoding="utf-8").strip()
332
332
  return None
333
333
 
334
- cwd = Path.cwd()
335
- soul = _read_md(str(cwd / "context" / "soul"))
336
- persona = _read_md(str(cwd / "context" / "persona"))
334
+ home = config.PYTHONCLAW_HOME
335
+ soul = _read_md(str(home / "context" / "soul"))
336
+ persona = _read_md(str(home / "context" / "persona"))
337
337
 
338
338
  def _tool_info(schema: dict) -> dict:
339
339
  fn = schema.get("function", {})
@@ -373,7 +373,7 @@ async def _api_save_soul(request: Request):
373
373
  if not content:
374
374
  return JSONResponse({"ok": False, "error": "Content cannot be empty."}, status_code=400)
375
375
 
376
- soul_dir = Path.cwd() / "context" / "soul"
376
+ soul_dir = config.PYTHONCLAW_HOME / "context" / "soul"
377
377
  soul_dir.mkdir(parents=True, exist_ok=True)
378
378
  soul_file = soul_dir / "SOUL.md"
379
379
  soul_file.write_text(content + "\n", encoding="utf-8")
@@ -393,7 +393,7 @@ async def _api_save_persona(request: Request):
393
393
  if not content:
394
394
  return JSONResponse({"ok": False, "error": "Content cannot be empty."}, status_code=400)
395
395
 
396
- persona_dir = Path.cwd() / "context" / "persona"
396
+ persona_dir = config.PYTHONCLAW_HOME / "context" / "persona"
397
397
  persona_dir.mkdir(parents=True, exist_ok=True)
398
398
  persona_file = persona_dir / "persona.md"
399
399
  persona_file.write_text(content + "\n", encoding="utf-8")
@@ -598,12 +598,12 @@ def _reload_agent_identity() -> None:
598
598
  if _agent is None:
599
599
  return
600
600
  from ..core.agent import _load_text_dir_or_file
601
- cwd = Path.cwd()
601
+ home = config.PYTHONCLAW_HOME
602
602
  _agent.soul_instruction = _load_text_dir_or_file(
603
- str(cwd / "context" / "soul"), label="Soul"
603
+ str(home / "context" / "soul"), label="Soul"
604
604
  )
605
605
  _agent.persona_instruction = _load_text_dir_or_file(
606
- str(cwd / "context" / "persona"), label="Persona"
606
+ str(home / "context" / "persona"), label="Persona"
607
607
  )
608
608
  _agent._needs_onboarding = False
609
609
  _agent._init_system_prompt()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonclaw
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: OpenClaw reimagined in pure Python — autonomous AI agent with memory, RAG, skills, web dashboard, and multi-channel support.
5
5
  Author-email: Eric Wang <wangchen2007915@gmail.com>
6
6
  License: MIT
@@ -23,6 +23,8 @@ Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: openai
26
+ Requires-Dist: anthropic
27
+ Requires-Dist: google-generativeai
26
28
  Requires-Dist: python-telegram-bot[job-queue]>=20.0
27
29
  Requires-Dist: discord.py>=2.3
28
30
  Requires-Dist: apscheduler>=3.10
@@ -35,13 +37,6 @@ Requires-Dist: numpy>=1.24
35
37
  Requires-Dist: scikit-learn>=1.3
36
38
  Requires-Dist: beautifulsoup4
37
39
  Requires-Dist: requests
38
- Provides-Extra: all
39
- Requires-Dist: anthropic; extra == "all"
40
- Requires-Dist: google-generativeai; extra == "all"
41
- Provides-Extra: anthropic
42
- Requires-Dist: anthropic; extra == "anthropic"
43
- Provides-Extra: gemini
44
- Requires-Dist: google-generativeai; extra == "gemini"
45
40
  Dynamic: license-file
46
41
 
47
42
  <p align="center">
@@ -1,4 +1,6 @@
1
1
  openai
2
+ anthropic
3
+ google-generativeai
2
4
  python-telegram-bot[job-queue]>=20.0
3
5
  discord.py>=2.3
4
6
  apscheduler>=3.10
@@ -11,13 +13,3 @@ numpy>=1.24
11
13
  scikit-learn>=1.3
12
14
  beautifulsoup4
13
15
  requests
14
-
15
- [all]
16
- anthropic
17
- google-generativeai
18
-
19
- [anthropic]
20
- anthropic
21
-
22
- [gemini]
23
- google-generativeai
@@ -219,7 +219,7 @@ class TestAgentCronTools:
219
219
  assert "scheduled" in result.lower()
220
220
 
221
221
  def test_cron_add_persisted_to_json(self, tmp_path):
222
- from pythonclaw.scheduler.cron import DYNAMIC_JOBS_FILE
222
+ from pythonclaw.scheduler.cron import _dynamic_jobs_file as _djf; DYNAMIC_JOBS_FILE = _djf()
223
223
  cron_mgr = self._make_cron_manager(tmp_path)
224
224
  cron_mgr.add_dynamic_job("persist_job", "0 8 * * *", "Daily check")
225
225
  assert os.path.exists(DYNAMIC_JOBS_FILE)
@@ -235,7 +235,7 @@ class TestAgentCronTools:
235
235
  assert "removed" in result.lower()
236
236
 
237
237
  # Should no longer be in JSON
238
- from pythonclaw.scheduler.cron import DYNAMIC_JOBS_FILE
238
+ from pythonclaw.scheduler.cron import _dynamic_jobs_file as _djf; DYNAMIC_JOBS_FILE = _djf()
239
239
  with open(DYNAMIC_JOBS_FILE) as f:
240
240
  data = json.load(f)
241
241
  assert "rm_job" not in data
@@ -269,7 +269,7 @@ class TestAgentCronTools:
269
269
  c._scheduler.get_jobs.return_value = [mock_job]
270
270
 
271
271
  # Fake the dynamic_jobs.json
272
- from pythonclaw.scheduler.cron import DYNAMIC_JOBS_FILE
272
+ from pythonclaw.scheduler.cron import _dynamic_jobs_file as _djf; DYNAMIC_JOBS_FILE = _djf()
273
273
  os.makedirs(os.path.dirname(DYNAMIC_JOBS_FILE), exist_ok=True)
274
274
  with open(DYNAMIC_JOBS_FILE, "w") as f:
275
275
  json.dump({"my_dynamic_job": {"cron": "0 9 * * *", "prompt": "hi"}}, f)
@@ -279,7 +279,7 @@ class TestAgentCronTools:
279
279
  assert "[dynamic]" in result
280
280
 
281
281
  def test_load_dynamic_jobs_from_json(self, tmp_path):
282
- from pythonclaw.scheduler.cron import DYNAMIC_JOBS_FILE
282
+ from pythonclaw.scheduler.cron import _dynamic_jobs_file as _djf; DYNAMIC_JOBS_FILE = _djf()
283
283
  os.makedirs(os.path.dirname(DYNAMIC_JOBS_FILE), exist_ok=True)
284
284
  with open(DYNAMIC_JOBS_FILE, "w") as f:
285
285
  json.dump({
@@ -292,6 +292,6 @@ class TestAgentCronTools:
292
292
  assert jobs["pre_existing"]["cron"] == "0 7 * * *"
293
293
 
294
294
  def teardown_method(self, method):
295
- from pythonclaw.scheduler.cron import DYNAMIC_JOBS_FILE
295
+ from pythonclaw.scheduler.cron import _dynamic_jobs_file as _djf; DYNAMIC_JOBS_FILE = _djf()
296
296
  if os.path.exists(DYNAMIC_JOBS_FILE):
297
297
  os.remove(DYNAMIC_JOBS_FILE)
File without changes
File without changes
File without changes
File without changes