mcp-twin 1.2.0
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/.claude-plugin/marketplace.json +23 -0
- package/LICENSE +21 -0
- package/PLUGIN_SPEC.md +388 -0
- package/README.md +306 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +215 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-detector.d.ts +53 -0
- package/dist/config-detector.d.ts.map +1 -0
- package/dist/config-detector.js +319 -0
- package/dist/config-detector.js.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +272 -0
- package/dist/index.js.map +1 -0
- package/dist/twin-manager.d.ts +40 -0
- package/dist/twin-manager.d.ts.map +1 -0
- package/dist/twin-manager.js +518 -0
- package/dist/twin-manager.js.map +1 -0
- package/examples/http-server.py +247 -0
- package/package.json +97 -0
- package/skills/twin.md +186 -0
- package/src/cli.ts +217 -0
- package/src/config-detector.ts +340 -0
- package/src/index.ts +309 -0
- package/src/twin-manager.ts +596 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Example MCP Server with HTTP Mode Support for MCP Twin
|
|
4
|
+
|
|
5
|
+
This template shows how to add HTTP mode to your MCP server for zero-downtime reloads.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
# Stdio mode (default, for Claude Code direct connection)
|
|
9
|
+
python3 http-server.py
|
|
10
|
+
|
|
11
|
+
# HTTP mode (for MCP Twin)
|
|
12
|
+
python3 http-server.py --http --port 8101
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
import json
|
|
17
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# YOUR MCP TOOLS (Replace with your actual tools)
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
def my_tool(arg1: str, arg2: int = 10) -> dict:
|
|
25
|
+
"""Example tool - replace with your actual implementation."""
|
|
26
|
+
return {
|
|
27
|
+
"ok": True,
|
|
28
|
+
"result": f"Processed {arg1} with value {arg2}"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
TOOLS = [
|
|
33
|
+
{
|
|
34
|
+
"name": "my_tool",
|
|
35
|
+
"description": "An example tool that processes input",
|
|
36
|
+
"inputSchema": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"arg1": {"type": "string", "description": "First argument"},
|
|
40
|
+
"arg2": {"type": "integer", "description": "Second argument", "default": 10}
|
|
41
|
+
},
|
|
42
|
+
"required": ["arg1"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# MCP PROTOCOL HANDLERS
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
def send_response(response: dict):
|
|
53
|
+
"""Send JSON-RPC response to stdout."""
|
|
54
|
+
output = json.dumps(response)
|
|
55
|
+
sys.stdout.write(output + "\n")
|
|
56
|
+
sys.stdout.flush()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def send_error(request_id, code: int, message: str):
|
|
60
|
+
"""Send JSON-RPC error response."""
|
|
61
|
+
send_response({
|
|
62
|
+
"jsonrpc": "2.0",
|
|
63
|
+
"id": request_id,
|
|
64
|
+
"error": {"code": code, "message": message}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def handle_initialize(request_id):
|
|
69
|
+
"""Handle MCP initialize request."""
|
|
70
|
+
send_response({
|
|
71
|
+
"jsonrpc": "2.0",
|
|
72
|
+
"id": request_id,
|
|
73
|
+
"result": {
|
|
74
|
+
"protocolVersion": "2024-11-05",
|
|
75
|
+
"capabilities": {"tools": {}},
|
|
76
|
+
"serverInfo": {"name": "my-mcp-server", "version": "1.0.0"}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def handle_list_tools(request_id):
|
|
82
|
+
"""Handle tools/list request."""
|
|
83
|
+
send_response({
|
|
84
|
+
"jsonrpc": "2.0",
|
|
85
|
+
"id": request_id,
|
|
86
|
+
"result": {"tools": TOOLS}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def handle_call_tool(request_id, params: dict):
|
|
91
|
+
"""Handle tools/call request."""
|
|
92
|
+
tool_name = params.get("name")
|
|
93
|
+
arguments = params.get("arguments", {})
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
if tool_name == "my_tool":
|
|
97
|
+
result = my_tool(**arguments)
|
|
98
|
+
else:
|
|
99
|
+
send_error(request_id, -32601, f"Unknown tool: {tool_name}")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
send_response({
|
|
103
|
+
"jsonrpc": "2.0",
|
|
104
|
+
"id": request_id,
|
|
105
|
+
"result": {
|
|
106
|
+
"content": [{"type": "text", "text": json.dumps(result)}]
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
except Exception as e:
|
|
110
|
+
send_error(request_id, -32603, str(e))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# =============================================================================
|
|
114
|
+
# STDIO MODE (Default for Claude Code)
|
|
115
|
+
# =============================================================================
|
|
116
|
+
|
|
117
|
+
def main_stdio():
|
|
118
|
+
"""Run MCP server in stdio mode."""
|
|
119
|
+
print("[INFO] MCP Server starting (stdio mode)", file=sys.stderr)
|
|
120
|
+
|
|
121
|
+
for line in sys.stdin:
|
|
122
|
+
try:
|
|
123
|
+
request = json.loads(line)
|
|
124
|
+
method = request.get("method")
|
|
125
|
+
request_id = request.get("id")
|
|
126
|
+
params = request.get("params", {})
|
|
127
|
+
|
|
128
|
+
if method == "initialize":
|
|
129
|
+
handle_initialize(request_id)
|
|
130
|
+
elif method == "tools/list":
|
|
131
|
+
handle_list_tools(request_id)
|
|
132
|
+
elif method == "tools/call":
|
|
133
|
+
handle_call_tool(request_id, params)
|
|
134
|
+
else:
|
|
135
|
+
send_error(request_id, -32601, f"Unknown method: {method}")
|
|
136
|
+
|
|
137
|
+
except json.JSONDecodeError:
|
|
138
|
+
send_error(None, -32700, "Parse error")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
print(f"[ERROR] {e}", file=sys.stderr)
|
|
141
|
+
send_error(None, -32603, str(e))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# =============================================================================
|
|
145
|
+
# HTTP MODE (For MCP Twin)
|
|
146
|
+
# =============================================================================
|
|
147
|
+
|
|
148
|
+
def handle_http_request(request_body: str) -> str:
|
|
149
|
+
"""Handle HTTP request body and return response."""
|
|
150
|
+
import io
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
request = json.loads(request_body)
|
|
154
|
+
method = request.get("method")
|
|
155
|
+
request_id = request.get("id")
|
|
156
|
+
params = request.get("params", {})
|
|
157
|
+
|
|
158
|
+
# Capture stdout
|
|
159
|
+
old_stdout = sys.stdout
|
|
160
|
+
sys.stdout = captured = io.StringIO()
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
if method == "initialize":
|
|
164
|
+
handle_initialize(request_id)
|
|
165
|
+
elif method == "tools/list":
|
|
166
|
+
handle_list_tools(request_id)
|
|
167
|
+
elif method == "tools/call":
|
|
168
|
+
handle_call_tool(request_id, params)
|
|
169
|
+
else:
|
|
170
|
+
send_error(request_id, -32601, f"Unknown method: {method}")
|
|
171
|
+
finally:
|
|
172
|
+
sys.stdout = old_stdout
|
|
173
|
+
|
|
174
|
+
return captured.getvalue().strip()
|
|
175
|
+
|
|
176
|
+
except json.JSONDecodeError:
|
|
177
|
+
return json.dumps({
|
|
178
|
+
"jsonrpc": "2.0",
|
|
179
|
+
"id": None,
|
|
180
|
+
"error": {"code": -32700, "message": "Parse error"}
|
|
181
|
+
})
|
|
182
|
+
except Exception as e:
|
|
183
|
+
return json.dumps({
|
|
184
|
+
"jsonrpc": "2.0",
|
|
185
|
+
"id": None,
|
|
186
|
+
"error": {"code": -32603, "message": str(e)}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def run_http_server(port: int):
|
|
191
|
+
"""Run MCP server in HTTP mode for MCP Twin."""
|
|
192
|
+
|
|
193
|
+
class MCPHandler(BaseHTTPRequestHandler):
|
|
194
|
+
def log_message(self, format, *args):
|
|
195
|
+
print(f"[HTTP] {args[0]}", file=sys.stderr)
|
|
196
|
+
|
|
197
|
+
def do_POST(self):
|
|
198
|
+
if self.path == "/mcp":
|
|
199
|
+
content_length = int(self.headers.get('Content-Length', 0))
|
|
200
|
+
body = self.rfile.read(content_length).decode('utf-8')
|
|
201
|
+
|
|
202
|
+
response = handle_http_request(body)
|
|
203
|
+
|
|
204
|
+
self.send_response(200)
|
|
205
|
+
self.send_header('Content-Type', 'application/json')
|
|
206
|
+
self.end_headers()
|
|
207
|
+
self.wfile.write(response.encode('utf-8'))
|
|
208
|
+
else:
|
|
209
|
+
self.send_response(404)
|
|
210
|
+
self.end_headers()
|
|
211
|
+
|
|
212
|
+
def do_GET(self):
|
|
213
|
+
if self.path == "/health":
|
|
214
|
+
self.send_response(200)
|
|
215
|
+
self.send_header('Content-Type', 'application/json')
|
|
216
|
+
self.end_headers()
|
|
217
|
+
self.wfile.write(b'{"status": "ok", "server": "my-mcp-server"}')
|
|
218
|
+
else:
|
|
219
|
+
self.send_response(404)
|
|
220
|
+
self.end_headers()
|
|
221
|
+
|
|
222
|
+
print(f"[INFO] MCP Server starting (HTTP mode) on port {port}", file=sys.stderr)
|
|
223
|
+
server = HTTPServer(('localhost', port), MCPHandler)
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
server.serve_forever()
|
|
227
|
+
except KeyboardInterrupt:
|
|
228
|
+
print("[INFO] Server stopping", file=sys.stderr)
|
|
229
|
+
server.shutdown()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# =============================================================================
|
|
233
|
+
# MAIN ENTRY POINT
|
|
234
|
+
# =============================================================================
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
import argparse
|
|
238
|
+
|
|
239
|
+
parser = argparse.ArgumentParser(description="MCP Server with HTTP Mode Support")
|
|
240
|
+
parser.add_argument("--http", action="store_true", help="Run as HTTP server (for MCP Twin)")
|
|
241
|
+
parser.add_argument("--port", type=int, default=8101, help="HTTP port (default: 8101)")
|
|
242
|
+
args = parser.parse_args()
|
|
243
|
+
|
|
244
|
+
if args.http:
|
|
245
|
+
run_http_server(args.port)
|
|
246
|
+
else:
|
|
247
|
+
main_stdio()
|
package/package.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-twin",
|
|
3
|
+
"displayName": "MCP Twin - Hot Reload",
|
|
4
|
+
"version": "1.2.0",
|
|
5
|
+
"description": "Zero-downtime updates for MCP servers. No more restarting Claude Code when you change server code.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Prax Labs",
|
|
8
|
+
"url": "https://prax.chat"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/nothinginfinity/mcp-twin"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"claude",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"mcp",
|
|
18
|
+
"hot-reload",
|
|
19
|
+
"zero-downtime",
|
|
20
|
+
"development",
|
|
21
|
+
"plugin",
|
|
22
|
+
"prax",
|
|
23
|
+
"prax-chat"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"claude-code": ">=1.0.0"
|
|
28
|
+
},
|
|
29
|
+
"main": "dist/index.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
31
|
+
"bin": {
|
|
32
|
+
"mcp-twin": "./dist/cli.js"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"dev": "tsc --watch",
|
|
37
|
+
"test": "jest"
|
|
38
|
+
},
|
|
39
|
+
"claudePlugin": {
|
|
40
|
+
"type": "skill",
|
|
41
|
+
"commands": [
|
|
42
|
+
{
|
|
43
|
+
"name": "twin",
|
|
44
|
+
"description": "Manage MCP server twins",
|
|
45
|
+
"subcommands": [
|
|
46
|
+
{
|
|
47
|
+
"name": "start",
|
|
48
|
+
"description": "Start twin servers",
|
|
49
|
+
"args": "[server]"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "stop",
|
|
53
|
+
"description": "Stop twin servers",
|
|
54
|
+
"args": "[server]"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "reload",
|
|
58
|
+
"description": "Reload standby server",
|
|
59
|
+
"args": "<server>"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "swap",
|
|
63
|
+
"description": "Switch to standby server",
|
|
64
|
+
"args": "<server>"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "status",
|
|
68
|
+
"description": "Show twin status",
|
|
69
|
+
"args": "[server]"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"hooks": [
|
|
75
|
+
{
|
|
76
|
+
"event": "file:change",
|
|
77
|
+
"pattern": "**/*_server.py",
|
|
78
|
+
"handler": "onServerFileChange"
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"permissions": [
|
|
82
|
+
"process:spawn",
|
|
83
|
+
"process:kill",
|
|
84
|
+
"fs:read",
|
|
85
|
+
"fs:write",
|
|
86
|
+
"net:localhost"
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"devDependencies": {
|
|
90
|
+
"@types/node": "^20.19.27",
|
|
91
|
+
"jest": "^29.0.0",
|
|
92
|
+
"typescript": "^5.0.0"
|
|
93
|
+
},
|
|
94
|
+
"dependencies": {
|
|
95
|
+
"chokidar": "^3.5.3"
|
|
96
|
+
}
|
|
97
|
+
}
|
package/skills/twin.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# MCP Twin Skill
|
|
2
|
+
|
|
3
|
+
name: twin
|
|
4
|
+
description: Zero-downtime MCP server management with twin/hot-swap architecture
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
|
|
7
|
+
## Triggers
|
|
8
|
+
- /twin
|
|
9
|
+
- hot reload mcp
|
|
10
|
+
- reload mcp server
|
|
11
|
+
- swap mcp server
|
|
12
|
+
- mcp twin
|
|
13
|
+
|
|
14
|
+
## Instructions
|
|
15
|
+
|
|
16
|
+
You manage MCP server twins for zero-downtime development updates.
|
|
17
|
+
|
|
18
|
+
### Core Concept
|
|
19
|
+
Twin servers = two instances of the same MCP server running on different ports.
|
|
20
|
+
- One is "active" (receiving traffic)
|
|
21
|
+
- One is "standby" (ready to take over)
|
|
22
|
+
|
|
23
|
+
When developer edits code:
|
|
24
|
+
1. Reload standby with new code
|
|
25
|
+
2. Verify standby is healthy
|
|
26
|
+
3. Swap traffic to standby
|
|
27
|
+
4. Old active becomes new standby
|
|
28
|
+
|
|
29
|
+
Result: Zero downtime, no Claude Code restart needed.
|
|
30
|
+
|
|
31
|
+
### Commands
|
|
32
|
+
|
|
33
|
+
#### /twin start [server]
|
|
34
|
+
Start twin servers for an MCP server.
|
|
35
|
+
|
|
36
|
+
If no server specified, show detected servers and let user choose.
|
|
37
|
+
Auto-detect from `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
38
|
+
|
|
39
|
+
Example output:
|
|
40
|
+
```
|
|
41
|
+
Started twins for inbox-collab
|
|
42
|
+
|
|
43
|
+
Server A: port 8101 (active)
|
|
44
|
+
Server B: port 8102 (standby)
|
|
45
|
+
|
|
46
|
+
Edit your code, then: /twin reload inbox-collab
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### /twin reload <server>
|
|
50
|
+
Reload the standby server with updated code.
|
|
51
|
+
|
|
52
|
+
1. Stop standby process
|
|
53
|
+
2. Start new process with same port
|
|
54
|
+
3. Health check
|
|
55
|
+
4. Report status
|
|
56
|
+
|
|
57
|
+
Example output:
|
|
58
|
+
```
|
|
59
|
+
Reloaded inbox-collab standby
|
|
60
|
+
|
|
61
|
+
Standby (B): port 8102
|
|
62
|
+
Health: passing
|
|
63
|
+
Ready to swap
|
|
64
|
+
|
|
65
|
+
Run: /twin swap inbox-collab
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
If health check fails:
|
|
69
|
+
```
|
|
70
|
+
Reload failed for inbox-collab
|
|
71
|
+
|
|
72
|
+
Standby (B): port 8102
|
|
73
|
+
Health: FAILED
|
|
74
|
+
|
|
75
|
+
Check logs: ~/.mcp-twin/logs/inbox-collab_b.log
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### /twin swap <server>
|
|
79
|
+
Switch traffic to the standby server.
|
|
80
|
+
|
|
81
|
+
Only swap if standby is healthy. Otherwise warn user.
|
|
82
|
+
|
|
83
|
+
Example output:
|
|
84
|
+
```
|
|
85
|
+
Swapped inbox-collab
|
|
86
|
+
|
|
87
|
+
Previous active: A (port 8101)
|
|
88
|
+
New active: B (port 8102)
|
|
89
|
+
|
|
90
|
+
Your changes are now live!
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### /twin status [server]
|
|
94
|
+
Show status of twin servers.
|
|
95
|
+
|
|
96
|
+
If server specified, show detailed view.
|
|
97
|
+
If no server, show summary of all.
|
|
98
|
+
|
|
99
|
+
Example (all):
|
|
100
|
+
```
|
|
101
|
+
MCP Twin Status
|
|
102
|
+
|
|
103
|
+
inbox-collab
|
|
104
|
+
Active: A (8101) healthy
|
|
105
|
+
Standby: B (8102) healthy
|
|
106
|
+
Reloads: 5 | Swaps: 3
|
|
107
|
+
|
|
108
|
+
phi-proxy
|
|
109
|
+
Active: B (8104) healthy
|
|
110
|
+
Standby: A (8103) healthy
|
|
111
|
+
Reloads: 2 | Swaps: 1
|
|
112
|
+
|
|
113
|
+
Not running: zti-server, fsl-compression
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### /twin stop [server]
|
|
117
|
+
Stop twin servers.
|
|
118
|
+
|
|
119
|
+
Use `--all` to stop all twins.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
```
|
|
123
|
+
Stopped inbox-collab twins
|
|
124
|
+
|
|
125
|
+
Killed: PID 12345, 12346
|
|
126
|
+
Ports freed: 8101, 8102
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Auto-Detection
|
|
130
|
+
|
|
131
|
+
Read MCP servers from Claude config:
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"inbox-collab": {
|
|
136
|
+
"command": "python3",
|
|
137
|
+
"args": ["/path/to/server.py"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Extract script path from args, use for file watching and process management.
|
|
144
|
+
|
|
145
|
+
### Error Handling
|
|
146
|
+
|
|
147
|
+
**Server not found:**
|
|
148
|
+
```
|
|
149
|
+
Unknown server: foo-bar
|
|
150
|
+
|
|
151
|
+
Available servers:
|
|
152
|
+
inbox-collab, phi-proxy, zti-server
|
|
153
|
+
|
|
154
|
+
Run: /twin start <server>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Twins not running:**
|
|
158
|
+
```
|
|
159
|
+
No twins running for inbox-collab
|
|
160
|
+
|
|
161
|
+
Start first: /twin start inbox-collab
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Standby unhealthy:**
|
|
165
|
+
```
|
|
166
|
+
Cannot swap - standby is unhealthy
|
|
167
|
+
|
|
168
|
+
Standby (B): port 8102
|
|
169
|
+
State: failed
|
|
170
|
+
|
|
171
|
+
Fix the issue and run: /twin reload inbox-collab
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Pro Features (Coming Soon)
|
|
175
|
+
|
|
176
|
+
When user runs 5+ reloads in a session:
|
|
177
|
+
```
|
|
178
|
+
You're iterating fast! Prax Chat Pro coming soon:
|
|
179
|
+
|
|
180
|
+
- Auto-reload on file save
|
|
181
|
+
- Auto-swap option
|
|
182
|
+
- Health dashboard
|
|
183
|
+
- Rollback support
|
|
184
|
+
|
|
185
|
+
[Notify Me] [Maybe Later]
|
|
186
|
+
```
|