@smilintux/skcapstone 0.4.5 → 0.4.7

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 (44) 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 +856 -0
  15. package/scripts/proxy-monitor.sh +89 -0
  16. package/scripts/skgateway.mjs +856 -0
  17. package/scripts/telegram-catchup-all.sh +136 -0
  18. package/src/skcapstone/__init__.py +1 -1
  19. package/src/skcapstone/blueprint_registry.py +78 -0
  20. package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
  21. package/src/skcapstone/cli/__init__.py +2 -0
  22. package/src/skcapstone/cli/daemon.py +116 -41
  23. package/src/skcapstone/cli/itil.py +434 -0
  24. package/src/skcapstone/cli/skills_cmd.py +90 -26
  25. package/src/skcapstone/cli/soul.py +47 -24
  26. package/src/skcapstone/consciousness_config.py +27 -0
  27. package/src/skcapstone/coordination.py +1 -0
  28. package/src/skcapstone/daemon.py +47 -20
  29. package/src/skcapstone/dreaming.py +761 -0
  30. package/src/skcapstone/fuse_mount.py +21 -13
  31. package/src/skcapstone/heartbeat.py +33 -29
  32. package/src/skcapstone/itil.py +1104 -0
  33. package/src/skcapstone/launchd.py +426 -0
  34. package/src/skcapstone/mcp_server.py +258 -0
  35. package/src/skcapstone/mcp_tools/__init__.py +2 -0
  36. package/src/skcapstone/mcp_tools/gtd_tools.py +1 -1
  37. package/src/skcapstone/mcp_tools/itil_tools.py +657 -0
  38. package/src/skcapstone/mcp_tools/notification_tools.py +12 -11
  39. package/src/skcapstone/notifications.py +40 -27
  40. package/src/skcapstone/onboard.py +130 -10
  41. package/src/skcapstone/scheduled_tasks.py +107 -0
  42. package/src/skcapstone/service_health.py +81 -2
  43. package/src/skcapstone/soul.py +19 -0
  44. package/src/skcapstone/systemd.py +17 -0
@@ -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")
@@ -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
- """Remote skills registry — discover and install agent skills.
73
+ """Skills registry — discover and install agent skills.
23
74
 
24
- Browse skills at skills.smilintux.org, search by name or tag,
25
- and install skill packages into your local agent namespace.
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 the default registry.
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
- def skills_list(query: str, registry: str | None, json_out: bool) -> None:
40
- """List skills available in the remote registry.
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
- Without --query all skills are shown. With --query only skills
43
- matching the name, description, or tags are returned.
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,19 +101,28 @@ 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
104
+
105
+ skcapstone skills list --offline
52
106
  """
53
- # Try remote registry first, fall back to local catalog
54
107
  skill_entries = None
55
- source = "remote"
56
-
57
- client = get_registry_client(registry)
58
- if client is not None:
59
- try:
60
- skill_entries = client.search(query) if query else client.list_skills()
61
- except Exception:
62
- pass # fall through to local catalog
63
-
64
- # Fall back to local catalog (bundled with skskills)
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)
65
126
  if skill_entries is None:
66
127
  try:
67
128
  from skskills.catalog import SkillCatalog
@@ -74,7 +135,6 @@ def register_skills_commands(main: click.Group) -> None:
74
135
  skill_entries = [
75
136
  {
76
137
  "name": e.name,
77
- "version": "",
78
138
  "description": e.description,
79
139
  "tags": e.tags,
80
140
  "category": e.category,
@@ -83,10 +143,10 @@ def register_skills_commands(main: click.Group) -> None:
83
143
  }
84
144
  for e in entries
85
145
  ]
86
- source = "catalog"
146
+ source = "local"
87
147
  except ImportError:
88
148
  console.print(
89
- "[bold red]skskills not installed.[/] "
149
+ "[bold red]skskills not installed and GitHub unreachable.[/] "
90
150
  "Run: pip install skskills"
91
151
  )
92
152
  sys.exit(1)
@@ -103,11 +163,15 @@ def register_skills_commands(main: click.Group) -> None:
103
163
  console.print(f"\n [dim]No skills found{suffix}.[/]\n")
104
164
  return
105
165
 
166
+ source_labels = {
167
+ "github": "",
168
+ "remote": " [dim](registry)[/]",
169
+ "local": " [dim](local — offline)[/]",
170
+ }
106
171
  label = f"[bold]{len(skill_entries)}[/] skill(s)"
107
172
  if query:
108
173
  label += f" matching [cyan]'{query}'[/]"
109
- if source == "catalog":
110
- label += " [dim](local catalog)[/]"
174
+ label += source_labels.get(source, "")
111
175
 
112
176
  table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
113
177
  table.add_column("Name", style="cyan")
@@ -118,13 +182,13 @@ def register_skills_commands(main: click.Group) -> None:
118
182
  for s in skill_entries:
119
183
  table.add_row(
120
184
  s.get("name", ""),
121
- s.get("category", s.get("version", "")),
185
+ s.get("category", ""),
122
186
  s.get("description", ""),
123
187
  ", ".join(s.get("tags", [])),
124
188
  )
125
189
 
126
190
  console.print()
127
- console.print(Panel(label, title="Skills Registry", border_style="bright_blue"))
191
+ console.print(Panel(label, title="Skills Catalog", border_style="bright_blue"))
128
192
  console.print(table)
129
193
  console.print()
130
194