Show HN: Contrails – 코딩 에이전트 채팅을 보고 저장소에 저장하는 앱

hackernews | | 📰 뉴스
#claude #ai 서비스 #오픈소스 #저장소 #채팅 기록 #코딩 에이전트
원문 출처: hackernews · Genesis Park에서 요약 및 분석

요약

Contrails는 VS Code Copilot, Claude Code, Cursor 등 코딩 에이전트의 세션이 초기화되면서 개발 과정의 추론과 수정 내역이 사라지는 문제를 해결하기 위해 개발된 오픈소스 앱입니다. 에이전트의 채팅 파일을 실시간으로 감시하고 파싱하여, 프로젝트 저장소 내부에 읽기 쉬운 마크다운(Markdown) 형식으로 자동 저장합니다. 이렇게 기록된 대화 내역은 향후 관련 기능을 작업하거나 버그를 수정할 때 에이전트가 이전의 개발 맥락과 추론 과정을 참고할 수 있도록 도와줍니다. Go, React, TypeScript를 기반으로 Wails v2 프레임워크를 사용해 제작되었으며 macOS, Windows, Linux 운영체제를 모두 지원합니다.

본문

Preserve your coding agent trails. Contrails, short for "condensation trails", are the trails left behind by aircrafts at high altitudes. Contrails is an opensource app (macOS, Windows and Linux) that: - Watches your coding agent sessions (VS Code Copilot, Claude Code, and Cursor) - Parses them into readable Markdown - Saves them into your project repositories. This way you keep the reasoning that led to fixing a bug or implementing a feature. Built with Wails v2 (Go + React + TypeScript). Coding agents forget everything between sessions. The reasoning that led to a fix, the wrong approaches that were tried, the self-corrections — all of it vanishes. Contrails watches agent session files in real-time and outputs clean, human-readable Markdown into a contrails/ directory in your project, making your agent conversations part of your repo history. When working on a related feature in the future, you can reference relevant contrails to help the agent remember its previous reasoning. | Agent | Discovery | Watching | Format | |---|---|---|---| | VS Code Copilot | Scans workspaceStorage/ for workspaces with chatSessions/ | fsnotify on chatSessions/ directory | JSONL event log (v3) | | Claude Code | Scans ~/.claude/projects/ for session files | Signal watcher on ~/contrails/hook-signals/ via Stop hook | JSONL transcript | | Cursor | Scans workspaceStorage/ for per-workspace state.vscdb files | fsnotify on per-workspace storage + global globalStorage/ directory (debounced 2 s) | SQLite — per-workspace ItemTable for composer list + global cursorDiskKV for composerData:* + bubbleId:* keys | - Download Contrails-windows.zip from the latest release - Unzip the archive - Run contrails.exe - Download Contrails-macos.zip from the latest release - Unzip and drag contrails.app to your Applications folder - Remove the macOS quarantine flag: xattr -cr /Applications/contrails.app - Launch Contrails normally Why is step 3 needed? macOS requires apps to be notarized. Notarization costs $99/year — Apple effectively charges indie developers a yearly toll just to let users open their software. Until we bite that bullet, xattr -cr is the workaround. - Download Contrails-linux.zip from the latest release - Unzip the archive - Make the binary executable and run it: chmod +x contrails ./contrails Dependencies: Linux requires WebKit2GTK and GTK3 runtime libraries. On Ubuntu/Debian: sudo apt install libgtk-3-0 libwebkit2gtk-4.1-0 . On Fedora:sudo dnf install gtk3 webkit2gtk4.1 . Most desktop Linux distributions have these pre-installed. - Auto-discovery - Finds projects with agent chat sessions automatically. VS Code and Cursor workspaces are resolved via workspace.json ; Claude Code projects are discovered from~/.claude/projects/ - Filesystem watching - Uses fsnotify to detect new or modified VS Code chat sessions instantly - Signal-based capture - Claude Code sessions are captured via a Stop hook that writes signal files when a session ends - Deleted session handling - When a chat session file is deleted, the corresponding Markdown is flagged with a deletion banner rather than removed - Smart parsing - Preserves the order of text, thinking blocks, tool calls, and file edits exactly as they occurred during the conversation - Multi-agent projects - A single project can have VS Code Copilot, Claude Code, and Cursor sources simultaneously - Markdown output - Generates clean .md files - Project management - Track multiple workspaces, rename them, pause/resume watching - Persistent state - Projects list persists across restarts; selected project stored in localStorage - Anonymous telemetry - Optional, anonymous usage telemetry via PostHog (Go SDK). Tracks aggregate metrics like app starts, project counts, and contrails created. No personal data is collected. A persistent device UUID is generated on first launch — no accounts or sign-in required. Telemetry can be toggled on/off from the sidebar footer; the preference is persisted across restarts. Disabled entirely in dev builds (no API key injected) - Auto-update - On launch, checks GitHub Releases for a newer version. If available, shows a one-click update button. contrails/ ├── main.go # Wails app entry, macOS window config, build-time Version + PostHogAPIKey ├── app.go # Composition root: project CRUD, driver registry, processing dispatch ├── analytics.go # PostHog client wrapper — fail-safe, opt-out, device ID, event tracking ├── updater.go # GitHub Releases update checker + atomic .app bundle replacement ├── runtime.go # Interfaces (EventEmitter, DialogOpener) + Wails implementations ├── types.go # Project, AgentSource, event structs + type aliases to agent pkg ├── watcher.go # fsnotify-based VS Code directory watcher ├── agent/ │ ├── driver.go # AgentDriver interface + ProcessCallbacks │ ├── contrail.go # ParsedSession, ParsedMessage, MessagePart types + SessionParser interface │ ├── writer.go # WriteParsedSession — markdown rendering │ ├── logger.go # Logger interface + LogInfof/LogWarningf/LogErrorf helpers │ ├── format.go # FormatTimestamp, SanitizeFilename utilities │ ├── claudecode/ │ │ ├── driver.go # AgentDriver impl — hook lifecycle + signal watcher + ProcessAll │ │ ├── parser.go # Claude Code JSONL transcript → ParsedSession │ │ ├── scanner.go # Browse ~/.claude/projects/ for sessions │ │ ├── hook.go # Install/uninstall Stop hook in .claude/settings.local.json │ │ ├── hook_enforcer.go # Periodic enforcement — ensures Stop hook stays installed │ │ ├── signal_watcher.go # Watch ~/contrails/hook-signals/ via SignalHandler interface │ │ └── types.go # Claude Code raw types (SignalFile, contentBlock, etc.) │ ├── vscode/ │ │ ├── driver.go # AgentDriver impl — fsnotify lifecycle + ProcessAll + HealContrailNames │ │ ├── parser.go # VS Code JSONL event log → ParsedSession (materialize + walk) │ │ └── types.go # VS Code raw types (chatSession, jsonlEvent, etc.) │ └── cursor/ │ ├── driver.go # AgentDriver impl — fsnotify on workspaceStorage (debounced), ProcessAll │ ├── parser.go # Cursor SQLite cursorDiskKV → ParsedSession (composerData + bubbleId keys) │ ├── scanner.go # Scan workspaceStorage for per-workspace state.vscdb files │ └── types.go # Cursor raw types (composerData, bubble, capabilityType, etc.) └── frontend/ └── src/ ├── App.tsx # Root component: sidebar + main layout ├── App.css # All component styles ├── style.css # CSS reset, variables, base styles ├── types.ts # TypeScript interfaces ├── hooks/ │ └── useProjects.ts # State management, watcher event listener └── components/ ├── ProjectList.tsx # Sidebar project list with context menus ├── ProjectDetail.tsx # Selected project detail view ├── AddProjectDialog.tsx # Workspace browser + project config dialog ├── OnboardingTour.tsx # First-run guided tour └── ContrailsIcon.tsx # App icon component | File | Responsibility | |---|---| app.go | Composition root — project CRUD (persisted to platform config dir + contrails/projects.json ), driver registry + dispatch, native directory pickers, workspace scanning, incremental processing, telemetry settings, update check. Implements claudecode.SignalHandler to receive signal events via dependency inversion. | analytics.go | PostHog client wrapper — fail-safe design (all methods silently no-op on failure), device ID generation/persistence (~/.config/contrails/device_id ), opt-out support, event tracking (app lifecycle, project/source/contrail events). Disabled entirely when API key is empty (dev builds). | updater.go | GitHub Releases update checker — polls ThreePalmTrees/Contrails releases, semantic version comparison, downloads and atomically replaces the full .app bundle, relaunches via open . Skipped for dev builds. | runtime.go | Testability interfaces (Logger , EventEmitter , DialogOpener ) with production (Wails-backed) and test (Noop, Recording) implementations. Logger is a type alias for agent.Logger . | watcher.go | Wraps fsnotify to watch VS Code chatSessions/ directories for .jsonl files, emits events to frontend via Wails runtime | types.go | Project, AgentSource, event structs (WatcherEvent, FileProcessedEvent). Parsed session types are aliases to agent.ParsedSession etc. | agent/driver.go | AgentDriver interface (Setup, Teardown, Activate, Deactivate, ProcessAll) + ProcessCallbacks struct | agent/contrail.go | SessionParser interface, ParsedSession , ParsedMessage , MessagePart types and constants | agent/writer.go | WriteParsedSession — renders a ParsedSession as clean Markdown and writes to disk | agent/logger.go | Logger interface + LogInfof /LogWarningf /LogErrorf helpers | agent/format.go | FormatTimestamp , FormatISO8601Timestamp , SanitizeFilename — pure utility functions | agent/vscode/ | VS Code Copilot AgentDriver — manages fsnotify watching, batch processing with HealContrailNames , JSONL parser that materializes the event log (kind:0/1/2 patches with array splice semantics) and uses toolCallRounds as the authoritative text source | agent/claudecode/ | Claude Code AgentDriver — manages hook lifecycle and signal watching, JSONL transcript parser, project scanner (~/.claude/projects/ ), Stop hook installer, periodic hook enforcer | agent/cursor/ | Cursor AgentDriver — watches per-workspace + global state.vscdb via fsnotify (2 s debounce), SQLite parser that reads composer list from per-workspace ItemTable and conversation data (composerData:* + bubbleId:* keys) from global cursorDiskKV , workspace scanner | | File | Responsibility | |---|---| useProjects.ts | Centralized state hook — loads projects, listens for watcher events, auto-processes on file changes, persists selection to localStorage | ProjectList.tsx | Sidebar list with inline rename, status dots (green = watching), context menu (rename, pause, process, remove) | AddProjectDialog.tsx | Agent-aware project setup — browse auto-detected VS Code, Claude Code, and Cursor workspaces, configure name + output dir | ProjectDetail.tsx | Shows watch/output paths, status badge, manual process button | - Add a project — The app scans the VS Code workspace storage directory for directories containing a chatSessions/ subfolder (~/Library/Application Support/Code/User/workspaceStorage/ on macOS,%APPDATA%/Code/User/workspaceStorage/ on Windows,~/.config/Code/User/workspaceStorage/ on Linux). It readsworkspace.json to resolve the project name. - Watch — fsnotify monitors thechatSessions/ directory. Any.jsonl file create or modify triggers incremental processing — only files modified after the project'slastProcessedAt timestamp are processed. When a project is first added,lastProcessedAt is set to the current time so existing files aren't auto-processed; use "Process All Now" for the initial backlog. When a.jsonl file is deleted, the corresponding Markdown file is flagged with a deletion banner. - Parse — Each {uuid}.jsonl is a v3 event log. The parser materializes the final session state by replaying events:kind:0 — initial state snapshotkind:1 — scalar patch at a key path (e.g.,["customTitle"] )kind:2 — array patch at a key path with optional splice indexi (e.g.,["requests", 0, "response"] ) Once materialized, the parser uses a dual strategy. When result.metadata.toolCallRounds is available (completed requests), it serves as the authoritative text source — the streaming protocol can lose text fragments during splice operations, buttoolCallRounds preserves the complete narrative. Tool call details, file edits, and thinking blocks are still extracted from the deduplicatedresponse[] , correlated totoolCallRounds entries by position. For in-progress requests withouttoolCallRounds , the parser falls back to walking the deduplicatedresponse[] directly. - Output — A Markdown file is written to the output directory (default: {project}/contrails/ ). The filename uses the session'scustomTitle when available, falling back to{sessionId}.md for untitled sessions. When a title changes, the old file is automatically cleaned up. - Healing — At app startup and during "Process All Now", contrails scans for .md files that still use a session ID as their filename. If the source JSONL now has acustomTitle , the file is re-processed and renamed automatically. - Add a project — The app scans ~/.claude/projects/ for directories containing.jsonl session files. Projects are also discoverable by browsing to any directory on disk. - Hook install and enforcement — When a Claude Code source is added, Contrails installs a Stop hook into {project}/.claude/settings.local.json . The hook writes the session metadata (cwd, transcript path, session ID) to a signal file in~/contrails/hook-signals/ when a session ends. AHookEnforcer runs every 5 seconds while the app is open, verifying the hook is still present and re-installing it if the file was deleted or the hook entry was removed. Hooks are not removed on app close — only when the Claude Code source is explicitly removed from a project. - Signal watch — The SignalWatcher monitors~/contrails/hook-signals/ viafsnotify . When a signal file appears, it matches thecwd to a registered project via theSignalHandler interface (implemented byApp ), parses the Claude Code transcript, and writes the contrail. - Parse — Claude Code transcripts are JSONL files where each line is a message with role (human/assistant) andcontent blocks (text, tool_use, tool_result, thinking). The parser extracts the full conversation including tool calls, reasoning, and file edits. - Output — Same Markdown format and output directory as VS Code. Title is derived from the first user message if not available. - Add a project — The app scans the Cursor workspace storage directory for per-workspace state.vscdb files (~/Library/Application Support/Cursor/User/workspaceStorage/ on macOS,%APPDATA%/Cursor/User/workspaceStorage/ on Windows,~/.config/Cursor/User/workspaceStorage/ on Linux) and resolves the workspace path viaworkspace.json . - Watch — fsnotify monitors both the per-workspace storage directory and the globalglobalStorage/ directory for writes tostate.vscdb or its WAL companion. The global database is always watched because bubble (message) content is written there on every Cursor interaction, regardless of workspace. Cursor writes in rapid bursts, so notifications are debounced by 2 seconds before processing. - Parse — Cursor uses two SQLite databases. The per-workspace state.vscdb stores the composer list in anItemTable under keycomposer.composerData . The globalstate.vscdb (inglobalStorage/ under the Cursor config directory) stores conversation data in acursorDiskKV table. Each composer session has acomposerData: key with metadata and afullConversationHeadersOnly array that provides the authoritative message order. Individual messages are stored asbubbleId:: keys. The parser reads bubbles in header order:type=1 → user message,type=2 → AI message. Rich content (tool calls, thinking blocks, code) is decoded from typed capability fields (capabilityType 15 = tool call, 30 = thinking block). - Output — Same Markdown format and output dire

Genesis Park 편집팀이 AI를 활용하여 작성한 분석입니다. 원문은 출처 링크를 통해 확인할 수 있습니다.

공유

관련 저널 읽기

전체 보기 →