CLAUDE.md
md
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**hacknight** is a demonstration application built for the HackNight event by GitHub. It's a simple web application built using the Skykit framework (from The Skyscape project).
## Architecture
This is a simple Go web application using the **Skykit framework** pattern:
- **Framework:** `theskyscape.com/repo/skykit` - External dependency providing MVC framework, database connectivity, and server infrastructure
- **Pattern:** MVC architecture with embedded views (HTMX/HATEOAS)
- **Frontend:** HTMX + Tailwind CSS + DaisyUI (dark theme)
- **Database:** LibSQL via Skykit's `ConnectDB()` helper
### Project Structure
```
hacknight/
├── main.go              # Application entry point, embeds views, configures Skykit
├── controllers/         # HTTP request handlers
│   └── home.go         # Home controller with homepage route
├── models/             # Data models and database connection
│   └── database.go     # Global DB connection via Skykit
├── internal/           # Internal packages
│   └── friendli/       # Friendli.ai API client library
│       ├── client.go   # Core HTTP client with authentication
│       ├── chat.go     # Chat completions service
│       ├── stream.go   # Streaming response handler
│       ├── types.go    # Request/response types
│       └── errors.go   # Error types and handling
├── views/              # HTML templates (embedded in binary)
│   └── homepage.html   # Landing page
├── go.mod              # Module dependencies
└── go.sum              # Dependency checksums
```
### Skykit Framework Pattern
This application follows the Skykit framework conventions:
**Application Entry Point:**
```go
// main.go
//go:embed all:views
var views embed.FS
func main() {
    skykit.Serve(views,
        skykit.WithValue("hostPrefix", os.Getenv("HOST_PREFIX")),
        skykit.WithController(controllers.Home()),
    )
}
```
**Controller Pattern:**
```go
// controllers/home.go
func Home() (string, skykit.Handler) {
    return "home", &HomeController{}
}
type HomeController struct {
    skykit.Controller  // Embed base controller
}
func (c *HomeController) Setup(app *skykit.Application) {
    c.Controller.Setup(app)
    http.Handle("/", c.Serve("homepage.html", nil))
}
func (c HomeController) Handle(r *http.Request) skykit.Handler {
    c.Request = r
    return &c
}
```
**Key Conventions:**
- Controllers have factory functions returning `(name string, handler skykit.Handler)`
- Embed `skykit.Controller` base struct
- Implement `Setup(app *skykit.Application)` for route registration
- Implement `Handle(r *http.Request) skykit.Handler` with value receiver
- Views are embedded using `//go:embed all:views` and passed to `skykit.Serve()`
### Friendli.ai Integration
The project includes an internal library for interacting with **Friendli.ai** (https://friendli.ai), an AI infrastructure platform for deploying and running LLMs. The library is located at `internal/friendli/` and provides a Go client for Friendli.ai's serverless API.
**Key Features:**
- Chat completions API (conversational AI)
- Streaming and non-streaming support
- OpenAI-compatible API structure
- Support for 468,834+ open-source models from Hugging Face
- Tool/function calling capabilities
**Basic Usage (Non-Streaming):**
```go
import (
    "hacknight/internal/friendli"
    "hacknight/models"
)
// Get API key from database
apiKey, err := models.GetFriendliAPIKey()
if err != nil || apiKey == "" {
    // API key not configured
}
// Initialize client
client, err := friendli.NewClient(apiKey)
if err != nil {
    // handle error
}
// Create a chat completion request
req := friendli.NewChatCompletionRequest(
    "meta-llama-3.1-8b-instruct",
    []friendli.Message{
        friendli.NewSystemMessage("You are a helpful assistant"),
        friendli.NewUserMessage("Hello!"),
    },
).WithTemperature(0.7).WithMaxTokens(200)
// Get response
resp, err := client.Chat.CreateCompletion(context.Background(), req)
if err != nil {
    // handle error
}
// Access the response
message := resp.Choices[0].Message.Content
```
**Streaming Usage:**
```go
// Create streaming request
req := friendli.NewChatCompletionRequest(
    "meta-llama-3.1-8b-instruct",
    []friendli.Message{
        friendli.NewUserMessage("Tell me a story"),
    },
)
// Get streaming response
stream, err := client.Chat.CreateCompletionStream(context.Background(), req)
if err != nil {
    // handle error
}
defer stream.Close()
// Read chunks
for {
    chunk, err := stream.Recv()
    if err == io.EOF {
        break
    }
    if err != nil {
        // handle error
    }
    // Process chunk
    if len(chunk.Choices) > 0 {
        content := chunk.Choices[0].Delta.Content
        fmt.Print(content)
    }
}
```
**Helper Methods:**
```go
// Collect entire stream into a string
content, err := stream.CollectContent()
// Or collect all chunks
chunks, err := stream.CollectAll()
// Stream to channels (useful for HTMX SSE)
chunkCh := make(chan *friendli.ChatCompletionChunk, 10)
errCh := make(chan error, 1)
go stream.StreamToChannel(chunkCh, errCh)
```
**Using in Controllers:**
```go
// Get API key from database in handler (allows dynamic updates)
func (c *ProjectsController) handleAIFeature(w http.ResponseWriter, r *http.Request) {
    // Get API key from database
    apiKey, err := models.GetFriendliAPIKey()
    if err != nil || apiKey == "" {
        c.Render(w, r, "error-message.html", "API key not configured")
        return
    }
    // Initialize Friendli client
    client, err := friendli.NewClient(apiKey)
    if err != nil {
        c.Render(w, r, "error-message.html", fmt.Sprintf("Failed to create AI client: %v", err))
        return
    }
    // Use client for streaming or non-streaming requests
    // See actual implementation in controllers/projects.go:
    // - handleAIGenerateTasks() for streaming task generation (line ~260)
    // - handleAIProjectSummary() for streaming project analysis (line ~370)
}
```
**Note:** The application now manages API keys through the database Settings model.
See `models/settings.go` and `controllers/projects.go` for the complete implementation.
**Available Models:**
- Llama 3.1 (8B, 70B, 405B)
- Llama 3.2 Vision (11B, 90B)
- Mistral 7B / 7B Instruct
- CodeLlama variants
- 468,834+ other Hugging Face models
**API Reference:**
- `friendli.NewClient(apiKey)` - Create client with API key
- `client.Chat.CreateCompletion(ctx, req)` - Non-streaming chat
- `client.Chat.CreateCompletionStream(ctx, req)` - Streaming chat
- `stream.Recv()` - Read next chunk from stream
- `stream.Close()` - Close stream and cleanup
- `stream.CollectContent()` - Read entire stream as string
- `stream.StreamToChannel(chunkCh, errCh)` - Stream to channels
**Error Handling:**
```go
if err != nil {
    if apiErr, ok := err.(*friendli.APIError); ok {
        if apiErr.IsRateLimitError() {
            // handle rate limit
        } else if apiErr.IsAuthenticationError() {
            // handle auth error
        } else if apiErr.IsValidationError() {
            // handle validation error
        }
    }
}
```
## Development Workflow
### Running the Application
```bash
# Set optional environment variables
export HOST_PREFIX=""        # Optional: Host prefix for routing
# Run the application
go run .
# Build binary
go build -o app .
```
**Note:** Skykit handles port configuration and other server settings internally.
### Dependencies
Install/update dependencies:
```bash
go mod download
go mod tidy
```
The application depends on `theskyscape.com/repo/skykit` which provides:
- MVC framework and routing
- Database connectivity (LibSQL)
- Server infrastructure and configuration
- Template rendering
### Adding New Features
**Adding a Controller:**
1. Create new file in `controllers/`
2. Follow the Skykit controller pattern (factory function, embed `skykit.Controller`)
3. Implement `Setup()` and `Handle()` methods
4. Register in `main.go` using `skykit.WithController()`
**Adding a View:**
1. Create HTML file in `views/` directory
2. Views are automatically embedded via `//go:embed all:views`
3. Reference in controller using `c.Serve("filename.html", data)`
4. Use HTMX for dynamic features
**Database Access:**
```go
// Access the global DB connection
import "hacknight/models"
// Use models.DB in your controllers
result, err := models.DB.Query("SELECT * FROM table")
```
### Frontend Stack
- **HTMX 2.0.8** - Dynamic HTML without JavaScript
- **Tailwind CSS 4** - Utility-first styling
- **DaisyUI 5** - Component library (using "dark" theme)
Views should use `data-theme="dark"` on the `<html>` tag for consistent theming.
## Environment Variables
- `HOST_PREFIX` - Optional host prefix for routing (used in multi-app deployments)
## Configuration
### Friendli API Key (Database-Stored)
The Friendli API key is now configured through the web UI and stored in the database:
1. Navigate to any project page
2. If not configured, click "Configure API Key" button in navbar (yellow warning button)
3. Enter your API key in the modal dialog
4. Key is saved to the `settings` table in the database
5. No restart required - changes take effect immediately
**Obtaining an API Key:**
- Get your key from: https://suite.friendli.ai (Settings → Tokens)
- Format: `flp_XXX`
- Free tier includes $5 in credits
**Benefits:**
- No environment variable configuration needed
- Can be changed through UI without server restart
- Persisted in database across restarts
## Module Information
- **Module name:** `hacknight`
- **Go version:** 1.23.2
- **Main dependency:** `theskyscape.com/repo/skykit v0.0.0-20251111223751-bfc3029ae897`
We used Claude for this project with the Skykit for rapid development and clean context files
Skyscape Test
@testing