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.
- {netra_zen-1.0.5.dist-info → netra_zen-1.0.7.dist-info}/METADATA +31 -4
- netra_zen-1.0.7.dist-info/RECORD +27 -0
- {netra_zen-1.0.5.dist-info → netra_zen-1.0.7.dist-info}/top_level.txt +1 -0
- scripts/__init__.py +1 -0
- scripts/__main__.py +5 -0
- scripts/agent_cli.py +6143 -0
- scripts/agent_logs.py +249 -0
- scripts/bump_version.py +138 -0
- scripts/demo_log_collection.py +144 -0
- scripts/embed_release_credentials.py +75 -0
- zen/__main__.py +11 -0
- zen/telemetry/embedded_credentials.py +42 -9
- zen_orchestrator.py +47 -0
- netra_zen-1.0.5.dist-info/RECORD +0 -19
- {netra_zen-1.0.5.dist-info → netra_zen-1.0.7.dist-info}/WHEEL +0 -0
- {netra_zen-1.0.5.dist-info → netra_zen-1.0.7.dist-info}/entry_points.txt +0 -0
- {netra_zen-1.0.5.dist-info → netra_zen-1.0.7.dist-info}/licenses/LICENSE.md +0 -0
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
|
scripts/bump_version.py
ADDED
@@ -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
@@ -1,20 +1,50 @@
|
|
1
|
-
"""
|
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
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
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
|
-
|
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)
|