182 lines
5.9 KiB
Python
Executable File
182 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import re
|
|
from datetime import datetime
|
|
|
|
|
|
def find_latest_quarantine():
|
|
candidates = sorted(glob.glob(os.path.expanduser('~/vscode-chat-backups/quarantine-*')), reverse=True)
|
|
return candidates[0] if candidates else None
|
|
|
|
|
|
def find_largest_chat_json(base_dir):
|
|
files = glob.glob(os.path.join(base_dir, '**', 'chatSessions', '*.json'), recursive=True)
|
|
if not files:
|
|
return None
|
|
return max(files, key=lambda p: os.path.getsize(p))
|
|
|
|
|
|
def clean_text(text):
|
|
text = text or ''
|
|
text = re.sub(r'\s+', ' ', text).strip()
|
|
return text
|
|
|
|
|
|
def get_request_text(req):
|
|
msg = req.get('message')
|
|
if isinstance(msg, dict):
|
|
return clean_text(msg.get('text') or msg.get('value') or '')
|
|
if isinstance(msg, str):
|
|
return clean_text(msg)
|
|
return ''
|
|
|
|
|
|
def get_response_text(req):
|
|
response = req.get('response', [])
|
|
chunks = []
|
|
if isinstance(response, list):
|
|
for entry in response:
|
|
if isinstance(entry, dict):
|
|
val = entry.get('value') or entry.get('text') or ''
|
|
if isinstance(val, str) and val.strip():
|
|
chunks.append(clean_text(val))
|
|
elif isinstance(entry, str) and entry.strip():
|
|
chunks.append(clean_text(entry))
|
|
if len(chunks) >= 2:
|
|
break
|
|
return clean_text(' '.join(chunks))
|
|
|
|
|
|
def detect_files_and_commands(texts):
|
|
file_pattern = re.compile(r'([A-Za-z0-9_./-]+\.(?:php|blade\.php|json|md|sh|yml|yaml|js|ts|sql|conf))')
|
|
cmd_pattern = re.compile(r'\b(?:php artisan|composer|npm|yarn|git|docker|mysql|rsync|tar|bash)\b[^\n`]*')
|
|
files = []
|
|
commands = []
|
|
for txt in texts:
|
|
if not txt:
|
|
continue
|
|
files.extend(file_pattern.findall(txt))
|
|
commands.extend(cmd_pattern.findall(txt))
|
|
uniq_files = sorted(set(files))[:30]
|
|
uniq_commands = []
|
|
seen = set()
|
|
for cmd in commands:
|
|
c = clean_text(cmd)
|
|
if c and c not in seen:
|
|
seen.add(c)
|
|
uniq_commands.append(c)
|
|
if len(uniq_commands) >= 20:
|
|
break
|
|
return uniq_files, uniq_commands
|
|
|
|
|
|
def summarize(chat_file, output_dir):
|
|
with open(chat_file, 'r', encoding='utf-8', errors='replace') as f:
|
|
data = json.load(f)
|
|
|
|
requests = data.get('requests', []) if isinstance(data, dict) else []
|
|
session_id = data.get('sessionId', os.path.splitext(os.path.basename(chat_file))[0])
|
|
creation = data.get('creationDate')
|
|
last_date = data.get('lastMessageDate')
|
|
|
|
user_samples = []
|
|
assistant_samples = []
|
|
all_texts = []
|
|
|
|
for req in requests:
|
|
if not isinstance(req, dict):
|
|
continue
|
|
ut = get_request_text(req)
|
|
at = get_response_text(req)
|
|
if ut:
|
|
all_texts.append(ut)
|
|
if at:
|
|
all_texts.append(at)
|
|
if ut and len(user_samples) < 5:
|
|
user_samples.append(ut[:300])
|
|
if at and len(assistant_samples) < 5:
|
|
assistant_samples.append(at[:300])
|
|
|
|
files, commands = detect_files_and_commands(all_texts)
|
|
|
|
title = user_samples[0][:120] if user_samples else f"Session {session_id}"
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
out_path = os.path.join(output_dir, f'{session_id}.md')
|
|
|
|
def fmt_ts(ts):
|
|
if isinstance(ts, (int, float)):
|
|
try:
|
|
return datetime.fromtimestamp(ts / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
except Exception:
|
|
return str(ts)
|
|
return str(ts) if ts is not None else 'n/a'
|
|
|
|
with open(out_path, 'w', encoding='utf-8') as out:
|
|
out.write(f"# Chat Summary: {session_id}\n\n")
|
|
out.write(f"- Source: {chat_file}\n")
|
|
out.write(f"- Approx size: {os.path.getsize(chat_file) / (1024*1024):.1f} MB\n")
|
|
out.write(f"- Requests: {len(requests)}\n")
|
|
out.write(f"- Creation: {fmt_ts(creation)}\n")
|
|
out.write(f"- Last message: {fmt_ts(last_date)}\n")
|
|
out.write(f"- Working title: {title}\n\n")
|
|
|
|
out.write("## User intents (sample)\n")
|
|
for s in user_samples:
|
|
out.write(f"- {s}\n")
|
|
if not user_samples:
|
|
out.write("- n/a\n")
|
|
|
|
out.write("\n## Assistant outputs (sample)\n")
|
|
for s in assistant_samples:
|
|
out.write(f"- {s}\n")
|
|
if not assistant_samples:
|
|
out.write("- n/a\n")
|
|
|
|
out.write("\n## Mentioned files (auto-detected)\n")
|
|
for p in files:
|
|
out.write(f"- {p}\n")
|
|
if not files:
|
|
out.write("- n/a\n")
|
|
|
|
out.write("\n## Mentioned commands (auto-detected)\n")
|
|
for c in commands:
|
|
out.write(f"- {c}\n")
|
|
if not commands:
|
|
out.write("- n/a\n")
|
|
|
|
out.write("\n## Essential continuity pack\n")
|
|
out.write("- Goal and scope decided\n")
|
|
out.write("- Files actually changed\n")
|
|
out.write("- Commands run + outcomes\n")
|
|
out.write("- Bugs confirmed + reproduction\n")
|
|
out.write("- Next step and acceptance criteria\n")
|
|
|
|
return out_path
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Summarize a VS Code Copilot chat session JSON')
|
|
parser.add_argument('--chat-file', default='', help='Path to specific chatSessions/*.json file')
|
|
parser.add_argument('--output-dir', default='docs/ai/chat-summaries', help='Output directory for markdown summary')
|
|
args = parser.parse_args()
|
|
|
|
chat_file = args.chat_file.strip()
|
|
if not chat_file:
|
|
quarantine = find_latest_quarantine()
|
|
if not quarantine:
|
|
raise SystemExit('No quarantine folder found and --chat-file not provided.')
|
|
chat_file = find_largest_chat_json(quarantine)
|
|
if not chat_file:
|
|
raise SystemExit('No chat session JSON found in latest quarantine.')
|
|
|
|
out = summarize(chat_file, args.output_dir)
|
|
print(f'Summary created: {out}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|