fss-link 1.5.7 → 1.6.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 (40) hide show
  1. package/README.md +1 -1
  2. package/bundle/fss-link.js +4785 -3183
  3. package/package.json +4 -1
  4. package/scripts/analyze-session-logs.sh +279 -0
  5. package/scripts/build.js +55 -0
  6. package/scripts/build_package.js +37 -0
  7. package/scripts/build_sandbox.js +195 -0
  8. package/scripts/build_vscode_companion.js +30 -0
  9. package/scripts/check-build-status.js +148 -0
  10. package/scripts/check-publish.js +101 -0
  11. package/scripts/clean.js +55 -0
  12. package/scripts/copy_bundle_assets.js +40 -0
  13. package/scripts/copy_files.js +56 -0
  14. package/scripts/create_alias.sh +39 -0
  15. package/scripts/emergency-kill-all-tests.sh +95 -0
  16. package/scripts/emergency-kill-vitest.sh +95 -0
  17. package/scripts/extract-session-logs.sh +202 -0
  18. package/scripts/generate-git-commit-info.js +71 -0
  19. package/scripts/get-previous-tag.js +213 -0
  20. package/scripts/get-release-version.js +119 -0
  21. package/scripts/index-session-logs.sh +173 -0
  22. package/scripts/install-linux.sh +294 -0
  23. package/scripts/install-macos.sh +343 -0
  24. package/scripts/install-windows.ps1 +427 -0
  25. package/scripts/local_telemetry.js +219 -0
  26. package/scripts/memory-monitor.sh +165 -0
  27. package/scripts/postinstall-message.js +31 -0
  28. package/scripts/prepare-package.js +51 -0
  29. package/scripts/process-session-log.py +302 -0
  30. package/scripts/quick-install.sh +195 -0
  31. package/scripts/sandbox_command.js +126 -0
  32. package/scripts/start.js +76 -0
  33. package/scripts/telemetry.js +85 -0
  34. package/scripts/telemetry_gcp.js +188 -0
  35. package/scripts/telemetry_utils.js +421 -0
  36. package/scripts/test-windows-paths.js +51 -0
  37. package/scripts/tests/get-release-version.test.js +110 -0
  38. package/scripts/tests/test-setup.ts +12 -0
  39. package/scripts/tests/vitest.config.ts +20 -0
  40. package/scripts/version.js +83 -0
@@ -0,0 +1,165 @@
1
+ #!/bin/bash
2
+
3
+ # 📊 Memory Monitor for Test Processes
4
+ # Purpose: Monitor memory usage during tests and auto-kill if dangerous
5
+ # Usage: ./scripts/memory-monitor.sh [--auto-kill] [--threshold-gb=8]
6
+
7
+ # Default settings
8
+ AUTO_KILL=false
9
+ THRESHOLD_GB=8
10
+ INTERVAL=5
11
+
12
+ # Colors
13
+ RED='\033[0;31m'
14
+ YELLOW='\033[1;33m'
15
+ GREEN='\033[0;32m'
16
+ BLUE='\033[0;34m'
17
+ NC='\033[0m'
18
+
19
+ # Parse arguments
20
+ for arg in "$@"; do
21
+ case $arg in
22
+ --auto-kill)
23
+ AUTO_KILL=true
24
+ shift
25
+ ;;
26
+ --threshold-gb=*)
27
+ THRESHOLD_GB="${arg#*=}"
28
+ shift
29
+ ;;
30
+ --interval=*)
31
+ INTERVAL="${arg#*=}"
32
+ shift
33
+ ;;
34
+ --help)
35
+ echo "Memory Monitor for Test Processes"
36
+ echo ""
37
+ echo "Usage: $0 [options]"
38
+ echo ""
39
+ echo "Options:"
40
+ echo " --auto-kill Automatically kill processes exceeding threshold"
41
+ echo " --threshold-gb=N Memory threshold in GB (default: 8)"
42
+ echo " --interval=N Check interval in seconds (default: 5)"
43
+ echo " --help Show this help"
44
+ echo ""
45
+ echo "Examples:"
46
+ echo " $0 # Monitor only"
47
+ echo " $0 --auto-kill # Monitor and auto-kill at 8GB"
48
+ echo " $0 --auto-kill --threshold-gb=4 # Auto-kill at 4GB"
49
+ exit 0
50
+ ;;
51
+ esac
52
+ done
53
+
54
+ THRESHOLD_KB=$((THRESHOLD_GB * 1024 * 1024))
55
+
56
+ echo -e "${BLUE}📊 FSS Link Memory Monitor${NC}"
57
+ echo -e "${YELLOW}Threshold: ${THRESHOLD_GB}GB | Auto-kill: ${AUTO_KILL} | Interval: ${INTERVAL}s${NC}"
58
+ echo -e "${YELLOW}Press Ctrl+C to stop monitoring${NC}"
59
+ echo ""
60
+
61
+ # Function to format memory
62
+ format_memory() {
63
+ local kb=$1
64
+ if [ $kb -gt 1048576 ]; then
65
+ echo "$((kb / 1024 / 1024))GB"
66
+ elif [ $kb -gt 1024 ]; then
67
+ echo "$((kb / 1024))MB"
68
+ else
69
+ echo "${kb}KB"
70
+ fi
71
+ }
72
+
73
+ # Function to kill dangerous processes
74
+ kill_dangerous_processes() {
75
+ local pids="$1"
76
+ echo -e "${RED}🚨 EMERGENCY: Memory threshold exceeded!${NC}"
77
+ echo -e "${RED}💀 Killing dangerous processes...${NC}"
78
+
79
+ for pid in $pids; do
80
+ if [ -n "$pid" ]; then
81
+ kill -KILL "$pid" 2>/dev/null && echo "Killed PID $pid" || true
82
+ fi
83
+ done
84
+
85
+ # Run the emergency script as backup
86
+ echo -e "${YELLOW}Running emergency cleanup...${NC}"
87
+ ./scripts/emergency-kill-vitest.sh --force
88
+ }
89
+
90
+ # Monitoring loop
91
+ while true; do
92
+ clear
93
+ echo -e "${BLUE}📊 FSS Link Memory Monitor - $(date)${NC}"
94
+ echo -e "${YELLOW}Threshold: ${THRESHOLD_GB}GB | Auto-kill: ${AUTO_KILL}${NC}"
95
+ echo ""
96
+
97
+ # System memory overview
98
+ echo -e "${GREEN}💻 System Memory:${NC}"
99
+ free -h
100
+ echo ""
101
+
102
+ # Find test processes
103
+ TEST_PIDS=$(pgrep -f "vitest\|node.*test\|jest\|mocha" 2>/dev/null || true)
104
+
105
+ if [ -z "$TEST_PIDS" ]; then
106
+ echo -e "${GREEN}✅ No test processes running${NC}"
107
+ else
108
+ echo -e "${YELLOW}🧪 Test Processes:${NC}"
109
+ printf "%-8s %-8s %-10s %-10s %s\n" "PID" "CPU%" "MEMORY" "STATUS" "COMMAND"
110
+ echo "────────────────────────────────────────────────────────────────"
111
+
112
+ DANGEROUS_PIDS=""
113
+ TOTAL_TEST_MEMORY=0
114
+
115
+ for pid in $TEST_PIDS; do
116
+ if [ -n "$pid" ]; then
117
+ # Get process info
118
+ if ps -p "$pid" > /dev/null 2>&1; then
119
+ INFO=$(ps -p "$pid" -o pid,%cpu,rss,comm --no-headers 2>/dev/null)
120
+ if [ -n "$INFO" ]; then
121
+ MEMORY_KB=$(echo "$INFO" | awk '{print $3}')
122
+ CPU=$(echo "$INFO" | awk '{print $2}')
123
+ COMMAND=$(echo "$INFO" | awk '{print $4}')
124
+
125
+ TOTAL_TEST_MEMORY=$((TOTAL_TEST_MEMORY + MEMORY_KB))
126
+
127
+ # Format memory for display
128
+ MEMORY_DISPLAY=$(format_memory $MEMORY_KB)
129
+
130
+ # Check if dangerous
131
+ STATUS="OK"
132
+ if [ $MEMORY_KB -gt $THRESHOLD_KB ]; then
133
+ STATUS="${RED}DANGER${NC}"
134
+ DANGEROUS_PIDS="$DANGEROUS_PIDS $pid"
135
+ elif [ $MEMORY_KB -gt $((THRESHOLD_KB / 2)) ]; then
136
+ STATUS="${YELLOW}WARNING${NC}"
137
+ else
138
+ STATUS="${GREEN}OK${NC}"
139
+ fi
140
+
141
+ printf "%-8s %-8s %-10s %-20s %s\n" "$pid" "$CPU%" "$MEMORY_DISPLAY" "$STATUS" "$COMMAND"
142
+ fi
143
+ fi
144
+ fi
145
+ done
146
+
147
+ echo ""
148
+ echo -e "${BLUE}📈 Total Test Memory: $(format_memory $TOTAL_TEST_MEMORY)${NC}"
149
+
150
+ # Check for auto-kill
151
+ if [ "$AUTO_KILL" = true ] && [ -n "$DANGEROUS_PIDS" ]; then
152
+ kill_dangerous_processes "$DANGEROUS_PIDS"
153
+ break
154
+ elif [ -n "$DANGEROUS_PIDS" ]; then
155
+ echo ""
156
+ echo -e "${RED}⚠️ DANGEROUS processes detected!${NC}"
157
+ echo -e "${YELLOW}Run with --auto-kill to automatically terminate them${NC}"
158
+ echo -e "${YELLOW}Or manually run: ./scripts/emergency-kill-vitest.sh${NC}"
159
+ fi
160
+ fi
161
+
162
+ echo ""
163
+ echo -e "${BLUE}Next check in ${INTERVAL} seconds... (Ctrl+C to stop)${NC}"
164
+ sleep $INTERVAL
165
+ done
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ // FSS Link postinstall guidance — printed after npm install -g fss-link
8
+ 'use strict';
9
+
10
+ const lines = [
11
+ '',
12
+ '╔════════════════════════════════════════╗',
13
+ '║ FSS Link installed! ║',
14
+ '╚════════════════════════════════════════╝',
15
+ '',
16
+ 'Get started:',
17
+ ' fss-link Launch interactive mode',
18
+ ' fss-link --help Show all options',
19
+ '',
20
+ 'Quick setup (CLI flags):',
21
+ ' fss-link --provider lm-studio --model your-model',
22
+ ' fss-link --provider bob-ai --model qwen3.5-35b-a3b',
23
+ ' fss-link --provider openai-api-key --model gpt-4o',
24
+ '',
25
+ 'Or launch fss-link and use /auth to configure interactively.',
26
+ '',
27
+ 'Settings saved to: ~/.fss-link/',
28
+ '',
29
+ ];
30
+
31
+ console.log(lines.join('\n'));
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ // ES module equivalent of __dirname
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ const rootDir = path.resolve(__dirname, '..');
16
+
17
+ function copyFiles(packageName, filesToCopy) {
18
+ const packageDir = path.resolve(rootDir, 'packages', packageName);
19
+ if (!fs.existsSync(packageDir)) {
20
+ console.error(`Error: Package directory not found at ${packageDir}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ console.log(`Preparing package: ${packageName}`);
25
+ for (const [source, dest] of Object.entries(filesToCopy)) {
26
+ const sourcePath = path.resolve(rootDir, source);
27
+ const destPath = path.resolve(packageDir, dest);
28
+ try {
29
+ fs.copyFileSync(sourcePath, destPath);
30
+ console.log(`Copied ${source} to packages/${packageName}/`);
31
+ } catch (err) {
32
+ console.error(`Error copying ${source}:`, err);
33
+ process.exit(1);
34
+ }
35
+ }
36
+ }
37
+
38
+ // Prepare 'core' package
39
+ copyFiles('core', {
40
+ 'README.md': 'README.md',
41
+ LICENSE: 'LICENSE',
42
+ '.npmrc': '.npmrc',
43
+ });
44
+
45
+ // Prepare 'cli' package
46
+ copyFiles('cli', {
47
+ 'README.md': 'README.md',
48
+ LICENSE: 'LICENSE',
49
+ });
50
+
51
+ console.log('Successfully prepared all packages.');
@@ -0,0 +1,302 @@
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()