@heytherevibin/skillforge 0.10.1 → 0.11.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 (58) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/CONTRIBUTING.md +5 -3
  3. package/README.md +37 -345
  4. package/RELEASING.md +7 -6
  5. package/STRATEGY.md +2 -2
  6. package/bin/cli.js +297 -52
  7. package/ci/test-user-env-profile.cjs +65 -0
  8. package/docs/README.md +14 -0
  9. package/docs/architecture-and-data.md +90 -0
  10. package/docs/cli-reference.md +57 -0
  11. package/docs/environment-and-configuration.md +76 -0
  12. package/docs/getting-started.md +88 -0
  13. package/docs/mcp-integration.md +75 -0
  14. package/docs/troubleshooting.md +50 -0
  15. package/lib/templates/claude-code-skillforge-global.md +3 -3
  16. package/lib/templates/cursor-skillforge-global.md +6 -2
  17. package/lib/user-env-profile.js +141 -0
  18. package/package.json +3 -2
  19. package/python/app/agent_cli.py +334 -0
  20. package/python/app/explain_route.py +170 -0
  21. package/python/app/health_cli.py +13 -0
  22. package/python/app/main.py +131 -48
  23. package/python/app/materialize.py +150 -68
  24. package/python/app/mcp_contract.py +2 -1
  25. package/python/app/mcp_operator.py +252 -0
  26. package/python/app/mcp_server.py +290 -118
  27. package/python/app/npm_pkg_version.py +38 -0
  28. package/python/app/pick_diversify.py +51 -0
  29. package/python/app/replay_cli.py +145 -0
  30. package/python/app/route_cli.py +251 -87
  31. package/python/app/route_cli_pick.py +35 -0
  32. package/python/app/route_policies.py +18 -3
  33. package/python/app/route_quality.py +70 -1
  34. package/python/app/router_llm.py +85 -0
  35. package/python/app/router_mode.py +21 -0
  36. package/python/app/routing_signals.py +7 -1
  37. package/python/app/skill_manifest.py +67 -0
  38. package/python/app/skills_author_cli.py +117 -0
  39. package/python/app/tips_cli.py +37 -0
  40. package/python/app/tools_cli.py +276 -0
  41. package/python/fixtures/route_eval/smoke.json +5 -0
  42. package/python/requirements.txt +1 -0
  43. package/python/tests/test_capabilities_bundle.py +33 -0
  44. package/python/tests/test_materialize_hosts.py +108 -0
  45. package/python/tests/test_mcp_contract.py +1 -1
  46. package/python/tests/test_mcp_initialize_clientinfo.py +26 -0
  47. package/python/tests/test_mcp_operator.py +84 -0
  48. package/python/tests/test_npm_pkg_version.py +21 -0
  49. package/python/tests/test_pick_diversify.py +47 -0
  50. package/python/tests/test_replay_cli.py +31 -0
  51. package/python/tests/test_route_cli_pick.py +25 -0
  52. package/python/tests/test_route_policies.py +29 -0
  53. package/python/tests/test_route_quality.py +72 -0
  54. package/python/tests/test_router_llm.py +63 -0
  55. package/python/tests/test_router_mode_env.py +21 -0
  56. package/python/tests/test_routing_signals.py +20 -0
  57. package/python/tests/test_skill_manifest.py +48 -0
  58. package/python/tests/test_tools_cli.py +69 -0
@@ -7,6 +7,7 @@ Schema **1.4** adds optional ``context_redaction`` (hit counts when scrubbing is
7
7
  Schema **1.5** adds optional ``route_quality`` (shortlist margins, hybrid diagnostics, policy/session).
8
8
  Schema **1.6** adds optional ``feedback_effect`` (per-pick learned weights / thumbs / uses used in ranking).
9
9
  Schema **1.7** adds optional ``routing_overlay`` (project exclude/boost/notes audit for embedding shortlist).
10
+ Schema **1.8** bumps embedded ``route_quality`` to **route_quality/2** (ambiguous shortlist hints, diversify meta).
10
11
  """
11
12
  from __future__ import annotations
12
13
 
@@ -21,7 +22,7 @@ class _SkillBody(Protocol):
21
22
  body: str
22
23
 
23
24
 
24
- MCP_RESPONSE_SCHEMA_VERSION = "1.7"
25
+ MCP_RESPONSE_SCHEMA_VERSION = "1.8"
25
26
 
26
27
 
27
28
  def build_route_skills_meta(
@@ -0,0 +1,252 @@
1
+ """Shared logic for MCP operator/observability tools (read-only SQLite + env snapshots)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import math
6
+ import os
7
+ import sqlite3
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from app.main import Router
12
+ from app.mcp_contract import MCP_RESPONSE_SCHEMA_VERSION
13
+ from app.npm_pkg_version import published_package_version
14
+ from app.project_index import ensure_project_index_schema
15
+ from app.routing_signals import host_pick_max_candidates
16
+
17
+ # MCP events_recent caps (keep LLM-visible text bounded; `_meta.rows` retains JSON payloads).
18
+ EVENTS_META_ROW_CAP = 100
19
+ EVENTS_MARKDOWN_PREVIEW_MAX_LINES = 150
20
+
21
+ # Keep insertion order aligned with MCPServer.handle_tools_list (`app/mcp_server.py`).
22
+ MCP_PUBLISHED_TOOL_NAMES: tuple[str, ...] = (
23
+ "route_skills",
24
+ "search_skills",
25
+ "explain_route",
26
+ "get_skill",
27
+ "list_skills",
28
+ "skill_feedback",
29
+ "disable_skill",
30
+ "skill_referenced",
31
+ "materialize_project",
32
+ "skillforge_bootstrap",
33
+ "capabilities",
34
+ "get_router_status",
35
+ "project_index_status",
36
+ "weights_snapshot",
37
+ "events_recent",
38
+ )
39
+
40
+
41
+ def _truthy(env_name: str, default: str = "0") -> bool:
42
+ return os.getenv(env_name, default).strip().lower() not in ("0", "false", "no", "")
43
+
44
+
45
+ def build_router_status_dict(router: Router | None, *, skill_count: int) -> dict[str, Any]:
46
+ """JSON-serializable operator snapshot for get_router_status."""
47
+ from app import main as m
48
+
49
+ r = router
50
+ rl = getattr(r, "router_llm", None) if r else None
51
+ backend = "none"
52
+ if rl is not None:
53
+ backend = getattr(rl, "backend_name", "unknown")
54
+ anth_ok = bool(r and r.anthropic is not None)
55
+ return {
56
+ "skillforge_router_mode": m.SKILLFORGE_ROUTER_MODE,
57
+ "top_k_candidates": m.TOP_K_CANDIDATES,
58
+ "max_active_skills": m.MAX_ACTIVE_SKILLS,
59
+ "reroute_threshold": m.REROUTE_THRESHOLD,
60
+ "embed_model": m.EMBED_MODEL,
61
+ "router_model": m.ROUTER_MODEL,
62
+ "anthropic_available": anth_ok,
63
+ "router_llm_backend": backend,
64
+ "router_llm_active": bool(rl is not None),
65
+ "context_mode": r.context_mode if r else m.SKILLFORGE_CONTEXT_MODE,
66
+ "router_hybrid": r._hybrid_mode if r else m.ROUTER_HYBRID_MODE,
67
+ "router_hybrid_alpha": round(m.ROUTER_HYBRID_ALPHA, 4),
68
+ "haiku_rerank_enabled": _truthy("SKILLFORGE_HAIKU_RERANK", "0"),
69
+ "skills_loaded_count": skill_count,
70
+ "host_pick_max": host_pick_max_candidates(top_k_cap=m.TOP_K_CANDIDATES),
71
+ "pick_diversify_enabled": _truthy("SKILLFORGE_PICK_DIVERSIFY", "0"),
72
+ "pick_max_per_source": os.getenv("SKILLFORGE_PICK_MAX_PER_SOURCE", "2").strip(),
73
+ "route_ambiguity_disabled": _truthy("SKILLFORGE_ROUTE_AMBIGUITY_DISABLE", "0"),
74
+ "mcp_server_semver": published_package_version(),
75
+ }
76
+
77
+
78
+ def format_capabilities_markdown(bundle: dict[str, Any]) -> str:
79
+ return (
80
+ "# Skillforge — capabilities (session bootstrap)\n\n"
81
+ "Single JSON bundle: MCP response schema, package semver, tool names, progressive loading hints, "
82
+ "and `router_snapshot` (same payload shape as `get_router_status`).\n\n```json\n"
83
+ f"{json.dumps(bundle, indent=2)}\n```"
84
+ )
85
+
86
+
87
+ def build_capabilities_bundle(router: Router | None, *, skill_count: int) -> dict[str, Any]:
88
+ """Session-start bundle: versioning + advertised tools + router env snapshot."""
89
+ return {
90
+ "bundle_version": "1",
91
+ "mcp_response_schema_version": MCP_RESPONSE_SCHEMA_VERSION,
92
+ "package_semver": published_package_version(),
93
+ "mcp_tools": list(MCP_PUBLISHED_TOOL_NAMES),
94
+ "progressive_loading": {
95
+ "get_skill_formats": ["card", "summary", "full"],
96
+ "note": "`card`: routing-card text only; see `get_skill.format`. `summary|full`: SKILL.md excerpts.",
97
+ },
98
+ "replay_cli": {"command": "skillforge replay [--session-id=…] [--user=…] [--json]"},
99
+ "user_env_profile": {
100
+ "file": "~/.skillforge/env",
101
+ "path_command": "skillforge config path",
102
+ "init_command": "skillforge config init [--force]",
103
+ "validate_command": "skillforge config validate",
104
+ },
105
+ "tools_command": {"command": "skillforge tools … [--json] · skillforge tools -h (MCP parity subcommands)"},
106
+ "route_cli_hints": {
107
+ "interactive_tty": "-i or SKILLFORGE_ROUTE_INTERACTIVE=1 after host-mode shortlist",
108
+ "json_stdout": "--json (phases host_shortlist_prompt · host_shortlist_static · context · explain_only)",
109
+ "explain_flags": "--explain · --explain-only",
110
+ },
111
+ "tips_command": "skillforge tips",
112
+ "standalone_agent": {"command": "skillforge agent [--prompt TEXT] (--base-url, --model)"},
113
+ "manifest": {
114
+ "strict_catalog_env": "SKILLFORGE_SKILL_MANIFEST_STRICT",
115
+ "lint_command": "skillforge skills lint [paths]",
116
+ },
117
+ "router_snapshot": build_router_status_dict(router, skill_count=skill_count),
118
+ }
119
+
120
+
121
+ def project_index_status_dict(con: sqlite3.Connection) -> dict[str, Any]:
122
+ ensure_project_index_schema(con)
123
+ cur = con.execute("SELECT COUNT(*) FROM project_chunks")
124
+ chunk_count = int(cur.fetchone()[0])
125
+ cur = con.execute("SELECT COUNT(DISTINCT path) FROM project_chunks")
126
+ file_count = int(cur.fetchone()[0])
127
+ meta: dict[str, Any] = {
128
+ "chunk_count": chunk_count,
129
+ "distinct_paths": file_count,
130
+ }
131
+ cur = con.execute("SELECT key, value FROM project_index_meta")
132
+ raw_meta = {row[0]: row[1] for row in cur.fetchall()}
133
+ embed_model = raw_meta.get("embed_model")
134
+ if embed_model:
135
+ meta["embed_model"] = embed_model
136
+ edim = raw_meta.get("embedding_dim")
137
+ if edim:
138
+ meta["embedding_dim"] = edim
139
+ ts = raw_meta.get("last_index_ts")
140
+ if ts:
141
+ try:
142
+ ft = float(ts)
143
+ meta["last_index_ts"] = ft if math.isfinite(ft) else None
144
+ except ValueError:
145
+ meta["last_index_ts"] = None
146
+ stats_raw = raw_meta.get("last_index_stats")
147
+ if stats_raw:
148
+ try:
149
+ meta["last_index_stats"] = json.loads(stats_raw)
150
+ except json.JSONDecodeError:
151
+ meta["last_index_stats"] = None
152
+ return meta
153
+
154
+
155
+ def events_recent_rows(
156
+ con: sqlite3.Connection,
157
+ *,
158
+ limit: int,
159
+ user_id: str = "",
160
+ event_type: str | None = None,
161
+ ) -> list[dict[str, Any]]:
162
+ lim = max(1, min(int(limit), 500))
163
+ uid = user_id.strip()
164
+ if event_type and str(event_type).strip():
165
+ et = str(event_type).strip()
166
+ cur = con.execute(
167
+ "SELECT ts, session_id, event_type, payload FROM events "
168
+ "WHERE user_id = ? AND event_type = ? ORDER BY ts DESC LIMIT ?",
169
+ (uid, et, lim),
170
+ )
171
+ else:
172
+ cur = con.execute(
173
+ "SELECT ts, session_id, event_type, payload FROM events "
174
+ "WHERE user_id = ? ORDER BY ts DESC LIMIT ?",
175
+ (uid, lim),
176
+ )
177
+ rows: list[dict[str, Any]] = []
178
+ for ts, sid, et_, payload in cur.fetchall():
179
+ row: dict[str, Any] = {
180
+ "ts": float(ts) if ts is not None else None,
181
+ "session_id": sid,
182
+ "event_type": et_,
183
+ }
184
+ if payload:
185
+ try:
186
+ row["payload"] = json.loads(payload)
187
+ except json.JSONDecodeError:
188
+ row["payload"] = payload
189
+ else:
190
+ row["payload"] = None
191
+ rows.append(row)
192
+ return rows
193
+
194
+
195
+ def format_router_status_markdown(snapshot: dict[str, Any]) -> str:
196
+ lines = ["# Skillforge — router status", ""]
197
+ for k, v in snapshot.items():
198
+ lines.append(f"- **{k}:** `{v}`")
199
+ return "\n".join(lines)
200
+
201
+
202
+ def format_project_index_markdown(snapshot: dict[str, Any], root: Path) -> str:
203
+ lines = [
204
+ "# Skillforge — project index",
205
+ "",
206
+ f"**Orchestrator DB root:** `{root}`",
207
+ f"**Chunk rows:** `{snapshot.get('chunk_count', 0)}`",
208
+ f"**Distinct paths:** `{snapshot.get('distinct_paths', 0)}`",
209
+ ]
210
+ if snapshot.get("embed_model"):
211
+ lines.append(f"**Embed model (at index):** `{snapshot['embed_model']}`")
212
+ if snapshot.get("embedding_dim"):
213
+ lines.append(f"**Embedding dim:** `{snapshot['embedding_dim']}`")
214
+ if snapshot.get("last_index_ts") is not None:
215
+ lines.append(f"**Last index unix ts:** `{snapshot['last_index_ts']}`")
216
+ stats = snapshot.get("last_index_stats")
217
+ if isinstance(stats, dict) and stats:
218
+ lines.extend(["", "## Last index run", "```json", json.dumps(stats, indent=2), "```"])
219
+ return "\n".join(lines)
220
+
221
+
222
+ def format_events_markdown(
223
+ rows: list[dict[str, Any]],
224
+ *,
225
+ preview_max_lines: int = EVENTS_MARKDOWN_PREVIEW_MAX_LINES,
226
+ ) -> str:
227
+ lines = [f"# Skillforge — recent events ({len(rows)} rows)", ""]
228
+ cap_lines = max(1, preview_max_lines)
229
+ display = rows[:cap_lines]
230
+ for r in display:
231
+ et = r.get("event_type") or "?"
232
+ sid = (r.get("session_id") or "-")[:12]
233
+ ts = r.get("ts")
234
+ preview = ""
235
+ p = r.get("payload")
236
+ if isinstance(p, dict):
237
+ if et == "route":
238
+ preview = str(p.get("picked") or p.get("picked_names") or "")[:160]
239
+ elif et == "host_shortlist":
240
+ preview = f"candidates={len(p.get('candidates') or [])}"
241
+ elif et == "feedback":
242
+ preview = f"skill={p.get('skill')}"
243
+ else:
244
+ preview = json.dumps(p)[:120]
245
+ lines.append(f"- **{ts}** `{et}` session=`{sid}` {preview}".strip())
246
+ if len(rows) > len(display):
247
+ lines.append("")
248
+ lines.append(
249
+ f"_Markdown preview truncated ({len(display)} of {len(rows)} rows). "
250
+ f"Use `_meta.rows` (cap {EVENTS_META_ROW_CAP} JSON rows) for structured payloads._"
251
+ )
252
+ return "\n".join(lines)