tweek 0.3.0__py3-none-any.whl → 0.4.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.
Files changed (63) hide show
  1. tweek/__init__.py +2 -2
  2. tweek/audit.py +2 -2
  3. tweek/cli.py +78 -6559
  4. tweek/cli_config.py +643 -0
  5. tweek/cli_configure.py +413 -0
  6. tweek/cli_core.py +718 -0
  7. tweek/cli_dry_run.py +390 -0
  8. tweek/cli_helpers.py +316 -0
  9. tweek/cli_install.py +1666 -0
  10. tweek/cli_logs.py +301 -0
  11. tweek/cli_mcp.py +148 -0
  12. tweek/cli_memory.py +343 -0
  13. tweek/cli_plugins.py +748 -0
  14. tweek/cli_protect.py +564 -0
  15. tweek/cli_proxy.py +405 -0
  16. tweek/cli_security.py +236 -0
  17. tweek/cli_skills.py +289 -0
  18. tweek/cli_uninstall.py +551 -0
  19. tweek/cli_vault.py +313 -0
  20. tweek/config/__init__.py +8 -0
  21. tweek/config/allowed_dirs.yaml +16 -17
  22. tweek/config/families.yaml +4 -1
  23. tweek/config/manager.py +49 -0
  24. tweek/config/models.py +307 -0
  25. tweek/config/patterns.yaml +29 -5
  26. tweek/config/templates/config.yaml.template +212 -0
  27. tweek/config/templates/env.template +45 -0
  28. tweek/config/templates/overrides.yaml.template +121 -0
  29. tweek/config/templates/tweek.yaml.template +20 -0
  30. tweek/config/templates.py +136 -0
  31. tweek/config/tiers.yaml +5 -4
  32. tweek/diagnostics.py +112 -32
  33. tweek/hooks/overrides.py +4 -0
  34. tweek/hooks/post_tool_use.py +46 -1
  35. tweek/hooks/pre_tool_use.py +149 -49
  36. tweek/integrations/openclaw.py +84 -0
  37. tweek/licensing.py +1 -1
  38. tweek/mcp/__init__.py +7 -9
  39. tweek/mcp/clients/chatgpt.py +2 -2
  40. tweek/mcp/clients/claude_desktop.py +2 -2
  41. tweek/mcp/clients/gemini.py +2 -2
  42. tweek/mcp/proxy.py +165 -1
  43. tweek/memory/provenance.py +438 -0
  44. tweek/memory/queries.py +2 -0
  45. tweek/memory/safety.py +23 -4
  46. tweek/memory/schemas.py +1 -0
  47. tweek/memory/store.py +101 -71
  48. tweek/plugins/screening/heuristic_scorer.py +1 -1
  49. tweek/security/integrity.py +77 -0
  50. tweek/security/llm_reviewer.py +162 -68
  51. tweek/security/local_reviewer.py +44 -2
  52. tweek/security/model_registry.py +73 -7
  53. tweek/skill_template/overrides-reference.md +1 -1
  54. tweek/skills/context.py +221 -0
  55. tweek/skills/scanner.py +2 -2
  56. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/METADATA +9 -7
  57. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/RECORD +62 -39
  58. tweek/mcp/server.py +0 -320
  59. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/WHEEL +0 -0
  60. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/entry_points.txt +0 -0
  61. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/LICENSE +0 -0
  62. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/NOTICE +0 -0
  63. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/top_level.txt +0 -0
@@ -54,6 +54,7 @@ from tweek.skills.guard import (
54
54
  )
55
55
  from tweek.skills.fingerprints import get_fingerprints
56
56
  from tweek.sandbox.project import get_project_sandbox
57
+ from tweek.plugins.base import ReDoSProtection, RegexTimeoutError
57
58
 
58
59
 
59
60
  # =============================================================================
@@ -264,6 +265,7 @@ def _resolve_enforcement(
264
265
  enforcement_policy: Optional[EnforcementPolicy],
265
266
  has_non_pattern_trigger: bool = False,
266
267
  memory_adjustment: Optional[Dict] = None,
268
+ taint_level: str = "clean",
267
269
  ) -> str:
268
270
  """Determine the enforcement decision based on severity + confidence.
269
271
 
@@ -296,6 +298,7 @@ def _resolve_enforcement(
296
298
  decision = "ask"
297
299
 
298
300
  # Break-glass: downgrade "deny" to "ask" if active override exists
301
+ break_glass_used = False
299
302
  if decision == "deny":
300
303
  try:
301
304
  from tweek.hooks.break_glass import check_override
@@ -318,6 +321,7 @@ def _resolve_enforcement(
318
321
  },
319
322
  )
320
323
  decision = "ask" # Downgrade deny → ask (user still sees prompt)
324
+ break_glass_used = True
321
325
  except ImportError:
322
326
  pass # Break-glass module not available
323
327
 
@@ -337,6 +341,21 @@ def _resolve_enforcement(
337
341
  except Exception:
338
342
  pass # Memory is best-effort
339
343
 
344
+ # Provenance-based adjustment: modify decision based on session taint
345
+ # Skip when break-glass is active — explicit user overrides take priority
346
+ if not break_glass_used:
347
+ try:
348
+ from tweek.memory.provenance import adjust_enforcement_for_taint
349
+ if pattern_match:
350
+ decision = adjust_enforcement_for_taint(
351
+ base_decision=decision,
352
+ severity=severity,
353
+ confidence=confidence,
354
+ taint_level=taint_level,
355
+ )
356
+ except Exception:
357
+ pass # Provenance is best-effort
358
+
340
359
  return decision
341
360
 
342
361
 
@@ -362,15 +381,25 @@ class TierManager:
362
381
  return yaml.safe_load(f) or {}
363
382
 
364
383
  def get_base_tier(self, tool_name: str, skill_name: Optional[str] = None) -> str:
365
- """Get the base tier for a tool or skill."""
366
- # Skills override tools if specified
367
- if skill_name and skill_name in self.skills:
368
- return self.skills[skill_name]
384
+ """Get the base tier for a tool or skill.
369
385
 
370
- if tool_name in self.tools:
371
- return self.tools[tool_name]
386
+ Skills can only ESCALATE a tool's tier, never relax it.
387
+ This prevents a safe skill from disabling screening on dangerous tools.
388
+ For example, the 'explore' skill (safe) cannot downgrade Bash (dangerous).
389
+ But the 'deploy' skill (dangerous) can escalate Read (default) to dangerous.
390
+ """
391
+ tier_priority = {"safe": 0, "default": 1, "risky": 2, "dangerous": 3}
372
392
 
373
- return self.default_tier
393
+ # Start with the tool's inherent tier
394
+ tool_tier = self.tools.get(tool_name, self.default_tier)
395
+
396
+ if skill_name and skill_name in self.skills:
397
+ skill_tier = self.skills[skill_name]
398
+ # Skill can only escalate, never relax
399
+ if tier_priority.get(skill_tier, 1) > tier_priority.get(tool_tier, 1):
400
+ return skill_tier
401
+
402
+ return tool_tier
374
403
 
375
404
  def check_escalations(self, content: str) -> Optional[Dict]:
376
405
  """Check if content triggers any escalation patterns.
@@ -551,6 +580,21 @@ class PatternMatcher:
551
580
  extra = self._load_patterns(user_patterns)
552
581
  self._merge_patterns(extra)
553
582
 
583
+ # Pre-compile all regex patterns for performance and ReDoS protection.
584
+ # These are our own trusted patterns (not user-supplied), so skip
585
+ # ReDoS validation during compilation (validate=False).
586
+ self._compiled_patterns: List[Tuple[re.Pattern, dict]] = []
587
+ for pattern in self.patterns:
588
+ regex = pattern.get("regex", "")
589
+ if not regex:
590
+ continue
591
+ try:
592
+ compiled = re.compile(regex, re.IGNORECASE | re.DOTALL)
593
+ self._compiled_patterns.append((compiled, pattern))
594
+ except re.error:
595
+ # Skip invalid patterns at compile time
596
+ continue
597
+
554
598
  def _merge_patterns(self, extra_patterns: List[dict]):
555
599
  """Merge additional patterns into existing set (additive).
556
600
 
@@ -587,25 +631,33 @@ class PatternMatcher:
587
631
  """Check content against all patterns.
588
632
 
589
633
  Returns the first matching pattern, or None.
634
+ Uses timeout-protected regex execution to prevent ReDoS.
590
635
  """
591
636
  content = self._normalize(content)
592
- for pattern in self.patterns:
637
+ if len(content) > ReDoSProtection.MAX_INPUT_LENGTH:
638
+ content = content[:ReDoSProtection.MAX_INPUT_LENGTH]
639
+ for compiled, pattern in self._compiled_patterns:
593
640
  try:
594
- if re.search(pattern.get("regex", ""), content, re.IGNORECASE | re.DOTALL):
641
+ if ReDoSProtection.safe_search(compiled, content, timeout=2.0):
595
642
  return pattern
596
- except re.error:
643
+ except (RegexTimeoutError, re.error):
597
644
  continue
598
645
  return None
599
646
 
600
647
  def check_all(self, content: str) -> List[dict]:
601
- """Check content against all patterns, returning all matches."""
648
+ """Check content against all patterns, returning all matches.
649
+
650
+ Uses timeout-protected regex execution to prevent ReDoS.
651
+ """
602
652
  content = self._normalize(content)
653
+ if len(content) > ReDoSProtection.MAX_INPUT_LENGTH:
654
+ content = content[:ReDoSProtection.MAX_INPUT_LENGTH]
603
655
  matches = []
604
- for pattern in self.patterns:
656
+ for compiled, pattern in self._compiled_patterns:
605
657
  try:
606
- if re.search(pattern.get("regex", ""), content, re.IGNORECASE | re.DOTALL):
658
+ if ReDoSProtection.safe_search(compiled, content, timeout=2.0):
607
659
  matches.append(pattern)
608
- except re.error:
660
+ except (RegexTimeoutError, re.error):
609
661
  continue
610
662
  return matches
611
663
 
@@ -762,7 +814,7 @@ def _extract_paths_from_bash(command: str) -> List[str]:
762
814
  python3 /home/user/script.py
763
815
 
764
816
  Intentionally simple -- does NOT handle pipes, subshells, or variable
765
- expansion. Content-based escalations (259 patterns) cover the rest.
817
+ expansion. Content-based escalations (262 patterns) cover the rest.
766
818
  """
767
819
  import shlex
768
820
 
@@ -877,6 +929,39 @@ def process_hook(input_data: dict, logger: SecurityLogger) -> dict:
877
929
  # Fingerprint check is best-effort — never break the hook
878
930
  pass
879
931
 
932
+ # =========================================================================
933
+ # SKILL CONTEXT TRACKING: Detect active skill from Skill tool invocations
934
+ # When Claude invokes the Skill tool, we extract the skill name and write
935
+ # a breadcrumb. Subsequent tool calls read it for tier/memory context.
936
+ # =========================================================================
937
+ _active_skill = None
938
+ try:
939
+ from tweek.skills.context import (
940
+ extract_skill_from_tool_input,
941
+ write_skill_breadcrumb,
942
+ read_skill_context,
943
+ )
944
+
945
+ if tool_name == "Skill" and session_id:
946
+ # Skill tool invocation — extract and record the active skill
947
+ skill = extract_skill_from_tool_input(tool_input)
948
+ if skill:
949
+ write_skill_breadcrumb(skill, session_id)
950
+ _active_skill = skill
951
+ _log(
952
+ EventType.SCREENING_COMPLETE,
953
+ tool_name,
954
+ decision="allow",
955
+ decision_reason=f"Skill context recorded: {skill}",
956
+ metadata={"skill_context": skill},
957
+ )
958
+ elif session_id:
959
+ # Non-Skill tool — check for active skill breadcrumb
960
+ _active_skill = read_skill_context(session_id)
961
+ except Exception:
962
+ # Skill context is best-effort — never break the hook
963
+ _active_skill = None
964
+
880
965
  # =========================================================================
881
966
  # SELF-PROTECTION: Block AI from modifying security override config
882
967
  # This file can only be edited by a human directly.
@@ -1112,9 +1197,10 @@ def process_hook(input_data: dict, logger: SecurityLogger) -> dict:
1112
1197
  tier_mgr = _get_tier_manager()
1113
1198
  pattern_matcher = _get_pattern_matcher()
1114
1199
 
1115
- # Determine effective tier
1200
+ # Determine effective tier (skill context overrides tool tier if configured)
1116
1201
  effective_tier, escalation = tier_mgr.get_effective_tier(
1117
1202
  tool_name, content,
1203
+ skill_name=_active_skill,
1118
1204
  target_paths=target_paths,
1119
1205
  working_dir=working_dir,
1120
1206
  )
@@ -1192,6 +1278,23 @@ def process_hook(input_data: dict, logger: SecurityLogger) -> dict:
1192
1278
  metadata={"escalation": escalation}
1193
1279
  )
1194
1280
 
1281
+ # =========================================================================
1282
+ # PROVENANCE: Record tool call and get session taint level
1283
+ # Tracks every tool call for taint decay; provides taint_level for
1284
+ # enforcement adjustment later in the pipeline.
1285
+ # =========================================================================
1286
+ # Default to "low" when no session tracking — clean session relaxation
1287
+ # requires active provenance tracking as a safety prerequisite.
1288
+ _session_taint_level = "low"
1289
+ try:
1290
+ from tweek.memory.provenance import get_taint_store
1291
+ if session_id:
1292
+ _taint_store = get_taint_store()
1293
+ _taint_state = _taint_store.record_tool_call(session_id, tool_name)
1294
+ _session_taint_level = _taint_state.get("taint_level", "clean")
1295
+ except Exception:
1296
+ _session_taint_level = "low" # Provenance unavailable — no relaxation
1297
+
1195
1298
  # =========================================================================
1196
1299
  # LAYER 1: Rate Limiting
1197
1300
  # =========================================================================
@@ -1538,7 +1641,7 @@ def process_hook(input_data: dict, logger: SecurityLogger) -> dict:
1538
1641
  if pattern_match or llm_triggered or session_triggered or sandbox_triggered or compliance_triggered:
1539
1642
  # Determine graduated enforcement decision
1540
1643
  has_non_pattern = llm_triggered or session_triggered or sandbox_triggered or compliance_triggered
1541
- decision = _resolve_enforcement(pattern_match, enforcement_policy, has_non_pattern, memory_adjustment)
1644
+ decision = _resolve_enforcement(pattern_match, enforcement_policy, has_non_pattern, memory_adjustment, _session_taint_level)
1542
1645
 
1543
1646
  # Build the warning message for ask/deny decisions
1544
1647
  final_msg = format_prompt_message(
@@ -1721,53 +1824,50 @@ def process_hook(input_data: dict, logger: SecurityLogger) -> dict:
1721
1824
  return {}
1722
1825
 
1723
1826
 
1724
- def check_allowed_directory() -> bool:
1827
+ def check_hook_enabled(hook_name: str = "pre_tool_use") -> bool:
1725
1828
  """
1726
- Check if current working directory is in the allowed list.
1829
+ Check if a Tweek hook should run in the current working directory.
1727
1830
 
1728
- This is a SAFETY CHECK to prevent Tweek from accidentally
1729
- running in production or other directories.
1831
+ Looks for a .tweek.yaml file in the project root (cwd). This file
1832
+ is created during `tweek protect claude-code` with hooks enabled
1833
+ by default. Users can manually set hooks to false to disable
1834
+ screening in a specific directory.
1835
+
1836
+ .tweek.yaml format:
1837
+ hooks:
1838
+ pre_tool_use: true # set false to disable pre-screening
1839
+ post_tool_use: true # set false to disable post-screening
1840
+
1841
+ If no .tweek.yaml exists, hooks are ENABLED (protected by default).
1842
+ This file is protected by Tweek self-protection — only a human
1843
+ can create or edit it.
1844
+
1845
+ Args:
1846
+ hook_name: "pre_tool_use" or "post_tool_use"
1730
1847
 
1731
1848
  Returns:
1732
- True if Tweek should activate, False to pass through
1849
+ True if the hook should run, False to skip
1733
1850
  """
1734
- config_path = Path(__file__).parent.parent / "config" / "allowed_dirs.yaml"
1851
+ tweek_config = Path.cwd() / ".tweek.yaml"
1735
1852
 
1736
- if not config_path.exists():
1737
- # No config = disabled everywhere (safe default)
1738
- return False
1853
+ if not tweek_config.exists():
1854
+ # No config = hooks enabled (protected by default)
1855
+ return True
1739
1856
 
1740
1857
  try:
1741
- with open(config_path) as f:
1858
+ with open(tweek_config) as f:
1742
1859
  config = yaml.safe_load(f) or {}
1743
1860
  except Exception:
1744
- return False
1745
-
1746
- # Check if globally enabled (production mode)
1747
- if config.get("global_enabled", False):
1748
- return True
1749
-
1750
- # Check allowed directories
1751
- allowed_dirs = config.get("allowed_directories", [])
1752
- cwd = Path.cwd().resolve()
1753
-
1754
- for allowed in allowed_dirs:
1755
- allowed_path = Path(allowed).expanduser().resolve()
1756
- try:
1757
- # Check if cwd is the allowed dir or a subdirectory
1758
- cwd.relative_to(allowed_path)
1759
- return True
1760
- except ValueError:
1761
- continue
1861
+ return True # Can't read config = fail safe, keep hooks on
1762
1862
 
1763
- return False
1863
+ hooks = config.get("hooks", {})
1864
+ return hooks.get(hook_name, True)
1764
1865
 
1765
1866
 
1766
1867
  def main():
1767
1868
  """Entry point for the hook."""
1768
- # SAFETY CHECK: Only activate in allowed directories
1769
- if not check_allowed_directory():
1770
- # Not in allowed directory - pass through without screening
1869
+ # Check if pre_tool_use hook is enabled for this directory
1870
+ if not check_hook_enabled("pre_tool_use"):
1771
1871
  print("{}")
1772
1872
  return
1773
1873
 
@@ -441,3 +441,87 @@ def setup_openclaw_protection(
441
441
 
442
442
  result.success = True
443
443
  return result
444
+
445
+
446
+ def remove_openclaw_protection() -> dict:
447
+ """
448
+ Remove Tweek protection from OpenClaw.
449
+
450
+ Reverses setup_openclaw_protection():
451
+ 1. Uninstalls the Tweek plugin from OpenClaw
452
+ 2. Removes the Tweek plugin entry from openclaw.json
453
+ 3. Removes the openclaw section from ~/.tweek/config.yaml
454
+
455
+ Returns:
456
+ dict with 'success', 'message', 'details', and optionally 'error'
457
+ """
458
+ details = []
459
+
460
+ # 1. Uninstall the npm plugin
461
+ if _check_plugin_installed():
462
+ try:
463
+ proc = subprocess.run(
464
+ ["openclaw", "plugins", "uninstall", OPENCLAW_PLUGIN_NAME],
465
+ capture_output=True,
466
+ text=True,
467
+ timeout=60,
468
+ )
469
+ if proc.returncode == 0:
470
+ details.append(f"Uninstalled {OPENCLAW_PLUGIN_NAME} plugin")
471
+ else:
472
+ details.append(f"Plugin uninstall returned non-zero (may already be removed)")
473
+ except subprocess.TimeoutExpired:
474
+ return {"success": False, "error": "Plugin uninstall timed out"}
475
+ except FileNotFoundError:
476
+ details.append("openclaw CLI not found, skipping plugin uninstall")
477
+ else:
478
+ details.append("Tweek plugin not found in OpenClaw (already removed)")
479
+
480
+ # 2. Remove Tweek entry from openclaw.json
481
+ if OPENCLAW_CONFIG.exists():
482
+ try:
483
+ with open(OPENCLAW_CONFIG) as f:
484
+ config = json.load(f)
485
+
486
+ plugins = config.get("plugins", {})
487
+ entries = plugins.get("entries", {})
488
+ if "tweek" in entries:
489
+ del entries["tweek"]
490
+ with open(OPENCLAW_CONFIG, "w") as f:
491
+ json.dump(config, f, indent=2)
492
+ details.append(f"Removed tweek entry from {OPENCLAW_CONFIG}")
493
+ else:
494
+ details.append("No tweek entry found in openclaw.json")
495
+ except (json.JSONDecodeError, IOError) as e:
496
+ details.append(f"Could not update openclaw.json: {e}")
497
+
498
+ # 3. Remove openclaw section from ~/.tweek/config.yaml
499
+ tweek_config_path = Path.home() / ".tweek" / "config.yaml"
500
+ if tweek_config_path.exists():
501
+ try:
502
+ import yaml
503
+ except ImportError:
504
+ yaml = None
505
+
506
+ if yaml:
507
+ try:
508
+ with open(tweek_config_path) as f:
509
+ tweek_config = yaml.safe_load(f) or {}
510
+
511
+ if "openclaw" in tweek_config:
512
+ del tweek_config["openclaw"]
513
+ with open(tweek_config_path, "w") as f:
514
+ yaml.dump(tweek_config, f, default_flow_style=False)
515
+ details.append("Removed openclaw section from ~/.tweek/config.yaml")
516
+ else:
517
+ details.append("No openclaw section in ~/.tweek/config.yaml")
518
+ except Exception as e:
519
+ details.append(f"Could not update ~/.tweek/config.yaml: {e}")
520
+ else:
521
+ details.append("PyYAML not available, skipping config.yaml cleanup")
522
+
523
+ return {
524
+ "success": True,
525
+ "message": "OpenClaw protection removed",
526
+ "details": details,
527
+ }
tweek/licensing.py CHANGED
@@ -74,7 +74,7 @@ class LicenseInfo:
74
74
  # Only compliance and team management features require a license.
75
75
  TIER_FEATURES = {
76
76
  Tier.FREE: [
77
- "pattern_matching", # All 215 patterns included free
77
+ "pattern_matching", # All 262 patterns included free
78
78
  "basic_logging",
79
79
  "vault_storage",
80
80
  "cli_commands",
tweek/mcp/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Tweek MCP Security Gateway & Proxy
3
+ Tweek MCP Security Proxy
4
4
 
5
5
  MCP (Model Context Protocol) integration for desktop LLM applications:
6
6
  - Claude Desktop
@@ -8,17 +8,15 @@ MCP (Model Context Protocol) integration for desktop LLM applications:
8
8
  - Gemini CLI
9
9
  - VS Code (Continue.dev)
10
10
 
11
- Two modes of operation:
12
- - **Proxy** (recommended): Transparently wraps upstream MCP servers with
13
- security screening and human-in-the-loop approval. Tools keep their
14
- original names. Use: tweek mcp proxy
15
- - **Gateway**: Exposes tweek_vault and tweek_status as new MCP tools for
16
- capabilities not available as built-in desktop client tools.
17
- Use: tweek mcp serve
11
+ Transparently wraps upstream MCP servers with security screening and
12
+ human-in-the-loop approval. Also provides built-in tweek_vault and
13
+ tweek_status tools alongside proxied upstream tools.
14
+
15
+ Use: tweek mcp proxy
18
16
 
19
17
  Built-in desktop client tools (Bash, Read, Write, etc.) cannot be
20
18
  intercepted via MCP — use CLI hooks for Claude Code, or the HTTP
21
19
  proxy for Cursor/direct API calls.
22
20
  """
23
21
 
24
- __all__ = ["create_server", "create_proxy"]
22
+ __all__ = ["create_proxy"]
@@ -31,8 +31,8 @@ class ChatGPTClient:
31
31
  """Get the arguments for the tweek MCP server."""
32
32
  tweek_path = shutil.which("tweek")
33
33
  if tweek_path:
34
- return ["mcp", "serve"]
35
- return ["-m", "tweek.mcp", "serve"]
34
+ return ["mcp", "proxy"]
35
+ return ["-m", "tweek.mcp", "proxy"]
36
36
 
37
37
  def install(self) -> Dict[str, Any]:
38
38
  """
@@ -52,8 +52,8 @@ class ClaudeDesktopClient:
52
52
  """Get the arguments for the tweek MCP server."""
53
53
  tweek_path = shutil.which("tweek")
54
54
  if tweek_path:
55
- return ["mcp", "serve"]
56
- return ["-m", "tweek.mcp", "serve"]
55
+ return ["mcp", "proxy"]
56
+ return ["-m", "tweek.mcp", "proxy"]
57
57
 
58
58
  def _build_server_config(self) -> Dict[str, Any]:
59
59
  """Build the MCP server configuration entry."""
@@ -35,8 +35,8 @@ class GeminiClient:
35
35
  """Get the arguments for the tweek MCP server."""
36
36
  tweek_path = shutil.which("tweek")
37
37
  if tweek_path:
38
- return ["mcp", "serve"]
39
- return ["-m", "tweek.mcp", "serve"]
38
+ return ["mcp", "proxy"]
39
+ return ["-m", "tweek.mcp", "proxy"]
40
40
 
41
41
  def _build_server_config(self) -> Dict[str, Any]:
42
42
  """Build the MCP server configuration entry."""