ashlet

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 11, 2026 License: MIT Imports: 4 Imported by: 0

README

ashlet

GitHub Release License GitHub stars

Live Demo

AI-powered shell auto-completion for Zsh. Suggestions appear as you type, powered by any OpenAI-compatible API.

ashlet runs a lightweight daemon (ashletd) that gathers context from your shell — working directory, command history, git status, project manifests — and sends it to an inference API. Candidates are streamed back and displayed inline below your prompt.

Zsh Shell <--> ashlet.zsh <--> (Unix socket) <--> ashletd <--> API provider

Table of Contents

Quickstart

# 1) Install
brew install paranoid-af/tap/ashlet

# 2) Enable in Zsh
echo 'source $(brew --prefix)/share/ashlet/ashlet.zsh' >> ~/.zshrc
exec zsh

# 3) Start the daemon
brew services start ashlet

# 4) Configure an API key (recommended)
ashlet   # creates ~/.config/ashlet/config.json and ~/.config/ashlet/prompt.md

# If you are running ./ashletd manually (not via brew services), you can also:
# export ASHLET_GENERATION_API_KEY="your-openrouter-key"

Type a command, wait for a suggestion, then press Tab to accept.

Try it:

git st

Requirements

  • Zsh 5.3+
  • Go 1.25+ (to build from source)
  • socat and jq (runtime)

Build from Source

# Build the daemon
make build

# Start it
./ashletd

# In your .zshrc
source /path/to/ashlet/shell/ashlet.zsh

Set an API key to enable completions:

export ASHLET_GENERATION_API_KEY="your-openrouter-key"

Or run ashlet in your shell to launch the configuration TUI, which creates ~/.config/ashlet/config.json.

How It Works

You type → Zsh hook fires → JSON request over Unix socket → ashletd gathers context →
API call → candidates parsed → displayed below prompt → Tab to accept

The daemon gathers rich context for each request:

  • Shell history — recent commands + semantically relevant ones (via optional embeddings)
  • Directory listing — files, detected package manager
  • Git info — repo root, staged files, recent commits, manifests (package.json, Makefile, etc.)
  • Cursor position — understands partial tokens

Privacy

ashlet sends context to your configured API provider to generate completions.

  • What gets sent:
    • The line you are typing (and cursor position)
    • Local context like directory info and (optionally) git metadata
    • History context:
      • With embeddings enabled and generation.no_raw_history: true (default), ashlet sends only semantically relevant history commands (not a raw recent-history window).
      • When embeddings are disabled, it may fall back to sending a recent commands window.
  • History redaction: In shell history only, environment variable references ($SECRET, ${API_KEY}) and assignments (TOKEN=abc) are redacted before being sent. Safe variables like $HOME, $PATH, and $PWD are preserved.
  • IMPORTANT: Your current input is not redacted. If you are typing sensitive content, press Escape to enable PRIVATE MODE until the next prompt (Enter / Ctrl+C). You will see ㊙ PRIVATE MODE ACTIVE - no input sent to AI below your prompt. A screenshot of how Private Mode enabled looks like
  • Local-only IPC: The shell client and daemon communicate over a Unix domain socket. Nothing is sent over the network except API calls to your configured provider.
  • Telemetry: When telemetry.openrouter is true (default), OpenRouter attribution headers are sent. Set it to false to disable.

Keybindings

Key Action
Tab Accept the displayed suggestion
Shift+Tab Fall through to default Zsh completion
Shift+Left / Shift+Right Browse between candidates
Escape Enable PRIVATE MODE (stop sending input) until next prompt

Troubleshooting

  • No suggestions appear
    • Ensure the daemon is running: brew services list (or start it with brew services start ashlet)
    • If you built from source, run ./ashletd and watch logs for errors
  • Tab doesn’t accept the suggestion
    • Make sure ashlet.zsh is sourced in your ~/.zshrc, then restart your shell
    • If Tab is bound by another plugin, you can still access regular Zsh completion via Shift+Tab
  • Missing dependencies (jq / socat)
    • Install them: brew install jq socat
  • Socket / connection problems
    • If you override the socket path, confirm ASHLET_SOCKET points to the same location for both shell + daemon
  • API auth / request failures
    • Set ASHLET_GENERATION_API_KEY (or run ashlet to create/edit ~/.config/ashlet/config.json)
  • Accidentally sending something sensitive
    • Use Escape to enable PRIVATE MODE before typing secrets, prventing sending requests (current input is not redacted in requests)

Configuration

After enabling it in your shell, you can run ashlet to generate your configuration files.

This includes:

  • config.json: General configuration, such as API base URL, your API key, and the model name.
  • prompt.md: Your custom prompt (Go text/template). See the default prompt at: DEFAULT PROMPT.
config.json

Config lives at ~/.config/ashlet/config.json:

{
  "version": 1,
  "generation": {
    "base_url": "https://openrouter.ai/api/v1",
    "api_key": "",
    "api_type": "responses",
    "model": "mistralai/codestral-2508",
    "max_tokens": 120,
    "temperature": 0.3,
    "no_raw_history": true
  },
  "embedding": {
    "base_url": "https://openrouter.ai/api/v1",
    "api_key": "",
    "model": "openai/text-embedding-3-small",
    "dimensions": 1536,
    "ttl_minutes": 60,
    "max_history_commands": 3000
  },
  "telemetry": {
    "openrouter": true
  }
}

Embeddings are optional. When disabled, ashlet uses recency-only history (no semantic search).

API Types
  • "responses" (default) — OpenAI Responses API (POST /responses). Works with OpenRouter.
  • "chat_completions" — Chat Completions format (POST /chat/completions). Use this for Ollama or other local providers.
Alternative Ways

You can override some config.json values via environment variables.

Config key Priority
generation.base_url $ASHLET_GENERATION_API_BASE_URL > generation.base_url in config.json
generation.api_key $ASHLET_GENERATION_API_KEY > generation.api_key in config.json
generation.model $ASHLET_GENERATION_MODEL > generation.model in config.json
embedding.base_url $ASHLET_EMBEDDING_API_BASE_URL > embedding.base_url in config.json
embedding.api_key $ASHLET_EMBEDDING_API_KEY > embedding.api_key in config.json
embedding.model $ASHLET_EMBEDDING_MODEL > embedding.model in config.json
prompt.md

Prompt lives at ~/.config/ashlet/prompt.md. It uses Go text/template syntax. See the default prompt at: DEFAULT PROMPT for template variables and format.

Shell Environment Variables
Variable Default Description
ASHLET_SOCKET auto Override the Unix socket path
ASHLET_MAX_CANDIDATES 4 Max suggestions per request
ASHLET_MIN_INPUT 2 Minimum characters before requesting
ASHLET_DELAY 0.05 Debounce delay (seconds)

Architecture

shell/          Zsh client — captures input, communicates over Unix socket, renders suggestions
generate/       Completion engine — context gathering, API inference, candidate parsing
index/          History indexing and embedding via API
serve/          Daemon entry point and Unix socket server (ashletd)
repl/           Interactive test REPL with cursor tracking (dev-only, not distributed)
default/        Embedded default config and prompt template
ashlet.go       Shared IPC types (Request, Response, Error)
config.go       Configuration types and path resolution

Dependency graph: root (ashlet) <- index <- generate <- serve

Development

make bootstrap    # Download Go dependencies
make build        # Build ashletd
make test         # Go tests + shell tests (bats)
make lint         # go vet + staticcheck + shellcheck
make format       # gofmt + shfmt
make repl         # Build and run the test REPL (dev-only)
Running Locally

Open two terminals to test end-to-end:

Terminal 1 — daemon:

./_run.sh            # build ashletd and start it
./_run.sh --verbose  # same, with request/response logging

Terminal 2 — shell client:

./shell/_run.sh

This launches an interactive zsh session with ashlet pre-loaded. Type commands to see completions.

Test REPL

For testing completions without the daemon or shell client:

make repl             # interactive, TOML output on screen
make repl > log.toml  # save structured output to file

The REPL calls the completion engine directly with raw terminal cursor tracking. Each submission outputs structured TOML (context gathered, request, response). Use :cwd <path> to change directory, :quit to exit. Embeddings are cached to .cache/ in the project root for fast subsequent runs (REPL-only — the daemon does not use disk cache).

Why Name It ashlet?

AI/Autocomplete Shell Scriptlet (and a reference to Ashley, to make it a bit more anthropomorphic).

License

See LICENSE for details.

Documentation

Overview

Package ashlet defines the request/response types for ashlet IPC. Messages are JSON-encoded and sent over a Unix domain socket, one per line.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ConfigDir

func ConfigDir() string

ConfigDir returns the config directory path. Resolution order: $ASHLET_CONFIG_DIR > $XDG_CONFIG_HOME/ashlet > ~/.config/ashlet

func ConfigPath

func ConfigPath() string

ConfigPath returns the full path to the config file.

func EmbeddingEnabled

func EmbeddingEnabled(cfg *Config) bool

EmbeddingEnabled returns true when both base_url and api_key are configured for embedding.

func OpenRouterTelemetryEnabled

func OpenRouterTelemetryEnabled(cfg *Config) bool

OpenRouterTelemetryEnabled returns whether OpenRouter attribution headers should be sent.

func PromptPath

func PromptPath() string

PromptPath returns the prompt file path.

func ResolveEmbeddingAPIKey

func ResolveEmbeddingAPIKey(cfg *Config) string

ResolveEmbeddingAPIKey returns the embedding API key. Priority: $ASHLET_EMBEDDING_API_KEY env > config value.

func ResolveEmbeddingBaseURL

func ResolveEmbeddingBaseURL(cfg *Config) string

ResolveEmbeddingBaseURL returns the embedding API base URL. Priority: $ASHLET_EMBEDDING_API_BASE_URL env > config value.

func ResolveEmbeddingModel

func ResolveEmbeddingModel(cfg *Config) string

ResolveEmbeddingModel returns the embedding model name. Priority: $ASHLET_EMBEDDING_MODEL env > config value.

func ResolveGenerationAPIKey

func ResolveGenerationAPIKey(cfg *Config) string

ResolveGenerationAPIKey returns the generation API key. Priority: $ASHLET_GENERATION_API_KEY env > config value.

func ResolveGenerationBaseURL

func ResolveGenerationBaseURL(cfg *Config) string

ResolveGenerationBaseURL returns the generation API base URL. Priority: $ASHLET_GENERATION_API_BASE_URL env > config value.

func ResolveGenerationModel

func ResolveGenerationModel(cfg *Config) string

ResolveGenerationModel returns the generation model name. Priority: $ASHLET_GENERATION_MODEL env > config value.

func ValidateConfig

func ValidateConfig(cfg *Config) []string

ValidateConfig checks configuration for potential issues and returns warnings.

Types

type Candidate

type Candidate struct {
	// Completion is the full command line suggestion.
	Completion string `json:"completion"`
	// CursorPos is the desired cursor position within the completion.
	// nil means cursor at end of completion.
	CursorPos *int `json:"cursor_pos,omitempty"`
	// Confidence is the model's confidence score (0.0 to 1.0).
	Confidence float64 `json:"confidence"`
}

Candidate represents a single completion suggestion with a confidence score.

type Config

type Config struct {
	Version    int              `json:"version"`
	Generation GenerationConfig `json:"generation"`
	Embedding  EmbeddingConfig  `json:"embedding"`
	Telemetry  TelemetryConfig  `json:"telemetry"`
}

Config represents the user's ashlet configuration.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns the default configuration from the embedded default_config.json.

func LoadConfig

func LoadConfig() (*Config, error)

LoadConfig loads config from disk or returns defaults if not found.

type ConfigRequest

type ConfigRequest struct {
	// Action is the config operation: "get", "reload", "defaults", or "default_prompt".
	Action string `json:"action"`
}

ConfigRequest is sent from the shell client for configuration operations.

type ConfigResponse

type ConfigResponse struct {
	// Config is the current configuration (for "get", "reload", and "defaults" actions).
	Config *Config `json:"config,omitempty"`
	// Prompt is the default prompt template (for "default_prompt" action).
	Prompt string `json:"prompt,omitempty"`
	// Warnings contains configuration warnings (for "validate" action).
	Warnings []string `json:"warnings,omitempty"`
	// Error is set when the operation fails.
	Error *Error `json:"error,omitempty"`
}

ConfigResponse is sent from the daemon in response to a ConfigRequest.

type ContextRequest

type ContextRequest struct {
	// Type is always "context".
	Type string `json:"type"`
	// Cwd is the directory to pre-cache context for.
	Cwd string `json:"cwd"`
}

ContextRequest is sent from the shell client to warm the directory context cache.

type ContextResponse

type ContextResponse struct {
	// OK is true when the warm-up was accepted.
	OK bool `json:"ok"`
	// Error is set when the operation fails.
	Error *Error `json:"error,omitempty"`
}

ContextResponse is sent from the daemon in response to a ContextRequest.

type EmbeddingConfig

type EmbeddingConfig struct {
	BaseURL            string `json:"base_url"`
	APIKey             string `json:"api_key"`
	Model              string `json:"model"`
	Dimensions         int    `json:"dimensions,omitempty"`
	TTLMinutes         int    `json:"ttl_minutes,omitempty"`
	MaxHistoryCommands int    `json:"max_history_commands,omitempty"`
}

EmbeddingConfig holds settings for the embedding API.

type Error

type Error struct {
	// Code is a machine-readable error identifier (e.g. "not_configured", "api_error").
	Code string `json:"code"`
	// Message is a human-readable error description.
	Message string `json:"message"`
}

Error describes a daemon-side error returned to the shell client.

type GenerationConfig

type GenerationConfig struct {
	BaseURL      string   `json:"base_url"`
	APIKey       string   `json:"api_key"`
	APIType      string   `json:"api_type"`
	Model        string   `json:"model"`
	MaxTokens    int      `json:"max_tokens,omitempty"`
	Temperature  float64  `json:"temperature,omitempty"`
	Stop         []string `json:"stop,omitempty"`
	NoRawHistory *bool    `json:"no_raw_history,omitempty"`
}

GenerationConfig holds settings for the generation API.

type Request

type Request struct {
	// RequestID is a per-session incrementing identifier assigned by the shell.
	// The daemon echoes it back in the response for ordering.
	RequestID int `json:"request_id"`
	// Input is the current command line content.
	Input string `json:"input"`
	// CursorPos is the cursor position within the input.
	CursorPos int `json:"cursor_pos"`
	// Cwd is the current working directory of the shell.
	Cwd string `json:"cwd"`
	// SessionID identifies the shell session.
	SessionID string `json:"session_id"`
	// MaxCandidates is the maximum number of completion candidates to return.
	MaxCandidates int `json:"max_candidates,omitempty"`
}

Request is sent from the shell client to the daemon.

type Response

type Response struct {
	// RequestID is echoed from the request for ordering on the client side.
	RequestID int `json:"request_id"`
	// Candidates is the list of completion suggestions, sorted by confidence descending.
	Candidates []Candidate `json:"candidates"`
	// Error is set when the daemon cannot fulfill the request.
	Error *Error `json:"error,omitempty"`
}

Response is sent from the daemon back to the shell client.

type TelemetryConfig

type TelemetryConfig struct {
	OpenRouter *bool `json:"openrouter,omitempty"`
}

TelemetryConfig holds telemetry settings.

Directories

Path Synopsis
Package defaults provides embedded default assets (prompt template and config).
Package defaults provides embedded default assets (prompt template and config).
Package generate orchestrates model inference to generate shell completions.
Package generate orchestrates model inference to generate shell completions.
Package index provides history indexing and embedding for ashlet.
Package index provides history indexing and embedding for ashlet.
Command ashlet-repl is an interactive test REPL for ashlet completions.
Command ashlet-repl is an interactive test REPL for ashlet completions.
Command ashletd is the ashlet daemon.
Command ashletd is the ashlet daemon.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL