#!/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()