tylor-mcp 1.0.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/.aws-setup.sh +25 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.mcp.json +12 -0
- package/AGENTS.md +93 -0
- package/CLAUDE.md +99 -0
- package/CLAUDE_PLATFORM_AWS_SETUP.md +105 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/assets/tylor_logo.png +0 -0
- package/assets/tylor_threads_concept.png +0 -0
- package/bin/tylor.js +23 -0
- package/hooks/kill-thread-trigger.sh +7 -0
- package/hooks/post-tool-use-code-index.sh +7 -0
- package/hooks/session-checkpoint.sh +7 -0
- package/hooks/session-start.sh +7 -0
- package/install.py +401 -0
- package/install.sh +260 -0
- package/package.json +24 -0
- package/pytest.ini +2 -0
- package/registry.json +26 -0
- package/server/.env.example +24 -0
- package/server/__init__.py +0 -0
- package/server/config.py +89 -0
- package/server/main.py +93 -0
- package/server/personas/analyst.md +15 -0
- package/server/personas/ceo.md +14 -0
- package/server/personas/code_agent.md +15 -0
- package/server/personas/cto.md +14 -0
- package/server/provision.py +260 -0
- package/server/provision_opensearch.py +154 -0
- package/server/requirements.txt +26 -0
- package/server/storage/__init__.py +0 -0
- package/server/storage/dynamo.py +399 -0
- package/server/storage/json_store.py +359 -0
- package/server/storage/opensearch.py +194 -0
- package/server/storage/s3.py +96 -0
- package/server/storage/tests/__init__.py +0 -0
- package/server/storage/tests/test_dynamo.py +452 -0
- package/server/storage/tests/test_json_store.py +226 -0
- package/server/storage/tests/test_opensearch.py +270 -0
- package/server/storage/tests/test_s3.py +125 -0
- package/server/tests/__init__.py +0 -0
- package/server/tests/test_install.py +606 -0
- package/server/tests/test_isolation.py +90 -0
- package/server/tests/test_ui_server.py +385 -0
- package/server/tests/test_ui_shader_background.py +52 -0
- package/server/tests/test_ui_story_6_3.py +105 -0
- package/server/tools/__init__.py +0 -0
- package/server/tools/_mcp.py +4 -0
- package/server/tools/agents.py +160 -0
- package/server/tools/ecc/__init__.py +1 -0
- package/server/tools/ecc/data.py +35 -0
- package/server/tools/ecc/diagrams.py +23 -0
- package/server/tools/ecc/pipeline.py +24 -0
- package/server/tools/ecc/presentation.py +24 -0
- package/server/tools/ecc/web.py +23 -0
- package/server/tools/executor.py +880 -0
- package/server/tools/harness.py +330 -0
- package/server/tools/help.py +162 -0
- package/server/tools/hooks.py +357 -0
- package/server/tools/personas.py +110 -0
- package/server/tools/registry.py +195 -0
- package/server/tools/router.py +117 -0
- package/server/tools/skill_installer.py +230 -0
- package/server/tools/summarizer.py +168 -0
- package/server/tools/tests/__init__.py +0 -0
- package/server/tools/tests/test_agents.py +246 -0
- package/server/tools/tests/test_code_index.py +108 -0
- package/server/tools/tests/test_ecc_tools.py +51 -0
- package/server/tools/tests/test_executor.py +584 -0
- package/server/tools/tests/test_help_agent101.py +149 -0
- package/server/tools/tests/test_hooks.py +124 -0
- package/server/tools/tests/test_kill_thread.py +125 -0
- package/server/tools/tests/test_new_thread_list_threads.py +293 -0
- package/server/tools/tests/test_personas.py +52 -0
- package/server/tools/tests/test_recall_memory.py +55 -0
- package/server/tools/tests/test_registry_client.py +308 -0
- package/server/tools/tests/test_router.py +263 -0
- package/server/tools/tests/test_skill_installer.py +174 -0
- package/server/tools/tests/test_switch_thread.py +163 -0
- package/server/tools/tests/test_thread_command_skills.py +54 -0
- package/server/tools/tests/test_thread_resolver.py +165 -0
- package/server/tools/tests/test_tier1_schema.py +296 -0
- package/server/tools/thread_resolver.py +75 -0
- package/server/tools/tylor.py +374 -0
- package/server/tools/ui.py +38 -0
- package/server/ui_server.py +292 -0
- package/server/validate.py +237 -0
- package/skills/add-skill/SKILL.md +37 -0
- package/skills/afk-status/SKILL.md +20 -0
- package/skills/bmad/SKILL.md +14 -0
- package/skills/help-agent101/SKILL.md +48 -0
- package/skills/kill-thread/SKILL.md +35 -0
- package/skills/list-threads/SKILL.md +35 -0
- package/skills/new-thread/SKILL.md +35 -0
- package/skills/recall/SKILL.md +39 -0
- package/skills/run/SKILL.md +33 -0
- package/skills/set-sandbox/SKILL.md +38 -0
- package/skills/switch-thread/SKILL.md +38 -0
- package/ui/claude-logo.png +0 -0
- package/ui/index.html +1314 -0
package/install.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tylor installer — patches all Claude clients on Mac / Windows / Linux / WSL.
|
|
4
|
+
|
|
5
|
+
Clients patched:
|
|
6
|
+
1. Claude Code CLI → ~/.claude/settings.json
|
|
7
|
+
2. Claude Code VSCode ext → ~/.claude/settings.json (same file as CLI)
|
|
8
|
+
3. Claude Desktop Mac → ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
9
|
+
4. Claude Desktop Windows → %APPDATA%/Claude/claude_desktop_config.json
|
|
10
|
+
5. Claude Desktop Linux → ~/.config/Claude/claude_desktop_config.json
|
|
11
|
+
6. GitHub Copilot CLI → ~/.copilot/mcp.json
|
|
12
|
+
7. Antigravity → ~/.gemini/antigravity/mcp_config.json
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python3 install.py # default: project JSON storage
|
|
16
|
+
python3 install.py --dynamo # use DynamoDB storage
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import platform
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
PLUGIN_DIR = Path(__file__).resolve().parent
|
|
28
|
+
SERVER_MAIN = PLUGIN_DIR / "server" / "main.py"
|
|
29
|
+
REQUIREMENTS = PLUGIN_DIR / "server" / "requirements.txt"
|
|
30
|
+
VENV_DIR = Path.home() / ".tylor" / "venv"
|
|
31
|
+
|
|
32
|
+
GREEN = "\033[92m"
|
|
33
|
+
RED = "\033[91m"
|
|
34
|
+
YELLOW = "\033[93m"
|
|
35
|
+
BOLD = "\033[1m"
|
|
36
|
+
RESET = "\033[0m"
|
|
37
|
+
|
|
38
|
+
def ok(msg): print(f" {GREEN}✓{RESET} {msg}")
|
|
39
|
+
def fail(msg): print(f" {RED}✗{RESET} {msg}")
|
|
40
|
+
def warn(msg): print(f" {YELLOW}⚠{RESET} {msg}")
|
|
41
|
+
def header(msg): print(f"\n{BOLD}{msg}{RESET}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ── Platform detection ────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
def is_windows() -> bool:
|
|
47
|
+
return platform.system() == "Windows" or "microsoft" in platform.uname().release.lower()
|
|
48
|
+
|
|
49
|
+
def is_mac() -> bool:
|
|
50
|
+
return platform.system() == "Darwin"
|
|
51
|
+
|
|
52
|
+
def is_linux() -> bool:
|
|
53
|
+
return platform.system() == "Linux"
|
|
54
|
+
|
|
55
|
+
def is_wsl() -> bool:
|
|
56
|
+
return is_linux() and "microsoft" in platform.uname().release.lower()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── Config file locations ─────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def claude_code_settings() -> Path:
|
|
62
|
+
"""Claude Code CLI + VSCode extension — ~/.claude/settings.json on all platforms."""
|
|
63
|
+
return Path.home() / ".claude" / "settings.json"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def claude_desktop_configs() -> list[Path]:
|
|
67
|
+
"""All possible Claude Desktop config file locations."""
|
|
68
|
+
candidates = []
|
|
69
|
+
if is_mac():
|
|
70
|
+
candidates.append(
|
|
71
|
+
Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
72
|
+
)
|
|
73
|
+
if is_windows():
|
|
74
|
+
appdata = os.environ.get("APPDATA", "")
|
|
75
|
+
if appdata:
|
|
76
|
+
candidates.append(Path(appdata) / "Claude" / "claude_desktop_config.json")
|
|
77
|
+
if is_linux() or is_wsl():
|
|
78
|
+
candidates.append(Path.home() / ".config" / "Claude" / "claude_desktop_config.json")
|
|
79
|
+
# WSL might also access Windows AppData
|
|
80
|
+
if is_wsl():
|
|
81
|
+
try:
|
|
82
|
+
win_appdata = subprocess.check_output(
|
|
83
|
+
["cmd.exe", "/c", "echo %APPDATA%"],
|
|
84
|
+
text=True, stderr=subprocess.DEVNULL
|
|
85
|
+
).strip()
|
|
86
|
+
if win_appdata:
|
|
87
|
+
wsl_path = subprocess.check_output(
|
|
88
|
+
["wslpath", win_appdata],
|
|
89
|
+
text=True, stderr=subprocess.DEVNULL
|
|
90
|
+
).strip()
|
|
91
|
+
candidates.append(
|
|
92
|
+
Path(wsl_path) / "Claude" / "claude_desktop_config.json"
|
|
93
|
+
)
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
return [p for p in candidates if p.parent.exists() or p.exists()]
|
|
97
|
+
|
|
98
|
+
def github_copilot_configs() -> list[Path]:
|
|
99
|
+
"""GitHub Copilot CLI config file locations."""
|
|
100
|
+
return [Path.home() / ".copilot" / "mcp.json"]
|
|
101
|
+
|
|
102
|
+
def antigravity_configs() -> list[Path]:
|
|
103
|
+
"""Antigravity agent config file locations."""
|
|
104
|
+
return [Path.home() / ".gemini" / "antigravity" / "mcp_config.json"]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ── Python / venv setup ───────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
def find_python() -> str:
|
|
110
|
+
"""Find a Python 3.8+ executable."""
|
|
111
|
+
for candidate in ("python3", "python", sys.executable):
|
|
112
|
+
try:
|
|
113
|
+
out = subprocess.check_output(
|
|
114
|
+
[candidate, "-c", "import sys; print(sys.version_info[:2])"],
|
|
115
|
+
text=True, stderr=subprocess.DEVNULL
|
|
116
|
+
).strip()
|
|
117
|
+
major, minor = eval(out)
|
|
118
|
+
if (major, minor) >= (3, 8):
|
|
119
|
+
return candidate
|
|
120
|
+
except Exception:
|
|
121
|
+
continue
|
|
122
|
+
return sys.executable
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def setup_venv() -> Path:
|
|
126
|
+
"""Create venv at ~/.tylor/venv and install deps. Returns python path."""
|
|
127
|
+
header("Setting up Python environment")
|
|
128
|
+
|
|
129
|
+
python = find_python()
|
|
130
|
+
py_in_venv = VENV_DIR / ("Scripts" if is_windows() else "bin") / ("python.exe" if is_windows() else "python3")
|
|
131
|
+
|
|
132
|
+
if not py_in_venv.exists():
|
|
133
|
+
print(f" Creating venv at {VENV_DIR} ...")
|
|
134
|
+
subprocess.run([python, "-m", "venv", str(VENV_DIR)], check=True)
|
|
135
|
+
|
|
136
|
+
pip = VENV_DIR / ("Scripts" if is_windows() else "bin") / ("pip.exe" if is_windows() else "pip")
|
|
137
|
+
print(f" Installing dependencies ...")
|
|
138
|
+
result = subprocess.run(
|
|
139
|
+
[str(pip), "install", "-q", "-r", str(REQUIREMENTS)],
|
|
140
|
+
capture_output=True, text=True
|
|
141
|
+
)
|
|
142
|
+
if result.returncode != 0:
|
|
143
|
+
fail(f"pip install failed:\n{result.stderr[-500:]}")
|
|
144
|
+
return py_in_venv
|
|
145
|
+
ok(f"Python environment ready ({py_in_venv})")
|
|
146
|
+
return py_in_venv
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── MCP server entry ──────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
def mcp_server_entry(python_path: Path) -> dict:
|
|
152
|
+
"""Build the mcpServers entry for any config file."""
|
|
153
|
+
# Use forward slashes everywhere — works on Mac/Linux/WSL
|
|
154
|
+
# Windows: Claude Desktop accepts forward slashes in JSON
|
|
155
|
+
py = python_path.as_posix()
|
|
156
|
+
main = SERVER_MAIN.as_posix()
|
|
157
|
+
cwd = PLUGIN_DIR.as_posix()
|
|
158
|
+
return {
|
|
159
|
+
"command": py,
|
|
160
|
+
"args": ["-m", "server.main"],
|
|
161
|
+
"env": {"PYTHONPATH": cwd},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ── Hooks entries ─────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
def hooks_entries() -> dict:
|
|
168
|
+
"""Build the hooks section. Shell scripts on Mac/Linux, Python on Windows."""
|
|
169
|
+
hooks_dir = PLUGIN_DIR / "hooks"
|
|
170
|
+
if is_windows():
|
|
171
|
+
# Windows: run hooks as Python scripts (no bash)
|
|
172
|
+
py = (VENV_DIR / "Scripts" / "python.exe").as_posix()
|
|
173
|
+
server_dir = (PLUGIN_DIR / "server").as_posix()
|
|
174
|
+
def win_hook(cmd: str) -> str:
|
|
175
|
+
return f"{py} -c \"import sys; sys.path.insert(0,'{server_dir}'); from server.tools.hooks import main; sys.argv=['hooks','{cmd}']; main()\""
|
|
176
|
+
return {
|
|
177
|
+
"SessionStart": [{"type": "command", "command": win_hook("session-start")}],
|
|
178
|
+
"Stop": [{"type": "command", "command": win_hook("session-checkpoint")}],
|
|
179
|
+
"PostToolUse": [
|
|
180
|
+
{"matcher": "kill_thread",
|
|
181
|
+
"hooks": [{"type": "command", "command": win_hook("kill-thread-trigger")}]},
|
|
182
|
+
{"matcher": "Read|Write|Edit|MultiEdit",
|
|
183
|
+
"hooks": [{"type": "command", "command": win_hook("post-tool-use-code-index")}]}
|
|
184
|
+
],
|
|
185
|
+
}
|
|
186
|
+
else:
|
|
187
|
+
def sh(name: str) -> str:
|
|
188
|
+
return (hooks_dir / name).as_posix()
|
|
189
|
+
return {
|
|
190
|
+
"SessionStart": [{"type": "command", "command": sh("session-start.sh")}],
|
|
191
|
+
"Stop": [{"type": "command", "command": sh("session-checkpoint.sh")}],
|
|
192
|
+
"PostToolUse": [
|
|
193
|
+
{"matcher": "kill_thread",
|
|
194
|
+
"hooks": [{"type": "command", "command": sh("kill-thread-trigger.sh")}]},
|
|
195
|
+
{"matcher": "Read|Write|Edit|MultiEdit",
|
|
196
|
+
"hooks": [{"type": "command", "command": sh("post-tool-use-code-index.sh")}]}
|
|
197
|
+
],
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ── Patch a single config file ────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
def patch_config(config_path: Path, python_path: Path, is_desktop: bool = False) -> bool:
|
|
204
|
+
"""
|
|
205
|
+
Patch an existing Claude config file with Tylor's MCP server + hooks.
|
|
206
|
+
Creates the file if it doesn't exist.
|
|
207
|
+
Returns True on success.
|
|
208
|
+
"""
|
|
209
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
|
|
211
|
+
settings: dict = {}
|
|
212
|
+
if config_path.exists():
|
|
213
|
+
try:
|
|
214
|
+
settings = json.loads(config_path.read_text(encoding="utf-8"))
|
|
215
|
+
except json.JSONDecodeError:
|
|
216
|
+
warn(f"Could not parse {config_path} — will overwrite")
|
|
217
|
+
settings = {}
|
|
218
|
+
|
|
219
|
+
# MCP server
|
|
220
|
+
servers = settings.setdefault("mcpServers", {})
|
|
221
|
+
servers["agent101"] = mcp_server_entry(python_path)
|
|
222
|
+
|
|
223
|
+
# Hooks — Claude Desktop doesn't support hooks, only Claude Code CLI/VSCode
|
|
224
|
+
if not is_desktop:
|
|
225
|
+
new_hooks = hooks_entries()
|
|
226
|
+
existing_hooks = settings.setdefault("hooks", {})
|
|
227
|
+
for event, entries in new_hooks.items():
|
|
228
|
+
event_list = existing_hooks.setdefault(event, [])
|
|
229
|
+
for entry in entries:
|
|
230
|
+
cmd = entry.get("command", "")
|
|
231
|
+
if not any(e.get("command") == cmd for e in event_list):
|
|
232
|
+
event_list.append(entry)
|
|
233
|
+
|
|
234
|
+
# Write atomically
|
|
235
|
+
tmp = config_path.with_suffix(".tmp")
|
|
236
|
+
try:
|
|
237
|
+
tmp.write_text(json.dumps(settings, indent=2), encoding="utf-8")
|
|
238
|
+
os.replace(tmp, config_path)
|
|
239
|
+
return True
|
|
240
|
+
except OSError as e:
|
|
241
|
+
fail(f"Could not write {config_path}: {e}")
|
|
242
|
+
tmp.unlink(missing_ok=True)
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ── Validate server starts ────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
def validate(python_path: Path) -> bool:
|
|
249
|
+
result = subprocess.run(
|
|
250
|
+
[str(python_path), "-c",
|
|
251
|
+
f"import sys; sys.path.insert(0,{str(PLUGIN_DIR)!r}); "
|
|
252
|
+
f"from server.tools._mcp import mcp; assert mcp.name=='agent101'"],
|
|
253
|
+
capture_output=True, text=True, cwd=str(PLUGIN_DIR)
|
|
254
|
+
)
|
|
255
|
+
if result.returncode == 0:
|
|
256
|
+
ok("MCP server validates correctly (name: agent101)")
|
|
257
|
+
return True
|
|
258
|
+
fail(f"Server validation failed: {result.stderr.strip()[-300:]}")
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ── Storage config ────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
def _bundle_bmad() -> None:
|
|
265
|
+
"""
|
|
266
|
+
Clone or update BMAD into ~/.tylor/bmad so the harness can use its
|
|
267
|
+
workflows silently. BMAD is never exposed directly to the user —
|
|
268
|
+
the harness activates it based on thread context.
|
|
269
|
+
"""
|
|
270
|
+
import subprocess
|
|
271
|
+
bmad_dir = Path.home() / ".tylor" / "bmad"
|
|
272
|
+
bmad_repo = "https://github.com/bmadcode/BMAD-METHOD"
|
|
273
|
+
|
|
274
|
+
if bmad_dir.exists():
|
|
275
|
+
# Already installed — pull latest silently
|
|
276
|
+
result = subprocess.run(
|
|
277
|
+
["git", "-C", str(bmad_dir), "pull", "--quiet"],
|
|
278
|
+
capture_output=True, text=True
|
|
279
|
+
)
|
|
280
|
+
if result.returncode == 0:
|
|
281
|
+
ok("BMAD updated")
|
|
282
|
+
else:
|
|
283
|
+
warn("BMAD update skipped (no internet or git not available)")
|
|
284
|
+
else:
|
|
285
|
+
# First install — try to clone
|
|
286
|
+
result = subprocess.run(
|
|
287
|
+
["git", "clone", "--quiet", "--depth=1", bmad_repo, str(bmad_dir)],
|
|
288
|
+
capture_output=True, text=True
|
|
289
|
+
)
|
|
290
|
+
if result.returncode == 0:
|
|
291
|
+
ok(f"BMAD bundled at {bmad_dir}")
|
|
292
|
+
else:
|
|
293
|
+
warn("BMAD not available (no internet or git not found) — harness will work without it")
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
# Point harness to BMAD location via config
|
|
297
|
+
config_file = Path.home() / ".tylor" / "config.json"
|
|
298
|
+
try:
|
|
299
|
+
import json
|
|
300
|
+
cfg = json.loads(config_file.read_text()) if config_file.exists() else {}
|
|
301
|
+
cfg["bmad_path"] = bmad_dir.as_posix()
|
|
302
|
+
config_file.write_text(json.dumps(cfg, indent=2))
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def configure_storage(use_dynamo: bool) -> None:
|
|
308
|
+
config_dir = Path.home() / ".tylor"
|
|
309
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
310
|
+
cfg: dict = {}
|
|
311
|
+
cfg_file = config_dir / "config.json"
|
|
312
|
+
if cfg_file.exists():
|
|
313
|
+
try:
|
|
314
|
+
cfg = json.loads(cfg_file.read_text())
|
|
315
|
+
except Exception:
|
|
316
|
+
cfg = {}
|
|
317
|
+
if use_dynamo:
|
|
318
|
+
cfg["storage_mode"] = "personal"
|
|
319
|
+
ok("Storage mode: Personal (AWS DynamoDB)")
|
|
320
|
+
else:
|
|
321
|
+
cfg["storage_mode"] = "project"
|
|
322
|
+
cfg["storage_path"] = (config_dir / "threads.json").as_posix()
|
|
323
|
+
ok("Storage mode: Project (local JSON, no AWS needed)")
|
|
324
|
+
cfg_file.write_text(json.dumps(cfg, indent=2))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
def main() -> None:
|
|
330
|
+
use_dynamo = "--dynamo" in sys.argv
|
|
331
|
+
|
|
332
|
+
print(f"\n{BOLD} Tylor installer{RESET}")
|
|
333
|
+
print(f" {'─' * 50}")
|
|
334
|
+
print(f" Platform : {platform.system()} {'(WSL)' if is_wsl() else ''}")
|
|
335
|
+
print(f" Plugin : {PLUGIN_DIR}")
|
|
336
|
+
|
|
337
|
+
# Step 1: Python venv
|
|
338
|
+
python_path = setup_venv()
|
|
339
|
+
|
|
340
|
+
# Step 2: Storage config
|
|
341
|
+
header("Configuring storage")
|
|
342
|
+
configure_storage(use_dynamo)
|
|
343
|
+
|
|
344
|
+
# Step 3: Patch Claude Code CLI + VSCode (same settings.json)
|
|
345
|
+
header("Patching Claude Code CLI / VSCode extension")
|
|
346
|
+
cli_path = claude_code_settings()
|
|
347
|
+
if patch_config(cli_path, python_path, is_desktop=False):
|
|
348
|
+
ok(f"Patched {cli_path}")
|
|
349
|
+
else:
|
|
350
|
+
fail(f"Failed to patch {cli_path}")
|
|
351
|
+
|
|
352
|
+
# Step 4: Patch Claude Desktop (all locations that exist)
|
|
353
|
+
header("Patching Claude Desktop")
|
|
354
|
+
desktop_configs = claude_desktop_configs()
|
|
355
|
+
if not desktop_configs:
|
|
356
|
+
warn("Claude Desktop config not found — skipping (install Claude Desktop first if needed)")
|
|
357
|
+
for cfg_path in desktop_configs:
|
|
358
|
+
# Desktop config may not exist yet — create it
|
|
359
|
+
if patch_config(cfg_path, python_path, is_desktop=True):
|
|
360
|
+
ok(f"Patched {cfg_path}")
|
|
361
|
+
else:
|
|
362
|
+
fail(f"Failed to patch {cfg_path}")
|
|
363
|
+
|
|
364
|
+
# Step 5: Patch GitHub Copilot
|
|
365
|
+
header("Patching GitHub Copilot CLI")
|
|
366
|
+
copilot_configs = github_copilot_configs()
|
|
367
|
+
for cfg_path in copilot_configs:
|
|
368
|
+
if patch_config(cfg_path, python_path, is_desktop=True):
|
|
369
|
+
ok(f"Patched {cfg_path}")
|
|
370
|
+
else:
|
|
371
|
+
fail(f"Failed to patch {cfg_path}")
|
|
372
|
+
|
|
373
|
+
# Step 6: Patch Antigravity
|
|
374
|
+
header("Patching Antigravity")
|
|
375
|
+
antigravity_cfgs = antigravity_configs()
|
|
376
|
+
for cfg_path in antigravity_cfgs:
|
|
377
|
+
if patch_config(cfg_path, python_path, is_desktop=True):
|
|
378
|
+
ok(f"Patched {cfg_path}")
|
|
379
|
+
else:
|
|
380
|
+
fail(f"Failed to patch {cfg_path}")
|
|
381
|
+
|
|
382
|
+
# Step 7: Bundle BMAD silently
|
|
383
|
+
header("Bundling BMAD (silent)")
|
|
384
|
+
_bundle_bmad()
|
|
385
|
+
|
|
386
|
+
# Step 8: Validate
|
|
387
|
+
header("Validating")
|
|
388
|
+
validate(python_path)
|
|
389
|
+
|
|
390
|
+
# Step 9: Done
|
|
391
|
+
print(f"\n{BOLD}{GREEN} ✓ Tylor installed successfully!{RESET}\n")
|
|
392
|
+
print(" Next steps:")
|
|
393
|
+
print(" 1. Restart Claude Code / Claude Desktop / VSCode")
|
|
394
|
+
print(" 2. Type /help-agent101 to see all commands")
|
|
395
|
+
if use_dynamo:
|
|
396
|
+
print(f" 3. Add AWS credentials to {PLUGIN_DIR / 'server' / '.env'}")
|
|
397
|
+
print()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
if __name__ == "__main__":
|
|
401
|
+
main()
|
package/install.sh
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tylor installer
|
|
3
|
+
# Usage: ./install.sh [project|personal]
|
|
4
|
+
# bash 3.2+ compatible (macOS default shell)
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
SETTINGS_FILE="$HOME/.claude/settings.json"
|
|
10
|
+
CONFIG_DIR="$HOME/.tylor"
|
|
11
|
+
BOLD='\033[1m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
RED='\033[0;31m'
|
|
14
|
+
YELLOW='\033[1;33m'
|
|
15
|
+
NC='\033[0m'
|
|
16
|
+
|
|
17
|
+
ok() { echo -e " ${GREEN}✓${NC} $*"; }
|
|
18
|
+
fail() { echo -e " ${RED}✗${NC} $*"; }
|
|
19
|
+
warn() { echo -e " ${YELLOW}⚠${NC} $*"; }
|
|
20
|
+
header() { echo -e "\n${BOLD}$*${NC}"; }
|
|
21
|
+
|
|
22
|
+
# Parse command line arguments
|
|
23
|
+
STORAGE_MODE="${1:-project}"
|
|
24
|
+
|
|
25
|
+
if [ "$STORAGE_MODE" != "project" ] && [ "$STORAGE_MODE" != "personal" ]; then
|
|
26
|
+
echo "Usage: $0 [project|personal]"
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Modes:"
|
|
29
|
+
echo " project - Local JSON storage, zero AWS setup (default)"
|
|
30
|
+
echo " personal - AWS DynamoDB storage, persistent across machines"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# 0. Configure storage mode
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
configure_storage_mode() {
|
|
38
|
+
header "Configuring storage mode: $STORAGE_MODE"
|
|
39
|
+
mkdir -p "$CONFIG_DIR"
|
|
40
|
+
|
|
41
|
+
if [ "$STORAGE_MODE" = "project" ]; then
|
|
42
|
+
python3 - <<PYEOF
|
|
43
|
+
import json
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
|
|
46
|
+
config_path = Path("$CONFIG_DIR/config.json")
|
|
47
|
+
data = {}
|
|
48
|
+
if config_path.exists():
|
|
49
|
+
try:
|
|
50
|
+
data = json.loads(config_path.read_text())
|
|
51
|
+
except Exception:
|
|
52
|
+
data = {}
|
|
53
|
+
|
|
54
|
+
data["storage_mode"] = "project"
|
|
55
|
+
data["storage_path"] = "$PLUGIN_DIR/.tylor/threads.json"
|
|
56
|
+
config_path.write_text(json.dumps(data, indent=2))
|
|
57
|
+
print(" \033[0;32m✓\033[0m Storage mode: Project (local JSON)")
|
|
58
|
+
print(f" threads.json: $PLUGIN_DIR/.tylor/threads.json")
|
|
59
|
+
PYEOF
|
|
60
|
+
else
|
|
61
|
+
python3 - <<PYEOF
|
|
62
|
+
import json
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
|
|
65
|
+
config_path = Path("$CONFIG_DIR/config.json")
|
|
66
|
+
data = {}
|
|
67
|
+
if config_path.exists():
|
|
68
|
+
try:
|
|
69
|
+
data = json.loads(config_path.read_text())
|
|
70
|
+
except Exception:
|
|
71
|
+
data = {}
|
|
72
|
+
|
|
73
|
+
# Only set if not already configured (idempotent)
|
|
74
|
+
if data.get("storage_mode") not in ("personal", "project"):
|
|
75
|
+
data["storage_mode"] = "personal"
|
|
76
|
+
config_path.write_text(json.dumps(data, indent=2))
|
|
77
|
+
print(" \033[0;32m✓\033[0m Storage mode: Personal (AWS DynamoDB)")
|
|
78
|
+
else:
|
|
79
|
+
print(f" Storage mode already set to '{data['storage_mode']}' — skipping")
|
|
80
|
+
PYEOF
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# 1. Install Python dependencies
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
install_deps() {
|
|
88
|
+
header "Installing dependencies"
|
|
89
|
+
if python3 -m pip install -r "$PLUGIN_DIR/server/requirements.txt" --quiet; then
|
|
90
|
+
ok "Dependencies installed"
|
|
91
|
+
else
|
|
92
|
+
fail "Dependency installation failed"
|
|
93
|
+
echo " Fix: ensure pip is available and you have internet access"
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# 2. Patch ~/.claude/settings.json (idempotent)
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
patch_settings_json() {
|
|
102
|
+
header "Registering MCP server and hooks in settings.json"
|
|
103
|
+
|
|
104
|
+
# Create settings file if absent
|
|
105
|
+
if [ ! -f "$SETTINGS_FILE" ]; then
|
|
106
|
+
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
|
107
|
+
echo '{}' > "$SETTINGS_FILE"
|
|
108
|
+
ok "Created $SETTINGS_FILE"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
python3 - <<PYEOF
|
|
112
|
+
import json, sys
|
|
113
|
+
from pathlib import Path
|
|
114
|
+
|
|
115
|
+
settings_file = Path("$SETTINGS_FILE")
|
|
116
|
+
plugin_dir = "$PLUGIN_DIR"
|
|
117
|
+
hooks_dir = plugin_dir + "/hooks"
|
|
118
|
+
|
|
119
|
+
data = json.loads(settings_file.read_text() or "{}")
|
|
120
|
+
|
|
121
|
+
# --- MCP server entry ---
|
|
122
|
+
servers = data.setdefault("mcpServers", {})
|
|
123
|
+
if "agent101" not in servers:
|
|
124
|
+
servers["agent101"] = {
|
|
125
|
+
"command": "python3",
|
|
126
|
+
"args": ["server/main.py"],
|
|
127
|
+
"cwd": plugin_dir,
|
|
128
|
+
}
|
|
129
|
+
print(" \033[0;32m✓\033[0m MCP server registered")
|
|
130
|
+
else:
|
|
131
|
+
print(" MCP server entry already present — skipping")
|
|
132
|
+
|
|
133
|
+
# --- Hooks (idempotent: check command before appending) ---
|
|
134
|
+
hooks = data.setdefault("hooks", {})
|
|
135
|
+
|
|
136
|
+
def add_hook(event, entry):
|
|
137
|
+
existing = hooks.setdefault(event, [])
|
|
138
|
+
cmd = entry.get("command")
|
|
139
|
+
if not any(h.get("command") == cmd for h in existing):
|
|
140
|
+
existing.append(entry)
|
|
141
|
+
print(f" \033[0;32m✓\033[0m {event} hook registered")
|
|
142
|
+
else:
|
|
143
|
+
print(f" {event} hook already present — skipping")
|
|
144
|
+
|
|
145
|
+
add_hook("SessionStart", {"command": hooks_dir + "/session-start.sh"})
|
|
146
|
+
add_hook("Stop", {"command": hooks_dir + "/session-checkpoint.sh"})
|
|
147
|
+
add_hook("PostToolUse", {"matcher": "kill_thread",
|
|
148
|
+
"command": hooks_dir + "/kill-thread-trigger.sh"})
|
|
149
|
+
for matcher in ("Read", "Write", "Edit", "MultiEdit"):
|
|
150
|
+
add_hook("PostToolUse", {"matcher": matcher,
|
|
151
|
+
"command": hooks_dir + "/post-tool-use-code-index.sh"})
|
|
152
|
+
|
|
153
|
+
settings_file.write_text(json.dumps(data, indent=2))
|
|
154
|
+
PYEOF
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# 3. Initialize registry.json
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
init_registry() {
|
|
161
|
+
header "Initializing skill registry"
|
|
162
|
+
local registry="$PLUGIN_DIR/registry.json"
|
|
163
|
+
if [ ! -f "$registry" ]; then
|
|
164
|
+
echo '{"version":"1.0","skills":[]}' > "$registry"
|
|
165
|
+
ok "registry.json created"
|
|
166
|
+
else
|
|
167
|
+
ok "registry.json already exists — skipping"
|
|
168
|
+
fi
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# 4. Validate MCP server can be imported (stdio transport exits immediately
|
|
173
|
+
# when not connected to Claude Code — import check is the correct gate)
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
validate_startup() {
|
|
176
|
+
header "Validating MCP server"
|
|
177
|
+
|
|
178
|
+
if python3 -c "
|
|
179
|
+
import sys
|
|
180
|
+
sys.path.insert(0, '$PLUGIN_DIR')
|
|
181
|
+
from server.main import mcp
|
|
182
|
+
assert mcp.name == 'agent101', f'Unexpected server name: {mcp.name}'
|
|
183
|
+
" 2>/dev/null; then
|
|
184
|
+
ok "MCP server imports and initializes correctly (name: agent101)"
|
|
185
|
+
ok "Claude Code will start it automatically via stdio on next session"
|
|
186
|
+
return 0
|
|
187
|
+
else
|
|
188
|
+
fail "MCP server failed to import"
|
|
189
|
+
echo " Fix: check Python version (3.11+ required) and run:"
|
|
190
|
+
echo " python3 -c \"from server.main import mcp; print(mcp.name)\""
|
|
191
|
+
return 1
|
|
192
|
+
fi
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# 5. Validate AWS connectivity (advisory — never blocks install)
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
validate_aws() {
|
|
199
|
+
python3 "$PLUGIN_DIR/server/validate.py" "$PLUGIN_DIR"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# 6. Provision AWS resources (advisory — never blocks install)
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
provision_aws() {
|
|
206
|
+
python3 "$PLUGIN_DIR/server/provision.py" "$PLUGIN_DIR"
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
# 7. Provision OpenSearch index (advisory — never blocks install)
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
provision_opensearch() {
|
|
213
|
+
python3 "$PLUGIN_DIR/server/provision_opensearch.py"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# Main
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
main() {
|
|
220
|
+
echo ""
|
|
221
|
+
echo -e "${BOLD}Tylor installer${NC}"
|
|
222
|
+
echo "Plugin directory: $PLUGIN_DIR"
|
|
223
|
+
|
|
224
|
+
ERRORS=0
|
|
225
|
+
|
|
226
|
+
configure_storage_mode
|
|
227
|
+
install_deps || ERRORS=$((ERRORS + 1))
|
|
228
|
+
patch_settings_json || ERRORS=$((ERRORS + 1))
|
|
229
|
+
init_registry
|
|
230
|
+
validate_startup || ERRORS=$((ERRORS + 1))
|
|
231
|
+
|
|
232
|
+
# AWS steps: skip entirely in Project mode
|
|
233
|
+
if [ "$STORAGE_MODE" = "personal" ]; then
|
|
234
|
+
validate_aws
|
|
235
|
+
provision_aws
|
|
236
|
+
provision_opensearch
|
|
237
|
+
else
|
|
238
|
+
warn "Project mode selected — skipping AWS validation and provisioning"
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
echo ""
|
|
242
|
+
if [ "$ERRORS" -eq 0 ]; then
|
|
243
|
+
echo -e "${GREEN}${BOLD}Tylor installed ✓${NC}"
|
|
244
|
+
echo ""
|
|
245
|
+
echo " Next steps:"
|
|
246
|
+
if [ "$STORAGE_MODE" = "project" ]; then
|
|
247
|
+
echo " 1. Restart Claude Code to load the MCP server"
|
|
248
|
+
echo " 2. Type /help-agent101 in Claude Code to see all available commands"
|
|
249
|
+
else
|
|
250
|
+
echo " 1. Restart Claude Code to load the MCP server"
|
|
251
|
+
echo " 2. Add your AWS credentials to server/.env (see server/.env.example)"
|
|
252
|
+
echo " 3. Type /help-agent101 in Claude Code to see all available commands"
|
|
253
|
+
fi
|
|
254
|
+
else
|
|
255
|
+
echo -e "${RED}${BOLD}Installation completed with $ERRORS error(s) — see messages above${NC}"
|
|
256
|
+
exit 1
|
|
257
|
+
fi
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
main "$@"
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tylor-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Give Claude Code persistent memory, laser-focused context, and an autonomous team of specialists.",
|
|
5
|
+
"main": "server/main.py",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tylor-mcp": "bin/tylor.js"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/GunjanGrunge/tylor.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude",
|
|
15
|
+
"mcp",
|
|
16
|
+
"agents",
|
|
17
|
+
"threads",
|
|
18
|
+
"ai",
|
|
19
|
+
"tylor",
|
|
20
|
+
"github-copilot"
|
|
21
|
+
],
|
|
22
|
+
"author": "Gunjan Grunge",
|
|
23
|
+
"license": "MIT"
|
|
24
|
+
}
|
package/pytest.ini
ADDED