up-cli 0.1.0__py3-none-any.whl → 0.2.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.
- up/cli.py +27 -1
- up/commands/dashboard.py +248 -0
- up/commands/learn.py +381 -0
- up/commands/new.py +108 -10
- up/commands/start.py +414 -0
- up/commands/status.py +205 -0
- up/commands/summarize.py +122 -0
- up/context.py +367 -0
- up/summarizer.py +407 -0
- up/templates/__init__.py +70 -2
- up/templates/config/__init__.py +502 -20
- up/templates/learn/__init__.py +567 -14
- up/templates/loop/__init__.py +480 -21
- up/templates/mcp/__init__.py +474 -0
- up/templates/projects/__init__.py +786 -0
- up_cli-0.2.0.dist-info/METADATA +374 -0
- up_cli-0.2.0.dist-info/RECORD +23 -0
- up_cli-0.1.0.dist-info/METADATA +0 -186
- up_cli-0.1.0.dist-info/RECORD +0 -14
- {up_cli-0.1.0.dist-info → up_cli-0.2.0.dist-info}/WHEEL +0 -0
- {up_cli-0.1.0.dist-info → up_cli-0.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) server templates."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import date
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_mcp_config(target_dir: Path, ai_target: str, force: bool = False) -> None:
|
|
8
|
+
"""Create MCP server configuration files.
|
|
9
|
+
|
|
10
|
+
Creates:
|
|
11
|
+
- .mcp/config.json - MCP server configuration
|
|
12
|
+
- .mcp/tools/ - Custom tool definitions
|
|
13
|
+
"""
|
|
14
|
+
mcp_dir = target_dir / ".mcp"
|
|
15
|
+
mcp_dir.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
|
|
17
|
+
_create_mcp_config_json(mcp_dir, force)
|
|
18
|
+
_create_mcp_tools_dir(mcp_dir, force)
|
|
19
|
+
_create_mcp_readme(mcp_dir, force)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _write_file(path: Path, content: str, force: bool) -> None:
|
|
23
|
+
"""Write file if it doesn't exist or force is True."""
|
|
24
|
+
if path.exists() and not force:
|
|
25
|
+
return
|
|
26
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
path.write_text(content)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _create_mcp_config_json(mcp_dir: Path, force: bool) -> None:
|
|
31
|
+
"""Create MCP configuration file."""
|
|
32
|
+
content = """{
|
|
33
|
+
"$schema": "https://modelcontextprotocol.io/schema/config.json",
|
|
34
|
+
"version": "1.0.0",
|
|
35
|
+
"servers": {
|
|
36
|
+
"project-tools": {
|
|
37
|
+
"description": "Project-specific tools for AI assistants",
|
|
38
|
+
"command": "python",
|
|
39
|
+
"args": ["-m", "mcp_server"],
|
|
40
|
+
"env": {},
|
|
41
|
+
"tools": [
|
|
42
|
+
"project_status",
|
|
43
|
+
"run_tests",
|
|
44
|
+
"lint_code",
|
|
45
|
+
"search_code"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"resources": {
|
|
50
|
+
"docs": {
|
|
51
|
+
"uri": "file://./docs/",
|
|
52
|
+
"description": "Project documentation"
|
|
53
|
+
},
|
|
54
|
+
"context": {
|
|
55
|
+
"uri": "file://./docs/CONTEXT.md",
|
|
56
|
+
"description": "Current project context"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"prompts": {
|
|
60
|
+
"project-overview": {
|
|
61
|
+
"description": "Get a quick project overview",
|
|
62
|
+
"template": "Read docs/CONTEXT.md and provide a summary of the current project state."
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
"""
|
|
67
|
+
_write_file(mcp_dir / "config.json", content, force)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _create_mcp_tools_dir(mcp_dir: Path, force: bool) -> None:
|
|
71
|
+
"""Create MCP tools directory with sample tools."""
|
|
72
|
+
tools_dir = mcp_dir / "tools"
|
|
73
|
+
tools_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
|
|
75
|
+
# Project status tool
|
|
76
|
+
status_tool = """{
|
|
77
|
+
"name": "project_status",
|
|
78
|
+
"description": "Get current project status including loop state and circuit breakers",
|
|
79
|
+
"inputSchema": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"verbose": {
|
|
83
|
+
"type": "boolean",
|
|
84
|
+
"description": "Include detailed information",
|
|
85
|
+
"default": false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
"""
|
|
91
|
+
_write_file(tools_dir / "project_status.json", status_tool, force)
|
|
92
|
+
|
|
93
|
+
# Run tests tool
|
|
94
|
+
tests_tool = """{
|
|
95
|
+
"name": "run_tests",
|
|
96
|
+
"description": "Run project tests with optional filtering",
|
|
97
|
+
"inputSchema": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"properties": {
|
|
100
|
+
"path": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"description": "Test file or directory path"
|
|
103
|
+
},
|
|
104
|
+
"pattern": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "Test name pattern to match"
|
|
107
|
+
},
|
|
108
|
+
"verbose": {
|
|
109
|
+
"type": "boolean",
|
|
110
|
+
"default": false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
"""
|
|
116
|
+
_write_file(tools_dir / "run_tests.json", tests_tool, force)
|
|
117
|
+
|
|
118
|
+
# Lint code tool
|
|
119
|
+
lint_tool = """{
|
|
120
|
+
"name": "lint_code",
|
|
121
|
+
"description": "Run linter on specified files or directories",
|
|
122
|
+
"inputSchema": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {
|
|
125
|
+
"path": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "File or directory to lint"
|
|
128
|
+
},
|
|
129
|
+
"fix": {
|
|
130
|
+
"type": "boolean",
|
|
131
|
+
"description": "Automatically fix issues",
|
|
132
|
+
"default": false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
"""
|
|
138
|
+
_write_file(tools_dir / "lint_code.json", lint_tool, force)
|
|
139
|
+
|
|
140
|
+
# Search code tool
|
|
141
|
+
search_tool = """{
|
|
142
|
+
"name": "search_code",
|
|
143
|
+
"description": "Search for patterns in the codebase",
|
|
144
|
+
"inputSchema": {
|
|
145
|
+
"type": "object",
|
|
146
|
+
"properties": {
|
|
147
|
+
"pattern": {
|
|
148
|
+
"type": "string",
|
|
149
|
+
"description": "Search pattern (regex supported)"
|
|
150
|
+
},
|
|
151
|
+
"file_pattern": {
|
|
152
|
+
"type": "string",
|
|
153
|
+
"description": "File glob pattern to search",
|
|
154
|
+
"default": "**/*"
|
|
155
|
+
},
|
|
156
|
+
"context_lines": {
|
|
157
|
+
"type": "integer",
|
|
158
|
+
"description": "Number of context lines",
|
|
159
|
+
"default": 2
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"required": ["pattern"]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
"""
|
|
166
|
+
_write_file(tools_dir / "search_code.json", search_tool, force)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _create_mcp_readme(mcp_dir: Path, force: bool) -> None:
|
|
170
|
+
"""Create MCP README with usage instructions."""
|
|
171
|
+
content = """# MCP (Model Context Protocol) Configuration
|
|
172
|
+
|
|
173
|
+
This directory contains MCP server configuration for enhanced AI assistant integration.
|
|
174
|
+
|
|
175
|
+
## Overview
|
|
176
|
+
|
|
177
|
+
MCP provides a standardized way to expose tools, resources, and prompts to AI assistants
|
|
178
|
+
like Claude and Cursor.
|
|
179
|
+
|
|
180
|
+
## Structure
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
.mcp/
|
|
184
|
+
├── config.json # Main MCP configuration
|
|
185
|
+
├── tools/ # Tool definitions
|
|
186
|
+
│ ├── project_status.json
|
|
187
|
+
│ ├── run_tests.json
|
|
188
|
+
│ ├── lint_code.json
|
|
189
|
+
│ └── search_code.json
|
|
190
|
+
└── README.md # This file
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Configuration
|
|
194
|
+
|
|
195
|
+
### config.json
|
|
196
|
+
|
|
197
|
+
The main configuration file defines:
|
|
198
|
+
|
|
199
|
+
- **servers**: MCP server definitions
|
|
200
|
+
- **resources**: Read-only data sources
|
|
201
|
+
- **prompts**: Pre-defined prompt templates
|
|
202
|
+
|
|
203
|
+
### Tools
|
|
204
|
+
|
|
205
|
+
Each tool in `tools/` is a JSON file with:
|
|
206
|
+
|
|
207
|
+
- `name`: Tool identifier
|
|
208
|
+
- `description`: What the tool does
|
|
209
|
+
- `inputSchema`: JSON Schema for parameters
|
|
210
|
+
|
|
211
|
+
## Usage with Claude Code
|
|
212
|
+
|
|
213
|
+
Claude Code automatically detects MCP configuration.
|
|
214
|
+
Tools become available as `/tool_name` commands.
|
|
215
|
+
|
|
216
|
+
## Usage with Cursor
|
|
217
|
+
|
|
218
|
+
Add to your Cursor settings:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"mcp.servers": {
|
|
223
|
+
"project": {
|
|
224
|
+
"configPath": ".mcp/config.json"
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Implementing Tool Handlers
|
|
231
|
+
|
|
232
|
+
Create a Python MCP server:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from mcp import Server, Tool
|
|
236
|
+
|
|
237
|
+
server = Server("project-tools")
|
|
238
|
+
|
|
239
|
+
@server.tool("project_status")
|
|
240
|
+
async def project_status(verbose: bool = False):
|
|
241
|
+
# Implementation
|
|
242
|
+
return {"status": "healthy"}
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
server.run()
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Resources
|
|
249
|
+
|
|
250
|
+
- [MCP Documentation](https://modelcontextprotocol.io/docs)
|
|
251
|
+
- [MCP Python SDK](https://github.com/anthropics/mcp-python-sdk)
|
|
252
|
+
"""
|
|
253
|
+
_write_file(mcp_dir / "README.md", content, force)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def create_mcp_server_stub(target_dir: Path, force: bool = False) -> None:
|
|
257
|
+
"""Create a stub MCP server implementation."""
|
|
258
|
+
content = '''#!/usr/bin/env python3
|
|
259
|
+
"""
|
|
260
|
+
MCP Server for Project Tools
|
|
261
|
+
|
|
262
|
+
A Model Context Protocol server that provides project-specific tools
|
|
263
|
+
to AI assistants.
|
|
264
|
+
|
|
265
|
+
Usage:
|
|
266
|
+
python -m mcp_server
|
|
267
|
+
|
|
268
|
+
Or install as a package and run:
|
|
269
|
+
up-mcp-server
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
import json
|
|
273
|
+
import subprocess
|
|
274
|
+
from pathlib import Path
|
|
275
|
+
from typing import Any
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class MCPServer:
|
|
279
|
+
"""Simple MCP server implementation."""
|
|
280
|
+
|
|
281
|
+
def __init__(self, workspace: Path = None):
|
|
282
|
+
self.workspace = workspace or Path.cwd()
|
|
283
|
+
self.tools = {
|
|
284
|
+
"project_status": self.project_status,
|
|
285
|
+
"run_tests": self.run_tests,
|
|
286
|
+
"lint_code": self.lint_code,
|
|
287
|
+
"search_code": self.search_code,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async def project_status(self, verbose: bool = False) -> dict:
|
|
291
|
+
"""Get project status."""
|
|
292
|
+
status = {"healthy": True, "systems": {}}
|
|
293
|
+
|
|
294
|
+
# Check loop state
|
|
295
|
+
loop_file = self.workspace / ".loop_state.json"
|
|
296
|
+
if loop_file.exists():
|
|
297
|
+
try:
|
|
298
|
+
data = json.loads(loop_file.read_text())
|
|
299
|
+
status["systems"]["loop"] = {
|
|
300
|
+
"iteration": data.get("iteration", 0),
|
|
301
|
+
"phase": data.get("phase", "UNKNOWN"),
|
|
302
|
+
}
|
|
303
|
+
except json.JSONDecodeError:
|
|
304
|
+
status["systems"]["loop"] = {"error": "Invalid state"}
|
|
305
|
+
|
|
306
|
+
# Check context budget
|
|
307
|
+
context_file = self.workspace / ".claude/context_budget.json"
|
|
308
|
+
if context_file.exists():
|
|
309
|
+
try:
|
|
310
|
+
data = json.loads(context_file.read_text())
|
|
311
|
+
status["systems"]["context"] = {
|
|
312
|
+
"usage_percent": data.get("usage_percent", 0),
|
|
313
|
+
"status": data.get("status", "OK"),
|
|
314
|
+
}
|
|
315
|
+
except json.JSONDecodeError:
|
|
316
|
+
status["systems"]["context"] = {"error": "Invalid state"}
|
|
317
|
+
|
|
318
|
+
if verbose:
|
|
319
|
+
status["workspace"] = str(self.workspace)
|
|
320
|
+
status["initialized"] = (self.workspace / ".claude").exists()
|
|
321
|
+
|
|
322
|
+
return status
|
|
323
|
+
|
|
324
|
+
async def run_tests(
|
|
325
|
+
self,
|
|
326
|
+
path: str = None,
|
|
327
|
+
pattern: str = None,
|
|
328
|
+
verbose: bool = False
|
|
329
|
+
) -> dict:
|
|
330
|
+
"""Run tests."""
|
|
331
|
+
cmd = ["pytest"]
|
|
332
|
+
|
|
333
|
+
if path:
|
|
334
|
+
cmd.append(path)
|
|
335
|
+
|
|
336
|
+
if pattern:
|
|
337
|
+
cmd.extend(["-k", pattern])
|
|
338
|
+
|
|
339
|
+
if verbose:
|
|
340
|
+
cmd.append("-v")
|
|
341
|
+
else:
|
|
342
|
+
cmd.append("-q")
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
result = subprocess.run(
|
|
346
|
+
cmd,
|
|
347
|
+
cwd=self.workspace,
|
|
348
|
+
capture_output=True,
|
|
349
|
+
text=True,
|
|
350
|
+
timeout=120,
|
|
351
|
+
)
|
|
352
|
+
return {
|
|
353
|
+
"success": result.returncode == 0,
|
|
354
|
+
"output": result.stdout,
|
|
355
|
+
"errors": result.stderr,
|
|
356
|
+
}
|
|
357
|
+
except subprocess.TimeoutExpired:
|
|
358
|
+
return {"success": False, "error": "Test timeout"}
|
|
359
|
+
except FileNotFoundError:
|
|
360
|
+
return {"success": False, "error": "pytest not found"}
|
|
361
|
+
|
|
362
|
+
async def lint_code(self, path: str = None, fix: bool = False) -> dict:
|
|
363
|
+
"""Run linter."""
|
|
364
|
+
cmd = ["ruff", "check"]
|
|
365
|
+
|
|
366
|
+
if path:
|
|
367
|
+
cmd.append(path)
|
|
368
|
+
else:
|
|
369
|
+
cmd.append(".")
|
|
370
|
+
|
|
371
|
+
if fix:
|
|
372
|
+
cmd.append("--fix")
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
result = subprocess.run(
|
|
376
|
+
cmd,
|
|
377
|
+
cwd=self.workspace,
|
|
378
|
+
capture_output=True,
|
|
379
|
+
text=True,
|
|
380
|
+
timeout=60,
|
|
381
|
+
)
|
|
382
|
+
return {
|
|
383
|
+
"success": result.returncode == 0,
|
|
384
|
+
"output": result.stdout,
|
|
385
|
+
"errors": result.stderr,
|
|
386
|
+
}
|
|
387
|
+
except FileNotFoundError:
|
|
388
|
+
return {"success": False, "error": "ruff not found"}
|
|
389
|
+
|
|
390
|
+
async def search_code(
|
|
391
|
+
self,
|
|
392
|
+
pattern: str,
|
|
393
|
+
file_pattern: str = "**/*",
|
|
394
|
+
context_lines: int = 2
|
|
395
|
+
) -> dict:
|
|
396
|
+
"""Search code for pattern."""
|
|
397
|
+
cmd = [
|
|
398
|
+
"rg", # ripgrep
|
|
399
|
+
"-n", # line numbers
|
|
400
|
+
f"-C{context_lines}", # context
|
|
401
|
+
"-g", file_pattern,
|
|
402
|
+
pattern,
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
result = subprocess.run(
|
|
407
|
+
cmd,
|
|
408
|
+
cwd=self.workspace,
|
|
409
|
+
capture_output=True,
|
|
410
|
+
text=True,
|
|
411
|
+
timeout=30,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
matches = []
|
|
415
|
+
if result.stdout:
|
|
416
|
+
for line in result.stdout.strip().split("\\n"):
|
|
417
|
+
matches.append(line)
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
"success": True,
|
|
421
|
+
"match_count": len([m for m in matches if ":" in m]),
|
|
422
|
+
"matches": matches[:100], # Limit results
|
|
423
|
+
}
|
|
424
|
+
except FileNotFoundError:
|
|
425
|
+
return {"success": False, "error": "ripgrep not found"}
|
|
426
|
+
|
|
427
|
+
async def handle_request(self, request: dict) -> dict:
|
|
428
|
+
"""Handle incoming MCP request."""
|
|
429
|
+
method = request.get("method")
|
|
430
|
+
params = request.get("params", {})
|
|
431
|
+
|
|
432
|
+
if method == "tools/list":
|
|
433
|
+
return {
|
|
434
|
+
"tools": [
|
|
435
|
+
{"name": name, "description": func.__doc__}
|
|
436
|
+
for name, func in self.tools.items()
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if method == "tools/call":
|
|
441
|
+
tool_name = params.get("name")
|
|
442
|
+
tool_args = params.get("arguments", {})
|
|
443
|
+
|
|
444
|
+
if tool_name in self.tools:
|
|
445
|
+
result = await self.tools[tool_name](**tool_args)
|
|
446
|
+
return {"result": result}
|
|
447
|
+
else:
|
|
448
|
+
return {"error": f"Unknown tool: {tool_name}"}
|
|
449
|
+
|
|
450
|
+
return {"error": f"Unknown method: {method}"}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
async def main():
|
|
454
|
+
"""Run MCP server."""
|
|
455
|
+
import sys
|
|
456
|
+
|
|
457
|
+
server = MCPServer()
|
|
458
|
+
|
|
459
|
+
# Simple stdin/stdout protocol for demonstration
|
|
460
|
+
# In production, use the official MCP SDK
|
|
461
|
+
for line in sys.stdin:
|
|
462
|
+
try:
|
|
463
|
+
request = json.loads(line)
|
|
464
|
+
response = await server.handle_request(request)
|
|
465
|
+
print(json.dumps(response), flush=True)
|
|
466
|
+
except json.JSONDecodeError:
|
|
467
|
+
print(json.dumps({"error": "Invalid JSON"}), flush=True)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
if __name__ == "__main__":
|
|
471
|
+
import asyncio
|
|
472
|
+
asyncio.run(main())
|
|
473
|
+
'''
|
|
474
|
+
_write_file(target_dir / "mcp_server.py", content, force)
|