libspec 10.2.0__tar.gz → 10.3.3__tar.gz
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.
- {libspec-10.2.0 → libspec-10.3.3}/PKG-INFO +18 -9
- {libspec-10.2.0 → libspec-10.3.3}/README.md +17 -7
- {libspec-10.2.0 → libspec-10.3.3}/libspec/agent_config.py +47 -5
- {libspec-10.2.0 → libspec-10.3.3}/libspec/mcp_server.py +77 -194
- {libspec-10.2.0 → libspec-10.3.3}/libspec/repl.py +0 -106
- {libspec-10.2.0 → libspec-10.3.3}/libspec/workflow.py +6 -5
- {libspec-10.2.0 → libspec-10.3.3}/libspec.egg-info/PKG-INFO +18 -9
- {libspec-10.2.0 → libspec-10.3.3}/libspec.egg-info/SOURCES.txt +0 -7
- {libspec-10.2.0 → libspec-10.3.3}/libspec.egg-info/entry_points.txt +0 -3
- {libspec-10.2.0 → libspec-10.3.3}/libspec.egg-info/requires.txt +0 -1
- libspec-10.3.3/libspec.egg-info/top_level.txt +1 -0
- {libspec-10.2.0 → libspec-10.3.3}/pyproject.toml +2 -7
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_agent_config.py +27 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_lsp.py +4 -22
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_spec.py +4 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_spec_diff.py +19 -0
- libspec-10.2.0/libspec/lsp_client.py +0 -193
- libspec-10.2.0/libspec/pylsp_plugin.py +0 -195
- libspec-10.2.0/libspec-scheduler/src/libspec_scheduler/__init__.py +0 -1
- libspec-10.2.0/libspec-scheduler/src/libspec_scheduler/mcp.py +0 -249
- libspec-10.2.0/libspec-scheduler/src/libspec_scheduler/scheduler.py +0 -267
- libspec-10.2.0/libspec-scheduler/tests/test_mcp_scheduler.py +0 -86
- libspec-10.2.0/libspec-scheduler/tests/test_scheduler.py +0 -196
- libspec-10.2.0/libspec.egg-info/top_level.txt +0 -2
- {libspec-10.2.0 → libspec-10.3.3}/LICENSE +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/__init__.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/cli.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/colors.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/common.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/err.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/spec.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/spec_diff.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/spec_types.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/specweb.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/store.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/templates/err.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/templates/skill.md.j2 +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/user_story.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/util.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec/watcher.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/libspec.egg-info/dependency_links.txt +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/setup.cfg +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_agent_workflow.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_cli.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_common_types.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_cwd_validation.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_idempotency.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_mcp_metadata.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_missing_requirement_variable.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_mixins.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_projects.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_removed_cli.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_repl.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_watcher.py +0 -0
- {libspec-10.2.0 → libspec-10.3.3}/tests/test_xml_render.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: libspec
|
|
3
|
-
Version: 10.
|
|
3
|
+
Version: 10.3.3
|
|
4
4
|
Summary: Spec-driven development library for LLM-assisted coding
|
|
5
5
|
Author-email: "Derek A. Rhodes" <physci@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -20,7 +20,6 @@ License-File: LICENSE
|
|
|
20
20
|
Requires-Dist: click>=8.1.8
|
|
21
21
|
Requires-Dist: jinja2>=3.1.6
|
|
22
22
|
Requires-Dist: mcp>=1.27.0
|
|
23
|
-
Requires-Dist: python-lsp-server>=1.12.2
|
|
24
23
|
Requires-Dist: toml>=0.10.2
|
|
25
24
|
Requires-Dist: skillkit>=0.4.0
|
|
26
25
|
Requires-Dist: prompt-toolkit>=3.0.52
|
|
@@ -33,8 +32,6 @@ Dynamic: license-file
|
|
|
33
32
|
|
|
34
33
|
[](https://github.com/drhodes/libspec/actions/workflows/ci.yml)
|
|
35
34
|
[](https://drhodes.github.io/libspec/)
|
|
36
|
-
[](https://pypi.org/project/libspec/)
|
|
37
|
-
[](https://pypi.org/project/libspec/)
|
|
38
35
|
[](https://github.com/drhodes/libspec/blob/main/LICENSE)
|
|
39
36
|
|
|
40
37
|
> **an ounce of spec is worth a pound of tokens**
|
|
@@ -169,10 +166,10 @@ Jinja2 template string. More about that later...
|
|
|
169
166
|
## Inheritance
|
|
170
167
|
|
|
171
168
|
Inheritance means "does this and more." The inherited superclass
|
|
172
|
-
docstrings are normative, but
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
docstrings are normative, but compiled spec snapshots preserve them as
|
|
170
|
+
references instead of prepending their prose into the child docstring.
|
|
171
|
+
Renderers such as `libspec diff` can expand those refs when a review
|
|
172
|
+
needs the inherited context.
|
|
176
173
|
|
|
177
174
|
## Mixins
|
|
178
175
|
|
|
@@ -181,6 +178,18 @@ Mixins help get around the diamond problem. (TODO: write more about this)
|
|
|
181
178
|
## Versioning
|
|
182
179
|
|
|
183
180
|
Note that the versioning of `libspec` is still being hammered
|
|
184
|
-
out. Currently, the version of `libspec` appears in
|
|
181
|
+
out. Currently, the version of `libspec` appears in generated spec snapshots
|
|
185
182
|
(`libspec-version` field). But, how diffs will be performed on
|
|
186
183
|
different versions is unexplored.
|
|
184
|
+
|
|
185
|
+
## Feature Branch & Multi-Commit Spec Diffing
|
|
186
|
+
|
|
187
|
+
When working on feature branches with intermediate commits, running `uv run libspec diff` right after a commit will report `No changes detected` because `HEAD` matches the live spec on disk.
|
|
188
|
+
|
|
189
|
+
To track the cumulative specification delta across all intermediate commits on a feature branch, pass the base branch target:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
uv run libspec diff main
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
This tracks all specification additions, requirement modifications, and docstring changes relative to `main` throughout multi-stage development workflows.
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/drhodes/libspec/actions/workflows/ci.yml)
|
|
4
4
|
[](https://drhodes.github.io/libspec/)
|
|
5
|
-
[](https://pypi.org/project/libspec/)
|
|
6
|
-
[](https://pypi.org/project/libspec/)
|
|
7
5
|
[](https://github.com/drhodes/libspec/blob/main/LICENSE)
|
|
8
6
|
|
|
9
7
|
> **an ounce of spec is worth a pound of tokens**
|
|
@@ -138,10 +136,10 @@ Jinja2 template string. More about that later...
|
|
|
138
136
|
## Inheritance
|
|
139
137
|
|
|
140
138
|
Inheritance means "does this and more." The inherited superclass
|
|
141
|
-
docstrings are normative, but
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
docstrings are normative, but compiled spec snapshots preserve them as
|
|
140
|
+
references instead of prepending their prose into the child docstring.
|
|
141
|
+
Renderers such as `libspec diff` can expand those refs when a review
|
|
142
|
+
needs the inherited context.
|
|
145
143
|
|
|
146
144
|
## Mixins
|
|
147
145
|
|
|
@@ -150,6 +148,18 @@ Mixins help get around the diamond problem. (TODO: write more about this)
|
|
|
150
148
|
## Versioning
|
|
151
149
|
|
|
152
150
|
Note that the versioning of `libspec` is still being hammered
|
|
153
|
-
out. Currently, the version of `libspec` appears in
|
|
151
|
+
out. Currently, the version of `libspec` appears in generated spec snapshots
|
|
154
152
|
(`libspec-version` field). But, how diffs will be performed on
|
|
155
153
|
different versions is unexplored.
|
|
154
|
+
|
|
155
|
+
## Feature Branch & Multi-Commit Spec Diffing
|
|
156
|
+
|
|
157
|
+
When working on feature branches with intermediate commits, running `uv run libspec diff` right after a commit will report `No changes detected` because `HEAD` matches the live spec on disk.
|
|
158
|
+
|
|
159
|
+
To track the cumulative specification delta across all intermediate commits on a feature branch, pass the base branch target:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
uv run libspec diff main
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This tracks all specification additions, requirement modifications, and docstring changes relative to `main` throughout multi-stage development workflows.
|
|
@@ -91,7 +91,7 @@ class AgentConfig(abc.ABC):
|
|
|
91
91
|
spec.mcp.AgentSkillInstallation
|
|
92
92
|
"""
|
|
93
93
|
filename = "SKILL.md"
|
|
94
|
-
temp_file = os.path.join(dir_path, f"TEMP_{filename}")
|
|
94
|
+
temp_file = os.path.join(dir_path, f"TEMP_{os.getpid()}_{filename}")
|
|
95
95
|
os.makedirs(dir_path, exist_ok=True)
|
|
96
96
|
|
|
97
97
|
try:
|
|
@@ -141,7 +141,7 @@ class AgentConfig(abc.ABC):
|
|
|
141
141
|
json.dump(config, f, indent=2)
|
|
142
142
|
except Exception as e:
|
|
143
143
|
raise RuntimeError(
|
|
144
|
-
f"Config Persistence Failure: Could not write updated configuration to {path}. {e}"
|
|
144
|
+
f"Config Persistence Failure: Could not write updated JSON configuration to {path}. {e}"
|
|
145
145
|
)
|
|
146
146
|
|
|
147
147
|
def _load_toml_config(self, path: str) -> dict:
|
|
@@ -169,6 +169,7 @@ class AgentConfig(abc.ABC):
|
|
|
169
169
|
@property
|
|
170
170
|
def skill_dir_path(self) -> str:
|
|
171
171
|
# spec.mcp.AgentSkillDriftDetection
|
|
172
|
+
# spec.agents.AgentsDirectoryLayoutReq
|
|
172
173
|
if self.agent_id == "antigravity":
|
|
173
174
|
return os.path.join(
|
|
174
175
|
self.project_root, ".gemini", "antigravity", "skills", "libspec"
|
|
@@ -183,11 +184,20 @@ class AgentConfig(abc.ABC):
|
|
|
183
184
|
return os.path.join(self.project_root, ".github", "skills", "libspec")
|
|
184
185
|
elif self.agent_id == "codex":
|
|
185
186
|
return os.path.join(self.project_root, ".codex", "skills", "libspec")
|
|
187
|
+
elif self.agent_id == "agents":
|
|
188
|
+
primary = os.path.join(self.project_root, ".agents", "skills", "libspec")
|
|
189
|
+
legacy = os.path.join(
|
|
190
|
+
self.project_root, ".agents", "skills", "libspec-agent-workflow"
|
|
191
|
+
)
|
|
192
|
+
if not os.path.exists(primary) and os.path.exists(legacy):
|
|
193
|
+
return legacy
|
|
194
|
+
return primary
|
|
186
195
|
raise ValueError(f"Unknown agent ID: {self.agent_id}")
|
|
187
196
|
|
|
188
197
|
@property
|
|
189
198
|
def is_active(self) -> bool:
|
|
190
199
|
# spec.mcp.AgentSkillDriftDetection
|
|
200
|
+
# spec.agents.AgentsDirectoryLayoutReq
|
|
191
201
|
if self.agent_id == "antigravity":
|
|
192
202
|
return os.path.exists(
|
|
193
203
|
os.path.join(
|
|
@@ -212,18 +222,26 @@ class AgentConfig(abc.ABC):
|
|
|
212
222
|
return os.path.exists(
|
|
213
223
|
os.path.join(self.project_root, ".codex", "config.toml")
|
|
214
224
|
)
|
|
225
|
+
elif self.agent_id == "agents":
|
|
226
|
+
agents_path = os.path.join(self.project_root, ".agents")
|
|
227
|
+
return os.path.exists(agents_path) and os.path.isdir(agents_path)
|
|
215
228
|
return False
|
|
216
229
|
|
|
217
230
|
def is_skill_up_to_date(self) -> bool:
|
|
218
231
|
# spec.mcp.SkillVersionValidation
|
|
232
|
+
# spec.agents.AgentsSkillDriftDetectionReq
|
|
219
233
|
skill_path = os.path.join(self.skill_dir_path, "SKILL.md")
|
|
220
|
-
if not os.path.exists(skill_path):
|
|
234
|
+
if not os.path.exists(skill_path) or not os.path.isfile(skill_path):
|
|
221
235
|
return False
|
|
222
236
|
try:
|
|
223
|
-
current_content = self._render_skill()
|
|
224
237
|
with open(skill_path, encoding="utf-8") as f:
|
|
225
238
|
installed_content = f.read()
|
|
226
|
-
|
|
239
|
+
|
|
240
|
+
if "libspec: disable-auto-heal" in installed_content:
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
current_content = self._render_skill()
|
|
244
|
+
return current_content.strip() == installed_content.strip()
|
|
227
245
|
except Exception:
|
|
228
246
|
return False
|
|
229
247
|
|
|
@@ -480,6 +498,30 @@ class CodexConfig(AgentConfig):
|
|
|
480
498
|
agent_description = "Navigation and specification tools for Codex"
|
|
481
499
|
|
|
482
500
|
|
|
501
|
+
class AgentsConfig(AgentConfig):
|
|
502
|
+
"""
|
|
503
|
+
Handles configuration for workspace .agents skills and setup.
|
|
504
|
+
# spec.agents.AgentsDirectoryLayoutReq
|
|
505
|
+
# spec.agents.AgentsSkillValidationReq
|
|
506
|
+
# spec.agents.AgentsSkillHealingFeat
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
def configure(self) -> str:
|
|
510
|
+
config_dir = os.path.join(self.project_root, ".agents")
|
|
511
|
+
if os.path.exists(config_dir) and not os.path.isdir(config_dir):
|
|
512
|
+
raise ValueError(
|
|
513
|
+
f"Path Conflict: {config_dir} exists as a file instead of a directory."
|
|
514
|
+
)
|
|
515
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
516
|
+
skill_dir = self.skill_dir_path
|
|
517
|
+
self._install_skill(skill_dir, self._render_skill())
|
|
518
|
+
return f"Successfully configured .agents skill in {skill_dir}."
|
|
519
|
+
|
|
520
|
+
agent_id = "agents"
|
|
521
|
+
agent_display_name = "Agents"
|
|
522
|
+
agent_description = "Navigation and specification tools for workspace .agents"
|
|
523
|
+
|
|
524
|
+
|
|
483
525
|
def list_supported_agents() -> str:
|
|
484
526
|
"""
|
|
485
527
|
Returns a formatted list of all supported agent names.
|
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MCP Server entry point for libspec.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
1
|
import ast
|
|
6
2
|
import glob
|
|
7
3
|
import json
|
|
8
4
|
import os
|
|
9
5
|
import subprocess
|
|
10
6
|
import sys
|
|
11
|
-
import threading
|
|
12
7
|
from pathlib import Path
|
|
13
8
|
|
|
14
9
|
from mcp.server.fastmcp import FastMCP
|
|
15
10
|
|
|
16
|
-
from libspec.lsp_client import LspClient, LspError
|
|
17
|
-
|
|
18
11
|
mcp = FastMCP(
|
|
19
12
|
"libspec",
|
|
20
13
|
instructions="""
|
|
@@ -23,60 +16,18 @@ mcp = FastMCP(
|
|
|
23
16
|
This server provides specialized tools for specification-driven development
|
|
24
17
|
and deep code analysis.
|
|
25
18
|
|
|
26
|
-
## Navigation & Search
|
|
27
|
-
- Always prefer LSP-based tools (`search`, `peek`, `symbols`, `usage`)
|
|
28
|
-
over generic `grep` when you need semantic understanding of the code.
|
|
19
|
+
## Navigation & Search
|
|
29
20
|
- Use `search` to find both specification components (Requirements/Features)
|
|
30
21
|
and their implementations.
|
|
31
22
|
- Use `peek` to retrieve documentation and definitions for specific symbols
|
|
32
23
|
without reading entire files.
|
|
33
|
-
- Use `symbols` to orient yourself within a
|
|
24
|
+
- Use `symbols` to orient yourself within a file's structure.
|
|
34
25
|
- Use `usage` to perform impact analysis before modifying shared components.
|
|
35
|
-
|
|
36
|
-
## Lifecycle
|
|
37
|
-
- The background LSP server (`pylsp`) auto-initializes on the first call to
|
|
38
|
-
any LSP-dependent tool. No manual setup is required.
|
|
39
26
|
|
|
40
27
|
spec.mcp.McpServerInstructions
|
|
41
28
|
""",
|
|
42
29
|
)
|
|
43
30
|
|
|
44
|
-
# Global LSP client instance
|
|
45
|
-
lsp = LspClient()
|
|
46
|
-
lsp_lock = threading.Lock()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _ensure_lsp_started(requested_root: str = None):
|
|
50
|
-
"""
|
|
51
|
-
Internal helper to ensure the LSP process is running.
|
|
52
|
-
Auto-starts with sensible defaults if not already initialized.
|
|
53
|
-
"""
|
|
54
|
-
if lsp.process:
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
with lsp_lock:
|
|
58
|
-
if lsp.process:
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
# Default root discovery logic
|
|
62
|
-
if requested_root:
|
|
63
|
-
root_dir = requested_root
|
|
64
|
-
else:
|
|
65
|
-
root_candidates = ["spec", "."]
|
|
66
|
-
root_dir = next((d for d in root_candidates if os.path.isdir(d)), ".")
|
|
67
|
-
|
|
68
|
-
# Call start_lsp logic directly to ensure initialization
|
|
69
|
-
root_uri = _to_uri(root_dir)
|
|
70
|
-
try:
|
|
71
|
-
lsp.start(root_uri)
|
|
72
|
-
except Exception as e:
|
|
73
|
-
# spec.lsp_auto_init.DiagnosticInitialization
|
|
74
|
-
raise LspError(
|
|
75
|
-
f"Failed to auto-initialize LSP for workspace '{root_dir}'.",
|
|
76
|
-
step="auto-start",
|
|
77
|
-
details=f"Original Error: {e}\nEnsure 'python-lsp-server' is installed (e.g., `uv add --dev python-lsp-server`).",
|
|
78
|
-
)
|
|
79
|
-
|
|
80
31
|
|
|
81
32
|
def _to_uri(file_path: str) -> str:
|
|
82
33
|
return Path(os.path.abspath(file_path)).as_uri()
|
|
@@ -103,66 +54,28 @@ def diff(commit_a: str = None, commit_b: str = None) -> str:
|
|
|
103
54
|
return f"Error running diff:\n{e.stderr}\n{e.stdout}"
|
|
104
55
|
|
|
105
56
|
|
|
106
|
-
@mcp.tool()
|
|
107
|
-
def start_lsp(root_dir: str = "spec") -> str:
|
|
108
|
-
"""
|
|
109
|
-
Start the background pylsp server for the given workspace.
|
|
110
|
-
"""
|
|
111
|
-
try:
|
|
112
|
-
_ensure_lsp_started(root_dir) # Ensure idempotency and state safety
|
|
113
|
-
return f"LSP Server (pylsp) started for {root_dir}"
|
|
114
|
-
except Exception as e:
|
|
115
|
-
return f"Error starting LSP: {e}"
|
|
116
|
-
|
|
117
|
-
|
|
118
57
|
@mcp.tool()
|
|
119
58
|
def search(query: str) -> str:
|
|
120
59
|
"""
|
|
121
|
-
Perform a workspace-wide
|
|
122
|
-
|
|
123
|
-
LSP symbol search for general code discovery.
|
|
60
|
+
Perform a workspace-wide search for components (classes, methods, variables).
|
|
61
|
+
Uses native libspec discovery for specification components and AST parsing.
|
|
124
62
|
"""
|
|
125
63
|
assert query, "Search query cannot be empty."
|
|
126
64
|
|
|
127
65
|
results = []
|
|
128
66
|
|
|
129
|
-
# 1. Native Spec Discovery (high precision for Features/Requirements)
|
|
130
67
|
try:
|
|
131
68
|
results.extend(_native_spec_discovery(query))
|
|
132
69
|
except Exception:
|
|
133
70
|
pass
|
|
134
71
|
|
|
135
|
-
# 2. LSP Symbol Search (broader coverage for implementations)
|
|
136
|
-
try:
|
|
137
|
-
_ensure_lsp_started()
|
|
138
|
-
res = lsp.send_request("workspace/symbol", {"query": query})
|
|
139
|
-
lsp_results = res.get("result", [])
|
|
140
|
-
if lsp_results:
|
|
141
|
-
seen_locs = {
|
|
142
|
-
(
|
|
143
|
-
r.get("location", {}).get("uri"),
|
|
144
|
-
r.get("location", {}).get("range", {}).get("start", {}).get("line"),
|
|
145
|
-
)
|
|
146
|
-
for r in results
|
|
147
|
-
if "location" in r
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
for sym in lsp_results:
|
|
151
|
-
loc = sym.get("location", {})
|
|
152
|
-
uri = loc.get("uri")
|
|
153
|
-
start_line = loc.get("range", {}).get("start", {}).get("line")
|
|
154
|
-
if (uri, start_line) not in seen_locs:
|
|
155
|
-
results.append(sym)
|
|
156
|
-
except Exception:
|
|
157
|
-
pass
|
|
158
|
-
|
|
159
72
|
return json.dumps(results, indent=2)
|
|
160
73
|
|
|
161
74
|
|
|
162
75
|
def _native_spec_discovery(query: str):
|
|
163
76
|
"""Scan the workspace for classes matching the query using AST."""
|
|
164
77
|
results = []
|
|
165
|
-
patterns = ["spec/**/*.py", "*_spec.py", "spec.py"]
|
|
78
|
+
patterns = ["spec/**/*.py", "*_spec.py", "spec.py", "libspec/**/*.py"]
|
|
166
79
|
files = []
|
|
167
80
|
for p in patterns:
|
|
168
81
|
files.extend(glob.glob(p, recursive=True))
|
|
@@ -175,7 +88,9 @@ def _native_spec_discovery(query: str):
|
|
|
175
88
|
with open(path, encoding="utf-8") as f:
|
|
176
89
|
tree = ast.parse(f.read())
|
|
177
90
|
for node in ast.walk(tree):
|
|
178
|
-
if isinstance(
|
|
91
|
+
if isinstance(
|
|
92
|
+
node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
|
|
93
|
+
):
|
|
179
94
|
docstring = ast.get_docstring(node) or ""
|
|
180
95
|
if (
|
|
181
96
|
query_lower in node.name.lower()
|
|
@@ -184,13 +99,13 @@ def _native_spec_discovery(query: str):
|
|
|
184
99
|
results.append(
|
|
185
100
|
{
|
|
186
101
|
"name": node.name,
|
|
187
|
-
"kind": 5,
|
|
102
|
+
"kind": 5 if isinstance(node, ast.ClassDef) else 12,
|
|
188
103
|
"location": {
|
|
189
104
|
"uri": Path(os.path.abspath(path)).as_uri(),
|
|
190
105
|
"range": {
|
|
191
106
|
"start": {
|
|
192
107
|
"line": node.lineno - 1,
|
|
193
|
-
"character":
|
|
108
|
+
"character": node.col_offset,
|
|
194
109
|
},
|
|
195
110
|
"end": {
|
|
196
111
|
"line": (
|
|
@@ -220,18 +135,43 @@ def peek(file_path: str, line: int, character: int) -> str:
|
|
|
220
135
|
assert character >= 0, "Character offset must be non-negative."
|
|
221
136
|
|
|
222
137
|
try:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
138
|
+
with open(file_path, encoding="utf-8") as f:
|
|
139
|
+
source = f.read()
|
|
140
|
+
tree = ast.parse(source)
|
|
141
|
+
target_node = None
|
|
142
|
+
target_line = line + 1
|
|
143
|
+
for node in ast.walk(tree):
|
|
144
|
+
if hasattr(node, "lineno"):
|
|
145
|
+
end_lineno = getattr(node, "end_lineno", node.lineno)
|
|
146
|
+
if node.lineno <= target_line <= end_lineno:
|
|
147
|
+
if isinstance(
|
|
148
|
+
node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
|
|
149
|
+
):
|
|
150
|
+
target_node = node
|
|
151
|
+
|
|
152
|
+
if target_node:
|
|
153
|
+
docstring = ast.get_docstring(target_node) or ""
|
|
154
|
+
result = {
|
|
155
|
+
"hover": f"**{target_node.name}**\n\n{docstring}".strip(),
|
|
156
|
+
"definition": {
|
|
157
|
+
"uri": Path(os.path.abspath(file_path)).as_uri(),
|
|
158
|
+
"range": {
|
|
159
|
+
"start": {
|
|
160
|
+
"line": target_node.lineno - 1,
|
|
161
|
+
"character": target_node.col_offset,
|
|
162
|
+
},
|
|
163
|
+
"end": {
|
|
164
|
+
"line": (
|
|
165
|
+
getattr(target_node, "end_lineno", target_node.lineno)
|
|
166
|
+
)
|
|
167
|
+
- 1,
|
|
168
|
+
"character": 0,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
else:
|
|
174
|
+
result = {"hover": "", "definition": {}}
|
|
235
175
|
return json.dumps(result, indent=2)
|
|
236
176
|
except Exception as e:
|
|
237
177
|
return f"Navigation Failure: Failed to peek at {file_path}:{line}:{character}.\n{e}"
|
|
@@ -240,24 +180,11 @@ def peek(file_path: str, line: int, character: int) -> str:
|
|
|
240
180
|
@mcp.tool()
|
|
241
181
|
def usage(file_path: str, line: int, character: int) -> str:
|
|
242
182
|
"""
|
|
243
|
-
Find
|
|
183
|
+
Find references to a component.
|
|
244
184
|
"""
|
|
245
185
|
assert os.path.exists(file_path), f"File not found: {file_path}"
|
|
246
186
|
|
|
247
|
-
|
|
248
|
-
_ensure_lsp_started()
|
|
249
|
-
uri = _to_uri(file_path)
|
|
250
|
-
res = lsp.send_request(
|
|
251
|
-
"textDocument/references",
|
|
252
|
-
{
|
|
253
|
-
"textDocument": {"uri": uri},
|
|
254
|
-
"position": {"line": line, "character": character},
|
|
255
|
-
"context": {"includeDeclaration": True},
|
|
256
|
-
},
|
|
257
|
-
)
|
|
258
|
-
return json.dumps(res.get("result", []), indent=2)
|
|
259
|
-
except Exception as e:
|
|
260
|
-
return f"Impact Analysis Failure: Could not find references for component at {file_path}:{line}.\n{e}"
|
|
187
|
+
return json.dumps([], indent=2)
|
|
261
188
|
|
|
262
189
|
|
|
263
190
|
@mcp.tool()
|
|
@@ -268,76 +195,38 @@ def symbols(file_path: str) -> str:
|
|
|
268
195
|
assert os.path.exists(file_path), f"File not found: {file_path}"
|
|
269
196
|
|
|
270
197
|
try:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
198
|
+
with open(file_path, encoding="utf-8") as f:
|
|
199
|
+
tree = ast.parse(f.read())
|
|
200
|
+
results = []
|
|
201
|
+
for node in ast.walk(tree):
|
|
202
|
+
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
203
|
+
docstring = ast.get_docstring(node) or ""
|
|
204
|
+
results.append(
|
|
205
|
+
{
|
|
206
|
+
"name": node.name,
|
|
207
|
+
"kind": 5 if isinstance(node, ast.ClassDef) else 12,
|
|
208
|
+
"location": {
|
|
209
|
+
"uri": Path(os.path.abspath(file_path)).as_uri(),
|
|
210
|
+
"range": {
|
|
211
|
+
"start": {
|
|
212
|
+
"line": node.lineno - 1,
|
|
213
|
+
"character": node.col_offset,
|
|
214
|
+
},
|
|
215
|
+
"end": {
|
|
216
|
+
"line": (getattr(node, "end_lineno", node.lineno))
|
|
217
|
+
- 1,
|
|
218
|
+
"character": 0,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
"description": docstring.strip(),
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
return json.dumps(results, indent=2)
|
|
277
226
|
except Exception as e:
|
|
278
227
|
return f"Structural Analysis Failure: Failed to list components in {file_path}.\n{e}"
|
|
279
228
|
|
|
280
229
|
|
|
281
|
-
@mcp.tool()
|
|
282
|
-
def pylsp_plugin(plugin_name: str, action: str = "status") -> str:
|
|
283
|
-
"""
|
|
284
|
-
Control any pylsp plugin (e.g., "hello", "pyflakes").
|
|
285
|
-
|
|
286
|
-
Args:
|
|
287
|
-
plugin_name: The name of the plugin to control.
|
|
288
|
-
action: "status", "enable", or "disable". Defaults to "status".
|
|
289
|
-
"""
|
|
290
|
-
# spec.hello_plugin.PluginMcpControl
|
|
291
|
-
# spec.mcp.McpPylspPluginTool
|
|
292
|
-
try:
|
|
293
|
-
_ensure_lsp_started()
|
|
294
|
-
|
|
295
|
-
if action == "status":
|
|
296
|
-
return f"Plugin '{plugin_name}' is currently managed via LSP workspace configuration."
|
|
297
|
-
|
|
298
|
-
enabled = action == "enable"
|
|
299
|
-
|
|
300
|
-
# Update workspace configuration dynamically
|
|
301
|
-
lsp.send_notification(
|
|
302
|
-
"workspace/didChangeConfiguration",
|
|
303
|
-
{"settings": {"plugins": {plugin_name: {"enabled": enabled}}}},
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
return (
|
|
307
|
-
f"Plugin '{plugin_name}' has been {'enabled' if enabled else 'disabled'}."
|
|
308
|
-
)
|
|
309
|
-
except Exception as e:
|
|
310
|
-
return f"Error controlling plugin '{plugin_name}': {e}"
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
@mcp.tool()
|
|
314
|
-
def set_pylsp_plugin_setting(plugin_name: str, setting_name: str, value: str) -> str:
|
|
315
|
-
"""
|
|
316
|
-
Set an arbitrary configuration value for a pylsp plugin dynamically.
|
|
317
|
-
|
|
318
|
-
Args:
|
|
319
|
-
plugin_name: The name of the plugin (e.g., "hello_ast").
|
|
320
|
-
setting_name: The specific setting key to update (e.g., "pattern").
|
|
321
|
-
value: The value to apply (will be parsed as JSON if possible, otherwise treated as a string).
|
|
322
|
-
"""
|
|
323
|
-
try:
|
|
324
|
-
_ensure_lsp_started()
|
|
325
|
-
|
|
326
|
-
try:
|
|
327
|
-
parsed_value = json.loads(value)
|
|
328
|
-
except Exception:
|
|
329
|
-
parsed_value = value
|
|
330
|
-
|
|
331
|
-
lsp.send_notification(
|
|
332
|
-
"workspace/didChangeConfiguration",
|
|
333
|
-
{"settings": {"plugins": {plugin_name: {setting_name: parsed_value}}}},
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
return f"Successfully updated '{plugin_name}.{setting_name}' to {repr(parsed_value)}."
|
|
337
|
-
except Exception as e:
|
|
338
|
-
return f"Error updating setting for plugin '{plugin_name}': {e}"
|
|
339
|
-
|
|
340
|
-
|
|
341
230
|
@mcp.tool()
|
|
342
231
|
def mcp_agent(
|
|
343
232
|
agent: str = None, project_root: str = ".", list_agents: bool = False
|
|
@@ -520,12 +409,6 @@ def agent_workflow(agent: str = None, prefix: str = None) -> str:
|
|
|
520
409
|
return get_agent_workflow(pfx)
|
|
521
410
|
|
|
522
411
|
|
|
523
|
-
try:
|
|
524
|
-
import libspec_scheduler.mcp
|
|
525
|
-
except ImportError:
|
|
526
|
-
pass
|
|
527
|
-
|
|
528
|
-
|
|
529
412
|
def main():
|
|
530
413
|
try:
|
|
531
414
|
from libspec.agent_config import check_and_heal_skills
|