@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.
- package/CHANGELOG.md +49 -0
- package/CONTRIBUTING.md +5 -3
- package/README.md +37 -345
- package/RELEASING.md +7 -6
- package/STRATEGY.md +2 -2
- package/bin/cli.js +297 -52
- package/ci/test-user-env-profile.cjs +65 -0
- package/docs/README.md +14 -0
- package/docs/architecture-and-data.md +90 -0
- package/docs/cli-reference.md +57 -0
- package/docs/environment-and-configuration.md +76 -0
- package/docs/getting-started.md +88 -0
- package/docs/mcp-integration.md +75 -0
- package/docs/troubleshooting.md +50 -0
- package/lib/templates/claude-code-skillforge-global.md +3 -3
- package/lib/templates/cursor-skillforge-global.md +6 -2
- package/lib/user-env-profile.js +141 -0
- package/package.json +3 -2
- package/python/app/agent_cli.py +334 -0
- package/python/app/explain_route.py +170 -0
- package/python/app/health_cli.py +13 -0
- package/python/app/main.py +131 -48
- package/python/app/materialize.py +150 -68
- package/python/app/mcp_contract.py +2 -1
- package/python/app/mcp_operator.py +252 -0
- package/python/app/mcp_server.py +290 -118
- package/python/app/npm_pkg_version.py +38 -0
- package/python/app/pick_diversify.py +51 -0
- package/python/app/replay_cli.py +145 -0
- package/python/app/route_cli.py +251 -87
- package/python/app/route_cli_pick.py +35 -0
- package/python/app/route_policies.py +18 -3
- package/python/app/route_quality.py +70 -1
- package/python/app/router_llm.py +85 -0
- package/python/app/router_mode.py +21 -0
- package/python/app/routing_signals.py +7 -1
- package/python/app/skill_manifest.py +67 -0
- package/python/app/skills_author_cli.py +117 -0
- package/python/app/tips_cli.py +37 -0
- package/python/app/tools_cli.py +276 -0
- package/python/fixtures/route_eval/smoke.json +5 -0
- package/python/requirements.txt +1 -0
- package/python/tests/test_capabilities_bundle.py +33 -0
- package/python/tests/test_materialize_hosts.py +108 -0
- package/python/tests/test_mcp_contract.py +1 -1
- package/python/tests/test_mcp_initialize_clientinfo.py +26 -0
- package/python/tests/test_mcp_operator.py +84 -0
- package/python/tests/test_npm_pkg_version.py +21 -0
- package/python/tests/test_pick_diversify.py +47 -0
- package/python/tests/test_replay_cli.py +31 -0
- package/python/tests/test_route_cli_pick.py +25 -0
- package/python/tests/test_route_policies.py +29 -0
- package/python/tests/test_route_quality.py +72 -0
- package/python/tests/test_router_llm.py +63 -0
- package/python/tests/test_router_mode_env.py +21 -0
- package/python/tests/test_routing_signals.py +20 -0
- package/python/tests/test_skill_manifest.py +48 -0
- 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.
|
|
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)
|