@smilintux/skcapstone 0.4.6 → 0.5.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.
- package/.github/workflows/publish.yml +8 -1
- package/docs/CUSTOM_AGENT.md +184 -0
- package/docs/GETTING_STARTED.md +3 -0
- package/launchd/com.skcapstone.daemon.plist +52 -0
- package/launchd/com.skcapstone.memory-compress.plist +45 -0
- package/launchd/com.skcapstone.skcomm-heartbeat.plist +33 -0
- package/launchd/com.skcapstone.skcomm-queue-drain.plist +34 -0
- package/launchd/install-launchd.sh +156 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/archive-sessions.sh +88 -0
- package/scripts/install.sh +39 -8
- package/scripts/notion-api.py +259 -0
- package/scripts/nvidia-proxy.mjs +878 -0
- package/scripts/proxy-monitor.sh +89 -0
- package/scripts/refresh-anthropic-token.sh +94 -0
- package/scripts/skgateway.mjs +856 -0
- package/scripts/telegram-catchup-all.sh +136 -0
- package/scripts/watch-anthropic-token.sh +117 -0
- package/src/skcapstone/__init__.py +1 -1
- package/src/skcapstone/_cli_monolith.py +4 -4
- package/src/skcapstone/api.py +36 -35
- package/src/skcapstone/auction.py +8 -8
- package/src/skcapstone/blueprint_registry.py +2 -2
- package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
- package/src/skcapstone/brain_first.py +238 -0
- package/src/skcapstone/chat.py +4 -4
- package/src/skcapstone/cli/__init__.py +2 -0
- package/src/skcapstone/cli/agents_spawner.py +5 -2
- package/src/skcapstone/cli/chat.py +5 -2
- package/src/skcapstone/cli/consciousness.py +5 -2
- package/src/skcapstone/cli/daemon.py +116 -41
- package/src/skcapstone/cli/itil.py +434 -0
- package/src/skcapstone/cli/memory.py +4 -4
- package/src/skcapstone/cli/skills_cmd.py +2 -2
- package/src/skcapstone/cli/soul.py +5 -2
- package/src/skcapstone/cli/status.py +11 -8
- package/src/skcapstone/cli/upgrade_cmd.py +7 -4
- package/src/skcapstone/cli/watch_cmd.py +9 -6
- package/src/skcapstone/config_validator.py +7 -4
- package/src/skcapstone/consciousness_config.py +27 -0
- package/src/skcapstone/consciousness_loop.py +20 -18
- package/src/skcapstone/coordination.py +6 -2
- package/src/skcapstone/daemon.py +51 -42
- package/src/skcapstone/dashboard.py +8 -8
- package/src/skcapstone/defaults/lumina/config/claude-hooks.md +42 -0
- package/src/skcapstone/doctor.py +5 -2
- package/src/skcapstone/dreaming.py +1440 -0
- package/src/skcapstone/emotion_tracker.py +2 -2
- package/src/skcapstone/export.py +2 -2
- package/src/skcapstone/fuse_mount.py +21 -13
- package/src/skcapstone/heartbeat.py +33 -29
- package/src/skcapstone/itil.py +1104 -0
- package/src/skcapstone/launchd.py +426 -0
- package/src/skcapstone/mcp_server.py +306 -4
- package/src/skcapstone/mcp_tools/__init__.py +4 -0
- package/src/skcapstone/mcp_tools/_helpers.py +2 -2
- package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
- package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
- package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
- package/src/skcapstone/mcp_tools/did_tools.py +9 -6
- package/src/skcapstone/mcp_tools/gtd_tools.py +1 -1
- package/src/skcapstone/mcp_tools/itil_tools.py +657 -0
- package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
- package/src/skcapstone/mcp_tools/soul_tools.py +6 -2
- package/src/skcapstone/mdns_discovery.py +2 -2
- package/src/skcapstone/metrics.py +8 -8
- package/src/skcapstone/migrate_memories.py +2 -2
- package/src/skcapstone/models.py +14 -0
- package/src/skcapstone/onboard.py +137 -14
- package/src/skcapstone/peer_directory.py +2 -2
- package/src/skcapstone/providers/docker.py +2 -2
- package/src/skcapstone/scheduled_tasks.py +107 -0
- package/src/skcapstone/service_health.py +83 -4
- package/src/skcapstone/sync_watcher.py +2 -2
- package/src/skcapstone/systemd.py +17 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain-First Protocol — think before you act.
|
|
3
|
+
|
|
4
|
+
Before an agent acts on any task, it consults its memory to see if it
|
|
5
|
+
already knows something relevant. This avoids redundant work, surfaces
|
|
6
|
+
prior decisions, and grounds the agent in its own experience.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from skcapstone.brain_first import brain_first_check
|
|
10
|
+
|
|
11
|
+
result = brain_first_check("deploy the monitoring stack")
|
|
12
|
+
if result.has_memories:
|
|
13
|
+
# use result.memories as additional context
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
Configuration (config.yaml):
|
|
17
|
+
brain_first:
|
|
18
|
+
enabled: true # master toggle (default: true)
|
|
19
|
+
max_results: 5 # how many memories to surface (default: 5)
|
|
20
|
+
min_importance: 0.3 # ignore low-importance memories (default: 0.3)
|
|
21
|
+
auto_inject: false # auto-prepend memories to MCP tool responses (default: false)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger("skcapstone.brain_first")
|
|
34
|
+
|
|
35
|
+
# Stop-words to strip from queries before searching memory
|
|
36
|
+
_STOP_WORDS = frozenset({
|
|
37
|
+
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
38
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "shall",
|
|
39
|
+
"should", "may", "might", "must", "can", "could", "to", "of", "in",
|
|
40
|
+
"for", "on", "with", "at", "by", "from", "as", "into", "through",
|
|
41
|
+
"during", "before", "after", "above", "below", "between", "out",
|
|
42
|
+
"off", "over", "under", "again", "further", "then", "once", "here",
|
|
43
|
+
"there", "when", "where", "why", "how", "all", "each", "every",
|
|
44
|
+
"both", "few", "more", "most", "other", "some", "such", "no", "nor",
|
|
45
|
+
"not", "only", "own", "same", "so", "than", "too", "very", "just",
|
|
46
|
+
"because", "but", "and", "or", "if", "while", "about", "up", "it",
|
|
47
|
+
"its", "this", "that", "these", "those", "i", "me", "my", "we",
|
|
48
|
+
"our", "you", "your", "he", "him", "his", "she", "her", "they",
|
|
49
|
+
"them", "their", "what", "which", "who", "whom",
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class BrainFirstConfig:
|
|
55
|
+
"""Configuration for the brain-first protocol."""
|
|
56
|
+
|
|
57
|
+
enabled: bool = True
|
|
58
|
+
max_results: int = 5
|
|
59
|
+
min_importance: float = 0.3
|
|
60
|
+
auto_inject: bool = False
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_dict(cls, data: dict) -> "BrainFirstConfig":
|
|
64
|
+
"""Create config from a dict (e.g. from config.yaml brain_first section)."""
|
|
65
|
+
return cls(
|
|
66
|
+
enabled=data.get("enabled", True),
|
|
67
|
+
max_results=data.get("max_results", 5),
|
|
68
|
+
min_importance=data.get("min_importance", 0.3),
|
|
69
|
+
auto_inject=data.get("auto_inject", False),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class BrainFirstResult:
|
|
75
|
+
"""Result of a brain-first memory consultation."""
|
|
76
|
+
|
|
77
|
+
query: str
|
|
78
|
+
keywords: list[str]
|
|
79
|
+
memories: list[dict] = field(default_factory=list)
|
|
80
|
+
enabled: bool = True
|
|
81
|
+
error: Optional[str] = None
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def has_memories(self) -> bool:
|
|
85
|
+
"""Whether any relevant memories were found."""
|
|
86
|
+
return len(self.memories) > 0
|
|
87
|
+
|
|
88
|
+
def as_context(self) -> str:
|
|
89
|
+
"""Format memories as a context block for injection into prompts."""
|
|
90
|
+
if not self.has_memories:
|
|
91
|
+
return ""
|
|
92
|
+
lines = ["[Brain-First: relevant memories found]"]
|
|
93
|
+
for i, mem in enumerate(self.memories, 1):
|
|
94
|
+
content = mem.get("content", "")[:200]
|
|
95
|
+
layer = mem.get("layer", "?")
|
|
96
|
+
importance = mem.get("importance", 0)
|
|
97
|
+
tags = ", ".join(mem.get("tags", []))
|
|
98
|
+
lines.append(
|
|
99
|
+
f" {i}. [{layer}|imp={importance:.1f}] {content}"
|
|
100
|
+
+ (f" tags: {tags}" if tags else "")
|
|
101
|
+
)
|
|
102
|
+
return "\n".join(lines)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def extract_keywords(text: str) -> list[str]:
|
|
106
|
+
"""Extract meaningful keywords from a text string.
|
|
107
|
+
|
|
108
|
+
Strips stop-words and short tokens, keeping domain-relevant terms.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
text: Input text (task title, prompt, etc.).
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of unique keywords, longest first.
|
|
115
|
+
"""
|
|
116
|
+
# Lowercase, split on non-alphanumeric
|
|
117
|
+
tokens = re.split(r"[^a-zA-Z0-9_-]+", text.lower())
|
|
118
|
+
# Filter: no stop-words, no short tokens
|
|
119
|
+
keywords = list(dict.fromkeys(
|
|
120
|
+
t for t in tokens if t and t not in _STOP_WORDS and len(t) > 2
|
|
121
|
+
))
|
|
122
|
+
# Sort longest first (longer terms tend to be more specific)
|
|
123
|
+
keywords.sort(key=len, reverse=True)
|
|
124
|
+
return keywords
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _load_config() -> BrainFirstConfig:
|
|
128
|
+
"""Load brain-first config from the agent's config.yaml.
|
|
129
|
+
|
|
130
|
+
Falls back to defaults if the file or section is missing.
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
import yaml
|
|
134
|
+
except ImportError:
|
|
135
|
+
return BrainFirstConfig()
|
|
136
|
+
|
|
137
|
+
from . import AGENT_HOME, SKCAPSTONE_AGENT
|
|
138
|
+
|
|
139
|
+
for base in [
|
|
140
|
+
Path(AGENT_HOME).expanduser() / "agents" / SKCAPSTONE_AGENT,
|
|
141
|
+
Path(AGENT_HOME).expanduser(),
|
|
142
|
+
]:
|
|
143
|
+
config_file = base / "config" / "config.yaml"
|
|
144
|
+
if config_file.exists():
|
|
145
|
+
try:
|
|
146
|
+
data = yaml.safe_load(config_file.read_text(encoding="utf-8")) or {}
|
|
147
|
+
bf_data = data.get("brain_first", {})
|
|
148
|
+
if bf_data:
|
|
149
|
+
return BrainFirstConfig.from_dict(bf_data)
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
logger.debug("Failed to load brain_first config from %s: %s", config_file, exc)
|
|
152
|
+
|
|
153
|
+
return BrainFirstConfig()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def brain_first_check(
|
|
157
|
+
context: str,
|
|
158
|
+
config: Optional[BrainFirstConfig] = None,
|
|
159
|
+
tags: Optional[list[str]] = None,
|
|
160
|
+
) -> BrainFirstResult:
|
|
161
|
+
"""Consult memory before acting on a task.
|
|
162
|
+
|
|
163
|
+
This is the core brain-first function. Given a task description or
|
|
164
|
+
prompt context, it extracts keywords, searches memory, and returns
|
|
165
|
+
any relevant memories that the agent should consider.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
context: The task description, prompt, or action context.
|
|
169
|
+
config: Override config (uses agent config.yaml if None).
|
|
170
|
+
tags: Optional tag filter for the memory search.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
BrainFirstResult with any relevant memories.
|
|
174
|
+
"""
|
|
175
|
+
if config is None:
|
|
176
|
+
config = _load_config()
|
|
177
|
+
|
|
178
|
+
keywords = extract_keywords(context)
|
|
179
|
+
result = BrainFirstResult(
|
|
180
|
+
query=context,
|
|
181
|
+
keywords=keywords,
|
|
182
|
+
enabled=config.enabled,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if not config.enabled:
|
|
186
|
+
result.error = "brain-first protocol disabled"
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
if not keywords:
|
|
190
|
+
result.error = "no meaningful keywords extracted"
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
# Build a search query from top keywords (limit to 6 to avoid noise)
|
|
194
|
+
search_query = " ".join(keywords[:6])
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
from .memory_engine import search as memory_search
|
|
198
|
+
from .mcp_tools._helpers import _home
|
|
199
|
+
|
|
200
|
+
home = _home()
|
|
201
|
+
entries = memory_search(
|
|
202
|
+
home=home,
|
|
203
|
+
query=search_query,
|
|
204
|
+
tags=tags,
|
|
205
|
+
limit=config.max_results * 2, # over-fetch, then filter
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Filter by minimum importance
|
|
209
|
+
entries = [e for e in entries if e.importance >= config.min_importance]
|
|
210
|
+
|
|
211
|
+
# Truncate to max_results
|
|
212
|
+
entries = entries[:config.max_results]
|
|
213
|
+
|
|
214
|
+
result.memories = [
|
|
215
|
+
{
|
|
216
|
+
"memory_id": e.memory_id,
|
|
217
|
+
"content": e.content[:300],
|
|
218
|
+
"layer": e.layer.value,
|
|
219
|
+
"tags": e.tags,
|
|
220
|
+
"importance": e.importance,
|
|
221
|
+
"access_count": e.access_count,
|
|
222
|
+
"source": e.source,
|
|
223
|
+
}
|
|
224
|
+
for e in entries
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
logger.info(
|
|
228
|
+
"Brain-first check: %d memories found for %d keywords from '%s'",
|
|
229
|
+
len(result.memories),
|
|
230
|
+
len(keywords),
|
|
231
|
+
context[:80],
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
except Exception as exc:
|
|
235
|
+
result.error = f"memory search failed: {exc}"
|
|
236
|
+
logger.warning("Brain-first check failed: %s", exc)
|
|
237
|
+
|
|
238
|
+
return result
|
package/src/skcapstone/chat.py
CHANGED
|
@@ -204,8 +204,8 @@ class AgentChat:
|
|
|
204
204
|
thread_id=msg_dict.get("thread_id"),
|
|
205
205
|
)
|
|
206
206
|
history.store_message(chat_msg)
|
|
207
|
-
except Exception:
|
|
208
|
-
|
|
207
|
+
except Exception as exc:
|
|
208
|
+
logger.warning("Failed to store received message in history: %s", exc)
|
|
209
209
|
except Exception as exc:
|
|
210
210
|
logger.warning("Receive error: %s", exc)
|
|
211
211
|
|
|
@@ -410,8 +410,8 @@ class AgentChat:
|
|
|
410
410
|
state["last_recv_thread"] = recv_thread
|
|
411
411
|
display = _format_content(content)
|
|
412
412
|
print(f"\n \033[32m{sender}\033[0m \033[2m[{ts}]\033[0m {display}\n")
|
|
413
|
-
except Exception:
|
|
414
|
-
|
|
413
|
+
except Exception as exc:
|
|
414
|
+
logger.warning("Chat poll loop error: %s", exc)
|
|
415
415
|
|
|
416
416
|
# Print header
|
|
417
417
|
tr_label = "✓ connected" if transport_ok else "✗ local-only"
|
|
@@ -86,6 +86,7 @@ from .search_cmd import register_search_commands
|
|
|
86
86
|
from .mood_cmd import register_mood_commands
|
|
87
87
|
from .register_cmd import register_register_commands
|
|
88
88
|
from .gtd import register_gtd_commands
|
|
89
|
+
from .itil import register_itil_commands
|
|
89
90
|
from .skseed import register_skseed_commands
|
|
90
91
|
from .service_cmd import register_service_commands
|
|
91
92
|
from .telegram import register_telegram_commands
|
|
@@ -138,6 +139,7 @@ register_search_commands(main)
|
|
|
138
139
|
register_mood_commands(main)
|
|
139
140
|
register_register_commands(main)
|
|
140
141
|
register_gtd_commands(main)
|
|
142
|
+
register_itil_commands(main)
|
|
141
143
|
register_skseed_commands(main)
|
|
142
144
|
register_service_commands(main)
|
|
143
145
|
register_telegram_commands(main)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Optional
|
|
7
8
|
|
|
@@ -12,6 +13,8 @@ from ._common import AGENT_HOME, console
|
|
|
12
13
|
from rich.panel import Panel
|
|
13
14
|
from rich.table import Table
|
|
14
15
|
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
15
18
|
|
|
16
19
|
def _resolve_provider_backend(provider: Optional[str], home_path: Path):
|
|
17
20
|
"""Resolve provider string to backend object and ProviderType.
|
|
@@ -37,8 +40,8 @@ def _resolve_provider_backend(provider: Optional[str], home_path: Path):
|
|
|
37
40
|
prov_backend = DockerProvider()
|
|
38
41
|
elif prov_type == ProviderType.PROXMOX:
|
|
39
42
|
prov_backend = ProxmoxProvider()
|
|
40
|
-
except Exception:
|
|
41
|
-
|
|
43
|
+
except Exception as exc:
|
|
44
|
+
logger.warning("Failed to initialize provider backend for %s: %s", provider, exc)
|
|
42
45
|
return prov_backend, prov_type
|
|
43
46
|
|
|
44
47
|
|
|
@@ -13,12 +13,15 @@ skcapstone chat summary <peer> LLM-powered conversation summary
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
|
+
import logging
|
|
16
17
|
import sys
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Optional
|
|
19
20
|
|
|
20
21
|
import click
|
|
21
22
|
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
22
25
|
from ._common import AGENT_HOME, console, get_runtime
|
|
23
26
|
from ._validators import validate_agent_name
|
|
24
27
|
|
|
@@ -85,8 +88,8 @@ def _run_llm_chat(peer: str, home_path: Path, identity: str) -> None:
|
|
|
85
88
|
content = msg.get("content", "")[:100]
|
|
86
89
|
console.print(f" {label}: {content}")
|
|
87
90
|
console.print()
|
|
88
|
-
except Exception:
|
|
89
|
-
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
logger.warning("Failed to load previous conversation history with %s: %s", peer, exc)
|
|
90
93
|
|
|
91
94
|
console.print(f"[bold]Chat with [cyan]{peer}[/][/] [dim]Ctrl+C or /quit to exit[/]\n")
|
|
92
95
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import logging
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
@@ -10,6 +11,8 @@ import click
|
|
|
10
11
|
|
|
11
12
|
from ._common import AGENT_HOME, console
|
|
12
13
|
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
def register_consciousness_commands(main: click.Group) -> None:
|
|
15
18
|
"""Register the consciousness command group."""
|
|
@@ -233,8 +236,8 @@ def register_consciousness_commands(main: click.Group) -> None:
|
|
|
233
236
|
try:
|
|
234
237
|
file_data = json.loads(daily.read_text(encoding="utf-8"))
|
|
235
238
|
quality = file_data.get("quality_avg", {})
|
|
236
|
-
except Exception:
|
|
237
|
-
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
logger.warning("Failed to read daily quality metrics from %s: %s", daily, exc)
|
|
238
241
|
|
|
239
242
|
if not quality or quality.get("count", 0) == 0:
|
|
240
243
|
if json_out:
|
|
@@ -270,60 +270,117 @@ def register_daemon_commands(main: click.Group) -> None:
|
|
|
270
270
|
console.print(f" [yellow]API unreachable on port {effective_port}[/]\n")
|
|
271
271
|
|
|
272
272
|
@daemon.command("install")
|
|
273
|
-
|
|
274
|
-
|
|
273
|
+
@click.option("--agent", "agent_name", default=None,
|
|
274
|
+
help="Agent name for SKCAPSTONE_AGENT (default: from env or 'sovereign').")
|
|
275
|
+
@click.option("--start", is_flag=True, help="Start services immediately after installing.")
|
|
276
|
+
def daemon_install(agent_name: str | None, start: bool):
|
|
277
|
+
"""Install the daemon as a system service.
|
|
275
278
|
|
|
276
|
-
|
|
277
|
-
|
|
279
|
+
On Linux: installs systemd user service units.
|
|
280
|
+
On macOS: installs launchd plist files to ~/Library/LaunchAgents/.
|
|
281
|
+
|
|
282
|
+
The --agent flag sets the SKCAPSTONE_AGENT environment variable
|
|
283
|
+
in the service definition. If not provided, uses the
|
|
284
|
+
SKCAPSTONE_AGENT env var or defaults to 'sovereign'.
|
|
278
285
|
|
|
279
286
|
Examples:
|
|
280
287
|
|
|
281
288
|
skcapstone daemon install
|
|
289
|
+
|
|
290
|
+
skcapstone daemon install --agent myagent --start
|
|
282
291
|
"""
|
|
283
|
-
|
|
292
|
+
import platform
|
|
284
293
|
|
|
285
|
-
|
|
286
|
-
console.print("[red]systemd user session not available.[/]")
|
|
287
|
-
console.print("[dim]This command requires a Linux system with systemd.[/]")
|
|
288
|
-
raise SystemExit(1)
|
|
294
|
+
effective_agent = agent_name or os.environ.get("SKCAPSTONE_AGENT", "sovereign")
|
|
289
295
|
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
if platform.system() == "Darwin":
|
|
297
|
+
from ..launchd import install_service as launchd_install
|
|
292
298
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
299
|
+
console.print(f"\n[cyan]Installing launchd services for agent '{effective_agent}'...[/]")
|
|
300
|
+
result = launchd_install(agent_name=effective_agent, start=start)
|
|
301
|
+
|
|
302
|
+
if result["installed"]:
|
|
303
|
+
for svc in result.get("services", []):
|
|
304
|
+
status = "[green]loaded[/]" if svc.get("loaded") else "[green]installed[/]"
|
|
305
|
+
console.print(f" [green]✓[/] {svc['label']} — {status}")
|
|
306
|
+
console.print()
|
|
307
|
+
console.print("[dim] Manage: launchctl list | grep skcapstone[/]")
|
|
308
|
+
if not start:
|
|
309
|
+
console.print("[dim] Start: launchctl start com.skcapstone.daemon[/]")
|
|
310
|
+
console.print("[dim] Or re-run with --start to load immediately.[/]")
|
|
311
|
+
else:
|
|
312
|
+
console.print("[red]Installation failed. Check logs.[/]")
|
|
313
|
+
raise SystemExit(1)
|
|
314
|
+
console.print()
|
|
300
315
|
|
|
301
|
-
|
|
302
|
-
|
|
316
|
+
elif platform.system() == "Linux":
|
|
317
|
+
from ..systemd import install_service, systemd_available
|
|
318
|
+
|
|
319
|
+
if not systemd_available():
|
|
320
|
+
console.print("[red]systemd user session not available.[/]")
|
|
321
|
+
console.print("[dim]This command requires a Linux system with systemd.[/]")
|
|
322
|
+
raise SystemExit(1)
|
|
323
|
+
|
|
324
|
+
console.print("\n[cyan]Installing skcapstone systemd service...[/]")
|
|
325
|
+
result = install_service(start=start)
|
|
326
|
+
|
|
327
|
+
if result["installed"]:
|
|
328
|
+
console.print("[green] Unit files installed.[/]")
|
|
329
|
+
if result["enabled"]:
|
|
330
|
+
console.print("[green] Service enabled at login.[/]")
|
|
331
|
+
if result.get("started"):
|
|
332
|
+
console.print("[green] Service started.[/]")
|
|
333
|
+
console.print()
|
|
334
|
+
|
|
335
|
+
if not result["installed"]:
|
|
336
|
+
console.print("[red]Installation failed. Check logs.[/]")
|
|
337
|
+
raise SystemExit(1)
|
|
338
|
+
else:
|
|
339
|
+
console.print(f"[red]Auto-start not supported on {platform.system()}.[/]")
|
|
303
340
|
raise SystemExit(1)
|
|
304
341
|
|
|
305
342
|
@daemon.command("uninstall")
|
|
306
343
|
def daemon_uninstall():
|
|
307
|
-
"""Uninstall the
|
|
344
|
+
"""Uninstall the system service.
|
|
308
345
|
|
|
309
|
-
|
|
346
|
+
On Linux: stops, disables, and removes systemd unit files.
|
|
347
|
+
On macOS: unloads and removes launchd plist files.
|
|
310
348
|
|
|
311
349
|
Examples:
|
|
312
350
|
|
|
313
351
|
skcapstone daemon uninstall
|
|
314
352
|
"""
|
|
315
|
-
|
|
353
|
+
import platform
|
|
316
354
|
|
|
317
|
-
|
|
318
|
-
|
|
355
|
+
if platform.system() == "Darwin":
|
|
356
|
+
from ..launchd import uninstall_service as launchd_uninstall
|
|
319
357
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
358
|
+
console.print("\n[cyan]Uninstalling skcapstone launchd services...[/]")
|
|
359
|
+
result = launchd_uninstall()
|
|
360
|
+
|
|
361
|
+
if result["stopped"]:
|
|
362
|
+
console.print("[green] Services unloaded.[/]")
|
|
363
|
+
if result["removed"]:
|
|
364
|
+
for label in result.get("services", []):
|
|
365
|
+
console.print(f" [green]✓[/] Removed {label}")
|
|
366
|
+
console.print()
|
|
367
|
+
|
|
368
|
+
elif platform.system() == "Linux":
|
|
369
|
+
from ..systemd import uninstall_service
|
|
370
|
+
|
|
371
|
+
console.print("\n[cyan]Uninstalling skcapstone systemd service...[/]")
|
|
372
|
+
result = uninstall_service()
|
|
373
|
+
|
|
374
|
+
if result["stopped"]:
|
|
375
|
+
console.print("[green] Service stopped.[/]")
|
|
376
|
+
if result["disabled"]:
|
|
377
|
+
console.print("[green] Service disabled.[/]")
|
|
378
|
+
if result["removed"]:
|
|
379
|
+
console.print("[green] Unit files removed.[/]")
|
|
380
|
+
console.print()
|
|
381
|
+
|
|
382
|
+
else:
|
|
383
|
+
console.print(f"[red]Not supported on {platform.system()}.[/]")
|
|
327
384
|
|
|
328
385
|
@daemon.command("components")
|
|
329
386
|
@click.option("--agent", default=None, help="Named agent to query.")
|
|
@@ -413,7 +470,10 @@ def register_daemon_commands(main: click.Group) -> None:
|
|
|
413
470
|
@click.option("--lines", "-n", default=50, help="Number of lines (default: 50).")
|
|
414
471
|
@click.option("--follow", "-f", is_flag=True, help="Show the command to follow logs live.")
|
|
415
472
|
def daemon_logs(lines: int, follow: bool):
|
|
416
|
-
"""Show daemon logs
|
|
473
|
+
"""Show daemon logs.
|
|
474
|
+
|
|
475
|
+
On Linux: reads from journald.
|
|
476
|
+
On macOS: reads from ~/.skcapstone/logs/ files.
|
|
417
477
|
|
|
418
478
|
Examples:
|
|
419
479
|
|
|
@@ -423,14 +483,29 @@ def register_daemon_commands(main: click.Group) -> None:
|
|
|
423
483
|
|
|
424
484
|
skcapstone daemon logs -f
|
|
425
485
|
"""
|
|
426
|
-
|
|
486
|
+
import platform
|
|
427
487
|
|
|
428
|
-
if
|
|
429
|
-
|
|
430
|
-
|
|
488
|
+
if platform.system() == "Darwin":
|
|
489
|
+
if follow:
|
|
490
|
+
log_path = Path.home() / ".skcapstone" / "logs" / "daemon.stdout.log"
|
|
491
|
+
console.print(f"\n Run: [bold cyan]tail -f {log_path}[/]\n")
|
|
492
|
+
else:
|
|
493
|
+
from ..launchd import service_logs
|
|
494
|
+
output = service_logs(lines=lines)
|
|
495
|
+
if output.strip():
|
|
496
|
+
click.echo(output)
|
|
497
|
+
else:
|
|
498
|
+
console.print("[dim]No logs found in ~/.skcapstone/logs/[/]")
|
|
499
|
+
console.print("[dim]Is the service installed? Run: skcapstone daemon install[/]")
|
|
431
500
|
else:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
501
|
+
from ..systemd import service_logs
|
|
502
|
+
|
|
503
|
+
if follow:
|
|
504
|
+
cmd = service_logs(follow=True)
|
|
505
|
+
console.print(f"\n Run: [bold cyan]{cmd}[/]\n")
|
|
435
506
|
else:
|
|
436
|
-
|
|
507
|
+
output = service_logs(lines=lines)
|
|
508
|
+
if output.strip():
|
|
509
|
+
click.echo(output)
|
|
510
|
+
else:
|
|
511
|
+
console.print("[dim]No logs found. Is the service installed?[/]")
|