arkaos 2.9.0 → 2.10.0

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 (40) hide show
  1. package/VERSION +1 -1
  2. package/config/cognition/prompts/dreaming.md +208 -0
  3. package/config/cognition/prompts/research.md +194 -0
  4. package/config/cognition/schedules.yaml +25 -0
  5. package/core/cognition/__init__.py +7 -0
  6. package/core/cognition/__pycache__/__init__.cpython-313.pyc +0 -0
  7. package/core/cognition/capture/__init__.py +5 -0
  8. package/core/cognition/capture/__pycache__/__init__.cpython-313.pyc +0 -0
  9. package/core/cognition/capture/__pycache__/collector.cpython-313.pyc +0 -0
  10. package/core/cognition/capture/__pycache__/store.cpython-313.pyc +0 -0
  11. package/core/cognition/capture/collector.py +80 -0
  12. package/core/cognition/capture/store.py +158 -0
  13. package/core/cognition/insights/__init__.py +5 -0
  14. package/core/cognition/insights/__pycache__/__init__.cpython-313.pyc +0 -0
  15. package/core/cognition/insights/__pycache__/store.cpython-313.pyc +0 -0
  16. package/core/cognition/insights/store.py +155 -0
  17. package/core/cognition/memory/__init__.py +9 -0
  18. package/core/cognition/memory/__pycache__/__init__.cpython-313.pyc +0 -0
  19. package/core/cognition/memory/__pycache__/obsidian.cpython-313.pyc +0 -0
  20. package/core/cognition/memory/__pycache__/schemas.cpython-313.pyc +0 -0
  21. package/core/cognition/memory/__pycache__/vector.cpython-313.pyc +0 -0
  22. package/core/cognition/memory/__pycache__/writer.cpython-313.pyc +0 -0
  23. package/core/cognition/memory/obsidian.py +73 -0
  24. package/core/cognition/memory/schemas.py +141 -0
  25. package/core/cognition/memory/vector.py +223 -0
  26. package/core/cognition/memory/writer.py +57 -0
  27. package/core/cognition/research/__init__.py +5 -0
  28. package/core/cognition/research/__pycache__/__init__.cpython-313.pyc +0 -0
  29. package/core/cognition/research/__pycache__/profiler.cpython-313.pyc +0 -0
  30. package/core/cognition/research/profiler.py +256 -0
  31. package/core/cognition/scheduler/__init__.py +5 -0
  32. package/core/cognition/scheduler/__pycache__/__init__.cpython-313.pyc +0 -0
  33. package/core/cognition/scheduler/__pycache__/cli.cpython-313.pyc +0 -0
  34. package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
  35. package/core/cognition/scheduler/__pycache__/platform.cpython-313.pyc +0 -0
  36. package/core/cognition/scheduler/cli.py +86 -0
  37. package/core/cognition/scheduler/daemon.py +172 -0
  38. package/core/cognition/scheduler/platform.py +292 -0
  39. package/package.json +1 -1
  40. package/pyproject.toml +1 -1
@@ -0,0 +1,256 @@
1
+ """ResearchProfiler — infers adaptive research topics from project ecosystems.
2
+
3
+ Reads ecosystems.json, extracts stacks and domains, and maps them to curated
4
+ research topics for automated content monitoring and trend awareness.
5
+ """
6
+
7
+ import json
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+
11
+ import yaml
12
+
13
+
14
+ # --- Topic mapping dictionaries ---
15
+
16
+ STACK_TOPICS: dict[str, list[str]] = {
17
+ "laravel": [
18
+ "Laravel releases and security patches",
19
+ "Laravel ecosystem packages",
20
+ "PHP security advisories",
21
+ ],
22
+ "nuxt": [
23
+ "Nuxt 3/4 releases and migration guides",
24
+ "Vue 3 ecosystem updates",
25
+ ],
26
+ "vue": [
27
+ "Vue 3 composition API patterns",
28
+ "Vue ecosystem tooling updates",
29
+ ],
30
+ "react": [
31
+ "React releases and concurrent features",
32
+ "Next.js app router updates",
33
+ "React ecosystem library news",
34
+ ],
35
+ "python": [
36
+ "Python release notes and deprecations",
37
+ "FastAPI and async Python patterns",
38
+ "Python security CVEs",
39
+ ],
40
+ "shopify-liquid": [
41
+ "Shopify platform updates and API changes",
42
+ "Shopify theme development best practices",
43
+ "Shopify app ecosystem news",
44
+ ],
45
+ "ai": [
46
+ "Large language model releases and benchmarks",
47
+ "AI agent framework updates",
48
+ "Anthropic and OpenAI product announcements",
49
+ ],
50
+ "energy": [
51
+ "Energy sector digital transformation trends",
52
+ "API governance in regulated industries",
53
+ "Enterprise integration patterns for utilities",
54
+ ],
55
+ }
56
+
57
+ DOMAIN_TOPICS: dict[str, list[str]] = {
58
+ "ecommerce": [
59
+ "E-commerce conversion rate trends",
60
+ "Marketplace integration updates",
61
+ "Payment gateway changes and PSD2 compliance",
62
+ ],
63
+ "media": [
64
+ "Streaming technology updates",
65
+ "Content delivery network trends",
66
+ "Viral content mechanics and audience growth",
67
+ ],
68
+ "enterprise": [
69
+ "Enterprise architecture and API governance",
70
+ "System integration patterns",
71
+ "Cloud migration strategies",
72
+ ],
73
+ "saas": [
74
+ "SaaS pricing model trends",
75
+ "Product-led growth tactics",
76
+ "Micro-SaaS opportunity scouting",
77
+ ],
78
+ "news": [
79
+ "CMS architecture and headless content",
80
+ "SEO for news portals and Google News",
81
+ "Security hardening for content platforms",
82
+ ],
83
+ "events": [
84
+ "Event platform technology stack trends",
85
+ "Ticketing and registration UX patterns",
86
+ "Landing page conversion for events",
87
+ ],
88
+ "consulting": [
89
+ "Web application architecture trends",
90
+ "Client delivery workflow optimisation",
91
+ "TypeScript and Nuxt ecosystem updates",
92
+ ],
93
+ }
94
+
95
+ # Keywords used to infer domain from ecosystem description and type
96
+ _DOMAIN_SIGNALS: dict[str, list[str]] = {
97
+ "ecommerce": ["e-commerce", "ecommerce", "marketplace", "shopify", " erp ", "supplier"],
98
+ "media": ["media", "content", "viral", "streaming", "news portal", "quiz", "audience"],
99
+ "enterprise": ["enterprise integration", "api governance", "energy utility", "soap ", "kafka"],
100
+ "saas": ["micro-saas", "saas", "ai tools", "growth engine", "revenue target"],
101
+ "news": ["news", "portal", "cms", "journalism"],
102
+ "events": ["event", "conference", "landing", "registration"],
103
+ "consulting": ["consulting", "services"],
104
+ }
105
+
106
+
107
+ @dataclass
108
+ class ResearchTopic:
109
+ """A single research topic derived from a stack or domain."""
110
+
111
+ name: str
112
+ source: str # "stack" | "domain" | "tool" | "business"
113
+ search_queries: list[str] = field(default_factory=list)
114
+
115
+
116
+ @dataclass
117
+ class ResearchProfile:
118
+ """Aggregated research profile for all active project ecosystems."""
119
+
120
+ stacks: list[str]
121
+ domains: list[str]
122
+ tools: list[str]
123
+ business_interests: list[str]
124
+ competitors: list[str]
125
+ topics: list[ResearchTopic]
126
+
127
+ def to_yaml(self) -> str:
128
+ """Serialise the profile to a YAML string."""
129
+ data = {
130
+ "stacks": self.stacks,
131
+ "domains": self.domains,
132
+ "tools": self.tools,
133
+ "business_interests": self.business_interests,
134
+ "competitors": self.competitors,
135
+ "topics": [
136
+ {
137
+ "name": t.name,
138
+ "source": t.source,
139
+ "search_queries": t.search_queries,
140
+ }
141
+ for t in self.topics
142
+ ],
143
+ }
144
+ return yaml.dump(data, default_flow_style=False, allow_unicode=True)
145
+
146
+
147
+ def _normalise_stack_name(raw: str) -> str:
148
+ """Return a canonical stack key from a raw tech string."""
149
+ lower = raw.lower().strip()
150
+ if lower.startswith("laravel"):
151
+ return "laravel"
152
+ if lower.startswith("nuxt"):
153
+ return "nuxt"
154
+ if lower.startswith("vue"):
155
+ return "vue"
156
+ if lower.startswith("react") or lower.startswith("next"):
157
+ return "react"
158
+ if lower.startswith("python") or lower.startswith("fastapi") or lower.startswith("pydantic"):
159
+ return "python"
160
+ if "shopify" in lower:
161
+ return "shopify-liquid"
162
+ if "openai" in lower or "anthropic" in lower or "claude" in lower or "gpt" in lower:
163
+ return "ai"
164
+ return ""
165
+
166
+
167
+ def _extract_stacks_from_ecosystem(eco: dict) -> list[str]:
168
+ """Extract canonical stack names from an ecosystem's tech_stack dict."""
169
+ tech_stack = eco.get("tech_stack", {})
170
+ found: set[str] = set()
171
+ for _category, entries in tech_stack.items():
172
+ if not isinstance(entries, list):
173
+ continue
174
+ for entry in entries:
175
+ key = _normalise_stack_name(str(entry))
176
+ if key:
177
+ found.add(key)
178
+ return list(found)
179
+
180
+
181
+ def _infer_domain(eco: dict) -> str:
182
+ """Infer a domain label from ecosystem description, capabilities, and type."""
183
+ text = " ".join([
184
+ eco.get("description", ""),
185
+ eco.get("name", ""),
186
+ str(eco.get("capabilities", "")),
187
+ ]).lower()
188
+
189
+ for domain, signals in _DOMAIN_SIGNALS.items():
190
+ if any(sig in text for sig in signals):
191
+ return domain
192
+ return "consulting"
193
+
194
+
195
+ class ResearchProfiler:
196
+ """Builds a ResearchProfile by inspecting the ArkaOS ecosystems registry."""
197
+
198
+ def __init__(self, ecosystems_path: str) -> None:
199
+ """Load the ecosystems registry from disk."""
200
+ self._path = Path(ecosystems_path)
201
+
202
+ def build_profile(self) -> ResearchProfile:
203
+ """Parse ecosystems.json and produce a unified ResearchProfile."""
204
+ raw = self._load()
205
+ ecosystems = raw.get("ecosystems", {})
206
+
207
+ stacks: set[str] = set()
208
+ domains: set[str] = set()
209
+
210
+ for eco in ecosystems.values():
211
+ stacks.update(_extract_stacks_from_ecosystem(eco))
212
+ domains.add(_infer_domain(eco))
213
+
214
+ topics = self._generate_topics(stacks, domains)
215
+
216
+ return ResearchProfile(
217
+ stacks=sorted(stacks),
218
+ domains=sorted(domains),
219
+ tools=[],
220
+ business_interests=[],
221
+ competitors=[],
222
+ topics=topics,
223
+ )
224
+
225
+ def _load(self) -> dict:
226
+ """Read and parse the ecosystems JSON file."""
227
+ if not self._path.exists():
228
+ return {"ecosystems": {}}
229
+ with self._path.open(encoding="utf-8") as fh:
230
+ return json.load(fh)
231
+
232
+ def _generate_topics(
233
+ self,
234
+ stacks: set[str],
235
+ domains: set[str],
236
+ ) -> list[ResearchTopic]:
237
+ """Generate ResearchTopic instances from stack and domain mappings."""
238
+ topics: list[ResearchTopic] = []
239
+
240
+ for stack in sorted(stacks):
241
+ if stack in STACK_TOPICS:
242
+ topics.append(ResearchTopic(
243
+ name=f"{stack} ecosystem",
244
+ source="stack",
245
+ search_queries=STACK_TOPICS[stack],
246
+ ))
247
+
248
+ for domain in sorted(domains):
249
+ if domain in DOMAIN_TOPICS:
250
+ topics.append(ResearchTopic(
251
+ name=f"{domain} trends",
252
+ source="domain",
253
+ search_queries=DOMAIN_TOPICS[domain],
254
+ ))
255
+
256
+ return topics
@@ -0,0 +1,5 @@
1
+ """ArkaOS Cognitive Scheduler — cross-platform daemon for scheduled cognitive tasks."""
2
+
3
+ from .daemon import ArkaScheduler, ScheduleConfig
4
+
5
+ __all__ = ["ArkaScheduler", "ScheduleConfig"]
@@ -0,0 +1,86 @@
1
+ """CLI functions for managing the ArkaOS cognitive scheduler."""
2
+
3
+ from pathlib import Path
4
+
5
+ from core.cognition.scheduler.daemon import ArkaScheduler, ScheduleConfig
6
+
7
+
8
+ def list_schedules(config_path: str) -> list[dict]:
9
+ """Return a list of dicts with command, time, timeout, retry for each schedule."""
10
+ schedules = ScheduleConfig.load(config_path)
11
+ return [
12
+ {
13
+ "command": s.command,
14
+ "time": s.run_time.strftime("%H:%M"),
15
+ "timeout": s.timeout_minutes,
16
+ "retry": s.retry_on_fail,
17
+ }
18
+ for s in schedules
19
+ ]
20
+
21
+
22
+ def _last_run_date(log_dir: str, command: str) -> str:
23
+ """Return the most recent log date for a command, or 'never'."""
24
+ command_log_dir = Path(log_dir) / command
25
+ if not command_log_dir.exists():
26
+ return "never"
27
+
28
+ log_files = sorted(command_log_dir.glob("*.log"), reverse=True)
29
+ if not log_files:
30
+ return "never"
31
+
32
+ return log_files[0].stem
33
+
34
+
35
+ def scheduler_status(config_path: str, log_dir: str, lock_path: str) -> str:
36
+ """Return a formatted status string with schedule info and last runs."""
37
+ is_running = Path(lock_path).exists()
38
+ status_label = "RUNNING" if is_running else "STOPPED"
39
+
40
+ schedules = ScheduleConfig.load(config_path)
41
+
42
+ schedule_lines = []
43
+ for s in schedules:
44
+ time_str = s.run_time.strftime("%H:%M")
45
+ retry_str = ", retry" if s.retry_on_fail else ""
46
+ schedule_lines.append(
47
+ f" {s.command:<12} at {time_str} (timeout: {s.timeout_minutes}m{retry_str})"
48
+ )
49
+
50
+ last_run_lines = []
51
+ for s in schedules:
52
+ last_date = _last_run_date(log_dir, s.command)
53
+ last_run_lines.append(f" {s.command:<12} last: {last_date}")
54
+
55
+ lines = [
56
+ "=== ArkaOS Scheduler Status ===",
57
+ "",
58
+ f"Status: {status_label}",
59
+ "",
60
+ "Schedules:",
61
+ *schedule_lines,
62
+ "",
63
+ "Last runs:",
64
+ *last_run_lines,
65
+ "",
66
+ "===============================",
67
+ ]
68
+ return "\n".join(lines)
69
+
70
+
71
+ def run_now(command: str, config_path: str, log_dir: str, lock_path: str) -> bool:
72
+ """Execute a specific schedule immediately by command name.
73
+
74
+ Raises ValueError if the command is not found in the config.
75
+ """
76
+ scheduler = ArkaScheduler(
77
+ config_path=config_path,
78
+ log_dir=log_dir,
79
+ lock_path=lock_path,
80
+ )
81
+
82
+ match = next((s for s in scheduler.schedules if s.command == command), None)
83
+ if match is None:
84
+ raise ValueError(f"Unknown schedule command: {command!r}")
85
+
86
+ return scheduler.execute(match)
@@ -0,0 +1,172 @@
1
+ """ArkaScheduler — cross-platform daemon for running cognitive tasks on schedule.
2
+
3
+ Reads a YAML schedule config, acquires a file lock to prevent duplicate runs,
4
+ and executes Claude CLI commands with logging per task.
5
+ """
6
+
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime, time
13
+ from pathlib import Path
14
+
15
+ import yaml
16
+
17
+
18
+ @dataclass
19
+ class ScheduleConfig:
20
+ """Configuration for a single scheduled cognitive task."""
21
+
22
+ command: str
23
+ prompt_file: str
24
+ run_time: time
25
+ enabled: bool = True
26
+ retry_on_fail: bool = True
27
+ max_retries: int = 2
28
+ timeout_minutes: int = 60
29
+
30
+ @classmethod
31
+ def load(cls, config_path: str) -> "list[ScheduleConfig]":
32
+ """Load schedules from YAML, returning only enabled entries."""
33
+ with open(config_path) as fh:
34
+ data = yaml.safe_load(fh)
35
+
36
+ schedules = []
37
+ for _name, cfg in (data.get("schedules") or {}).items():
38
+ if not cfg.get("enabled", True):
39
+ continue
40
+ raw_time = cfg["time"]
41
+ hour, minute = (int(p) for p in raw_time.split(":"))
42
+ schedules.append(
43
+ cls(
44
+ command=cfg["command"],
45
+ prompt_file=cfg["prompt_file"],
46
+ run_time=time(hour, minute),
47
+ enabled=cfg.get("enabled", True),
48
+ retry_on_fail=cfg.get("retry_on_fail", True),
49
+ max_retries=cfg.get("max_retries", 2),
50
+ timeout_minutes=cfg.get("timeout_minutes", 60),
51
+ )
52
+ )
53
+ return schedules
54
+
55
+
56
+ class ArkaScheduler:
57
+ """Cross-platform scheduler daemon for ArkaOS cognitive tasks."""
58
+
59
+ def __init__(self, config_path: str, log_dir: str, lock_path: str) -> None:
60
+ self._config_path = config_path
61
+ self._log_dir = log_dir
62
+ self._lock_path = lock_path
63
+ self._lock_fd = None
64
+ self.schedules: list[ScheduleConfig] = ScheduleConfig.load(config_path)
65
+
66
+ # ------------------------------------------------------------------
67
+ # Lock management
68
+ # ------------------------------------------------------------------
69
+
70
+ def acquire_lock(self) -> bool:
71
+ """Acquire an exclusive file lock. Returns False if already locked."""
72
+ Path(self._lock_path).parent.mkdir(parents=True, exist_ok=True)
73
+ try:
74
+ fd = open(self._lock_path, "w") # noqa: WPS515
75
+ if sys.platform == "win32":
76
+ import msvcrt # type: ignore[import]
77
+
78
+ msvcrt.locking(fd.fileno(), msvcrt.LK_NBLCK, 1)
79
+ else:
80
+ import fcntl # type: ignore[import]
81
+
82
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
83
+ self._lock_fd = fd
84
+ return True
85
+ except (OSError, IOError):
86
+ return False
87
+
88
+ def release_lock(self) -> None:
89
+ """Release the file lock if held."""
90
+ if self._lock_fd is None:
91
+ return
92
+ try:
93
+ if sys.platform == "win32":
94
+ import msvcrt # type: ignore[import]
95
+
96
+ msvcrt.locking(self._lock_fd.fileno(), msvcrt.LK_UNLCK, 1)
97
+ else:
98
+ import fcntl # type: ignore[import]
99
+
100
+ fcntl.flock(self._lock_fd, fcntl.LOCK_UN)
101
+ finally:
102
+ self._lock_fd.close()
103
+ self._lock_fd = None
104
+
105
+ # ------------------------------------------------------------------
106
+ # Schedule logic
107
+ # ------------------------------------------------------------------
108
+
109
+ def _should_run(self, schedule: ScheduleConfig, current_time: time) -> bool:
110
+ """Return True when current_time matches schedule's run_time (HH:MM)."""
111
+ return (
112
+ current_time.hour == schedule.run_time.hour
113
+ and current_time.minute == schedule.run_time.minute
114
+ )
115
+
116
+ def _build_command(self, schedule: ScheduleConfig) -> list[str]:
117
+ """Build the Claude CLI invocation for a schedule."""
118
+ claude_bin = shutil.which("claude") or "claude"
119
+ prompt_path = os.path.expanduser(schedule.prompt_file)
120
+ prompt_content = Path(prompt_path).read_text(encoding="utf-8")
121
+ return [claude_bin, "-p", prompt_content, "--dangerously-skip-permissions"]
122
+
123
+ # ------------------------------------------------------------------
124
+ # Execution
125
+ # ------------------------------------------------------------------
126
+
127
+ def _log_path(self, command: str) -> Path:
128
+ """Return the log file path for today's run of a command."""
129
+ today = datetime.now().strftime("%Y-%m-%d")
130
+ log_dir = Path(self._log_dir) / command
131
+ log_dir.mkdir(parents=True, exist_ok=True)
132
+ return log_dir / f"{today}.log"
133
+
134
+ def execute(self, schedule: ScheduleConfig) -> bool:
135
+ """Run the scheduled command, writing output to a dated log file."""
136
+ log_file = self._log_path(schedule.command)
137
+ timeout_seconds = schedule.timeout_minutes * 60
138
+ attempts = 0
139
+ max_attempts = schedule.max_retries + 1 if schedule.retry_on_fail else 1
140
+
141
+ while attempts < max_attempts:
142
+ attempts += 1
143
+ cmd = self._build_command(schedule)
144
+ with open(log_file, "a", encoding="utf-8") as lf:
145
+ lf.write(f"\n--- attempt {attempts} at {datetime.now().isoformat()} ---\n")
146
+ try:
147
+ result = subprocess.run(
148
+ cmd,
149
+ stdout=lf,
150
+ stderr=lf,
151
+ timeout=timeout_seconds,
152
+ )
153
+ if result.returncode == 0:
154
+ return True
155
+ lf.write(f"exit code: {result.returncode}\n")
156
+ except subprocess.TimeoutExpired:
157
+ lf.write("TIMEOUT\n")
158
+ except Exception as exc: # noqa: BLE001
159
+ lf.write(f"ERROR: {exc}\n")
160
+
161
+ return False
162
+
163
+ # ------------------------------------------------------------------
164
+ # Main entry point
165
+ # ------------------------------------------------------------------
166
+
167
+ def run_once(self) -> None:
168
+ """Check all schedules against current time and execute matching ones."""
169
+ now = datetime.now().time().replace(second=0, microsecond=0)
170
+ for schedule in self.schedules:
171
+ if self._should_run(schedule, now):
172
+ self.execute(schedule)