fss-link 1.6.1 → 1.6.12

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 (43) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +63 -316
  3. package/bundle/fss-link.js +4038 -134262
  4. package/docs/CONFIGURATION.md +200 -0
  5. package/docs/README.md +192 -0
  6. package/docs/TOOLS.md +268 -0
  7. package/docs/assets/batch_0001_fss-link-terminal-interface.png +0 -0
  8. package/docs/assets/batch_0002_multiprovider-connection-hub-a.png +0 -0
  9. package/docs/assets/batch_0003_database-persistence-system-a.png +0 -0
  10. package/docs/assets/batch_0004_context-window-compression-a.png +0 -0
  11. package/docs/assets/batch_0005_file-operations-with-safety.png +0 -0
  12. package/docs/assets/batch_0006_semantic-search-rag-system.png +0 -0
  13. package/docs/assets/batch_0008_document-parsing-suite-a.png +0 -0
  14. package/docs/assets/batch_0009_web-research-tools-a.png +0 -0
  15. package/docs/assets/batch_0010_texttospeech-voice-output-a.png +0 -0
  16. package/docs/assets/batch_0011_shell-command-execution-a.png +0 -0
  17. package/docs/assets/batch_0012_mcp-extension-system-a.png +0 -0
  18. package/docs/assets/batch_0014_approval-mode-switches-a.png +0 -0
  19. package/docs/assets/batch_0015_folder-trust-system-a.png +0 -0
  20. package/package.json +53 -38
  21. package/scripts/check-publish.js +2 -1
  22. package/scripts/install-linux.sh +1 -2
  23. package/scripts/install-macos.sh +1 -2
  24. package/scripts/install-windows.ps1 +1 -2
  25. package/scripts/prebundle-sync-dist.js +51 -0
  26. package/README.pdf +0 -0
  27. package/scripts/analyze-session-logs.sh +0 -279
  28. package/scripts/emergency-kill-all-tests.sh +0 -95
  29. package/scripts/emergency-kill-vitest.sh +0 -95
  30. package/scripts/extract-session-logs.sh +0 -202
  31. package/scripts/get-previous-tag.js +0 -213
  32. package/scripts/get-release-version.js +0 -119
  33. package/scripts/index-session-logs.sh +0 -173
  34. package/scripts/local_telemetry.js +0 -219
  35. package/scripts/memory-monitor.sh +0 -165
  36. package/scripts/process-session-log.py +0 -302
  37. package/scripts/quick-install.sh +0 -195
  38. package/scripts/telemetry_gcp.js +0 -188
  39. package/scripts/telemetry_utils.js +0 -421
  40. package/scripts/test-windows-paths.js +0 -51
  41. package/scripts/tests/get-release-version.test.js +0 -110
  42. package/scripts/tests/test-setup.ts +0 -12
  43. package/scripts/tests/vitest.config.ts +0 -20
@@ -1,302 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- FSS Link Session Log Processor
4
- Converts JSON session logs to markdown format optimized for RAG indexing
5
- """
6
-
7
- import json
8
- import sys
9
- from pathlib import Path
10
- from datetime import datetime
11
- from typing import Dict, List, Any, Optional
12
- import re
13
-
14
-
15
- class SessionLogProcessor:
16
- """Process FSS Link session logs into RAG-optimized markdown"""
17
-
18
- def __init__(self, input_path: str, output_path: str):
19
- self.input_path = Path(input_path)
20
- self.output_path = Path(output_path)
21
- self.session_data: Dict[str, Any] = {}
22
- self.metadata: Dict[str, Any] = {}
23
- self.tool_calls: List[Dict[str, Any]] = []
24
- self.errors: List[Dict[str, Any]] = []
25
-
26
- def load_session(self) -> None:
27
- """Load session log JSON file"""
28
- try:
29
- with open(self.input_path, 'r', encoding='utf-8') as f:
30
- data = json.load(f)
31
- # Handle both array format and object format
32
- if isinstance(data, list):
33
- self.session_data = {'messages': data}
34
- else:
35
- self.session_data = data
36
- except Exception as e:
37
- print(f"Error loading session file: {e}", file=sys.stderr)
38
- sys.exit(1)
39
-
40
- def extract_metadata(self) -> None:
41
- """Extract session metadata from JSON structure (called after analyze_conversation)"""
42
- messages = self.session_data.get('messages', [])
43
-
44
- # Extract session ID from file path
45
- session_id = self.input_path.parent.name
46
-
47
- # Count messages by role
48
- user_messages = sum(1 for m in messages if m.get('role') == 'user')
49
- model_messages = sum(1 for m in messages if m.get('role') == 'model')
50
-
51
- # Estimate session duration from file modification time
52
- file_mtime = self.input_path.stat().st_mtime
53
- session_date = datetime.fromtimestamp(file_mtime)
54
-
55
- # Get unique tools from tool_calls analysis
56
- tools_used = sorted(list(set(call['tool'] for call in self.tool_calls)))
57
-
58
- self.metadata = {
59
- 'session_id': session_id,
60
- 'session_date': session_date.isoformat(),
61
- 'total_messages': len(messages),
62
- 'user_messages': user_messages,
63
- 'model_messages': model_messages,
64
- 'tools_used': tools_used,
65
- 'total_tool_calls': len(self.tool_calls),
66
- 'total_errors': len(self.errors),
67
- 'source_file': str(self.input_path),
68
- }
69
-
70
- def _get_message_text(self, message: Dict[str, Any]) -> str:
71
- """Extract text content from message structure"""
72
- parts = message.get('parts', [])
73
- if not parts:
74
- return ""
75
-
76
- # Handle both string and object parts
77
- text_parts = []
78
- for part in parts:
79
- if isinstance(part, str):
80
- text_parts.append(part)
81
- elif isinstance(part, dict):
82
- if 'text' in part:
83
- # Handle both string and list formats for text
84
- text = part['text']
85
- if isinstance(text, list):
86
- # Join list items (streaming format)
87
- text_parts.append(''.join(str(t) for t in text))
88
- else:
89
- text_parts.append(str(text))
90
- elif 'functionCall' in part:
91
- # Function call representation
92
- func = part['functionCall']
93
- func_name = func.get('name', 'unknown')
94
- text_parts.append(f"[FUNCTION CALL: {func_name}]")
95
-
96
- return ''.join(text_parts) # Use join without separator for streaming format
97
-
98
- def analyze_conversation(self) -> None:
99
- """Analyze conversation for tool calls, errors, and key events"""
100
- messages = self.session_data.get('messages', [])
101
-
102
- for idx, msg in enumerate(messages):
103
- role = msg.get('role', 'unknown')
104
- content = self._get_message_text(msg)
105
- parts = msg.get('parts', [])
106
-
107
- if not content:
108
- continue
109
-
110
- # Detect function calls from parts structure
111
- for part in parts:
112
- if isinstance(part, dict) and 'functionCall' in part:
113
- func_name = part['functionCall'].get('name', 'unknown')
114
- self.tool_calls.append({
115
- 'message_index': idx,
116
- 'tool': func_name,
117
- 'role': role,
118
- 'content_preview': f"[FUNCTION CALL: {func_name}]"
119
- })
120
-
121
- # Also detect from text content patterns
122
- tool_patterns = {
123
- 'read_file': r'\[FUNCTION CALL: read_file\]',
124
- 'grep': r'\[FUNCTION CALL: search_file_content\]',
125
- 'glob': r'\[FUNCTION CALL: glob\]',
126
- 'TodoWrite': r'\[FUNCTION CALL: todo_write\]',
127
- 'bash': r'\[FUNCTION CALL: run_shell_command\]',
128
- 'write': r'\[FUNCTION CALL: write_file\]',
129
- }
130
-
131
- for tool_name, pattern in tool_patterns.items():
132
- if re.search(pattern, content, re.IGNORECASE):
133
- # Only add if not already detected from functionCall
134
- already_detected = any(
135
- call['message_index'] == idx and tool_name in call['tool'].lower()
136
- for call in self.tool_calls
137
- )
138
- if not already_detected:
139
- self.tool_calls.append({
140
- 'message_index': idx,
141
- 'tool': tool_name,
142
- 'role': role,
143
- 'content_preview': content[:200]
144
- })
145
-
146
- # Detect errors
147
- error_patterns = [
148
- r'error:',
149
- r'failed:',
150
- r'exception:',
151
- r'traceback',
152
- r'❌',
153
- ]
154
- for pattern in error_patterns:
155
- if re.search(pattern, content, re.IGNORECASE):
156
- self.errors.append({
157
- 'message_index': idx,
158
- 'role': role,
159
- 'error_preview': content[:300]
160
- })
161
- break
162
-
163
- def generate_markdown(self) -> str:
164
- """Generate RAG-optimized markdown output"""
165
- lines = []
166
-
167
- # Document header with metadata
168
- lines.append(f"# FSS Link Session Log: {self.metadata['session_id'][:16]}...")
169
- lines.append("")
170
- lines.append(f"**Session Date**: {self.metadata['session_date']}")
171
- lines.append(f"**Total Messages**: {self.metadata['total_messages']}")
172
- lines.append(f"**User Messages**: {self.metadata['user_messages']}")
173
- lines.append(f"**Model Messages**: {self.metadata['model_messages']}")
174
- lines.append(f"**Tools Used**: {', '.join(self.metadata['tools_used']) if self.metadata['tools_used'] else 'None detected'}")
175
- lines.append("")
176
- lines.append("---")
177
- lines.append("")
178
-
179
- # Session summary
180
- lines.append("## Session Summary")
181
- lines.append("")
182
- lines.append(f"This session contained {self.metadata['total_messages']} messages with "
183
- f"{len(self.tool_calls)} tool calls and {len(self.errors)} errors detected.")
184
- lines.append("")
185
-
186
- # Tool calls section
187
- if self.tool_calls:
188
- lines.append("## Tool Calls Detected")
189
- lines.append("")
190
-
191
- # Group by tool
192
- tools_grouped = {}
193
- for call in self.tool_calls:
194
- tool = call['tool']
195
- if tool not in tools_grouped:
196
- tools_grouped[tool] = []
197
- tools_grouped[tool].append(call)
198
-
199
- for tool, calls in sorted(tools_grouped.items()):
200
- lines.append(f"### {tool} ({len(calls)} calls)")
201
- lines.append("")
202
- for call in calls[:5]: # Limit to first 5 per tool
203
- lines.append(f"**Message {call['message_index']}** ({call['role']}):")
204
- lines.append(f"```")
205
- lines.append(call['content_preview'])
206
- lines.append(f"```")
207
- lines.append("")
208
-
209
- if len(calls) > 5:
210
- lines.append(f"*... and {len(calls) - 5} more calls*")
211
- lines.append("")
212
-
213
- # Errors section
214
- if self.errors:
215
- lines.append("## Errors and Issues")
216
- lines.append("")
217
- for idx, error in enumerate(self.errors[:10]): # Limit to first 10
218
- lines.append(f"### Error {idx + 1} (Message {error['message_index']})")
219
- lines.append("")
220
- lines.append(f"**Role**: {error['role']}")
221
- lines.append("")
222
- lines.append("```")
223
- lines.append(error['error_preview'])
224
- lines.append("```")
225
- lines.append("")
226
-
227
- if len(self.errors) > 10:
228
- lines.append(f"*... and {len(self.errors) - 10} more errors*")
229
- lines.append("")
230
-
231
- # Full conversation
232
- lines.append("## Full Conversation")
233
- lines.append("")
234
-
235
- messages = self.session_data.get('messages', [])
236
- for idx, msg in enumerate(messages):
237
- role = msg.get('role', 'unknown')
238
- content = self._get_message_text(msg)
239
-
240
- if not content:
241
- continue
242
-
243
- # Limit very long messages
244
- if len(content) > 5000:
245
- content = content[:5000] + "\n\n*[Message truncated for length]*"
246
-
247
- lines.append(f"### Message {idx}: {role.upper()}")
248
- lines.append("")
249
- lines.append(content)
250
- lines.append("")
251
- lines.append("---")
252
- lines.append("")
253
-
254
- # Metadata footer for RAG
255
- lines.append("## Metadata")
256
- lines.append("")
257
- lines.append("```json")
258
- lines.append(json.dumps(self.metadata, indent=2))
259
- lines.append("```")
260
- lines.append("")
261
-
262
- return '\n'.join(lines)
263
-
264
- def process(self) -> None:
265
- """Main processing workflow"""
266
- print(f"Processing session log: {self.input_path.name}")
267
-
268
- # Load and analyze (order matters: analyze first, then extract metadata)
269
- self.load_session()
270
- self.analyze_conversation()
271
- self.extract_metadata()
272
-
273
- # Generate markdown
274
- markdown = self.generate_markdown()
275
-
276
- # Write output
277
- self.output_path.parent.mkdir(parents=True, exist_ok=True)
278
- with open(self.output_path, 'w', encoding='utf-8') as f:
279
- f.write(markdown)
280
-
281
- print(f"✓ Processed successfully")
282
- print(f" - Tools used: {', '.join(self.metadata['tools_used']) if self.metadata['tools_used'] else 'None'}")
283
- print(f" - Tool calls: {len(self.tool_calls)}")
284
- print(f" - Errors found: {len(self.errors)}")
285
- print(f" - Output: {self.output_path}")
286
-
287
-
288
- def main():
289
- """Main entry point"""
290
- if len(sys.argv) != 3:
291
- print("Usage: process-session-log.py <input_json> <output_md>", file=sys.stderr)
292
- sys.exit(1)
293
-
294
- input_path = sys.argv[1]
295
- output_path = sys.argv[2]
296
-
297
- processor = SessionLogProcessor(input_path, output_path)
298
- processor.process()
299
-
300
-
301
- if __name__ == '__main__':
302
- main()
@@ -1,195 +0,0 @@
1
- #!/bin/bash
2
-
3
- # FSS Link Quick Install Script
4
- # Universal installer that detects OS and runs appropriate installation
5
-
6
- set -e
7
-
8
- # Colors for output
9
- RED='\033[0;31m'
10
- GREEN='\033[0;32m'
11
- YELLOW='\033[1;33m'
12
- BLUE='\033[0;34m'
13
- NC='\033[0m'
14
-
15
- log_info() {
16
- echo -e "${BLUE}[INFO]${NC} $1"
17
- }
18
-
19
- log_success() {
20
- echo -e "${GREEN}[SUCCESS]${NC} $1"
21
- }
22
-
23
- log_warning() {
24
- echo -e "${YELLOW}[WARNING]${NC} $1"
25
- }
26
-
27
- log_error() {
28
- echo -e "${RED}[ERROR]${NC} $1"
29
- }
30
-
31
- # Detect operating system
32
- detect_os() {
33
- case "$(uname -s)" in
34
- Darwin)
35
- OS="macos"
36
- ;;
37
- Linux)
38
- OS="linux"
39
- ;;
40
- CYGWIN*|MINGW32*|MSYS*|MINGW*)
41
- OS="windows"
42
- ;;
43
- *)
44
- log_error "Unsupported operating system: $(uname -s)"
45
- exit 1
46
- ;;
47
- esac
48
-
49
- log_info "Detected OS: $OS"
50
- }
51
-
52
- # Download installation script
53
- download_installer() {
54
- local script_name="install-${OS}.sh"
55
- local script_url="https://raw.githubusercontent.com/FSSCoding/fss-link/main/scripts/${script_name}"
56
-
57
- log_info "Downloading $script_name..."
58
-
59
- if command -v curl &> /dev/null; then
60
- curl -fsSL "$script_url" -o "$script_name"
61
- elif command -v wget &> /dev/null; then
62
- wget -q "$script_url" -O "$script_name"
63
- else
64
- log_error "Neither curl nor wget found. Please install one of them."
65
- exit 1
66
- fi
67
-
68
- if [ -f "$script_name" ]; then
69
- chmod +x "$script_name"
70
- log_success "Downloaded $script_name"
71
- return 0
72
- else
73
- log_error "Failed to download $script_name"
74
- return 1
75
- fi
76
- }
77
-
78
- # Run the appropriate installer
79
- run_installer() {
80
- local script_name="install-${OS}.sh"
81
-
82
- if [ "$OS" = "windows" ]; then
83
- # For Windows, we need PowerShell
84
- log_info "For Windows installation, please run:"
85
- log_info "PowerShell -ExecutionPolicy Bypass -File install-windows.ps1"
86
- return 0
87
- fi
88
-
89
- log_info "Running $script_name..."
90
- ./"$script_name" "$@"
91
-
92
- # Clean up
93
- rm -f "$script_name"
94
- }
95
-
96
- # NPM fallback installation
97
- npm_install() {
98
- log_info "Attempting direct NPM installation..."
99
-
100
- if command -v npm &> /dev/null; then
101
- npm install -g fss-link
102
-
103
- if command -v fss-link &> /dev/null; then
104
- log_success "FSS Link installed via NPM"
105
- log_info "Run 'fss-link --help' to get started"
106
- return 0
107
- else
108
- log_error "NPM installation succeeded but fss-link command not found"
109
- return 1
110
- fi
111
- else
112
- log_error "NPM not found. Please install Node.js 20+ first."
113
- return 1
114
- fi
115
- }
116
-
117
- # Main function
118
- main() {
119
- echo "🚀 FSS Link Universal Quick Installer"
120
- echo "====================================="
121
- echo ""
122
-
123
- # Handle help flag
124
- if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
125
- cat << EOF
126
- FSS Link Universal Quick Installer
127
-
128
- Usage: $0 [options]
129
-
130
- Options:
131
- --help, -h Show this help message
132
- --npm-only Skip OS-specific installer, use NPM directly
133
- --version, -v Show script version
134
-
135
- This script will:
136
- 1. Detect your operating system
137
- 2. Download the appropriate installation script
138
- 3. Run the OS-specific installer
139
- 4. Verify the installation
140
-
141
- Supported Systems:
142
- - Linux (Ubuntu, Debian, CentOS, Fedora, Arch, openSUSE)
143
- - macOS (10.15+)
144
- - Windows (10+)
145
-
146
- Requirements:
147
- - Node.js 20.0.0 or higher
148
- - Build tools for native dependencies
149
- - Internet connection for downloading
150
-
151
- Examples:
152
- curl -fsSL https://raw.githubusercontent.com/FSSCoding/fss-link/main/scripts/quick-install.sh | bash
153
- bash quick-install.sh --npm-only
154
- EOF
155
- exit 0
156
- fi
157
-
158
- # Handle version flag
159
- if [ "${1:-}" = "--version" ] || [ "${1:-}" = "-v" ]; then
160
- echo "FSS Link Universal Quick Installer v1.0.0"
161
- exit 0
162
- fi
163
-
164
- # Handle NPM-only installation
165
- if [ "${1:-}" = "--npm-only" ]; then
166
- npm_install
167
- exit $?
168
- fi
169
-
170
- # Detect OS
171
- detect_os
172
-
173
- # Try downloading and running OS-specific installer
174
- if download_installer; then
175
- run_installer "$@"
176
- else
177
- log_warning "OS-specific installer download failed, trying NPM installation..."
178
- npm_install
179
- fi
180
-
181
- # Final verification
182
- if command -v fss-link &> /dev/null; then
183
- VERSION=$(fss-link --version 2>/dev/null || echo "unknown")
184
- log_success "🎉 FSS Link installation complete!"
185
- log_info "Version: $VERSION"
186
- log_info "Run 'fss-link --help' to get started"
187
- else
188
- log_error "Installation may have failed - fss-link command not found"
189
- log_info "Try running: npm install -g fss-link"
190
- exit 1
191
- fi
192
- }
193
-
194
- # Run main function with all arguments
195
- main "$@"
@@ -1,188 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * @license
5
- * Copyright 2025 Google LLC
6
- * SPDX-License-Identifier: Apache-2.0
7
- */
8
-
9
- import path from 'path';
10
- import fs from 'fs';
11
- import { spawn, execSync } from 'child_process';
12
- import {
13
- OTEL_DIR,
14
- BIN_DIR,
15
- fileExists,
16
- waitForPort,
17
- ensureBinary,
18
- manageTelemetrySettings,
19
- registerCleanup,
20
- } from './telemetry_utils.js';
21
-
22
- const OTEL_CONFIG_FILE = path.join(OTEL_DIR, 'collector-gcp.yaml');
23
- const OTEL_LOG_FILE = path.join(OTEL_DIR, 'collector-gcp.log');
24
-
25
- const getOtelConfigContent = (projectId) => `
26
- receivers:
27
- otlp:
28
- protocols:
29
- grpc:
30
- endpoint: "localhost:4317"
31
- processors:
32
- batch:
33
- timeout: 1s
34
- exporters:
35
- googlecloud:
36
- project: "${projectId}"
37
- metric:
38
- prefix: "custom.googleapis.com/gemini_cli"
39
- log:
40
- default_log_name: "gemini_cli"
41
- debug:
42
- verbosity: detailed
43
- service:
44
- telemetry:
45
- logs:
46
- level: "debug"
47
- metrics:
48
- level: "none"
49
- pipelines:
50
- traces:
51
- receivers: [otlp]
52
- processors: [batch]
53
- exporters: [googlecloud]
54
- metrics:
55
- receivers: [otlp]
56
- processors: [batch]
57
- exporters: [googlecloud, debug]
58
- logs:
59
- receivers: [otlp]
60
- processors: [batch]
61
- exporters: [googlecloud, debug]
62
- `;
63
-
64
- async function main() {
65
- console.log('✨ Starting Local Telemetry Exporter for Google Cloud ✨');
66
-
67
- let collectorProcess;
68
- let collectorLogFd;
69
-
70
- const originalSandboxSetting = manageTelemetrySettings(
71
- true,
72
- 'http://localhost:4317',
73
- 'gcp',
74
- );
75
- registerCleanup(
76
- () => [collectorProcess].filter((p) => p), // Function to get processes
77
- () => [collectorLogFd].filter((fd) => fd), // Function to get FDs
78
- originalSandboxSetting,
79
- );
80
-
81
- const projectId = process.env.OTLP_GOOGLE_CLOUD_PROJECT;
82
- if (!projectId) {
83
- console.error(
84
- '🛑 Error: OTLP_GOOGLE_CLOUD_PROJECT environment variable is not exported.',
85
- );
86
- console.log(
87
- ' Please set it to your Google Cloud Project ID and try again.',
88
- );
89
- console.log(' `export OTLP_GOOGLE_CLOUD_PROJECT=your-project-id`');
90
- process.exit(1);
91
- }
92
- console.log(`✅ Using OTLP Google Cloud Project ID: ${projectId}`);
93
-
94
- console.log('\n🔑 Please ensure you are authenticated with Google Cloud:');
95
- console.log(
96
- ' - Run `gcloud auth application-default login` OR ensure `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key.',
97
- );
98
- console.log(
99
- ' - The account needs "Cloud Trace Agent", "Monitoring Metric Writer", and "Logs Writer" roles.',
100
- );
101
-
102
- if (!fileExists(BIN_DIR)) fs.mkdirSync(BIN_DIR, { recursive: true });
103
-
104
- const otelcolPath = await ensureBinary(
105
- 'otelcol-contrib',
106
- 'open-telemetry/opentelemetry-collector-releases',
107
- (version, platform, arch, ext) =>
108
- `otelcol-contrib_${version}_${platform}_${arch}.${ext}`,
109
- 'otelcol-contrib',
110
- false, // isJaeger = false
111
- ).catch((e) => {
112
- console.error(`🛑 Error getting otelcol-contrib: ${e.message}`);
113
- return null;
114
- });
115
- if (!otelcolPath) process.exit(1);
116
-
117
- console.log('🧹 Cleaning up old processes and logs...');
118
- try {
119
- execSync('pkill -f "otelcol-contrib"');
120
- console.log('✅ Stopped existing otelcol-contrib process.');
121
- } catch (_e) {
122
- /* no-op */
123
- }
124
- try {
125
- fs.unlinkSync(OTEL_LOG_FILE);
126
- console.log('✅ Deleted old GCP collector log.');
127
- } catch (e) {
128
- if (e.code !== 'ENOENT') console.error(e);
129
- }
130
-
131
- if (!fileExists(OTEL_DIR)) fs.mkdirSync(OTEL_DIR, { recursive: true });
132
- fs.writeFileSync(OTEL_CONFIG_FILE, getOtelConfigContent(projectId));
133
- console.log(`📄 Wrote OTEL collector config to ${OTEL_CONFIG_FILE}`);
134
-
135
- console.log(`🚀 Starting OTEL collector for GCP... Logs: ${OTEL_LOG_FILE}`);
136
- collectorLogFd = fs.openSync(OTEL_LOG_FILE, 'a');
137
- collectorProcess = spawn(otelcolPath, ['--config', OTEL_CONFIG_FILE], {
138
- stdio: ['ignore', collectorLogFd, collectorLogFd],
139
- env: { ...process.env },
140
- });
141
-
142
- console.log(
143
- `⏳ Waiting for OTEL collector to start (PID: ${collectorProcess.pid})...`,
144
- );
145
-
146
- try {
147
- await waitForPort(4317);
148
- console.log(`✅ OTEL collector started successfully on port 4317.`);
149
- } catch (err) {
150
- console.error(`🛑 Error: OTEL collector failed to start on port 4317.`);
151
- console.error(err.message);
152
- if (collectorProcess && collectorProcess.pid) {
153
- process.kill(collectorProcess.pid, 'SIGKILL');
154
- }
155
- if (fileExists(OTEL_LOG_FILE)) {
156
- console.error('📄 OTEL Collector Log Output:');
157
- console.error(fs.readFileSync(OTEL_LOG_FILE, 'utf-8'));
158
- }
159
- process.exit(1);
160
- }
161
-
162
- collectorProcess.on('error', (err) => {
163
- console.error(`${collectorProcess.spawnargs[0]} process error:`, err);
164
- process.exit(1);
165
- });
166
-
167
- console.log(`\n✨ Local OTEL collector for GCP is running.`);
168
- console.log(
169
- '\n🚀 To send telemetry, run the Gemini CLI in a separate terminal window.',
170
- );
171
- console.log(`\n📄 Collector logs are being written to: ${OTEL_LOG_FILE}`);
172
- console.log(
173
- `📄 Tail collector logs in another terminal: tail -f ${OTEL_LOG_FILE}`,
174
- );
175
- console.log(`\n📊 View your telemetry data in Google Cloud Console:`);
176
- console.log(
177
- ` - Logs: https://console.cloud.google.com/logs/query;query=logName%3D%22projects%2F${projectId}%2Flogs%2Fgemini_cli%22?project=${projectId}`,
178
- );
179
- console.log(
180
- ` - Metrics: https://console.cloud.google.com/monitoring/metrics-explorer?project=${projectId}`,
181
- );
182
- console.log(
183
- ` - Traces: https://console.cloud.google.com/traces/list?project=${projectId}`,
184
- );
185
- console.log(`\nPress Ctrl+C to exit.`);
186
- }
187
-
188
- main();