Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

119 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Skiff

A small vessel with a big personality.

License: MPL-2.0


A personality-driven, multi-turn conversational agent for Discord, built with TypeScript and designed for extensibility. Skiff combines long-term memory, tool use, and customizable personas to create engaging interactions that go beyond simple Q&A.

Features

  • Multi-turn conversations via /ask, /clear, and @mentions
  • Switch models on the fly, /model per channel or /ask model: for a single question
  • Long-term memory, semantic search, automatic fact extraction, and topic knowledge
  • Living Logbook, durable storylines with decisions, commitments, risks, and open questions
  • The Wake, evidence-backed causal trails explaining how decisions and outcomes came to be
  • Multiple LLM backends, OpenAI, Anthropic, Ollama, or any OpenAI-compatible API
  • Embedded database, PGlite with pgvector, no external PostgreSQL needed
  • Discord tools, look up server info, users, react to messages, and more
  • Web + browser tools, Brave search, page fetch/markdown extraction, and Cloudflare CDP browser control
  • MCP integration, extend capabilities with external tool servers
  • Skills, activated on demand with minimal prompt overhead
  • Customizable personas, define a character with prose, voice, and example dialogue

Quickstart

git clone https://github.com/dougley/skiff.git
cd skiff
pnpm install

Create a .env file:

DISCORD_BOT_TOKEN=your-bot-token
OPENAI_API_KEY=sk-...

Run it:

pnpm build && pnpm start

Docker

The included docker-compose.yml runs the bot alongside Ollama for fully local inference, no external API keys needed (besides your Discord token):

docker compose up -d

This pulls nomic-embed-text for embeddings and uses Ollama as the embedding provider. Set your LLM provider and model in .env as usual.

Configuration

All config lives in environment variables. Only DISCORD_BOT_TOKEN and a LLM provider's API key are strictly required to get started, but there are many options for customizing behavior, access control, tools, and more.

LLM

Variable Default Description
DISCORD_BOT_TOKEN required Your Discord bot token
LLM_DEFAULT_MODEL gpt-4o-mini Model to use for chat, unless a channel overrides it with /model
LLM_ALLOWED_MODELS -- Comma-separated models /model and /ask model: may select. Unset means any model the provider accepts. The first 25 appear as slash-command choices (Discord's limit)
LLM_DEFAULT_PROVIDER openai openai, anthropic, or ollama
OPENAI_API_KEY -- OpenAI API key
OPENAI_API_BASE_URL -- Custom OpenAI-compatible endpoint
ANTHROPIC_API_KEY -- Anthropic API key
ANTHROPIC_API_BASE_URL -- Custom Anthropic endpoint
OLLAMA_API_KEY -- Ollama API key (if needed)
OLLAMA_BASE_URL http://localhost:11434 Ollama endpoint
VISION_ENABLED true Enable/disable vision (image) support
LLM_MAX_OUTPUT_TOKENS 8192 Maximum output tokens requested per generation step
LLM_REASONING_EFFORT -- off, minimal, low, medium, or high. Unset leaves the provider default. Maps to reasoning_effort on OpenAI/Ollama and an extended-thinking token budget on Anthropic (1k/4k/8k/16k, reserved on top of LLM_MAX_OUTPUT_TOKENS)
LLM_MAX_RETRIES 2 Retries after the first attempt, so the default is 3 tries. Applies to every provider call — chat, embeddings, memory extraction, and the sleep phases. 0 disables retrying
LLM_TURN_TIMEOUT_MS 180000 Overall timeout for one conversational turn, retries and their backoff included

Upstream failures

Provider calls retry with exponential backoff, honouring Retry-After. Only worthwhile failures are retried: HTTP 408/409/429/5xx and connection errors. A 4xx from a malformed request fails immediately, as does an abort or a timeout. Retries wrap the model call itself, so a retried turn never re-runs tools that already executed.

Every transient failure is logged as it happens, so a blip that the retry recovers from still shows up as a WARN rather than passing silently — that's the signal that a provider is degrading before it starts failing outright. Exhausting the budget is reported by the call site as usual, with the attempt count in the message.

During a chat turn the wait is also shown in the live status message (Provider failed (503), trying again in 2s), so a stalled turn reads as waiting rather than hung. It's replaced as soon as the turn moves on.

Retries share the turn budget rather than extending it — LLM_TURN_TIMEOUT_MS still caps the whole turn, and a timeout during backoff aborts rather than hanging. Web and Discord calls have their own error handling and aren't governed by LLM_MAX_RETRIES.

Switching models at runtime

LLM_DEFAULT_MODEL is the fallback, not a hard setting. Without restarting:

  • /model set <model> points this channel at another model. It sticks for @mentions too, and the conversation history carries over.
  • /model show reports what the channel is using and what it may pick.
  • /model reset goes back to LLM_DEFAULT_MODEL.
  • /ask question:… model:… overrides one question only, leaving the channel's model alone.

The Ask context-menu commands take no options, so they follow the channel's /model. Background work (memory extraction, sleep phases) keeps using MEMORY_EXTRACT_MODEL / LLM_DEFAULT_MODEL and is deliberately unaffected, so a channel on an expensive model doesn't drag the batch jobs along.

Only the model moves; the provider stays LLM_DEFAULT_PROVIDER, so pick models that provider serves. A channel with no override always follows LLM_DEFAULT_MODEL, so changing the env default still moves every channel that never opted out. Set LLM_ALLOWED_MODELS anywhere /model is reachable by people you'd rather not hand your priciest model to — a model that later drops off the allowlist falls back to the default instead of stranding the channel.

Memory & RAG

Variable Default Description
EMBEDDING_MODEL text-embedding-3-small Embedding model for RAG
EMBEDDING_PROVIDER openai openai, ollama, or disabled
MEMORY_EXTRACT_MODEL -- Model override for background memory extraction
RAG_TOP_K 5 Semantic search results to retrieve (1-20)
RAG_RECENT_LIMIT 12 Recent messages to include (1-50)
RAG_MIN_SIMILARITY 0.3 Minimum similarity score (0-1)

Web & Browser tools

The web tool group includes:

  • web_search (Brave Search)
  • fetch_url (HTML fetch; markdown extraction via Cloudflare when configured)
  • browser_cdp (Cloudflare Browser Rendering via CDP: session lifecycle, tabs, navigation, snapshots, screenshots, JS evaluation, and click/type/key interactions)
Variable Default Description
BRAVE_SEARCH_API_KEY -- API key for Brave Search (enables web_search)
CLOUDFLARE_ACCOUNT_ID -- Cloudflare account ID for Browser Rendering endpoints
CLOUDFLARE_API_TOKEN -- Cloudflare API token with Browser Rendering - Edit permission

Access Control

Control where and for whom the bot operates. Default is open (no restrictions).

Caution

Leaving the bot open can lead to abuse. Consider using allowlist policies and specifying allowed guilds, channels, or users.

Variable Default Description
ACCESS_POLICY open Guild/channel policy: open, disabled, or allowlist
ACCESS_DM_POLICY open DM policy: open, disabled, or allowlist
ACCESS_ALLOWED_GUILDS -- Comma-separated guild IDs (only when policy is allowlist)
ACCESS_ALLOWED_CHANNELS -- Comma-separated channel IDs (only when policy is allowlist)
ACCESS_ALLOWED_USERS -- Comma-separated user IDs (applies to both guild and DM allowlists)
DISABLED_TOOLS -- Tool groups or individual built-in tools turned off everywhere (see below)
TOOL_CHANNEL_RULES -- Per-channel tool restrictions (see below)
TOOL_GUILD_RULES -- Per-guild tool restrictions (see below)
TOOL_DM_RULES -- Tool groups disabled in all DMs (see below)
TOOL_USER_RULES -- Per-user tool restrictions (see below)

When ACCESS_POLICY is allowlist, each level is checked independently — if an allowlist is empty at a given level, that level is unrestricted. For example, setting only ACCESS_ALLOWED_GUILDS restricts by guild but allows all channels and users within those guilds.

Tool rules let you disable specific tool groups at different scopes. Rules from all layers are merged (union) — if a tool group is disabled at any level, it's unavailable.

Variable Format Scope
TOOL_GUILD_RULES guildId:group1,group2;... Guild-wide defaults
TOOL_CHANNEL_RULES channelId:group1,group2;... Per-channel overrides
TOOL_USER_RULES userId:group1,group2;... Per-user restrictions
TOOL_DM_RULES group1,group2,... All DMs (no ID prefix)
DISABLED_TOOLS group1,tool_name,... Everywhere, including autonomous turns
# Disable shell guild-wide, additionally disable web in one channel
TOOL_GUILD_RULES=999999999999999999:shell
TOOL_CHANNEL_RULES=111111111111111111:web

# Disable shell and web in all DMs
TOOL_DM_RULES=shell,web

# Restrict a specific user from using scheduler
TOOL_USER_RULES=222222222222222222:scheduler

Available tool groups: discord, persona, memory, topic, logbook, web, scheduler, heartbeat, shell, mcp, user-input, skills.

The web group controls web_search, fetch_url, and browser_cdp together.

Replacing a built-in tool. DISABLED_TOOLS is the only rule that takes individual tool names, which is what you want when an MCP server does one job better than the built-in — the group vars would take out its neighbours too:

# drop Brave search, keep page fetching and the browser
DISABLED_TOOLS=web_search

# turn off a whole group everywhere, without listing every guild
DISABLED_TOOLS=shell,heartbeat

It names built-in tools. An MCP tool normally has to yield to a built-in of the same name and gets exposed as mcp_<name> instead; disabling the built-in frees the name, so the replacement is offered under it directly and the model sees one tool rather than two. MCP tools themselves are governed by mcp.json and the mcp group. A name that matches no built-in does nothing.

Shell

Gives the LLM access to the shell. Disabled by default.

Note

Skiff is designed with the assumption the agent is running in a container, or some other isolated environment with limited access to the host system. If that's not the case, be extra cautious with shell tools. Skiff makes best-effort safety measures, but a malicious or careless prompt could still cause damage.

Warning

Shell tools are dangerous, use with caution and only for trusted users. Always review your tool rules to ensure you don't accidentally expose powerful capabilities.

Variable Default Description
SHELL_ENABLED false Enable shell tools (true / false)
SHELL_WORK_DIR /home/skiff Working directory for shell commands
SHELL_ALLOWED_DIRS /tmp Comma-separated directories the shell can access

Heartbeat

Proactive monitoring — the bot periodically checks in on enabled channels.

Variable Default Description
HEARTBEAT_ENABLED true Enable the heartbeat system
HEARTBEAT_INTERVAL_MINUTES 30 Minutes between heartbeat checks (1-1440)
HEARTBEAT_CHECKLIST_PATH ./HEARTBEAT.md Markdown file with heartbeat instructions
HEARTBEAT_QUIET_HOURS_START 23:00 Start of quiet hours (HH:MM)
HEARTBEAT_QUIET_HOURS_END 08:00 End of quiet hours (HH:MM)
HEARTBEAT_TIMEZONE UTC IANA timezone for quiet hours
HEARTBEAT_ACK_MAX_CHARS 300 Max characters for heartbeat acknowledgments (0-1000)

Skills

Skills extend Skiff's capabilities without touching core code. Drop a directory with a SKILL.md file into skills/ and the LLM can activate it on demand. See skills/README.md for the full format and examples.

Skills are able to define their own tools in the form of MCP tool servers. When a skill is activated, its tools become available to the agent for the duration of the conversation, and are automatically removed when the conversation ends.

Variable Default Description
SKILLS_DIR ./skills Directory to scan for skills

Sleep Cycle (Dream Pass)

The sleep cycle is a background maintenance system that runs during idle periods. It consolidates memories, deduplicates knowledge, evolves the persona, and can auto-author new skills. Enabled per-scope via the /sleep-cycle enable Discord command — in a server it dreams over that guild's memory, in a DM it dreams over that conversation's own facts, topics, and persona notes. Runs in dry-run mode by default (changes are logged but not applied). Use /sleep-cycle set-report-channel to get a digest after each pass (DMs report into the DM by default).

Variable Default Description
SLEEP_CONSOLIDATE_LOOKBACK_DAYS 30 Days of user activity to scan for fact consolidation
SLEEP_CONSOLIDATE_MAX_USERS 10 Max users to process per pass
SLEEP_CONSOLIDATE_MIN_FACTS 2 Minimum facts per user to trigger consolidation
SLEEP_DEDUPE_SIMILARITY 0.9 Cosine similarity threshold for topic deduplication (0-1)
SLEEP_MAX_TOPICS 200 Max topics to scan for deduplication
SLEEP_MAX_MERGES_PER_RUN 20 Max topic merges per pass
SLEEP_CLUSTER_THRESHOLD 0.85 Cosine similarity threshold for message clustering (0-1)
SLEEP_MIN_CLUSTER_SIZE 5 Minimum messages in a cluster to synthesize a new topic
SLEEP_MAX_CLUSTERS_PER_RUN 3 Max new topics to synthesize per pass
SLEEP_MAX_SAMPLES 500 Max message embeddings to consider for clustering
SLEEP_NEW_TOPIC_OVERLAP_THRESHOLD 0.85 Skip clusters that overlap existing topics above this threshold (0-1)
SLEEP_SYNTHESIZE_LOOKBACK_DAYS 7 Days of messages to scan for topic synthesis
SLEEP_REFLECT_LOOKBACK_DAYS 14 Days of messages to reflect on for persona growth
SLEEP_REFLECT_MAX_MESSAGES 120 Max messages to include in persona reflection
SLEEP_REFLECT_MIN_CONFIDENCE 70 Minimum confidence (0-100) for a persona note to be kept
SLEEP_REFLECT_MAX_ADDENDA_PER_RUN 3 Max persona notes to generate per pass
SLEEP_PROPOSE_LOOKBACK_DAYS 7 Days of user messages to scan for skill proposals
SLEEP_PROPOSE_MAX_USER_MESSAGES 200 Max user messages to consider for skill proposals
SLEEP_PROPOSE_MIN_CONFIDENCE 75 Minimum confidence (0-100) for a skill proposal to be kept
SLEEP_MAX_ADDENDA_PER_SCOPE 15 Max persona addenda injected into a single system prompt

Logbook

The Logbook tracks endeavors that unfold across conversations. A storyline has a goal, a concise current state, owners, lifecycle status, and an append-only history of decisions, commitments, open questions, risks, milestones, and notes. Ask Skiff to track something, then use /logbook list or /logbook show to inspect it. Relevant active storylines are recalled automatically during later conversations.

The Wake connects those events with typed relationships such as supports, depends_on, contradicts, supersedes, and caused_by. Ask Skiff why a decision was made, or use /wake, to trace the reasoning back to its source messages. Skiff can add supporting evidence over time without rewriting history, and its dream pass can propose high-confidence links already explicit in the Logbook.

General

Variable Default Description
DATABASE_URL file://pg_data PGlite data directory
LOG_LEVEL info trace, debug, info, warn, error, fatal
NODE_ENV development development, production, or test
MCP_CONFIG_PATH mcp.json Path to MCP server config
MCP_TOOLS_CACHE_TTL_MS 300000 How long MCP tool listings are reused before refetching (0 to disable)
PERSONA_FILE ./agent.persona.json Path to persona file
GUILD_ID -- Restrict command registration to a single guild (faster for dev)
CONTEXT_WINDOW_SIZE 200000 Max context window size in tokens

Custom Personas

Skiff's personality is defined by a persona JSON file: prose description, a voice list, and examples of how the character talks. It deliberately avoids numeric trait scoring (Big Five, MBTI, etc.) because models mirror a few concrete example exchanges far more reliably than they interpret a creativity: 0.6. Point PERSONA_FILE at any valid persona:

PERSONA_FILE=examples/agent.persona.research-analyst.json pnpm dev

The examples/ directory has ready-made personas:

Persona Style
template Blank slate, fill in the blanks
customer-support (Mara) Warm, empathetic, checklist-driven
research-analyst (Iris) Methodical, evidence-first, thorough
playful-storyteller (Jun) Creative, expressive, collaborative
strict-sysadmin (Kade) Blunt, safety-first, asks for logs
kid-friendly-tutor (Pip) Patient, encouraging, safe

To build your own, copy examples/agent.persona.template.json and fill it in. See examples/README.md for the full field reference.

License

MPL-2.0

About

A small vessel with a big personality

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages