From 8edbfacda55520c054f3578401ed3798493b2e3e Mon Sep 17 00:00:00 2001 From: ryyst Date: Sun, 5 Oct 2025 22:22:42 +0300 Subject: [PATCH 1/3] Add Claude --- CLAUDE.md | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0a44003 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,160 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**kvs-sh** is an interactive CLI shell for the KVS (Key-Value Store) distributed database. Think `mongosh`, `psql`, or `redis-cli` but for KVS. Single Go file implementation (~600 lines) with readline support for command history and auto-completion. + +## KVS Server Reference + +The complete KVS server source code is located at `../kvs/` relative to this repository. Reference it when: +- Understanding API contracts and response formats +- Verifying authentication/authorization behavior +- Checking endpoint paths and HTTP methods +- Understanding data structures (StoredValue, Member, User, Group types) +- Implementing new shell commands for backend features + +Key server locations: +- `../kvs/server/handlers.go` - All API endpoint implementations +- `../kvs/server/routes.go` - Route definitions and HTTP methods +- `../kvs/types/types.go` - Canonical type definitions (StoredValue, etc.) +- `../kvs/auth/` - JWT authentication and permission system details +- `../kvs/README.md` - Complete API documentation with curl examples +- `../kvs/CLAUDE.md` - Server architecture and development guidance + +When adding new commands to kvs-sh, always verify the actual backend implementation in `../kvs/` rather than making assumptions about API behavior. + +## Build & Run + +```bash +# Build +go build -o kvs-shell + +# Run +./kvs-shell + +# Install to system +go install +``` + +## Quick Start with Server + +```bash +# 1. Start KVS server (in ../kvs/) +cd ../kvs && ./kvs config.yaml + +# 2. Run shell +./kvs-shell + +# 3. In shell: +# connect http://localhost:8080 +# auth $KVS_ROOT_TOKEN +# put test/data '{"hello":"world"}' +``` + +**Important:** If auth fails with "token not found", the database has old data. Either: +- Create a new token via the API, or +- Wipe database: `rm -rf ../kvs/data && mkdir ../kvs/data` (restart server to generate root token) + +## Testing + +```bash +go test ./... +``` + +## Architecture + +### Single-File Design +Everything lives in `main.go` - intentionally kept simple. The shell is a stateful REPL that maintains: +- HTTP client connection to KVS backend +- Current JWT auth token +- In-memory profile storage (username, token, server URL combinations) +- Active profile selection + +### Core Components + +**KVSClient (main.go:49)**: Central state container holding: +- `baseURL`: KVS server endpoint +- `currentToken`: JWT for authentication +- `profiles`: In-memory map of saved user profiles (NOT persisted to disk yet) +- `httpClient`: Reusable HTTP client with 30s timeout + +**Command Router (main.go:465)**: Main loop uses switch statement on first word of input, delegates to `handle*` methods + +**Command Parser (main.go:527)**: Custom parser respecting quotes - critical for JSON arguments like `put key '{"foo": "bar"}'` + +**Auto-completion (main.go:503)**: Prefix tree using readline library for tab completion of commands + +### HTTP Communication Pattern +All API calls go through `doRequest()` (main.go:85) which: +1. Serializes body to JSON if present +2. Sets `Authorization: Bearer ` header if token exists +3. Returns `(body, statusCode, error)` tuple +4. Delegates HTTP status interpretation to caller + +### Key Backend API Mappings +- `GET /kv/` → Returns `StoredValue` with UUID, timestamp, data +- `PUT /kv/` → Accepts raw JSON, returns UUID + timestamp +- `DELETE /kv/` → Returns 204 on success +- `GET /members/` → Returns array of cluster `Member` objects +- `GET /api/users/` → Returns `User` object +- `GET /health` → Returns service status map + +### Type Definitions (main.go:18-46) +Backend response structures are mirrored in the client: +- `StoredValue`: The core KV response with UUID, timestamp (int64 unix millis), and `json.RawMessage` data +- `Member`: Cluster member info with ID, address, last_seen timestamp +- `User`: User record with nickname hash, groups array, created/updated timestamps +- `Group`: Group definition (defined but not yet used in commands) + +### Profile System +Profiles are in-memory only (see roadmap for persistence plan). When you `profile use `, it swaps the client's token, userUUID, and baseURL atomically. The `*` marker in profile listings indicates active profile. + +### Color Scheme +Uses fatih/color package with semantic coloring: +- Cyan: labels, headers, structured data keys +- Green: success messages, confirmations +- Yellow: warnings, "not found" states +- Red: errors, invalid usage +- Magenta: banner/title text + +### Command Parsing - Critical Implementation Details + +**parseCommand()** (main.go:527-570): Handles both single (`'`) and double (`"`) quotes intelligently. + +**Key behavior:** +- Quotes are **only stripped** when they delimit an entire argument (start and end) +- Supports both `'` and `"` as quote delimiters +- Tracks which quote type started the argument to handle nesting correctly +- Preserves quotes that appear mid-argument (for unquoted JSON) + +**Examples:** +```bash +put key '{"foo":"bar"}' # ✅ Strips outer ', preserves inner " +put key {"foo":"bar"} # ✅ Works (no spaces, compact JSON) +put key {"foo": "bar"} # ❌ Fails - spaces split into multiple args +put key "{\"foo\":\"bar\"}" # ✅ Works (escaped quotes in double-quoted arg) +``` + +**JSON Error Handling** (main.go:393-425): +- `printJSONError()` provides user-friendly error messages with examples +- Detects error type (invalid chars, incomplete JSON, etc.) and shows context-specific tips +- Shows what input was received to help debug quoting issues + +## Backend Assumptions + +The shell expects the KVS backend to: +1. Accept JWT tokens in `Authorization: Bearer ` header +2. Return timestamps as Unix milliseconds (int64) +3. Use standard REST semantics (404 for not found, 204 for delete, 200/201 for success) +4. Accept raw JSON payloads for PUT operations (not wrapped in metadata) + +## Known Limitations & Roadmap Items + +1. **No persistent profiles** - restart loses all `profile add` data (roadmap #1) +2. **No metadata commands** - backend supports owner/group/permissions but shell doesn't expose yet (roadmap #6) +3. **No group management** - `Group` type defined but no CRUD operations (roadmap #5) +4. **No TLS support** - HTTP only, no certificate handling (roadmap #8) +5. **No tab completion for keys** - only completes command names, not dynamic keys from server (roadmap #10) +6. **user create not implemented** - Listed in help but just shows "not implemented" message From 88a3b9824a5d2ba9a09c876a9342695577930109 Mon Sep 17 00:00:00 2001 From: ryyst Date: Sun, 5 Oct 2025 22:23:10 +0300 Subject: [PATCH 2/3] fix: show friendlier error message for JSON parsing --- main.go | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index e5fe0d0..6da0847 100644 --- a/main.go +++ b/main.go @@ -243,7 +243,7 @@ func (c *KVSClient) handlePut(args []string) { // Parse JSON data var data json.RawMessage if err := json.Unmarshal([]byte(args[1]), &data); err != nil { - fmt.Println(red("Invalid JSON:"), err) + printJSONError(args[1], err) return } @@ -390,6 +390,40 @@ func (c *KVSClient) handleUserGet(args []string) { fmt.Printf(" Updated: %s\n", time.Unix(user.UpdatedAt, 0).Format(time.RFC3339)) } +// printJSONError displays a user-friendly JSON parsing error with helpful hints +func printJSONError(input string, err error) { + fmt.Println(red("❌ Invalid JSON:"), err) + fmt.Println() + + // Show what was received + if len(input) > 100 { + fmt.Println(yellow("Received:"), input[:100]+"...") + } else { + fmt.Println(yellow("Received:"), input) + } + fmt.Println() + + // Provide helpful hints based on common errors + errMsg := err.Error() + + if strings.Contains(errMsg, "looking for beginning of") || + strings.Contains(errMsg, "invalid character") { + fmt.Println(cyan("💡 Tip:"), "JSON with spaces needs quotes:") + fmt.Println(green(" ✓ Correct:"), "put key '{\"hello\":\"world\"}'") + fmt.Println(green(" ✓ Correct:"), "put key '{\"a\":1,\"b\":2}'") + fmt.Println(green(" ✓ Compact:"), "put key {\"a\":1,\"b\":2} (no spaces)") + fmt.Println(red(" ✗ Wrong: "), "put key {\"a\": 1, \"b\": 2} (spaces without quotes)") + } else if strings.Contains(errMsg, "unexpected end of JSON") { + fmt.Println(cyan("💡 Tip:"), "JSON appears incomplete or cut off") + fmt.Println(green(" Check:"), "Are all brackets/braces matched? {}, []") + fmt.Println(green(" Check:"), "Did spaces split your JSON into multiple arguments?") + } else { + fmt.Println(cyan("💡 Tip:"), "Wrap JSON in quotes for complex values:") + fmt.Println(green(" Single quotes:"), "put key '{\"your\":\"json\"}'") + fmt.Println(green(" Double quotes:"), "put key \"{\\\"your\\\":\\\"json\\\"}\"") + } +} + func (c *KVSClient) handleHelp(args []string) { help := ` KVS Interactive Shell - Available Commands: From 999deccb74a9ca81d1f5757d4a3830056e433917 Mon Sep 17 00:00:00 2001 From: ryyst Date: Sun, 5 Oct 2025 22:23:31 +0300 Subject: [PATCH 3/3] fix: JSON quote parsing --- main.go | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index 6da0847..77381da 100644 --- a/main.go +++ b/main.go @@ -561,23 +561,39 @@ var completer = readline.NewPrefixCompleter( func parseCommand(line string) []string { var parts []string var current strings.Builder + var quoteChar byte // Track which quote char started this argument (' or ") inQuotes := false for i := 0; i < len(line); i++ { ch := line[i] - switch ch { - case '"': - inQuotes = !inQuotes - case ' ', '\t': - if inQuotes { - current.WriteByte(ch) - } else if current.Len() > 0 { + + // Check if this is a quote character at the start of a new argument + if (ch == '"' || ch == '\'') && current.Len() == 0 && !inQuotes { + // Starting a quoted argument - remember the quote type + quoteChar = ch + inQuotes = true + continue // Don't add the quote itself + } + + // Check if this is the closing quote + if inQuotes && ch == quoteChar { + // End quoted argument + inQuotes = false + quoteChar = 0 + continue // Don't add the quote itself + } + + // Handle spaces/tabs + if (ch == ' ' || ch == '\t') && !inQuotes { + if current.Len() > 0 { parts = append(parts, current.String()) current.Reset() } - default: - current.WriteByte(ch) + continue } + + // Add all other characters (including quotes inside unquoted args) + current.WriteByte(ch) } if current.Len() > 0 {