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.
- package/README.md +1 -1
- package/bundle/fss-link.js +4785 -3183
- package/package.json +4 -1
- package/scripts/analyze-session-logs.sh +279 -0
- package/scripts/build.js +55 -0
- package/scripts/build_package.js +37 -0
- package/scripts/build_sandbox.js +195 -0
- package/scripts/build_vscode_companion.js +30 -0
- package/scripts/check-build-status.js +148 -0
- package/scripts/check-publish.js +101 -0
- package/scripts/clean.js +55 -0
- package/scripts/copy_bundle_assets.js +40 -0
- package/scripts/copy_files.js +56 -0
- package/scripts/create_alias.sh +39 -0
- package/scripts/emergency-kill-all-tests.sh +95 -0
- package/scripts/emergency-kill-vitest.sh +95 -0
- package/scripts/extract-session-logs.sh +202 -0
- package/scripts/generate-git-commit-info.js +71 -0
- package/scripts/get-previous-tag.js +213 -0
- package/scripts/get-release-version.js +119 -0
- package/scripts/index-session-logs.sh +173 -0
- package/scripts/install-linux.sh +294 -0
- package/scripts/install-macos.sh +343 -0
- package/scripts/install-windows.ps1 +427 -0
- package/scripts/local_telemetry.js +219 -0
- package/scripts/memory-monitor.sh +165 -0
- package/scripts/postinstall-message.js +31 -0
- package/scripts/prepare-package.js +51 -0
- package/scripts/process-session-log.py +302 -0
- package/scripts/quick-install.sh +195 -0
- package/scripts/sandbox_command.js +126 -0
- package/scripts/start.js +76 -0
- package/scripts/telemetry.js +85 -0
- package/scripts/telemetry_gcp.js +188 -0
- package/scripts/telemetry_utils.js +421 -0
- package/scripts/test-windows-paths.js +51 -0
- package/scripts/tests/get-release-version.test.js +110 -0
- package/scripts/tests/test-setup.ts +12 -0
- package/scripts/tests/vitest.config.ts +20 -0
- 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()
|