@smilintux/skcapstone 0.2.6 → 0.3.2
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/README.md +61 -0
- package/docs/CUSTOM_AGENT.md +184 -0
- package/docs/GETTING_STARTED.md +3 -0
- package/openclaw-plugin/src/index.ts +75 -4
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/archive-sessions.sh +72 -0
- package/scripts/install.ps1 +2 -1
- package/scripts/install.sh +2 -1
- package/scripts/nvidia-proxy.mjs +727 -0
- package/scripts/telegram-catchup-all.sh +136 -0
- package/src/skcapstone/__init__.py +70 -1
- package/src/skcapstone/agent_card.py +4 -1
- package/src/skcapstone/blueprint_registry.py +78 -0
- package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
- package/src/skcapstone/cli/__init__.py +2 -0
- package/src/skcapstone/cli/_common.py +5 -5
- package/src/skcapstone/cli/card.py +36 -5
- package/src/skcapstone/cli/config_cmd.py +53 -1
- package/src/skcapstone/cli/itil.py +434 -0
- package/src/skcapstone/cli/peer.py +3 -1
- package/src/skcapstone/cli/peers_dir.py +3 -1
- package/src/skcapstone/cli/preflight_cmd.py +4 -0
- package/src/skcapstone/cli/skills_cmd.py +120 -24
- package/src/skcapstone/cli/soul.py +47 -24
- package/src/skcapstone/cli/status.py +17 -11
- package/src/skcapstone/cli/usage_cmd.py +7 -2
- package/src/skcapstone/consciousness_config.py +27 -0
- package/src/skcapstone/coordination.py +1 -0
- package/src/skcapstone/daemon.py +28 -9
- package/src/skcapstone/defaults/lumina/manifest.json +1 -1
- package/src/skcapstone/doctor.py +115 -0
- package/src/skcapstone/dreaming.py +761 -0
- package/src/skcapstone/itil.py +1104 -0
- package/src/skcapstone/mcp_server.py +258 -0
- package/src/skcapstone/mcp_tools/__init__.py +2 -0
- 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/notification_tools.py +12 -11
- package/src/skcapstone/notifications.py +40 -27
- package/src/skcapstone/onboard.py +46 -0
- package/src/skcapstone/pillars/sync.py +11 -4
- package/src/skcapstone/register.py +8 -0
- package/src/skcapstone/scheduled_tasks.py +107 -0
- package/src/skcapstone/service_health.py +81 -2
- package/src/skcapstone/soul.py +19 -0
- package/systemd/skcapstone.service +5 -6
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""ITIL service management CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ._common import AGENT_HOME, SHARED_ROOT, console
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_itil_commands(main: click.Group) -> None:
|
|
15
|
+
"""Register the itil command group."""
|
|
16
|
+
|
|
17
|
+
@main.group()
|
|
18
|
+
def itil():
|
|
19
|
+
"""ITIL service management — incidents, problems, changes."""
|
|
20
|
+
|
|
21
|
+
# ── itil status ───────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
@itil.command("status")
|
|
24
|
+
def itil_status():
|
|
25
|
+
"""Show ITIL dashboard: open incidents, active problems, pending changes."""
|
|
26
|
+
from ..itil import ITILManager
|
|
27
|
+
|
|
28
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
29
|
+
status = mgr.get_status()
|
|
30
|
+
|
|
31
|
+
inc = status["incidents"]
|
|
32
|
+
prb = status["problems"]
|
|
33
|
+
chg = status["changes"]
|
|
34
|
+
kedb = status["kedb"]
|
|
35
|
+
|
|
36
|
+
console.print(f"\n[bold]ITIL Dashboard[/bold]")
|
|
37
|
+
console.print(f" Incidents: [red]{inc['open']}[/red] open / {inc['total']} total")
|
|
38
|
+
for sev, count in inc.get("by_severity", {}).items():
|
|
39
|
+
if count:
|
|
40
|
+
console.print(f" {sev}: {count}")
|
|
41
|
+
console.print(f" Problems: [yellow]{prb['active']}[/yellow] active / {prb['total']} total")
|
|
42
|
+
console.print(f" Changes: [blue]{chg['pending']}[/blue] pending / {chg['total']} total")
|
|
43
|
+
console.print(f" KEDB: {kedb['total']} entries")
|
|
44
|
+
|
|
45
|
+
if inc["open_list"]:
|
|
46
|
+
console.print(f"\n[bold red]Open Incidents:[/bold red]")
|
|
47
|
+
for i in inc["open_list"]:
|
|
48
|
+
console.print(
|
|
49
|
+
f" [{i['id']}] {i['severity'].upper()} {i['title']} "
|
|
50
|
+
f"({i['status']}) @{i['managed_by']}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if chg["pending_list"]:
|
|
54
|
+
console.print(f"\n[bold blue]Pending Changes:[/bold blue]")
|
|
55
|
+
for c in chg["pending_list"]:
|
|
56
|
+
console.print(
|
|
57
|
+
f" [{c['id']}] {c['title']} ({c['status']}, "
|
|
58
|
+
f"{c['change_type']}) @{c['managed_by']}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
console.print()
|
|
62
|
+
|
|
63
|
+
# ── itil incident ─────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
@itil.group()
|
|
66
|
+
def incident():
|
|
67
|
+
"""Incident management."""
|
|
68
|
+
|
|
69
|
+
@incident.command("create")
|
|
70
|
+
@click.option("--title", "-t", required=True, help="Incident title")
|
|
71
|
+
@click.option(
|
|
72
|
+
"--severity", "-s", default="sev3",
|
|
73
|
+
type=click.Choice(["sev1", "sev2", "sev3", "sev4"]),
|
|
74
|
+
help="Severity level",
|
|
75
|
+
)
|
|
76
|
+
@click.option("--service", multiple=True, help="Affected service(s)")
|
|
77
|
+
@click.option("--impact", default="", help="Business impact")
|
|
78
|
+
@click.option("--by", "managed_by", default="human", help="Managing agent")
|
|
79
|
+
@click.option("--tag", multiple=True, help="Tags")
|
|
80
|
+
def incident_create(title, severity, service, impact, managed_by, tag):
|
|
81
|
+
"""Create a new incident."""
|
|
82
|
+
from ..itil import ITILManager
|
|
83
|
+
|
|
84
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
85
|
+
inc = mgr.create_incident(
|
|
86
|
+
title=title,
|
|
87
|
+
severity=severity,
|
|
88
|
+
affected_services=list(service),
|
|
89
|
+
impact=impact,
|
|
90
|
+
managed_by=managed_by,
|
|
91
|
+
created_by=managed_by,
|
|
92
|
+
tags=list(tag),
|
|
93
|
+
)
|
|
94
|
+
console.print(
|
|
95
|
+
f"\n [green]Created:[/green] {inc.id} — {inc.title} "
|
|
96
|
+
f"({inc.severity.value}, {inc.status.value})"
|
|
97
|
+
)
|
|
98
|
+
if inc.gtd_item_ids:
|
|
99
|
+
console.print(f" [dim]GTD item(s): {', '.join(inc.gtd_item_ids)}[/dim]")
|
|
100
|
+
console.print()
|
|
101
|
+
|
|
102
|
+
@incident.command("list")
|
|
103
|
+
@click.option(
|
|
104
|
+
"--status", type=click.Choice([
|
|
105
|
+
"detected", "acknowledged", "investigating",
|
|
106
|
+
"escalated", "resolved", "closed",
|
|
107
|
+
]),
|
|
108
|
+
help="Filter by status",
|
|
109
|
+
)
|
|
110
|
+
@click.option(
|
|
111
|
+
"--severity",
|
|
112
|
+
type=click.Choice(["sev1", "sev2", "sev3", "sev4"]),
|
|
113
|
+
help="Filter by severity",
|
|
114
|
+
)
|
|
115
|
+
@click.option("--service", help="Filter by affected service")
|
|
116
|
+
def incident_list(status, severity, service):
|
|
117
|
+
"""List incidents."""
|
|
118
|
+
from ..itil import ITILManager
|
|
119
|
+
|
|
120
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
121
|
+
incidents = mgr.list_incidents(status=status, severity=severity, service=service)
|
|
122
|
+
|
|
123
|
+
if not incidents:
|
|
124
|
+
console.print("\n [dim]No incidents found[/dim]\n")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
console.print(f"\n[bold]Incidents ({len(incidents)}):[/bold]")
|
|
128
|
+
for i in incidents:
|
|
129
|
+
sev = i.severity.value.upper()
|
|
130
|
+
console.print(
|
|
131
|
+
f" [{i.id}] {sev} {i.title} ({i.status.value}) @{i.managed_by}"
|
|
132
|
+
)
|
|
133
|
+
console.print()
|
|
134
|
+
|
|
135
|
+
@incident.command("update")
|
|
136
|
+
@click.argument("incident_id")
|
|
137
|
+
@click.option("--agent", default="human", help="Agent making the update")
|
|
138
|
+
@click.option(
|
|
139
|
+
"--status", "new_status",
|
|
140
|
+
type=click.Choice([
|
|
141
|
+
"acknowledged", "investigating", "escalated", "resolved", "closed",
|
|
142
|
+
]),
|
|
143
|
+
help="New status",
|
|
144
|
+
)
|
|
145
|
+
@click.option(
|
|
146
|
+
"--severity",
|
|
147
|
+
type=click.Choice(["sev1", "sev2", "sev3", "sev4"]),
|
|
148
|
+
help="New severity",
|
|
149
|
+
)
|
|
150
|
+
@click.option("--note", default="", help="Timeline note")
|
|
151
|
+
@click.option("--resolution", default=None, help="Resolution summary")
|
|
152
|
+
def incident_update(incident_id, agent, new_status, severity, note, resolution):
|
|
153
|
+
"""Update an incident status or metadata."""
|
|
154
|
+
from ..itil import ITILManager
|
|
155
|
+
|
|
156
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
157
|
+
try:
|
|
158
|
+
inc = mgr.update_incident(
|
|
159
|
+
incident_id=incident_id,
|
|
160
|
+
agent=agent,
|
|
161
|
+
new_status=new_status,
|
|
162
|
+
severity=severity,
|
|
163
|
+
note=note,
|
|
164
|
+
resolution_summary=resolution,
|
|
165
|
+
)
|
|
166
|
+
console.print(
|
|
167
|
+
f"\n [green]Updated:[/green] {inc.id} -> {inc.status.value} "
|
|
168
|
+
f"({inc.severity.value})\n"
|
|
169
|
+
)
|
|
170
|
+
except ValueError as exc:
|
|
171
|
+
console.print(f"\n [red]Error:[/red] {exc}\n")
|
|
172
|
+
|
|
173
|
+
# ── itil problem ──────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
@itil.group()
|
|
176
|
+
def problem():
|
|
177
|
+
"""Problem management."""
|
|
178
|
+
|
|
179
|
+
@problem.command("create")
|
|
180
|
+
@click.option("--title", "-t", required=True, help="Problem title")
|
|
181
|
+
@click.option("--by", "managed_by", default="human", help="Managing agent")
|
|
182
|
+
@click.option("--incident", "incident_ids", multiple=True, help="Related incident ID(s)")
|
|
183
|
+
@click.option("--workaround", default="", help="Known workaround")
|
|
184
|
+
@click.option("--tag", multiple=True, help="Tags")
|
|
185
|
+
def problem_create(title, managed_by, incident_ids, workaround, tag):
|
|
186
|
+
"""Create a new problem record."""
|
|
187
|
+
from ..itil import ITILManager
|
|
188
|
+
|
|
189
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
190
|
+
prb = mgr.create_problem(
|
|
191
|
+
title=title,
|
|
192
|
+
managed_by=managed_by,
|
|
193
|
+
created_by=managed_by,
|
|
194
|
+
related_incident_ids=list(incident_ids),
|
|
195
|
+
workaround=workaround,
|
|
196
|
+
tags=list(tag),
|
|
197
|
+
)
|
|
198
|
+
console.print(
|
|
199
|
+
f"\n [green]Created:[/green] {prb.id} — {prb.title} ({prb.status.value})\n"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
@problem.command("list")
|
|
203
|
+
@click.option(
|
|
204
|
+
"--status",
|
|
205
|
+
type=click.Choice(["identified", "analyzing", "known_error", "resolved"]),
|
|
206
|
+
help="Filter by status",
|
|
207
|
+
)
|
|
208
|
+
def problem_list(status):
|
|
209
|
+
"""List problems."""
|
|
210
|
+
from ..itil import ITILManager
|
|
211
|
+
|
|
212
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
213
|
+
problems = mgr.list_problems(status=status)
|
|
214
|
+
|
|
215
|
+
if not problems:
|
|
216
|
+
console.print("\n [dim]No problems found[/dim]\n")
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
console.print(f"\n[bold]Problems ({len(problems)}):[/bold]")
|
|
220
|
+
for p in problems:
|
|
221
|
+
console.print(
|
|
222
|
+
f" [{p.id}] {p.title} ({p.status.value}) @{p.managed_by}"
|
|
223
|
+
)
|
|
224
|
+
console.print()
|
|
225
|
+
|
|
226
|
+
@problem.command("update")
|
|
227
|
+
@click.argument("problem_id")
|
|
228
|
+
@click.option("--agent", default="human", help="Agent making the update")
|
|
229
|
+
@click.option(
|
|
230
|
+
"--status", "new_status",
|
|
231
|
+
type=click.Choice(["analyzing", "known_error", "resolved"]),
|
|
232
|
+
help="New status",
|
|
233
|
+
)
|
|
234
|
+
@click.option("--root-cause", default=None, help="Root cause description")
|
|
235
|
+
@click.option("--workaround", default=None, help="Workaround")
|
|
236
|
+
@click.option("--note", default="", help="Timeline note")
|
|
237
|
+
@click.option("--create-kedb", is_flag=True, help="Create KEDB entry")
|
|
238
|
+
def problem_update(problem_id, agent, new_status, root_cause, workaround, note, create_kedb):
|
|
239
|
+
"""Update a problem record."""
|
|
240
|
+
from ..itil import ITILManager
|
|
241
|
+
|
|
242
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
243
|
+
try:
|
|
244
|
+
prb = mgr.update_problem(
|
|
245
|
+
problem_id=problem_id,
|
|
246
|
+
agent=agent,
|
|
247
|
+
new_status=new_status,
|
|
248
|
+
root_cause=root_cause,
|
|
249
|
+
workaround=workaround,
|
|
250
|
+
note=note,
|
|
251
|
+
create_kedb=create_kedb,
|
|
252
|
+
)
|
|
253
|
+
console.print(
|
|
254
|
+
f"\n [green]Updated:[/green] {prb.id} -> {prb.status.value}\n"
|
|
255
|
+
)
|
|
256
|
+
if prb.kedb_id:
|
|
257
|
+
console.print(f" [dim]KEDB entry: {prb.kedb_id}[/dim]\n")
|
|
258
|
+
except ValueError as exc:
|
|
259
|
+
console.print(f"\n [red]Error:[/red] {exc}\n")
|
|
260
|
+
|
|
261
|
+
# ── itil change ───────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
@itil.group()
|
|
264
|
+
def change():
|
|
265
|
+
"""Change management (RFC)."""
|
|
266
|
+
|
|
267
|
+
@change.command("propose")
|
|
268
|
+
@click.option("--title", "-t", required=True, help="Change title")
|
|
269
|
+
@click.option(
|
|
270
|
+
"--type", "change_type", default="normal",
|
|
271
|
+
type=click.Choice(["standard", "normal", "emergency"]),
|
|
272
|
+
help="Change type",
|
|
273
|
+
)
|
|
274
|
+
@click.option(
|
|
275
|
+
"--risk", default="medium",
|
|
276
|
+
type=click.Choice(["low", "medium", "high"]),
|
|
277
|
+
help="Risk level",
|
|
278
|
+
)
|
|
279
|
+
@click.option("--rollback", default="", help="Rollback plan")
|
|
280
|
+
@click.option("--test-plan", default="", help="Test plan")
|
|
281
|
+
@click.option("--by", "managed_by", default="human", help="Managing agent")
|
|
282
|
+
@click.option("--implementer", default=None, help="Implementing agent")
|
|
283
|
+
@click.option("--problem", "related_problem_id", default=None, help="Related problem ID")
|
|
284
|
+
@click.option("--tag", multiple=True, help="Tags")
|
|
285
|
+
def change_propose(title, change_type, risk, rollback, test_plan,
|
|
286
|
+
managed_by, implementer, related_problem_id, tag):
|
|
287
|
+
"""Propose a new change (RFC)."""
|
|
288
|
+
from ..itil import ITILManager
|
|
289
|
+
|
|
290
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
291
|
+
chg = mgr.propose_change(
|
|
292
|
+
title=title,
|
|
293
|
+
change_type=change_type,
|
|
294
|
+
risk=risk,
|
|
295
|
+
rollback_plan=rollback,
|
|
296
|
+
test_plan=test_plan,
|
|
297
|
+
managed_by=managed_by,
|
|
298
|
+
created_by=managed_by,
|
|
299
|
+
implementer=implementer,
|
|
300
|
+
related_problem_id=related_problem_id,
|
|
301
|
+
tags=list(tag),
|
|
302
|
+
)
|
|
303
|
+
console.print(
|
|
304
|
+
f"\n [green]Proposed:[/green] {chg.id} — {chg.title} "
|
|
305
|
+
f"({chg.change_type.value}, {chg.status.value})\n"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
@change.command("list")
|
|
309
|
+
@click.option(
|
|
310
|
+
"--status",
|
|
311
|
+
type=click.Choice([
|
|
312
|
+
"proposed", "reviewing", "approved", "rejected",
|
|
313
|
+
"implementing", "deployed", "verified", "failed", "closed",
|
|
314
|
+
]),
|
|
315
|
+
help="Filter by status",
|
|
316
|
+
)
|
|
317
|
+
def change_list(status):
|
|
318
|
+
"""List changes."""
|
|
319
|
+
from ..itil import ITILManager
|
|
320
|
+
|
|
321
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
322
|
+
changes = mgr.list_changes(status=status)
|
|
323
|
+
|
|
324
|
+
if not changes:
|
|
325
|
+
console.print("\n [dim]No changes found[/dim]\n")
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
console.print(f"\n[bold]Changes ({len(changes)}):[/bold]")
|
|
329
|
+
for c in changes:
|
|
330
|
+
console.print(
|
|
331
|
+
f" [{c.id}] {c.title} ({c.status.value}, "
|
|
332
|
+
f"{c.change_type.value}) @{c.managed_by}"
|
|
333
|
+
)
|
|
334
|
+
console.print()
|
|
335
|
+
|
|
336
|
+
@change.command("update")
|
|
337
|
+
@click.argument("change_id")
|
|
338
|
+
@click.option("--agent", default="human", help="Agent making the update")
|
|
339
|
+
@click.option(
|
|
340
|
+
"--status", "new_status",
|
|
341
|
+
type=click.Choice([
|
|
342
|
+
"reviewing", "approved", "rejected", "implementing",
|
|
343
|
+
"deployed", "verified", "failed", "closed",
|
|
344
|
+
]),
|
|
345
|
+
help="New status",
|
|
346
|
+
)
|
|
347
|
+
@click.option("--note", default="", help="Timeline note")
|
|
348
|
+
def change_update(change_id, agent, new_status, note):
|
|
349
|
+
"""Update a change status."""
|
|
350
|
+
from ..itil import ITILManager
|
|
351
|
+
|
|
352
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
353
|
+
try:
|
|
354
|
+
chg = mgr.update_change(
|
|
355
|
+
change_id=change_id,
|
|
356
|
+
agent=agent,
|
|
357
|
+
new_status=new_status,
|
|
358
|
+
note=note,
|
|
359
|
+
)
|
|
360
|
+
console.print(
|
|
361
|
+
f"\n [green]Updated:[/green] {chg.id} -> {chg.status.value}\n"
|
|
362
|
+
)
|
|
363
|
+
except ValueError as exc:
|
|
364
|
+
console.print(f"\n [red]Error:[/red] {exc}\n")
|
|
365
|
+
|
|
366
|
+
# ── itil cab ──────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
@itil.group()
|
|
369
|
+
def cab():
|
|
370
|
+
"""Change Advisory Board voting."""
|
|
371
|
+
|
|
372
|
+
@cab.command("vote")
|
|
373
|
+
@click.argument("change_id")
|
|
374
|
+
@click.option("--agent", default="human", help="Voting agent")
|
|
375
|
+
@click.option(
|
|
376
|
+
"--decision", default="approved",
|
|
377
|
+
type=click.Choice(["approved", "rejected", "abstain"]),
|
|
378
|
+
help="Vote decision",
|
|
379
|
+
)
|
|
380
|
+
@click.option("--conditions", default="", help="Approval conditions")
|
|
381
|
+
def cab_vote(change_id, agent, decision, conditions):
|
|
382
|
+
"""Submit a CAB vote for a change."""
|
|
383
|
+
from ..itil import ITILManager
|
|
384
|
+
|
|
385
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
386
|
+
vote = mgr.submit_cab_vote(
|
|
387
|
+
change_id=change_id,
|
|
388
|
+
agent=agent,
|
|
389
|
+
decision=decision,
|
|
390
|
+
conditions=conditions,
|
|
391
|
+
)
|
|
392
|
+
console.print(
|
|
393
|
+
f"\n [green]Voted:[/green] {vote.agent} -> {vote.decision.value} "
|
|
394
|
+
f"on {vote.change_id}\n"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# ── itil kedb ─────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
@itil.group()
|
|
400
|
+
def kedb():
|
|
401
|
+
"""Known Error Database."""
|
|
402
|
+
|
|
403
|
+
@kedb.command("search")
|
|
404
|
+
@click.argument("query")
|
|
405
|
+
def kedb_search(query):
|
|
406
|
+
"""Search the Known Error Database."""
|
|
407
|
+
from ..itil import ITILManager
|
|
408
|
+
|
|
409
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
410
|
+
results = mgr.search_kedb(query)
|
|
411
|
+
|
|
412
|
+
if not results:
|
|
413
|
+
console.print(f"\n [dim]No KEDB entries matching '{query}'[/dim]\n")
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
console.print(f"\n[bold]KEDB Results ({len(results)}):[/bold]")
|
|
417
|
+
for e in results:
|
|
418
|
+
console.print(f" [{e.id}] {e.title}")
|
|
419
|
+
if e.workaround:
|
|
420
|
+
console.print(f" [dim]Workaround: {e.workaround[:100]}[/dim]")
|
|
421
|
+
if e.root_cause:
|
|
422
|
+
console.print(f" [dim]Root cause: {e.root_cause[:100]}[/dim]")
|
|
423
|
+
console.print()
|
|
424
|
+
|
|
425
|
+
# ── itil board ────────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
@itil.command("board")
|
|
428
|
+
def itil_board():
|
|
429
|
+
"""Generate ITIL-BOARD.md overview."""
|
|
430
|
+
from ..itil import ITILManager
|
|
431
|
+
|
|
432
|
+
mgr = ITILManager(Path(SHARED_ROOT).expanduser())
|
|
433
|
+
path = mgr.write_board_md()
|
|
434
|
+
console.print(f"\n [green]Generated:[/green] {path}\n")
|
|
@@ -20,7 +20,9 @@ def register_peer_commands(main: click.Group) -> None:
|
|
|
20
20
|
|
|
21
21
|
@main.group()
|
|
22
22
|
def peer():
|
|
23
|
-
"""Peer management — discover, add, and manage trusted contacts.
|
|
23
|
+
"""Peer management — discover, add, and manage trusted contacts.
|
|
24
|
+
|
|
25
|
+
Identity-layer peers (PGP keys, trust). For transport routing, see 'peers'."""
|
|
24
26
|
|
|
25
27
|
@peer.command("add")
|
|
26
28
|
@click.option("--card", "card_path", type=click.Path(exists=True), help="Import from identity card.")
|
|
@@ -17,7 +17,9 @@ def register_peers_dir_commands(main: click.Group) -> None:
|
|
|
17
17
|
|
|
18
18
|
@main.group("peers")
|
|
19
19
|
def peers_dir():
|
|
20
|
-
"""Peer transport directory — routing addresses for the mesh.
|
|
20
|
+
"""Peer transport directory — routing addresses for the mesh.
|
|
21
|
+
|
|
22
|
+
SKComm transport endpoints. For identity/trust peers, see 'peer'."""
|
|
21
23
|
|
|
22
24
|
@peers_dir.command("list")
|
|
23
25
|
@click.option("--home", "sk_home", default=AGENT_HOME, type=click.Path())
|
|
@@ -33,8 +33,12 @@ def register_preflight_commands(main: click.Group) -> None:
|
|
|
33
33
|
skcapstone preflight --json-out
|
|
34
34
|
"""
|
|
35
35
|
import json
|
|
36
|
+
from .. import ensure_skeleton
|
|
36
37
|
from ..preflight import PreflightChecker
|
|
37
38
|
|
|
39
|
+
# Ensure all expected directories exist before checking
|
|
40
|
+
ensure_skeleton()
|
|
41
|
+
|
|
38
42
|
checker = PreflightChecker(home=Path(home).expanduser())
|
|
39
43
|
summary = checker.run_all()
|
|
40
44
|
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import logging
|
|
6
7
|
import sys
|
|
8
|
+
import urllib.request
|
|
9
|
+
from typing import Optional
|
|
7
10
|
|
|
8
11
|
import click
|
|
12
|
+
import yaml
|
|
9
13
|
|
|
10
14
|
from ._common import AGENT_HOME, console
|
|
11
15
|
from ..registry_client import get_registry_client
|
|
@@ -13,18 +17,65 @@ from ..registry_client import get_registry_client
|
|
|
13
17
|
from rich.panel import Panel
|
|
14
18
|
from rich.table import Table
|
|
15
19
|
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Raw catalog.yaml from the skskills GitHub repo (always fresh)
|
|
23
|
+
_GITHUB_CATALOG_URL = (
|
|
24
|
+
"https://raw.githubusercontent.com/smilinTux/skskills/main/catalog.yaml"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _fetch_github_catalog(query: str = "") -> Optional[list[dict]]:
|
|
29
|
+
"""Fetch catalog.yaml from the skskills GitHub repo.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
List of skill entry dicts, or None on failure.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
req = urllib.request.Request(_GITHUB_CATALOG_URL, headers={"User-Agent": "skcapstone"})
|
|
36
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
37
|
+
raw = yaml.safe_load(resp.read().decode("utf-8"))
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
logger.debug("GitHub catalog fetch failed: %s", exc)
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
entries = []
|
|
43
|
+
q = query.lower()
|
|
44
|
+
for item in raw.get("skills", []):
|
|
45
|
+
name = item.get("name", "")
|
|
46
|
+
desc = item.get("description", "").strip()
|
|
47
|
+
tags = item.get("tags", [])
|
|
48
|
+
|
|
49
|
+
if q and not (
|
|
50
|
+
q in name.lower()
|
|
51
|
+
or q in desc.lower()
|
|
52
|
+
or any(q in t.lower() for t in tags)
|
|
53
|
+
):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
entries.append({
|
|
57
|
+
"name": name,
|
|
58
|
+
"description": desc,
|
|
59
|
+
"tags": tags,
|
|
60
|
+
"category": item.get("category", ""),
|
|
61
|
+
"pip": item.get("pip", ""),
|
|
62
|
+
"git": item.get("git", ""),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return entries
|
|
66
|
+
|
|
16
67
|
|
|
17
68
|
def register_skills_commands(main: click.Group) -> None:
|
|
18
69
|
"""Register the skills command group."""
|
|
19
70
|
|
|
20
71
|
@main.group()
|
|
21
72
|
def skills():
|
|
22
|
-
"""
|
|
73
|
+
"""Skills registry — discover and install agent skills.
|
|
23
74
|
|
|
24
|
-
|
|
25
|
-
|
|
75
|
+
Fetches the latest skill catalog from GitHub. Falls back to the
|
|
76
|
+
locally installed catalog if offline.
|
|
26
77
|
|
|
27
|
-
Set SKSKILLS_REGISTRY_URL to override
|
|
78
|
+
Set SKSKILLS_REGISTRY_URL to override with a custom registry server.
|
|
28
79
|
"""
|
|
29
80
|
|
|
30
81
|
@skills.command("list")
|
|
@@ -36,11 +87,12 @@ def register_skills_commands(main: click.Group) -> None:
|
|
|
36
87
|
help="Override the skills registry URL.",
|
|
37
88
|
)
|
|
38
89
|
@click.option("--json", "json_out", is_flag=True, help="Output raw JSON.")
|
|
39
|
-
|
|
40
|
-
|
|
90
|
+
@click.option("--offline", is_flag=True, help="Use local catalog only (no network).")
|
|
91
|
+
def skills_list(query: str, registry: str | None, json_out: bool, offline: bool) -> None:
|
|
92
|
+
"""List skills available in the catalog.
|
|
41
93
|
|
|
42
|
-
|
|
43
|
-
|
|
94
|
+
Pulls the latest catalog from the skskills GitHub repo.
|
|
95
|
+
Falls back to local catalog if offline or fetch fails.
|
|
44
96
|
|
|
45
97
|
Examples:
|
|
46
98
|
|
|
@@ -49,20 +101,58 @@ def register_skills_commands(main: click.Group) -> None:
|
|
|
49
101
|
skcapstone skills list --query syncthing
|
|
50
102
|
|
|
51
103
|
skcapstone skills list --query identity --json
|
|
52
|
-
"""
|
|
53
|
-
client = get_registry_client(registry)
|
|
54
|
-
if client is None:
|
|
55
|
-
console.print(
|
|
56
|
-
"[bold red]skskills not installed.[/] "
|
|
57
|
-
"Run: pip install skskills"
|
|
58
|
-
)
|
|
59
|
-
sys.exit(1)
|
|
60
104
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
105
|
+
skcapstone skills list --offline
|
|
106
|
+
"""
|
|
107
|
+
skill_entries = None
|
|
108
|
+
source = "github"
|
|
109
|
+
|
|
110
|
+
# 1. Try custom registry server if configured
|
|
111
|
+
if registry:
|
|
112
|
+
client = get_registry_client(registry)
|
|
113
|
+
if client is not None:
|
|
114
|
+
try:
|
|
115
|
+
skill_entries = client.search(query) if query else client.list_skills()
|
|
116
|
+
source = "remote"
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
# 2. Try GitHub raw catalog (always fresh, no server needed)
|
|
121
|
+
if skill_entries is None and not offline:
|
|
122
|
+
skill_entries = _fetch_github_catalog(query)
|
|
123
|
+
source = "github"
|
|
124
|
+
|
|
125
|
+
# 3. Fall back to local catalog (bundled with skskills package)
|
|
126
|
+
if skill_entries is None:
|
|
127
|
+
try:
|
|
128
|
+
from skskills.catalog import SkillCatalog
|
|
129
|
+
|
|
130
|
+
catalog = SkillCatalog()
|
|
131
|
+
if query:
|
|
132
|
+
entries = catalog.search(query)
|
|
133
|
+
else:
|
|
134
|
+
entries = catalog.list_all()
|
|
135
|
+
skill_entries = [
|
|
136
|
+
{
|
|
137
|
+
"name": e.name,
|
|
138
|
+
"description": e.description,
|
|
139
|
+
"tags": e.tags,
|
|
140
|
+
"category": e.category,
|
|
141
|
+
"pip": e.pip,
|
|
142
|
+
"git": e.git,
|
|
143
|
+
}
|
|
144
|
+
for e in entries
|
|
145
|
+
]
|
|
146
|
+
source = "local"
|
|
147
|
+
except ImportError:
|
|
148
|
+
console.print(
|
|
149
|
+
"[bold red]skskills not installed and GitHub unreachable.[/] "
|
|
150
|
+
"Run: pip install skskills"
|
|
151
|
+
)
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
console.print(f"[bold red]Catalog error:[/] {exc}")
|
|
155
|
+
sys.exit(1)
|
|
66
156
|
|
|
67
157
|
if json_out:
|
|
68
158
|
click.echo(json.dumps(skill_entries, indent=2))
|
|
@@ -73,26 +163,32 @@ def register_skills_commands(main: click.Group) -> None:
|
|
|
73
163
|
console.print(f"\n [dim]No skills found{suffix}.[/]\n")
|
|
74
164
|
return
|
|
75
165
|
|
|
166
|
+
source_labels = {
|
|
167
|
+
"github": "",
|
|
168
|
+
"remote": " [dim](registry)[/]",
|
|
169
|
+
"local": " [dim](local — offline)[/]",
|
|
170
|
+
}
|
|
76
171
|
label = f"[bold]{len(skill_entries)}[/] skill(s)"
|
|
77
172
|
if query:
|
|
78
173
|
label += f" matching [cyan]'{query}'[/]"
|
|
174
|
+
label += source_labels.get(source, "")
|
|
79
175
|
|
|
80
176
|
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
|
|
81
177
|
table.add_column("Name", style="cyan")
|
|
82
|
-
table.add_column("
|
|
178
|
+
table.add_column("Category", style="dim")
|
|
83
179
|
table.add_column("Description")
|
|
84
180
|
table.add_column("Tags", style="dim")
|
|
85
181
|
|
|
86
182
|
for s in skill_entries:
|
|
87
183
|
table.add_row(
|
|
88
184
|
s.get("name", ""),
|
|
89
|
-
s.get("
|
|
185
|
+
s.get("category", ""),
|
|
90
186
|
s.get("description", ""),
|
|
91
187
|
", ".join(s.get("tags", [])),
|
|
92
188
|
)
|
|
93
189
|
|
|
94
190
|
console.print()
|
|
95
|
-
console.print(Panel(label, title="Skills
|
|
191
|
+
console.print(Panel(label, title="Skills Catalog", border_style="bright_blue"))
|
|
96
192
|
console.print(table)
|
|
97
193
|
console.print()
|
|
98
194
|
|