nucleus-mcp 1.0.7 → 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.
Files changed (135) hide show
  1. package/package.json +2 -4
  2. package/.github/workflows/ci.yml +0 -72
  3. package/.smithery/shttp/module.js +0 -3
  4. package/.smithery/shttp/module.js.map +0 -7
  5. package/.smithery/stdio/index.cjs +0 -74
  6. package/.smithery/stdio/index.cjs.map +0 -7
  7. package/CHANGELOG.md +0 -65
  8. package/CODE_OF_CONDUCT.md +0 -34
  9. package/COMPARISON.md +0 -20
  10. package/CONTRIBUTING.md +0 -137
  11. package/DEVELOPMENT.md +0 -109
  12. package/LICENSE +0 -21
  13. package/README.md +0 -319
  14. package/ROADMAP.md +0 -24
  15. package/SECURITY.md +0 -88
  16. package/docs/COMMUNITY_FAQ.md +0 -48
  17. package/docs/COMPETITIVE_ANALYSIS.md +0 -90
  18. package/docs/DEMO_SCRIPT.md +0 -86
  19. package/docs/ENTERPRISE.md +0 -169
  20. package/docs/FAQ.md +0 -228
  21. package/docs/QUICK_START.md +0 -155
  22. package/docs/README.md +0 -67
  23. package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
  24. package/docs/social-preview-hq.png +0 -0
  25. package/docs/social-preview.png +0 -0
  26. package/examples/basic_usage.py +0 -84
  27. package/package.json.real +0 -24
  28. package/pyproject.toml +0 -50
  29. package/registry-server.json +0 -20
  30. package/scripts/demo_governance.py +0 -109
  31. package/scripts/sse_bridge.py +0 -87
  32. package/scripts/verify_windows_paths.py +0 -48
  33. package/server.json +0 -20
  34. package/src/mcp_server_nucleus/__init__.py +0 -8583
  35. package/src/mcp_server_nucleus/__main__.py +0 -9
  36. package/src/mcp_server_nucleus/cli.py +0 -1225
  37. package/src/mcp_server_nucleus/commitment_core.py +0 -146
  38. package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
  39. package/src/mcp_server_nucleus/core/__init__.py +0 -6
  40. package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
  41. package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
  42. package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
  43. package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
  44. package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
  45. package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
  46. package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
  47. package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
  48. package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
  49. package/src/mcp_server_nucleus/runtime/agent.py +0 -517
  50. package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
  51. package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
  52. package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
  53. package/src/mcp_server_nucleus/runtime/auth.py +0 -98
  54. package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
  55. package/src/mcp_server_nucleus/runtime/broker.py +0 -126
  56. package/src/mcp_server_nucleus/runtime/budget.py +0 -153
  57. package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
  58. package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
  59. package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
  60. package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
  61. package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
  62. package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
  63. package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
  64. package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
  65. package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
  66. package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
  67. package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
  68. package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
  69. package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
  70. package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
  71. package/src/mcp_server_nucleus/runtime/common.py +0 -134
  72. package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
  73. package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
  74. package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
  75. package/src/mcp_server_nucleus/runtime/critic.py +0 -150
  76. package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
  77. package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
  78. package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
  79. package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
  80. package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
  81. package/src/mcp_server_nucleus/runtime/export.py +0 -64
  82. package/src/mcp_server_nucleus/runtime/factory.py +0 -594
  83. package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
  84. package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
  85. package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
  86. package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
  87. package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
  88. package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
  89. package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
  90. package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
  91. package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
  92. package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
  93. package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
  94. package/src/mcp_server_nucleus/runtime/installer.py +0 -81
  95. package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
  96. package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
  97. package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
  98. package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
  99. package/src/mcp_server_nucleus/runtime/locking.py +0 -228
  100. package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
  101. package/src/mcp_server_nucleus/runtime/memory.py +0 -158
  102. package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
  103. package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
  104. package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
  105. package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
  106. package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
  107. package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
  108. package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
  109. package/src/mcp_server_nucleus/runtime/policy.py +0 -117
  110. package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
  111. package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
  112. package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
  113. package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
  114. package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
  115. package/src/mcp_server_nucleus/runtime/registry.py +0 -82
  116. package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
  117. package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
  118. package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
  119. package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
  120. package/src/mcp_server_nucleus/runtime/storage.py +0 -83
  121. package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
  122. package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
  123. package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
  124. package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
  125. package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
  126. package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
  127. package/src/mcp_server_nucleus/runtime/team.py +0 -59
  128. package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
  129. package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
  130. package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
  131. package/src/mcp_server_nucleus/tool_tiers.py +0 -243
  132. package/tests/__init__.py +0 -1
  133. package/tests/conftest.py +0 -42
  134. package/tests/test_core.py +0 -235
  135. package/tests/test_search.py +0 -69
@@ -1,92 +0,0 @@
1
- """
2
- GitFetcher: The Supply Line.
3
- Securely clones and checks out specific commits of Agent Repositories.
4
-
5
- Strategic Role:
6
- - "The Truck": Moves bytes from GitHub to Nucleus.
7
- - "Immutable": Always checks out a specific hash (The Stamp).
8
- - "Secure": Validates URLs and prevents command injection.
9
- """
10
-
11
- import logging
12
- import shutil
13
- import subprocess
14
- from pathlib import Path
15
-
16
- logger = logging.getLogger("GIT_FETCHER")
17
-
18
- class GitFetcher:
19
- def __init__(self):
20
- pass
21
-
22
- def fetch(self, url: str, destination: Path, commit_hash: str) -> Path:
23
- """
24
- Clones a repo to destination and checks out a specific commit.
25
-
26
- Args:
27
- url: Git URL (https://github.com/... or file://...)
28
- destination: Local path to install to.
29
- commit_hash: The SHA-1 hash to checkout.
30
-
31
- Returns:
32
- Path to the checked out directory (destination).
33
- """
34
- destination = Path(destination)
35
-
36
- logger.info(f"🚚 Fetching {url} -> {destination} @ {commit_hash[:7]}")
37
-
38
- # 1. Cleanup target if exists
39
- if destination.exists():
40
- shutil.rmtree(destination)
41
- destination.mkdir(parents=True)
42
-
43
- try:
44
- # 2. Clone (No checkout initially for speed/safety)
45
- # We use subprocess directly to avoid external lib dependencies like GitPython
46
- # for this core component.
47
-
48
- # Note: We clone to a temp folder inside destination or directly?
49
- # Git won't clone into non-empty dir usually.
50
- # But we just cleared it.
51
-
52
- subprocess.check_call(
53
- ["git", "clone", "--no-checkout", url, "."],
54
- cwd=destination,
55
- stdout=subprocess.DEVNULL,
56
- stderr=subprocess.PIPE
57
- )
58
-
59
- # 3. Checkout Specific Hash
60
- subprocess.check_call(
61
- ["git", "checkout", commit_hash],
62
- cwd=destination,
63
- stdout=subprocess.DEVNULL,
64
- stderr=subprocess.PIPE
65
- )
66
-
67
- # 4. Verify Head (Paranoid Check)
68
- current_hash = subprocess.check_output(
69
- ["git", "rev-parse", "HEAD"],
70
- cwd=destination
71
- ).decode().strip()
72
-
73
- if current_hash != commit_hash:
74
- raise ValueError(f"Hash Mismatch! Requested {commit_hash}, got {current_hash}")
75
-
76
- # 5. Cleanup .git folder?
77
- # Phase 57 Spec says "The Stamp" is the commit hash.
78
- # We might want to keep .git for provenance or remove it for "frozen" artifact.
79
- # NukePacker wraps the RESULT. So this is just a staging step.
80
- # Let's clean up .git to ensure it's a pure artifact source.
81
- shutil.rmtree(destination / ".git")
82
-
83
- logger.info("✅ Fetch & Checkout Verified.")
84
- return destination
85
-
86
- except subprocess.CalledProcessError as e:
87
- # Capture stderr for better error
88
- raise RuntimeError(f"Git Command Failed: {e}")
89
- except Exception as e:
90
- if destination.exists():
91
- shutil.rmtree(destination)
92
- raise e
@@ -1,208 +0,0 @@
1
- """
2
- File Monitor - Phase 50 Implementation
3
- ======================================
4
- Watches the .brain folder for changes and emits events.
5
-
6
- This is the core of "Native Sync" (Tiers 1-3 in NUCLEUS_PRODUCT_SPECS.md).
7
- When a file changes, we notify all connected MCP clients.
8
-
9
- Usage:
10
- monitor = FileMonitor(brain_path)
11
- monitor.start()
12
- # ... later
13
- monitor.stop()
14
- """
15
-
16
- import os
17
- import threading
18
- import logging
19
- from pathlib import Path
20
- from typing import Callable, Optional, List
21
- from dataclasses import dataclass, field
22
- from datetime import datetime
23
-
24
- try:
25
- from watchdog.observers import Observer
26
- from watchdog.events import FileSystemEventHandler, FileSystemEvent
27
- WATCHDOG_AVAILABLE = True
28
- except ImportError:
29
- WATCHDOG_AVAILABLE = False
30
- Observer = None
31
- FileSystemEventHandler = object
32
- FileSystemEvent = None
33
-
34
- logger = logging.getLogger("nucleus.file_monitor")
35
-
36
-
37
- @dataclass
38
- class FileChangeEvent:
39
- """Represents a file change in the brain folder."""
40
- event_type: str # created, modified, deleted, moved
41
- path: str
42
- timestamp: datetime = field(default_factory=datetime.now)
43
- is_directory: bool = False
44
-
45
- def to_dict(self) -> dict:
46
- return {
47
- "event_type": self.event_type,
48
- "path": self.path,
49
- "timestamp": self.timestamp.isoformat(),
50
- "is_directory": self.is_directory
51
- }
52
-
53
-
54
- class BrainEventHandler(FileSystemEventHandler):
55
- """Handles file system events in the .brain folder."""
56
-
57
- def __init__(self, callback: Callable[[FileChangeEvent], None]):
58
- self.callback = callback
59
- self._debounce_cache: dict = {}
60
- self._debounce_ms = 100 # Ignore duplicate events within 100ms
61
-
62
- def _should_ignore(self, path: str) -> bool:
63
- """Ignore temporary files, caches, and hidden files."""
64
- basename = os.path.basename(path)
65
- ignored_patterns = [
66
- '.DS_Store',
67
- '__pycache__',
68
- '.git',
69
- '*.pyc',
70
- '*.swp',
71
- '*.tmp',
72
- '.resolved.', # Archive backup files
73
- ]
74
- for pattern in ignored_patterns:
75
- if pattern.startswith('*'):
76
- if basename.endswith(pattern[1:]):
77
- return True
78
- elif pattern in basename:
79
- return True
80
- return False
81
-
82
- def _debounce(self, path: str) -> bool:
83
- """Return True if we should process this event (not debounced)."""
84
- now = datetime.now()
85
- key = path
86
- if key in self._debounce_cache:
87
- last_time = self._debounce_cache[key]
88
- if (now - last_time).total_seconds() * 1000 < self._debounce_ms:
89
- return False
90
- self._debounce_cache[key] = now
91
- return True
92
-
93
- def _emit(self, event_type: str, event: FileSystemEvent):
94
- """Emit a file change event."""
95
- if self._should_ignore(event.src_path):
96
- return
97
- if not self._debounce(event.src_path):
98
- return
99
-
100
- change = FileChangeEvent(
101
- event_type=event_type,
102
- path=event.src_path,
103
- is_directory=event.is_directory
104
- )
105
- logger.debug(f"File change detected: {change.event_type} -> {change.path}")
106
- self.callback(change)
107
-
108
- def on_created(self, event):
109
- self._emit("created", event)
110
-
111
- def on_modified(self, event):
112
- self._emit("modified", event)
113
-
114
- def on_deleted(self, event):
115
- self._emit("deleted", event)
116
-
117
- def on_moved(self, event):
118
- self._emit("moved", event)
119
-
120
-
121
- class FileMonitor:
122
- """
123
- Monitors the .brain folder for file changes.
124
-
125
- This enables "push" notifications to MCP clients when files change,
126
- solving the sync problem across Chats/IDEs/CLI.
127
- """
128
-
129
- def __init__(self, brain_path: str, on_change: Optional[Callable[[FileChangeEvent], None]] = None):
130
- self.brain_path = Path(brain_path)
131
- self.on_change = on_change or self._default_handler
132
- self._observer: Optional[Observer] = None
133
- self._running = False
134
- self._event_queue: List[FileChangeEvent] = []
135
- self._lock = threading.Lock()
136
-
137
- if not WATCHDOG_AVAILABLE:
138
- logger.warning("watchdog library not installed. File monitoring disabled.")
139
-
140
- def _default_handler(self, event: FileChangeEvent):
141
- """Default handler just logs and queues events."""
142
- with self._lock:
143
- self._event_queue.append(event)
144
- # Keep queue bounded
145
- if len(self._event_queue) > 100:
146
- self._event_queue = self._event_queue[-50:]
147
-
148
- def start(self) -> bool:
149
- """Start watching the brain folder."""
150
- if not WATCHDOG_AVAILABLE:
151
- logger.error("Cannot start file monitor: watchdog not installed.")
152
- return False
153
-
154
- if self._running:
155
- logger.warning("File monitor already running.")
156
- return True
157
-
158
- if not self.brain_path.exists():
159
- logger.warning(f"Brain path does not exist: {self.brain_path}")
160
- return False
161
-
162
- try:
163
- handler = BrainEventHandler(self.on_change)
164
- self._observer = Observer()
165
- self._observer.schedule(handler, str(self.brain_path), recursive=True)
166
- self._observer.start()
167
- self._running = True
168
- logger.info(f"📡 File monitor started: {self.brain_path}")
169
- return True
170
- except Exception as e:
171
- logger.error(f"Failed to start file monitor: {e}")
172
- return False
173
-
174
- def stop(self):
175
- """Stop watching the brain folder."""
176
- if self._observer and self._running:
177
- self._observer.stop()
178
- self._observer.join(timeout=2)
179
- self._running = False
180
- logger.info("📡 File monitor stopped.")
181
-
182
- def get_pending_events(self) -> List[FileChangeEvent]:
183
- """Get and clear pending events."""
184
- with self._lock:
185
- events = self._event_queue.copy()
186
- self._event_queue.clear()
187
- return events
188
-
189
- @property
190
- def is_running(self) -> bool:
191
- return self._running
192
-
193
-
194
- # Singleton instance for global access
195
- _global_monitor: Optional[FileMonitor] = None
196
-
197
-
198
- def get_file_monitor() -> Optional[FileMonitor]:
199
- """Get the global file monitor instance."""
200
- return _global_monitor
201
-
202
-
203
- def init_file_monitor(brain_path: str, on_change: Optional[Callable] = None) -> FileMonitor:
204
- """Initialize and return the global file monitor."""
205
- global _global_monitor
206
- if _global_monitor is None:
207
- _global_monitor = FileMonitor(brain_path, on_change)
208
- return _global_monitor
@@ -1,91 +0,0 @@
1
- """
2
- Nucleus Firestore Bridge
3
- ========================
4
- Bridges the Local Brain (CLI) with the Cloud Brain (Orchestrator) via Firestore.
5
-
6
- Logic:
7
- - When an event is emitted locally, we push it to the 'nucleus-events' collection.
8
- - This allows the Cloud Orchestrator to see local events (like "Task Added").
9
-
10
- Location: mcp_server_nucleus/runtime/firestore_bridge.py
11
- """
12
-
13
- import logging
14
- from typing import Dict, Any
15
-
16
- try:
17
- from google.cloud import firestore
18
- HAS_FIRESTORE = True
19
- except ImportError:
20
- HAS_FIRESTORE = False
21
-
22
- logger = logging.getLogger(__name__)
23
-
24
- # Configuration
25
- PROJECT_ID = "gen-lang-client-0894185576"
26
- COLLECTION_NAME = "nucleus-events"
27
-
28
- class FirestoreBridge:
29
- def __init__(self, project_id: str = PROJECT_ID):
30
- self.project_id = project_id
31
- self.client = None
32
- self.enabled = False
33
-
34
- # Only enable if we have the library and credentials (or can get them)
35
- if HAS_FIRESTORE:
36
- try:
37
- # This will use Application Default Credentials (gcloud auth application-default login)
38
- self.client = firestore.Client(project=project_id)
39
- self.enabled = True
40
- except Exception as e:
41
- logger.debug(f"Firestore Bridge disabled (no creds): {e}")
42
-
43
- def push_event(self, event: Dict[str, Any]) -> bool:
44
- """Push a local event to the cloud."""
45
- if not self.enabled or not self.client:
46
- return False
47
-
48
- try:
49
- # key by event_id for idempotency
50
- event_id = event.get("event_id")
51
- if not event_id:
52
- return False
53
-
54
- doc_ref = self.client.collection(COLLECTION_NAME).document(event_id)
55
- doc_ref.set(event)
56
- return True
57
- except Exception as e:
58
- logger.warning(f"Failed to push event to cloud: {e}")
59
- return False
60
-
61
- def list_cloud_tasks(self, limit: int = 50) -> list:
62
- """Fetch tasks from the cloud 'nucleus-tasks' collection."""
63
- if not self.enabled or not self.client:
64
- return []
65
-
66
- try:
67
- # We assume cloud tasks are in 'nucleus-tasks'
68
- # Ordered by priority (asc) and created_at (desc)
69
- docs = self.client.collection("nucleus-tasks") \
70
- .order_by("priority", direction=firestore.Query.ASCENDING) \
71
- .limit(limit) \
72
- .stream()
73
-
74
- tasks = []
75
- for doc in docs:
76
- t = doc.to_dict()
77
- t["source"] = "cloud" # Mark as cloud source
78
- tasks.append(t)
79
- return tasks
80
- except Exception as e:
81
- logger.warning(f"Failed to list cloud tasks: {e}")
82
- return []
83
-
84
- # Singleton instance
85
- _bridge = None
86
-
87
- def get_bridge() -> FirestoreBridge:
88
- global _bridge
89
- if _bridge is None:
90
- _bridge = FirestoreBridge()
91
- return _bridge
@@ -1,186 +0,0 @@
1
- """
2
- GCloud Ops - Phase 49 Implementation
3
- ====================================
4
- Wraps the local `gcloud` CLI to query infrastructure state.
5
-
6
- This enables "Smooth across IDEs and CLI" (Tier 4 in NUCLEUS_PRODUCT_SPECS.md).
7
- The agent becomes the DevOps engineer using YOUR local gcloud auth.
8
-
9
- Usage:
10
- ops = GCloudOps()
11
- status = ops.get_cloud_run_services()
12
- """
13
-
14
- import os
15
- import json
16
- import subprocess
17
- import shutil
18
- import logging
19
- from typing import Optional, List, Dict, Any
20
- from dataclasses import dataclass
21
-
22
- logger = logging.getLogger("nucleus.gcloud_ops")
23
-
24
-
25
- def find_gcloud() -> Optional[str]:
26
- """Find gcloud binary in PATH or common locations."""
27
- # 1. Check PATH
28
- gcloud_path = shutil.which("gcloud")
29
- if gcloud_path:
30
- return gcloud_path
31
-
32
- # 2. Check common locations (Mac/Linux)
33
- common_paths = [
34
- os.path.expanduser("~/google-cloud-sdk/bin/gcloud"),
35
- "/usr/local/bin/gcloud",
36
- "/opt/homebrew/bin/gcloud",
37
- "/usr/bin/gcloud",
38
- # Homebrew on Apple Silicon
39
- "/opt/homebrew/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/gcloud",
40
- ]
41
- for p in common_paths:
42
- if os.path.exists(p):
43
- return p
44
-
45
- return None
46
-
47
-
48
- @dataclass
49
- class GCloudResult:
50
- """Result of a gcloud command."""
51
- success: bool
52
- data: Any = None
53
- error: Optional[str] = None
54
- command: str = ""
55
-
56
- def to_dict(self) -> dict:
57
- return {
58
- "success": self.success,
59
- "data": self.data,
60
- "error": self.error,
61
- "command": self.command
62
- }
63
-
64
-
65
- class GCloudOps:
66
- """
67
- Wraps gcloud CLI for infrastructure queries.
68
-
69
- Uses the user's local gcloud authentication - no API keys needed.
70
- """
71
-
72
- def __init__(self, project: Optional[str] = None, region: str = "us-central1"):
73
- self.gcloud_path = find_gcloud()
74
- self.project = project or os.environ.get("GCLOUD_PROJECT")
75
- self.region = region
76
-
77
- @property
78
- def is_available(self) -> bool:
79
- """Check if gcloud is available."""
80
- return self.gcloud_path is not None
81
-
82
- def _run(self, args: List[str], format_json: bool = True) -> GCloudResult:
83
- """Run a gcloud command and return result."""
84
- if not self.is_available:
85
- return GCloudResult(
86
- success=False,
87
- error="gcloud CLI not found. Install: https://cloud.google.com/sdk/docs/install",
88
- command=" ".join(args)
89
- )
90
-
91
- cmd = [self.gcloud_path] + args
92
- if format_json:
93
- cmd.extend(["--format", "json"])
94
-
95
- full_cmd = " ".join(cmd)
96
-
97
- try:
98
- result = subprocess.run(
99
- cmd,
100
- capture_output=True,
101
- text=True,
102
- timeout=30
103
- )
104
-
105
- if result.returncode != 0:
106
- return GCloudResult(
107
- success=False,
108
- error=result.stderr.strip() or f"Command failed with exit code {result.returncode}",
109
- command=full_cmd
110
- )
111
-
112
- if format_json and result.stdout.strip():
113
- try:
114
- data = json.loads(result.stdout)
115
- except json.JSONDecodeError:
116
- data = result.stdout.strip()
117
- else:
118
- data = result.stdout.strip()
119
-
120
- return GCloudResult(success=True, data=data, command=full_cmd)
121
-
122
- except subprocess.TimeoutExpired:
123
- return GCloudResult(
124
- success=False,
125
- error="Command timed out (30s)",
126
- command=full_cmd
127
- )
128
- except Exception as e:
129
- return GCloudResult(
130
- success=False,
131
- error=str(e),
132
- command=full_cmd
133
- )
134
-
135
- def get_current_project(self) -> GCloudResult:
136
- """Get the current active project."""
137
- return self._run(["config", "get-value", "project"], format_json=False)
138
-
139
- def get_account(self) -> GCloudResult:
140
- """Get the current authenticated account."""
141
- return self._run(["config", "get-value", "account"], format_json=False)
142
-
143
- def list_cloud_run_services(self, project: Optional[str] = None) -> GCloudResult:
144
- """List Cloud Run services."""
145
- args = ["run", "services", "list"]
146
- if project or self.project:
147
- args.extend(["--project", project or self.project])
148
- args.extend(["--region", self.region])
149
- return self._run(args)
150
-
151
- def get_cloud_run_service(self, service_name: str, project: Optional[str] = None) -> GCloudResult:
152
- """Get details of a specific Cloud Run service."""
153
- args = ["run", "services", "describe", service_name]
154
- if project or self.project:
155
- args.extend(["--project", project or self.project])
156
- args.extend(["--region", self.region])
157
- return self._run(args)
158
-
159
- def get_cloud_run_revisions(self, service_name: str, project: Optional[str] = None) -> GCloudResult:
160
- """List revisions for a Cloud Run service."""
161
- args = ["run", "revisions", "list", "--service", service_name]
162
- if project or self.project:
163
- args.extend(["--project", project or self.project])
164
- args.extend(["--region", self.region])
165
- return self._run(args)
166
-
167
- def check_auth_status(self) -> Dict[str, Any]:
168
- """Check overall gcloud authentication status."""
169
- return {
170
- "gcloud_available": self.is_available,
171
- "gcloud_path": self.gcloud_path,
172
- "project": self.get_current_project().data if self.is_available else None,
173
- "account": self.get_account().data if self.is_available else None,
174
- }
175
-
176
-
177
- # Singleton instance
178
- _gcloud_ops: Optional[GCloudOps] = None
179
-
180
-
181
- def get_gcloud_ops() -> GCloudOps:
182
- """Get or create the global GCloudOps instance."""
183
- global _gcloud_ops
184
- if _gcloud_ops is None:
185
- _gcloud_ops = GCloudOps()
186
- return _gcloud_ops