diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ca6ae81..e179913 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,8 +9,8 @@ permissions: contents: write jobs: - release: - name: Build and publish binaries + build: + name: Build ${{ matrix.suffix }} runs-on: ubuntu-latest strategy: matrix: @@ -73,7 +73,7 @@ jobs: publish: name: Publish GitHub Release - needs: release + needs: build runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 @@ -82,8 +82,13 @@ jobs: path: dist/ merge-multiple: true + - name: Generate checksums + working-directory: dist + run: sha256sum * > SHA256SUMS.txt + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - files: dist/** + files: | + dist/** generate_release_notes: true diff --git a/README.md b/README.md index b704a69..31289b9 100644 --- a/README.md +++ b/README.md @@ -106,9 +106,40 @@ seshat is the **headless runtime**: pure Go, no UI, no users, no billing. It is The engine is intentionally kept minimal and fast. If you need something from the SDK that is not exposed yet, open an issue and we will prioritize it. -### 📦 Prebuilt binaries (coming soon) +### 📦 Installation -You will soon be able to download a single binary to use the CLI or embed the SDK in your application without building from source. Watch this repo for releases. +**End users — one command, fully configured:** + +```bash +curl -fsSL https://raw.githubusercontent.com/EngineerProjects/seshat/main/scripts/install.sh | bash +``` + +Downloads the right binary for your platform, adds it to your PATH, installs `uv` and `docling-serve` for document processing, and leaves the runtime directory (`~/.config/seshat-cli/`) ready. The DB and sessions are created on first run. + +Options: +```bash +NO_PYTHON=1 bash <(curl -fsSL ...) # skip uv + docling (minimal install) +VERSION=v0.1.0 bash <(curl -fsSL ...) # pin a specific version +``` + +**Developers — Go toolchain:** + +```bash +# Install the CLI binary +go install github.com/EngineerProjects/seshat/cmd/cli@latest + +# Then set up document processing if needed +seshat setup + +# Or check what is already installed +seshat setup --check +``` + +**SDK — embed in your Go application:** + +```bash +go get github.com/EngineerProjects/seshat@latest +``` --- @@ -118,20 +149,16 @@ You will soon be able to download a single binary to use the CLI or embed the SD An AI agent in your terminal. Multi-provider, local-first, skills-aware. -**Build from source** +**Install** ```bash -git clone https://github.com/EngineerProjects/seshat -cd seshat -make build # produces bin/seshat and bin/seshat-grpc -``` +# End users — full setup in one command: +curl -fsSL https://raw.githubusercontent.com/EngineerProjects/seshat/main/scripts/install.sh | bash -Add `bin/` to your PATH, or copy `bin/seshat` to `/usr/local/bin`: - -```bash -export PATH="$PATH:$(pwd)/bin" -# or -sudo cp bin/seshat /usr/local/bin/seshat +# Developers — binary only via Go toolchain: +go install github.com/EngineerProjects/seshat/cmd/cli@latest +seshat setup # install uv + docling-serve afterwards if needed +seshat setup --check # check what is already configured ``` **Configure a provider** @@ -139,20 +166,20 @@ sudo cp bin/seshat /usr/local/bin/seshat ```bash seshat config --provider anthropic --api-key sk-ant-... seshat config --model anthropic:claude-sonnet-4-20250514 - seshat config --print ``` **Run** ```bash -seshat chat # interactive TUI session -seshat chat --resume # resume a specific session -seshat chat --continue # resume the most recent session -seshat run "list all TODO comments in this codebase" # one-shot task -seshat sessions list # browse past sessions -seshat sessions list --status active # active sessions only -seshat help # full command reference +seshat chat # interactive TUI session +seshat chat --resume # resume a specific session +seshat chat --continue # resume the most recent session +seshat run "list all TODO comments in this codebase" # one-shot task +seshat sessions list # browse past sessions +seshat setup --check # show uv / docling status +seshat version # print installed version +seshat help # full command reference ``` Sessions are persisted locally in SQLite. Skills are loaded from `.seshat/skills/` in your project. The full tool set is available: file edits, sandboxed bash, web search, browser, MCP servers, sub-agents. @@ -309,32 +336,28 @@ Full model listings and capabilities: [`docs/providers.md`](./docs/providers.md) ## 🚀 Quick Start ```bash -# 1. Clone and build -git clone https://github.com/EngineerProjects/seshat -cd seshat -make build # produces bin/seshat and bin/seshat-grpc -export PATH="$PATH:$(pwd)/bin" +# 1. Install +curl -fsSL https://raw.githubusercontent.com/EngineerProjects/seshat/main/scripts/install.sh | bash + +# Reload your shell (or open a new terminal) if prompted, then: -# 2. Set your API key and model +# 2. Configure your provider seshat config --provider anthropic --api-key sk-ant-... seshat config --model anthropic:claude-sonnet-4-20250514 # 3. Start chatting -seshat chat # new session +seshat chat # new session (opens TUI) seshat chat --continue # resume last session seshat chat --resume # resume a specific session - -# One-shot task in the current directory -seshat run "list all TODO comments in this codebase" - -# Start the gRPC server (port 50051) -ANTHROPIC_API_KEY=sk-ant-... ./bin/seshat-grpc +seshat run "list all TODO comments in this codebase" # one-shot task ``` > **No API key?** Use Ollama for free local inference: > `seshat config --provider ollama --model ollama:llama3.2` > (requires [Ollama](https://ollama.com) running locally) +> **Developers building from source:** see the [Development](#️-development) section below. + --- ## ⚡ Skills @@ -396,30 +419,45 @@ Full architecture diagrams (Mermaid): [`docs/vision/diagrams.md`](./docs/vision/ ## 🛠️ Development -### First-time setup +### First-time setup (build from source) ```bash # Linux / macOS — installs all dependencies, builds, wires git hooks +git clone https://github.com/EngineerProjects/seshat +cd seshat make setup # Windows (PowerShell — make is not available by default on Windows) powershell -ExecutionPolicy Bypass -File scripts\setup.ps1 ``` -`make setup` handles everything: Go version check, ripgrep, uv, Python venv with docling-serve, and the final build. +`make setup` handles everything: Go version check, ripgrep, uv, Python venv with docling-serve, and the final build. Binaries land in `bin/`. ### Daily commands ```bash -make build # build CLI and gRPC binaries to bin/ -make test # run all tests -make test-race # run tests with race detector -make lint # golangci-lint -make fmt # gofmt -make hooks # (re-)install git pre-commit hooks -make install-deps # install ripgrep only (included in make setup) +make build # build CLI and gRPC binaries to bin/ +make test # run all tests +make test-race # run tests with race detector +make lint # golangci-lint +make fmt # gofmt +make hooks # (re-)install git pre-commit hooks make install-python # install/update the Python venv + docling-serve only make start-docling # start docling-serve manually (auto-started by seshat chat) +make clean # remove bin/ +make clean-runtime # erase runtime data (~/.config/seshat-cli) +make clean-all # both +``` + +### seshat setup (runtime, not source) + +When the CLI is installed via `curl | bash` or `go install`, use the built-in setup command to manage the Python environment: + +```bash +seshat setup # install uv + docling-serve +seshat setup --check # show status without installing +seshat setup --python 3.12 # use a specific Python version +seshat setup --extras gpu # GPU-accelerated docling ``` ### Runtime data directory diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 839e3d0..5aa2440 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -9,6 +9,9 @@ import ( "github.com/EngineerProjects/seshat/pkg/runtimepath" ) +// version is set at build time via -ldflags "-X main.version=v1.2.3". +var version = "dev" + func main() { // Pin the CLI runtime root to the platform config dir (seshat-cli), // isolated from the seshat-product backend (seshat). SESHAT_RUNTIME_ROOT takes precedence. diff --git a/cmd/cli/root.go b/cmd/cli/root.go index 980c16f..8af7ca0 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -25,6 +25,11 @@ func execute(ctx context.Context, args []string, stdin io.Reader, stdout, stderr return runMemory(args[1:], stdout, stderr) case "login": return runLogin(ctx, args[1:], stdout, stderr) + case "setup": + return runSetup(args[1:], stdout, stderr) + case "version", "--version", "-v": + fmt.Fprintln(stdout, version) + return nil case "help", "-h", "--help": printUsage(stdout) return nil @@ -63,4 +68,10 @@ func printUsage(out io.Writer) { fmt.Fprintln(out, " seshat login [--provider openai|anthropic] [--client-id ID]") fmt.Fprintln(out, " Authenticate via browser using your ChatGPT subscription.") fmt.Fprintln(out, " Runs a device-code flow — no API key required.") + fmt.Fprintln(out, "") + fmt.Fprintln(out, " seshat setup [--check] [--python VERSION] [--extras EXTRAS]") + fmt.Fprintln(out, " Install uv + docling-serve for document processing.") + fmt.Fprintln(out, " --check show status without installing.") + fmt.Fprintln(out, "") + fmt.Fprintln(out, " seshat version Print the current version.") } diff --git a/cmd/cli/setup.go b/cmd/cli/setup.go new file mode 100644 index 0000000..31b6f86 --- /dev/null +++ b/cmd/cli/setup.go @@ -0,0 +1,164 @@ +package main + +import ( + "flag" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/EngineerProjects/seshat/pkg/runtimepath" +) + +func runSetup(args []string, stdout, stderr io.Writer) error { + fs := flag.NewFlagSet("setup", flag.ContinueOnError) + fs.SetOutput(stderr) + checkOnly := fs.Bool("check", false, "report what is installed without installing anything") + pythonVer := fs.String("python", "3.11", "Python version for the docling venv") + extras := fs.String("extras", "", "docling-serve pip extras (e.g. gpu)") + if err := fs.Parse(args); err != nil { + return err + } + + root := os.Getenv(runtimepath.EnvRuntimeRoot) + if root == "" { + root = runtimepath.DefaultConfigDir("seshat-cli") + } + venv := filepath.Join(root, ".venv") + + if *checkOnly { + return setupCheck(stdout, root, venv) + } + return setupInstall(stdout, stderr, root, venv, *pythonVer, *extras) +} + +// ── Check ───────────────────────────────────────────────────────────────────── + +func setupCheck(out io.Writer, root, venv string) error { + fmt.Fprintln(out, "Seshat setup status") + fmt.Fprintln(out, "-------------------") + fmt.Fprintf(out, "Runtime root : %s\n", root) + + uvPath, uvErr := exec.LookPath("uv") + if uvErr == nil { + uvVer, _ := cmdOutput("uv", "--version") + fmt.Fprintf(out, "uv : %s (%s)\n", strings.TrimSpace(uvVer), uvPath) + } else { + fmt.Fprintln(out, "uv : not found") + } + + doclingBin := filepath.Join(venv, "bin", "docling-serve") + if _, err := os.Stat(doclingBin); err == nil { + fmt.Fprintf(out, "docling-serve: %s\n", doclingBin) + } else { + fmt.Fprintln(out, "docling-serve: not installed") + } + + if _, err := os.Stat(root); err == nil { + fmt.Fprintf(out, "runtime dir : OK (%s)\n", root) + } else { + fmt.Fprintf(out, "runtime dir : not yet created (created on first run)\n") + } + return nil +} + +// ── Install ─────────────────────────────────────────────────────────────────── + +func setupInstall(stdout, stderr io.Writer, root, venv, pythonVer, extras string) error { + logf := func(msg string, a ...any) { fmt.Fprintf(stdout, "[seshat] "+msg+"\n", a...) } + fail := func(msg string, a ...any) error { return fmt.Errorf(msg, a...) } + + // 1. Install uv if missing + if _, err := exec.LookPath("uv"); err != nil { + logf("uv not found — installing via official installer...") + if err := installUV(stdout, stderr); err != nil { + return fail("uv installation failed: %w", err) + } + // uv installer places the binary in ~/.local/bin or ~/.cargo/bin + extra := os.Getenv("HOME") + "/.local/bin" + string(os.PathListSeparator) + + os.Getenv("HOME") + "/.cargo/bin" + os.Setenv("PATH", extra+string(os.PathListSeparator)+os.Getenv("PATH")) + + if _, err := exec.LookPath("uv"); err != nil { + return fail("uv installed but not found on PATH — reopen your terminal and retry") + } + } + uvVer, _ := cmdOutput("uv", "--version") + logf("uv: %s", strings.TrimSpace(uvVer)) + + // 2. Create runtime root + if err := os.MkdirAll(root, 0o750); err != nil { + return fail("could not create runtime root %s: %w", root, err) + } + logf("Runtime root: %s", root) + + // 3. Create venv + if _, err := os.Stat(venv); os.IsNotExist(err) { + logf("Creating Python %s venv at %s ...", pythonVer, venv) + if err := runCmd(stdout, stderr, "uv", "venv", venv, "--python", pythonVer, "--seed"); err != nil { + return fail("venv creation failed: %w", err) + } + } else { + logf("Venv already exists at %s", venv) + } + + // 4. Install docling-serve + pkg := "docling-serve" + if extras != "" { + pkg = "docling-serve[" + extras + "]" + } + logf("Installing %s ...", pkg) + pythonBin := filepath.Join(venv, "bin", "python") + if err := runCmd(stdout, stderr, "uv", "pip", "install", "--python", pythonBin, pkg); err != nil { + return fail("docling-serve installation failed: %w", err) + } + + // 5. Verify + doclingBin := filepath.Join(venv, "bin", "docling-serve") + if _, err := os.Stat(doclingBin); err != nil { + return fail("docling-serve not found at %s after install", doclingBin) + } + + logf("Setup complete.") + fmt.Fprintln(stdout, "") + fmt.Fprintf(stdout, " Runtime: %s\n", root) + fmt.Fprintf(stdout, " Venv: %s\n", venv) + fmt.Fprintf(stdout, " Docling: %s\n", doclingBin) + fmt.Fprintln(stdout, "") + fmt.Fprintln(stdout, " seshat auto-starts docling on launch when the venv is present.") + fmt.Fprintln(stdout, " Run: seshat chat") + fmt.Fprintln(stdout, "") + return nil +} + +// installUV downloads and runs the official uv installer. +func installUV(stdout, stderr io.Writer) error { + // Try curl first, then wget. + var cmd *exec.Cmd + if _, err := exec.LookPath("curl"); err == nil { + cmd = exec.Command("sh", "-c", "curl -LsSf https://astral.sh/uv/install.sh | sh") + } else if _, err := exec.LookPath("wget"); err == nil { + cmd = exec.Command("sh", "-c", "wget -qO- https://astral.sh/uv/install.sh | sh") + } else { + return fmt.Errorf("neither curl nor wget found; install uv manually: https://docs.astral.sh/uv/getting-started/installation/") + } + cmd.Stdout = stdout + cmd.Stderr = stderr + return cmd.Run() +} + +// runCmd runs a command streaming output to stdout/stderr. +func runCmd(stdout, stderr io.Writer, name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Stdout = stdout + cmd.Stderr = stderr + return cmd.Run() +} + +// cmdOutput runs a command and returns its combined output. +func cmdOutput(name string, args ...string) (string, error) { + out, err := exec.Command(name, args...).CombinedOutput() + return string(out), err +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..9b230b8 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# install.sh — Seshat end-user installer +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/EngineerProjects/seshat/main/scripts/install.sh | bash +# +# Options (env vars): +# VERSION=v0.1.0 Install a specific version (default: latest) +# INSTALL_DIR=... Binary destination (default: ~/.local/bin) +# NO_PYTHON=1 Skip uv + docling-serve setup +# DOCLING_EXTRAS=gpu Install docling-serve[gpu] instead of the base package +# PYTHON_VERSION=3.12 Python version for the docling venv (default: 3.11) +# +# Developer / SDK usage (no installer needed): +# go install github.com/EngineerProjects/seshat/cmd/cli@latest +# go get github.com/EngineerProjects/seshat@latest # in your go.mod + +set -euo pipefail + +REPO="EngineerProjects/seshat" +BINARY="seshat" +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" +NO_PYTHON="${NO_PYTHON:-0}" +PYTHON_VERSION="${PYTHON_VERSION:-3.11}" +DOCLING_EXTRAS="${DOCLING_EXTRAS:-}" + +# ── Colors ──────────────────────────────────────────────────────────────────── +if [ -t 1 ]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' + BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; BLUE=''; BOLD=''; NC='' +fi + +info() { echo -e "${BLUE}[seshat]${NC} $*"; } +success() { echo -e "${GREEN}[seshat]${NC} $*"; } +warn() { echo -e "${YELLOW}[seshat]${NC} $*"; } +error() { echo -e "${RED}[seshat]${NC} $*" >&2; } +step() { echo -e "\n${BOLD}▸ $*${NC}"; } + +# ── Runtime root (mirrors Go DefaultConfigDir logic) ────────────────────────── +_runtime_root() { + if [ -n "${SESHAT_RUNTIME_ROOT:-}" ]; then echo "$SESHAT_RUNTIME_ROOT"; return; fi + if [ -n "${XDG_CONFIG_HOME:-}" ]; then echo "$XDG_CONFIG_HOME/seshat-cli"; return; fi + echo "$HOME/.config/seshat-cli" +} +RUNTIME_ROOT="$(_runtime_root)" + +# ── Detect OS / arch ────────────────────────────────────────────────────────── +step "Detecting platform" + +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "$OS" in + Linux) os="linux" ;; + Darwin) os="darwin" ;; + *) + error "Unsupported OS: $OS" + error "Download a binary manually: https://github.com/$REPO/releases" + exit 1 + ;; +esac + +case "$ARCH" in + x86_64|amd64) arch="amd64" ;; + arm64|aarch64) arch="arm64" ;; + *) + error "Unsupported architecture: $ARCH" + exit 1 + ;; +esac + +SUFFIX="${os}-${arch}" +info "Platform: $SUFFIX" + +# ── Resolve version ─────────────────────────────────────────────────────────── +step "Resolving version" + +if [ -z "${VERSION:-}" ]; then + info "Fetching latest release..." + _dl() { command -v curl &>/dev/null && curl -fsSL "$1" || wget -qO- "$1"; } + VERSION="$(_dl "https://api.github.com/repos/$REPO/releases/latest" \ + | grep '"tag_name"' | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')" +fi + +[ -z "$VERSION" ] && { error "Could not resolve version. Set VERSION=vX.Y.Z explicitly."; exit 1; } +info "Version: $VERSION" + +# ── Download binary ─────────────────────────────────────────────────────────── +step "Downloading seshat $VERSION" + +BASE_URL="https://github.com/$REPO/releases/download/$VERSION" +BIN_ASSET="${BINARY}-${SUFFIX}" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +_dl() { command -v curl &>/dev/null && curl -fsSL "$1" -o "$2" || wget -qO "$2" "$1"; } + +info "Binary: $BIN_ASSET" +_dl "$BASE_URL/$BIN_ASSET" "$TMP_DIR/$BINARY" +_dl "$BASE_URL/SHA256SUMS.txt" "$TMP_DIR/SHA256SUMS.txt" + +# ── Verify checksum ─────────────────────────────────────────────────────────── +step "Verifying checksum" + +(cd "$TMP_DIR" && grep "$BIN_ASSET" SHA256SUMS.txt | sha256sum --check --status) \ + || { error "Checksum verification failed — aborting."; exit 1; } +success "Checksum OK" + +# ── Install binary ──────────────────────────────────────────────────────────── +step "Installing binary" + +mkdir -p "$INSTALL_DIR" +chmod +x "$TMP_DIR/$BINARY" +mv "$TMP_DIR/$BINARY" "$INSTALL_DIR/$BINARY" +success "Installed: $INSTALL_DIR/$BINARY" + +# ── Add to PATH in shell profile ────────────────────────────────────────────── +step "Configuring PATH" + +_add_to_path() { + local profile="$1" + local line='export PATH="$HOME/.local/bin:$PATH"' + if [ -f "$profile" ] && grep -qF '.local/bin' "$profile"; then + info "$profile already exports ~/.local/bin — skipping" + else + echo "" >> "$profile" + echo "# Added by seshat installer" >> "$profile" + echo "$line" >> "$profile" + success "Added PATH export to $profile" + fi +} + +case ":$PATH:" in + *":$INSTALL_DIR:"*) + success "$INSTALL_DIR already in PATH" + RELOAD_NEEDED=0 + ;; + *) + warn "$INSTALL_DIR not in PATH — adding to shell profile" + RELOAD_NEEDED=1 + + # Fish shell + if [ -n "${FISH_VERSION:-}" ] || echo "${SHELL:-}" | grep -q fish; then + FISH_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/fish" + mkdir -p "$FISH_DIR" + FISH_CONF="$FISH_DIR/config.fish" + if grep -qF '.local/bin' "$FISH_CONF" 2>/dev/null; then + info "$FISH_CONF already exports ~/.local/bin — skipping" + else + echo "" >> "$FISH_CONF" + echo "# Added by seshat installer" >> "$FISH_CONF" + echo 'fish_add_path $HOME/.local/bin' >> "$FISH_CONF" + success "Added PATH to $FISH_CONF" + fi + else + # Bash + [ -f "$HOME/.bashrc" ] && _add_to_path "$HOME/.bashrc" + # Zsh + [ -f "$HOME/.zshrc" ] && _add_to_path "$HOME/.zshrc" + # Fallback: .profile + if [ ! -f "$HOME/.bashrc" ] && [ ! -f "$HOME/.zshrc" ]; then + _add_to_path "$HOME/.profile" + fi + fi + ;; +esac + +# ── Python / docling setup via seshat setup ────────────────────────────────── +# Use the freshly installed binary so the logic lives in one place. +SESHAT_BIN="$INSTALL_DIR/seshat" +export PATH="$INSTALL_DIR:$HOME/.local/bin:$HOME/.cargo/bin:$PATH" + +if [ "$NO_PYTHON" = "1" ]; then + warn "Skipping Python setup (NO_PYTHON=1)" + warn "Run 'seshat setup' later to enable document processing." +else + step "Setting up Python environment (uv + docling-serve)" + + SETUP_ARGS="" + [ -n "$PYTHON_VERSION" ] && SETUP_ARGS="$SETUP_ARGS --python $PYTHON_VERSION" + [ -n "$DOCLING_EXTRAS" ] && SETUP_ARGS="$SETUP_ARGS --extras $DOCLING_EXTRAS" + + # shellcheck disable=SC2086 + SESHAT_RUNTIME_ROOT="$RUNTIME_ROOT" "$SESHAT_BIN" setup $SETUP_ARGS +fi + +# ── Done ────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${GREEN}${BOLD}✓ Seshat $VERSION installed successfully${NC}" +echo "" +echo " Binary: $INSTALL_DIR/seshat" +echo " Runtime: $RUNTIME_ROOT" +echo " (DB + sessions are created automatically on first run)" +echo "" + +if [ "${RELOAD_NEEDED:-0}" = "1" ]; then + echo -e "${YELLOW} Reload your shell to activate PATH:${NC}" + echo " source ~/.bashrc # or ~/.zshrc / open a new terminal" + echo "" +fi + +echo " Get started:" +echo " seshat config # configure your AI provider + API key" +echo " seshat chat # start chatting" +echo "" +echo " Docs: https://github.com/$REPO" +echo ""