@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.
Files changed (77) hide show
  1. package/.github/workflows/publish.yml +8 -1
  2. package/docs/CUSTOM_AGENT.md +184 -0
  3. package/docs/GETTING_STARTED.md +3 -0
  4. package/launchd/com.skcapstone.daemon.plist +52 -0
  5. package/launchd/com.skcapstone.memory-compress.plist +45 -0
  6. package/launchd/com.skcapstone.skcomm-heartbeat.plist +33 -0
  7. package/launchd/com.skcapstone.skcomm-queue-drain.plist +34 -0
  8. package/launchd/install-launchd.sh +156 -0
  9. package/package.json +1 -1
  10. package/pyproject.toml +1 -1
  11. package/scripts/archive-sessions.sh +88 -0
  12. package/scripts/install.sh +39 -8
  13. package/scripts/notion-api.py +259 -0
  14. package/scripts/nvidia-proxy.mjs +878 -0
  15. package/scripts/proxy-monitor.sh +89 -0
  16. package/scripts/refresh-anthropic-token.sh +94 -0
  17. package/scripts/skgateway.mjs +856 -0
  18. package/scripts/telegram-catchup-all.sh +136 -0
  19. package/scripts/watch-anthropic-token.sh +117 -0
  20. package/src/skcapstone/__init__.py +1 -1
  21. package/src/skcapstone/_cli_monolith.py +4 -4
  22. package/src/skcapstone/api.py +36 -35
  23. package/src/skcapstone/auction.py +8 -8
  24. package/src/skcapstone/blueprint_registry.py +2 -2
  25. package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
  26. package/src/skcapstone/brain_first.py +238 -0
  27. package/src/skcapstone/chat.py +4 -4
  28. package/src/skcapstone/cli/__init__.py +2 -0
  29. package/src/skcapstone/cli/agents_spawner.py +5 -2
  30. package/src/skcapstone/cli/chat.py +5 -2
  31. package/src/skcapstone/cli/consciousness.py +5 -2
  32. package/src/skcapstone/cli/daemon.py +116 -41
  33. package/src/skcapstone/cli/itil.py +434 -0
  34. package/src/skcapstone/cli/memory.py +4 -4
  35. package/src/skcapstone/cli/skills_cmd.py +2 -2
  36. package/src/skcapstone/cli/soul.py +5 -2
  37. package/src/skcapstone/cli/status.py +11 -8
  38. package/src/skcapstone/cli/upgrade_cmd.py +7 -4
  39. package/src/skcapstone/cli/watch_cmd.py +9 -6
  40. package/src/skcapstone/config_validator.py +7 -4
  41. package/src/skcapstone/consciousness_config.py +27 -0
  42. package/src/skcapstone/consciousness_loop.py +20 -18
  43. package/src/skcapstone/coordination.py +6 -2
  44. package/src/skcapstone/daemon.py +51 -42
  45. package/src/skcapstone/dashboard.py +8 -8
  46. package/src/skcapstone/defaults/lumina/config/claude-hooks.md +42 -0
  47. package/src/skcapstone/doctor.py +5 -2
  48. package/src/skcapstone/dreaming.py +1440 -0
  49. package/src/skcapstone/emotion_tracker.py +2 -2
  50. package/src/skcapstone/export.py +2 -2
  51. package/src/skcapstone/fuse_mount.py +21 -13
  52. package/src/skcapstone/heartbeat.py +33 -29
  53. package/src/skcapstone/itil.py +1104 -0
  54. package/src/skcapstone/launchd.py +426 -0
  55. package/src/skcapstone/mcp_server.py +306 -4
  56. package/src/skcapstone/mcp_tools/__init__.py +4 -0
  57. package/src/skcapstone/mcp_tools/_helpers.py +2 -2
  58. package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
  59. package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
  60. package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
  61. package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
  62. package/src/skcapstone/mcp_tools/did_tools.py +9 -6
  63. package/src/skcapstone/mcp_tools/gtd_tools.py +1 -1
  64. package/src/skcapstone/mcp_tools/itil_tools.py +657 -0
  65. package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
  66. package/src/skcapstone/mcp_tools/soul_tools.py +6 -2
  67. package/src/skcapstone/mdns_discovery.py +2 -2
  68. package/src/skcapstone/metrics.py +8 -8
  69. package/src/skcapstone/migrate_memories.py +2 -2
  70. package/src/skcapstone/models.py +14 -0
  71. package/src/skcapstone/onboard.py +137 -14
  72. package/src/skcapstone/peer_directory.py +2 -2
  73. package/src/skcapstone/providers/docker.py +2 -2
  74. package/src/skcapstone/scheduled_tasks.py +107 -0
  75. package/src/skcapstone/service_health.py +83 -4
  76. package/src/skcapstone/sync_watcher.py +2 -2
  77. 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
@@ -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
- pass
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
- pass
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
- pass
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
- pass
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
- pass
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
- def daemon_install():
274
- """Install the daemon as a systemd user service.
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
- Copies unit files to ~/.config/systemd/user/, enables at login,
277
- and starts immediately. No root required.
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
- from ..systemd import install_service, systemd_available
292
+ import platform
284
293
 
285
- if not systemd_available():
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
- console.print("\n[cyan]Installing skcapstone systemd service...[/]")
291
- result = install_service()
296
+ if platform.system() == "Darwin":
297
+ from ..launchd import install_service as launchd_install
292
298
 
293
- if result["installed"]:
294
- console.print("[green] Unit files installed.[/]")
295
- if result["enabled"]:
296
- console.print("[green] Service enabled at login.[/]")
297
- if result["started"]:
298
- console.print("[green] Service started.[/]")
299
- console.print()
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
- if not result["installed"]:
302
- console.print("[red]Installation failed. Check logs.[/]")
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 systemd user service.
344
+ """Uninstall the system service.
308
345
 
309
- Stops, disables, and removes the unit files.
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
- from ..systemd import uninstall_service
353
+ import platform
316
354
 
317
- console.print("\n[cyan]Uninstalling skcapstone systemd service...[/]")
318
- result = uninstall_service()
355
+ if platform.system() == "Darwin":
356
+ from ..launchd import uninstall_service as launchd_uninstall
319
357
 
320
- if result["stopped"]:
321
- console.print("[green] Service stopped.[/]")
322
- if result["disabled"]:
323
- console.print("[green] Service disabled.[/]")
324
- if result["removed"]:
325
- console.print("[green] Unit files removed.[/]")
326
- console.print()
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 from journald.
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
- from ..systemd import service_logs
486
+ import platform
427
487
 
428
- if follow:
429
- cmd = service_logs(follow=True)
430
- console.print(f"\n Run: [bold cyan]{cmd}[/]\n")
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
- output = service_logs(lines=lines)
433
- if output.strip():
434
- click.echo(output)
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
- console.print("[dim]No logs found. Is the service installed?[/]")
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?[/]")