# Brief Brief is a terminal-based ChatGPT client designed for fast, keyboard-first interaction with AI models. Built in Java using the TUI4J framework (a Java port of Bubble Tea), it provides a CRT-style green terminal aesthetic with full conversation management, slash commands, and integrated tool execution. The application supports OpenAI-compatible APIs including OpenAI, OpenRouter, Ollama, and LMStudio. The core functionality centers on multi-turn conversations with automatic context management and summarization. Brief includes built-in tools for weather forecasts (via Open-Meteo) and location services (via Apple Maps), which the LLM can invoke during conversations. Configuration is stored in `~/.config/brief/config` with support for environment variable overrides, making it suitable for both end-user installation via Homebrew and developer workflows. ## Installation ### Homebrew Installation (macOS) ```bash # Install via Homebrew brew install williamagh/tap/brief # Launch the application brief ``` ### Manual Installation ```bash # Download from GitHub releases (requires Java 25) # https://github.com/WilliamAGH/brief/releases/latest # Clone and build from source git clone https://github.com/WilliamAGH/brief.git cd brief cp .env-example .env # Add your API key to .env make run ``` ## Configuration Brief uses a configuration file at `~/.config/brief/config` with environment variable fallback. The app prompts for your API key on first launch. ### Configuration File Format ```properties # ~/.config/brief/config openai.api_key=sk-your-api-key-here openai.base_url=https://api.openai.com/v1 model=gpt-4o user.name=Your Name config.priority=env # Optional: Apple Maps for location features apple_maps.token=your-apple-maps-token # Summarization settings summary.disabled=false summary.target_tokens=8000 ``` ### Environment Variables ```bash # Required - API key for OpenAI or compatible provider export OPENAI_API_KEY="sk-your-key-here" # Optional - Custom endpoint for alternative providers export OPENAI_BASE_URL="https://api.openai.com/v1" # Optional - Default model selection export LLM_MODEL="gpt-4o" # Optional - Priority: "env" (default) or "config" export BRIEF_CONFIG_PRIORITY="env" # Optional - Apple Maps integration export APPLE_MAPS_TOKEN="your-apple-maps-token" # Display options export BRIEF_ALT_SCREEN=1 # Use alternate screen buffer export BRIEF_MOUSE=select # Mouse mode: select, 1, 0 export BRIEF_SHOW_TOOLS=1 # Show tool call messages export BRIEF_SCROLLBACK=1 # Print to terminal scrollback ``` ### OpenRouter Configuration ```bash export OPENAI_API_KEY="sk-or-your-openrouter-key" export OPENAI_BASE_URL="https://openrouter.ai/api/v1" export LLM_MODEL="anthropic/claude-sonnet-4-20250514" ``` ### Ollama (Local) Configuration ```bash # Requires: ollama serve && ollama pull llama3.2 export OPENAI_API_KEY="ollama" export OPENAI_BASE_URL="http://localhost:11434/v1" export LLM_MODEL="llama3.2" ``` ### LMStudio (Local) Configuration ```bash export OPENAI_API_KEY="lm-studio" export OPENAI_BASE_URL="http://localhost:1234/v1" ``` ## Config Class API The `Config` class manages user preferences and provides resolution between environment variables and the config file. ```java import com.williamcallahan.chatclient.Config; // Create config instance (loads from ~/.config/brief/config) Config config = new Config(); // Resolve settings (respects env vs config priority) String apiKey = config.resolveApiKey(); // OPENAI_API_KEY or openai.api_key String baseUrl = config.resolveBaseUrl(); // OPENAI_BASE_URL or openai.base_url String model = config.resolveModel(); // LLM_MODEL or model String appleMapsToken = config.resolveAppleMapsToken(); // Check if settings exist boolean hasKey = config.hasResolvedApiKey(); boolean hasMaps = config.hasAppleMapsToken(); boolean hasName = config.hasUserName(); // Get/set user preferences (persisted to config file) String userName = config.userName(); config.setUserName("Alice"); config.setModel("gpt-4o-mini"); config.setApiKey("sk-new-key"); config.setBaseUrl("https://api.openai.com/v1"); // Summary settings boolean summaryEnabled = config.isSummaryEnabled(); int targetTokens = config.getSummaryTargetTokens(); config.setSummaryDisabled(false); config.setSummaryTargetTokens(8000); // Check priority mode Config.Priority priority = config.priority(); // ENV or CONFIG ``` ## OpenAiService The `OpenAiService` class provides the shared OpenAI client configuration and model discovery. ```java import com.williamcallahan.chatclient.Config; import com.williamcallahan.chatclient.service.OpenAiService; import com.openai.client.OpenAIClient; import java.util.List; // Initialize with config Config config = new Config(); OpenAiService openAiService = new OpenAiService(config); // Get the underlying OpenAI client OpenAIClient client = openAiService.client(); // Get configured base URL (null for default) String baseUrl = openAiService.baseUrl(); // Discover available models from the API List availableModels = openAiService.modelChoices(); // Returns: ["gpt-4o", "gpt-4o-mini", "gpt-3.5-turbo", ...] // Use the client directly for API calls var response = client.chat().completions().create(params); ``` ## ChatCompletionService The `ChatCompletionService` wraps the OpenAI chat completions API with simplified methods. ```java import com.williamcallahan.chatclient.Config; import com.williamcallahan.chatclient.service.ChatCompletionService; import com.williamcallahan.chatclient.service.OpenAiService; import com.openai.models.chat.completions.*; import java.util.List; // Initialize services Config config = new Config(); OpenAiService openAiService = new OpenAiService(config); ChatCompletionService chatService = new ChatCompletionService(openAiService); // Simple text completion String response = chatService.complete( "Explain quantum computing in one sentence.", "gpt-4o-mini" ); // Returns: "Quantum computing uses quantum bits that can exist in multiple states..." // Full completion with parameters ChatCompletionCreateParams params = ChatCompletionCreateParams.builder() .model("gpt-4o") .temperature(0.7) .messages(List.of( ChatCompletionMessageParam.ofSystem( ChatCompletionSystemMessageParam.builder() .content("You are a helpful assistant.") .build() ), ChatCompletionMessageParam.ofUser( ChatCompletionUserMessageParam.builder() .content("What is the capital of France?") .build() ) )) .build(); ChatCompletion completion = chatService.complete(params); String answer = completion.choices().get(0).message().content().orElse(""); // Returns: "The capital of France is Paris." ``` ## ToolExecutor The `ToolExecutor` handles LLM tool calls, executing registered tools and managing the conversation loop. ```java import com.williamcallahan.chatclient.Config; import com.williamcallahan.chatclient.domain.Conversation; import com.williamcallahan.chatclient.service.*; import com.williamcallahan.chatclient.service.tools.*; import java.util.List; import java.time.OffsetDateTime; import java.time.ZoneOffset; // Set up services Config config = new Config(); OpenAiService openAiService = new OpenAiService(config); ChatCompletionService chatService = new ChatCompletionService(openAiService); // Register tools List tools = List.of( new WeatherForecastTool() // Add PlaceSearchTool and GeocodeAddressTool if Apple Maps is configured ); ToolExecutor executor = new ToolExecutor(chatService, tools); // Create a conversation Conversation conversation = Conversation.builder() .id("conv_001") .createdAt(OffsetDateTime.now(ZoneOffset.UTC)) .updatedAt(OffsetDateTime.now(ZoneOffset.UTC)) .defaultModel("gpt-4o") .build(); // Add a user message asking about weather conversation.addMessage(new ChatMessage( "msg_001", conversation.getId(), 0, Role.USER, ChatMessage.Source.USER_INPUT, "What's the weather like in San Francisco?", OffsetDateTime.now(ZoneOffset.UTC), "gpt-4o", "openai", null, null, null, null, null )); // Get response (executor handles tool calls automatically) String response = executor.respond(conversation, "gpt-4o"); // The executor will: // 1. Send the message to the LLM // 2. If LLM requests get_weather_forecast tool, execute it // 3. Return tool results to LLM // 4. Return final text response System.out.println(response); // Output: "The current weather in San Francisco is 62°F with partly cloudy skies..." ``` ## Tool Interface Custom tools implement the `Tool` interface for LLM function calling. ```java import com.williamcallahan.chatclient.service.tools.Tool; import com.openai.models.FunctionDefinition; import com.openai.models.FunctionParameters; import com.openai.core.JsonValue; import java.util.List; import java.util.Map; public class CustomTool implements Tool { @Override public String name() { return "my_custom_tool"; } @Override public FunctionDefinition definition() { Map props = Map.of( "query", Map.of( "type", "string", "description", "The search query" ), "limit", Map.of( "type", "integer", "description", "Maximum results to return", "default", 10 ) ); return FunctionDefinition.builder() .name(name()) .description("Search for items matching the query") .parameters( FunctionParameters.builder() .putAdditionalProperty("type", JsonValue.from("object")) .putAdditionalProperty("properties", JsonValue.from(props)) .putAdditionalProperty("required", JsonValue.from(List.of("query"))) .putAdditionalProperty("additionalProperties", JsonValue.from(false)) .build() ) .build(); } @Override public Object execute(Map arguments) throws Exception { String query = (String) arguments.get("query"); int limit = arguments.containsKey("limit") ? ((Number) arguments.get("limit")).intValue() : 10; // Perform the search and return results return Map.of( "query", query, "results", List.of("result1", "result2"), "count", 2 ); } } ``` ## WeatherForecastTool Built-in tool for weather forecasts using the Open-Meteo API. ```java import com.williamcallahan.chatclient.service.tools.WeatherForecastTool; import java.util.Map; WeatherForecastTool weatherTool = new WeatherForecastTool(); // Tool name for LLM function calling String name = weatherTool.name(); // "get_weather_forecast" // Execute the tool with arguments Map args = Map.of( "city", "San Francisco", "days", 5, "country_code", "US", "admin1", "California" ); Object result = weatherTool.execute(args); // Returns a Map with structure: // { // "current": { // "location": "San Francisco, United States", // "temp_F": 62.5, // "wind_mph": 12, // "condition": "Partly cloudy" // }, // "forecast": [ // {"date": "2024-01-15", "high_F": 65.0, "low_F": 52.0, "condition": "Clear sky"}, // {"date": "2024-01-16", "high_F": 63.0, "low_F": 50.0, "condition": "Mainly clear"}, // ... // ] // } ``` ## PlaceSearchTool Apple Maps integration for searching places and businesses. ```java import com.williamcallahan.applemaps.AppleMaps; import com.williamcallahan.chatclient.service.tools.PlaceSearchTool; import java.util.Map; // Requires Apple Maps token String token = System.getenv("APPLE_MAPS_TOKEN"); AppleMaps client = new AppleMaps(token); PlaceSearchTool searchTool = new PlaceSearchTool(client); // Search for places Map args = Map.of( "query", "coffee shops in San Francisco", "country_code", "US" ); Object result = searchTool.execute(args); // Returns: // { // "query": "coffee shops in San Francisco", // "count": 10, // "results": [ // { // "name": "Blue Bottle Coffee", // "address": "66 Mint St, San Francisco, CA 94103", // "category": "Coffee Shop", // "latitude": 37.7821, // "longitude": -122.4062, // "place_id": "..." // }, // ... // ] // } ``` ## GeocodeAddressTool Apple Maps integration for converting addresses to coordinates. ```java import com.williamcallahan.applemaps.AppleMaps; import com.williamcallahan.chatclient.service.tools.GeocodeAddressTool; import java.util.Map; String token = System.getenv("APPLE_MAPS_TOKEN"); AppleMaps client = new AppleMaps(token); GeocodeAddressTool geocodeTool = new GeocodeAddressTool(client); // Geocode an address Map args = Map.of( "address", "880 Harrison St, San Francisco, CA 94107", "country_code", "US" ); Object result = geocodeTool.execute(args); // Returns: // { // "query": "880 Harrison St, San Francisco, CA 94107", // "count": 1, // "results": [ // { // "formatted_address": "880 Harrison St, San Francisco, CA 94107", // "address_components": { // "street": "880 Harrison St", // "city": "San Francisco", // "state": "CA", // "postal_code": "94107" // }, // "latitude": 37.7784, // "longitude": -122.3982, // "place_id": "..." // } // ] // } ``` ## Conversation Domain Model The `Conversation` and `ChatMessage` classes model the chat state. ```java import com.williamcallahan.chatclient.domain.*; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.UUID; // Create a new conversation Conversation conversation = Conversation.builder() .id("c_" + UUID.randomUUID()) .createdAt(OffsetDateTime.now(ZoneOffset.UTC)) .updatedAt(OffsetDateTime.now(ZoneOffset.UTC)) .provider(Conversation.Provider.OPENAI) .apiFamily(Conversation.ApiFamily.CHAT_COMPLETIONS) .defaultModel("gpt-4o") .build(); // Add messages to the conversation ChatMessage userMessage = new ChatMessage( "msg_001", // id conversation.getId(), // conversationId 0, // index Role.USER, // role ChatMessage.Source.USER_INPUT, // source "Hello, how are you?", // content OffsetDateTime.now(ZoneOffset.UTC), // createdAt "gpt-4o", // model "openai", // provider null, // providerMessageId null, // toolCalls null, // toolCallId null, // usage null // error ); conversation.addMessage(userMessage); // Access conversation state String model = conversation.getDefaultModel(); int messageCount = conversation.getMessages().size(); ChatMessage lastMessage = conversation.getMessages().get(messageCount - 1); // Message sources: // - USER_INPUT: User typed message // - LLM_OUTPUT: Assistant response // - SYSTEM: System prompts // - TOOL_OUTPUT: Tool execution results // - INTERNAL: Hidden context messages // - LOCAL: Local display only (not sent to LLM) ``` ## Slash Commands Brief supports slash commands for quick actions. These are implemented via the `SlashCommand` interface. ```java // Available slash commands in Brief: // /weather [location] - Get weather forecast // /locate [query] - Search places with Apple Maps // /model - Switch AI model // /config - Edit settings // /new - Start new conversation // /clear - Clear conversation // /about - Show app info // /quit - Exit application ``` ### Keyboard Shortcuts ``` Enter Submit message Ctrl+J Insert newline (multi-line input) Shift+Enter Insert newline / Open command palette Esc Quit / Close overlay Shift+Up/Down Scroll history PgUp/PgDown Scroll history (5 lines) Home/End Jump to top/bottom of history ``` ## SummaryService The `SummaryService` handles automatic summarization of long pastes and context window management. ```java import com.williamcallahan.chatclient.Config; import com.williamcallahan.chatclient.domain.Conversation; import com.williamcallahan.chatclient.service.*; Config config = new Config(); OpenAiService openAiService = new OpenAiService(config); ChatCompletionService chatService = new ChatCompletionService(openAiService); SummaryService summaryService = new SummaryService(chatService, config); // Process a paste (summarizes if over ~8k tokens) String longText = "...very long pasted content..."; SummaryService.PasteSummary result = summaryService.processPaste(longText, 1); String displayText = result.displayText(); // "[Pasted text 1 (summarized)]" String actualText = result.actualText(); // Summarized content sent to LLM boolean wasSummarized = result.wasSummarized(); boolean wasTruncated = result.wasTruncated(); // Trim conversation if context window is exceeded SummaryService.TrimResult trimResult = summaryService.trimIfNeeded( conversation, "gpt-4o", 4000 // Reserve tokens for response ); // Check settings boolean enabled = summaryService.isSummaryEnabled(); int targetTokens = summaryService.getTargetTokens(); ``` ## Build Commands Build and development commands using Make. ```bash # Build and run the application make run # Build only (creates ./build/install/brief/bin/brief) make build # Clean build artifacts make clean # Build distribution zip make dist # Test a release build make release-test V=0.1.0 # Create and push a release tag make release V=0.1.0 # Run with local TUI4J development build make run-local-tui ``` ## Summary Brief provides a complete terminal-based ChatGPT client with a focus on keyboard-first interaction and developer ergonomics. The primary use cases include interactive AI conversations in the terminal, quick weather lookups using natural language, location and business searches via Apple Maps integration, and seamless switching between multiple AI providers (OpenAI, OpenRouter, Ollama, LMStudio). The modular tool system allows extending the assistant's capabilities with custom function implementations. For integration, Brief can be embedded as a library by using the core services (`Config`, `OpenAiService`, `ChatCompletionService`, `ToolExecutor`) without the TUI layer. The conversation model supports serialization via Jackson annotations, enabling persistence and restoration of chat history. Alternative providers are supported through the OpenAI-compatible API pattern, making Brief suitable for both cloud and local LLM deployments.