Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 65 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

No context switching. No new window. The answer streams in below your prompt and you keep working.
Just press shift with enter to trigger claude response.

## Install

```sh
Expand All @@ -29,44 +30,44 @@ Or build from source:

```sh
git clone https://github.com/CCALITA/inline-cli.git
cd inline-clig
make build
# Binary is at ./build/inline-cli
cd inline-cli
make build # → ./build/inline-cli
cp ./build/inline-cli ~/.local/bin/inline-cli # or anywhere in your PATH
```

Then add to your shell config:
Then set up your shell and backend:

```sh
# zsh (~/.zshrc)
eval "$(inline-cli init zsh)"

# bash (~/.bashrc)
eval "$(inline-cli init bash)"
```
# 1. Add shell integration (pick one)
echo 'eval "$(inline-cli init zsh)"' >> ~/.zshrc # zsh
echo 'eval "$(inline-cli init bash)"' >> ~/.bashrc # bash

Set your API key:
# 2. Choose a backend
inline-cli setup

```sh
export ANTHROPIC_API_KEY=sk-ant-...
# 3. Restart your shell
exec $SHELL
```

Restart your shell. Done.

## How it works

```
┌─────────────────┐ Unix socket ┌──────────────┐ HTTPS/SSE ┌───────────┐
│ shell widget │ ──────────────────> │ daemon │ ─────────────────>│ Claude API│
│ captures buffer │ <── NDJSON stream── │ per-dir │ <── streaming ── │ │
│ renders output │ │ sessions │ │ │
└─────────────────┘ └──────────────┘ └───────────┘
│ renders output │ │ sessions │ └───────────┘
└─────────────────┘ │ │ or
│ │ ───> claude CLI
│ │ ───> gemini CLI
│ │ ───> opencode CLI
└──────────────┘
```

**Three pieces:**

1. **Shell integration** — A zsh ZLE widget or bash readline binding captures your command-line buffer on <kbd>Shift</kbd>+<kbd>Enter</kbd> (or <kbd>Ctrl</kbd>+<kbd>J</kbd>) and pipes it to the CLI.
2. **Background daemon** — A long-lived Go process manages conversation sessions over a Unix domain socket. Sub-millisecond IPC. No cold start per query.
3. **Pluggable backend** — Talks to Claude via direct API, the `claude` CLI, or ACP. Streams responses as markdown to your terminal.
3. **Pluggable backend** — Talks to Claude via direct API, the `claude` CLI, Gemini CLI, or OpenCode CLI. Streams responses as markdown to your terminal.

### Directory-scoped sessions

Expand Down Expand Up @@ -104,6 +105,12 @@ shift + enter
# Check what's running
inline-cli status

# Manage backends
inline-cli setup # Interactive first-time setup
inline-cli backend list # List backends with install status
inline-cli backend show # Show current backend
inline-cli backend set gemini # Switch backend (auto-restarts daemon)

# Manage the daemon
inline-cli daemon start
inline-cli daemon stop
Expand All @@ -114,16 +121,18 @@ inline-cli daemon stop
Config lives at `~/.inline-cli/config.toml`. All fields are optional — defaults are sensible.

```toml
# Backend: "api" (default), "cli", or "acp"
# Backend: "api" (default), "claude", "gemini", "opencode"
backend = "api"

# API backend settings
api_key = "sk-ant-..." # or set ANTHROPIC_API_KEY env var
model = "claude-sonnet-4-20250514" # default model
api_base_url = "" # custom API endpoint (proxy, gateway)

# CLI backend settings (uses `claude` command)
cli_path = "" # auto-detected from PATH if empty
# CLI backend paths (auto-detected from PATH if empty)
cli_path = ""
gemini_path = ""
opencode_path = ""

# Session settings
max_session_idle_minutes = 30
Expand All @@ -132,34 +141,36 @@ max_messages = 50

### Backends

| Backend | Config | What it does |
| ------------------- | ------------------------- | ---------------------------------------------------------------------------------- |
| **`api`** (default) | Needs `ANTHROPIC_API_KEY` | Direct HTTPS to Anthropic Messages API with SSE streaming |
| **`cli`** | Needs `claude` in PATH | Execs `claude -p <prompt>` and streams stdout. Uses your existing claude CLI auth. |
| **`acp`** | — | Agent Communication Protocol (planned) |
| Backend | Config | What it does |
| ---------------------- | ------------------------- | ---------------------------------------------------------------------------------- |
| **`api`** (default) | Needs `ANTHROPIC_API_KEY` | Direct HTTPS to Anthropic Messages API with SSE streaming |
| **`claude`** | Needs `claude` in PATH | Execs `claude -p <prompt>` and streams stdout. Uses your existing Claude CLI auth. |
| **`gemini`** | Needs `gemini` in PATH | Execs `gemini -p <prompt> -o text` and streams stdout. Uses Gemini CLI auth. |
| **`opencode`** | Needs `opencode` in PATH | Execs `opencode run --format json` and parses the JSON event stream. |

Switch backends via config file or env var:
Switch backends via the CLI or config file:

```sh
# Use claude CLI instead of direct API
export INLINE_CLI_BACKEND=cli
# Interactive setup (detects installed CLIs)
inline-cli setup

# Or in config.toml
backend = "cli"
# Direct switch
inline-cli backend set gemini
```

### Environment variables

| Variable | Purpose |
| ------------------------- | -------------------------------------- |
| `ANTHROPIC_API_KEY` | API key (required for `api` backend) |
| `INLINE_CLI_BACKEND` | Backend selection: `api`, `cli`, `acp` |
| `INLINE_CLI_MODEL` | Override model |
| `INLINE_CLI_SOCKET` | Custom socket path |
| `INLINE_CLI_API_BASE_URL` | Custom API endpoint |
| `INLINE_CLI_CLI_PATH` | Path to `claude` binary |
| `INLINE_CLI_MAX_IDLE` | Session idle timeout (minutes) |
| `INLINE_CLI_NO_PROMPT` | Set to `1` to disable prompt indicator |
| Variable | Purpose |
| --------------------------- | ------------------------------------------------ |
| `ANTHROPIC_API_KEY` | API key (required for `api` backend) |
| `INLINE_CLI_MODEL` | Override model |
| `INLINE_CLI_SOCKET` | Custom socket path |
| `INLINE_CLI_API_BASE_URL` | Custom API endpoint |
| `INLINE_CLI_CLI_PATH` | Path to `claude` binary |
| `INLINE_CLI_GEMINI_PATH` | Path to `gemini` binary |
| `INLINE_CLI_OPENCODE_PATH` | Path to `opencode` binary |
| `INLINE_CLI_MAX_IDLE` | Session idle timeout (minutes) |
| `INLINE_CLI_NO_PROMPT` | Set to `1` to disable prompt indicator |

Precedence: env vars > config file > defaults.

Expand Down Expand Up @@ -230,13 +241,23 @@ make release

Produces `{linux,darwin}_{amd64,arm64}` tarballs in `./build/` with SHA-256 checksums.

### Make targets

| Target | What it does |
| -------------- | --------------------------------------------------- |
| `make build` | Build binary → `./build/inline-cli` |
| `make test` | Run all tests with `-race -cover` |
| `make lint` | Run `go vet` |
| `make clean` | Remove `./build/` |
| `make release` | Cross-compile + tarball + SHA-256 checksums |

## Architecture

```
inline-cli/
├── cmd/inline-cli/ # CLI entry point + embedded shell scripts
├── internal/
│ ├── backend/ # Backend interface + implementations (API, CLI, ACP)
│ ├── backend/ # Backend interface + implementations (API, Claude CLI, Gemini CLI, OpenCode CLI)
│ ├── claude/ # Claude API client + SSE streaming parser
│ ├── config/ # Config loading (TOML + env vars)
│ ├── daemon/ # Daemon lifecycle + Unix socket server
Expand All @@ -251,10 +272,10 @@ Single Go binary, ~14MB. No runtime dependencies.

## Requirements

- **Go 1.22+** (build only)
- **Go 1.26+** (build only)
- **macOS** or **Linux**
- **zsh** or **bash**
- One of: [Anthropic API key](https://console.anthropic.com/), `claude` CLI installed, or ACP endpoint
- One of: [Anthropic API key](https://console.anthropic.com/), `claude` CLI, `gemini` CLI, or `opencode` CLI

## Uninstall

Expand Down
167 changes: 167 additions & 0 deletions cmd/inline-cli/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package main

import (
"fmt"
"os"
"os/exec"
"strings"

"github.com/spf13/cobra"

"github.com/CCALITA/inline-cli/internal/config"
"github.com/CCALITA/inline-cli/internal/daemon"
)

// backendInfo describes a supported backend.
type backendInfo struct {
Name string // config value
Desc string // human-readable description
Binary string // CLI binary name to check (empty for API-based)
}

var backends = []backendInfo{
{Name: "api", Desc: "Anthropic API (requires API key)", Binary: ""},
{Name: "claude", Desc: "Claude CLI", Binary: "claude"},
{Name: "gemini", Desc: "Gemini CLI", Binary: "gemini"},
{Name: "opencode", Desc: "OpenCode CLI", Binary: "opencode"},
}

func (b backendInfo) isInstalled() bool {
if b.Binary == "" {
return true
}
_, err := exec.LookPath(b.Binary)
return err == nil
}

func (b backendInfo) installStatus() string {
if b.Binary == "" {
return ""
}
if b.isInstalled() {
return " \033[32m✓ installed\033[0m"
}
return " \033[31m✗ not found\033[0m"
}

func findBackend(name string) (backendInfo, bool) {
for _, b := range backends {
if b.Name == name {
return b, true
}
}
return backendInfo{}, false
}

func validBackendNames() []string {
names := make([]string, len(backends))
for i, b := range backends {
names[i] = b.Name
}
return names
}

func restartDaemonIfRunning() {
cfg, err := config.Load()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not reload config: %v\n", err)
return
}
d := daemon.NewDaemon(cfg.PIDFile, cfg.SocketPath)
if d.IsRunning() {
if err := d.Stop(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to stop daemon: %v\n", err)
} else {
fmt.Println("Daemon stopped. Next query will use the new backend.")
}
}
}

func newBackendCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "backend",
Short: "Manage backends",
}

cmd.AddCommand(
newBackendListCmd(),
newBackendShowCmd(),
newBackendSetCmd(),
)

return cmd
}

func newBackendListCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List available backends",
RunE: runBackendList,
}
}

func newBackendShowCmd() *cobra.Command {
return &cobra.Command{
Use: "show",
Short: "Show the current backend",
RunE: runBackendShow,
}
}

func newBackendSetCmd() *cobra.Command {
return &cobra.Command{
Use: "set <backend>",
Short: "Switch to a different backend",
Args: cobra.ExactArgs(1),
ValidArgs: validBackendNames(),
RunE: runBackendSet,
}
}

func runBackendList(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return err
}

active := cfg.BackendName()

for _, b := range backends {
marker := " "
activeSuffix := ""
if b.Name == active {
marker = "* "
activeSuffix = " (active)"
}
fmt.Printf("%s%-10s — %s%s%s\n", marker, b.Name, b.Desc, b.installStatus(), activeSuffix)
}

return nil
}

func runBackendShow(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return err
}

fmt.Println(cfg.BackendName())
return nil
}

func runBackendSet(cmd *cobra.Command, args []string) error {
name := args[0]

if _, ok := findBackend(name); !ok {
return fmt.Errorf("unknown backend %q (supported: %s)", name, strings.Join(validBackendNames(), ", "))
}

if err := config.SaveBackend(name); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}

fmt.Printf("Backend set to %q\n", name)
restartDaemonIfRunning()

return nil
}
2 changes: 2 additions & 0 deletions cmd/inline-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func main() {
newStatusCmd(),
newStopSessionCmd(),
newInitCmd(),
newBackendCmd(),
newSetupCmd(),
)

if err := rootCmd.Execute(); err != nil {
Expand Down
Loading