netra-zen 1.0.5__py3-none-any.whl → 1.0.7__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.
scripts/agent_logs.py ADDED
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Agent Logs Collection Helper
4
+ Collects recent JSONL logs from .claude/Projects for agent CLI integration
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import os
10
+ import platform
11
+ from pathlib import Path
12
+ from typing import Optional, List, Dict, Any
13
+
14
+ # Configure module logger
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _get_default_user() -> Optional[str]:
19
+ """
20
+ Get default username for Windows path resolution.
21
+
22
+ Returns:
23
+ Username from environment or None if not available
24
+ """
25
+ return os.environ.get('USERNAME') or os.environ.get('USER')
26
+
27
+
28
+ def _resolve_projects_root(
29
+ platform_name: Optional[str] = None,
30
+ username: Optional[str] = None,
31
+ base_path: Optional[Path] = None
32
+ ) -> Path:
33
+ """
34
+ Resolve the .claude/Projects root directory based on platform.
35
+
36
+ Args:
37
+ platform_name: Platform identifier ('Darwin', 'Windows', 'Linux') or None for auto-detect
38
+ username: Windows username override
39
+ base_path: Direct path override (bypasses platform resolution)
40
+
41
+ Returns:
42
+ Path to .claude/Projects directory
43
+
44
+ Raises:
45
+ ValueError: If path cannot be resolved
46
+ """
47
+ if base_path:
48
+ return Path(base_path).resolve()
49
+
50
+ platform_name = platform_name or platform.system()
51
+
52
+ if platform_name == 'Windows':
53
+ # Windows: C:\Users\<username>\.claude\Projects
54
+ if username:
55
+ user_home = Path(f"C:/Users/{username}")
56
+ else:
57
+ user_home = Path(os.environ.get('USERPROFILE', Path.home()))
58
+ else:
59
+ # macOS/Linux: ~/.claude/Projects
60
+ user_home = Path.home()
61
+
62
+ projects_root = user_home / ".claude" / "Projects"
63
+
64
+ return projects_root.resolve()
65
+
66
+
67
+ def _sanitize_project_name(project_name: str) -> str:
68
+ """
69
+ Sanitize project name to prevent directory traversal attacks.
70
+
71
+ Args:
72
+ project_name: Raw project name
73
+
74
+ Returns:
75
+ Sanitized project name safe for path construction
76
+
77
+ Raises:
78
+ ValueError: If project name contains dangerous patterns
79
+ """
80
+ if not project_name:
81
+ raise ValueError("Project name cannot be empty")
82
+
83
+ # Remove path separators and parent directory references
84
+ dangerous_patterns = ['..', '/', '\\', '\0']
85
+ for pattern in dangerous_patterns:
86
+ if pattern in project_name:
87
+ raise ValueError(f"Project name contains invalid pattern: {pattern}")
88
+
89
+ # Remove leading/trailing whitespace and dots
90
+ sanitized = project_name.strip().strip('.')
91
+
92
+ if not sanitized:
93
+ raise ValueError("Project name invalid after sanitization")
94
+
95
+ return sanitized
96
+
97
+
98
+ def _find_most_recent_project(projects_root: Path) -> Optional[Path]:
99
+ """
100
+ Find the most recently modified project directory.
101
+
102
+ Args:
103
+ projects_root: Path to .claude/Projects directory
104
+
105
+ Returns:
106
+ Path to most recent project directory or None if no projects found
107
+ """
108
+ if not projects_root.exists() or not projects_root.is_dir():
109
+ logger.warning(f"Projects root does not exist: {projects_root}")
110
+ return None
111
+
112
+ try:
113
+ # Get all subdirectories
114
+ project_dirs = [p for p in projects_root.iterdir() if p.is_dir()]
115
+
116
+ if not project_dirs:
117
+ logger.warning(f"No project directories found in {projects_root}")
118
+ return None
119
+
120
+ # Sort by modification time, most recent first
121
+ project_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
122
+
123
+ return project_dirs[0]
124
+
125
+ except Exception as e:
126
+ logger.error(f"Error finding most recent project: {e}")
127
+ return None
128
+
129
+
130
+ def _collect_jsonl_files(project_path: Path, limit: int) -> List[Dict[str, Any]]:
131
+ """
132
+ Collect and parse JSONL files from project directory.
133
+
134
+ Args:
135
+ project_path: Path to project directory
136
+ limit: Maximum number of log files to read
137
+
138
+ Returns:
139
+ List of parsed log entries (dicts)
140
+ """
141
+ if not project_path.exists() or not project_path.is_dir():
142
+ logger.warning(f"Project path does not exist: {project_path}")
143
+ return []
144
+
145
+ try:
146
+ # Find all .jsonl files
147
+ jsonl_files = list(project_path.glob("*.jsonl"))
148
+
149
+ if not jsonl_files:
150
+ logger.info(f"No .jsonl files found in {project_path}")
151
+ return []
152
+
153
+ # Sort by modification time, most recent first
154
+ jsonl_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
155
+
156
+ # Limit number of files to read
157
+ jsonl_files = jsonl_files[:limit]
158
+
159
+ all_logs = []
160
+
161
+ for jsonl_file in jsonl_files:
162
+ try:
163
+ with open(jsonl_file, 'r', encoding='utf-8') as f:
164
+ for line_num, line in enumerate(f, 1):
165
+ line = line.strip()
166
+ if not line:
167
+ continue
168
+
169
+ try:
170
+ log_entry = json.loads(line)
171
+ all_logs.append(log_entry)
172
+ except json.JSONDecodeError as e:
173
+ logger.debug(
174
+ f"Skipping malformed JSON in {jsonl_file.name}:{line_num}: {e}"
175
+ )
176
+ continue
177
+
178
+ except Exception as e:
179
+ logger.warning(f"Error reading {jsonl_file.name}: {e}")
180
+ continue
181
+
182
+ logger.info(f"Collected {len(all_logs)} log entries from {len(jsonl_files)} files")
183
+ return all_logs
184
+
185
+ except Exception as e:
186
+ logger.error(f"Error collecting JSONL files: {e}")
187
+ return []
188
+
189
+
190
+ def collect_recent_logs(
191
+ limit: int = 5,
192
+ project_name: Optional[str] = None,
193
+ base_path: Optional[str] = None,
194
+ username: Optional[str] = None,
195
+ platform_name: Optional[str] = None
196
+ ) -> Optional[List[Dict[str, Any]]]:
197
+ """
198
+ Collect recent JSONL logs from .claude/Projects directory.
199
+
200
+ Args:
201
+ limit: Maximum number of log files to read (default: 5)
202
+ project_name: Specific project name or None for most recent
203
+ base_path: Direct path override to logs directory
204
+ username: Windows username override
205
+ platform_name: Platform override for testing ('Darwin', 'Windows', 'Linux')
206
+
207
+ Returns:
208
+ List of log entry dicts or None if no logs found
209
+
210
+ Raises:
211
+ ValueError: If limit is not positive or project_name is invalid
212
+ """
213
+ if limit < 1:
214
+ raise ValueError(f"Limit must be positive, got {limit}")
215
+
216
+ try:
217
+ # Resolve projects root
218
+ base = Path(base_path) if base_path else None
219
+ projects_root = _resolve_projects_root(
220
+ platform_name=platform_name,
221
+ username=username,
222
+ base_path=base
223
+ )
224
+
225
+ # Determine target project
226
+ if project_name:
227
+ sanitized_name = _sanitize_project_name(project_name)
228
+ project_path = projects_root / sanitized_name
229
+
230
+ if not project_path.exists():
231
+ logger.warning(f"Specified project does not exist: {project_path}")
232
+ return None
233
+ else:
234
+ # Auto-detect most recent project
235
+ project_path = _find_most_recent_project(projects_root)
236
+ if not project_path:
237
+ return None
238
+
239
+ # Collect logs
240
+ logs = _collect_jsonl_files(project_path, limit)
241
+
242
+ if not logs:
243
+ return None
244
+
245
+ return logs
246
+
247
+ except Exception as e:
248
+ logger.error(f"Failed to collect logs: {e}")
249
+ return None
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Version bump utility for Zen Orchestrator.
4
+ Updates version in all relevant files.
5
+
6
+ Usage:
7
+ python scripts/bump_version.py patch # 1.0.0 -> 1.0.1
8
+ python scripts/bump_version.py minor # 1.0.0 -> 1.1.0
9
+ python scripts/bump_version.py major # 1.0.0 -> 2.0.0
10
+ python scripts/bump_version.py 1.2.3 # Set specific version
11
+ """
12
+
13
+ import re
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Tuple
17
+
18
+
19
+ def parse_version(version_str: str) -> Tuple[int, int, int]:
20
+ """Parse version string to tuple of integers."""
21
+ match = re.match(r'^(\d+)\.(\d+)\.(\d+)$', version_str)
22
+ if not match:
23
+ raise ValueError(f"Invalid version format: {version_str}")
24
+ return tuple(map(int, match.groups()))
25
+
26
+
27
+ def format_version(version_tuple: Tuple[int, int, int]) -> str:
28
+ """Format version tuple to string."""
29
+ return '.'.join(map(str, version_tuple))
30
+
31
+
32
+ def get_current_version() -> str:
33
+ """Get current version from __init__.py."""
34
+ init_file = Path(__file__).parent.parent / "__init__.py"
35
+ content = init_file.read_text()
36
+ match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content)
37
+ if not match:
38
+ raise ValueError("Could not find version in __init__.py")
39
+ return match.group(1)
40
+
41
+
42
+ def bump_version(current: str, bump_type: str) -> str:
43
+ """Bump version based on type."""
44
+ if re.match(r'^\d+\.\d+\.\d+$', bump_type):
45
+ # Specific version provided
46
+ return bump_type
47
+
48
+ major, minor, patch = parse_version(current)
49
+
50
+ if bump_type == 'major':
51
+ return format_version((major + 1, 0, 0))
52
+ elif bump_type == 'minor':
53
+ return format_version((major, minor + 1, 0))
54
+ elif bump_type == 'patch':
55
+ return format_version((major, minor, patch + 1))
56
+ else:
57
+ raise ValueError(f"Invalid bump type: {bump_type}")
58
+
59
+
60
+ def update_file(file_path: Path, old_version: str, new_version: str, patterns: list):
61
+ """Update version in a file using specified patterns."""
62
+ if not file_path.exists():
63
+ print(f" ⚠️ {file_path} does not exist, skipping...")
64
+ return
65
+
66
+ content = file_path.read_text()
67
+ original_content = content
68
+
69
+ for pattern in patterns:
70
+ old_pattern = pattern.format(version=old_version)
71
+ new_pattern = pattern.format(version=new_version)
72
+ content = content.replace(old_pattern, new_pattern)
73
+
74
+ if content != original_content:
75
+ file_path.write_text(content)
76
+ print(f" ✅ Updated {file_path}")
77
+ else:
78
+ print(f" ℹ️ No changes in {file_path}")
79
+
80
+
81
+ def main():
82
+ """Main function."""
83
+ if len(sys.argv) != 2:
84
+ print(__doc__)
85
+ sys.exit(1)
86
+
87
+ bump_type = sys.argv[1]
88
+
89
+ # Get current version
90
+ try:
91
+ current = get_current_version()
92
+ print(f"Current version: {current}")
93
+ except Exception as e:
94
+ print(f"Error getting current version: {e}")
95
+ sys.exit(1)
96
+
97
+ # Calculate new version
98
+ try:
99
+ new = bump_version(current, bump_type)
100
+ print(f"New version: {new}")
101
+ except Exception as e:
102
+ print(f"Error calculating new version: {e}")
103
+ sys.exit(1)
104
+
105
+ # Update files
106
+ base_path = Path(__file__).parent.parent
107
+
108
+ files_to_update = [
109
+ (
110
+ base_path / "__init__.py",
111
+ ['__version__ = "{version}"']
112
+ ),
113
+ (
114
+ base_path / "setup.py",
115
+ ['version="{version}"']
116
+ ),
117
+ (
118
+ base_path / "pyproject.toml",
119
+ ['version = "{version}"']
120
+ ),
121
+ ]
122
+
123
+ print("\nUpdating files:")
124
+ for file_path, patterns in files_to_update:
125
+ update_file(file_path, current, new, patterns)
126
+
127
+ print(f"\n✨ Version bumped from {current} to {new}")
128
+ print("\nNext steps:")
129
+ print(f" 1. Update CHANGELOG.md with changes for v{new}")
130
+ print(f" 2. Commit: git commit -am 'Bump version to {new}'")
131
+ print(f" 3. Tag: git tag -a v{new} -m 'Release version {new}'")
132
+ print(f" 4. Push: git push origin main --tags")
133
+ print(f" 5. Build: python -m build")
134
+ print(f" 6. Upload: python -m twine upload dist/*")
135
+
136
+
137
+ if __name__ == "__main__":
138
+ main()
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Demonstration of log collection from .claude/Projects
4
+
5
+ This script shows how the zen --apex --send-logs functionality works
6
+ """
7
+ import sys
8
+ from pathlib import Path
9
+ import json
10
+
11
+ # Add parent to path for imports
12
+ sys.path.insert(0, str(Path(__file__).parent.parent))
13
+
14
+ from scripts.agent_logs import collect_recent_logs
15
+
16
+
17
+ def demo_log_collection():
18
+ """Demonstrate log collection with various scenarios"""
19
+
20
+ print("=" * 60)
21
+ print("Zen Apex Log Collection Demo")
22
+ print("=" * 60)
23
+ print()
24
+
25
+ # Check if .claude/Projects exists
26
+ claude_path = Path.home() / ".claude" / "Projects"
27
+
28
+ if not claude_path.exists():
29
+ print("❌ .claude/Projects does not exist")
30
+ print(f" Expected location: {claude_path}")
31
+ print()
32
+ print("Creating test directory...")
33
+ claude_path.mkdir(parents=True, exist_ok=True)
34
+ test_project = claude_path / "demo-project"
35
+ test_project.mkdir(exist_ok=True)
36
+
37
+ # Create sample log
38
+ sample_log = {
39
+ "type": "demo_event",
40
+ "timestamp": "2025-01-08T12:00:00",
41
+ "message": "This is a demo log entry",
42
+ "data": {"key": "value"}
43
+ }
44
+ (test_project / "demo-session.jsonl").write_text(json.dumps(sample_log) + "\n")
45
+ print(f"✅ Created demo project at {test_project}")
46
+ print()
47
+
48
+ # Scenario 1: Collect with defaults
49
+ print("Scenario 1: Collect logs with defaults (limit=5, auto-detect project)")
50
+ print("-" * 60)
51
+ logs = collect_recent_logs(limit=5)
52
+
53
+ if logs:
54
+ print(f"✅ Collected {len(logs)} log entries")
55
+ print(f" Total entries: {len(logs)}")
56
+ print()
57
+ print(" Sample entry (first):")
58
+ print(f" {json.dumps(logs[0], indent=4)}")
59
+ else:
60
+ print("⚠️ No logs found")
61
+ print(" Tip: Run Claude Code with some commands to generate logs")
62
+ print()
63
+
64
+ # Scenario 2: List available projects
65
+ print("Scenario 2: List available projects")
66
+ print("-" * 60)
67
+ if claude_path.exists():
68
+ projects = [p for p in claude_path.iterdir() if p.is_dir()]
69
+ if projects:
70
+ print(f"Found {len(projects)} project(s):")
71
+ for proj in sorted(projects, key=lambda p: p.stat().st_mtime, reverse=True):
72
+ jsonl_count = len(list(proj.glob("*.jsonl")))
73
+ mtime = proj.stat().st_mtime
74
+ from datetime import datetime
75
+ mtime_str = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
76
+ marker = " ← most recent" if proj == projects[0] else ""
77
+ print(f" • {proj.name}: {jsonl_count} .jsonl files (modified: {mtime_str}){marker}")
78
+ else:
79
+ print(" No projects found")
80
+ print()
81
+
82
+ # Scenario 3: Collect from specific project
83
+ if claude_path.exists():
84
+ projects = [p for p in claude_path.iterdir() if p.is_dir()]
85
+ if projects:
86
+ specific_project = projects[0].name
87
+ print(f"Scenario 3: Collect from specific project '{specific_project}'")
88
+ print("-" * 60)
89
+ logs = collect_recent_logs(limit=3, project_name=specific_project)
90
+ if logs:
91
+ print(f"✅ Collected {len(logs)} entries from '{specific_project}'")
92
+ print(f" Entry types: {[log.get('type', 'unknown') for log in logs[:3]]}")
93
+ else:
94
+ print(f"⚠️ No logs in '{specific_project}'")
95
+ print()
96
+
97
+ # Scenario 4: Show what would be sent with --send-logs
98
+ print("Scenario 4: What gets sent with 'zen --apex --send-logs --message \"..\"'")
99
+ print("-" * 60)
100
+ logs = collect_recent_logs(limit=5)
101
+ if logs:
102
+ payload_preview = {
103
+ "type": "user_message",
104
+ "payload": {
105
+ "content": "your message here",
106
+ "run_id": "cli_20250108_120000_12345",
107
+ "thread_id": "cli_thread_abc123def456",
108
+ "timestamp": "2025-01-08T12:00:00",
109
+ "jsonl_logs": logs # This is what gets attached
110
+ }
111
+ }
112
+ print("Payload structure:")
113
+ print(json.dumps(payload_preview, indent=2)[:500] + "...")
114
+ print()
115
+ print(f"✅ {len(logs)} log entries would be attached to the message")
116
+ else:
117
+ print("⚠️ No logs would be attached (none found)")
118
+ print()
119
+
120
+ # Summary
121
+ print("=" * 60)
122
+ print("Summary")
123
+ print("=" * 60)
124
+ print()
125
+ print("To use log forwarding with zen --apex:")
126
+ print()
127
+ print(" # Basic usage (attaches last 5 log files)")
128
+ print(" zen --apex --send-logs --message \"analyze these sessions\"")
129
+ print()
130
+ print(" # Custom number of logs")
131
+ print(" zen --apex --send-logs --logs-count 10 --message \"review last 10\"")
132
+ print()
133
+ print(" # Specific project")
134
+ if claude_path.exists() and list(claude_path.iterdir()):
135
+ first_project = list(p for p in claude_path.iterdir() if p.is_dir())[0].name
136
+ print(f" zen --apex --send-logs --logs-project {first_project} --message \"...\"")
137
+ else:
138
+ print(" zen --apex --send-logs --logs-project PROJECT_NAME --message \"...\"")
139
+ print()
140
+ print("=" * 60)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ demo_log_collection()
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ """Embed telemetry credentials for release builds.
3
+
4
+ Usage:
5
+ COMMUNITY_CREDENTIALS="<base64-json>" python scripts/embed_release_credentials.py
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import json
12
+ import os
13
+ import sys
14
+ from pathlib import Path
15
+
16
+
17
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
18
+ TARGET_FILE = PROJECT_ROOT / "zen" / "telemetry" / "embedded_credentials.py"
19
+
20
+
21
+ def main() -> int:
22
+ encoded = os.getenv("COMMUNITY_CREDENTIALS", "").strip()
23
+ if not encoded:
24
+ print(
25
+ "COMMUNITY_CREDENTIALS is not set. Set the base64-encoded "
26
+ "service-account JSON before running this script.",
27
+ file=sys.stderr,
28
+ )
29
+ return 1
30
+
31
+ try:
32
+ decoded = base64.b64decode(encoded)
33
+ info = json.loads(decoded)
34
+ except Exception as exc: # pragma: no cover - defensive guard
35
+ print(f"Failed to decode telemetry credentials: {exc}", file=sys.stderr)
36
+ return 2
37
+
38
+ project_id = info.get("project_id", "netra-telemetry-public")
39
+ encoded_literal = repr(encoded)
40
+
41
+ generated = f'''"""Embedded telemetry credentials. AUTO-GENERATED - DO NOT COMMIT."""
42
+
43
+ import base64
44
+ import json
45
+ from google.oauth2 import service_account
46
+
47
+ _EMBEDDED_CREDENTIALS_B64 = {encoded_literal}
48
+ _CREDENTIALS_DICT = json.loads(
49
+ base64.b64decode(_EMBEDDED_CREDENTIALS_B64.encode("utf-8"))
50
+ )
51
+
52
+
53
+ def get_embedded_credentials():
54
+ """Return service account credentials."""
55
+ try:
56
+ return service_account.Credentials.from_service_account_info(
57
+ _CREDENTIALS_DICT,
58
+ scopes=["https://www.googleapis.com/auth/trace.append"],
59
+ )
60
+ except Exception:
61
+ return None
62
+
63
+
64
+ def get_project_id() -> str:
65
+ """Return GCP project ID."""
66
+ return _CREDENTIALS_DICT.get("project_id", {project_id!r})
67
+ '''
68
+
69
+ TARGET_FILE.write_text(generated)
70
+ print(f"Embedded release credentials written to {TARGET_FILE.relative_to(PROJECT_ROOT)}")
71
+ return 0
72
+
73
+
74
+ if __name__ == "__main__":
75
+ raise SystemExit(main())
zen/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Module entry point to support `python -m zen` invocation."""
2
+
3
+ from zen_orchestrator import run
4
+
5
+
6
+ def main() -> None:
7
+ run()
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -1,20 +1,50 @@
1
- """Embedded telemetry credentials. AUTO-GENERATED - DO NOT COMMIT."""
1
+ """Runtime loader for telemetry credentials from environment variables."""
2
+
3
+ from __future__ import annotations
2
4
 
3
5
  import base64
4
6
  import json
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
5
11
  from google.oauth2 import service_account
6
12
 
7
- _EMBEDDED_CREDENTIALS_B64 = 'ewogICJ0eXBlIjogInNlcnZpY2VfYWNjb3VudCIsCiAgInByb2plY3RfaWQiOiAibmV0cmEtdGVsZW1ldHJ5LXB1YmxpYyIsCiAgInByaXZhdGVfa2V5X2lkIjogImVjOWM4ZGNlZGZmMTUzNjM5YTUxOTcyMzc0MjYyNjkwNjZkNzAxYTQiLAogICJwcml2YXRlX2tleSI6ICItLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS1cbk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRREhUcmZFOHlQdUFCTDNcbk5aS3diZ1AwamRyaWRnY0UwMUlLMks1YkZ1bWFrUHVrRGxzV0dVaUswOXEyaVNYTWVUQmJPZDF0VjFoc3VJcUhcbnpQK0pZd0NTcVp4S0pIQS8yWUdVcERqeWhHRVd3QTNtS3laVXVUUS9yUTBUK20yV0cwdEMxUzBpQzB6U211cEpcbjBNeGhUUDRjKzYreEczSDVSWEF1YjdONmx6eFFWVnJSY3FHMTYydlF5SFA2OWIrd2RidTJHM0o5UkJVN1VUK1FcbkU2RHB2K01YaEp3MnRPdHZLbFBQT3BnWm9Sb0pmeXU1WlpzbHlJZCtzY3FhUTY5ZjBaSmpIRjlYQVdlT25mUTRcbmExbmV0LzJqRjZibWpuZmQ2MjhBODA5cEluTXJEL0FwZjJzUWJJdXJIYUI4am5uTEQ0eDMrbVhXRTgyMFJLNktcbjViZ0FQanNoQWdNQkFBRUNnZ0VBQmZERVZMWVlDakFkb0pscnpyOHF0a056cEpnV3Y3ZXlQSEZHcWgraDFSbndcbjdDd0Mza0xnNlFWbFFaZFBKWHZ2dTJwYlBaYnl3MlBST2ppN25adFNNU3pseEFaM3c0bHV2YkRTNHpTYnBiZFJcbityd3F3Mi8xUFJnaCtZaFhjNWZLNjVvcHd4Zmg0VzJkWWRlYnZlTXkrRWR1cmtsV2dYTG13L1dQbkkzdExlbzlcbjV1elZjbU42Qk04YkU3azFYK1M0RURBS0VRWlprUEdzTFQ4RXN4UmdWOWtnT1Zicm5VQ1Z0dXA1Q0NGbUR3U1Bcbmg0U25wMEsvTUp3b1U3NG4reTlFMXYxUXRnajE5TkhaNHJ2dFpnUlVaandHQy9Cc3ZkcE1PazArZTJEMlgvRk9cblZnc29xS2tDaklWUzRMcG5YSEpZbU5oajZWNHRXUnZ1OW1NTXhTL3FBUUtCZ1FEb3hZenlsdEZKL242ZEQvUHlcbnZLOFRaTHd5dFdCcjBXU3ZHU2VzM0JYRGZoKzBFbU4rRHpnZGdUb0ovbkhpazM5R21QM0tLN2htOFVvaFFHRy9cbkh0SFRuS0lBQlhrSU8yd2Z3N0h0V2pPTXRocHp2dFQxcmVEVHVjVk0wc2lCMHpjTldCMjFUamdQL3JYY2Q3NWVcbklERmNBN0hTbUJDLzB4bzk3aC8wV2YvOW9RS0JnUURiTWtnbjlVR2Y2SVRMNmxTcDdrYWJGL0NuaE4yU2VTMVdcbnd3R21iRThxTTU0UitDcVRUeHk2UHBRaFVSczlHM1VpVmQ1SXZWVDhuT095ZVBZVFJEbnFCYjJ4S214SFRodlZcbnVQcTgwQXB3anBMbzh6VkFDSy9iVFVjSmlKVGFBdXFHaXI1Ykc4YUlldVpIc0pLeWJ1NmhoNkhXMldwWXVVV1BcbkZ3TTl4elpOZ1FLQmdRQzg5dHJVZVJFUVE3VGZwbnJBM09JNEdUZ2E1bG1QVFo2eDh2YmRncEY4Y2FBbEhDUitcbnlyWWdaYThMTysrU0kzRllpNHpFR2pnS0FlblBFcWdIY21xZW9uSjFGL3hJYll6NlFIRHFJYWJsblZQZUVOWnJcblY2dkQxZlRReC9FVVM3Wk9jL0V5Slh5bnAzeFZyVFB5ejZtaWJERm9xQ0E0eVpSdElDbjZ3VEZxNFFLQmdFWFJcbnAxQXErOE0rb2dYOTF3ZmxvTkhIOTF5MG9vc0VWQis5cjZuZDkvMWVRYXhCbXZZZkRleDVBRi80WUsrL0xqbElcbmxxd2V1cEpZT3VMZlNxcHFZZlFiN2djZmx5dkRRblI2SGt2RURIODd1cW0reGlobVcvV0RrT3dGZUR4VkQzVFpcbmZyYXdpelZ2eUNmdm8xcDRvVVFNV3MxL3BUTXJtRzl5aWhMRWdKU0JBb0dCQU1GWm50ZUtUUDZrVVdrVmpOcndcbmUvQzBDbjJ6dk1YNXVnZURkS1FWNkwrY25mRWlRSzdzZ3R5eFp5ek5kMC82QXJ0YnBrcS9wcVlaYXpwVzVFMkxcbkxVMUF3MmdHT25GRlh2ZXg4aXpOZXViMGdvUVE4d3BtL3lrMVNVekR6VTV1dCtPbVFFRmpsbUYrNDkza0ZYcC9cbnc1MWh2WjVVL2loL1NYbjN6cjdEWE5QYlxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwKICAiY2xpZW50X2VtYWlsIjogInplbi1jb21tdW5pdHktdGVsZW1ldHJ5QG5ldHJhLXRlbGVtZXRyeS1wdWJsaWMuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLAogICJjbGllbnRfaWQiOiAiMTE0NzAwMDA0NzA1MDUxODg5NTY4IiwKICAiYXV0aF91cmkiOiAiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLAogICJ0b2tlbl91cmkiOiAiaHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20vdG9rZW4iLAogICJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vb2F1dGgyL3YxL2NlcnRzIiwKICAiY2xpZW50X3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS96ZW4tY29tbXVuaXR5LXRlbGVtZXRyeSU0MG5ldHJhLXRlbGVtZXRyeS1wdWJsaWMuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLAogICJ1bml2ZXJzZV9kb21haW4iOiAiZ29vZ2xlYXBpcy5jb20iCn0K'
8
- _CREDENTIALS_DICT = json.loads(
9
- base64.b64decode(_EMBEDDED_CREDENTIALS_B64.encode("utf-8"))
10
- )
13
+ _ENV_B64 = "COMMUNITY_CREDENTIALS"
14
+ _ENV_PATH = "ZEN_COMMUNITY_TELEMETRY_FILE"
15
+ _ENV_PROJECT = "ZEN_COMMUNITY_TELEMETRY_PROJECT"
16
+ _DEFAULT_PROJECT = "netra-telemetry-public"
17
+
18
+
19
+ def _load_service_account_dict() -> Optional[dict]:
20
+ """Load service account JSON from environment variables."""
21
+ encoded = os.getenv(_ENV_B64)
22
+ if encoded:
23
+ try:
24
+ raw = base64.b64decode(encoded)
25
+ return json.loads(raw)
26
+ except (ValueError, json.JSONDecodeError):
27
+ return None
28
+
29
+ path = os.getenv(_ENV_PATH)
30
+ if path:
31
+ candidate = Path(path).expanduser()
32
+ if candidate.exists():
33
+ try:
34
+ return json.loads(candidate.read_text())
35
+ except json.JSONDecodeError:
36
+ return None
37
+ return None
11
38
 
12
39
 
13
40
  def get_embedded_credentials():
14
- """Return service account credentials."""
41
+ """Return service account credentials or None."""
42
+ info = _load_service_account_dict()
43
+ if not info:
44
+ return None
15
45
  try:
16
46
  return service_account.Credentials.from_service_account_info(
17
- _CREDENTIALS_DICT,
47
+ info,
18
48
  scopes=["https://www.googleapis.com/auth/trace.append"],
19
49
  )
20
50
  except Exception:
@@ -22,5 +52,8 @@ def get_embedded_credentials():
22
52
 
23
53
 
24
54
  def get_project_id() -> str:
25
- """Return GCP project ID."""
26
- return _CREDENTIALS_DICT.get("project_id", 'netra-telemetry-public')
55
+ """Return GCP project ID for telemetry."""
56
+ info = _load_service_account_dict()
57
+ if info and "project_id" in info:
58
+ return info["project_id"]
59
+ return os.getenv(_ENV_PROJECT, _DEFAULT_PROJECT)