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.
Files changed (101) hide show
  1. package/.aws-setup.sh +25 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/.mcp.json +12 -0
  4. package/AGENTS.md +93 -0
  5. package/CLAUDE.md +99 -0
  6. package/CLAUDE_PLATFORM_AWS_SETUP.md +105 -0
  7. package/LICENSE +21 -0
  8. package/README.md +146 -0
  9. package/assets/tylor_logo.png +0 -0
  10. package/assets/tylor_threads_concept.png +0 -0
  11. package/bin/tylor.js +23 -0
  12. package/hooks/kill-thread-trigger.sh +7 -0
  13. package/hooks/post-tool-use-code-index.sh +7 -0
  14. package/hooks/session-checkpoint.sh +7 -0
  15. package/hooks/session-start.sh +7 -0
  16. package/install.py +401 -0
  17. package/install.sh +260 -0
  18. package/package.json +24 -0
  19. package/pytest.ini +2 -0
  20. package/registry.json +26 -0
  21. package/server/.env.example +24 -0
  22. package/server/__init__.py +0 -0
  23. package/server/config.py +89 -0
  24. package/server/main.py +93 -0
  25. package/server/personas/analyst.md +15 -0
  26. package/server/personas/ceo.md +14 -0
  27. package/server/personas/code_agent.md +15 -0
  28. package/server/personas/cto.md +14 -0
  29. package/server/provision.py +260 -0
  30. package/server/provision_opensearch.py +154 -0
  31. package/server/requirements.txt +26 -0
  32. package/server/storage/__init__.py +0 -0
  33. package/server/storage/dynamo.py +399 -0
  34. package/server/storage/json_store.py +359 -0
  35. package/server/storage/opensearch.py +194 -0
  36. package/server/storage/s3.py +96 -0
  37. package/server/storage/tests/__init__.py +0 -0
  38. package/server/storage/tests/test_dynamo.py +452 -0
  39. package/server/storage/tests/test_json_store.py +226 -0
  40. package/server/storage/tests/test_opensearch.py +270 -0
  41. package/server/storage/tests/test_s3.py +125 -0
  42. package/server/tests/__init__.py +0 -0
  43. package/server/tests/test_install.py +606 -0
  44. package/server/tests/test_isolation.py +90 -0
  45. package/server/tests/test_ui_server.py +385 -0
  46. package/server/tests/test_ui_shader_background.py +52 -0
  47. package/server/tests/test_ui_story_6_3.py +105 -0
  48. package/server/tools/__init__.py +0 -0
  49. package/server/tools/_mcp.py +4 -0
  50. package/server/tools/agents.py +160 -0
  51. package/server/tools/ecc/__init__.py +1 -0
  52. package/server/tools/ecc/data.py +35 -0
  53. package/server/tools/ecc/diagrams.py +23 -0
  54. package/server/tools/ecc/pipeline.py +24 -0
  55. package/server/tools/ecc/presentation.py +24 -0
  56. package/server/tools/ecc/web.py +23 -0
  57. package/server/tools/executor.py +880 -0
  58. package/server/tools/harness.py +330 -0
  59. package/server/tools/help.py +162 -0
  60. package/server/tools/hooks.py +357 -0
  61. package/server/tools/personas.py +110 -0
  62. package/server/tools/registry.py +195 -0
  63. package/server/tools/router.py +117 -0
  64. package/server/tools/skill_installer.py +230 -0
  65. package/server/tools/summarizer.py +168 -0
  66. package/server/tools/tests/__init__.py +0 -0
  67. package/server/tools/tests/test_agents.py +246 -0
  68. package/server/tools/tests/test_code_index.py +108 -0
  69. package/server/tools/tests/test_ecc_tools.py +51 -0
  70. package/server/tools/tests/test_executor.py +584 -0
  71. package/server/tools/tests/test_help_agent101.py +149 -0
  72. package/server/tools/tests/test_hooks.py +124 -0
  73. package/server/tools/tests/test_kill_thread.py +125 -0
  74. package/server/tools/tests/test_new_thread_list_threads.py +293 -0
  75. package/server/tools/tests/test_personas.py +52 -0
  76. package/server/tools/tests/test_recall_memory.py +55 -0
  77. package/server/tools/tests/test_registry_client.py +308 -0
  78. package/server/tools/tests/test_router.py +263 -0
  79. package/server/tools/tests/test_skill_installer.py +174 -0
  80. package/server/tools/tests/test_switch_thread.py +163 -0
  81. package/server/tools/tests/test_thread_command_skills.py +54 -0
  82. package/server/tools/tests/test_thread_resolver.py +165 -0
  83. package/server/tools/tests/test_tier1_schema.py +296 -0
  84. package/server/tools/thread_resolver.py +75 -0
  85. package/server/tools/tylor.py +374 -0
  86. package/server/tools/ui.py +38 -0
  87. package/server/ui_server.py +292 -0
  88. package/server/validate.py +237 -0
  89. package/skills/add-skill/SKILL.md +37 -0
  90. package/skills/afk-status/SKILL.md +20 -0
  91. package/skills/bmad/SKILL.md +14 -0
  92. package/skills/help-agent101/SKILL.md +48 -0
  93. package/skills/kill-thread/SKILL.md +35 -0
  94. package/skills/list-threads/SKILL.md +35 -0
  95. package/skills/new-thread/SKILL.md +35 -0
  96. package/skills/recall/SKILL.md +39 -0
  97. package/skills/run/SKILL.md +33 -0
  98. package/skills/set-sandbox/SKILL.md +38 -0
  99. package/skills/switch-thread/SKILL.md +38 -0
  100. package/ui/claude-logo.png +0 -0
  101. 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
@@ -0,0 +1,2 @@
1
+ [pytest]
2
+ asyncio_mode = auto