invar-tools 1.8.0__py3-none-any.whl → 1.11.0__py3-none-any.whl
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.
- invar/__init__.py +8 -0
- invar/core/doc_edit.py +187 -0
- invar/core/doc_parser.py +563 -0
- invar/core/language.py +88 -0
- invar/core/models.py +106 -0
- invar/core/patterns/detector.py +6 -1
- invar/core/patterns/p0_exhaustive.py +15 -3
- invar/core/patterns/p0_literal.py +15 -3
- invar/core/patterns/p0_newtype.py +15 -3
- invar/core/patterns/p0_nonempty.py +15 -3
- invar/core/patterns/p0_validation.py +15 -3
- invar/core/patterns/registry.py +5 -1
- invar/core/patterns/types.py +5 -1
- invar/core/property_gen.py +4 -0
- invar/core/rules.py +84 -18
- invar/core/sync_helpers.py +27 -1
- invar/core/ts_parsers.py +286 -0
- invar/core/ts_sig_parser.py +310 -0
- invar/mcp/handlers.py +408 -0
- invar/mcp/server.py +288 -143
- invar/node_tools/MANIFEST +7 -0
- invar/node_tools/__init__.py +51 -0
- invar/node_tools/fc-runner/cli.js +77 -0
- invar/node_tools/quick-check/cli.js +28 -0
- invar/node_tools/ts-analyzer/cli.js +480 -0
- invar/shell/claude_hooks.py +35 -12
- invar/shell/commands/doc.py +409 -0
- invar/shell/commands/guard.py +41 -1
- invar/shell/commands/init.py +154 -16
- invar/shell/commands/perception.py +157 -33
- invar/shell/commands/skill.py +187 -0
- invar/shell/commands/template_sync.py +65 -13
- invar/shell/commands/uninstall.py +60 -12
- invar/shell/commands/update.py +6 -14
- invar/shell/contract_coverage.py +1 -0
- invar/shell/doc_tools.py +459 -0
- invar/shell/fs.py +67 -13
- invar/shell/pi_hooks.py +6 -0
- invar/shell/prove/crosshair.py +3 -0
- invar/shell/prove/guard_ts.py +902 -0
- invar/shell/skill_manager.py +355 -0
- invar/shell/template_engine.py +28 -4
- invar/shell/templates.py +4 -4
- invar/templates/claude-md/python/critical-rules.md +33 -0
- invar/templates/claude-md/python/quick-reference.md +24 -0
- invar/templates/claude-md/typescript/critical-rules.md +40 -0
- invar/templates/claude-md/typescript/quick-reference.md +24 -0
- invar/templates/claude-md/universal/check-in.md +25 -0
- invar/templates/claude-md/universal/skills.md +73 -0
- invar/templates/claude-md/universal/workflow.md +55 -0
- invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
- invar/templates/config/AGENT.md.jinja +58 -0
- invar/templates/config/CLAUDE.md.jinja +16 -209
- invar/templates/config/context.md.jinja +19 -0
- invar/templates/examples/{README.md → python/README.md} +2 -0
- invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
- invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
- invar/templates/examples/python/core_shell.py +227 -0
- invar/templates/examples/python/functional.py +613 -0
- invar/templates/examples/typescript/README.md +31 -0
- invar/templates/examples/typescript/contracts.ts +163 -0
- invar/templates/examples/typescript/core_shell.ts +374 -0
- invar/templates/examples/typescript/functional.ts +601 -0
- invar/templates/examples/typescript/workflow.md +95 -0
- invar/templates/hooks/PostToolUse.sh.jinja +10 -1
- invar/templates/hooks/PreToolUse.sh.jinja +38 -0
- invar/templates/hooks/Stop.sh.jinja +1 -1
- invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
- invar/templates/hooks/pi/invar.ts.jinja +9 -0
- invar/templates/manifest.toml +7 -6
- invar/templates/onboard/assessment.md.jinja +214 -0
- invar/templates/onboard/patterns/python.md +347 -0
- invar/templates/onboard/patterns/typescript.md +452 -0
- invar/templates/onboard/roadmap.md.jinja +168 -0
- invar/templates/protocol/INVAR.md.jinja +51 -0
- invar/templates/protocol/python/architecture-examples.md +41 -0
- invar/templates/protocol/python/contracts-syntax.md +56 -0
- invar/templates/protocol/python/markers.md +44 -0
- invar/templates/protocol/python/tools.md +24 -0
- invar/templates/protocol/python/troubleshooting.md +38 -0
- invar/templates/protocol/typescript/architecture-examples.md +52 -0
- invar/templates/protocol/typescript/contracts-syntax.md +73 -0
- invar/templates/protocol/typescript/markers.md +48 -0
- invar/templates/protocol/typescript/tools.md +65 -0
- invar/templates/protocol/typescript/troubleshooting.md +104 -0
- invar/templates/protocol/universal/architecture.md +36 -0
- invar/templates/protocol/universal/completion.md +14 -0
- invar/templates/protocol/universal/contracts-concept.md +37 -0
- invar/templates/protocol/universal/header.md +17 -0
- invar/templates/protocol/universal/session.md +17 -0
- invar/templates/protocol/universal/six-laws.md +10 -0
- invar/templates/protocol/universal/usbv.md +14 -0
- invar/templates/protocol/universal/visible-workflow.md +25 -0
- invar/templates/skills/develop/SKILL.md.jinja +85 -3
- invar/templates/skills/extensions/_registry.yaml +93 -0
- invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
- invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
- invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
- invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
- invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
- invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
- invar/templates/skills/extensions/security/SKILL.md +382 -0
- invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
- invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
- invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
- invar/templates/skills/review/SKILL.md.jinja +220 -248
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/METADATA +336 -12
- invar_tools-1.11.0.dist-info/RECORD +178 -0
- invar/templates/examples/core_shell.py +0 -127
- invar/templates/protocol/INVAR.md +0 -310
- invar_tools-1.8.0.dist-info/RECORD +0 -116
- /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/NOTICE +0 -0
invar/mcp/handlers.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP tool handlers for Invar.
|
|
3
|
+
|
|
4
|
+
DX-76: Extracted from server.py to manage file size.
|
|
5
|
+
Contains all _run_* handler functions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
from mcp.types import TextContent
|
|
17
|
+
from returns.result import Success
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# @invar:allow shell_result: Pure validation helper, no I/O, returns tuple not Result
|
|
21
|
+
# @shell_complexity: Security validation requires multiple checks
|
|
22
|
+
def _validate_path(path: str) -> tuple[bool, str]:
|
|
23
|
+
"""Validate path argument for safety.
|
|
24
|
+
|
|
25
|
+
Returns (is_valid, error_message).
|
|
26
|
+
Rejects paths that could be interpreted as shell commands or flags.
|
|
27
|
+
"""
|
|
28
|
+
if not path:
|
|
29
|
+
return True, "" # Empty path defaults to "." in handlers
|
|
30
|
+
|
|
31
|
+
# Reject if looks like a flag (starts with -)
|
|
32
|
+
if path.startswith("-"):
|
|
33
|
+
return False, f"Invalid path: cannot start with '-': {path}"
|
|
34
|
+
|
|
35
|
+
# Reject shell metacharacters that could cause issues
|
|
36
|
+
dangerous_chars = [";", "&", "|", "$", "`", "\n", "\r"]
|
|
37
|
+
for char in dangerous_chars:
|
|
38
|
+
if char in path:
|
|
39
|
+
return False, f"Invalid path: contains forbidden character: {char!r}"
|
|
40
|
+
|
|
41
|
+
# Try to resolve path - this catches malformed paths
|
|
42
|
+
try:
|
|
43
|
+
Path(path).resolve()
|
|
44
|
+
except (OSError, ValueError) as e:
|
|
45
|
+
return False, f"Invalid path: {e}"
|
|
46
|
+
|
|
47
|
+
return True, ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
51
|
+
# @shell_complexity: Guard command with multiple optional flags
|
|
52
|
+
# @invar:allow shell_result: MCP handler for guard tool
|
|
53
|
+
async def _run_guard(args: dict[str, Any]) -> list[TextContent]:
|
|
54
|
+
"""Run invar guard command."""
|
|
55
|
+
path = args.get("path", ".")
|
|
56
|
+
is_valid, error = _validate_path(path)
|
|
57
|
+
if not is_valid:
|
|
58
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
59
|
+
|
|
60
|
+
cmd = [sys.executable, "-m", "invar.shell.commands.guard", "guard"]
|
|
61
|
+
cmd.append(path)
|
|
62
|
+
|
|
63
|
+
if args.get("changed", True):
|
|
64
|
+
cmd.append("--changed")
|
|
65
|
+
if args.get("strict", False):
|
|
66
|
+
cmd.append("--strict")
|
|
67
|
+
# DX-37: Optional coverage collection
|
|
68
|
+
if args.get("coverage", False):
|
|
69
|
+
cmd.append("--coverage")
|
|
70
|
+
# DX-63: Contract coverage check only
|
|
71
|
+
if args.get("contracts_only", False):
|
|
72
|
+
cmd.append("--contracts-only")
|
|
73
|
+
|
|
74
|
+
# DX-26: TTY auto-detection - MCP runs in non-TTY, so agent JSON output is automatic
|
|
75
|
+
# No explicit flag needed
|
|
76
|
+
|
|
77
|
+
return await _execute_command(cmd)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
81
|
+
# @invar:allow shell_result: MCP handler for sig tool
|
|
82
|
+
async def _run_sig(args: dict[str, Any]) -> list[TextContent]:
|
|
83
|
+
"""Run invar sig command."""
|
|
84
|
+
target = args.get("target", "")
|
|
85
|
+
if not target:
|
|
86
|
+
return [TextContent(type="text", text="Error: target is required")]
|
|
87
|
+
|
|
88
|
+
# Validate target (can be file path or file::symbol)
|
|
89
|
+
target_path = target.split("::")[0] if "::" in target else target
|
|
90
|
+
is_valid, error = _validate_path(target_path)
|
|
91
|
+
if not is_valid:
|
|
92
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
93
|
+
|
|
94
|
+
cmd = [sys.executable, "-m", "invar.shell.commands.guard", "sig", target, "--json"]
|
|
95
|
+
return await _execute_command(cmd)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
99
|
+
# @invar:allow shell_result: MCP handler for map tool
|
|
100
|
+
async def _run_map(args: dict[str, Any]) -> list[TextContent]:
|
|
101
|
+
"""Run invar map command."""
|
|
102
|
+
path = args.get("path", ".")
|
|
103
|
+
is_valid, error = _validate_path(path)
|
|
104
|
+
if not is_valid:
|
|
105
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
106
|
+
|
|
107
|
+
cmd = [sys.executable, "-m", "invar.shell.commands.guard", "map"]
|
|
108
|
+
cmd.append(path)
|
|
109
|
+
|
|
110
|
+
top = args.get("top", 10)
|
|
111
|
+
cmd.extend(["--top", str(top)])
|
|
112
|
+
|
|
113
|
+
cmd.append("--json")
|
|
114
|
+
return await _execute_command(cmd)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# DX-76: Document query handlers
|
|
118
|
+
# @shell_orchestration: MCP handler - calls shell layer directly
|
|
119
|
+
# @shell_complexity: MCP input validation + result handling
|
|
120
|
+
# @invar:allow shell_result: MCP handler for doc_toc tool
|
|
121
|
+
async def _run_doc_toc(args: dict[str, Any]) -> list[TextContent]:
|
|
122
|
+
"""Run invar_doc_toc - extract document structure."""
|
|
123
|
+
from dataclasses import asdict
|
|
124
|
+
|
|
125
|
+
from invar.shell.doc_tools import read_toc
|
|
126
|
+
|
|
127
|
+
file_path = args.get("file", "")
|
|
128
|
+
if not file_path:
|
|
129
|
+
return [TextContent(type="text", text="Error: file is required")]
|
|
130
|
+
|
|
131
|
+
is_valid, error = _validate_path(file_path)
|
|
132
|
+
if not is_valid:
|
|
133
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
134
|
+
|
|
135
|
+
path = Path(file_path)
|
|
136
|
+
result = read_toc(path)
|
|
137
|
+
|
|
138
|
+
if isinstance(result, Success):
|
|
139
|
+
toc = result.unwrap()
|
|
140
|
+
# Convert to JSON-serializable format
|
|
141
|
+
output = {
|
|
142
|
+
"sections": [_section_to_dict(s) for s in toc.sections],
|
|
143
|
+
"frontmatter": asdict(toc.frontmatter) if toc.frontmatter else None,
|
|
144
|
+
}
|
|
145
|
+
return [TextContent(type="text", text=json.dumps(output, indent=2))]
|
|
146
|
+
else:
|
|
147
|
+
return [TextContent(type="text", text=f"Error: {result.failure()}")]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# @invar:allow shell_result: Pure data transformation, no I/O
|
|
151
|
+
# @shell_orchestration: Helper for MCP response formatting
|
|
152
|
+
def _section_to_dict(section: Any) -> dict[str, Any]:
|
|
153
|
+
"""Convert Section to JSON-serializable dict (recursive)."""
|
|
154
|
+
return {
|
|
155
|
+
"title": section.title,
|
|
156
|
+
"slug": section.slug,
|
|
157
|
+
"level": section.level,
|
|
158
|
+
"line_start": section.line_start,
|
|
159
|
+
"line_end": section.line_end,
|
|
160
|
+
"char_count": section.char_count,
|
|
161
|
+
"path": section.path,
|
|
162
|
+
"children": [_section_to_dict(c) for c in section.children],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# @shell_orchestration: MCP handler - calls shell layer directly
|
|
167
|
+
# @shell_complexity: MCP input validation + result handling
|
|
168
|
+
# @invar:allow shell_result: MCP handler for doc_read tool
|
|
169
|
+
async def _run_doc_read(args: dict[str, Any]) -> list[TextContent]:
|
|
170
|
+
"""Run invar_doc_read - read a specific section."""
|
|
171
|
+
from invar.shell.doc_tools import read_section
|
|
172
|
+
|
|
173
|
+
file_path = args.get("file", "")
|
|
174
|
+
section_path = args.get("section", "")
|
|
175
|
+
|
|
176
|
+
if not file_path:
|
|
177
|
+
return [TextContent(type="text", text="Error: file is required")]
|
|
178
|
+
if not section_path:
|
|
179
|
+
return [TextContent(type="text", text="Error: section is required")]
|
|
180
|
+
|
|
181
|
+
is_valid, error = _validate_path(file_path)
|
|
182
|
+
if not is_valid:
|
|
183
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
184
|
+
|
|
185
|
+
path = Path(file_path)
|
|
186
|
+
result = read_section(path, section_path)
|
|
187
|
+
|
|
188
|
+
if isinstance(result, Success):
|
|
189
|
+
content = result.unwrap()
|
|
190
|
+
output = {"path": section_path, "content": content}
|
|
191
|
+
return [TextContent(type="text", text=json.dumps(output, indent=2))]
|
|
192
|
+
else:
|
|
193
|
+
return [TextContent(type="text", text=f"Error: {result.failure()}")]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# @shell_complexity: Multiple arg validation branches + error handling
|
|
197
|
+
# @invar:allow shell_result: MCP handler for doc_read_many tool
|
|
198
|
+
async def _run_doc_read_many(args: dict[str, Any]) -> list[TextContent]:
|
|
199
|
+
"""Run invar_doc_read_many - read multiple sections."""
|
|
200
|
+
from invar.shell.doc_tools import read_sections_batch
|
|
201
|
+
|
|
202
|
+
file_path = args.get("file", "")
|
|
203
|
+
sections = args.get("sections", [])
|
|
204
|
+
include_children = args.get("include_children", True)
|
|
205
|
+
|
|
206
|
+
if not file_path:
|
|
207
|
+
return [TextContent(type="text", text="Error: file is required")]
|
|
208
|
+
if not sections:
|
|
209
|
+
return [TextContent(type="text", text="Error: sections list is required")]
|
|
210
|
+
if not isinstance(sections, list):
|
|
211
|
+
return [TextContent(type="text", text="Error: sections must be a list")]
|
|
212
|
+
|
|
213
|
+
is_valid, error = _validate_path(file_path)
|
|
214
|
+
if not is_valid:
|
|
215
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
216
|
+
|
|
217
|
+
path = Path(file_path)
|
|
218
|
+
result = read_sections_batch(path, sections, include_children)
|
|
219
|
+
|
|
220
|
+
if isinstance(result, Success):
|
|
221
|
+
sections_data = result.unwrap()
|
|
222
|
+
return [TextContent(type="text", text=json.dumps(sections_data, indent=2))]
|
|
223
|
+
else:
|
|
224
|
+
return [TextContent(type="text", text=f"Error: {result.failure()}")]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# @shell_orchestration: MCP handler - calls shell layer directly
|
|
228
|
+
# @shell_complexity: MCP input validation + result handling
|
|
229
|
+
# @invar:allow shell_result: MCP handler for doc_find tool
|
|
230
|
+
async def _run_doc_find(args: dict[str, Any]) -> list[TextContent]:
|
|
231
|
+
"""Run invar_doc_find - find sections matching pattern."""
|
|
232
|
+
from invar.shell.doc_tools import find_sections
|
|
233
|
+
|
|
234
|
+
file_path = args.get("file", "")
|
|
235
|
+
pattern = args.get("pattern", "")
|
|
236
|
+
content_pattern = args.get("content")
|
|
237
|
+
|
|
238
|
+
if not file_path:
|
|
239
|
+
return [TextContent(type="text", text="Error: file is required")]
|
|
240
|
+
if not pattern:
|
|
241
|
+
return [TextContent(type="text", text="Error: pattern is required")]
|
|
242
|
+
|
|
243
|
+
is_valid, error = _validate_path(file_path)
|
|
244
|
+
if not is_valid:
|
|
245
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
246
|
+
|
|
247
|
+
path = Path(file_path)
|
|
248
|
+
result = find_sections(path, pattern, content_pattern)
|
|
249
|
+
|
|
250
|
+
if isinstance(result, Success):
|
|
251
|
+
sections = result.unwrap()
|
|
252
|
+
output = {
|
|
253
|
+
"matches": [
|
|
254
|
+
{
|
|
255
|
+
"path": s.path,
|
|
256
|
+
"title": s.title,
|
|
257
|
+
"level": s.level,
|
|
258
|
+
"line_start": s.line_start,
|
|
259
|
+
"line_end": s.line_end,
|
|
260
|
+
"char_count": s.char_count,
|
|
261
|
+
}
|
|
262
|
+
for s in sections
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
return [TextContent(type="text", text=json.dumps(output, indent=2))]
|
|
266
|
+
else:
|
|
267
|
+
return [TextContent(type="text", text=f"Error: {result.failure()}")]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# DX-76 Phase A-2: Extended editing handlers
|
|
271
|
+
# @shell_orchestration: MCP handler - calls shell layer directly
|
|
272
|
+
# @shell_complexity: MCP input validation + result handling
|
|
273
|
+
# @invar:allow shell_result: MCP handler for doc_replace tool
|
|
274
|
+
async def _run_doc_replace(args: dict[str, Any]) -> list[TextContent]:
|
|
275
|
+
"""Run invar_doc_replace - replace section content."""
|
|
276
|
+
from invar.shell.doc_tools import replace_section_content
|
|
277
|
+
|
|
278
|
+
file_path = args.get("file", "")
|
|
279
|
+
section_path = args.get("section", "")
|
|
280
|
+
content = args.get("content", "")
|
|
281
|
+
keep_heading = args.get("keep_heading", True)
|
|
282
|
+
|
|
283
|
+
if not file_path:
|
|
284
|
+
return [TextContent(type="text", text="Error: file is required")]
|
|
285
|
+
if not section_path:
|
|
286
|
+
return [TextContent(type="text", text="Error: section is required")]
|
|
287
|
+
if not content:
|
|
288
|
+
return [TextContent(type="text", text="Error: content is required")]
|
|
289
|
+
|
|
290
|
+
is_valid, error = _validate_path(file_path)
|
|
291
|
+
if not is_valid:
|
|
292
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
293
|
+
|
|
294
|
+
path = Path(file_path)
|
|
295
|
+
result = replace_section_content(path, section_path, content, keep_heading)
|
|
296
|
+
|
|
297
|
+
if isinstance(result, Success):
|
|
298
|
+
info = result.unwrap()
|
|
299
|
+
output = {"success": True, **info}
|
|
300
|
+
return [TextContent(type="text", text=json.dumps(output, indent=2))]
|
|
301
|
+
else:
|
|
302
|
+
return [TextContent(type="text", text=f"Error: {result.failure()}")]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# @shell_orchestration: MCP handler - calls shell layer directly
|
|
306
|
+
# @shell_complexity: MCP input validation + result handling
|
|
307
|
+
# @invar:allow shell_result: MCP handler for doc_insert tool
|
|
308
|
+
async def _run_doc_insert(args: dict[str, Any]) -> list[TextContent]:
|
|
309
|
+
"""Run invar_doc_insert - insert content relative to section."""
|
|
310
|
+
from invar.shell.doc_tools import insert_section_content
|
|
311
|
+
|
|
312
|
+
file_path = args.get("file", "")
|
|
313
|
+
anchor_path = args.get("anchor", "")
|
|
314
|
+
content = args.get("content", "")
|
|
315
|
+
position = args.get("position", "after")
|
|
316
|
+
|
|
317
|
+
if not file_path:
|
|
318
|
+
return [TextContent(type="text", text="Error: file is required")]
|
|
319
|
+
if not anchor_path:
|
|
320
|
+
return [TextContent(type="text", text="Error: anchor is required")]
|
|
321
|
+
if not content:
|
|
322
|
+
return [TextContent(type="text", text="Error: content is required")]
|
|
323
|
+
|
|
324
|
+
valid_positions = ("before", "after", "first_child", "last_child")
|
|
325
|
+
if position not in valid_positions:
|
|
326
|
+
return [TextContent(type="text", text=f"Error: position must be one of {valid_positions}")]
|
|
327
|
+
|
|
328
|
+
is_valid, error = _validate_path(file_path)
|
|
329
|
+
if not is_valid:
|
|
330
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
331
|
+
|
|
332
|
+
path = Path(file_path)
|
|
333
|
+
# Cast position to Literal type for type safety
|
|
334
|
+
pos: Literal["before", "after", "first_child", "last_child"] = position
|
|
335
|
+
result = insert_section_content(path, anchor_path, content, pos)
|
|
336
|
+
|
|
337
|
+
if isinstance(result, Success):
|
|
338
|
+
info = result.unwrap()
|
|
339
|
+
output = {"success": True, **info}
|
|
340
|
+
return [TextContent(type="text", text=json.dumps(output, indent=2))]
|
|
341
|
+
else:
|
|
342
|
+
return [TextContent(type="text", text=f"Error: {result.failure()}")]
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# @shell_orchestration: MCP handler - calls shell layer directly
|
|
346
|
+
# @shell_complexity: MCP input validation + result handling
|
|
347
|
+
# @invar:allow shell_result: MCP handler for doc_delete tool
|
|
348
|
+
async def _run_doc_delete(args: dict[str, Any]) -> list[TextContent]:
|
|
349
|
+
"""Run invar_doc_delete - delete a section."""
|
|
350
|
+
from invar.shell.doc_tools import delete_section_content
|
|
351
|
+
|
|
352
|
+
file_path = args.get("file", "")
|
|
353
|
+
section_path = args.get("section", "")
|
|
354
|
+
|
|
355
|
+
if not file_path:
|
|
356
|
+
return [TextContent(type="text", text="Error: file is required")]
|
|
357
|
+
if not section_path:
|
|
358
|
+
return [TextContent(type="text", text="Error: section is required")]
|
|
359
|
+
|
|
360
|
+
is_valid, error = _validate_path(file_path)
|
|
361
|
+
if not is_valid:
|
|
362
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
363
|
+
|
|
364
|
+
path = Path(file_path)
|
|
365
|
+
result = delete_section_content(path, section_path)
|
|
366
|
+
|
|
367
|
+
if isinstance(result, Success):
|
|
368
|
+
info = result.unwrap()
|
|
369
|
+
output = {"success": True, **info}
|
|
370
|
+
return [TextContent(type="text", text=json.dumps(output, indent=2))]
|
|
371
|
+
else:
|
|
372
|
+
return [TextContent(type="text", text=f"Error: {result.failure()}")]
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# @shell_complexity: Command execution with error handling branches
|
|
376
|
+
# @invar:allow shell_result: MCP subprocess wrapper utility
|
|
377
|
+
async def _execute_command(cmd: list[str], timeout: int = 600) -> list[TextContent]:
|
|
378
|
+
"""Execute a command and return the result.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
cmd: Command to execute
|
|
382
|
+
timeout: Maximum time in seconds (default: 600, accommodates full Guard cycle)
|
|
383
|
+
"""
|
|
384
|
+
try:
|
|
385
|
+
result = subprocess.run(
|
|
386
|
+
cmd,
|
|
387
|
+
capture_output=True,
|
|
388
|
+
text=True,
|
|
389
|
+
timeout=timeout,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
output = result.stdout
|
|
393
|
+
if result.stderr:
|
|
394
|
+
output += f"\n\nStderr:\n{result.stderr}"
|
|
395
|
+
|
|
396
|
+
# Try to parse as JSON for better formatting
|
|
397
|
+
try:
|
|
398
|
+
parsed = json.loads(result.stdout)
|
|
399
|
+
output = json.dumps(parsed, indent=2)
|
|
400
|
+
except json.JSONDecodeError:
|
|
401
|
+
pass
|
|
402
|
+
|
|
403
|
+
return [TextContent(type="text", text=output)]
|
|
404
|
+
|
|
405
|
+
except subprocess.TimeoutExpired:
|
|
406
|
+
return [TextContent(type="text", text=f"Error: Command timed out ({timeout}s)")]
|
|
407
|
+
except Exception as e:
|
|
408
|
+
return [TextContent(type="text", text=f"Error: {e}")]
|