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
17 changes: 12 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ jobs:
run: uv sync --dev

- name: Run tests
run: uv run pytest --cov=gobby --cov-report=xml --cov-report=term-missing --cov-fail-under=80 --deselect tests/hooks/test_hooks_context.py::test_session_start_context_injection
run: uv run pytest --cov=gobby --cov-report=xml --cov-report=term-missing --cov-fail-under=80 --ignore=tests/voice --ignore=tests/servers/routes/test_voice_routes.py --deselect tests/hooks/test_hooks_context.py::test_session_start_context_injection

- name: Upload coverage to Codecov
if: matrix.python-version == '3.13'
Expand Down Expand Up @@ -241,10 +241,17 @@ jobs:
- name: Check package contents
run: |
uv run python -c "
import tarfile, glob
files = glob.glob('dist/gobby-*.tar.gz')
if not files:
import glob, tarfile, zipfile
sdists = glob.glob('dist/gobby-*.tar.gz')
wheels = glob.glob('dist/gobby-*.whl')
if not sdists:
raise FileNotFoundError('No dist/gobby-*.tar.gz found')
t = tarfile.open(files[0])
if not wheels:
raise FileNotFoundError('No dist/gobby-*.whl found')
t = tarfile.open(sdists[0])
print('\\n'.join(t.getnames()[:20]))
with zipfile.ZipFile(wheels[0]) as wheel:
names = set(wheel.namelist())
if 'gobby/ui/web/dist/index.html' not in names:
raise FileNotFoundError('Wheel missing gobby/ui/web/dist/index.html')
"
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: uv sync --dev

- name: Run tests
run: uv run pytest --deselect tests/hooks/test_hooks_context.py::test_session_start_context_injection
run: uv run pytest --ignore=tests/voice --ignore=tests/servers/routes/test_voice_routes.py --deselect tests/hooks/test_hooks_context.py::test_session_start_context_injection

build:
name: Build package
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,6 @@ package-lock.json

# MCP config (contains machine-specific absolute paths)
.mcp.json

# Built web UI assets staged into the wheel by build_backend (regenerated each build)
src/gobby/ui/
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,42 @@ All notable changes to Gobby are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.2]

A patch release focused on packaging correctness for installed wheels, custom
embedding endpoints, and cleanup of early 0.4.x install rough edges.

### Added

- Add `gobby install --embedding-url`, `--embedding-model`, and
`--embedding-dim` support for custom OpenAI-compatible embedding endpoints,
including interactive install prompts and automatic dimension probing (#9,
#14514).

### Fixed

- Ship the built web UI inside published wheels and serve installed-wheel UI
traffic from the daemon port at `http://localhost:60887/`, while keeping
`:60889` documented as the frontend development server port (#10, #14512,
#14515).
- Verify built wheels contain `gobby/ui/web/dist/index.html` and include the
custom PEP 517 build backend in sdists so wheel-from-sdist builds cannot
silently lose the production UI (#10, #14515).
- Honor explicit custom embedding URLs even when no local LM Studio or Ollama
service is detected, infer Ollama-compatible endpoints on `:11434`, and skip
bundled local setup when users provide their own endpoint or model (#9,
#14515).
- Rename the global MCP server config from `~/.gobby/.mcp.json` to
`~/.gobby/mcp-servers.json` with idempotent migration, avoiding collisions
with Claude Code's project `.mcp.json` schema (#11, #14513).
- Keep main pytest jobs from pulling the heavy voice extra path while preserving
dedicated voice validation coverage (#14511).

### Release

- Bump the package and `src/gobby/__init__.py` version to 0.4.2 and regenerate
the workspace lockfile entry (#14510).

## [0.4.1]

A patch release on top of `0.4.0` focused on daemon stability, runner
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include build_backend/__init__.py
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ have are missing.
## What Gobby is

A Python 3.13+ daemon you run locally. SQLite at `~/.gobby/gobby-hub.db`.
HTTP on `:60887`, WebSocket on `:60888`, web UI on `:60889`, stdio MCP server
that your coding CLIs talk to.
HTTP and the installed web UI on `:60887`, WebSocket on `:60888`, dev web UI
on `:60889`, stdio MCP server that your coding CLIs talk to.

Three things make Gobby load-bearing:

Expand Down Expand Up @@ -225,7 +225,8 @@ Full release notes: [CHANGELOG.md](CHANGELOG.md).

- Python 3.13+ daemon (`uv` for everything)
- SQLite at `~/.gobby/gobby-hub.db`
- HTTP API on `localhost:60887`, WebSocket on `:60888`, web UI on `:60889`
- HTTP API and installed web UI on `localhost:60887`, WebSocket on `:60888`,
dev web UI on `:60889`
- stdio MCP server for coding assistants
- Hook adapters for Claude Code, Codex, Gemini CLI, Qwen CLI, Factory Droid
- Optional Qdrant + FalkorDB for vector and graph-backed search
Expand Down Expand Up @@ -318,7 +319,8 @@ gobby init # initialize .gobby/ for this repo
}
```

Open the local web UI at `http://localhost:60889` once the daemon is running.
Open the installed web UI at `http://localhost:60887/` once the daemon is running.
The `:60889` UI port is for `gobby ui dev` during frontend development.

Then either start interactive work in your CLI of choice — Gobby will track it
quietly — or hand it a task and let the build loop run:
Expand Down
127 changes: 127 additions & 0 deletions build_backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""PEP 517 build backend wrapper.

Wraps setuptools.build_meta to stage the web UI into the wheel.

Before every wheel/sdist build:
1. Honor ``GOBBY_SKIP_UI_BUILD=1`` to skip npm, while still requiring staged
UI assets for wheel builds.
2. If ``web/`` has ``package.json`` and ``npm`` is on PATH, run
``npm ci && npm run build`` in ``web/`` to produce ``web/dist/``.
3. Copy ``web/dist/`` into ``src/gobby/ui/web/dist/`` so the
``ui/web/dist/**/*`` package-data glob picks the assets up.
4. Verify built wheels contain ``gobby/ui/web/dist/index.html`` so release
artifacts cannot silently ship without the production UI.

Editable installs skip the UI build entirely; the dev workflow uses
``gobby ui dev`` against ``web/`` directly.
"""

from __future__ import annotations

import logging
import os
import shutil
import subprocess # nosec B404
import zipfile
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)


def _orig() -> Any:
"""Lazy-import setuptools.build_meta.

Kept lazy so importing this module (e.g., from tests that only exercise
`_stage_ui`) does not require setuptools to be installed at runtime.
"""
from setuptools import build_meta

return build_meta


def __getattr__(name: str) -> Any:
"""Forward PEP 517 hook attributes to setuptools.build_meta on first access."""
if name in {
"get_requires_for_build_wheel",
"get_requires_for_build_sdist",
"get_requires_for_build_editable",
"prepare_metadata_for_build_wheel",
"prepare_metadata_for_build_editable",
}:
return getattr(_orig(), name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


_REPO_ROOT = Path(__file__).resolve().parent.parent
_WEB_SRC = _REPO_ROOT / "web"
_DIST_SRC = _WEB_SRC / "dist"
_WHEEL_DEST = _REPO_ROOT / "src" / "gobby" / "ui" / "web" / "dist"
_WHEEL_UI_INDEX = "gobby/ui/web/dist/index.html"


def _stage_ui() -> None:
if os.environ.get("GOBBY_SKIP_UI_BUILD") == "1":
logger.info("GOBBY_SKIP_UI_BUILD=1 - skipping UI build step")
return

have_source = (_WEB_SRC / "package.json").exists()
have_npm = shutil.which("npm") is not None

if have_source and have_npm:
logger.info("Building web UI in %s", _WEB_SRC)
subprocess.run(["npm", "ci"], cwd=_WEB_SRC, check=True) # nosec B603 B607
subprocess.run(["npm", "run", "build"], cwd=_WEB_SRC, check=True) # nosec B603 B607
Comment on lines +73 to +74
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add timeouts to npm subprocess calls in _stage_ui().

These build-critical external calls currently have no timeout, so a network/npm stall can block CI/release indefinitely.

Suggested fix
-        subprocess.run(["npm", "ci"], cwd=_WEB_SRC, check=True)  # nosec B603 B607
-        subprocess.run(["npm", "run", "build"], cwd=_WEB_SRC, check=True)  # nosec B603 B607
+        subprocess.run(["npm", "ci"], cwd=_WEB_SRC, check=True, timeout=600)  # nosec B603 B607
+        subprocess.run(
+            ["npm", "run", "build"], cwd=_WEB_SRC, check=True, timeout=600
+        )  # nosec B603 B607
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
subprocess.run(["npm", "ci"], cwd=_WEB_SRC, check=True) # nosec B603 B607
subprocess.run(["npm", "run", "build"], cwd=_WEB_SRC, check=True) # nosec B603 B607
subprocess.run(["npm", "ci"], cwd=_WEB_SRC, check=True, timeout=600) # nosec B603 B607
subprocess.run(
["npm", "run", "build"], cwd=_WEB_SRC, check=True, timeout=600
) # nosec B603 B607
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build_backend/__init__.py` around lines 73 - 74, In _stage_ui(), the two
subprocess.run calls that invoke npm (the lines calling subprocess.run(["npm",
"ci"], ...) and subprocess.run(["npm", "run", "build"], ...)) lack timeouts; add
a timeout argument (e.g. timeout=600) to each call and wrap them in a try/except
that catches subprocess.TimeoutExpired to log or raise a clear error (propagate
a RuntimeError or re-raise with context) so CI won't hang indefinitely if npm
stalls. Ensure you update both calls in the _stage_ui() function and include
contextual information (command and timeout) in the error handling.


if not _DIST_SRC.exists():
if _WHEEL_DEST.exists():
logger.info("web/dist not available; reusing pre-staged %s", _WHEEL_DEST)
return
logger.warning(
"web/dist not found and npm build not possible - wheel UI asset verification will fail."
)
return

if _WHEEL_DEST.exists():
shutil.rmtree(_WHEEL_DEST)
_WHEEL_DEST.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(_DIST_SRC, _WHEEL_DEST)
logger.info("Staged web UI assets at %s", _WHEEL_DEST)


def _verify_wheel_contains_ui(wheel_path: Path) -> None:
with zipfile.ZipFile(wheel_path) as wheel:
if _WHEEL_UI_INDEX not in wheel.namelist():
raise RuntimeError(
f"Built wheel is missing {_WHEEL_UI_INDEX}; build web/dist before publishing."
)


def build_wheel(
wheel_directory: str,
config_settings: dict[str, Any] | None = None,
metadata_directory: str | None = None,
) -> str:
_stage_ui()
wheel_name = str(_orig().build_wheel(wheel_directory, config_settings, metadata_directory))
wheel_path = Path(wheel_name)
if not wheel_path.is_absolute():
wheel_path = Path(wheel_directory) / wheel_path
_verify_wheel_contains_ui(wheel_path)
return wheel_name


def build_sdist(
sdist_directory: str,
config_settings: dict[str, Any] | None = None,
) -> str:
_stage_ui()
return str(_orig().build_sdist(sdist_directory, config_settings))


def build_editable(
wheel_directory: str,
config_settings: dict[str, Any] | None = None,
metadata_directory: str | None = None,
) -> str:
return str(_orig().build_editable(wheel_directory, config_settings, metadata_directory))
24 changes: 21 additions & 3 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ flowchart TB
| --- | --- | --- |
| `~/.gobby/bootstrap.yaml` | Machine | Startup values needed before SQLite is open |
| `config_store` table in `~/.gobby/gobby-hub.db` | Machine | Runtime daemon settings and user overrides |
| `~/.gobby/.mcp.json` | Machine | Persistent downstream MCP server registry |
| `~/.gobby/mcp-servers.json` | Machine | Persistent downstream MCP server registry |
| `.gobby/project.json` | Project | Project identity, verification commands, and project hook settings |
| `~/.gobby/build.yaml` | Machine | Build lifecycle defaults |
| `<project>/.gobby/build.yaml` | Project | Build lifecycle defaults for one repository |
Expand Down Expand Up @@ -227,6 +227,24 @@ embeddings:
api_base: null
api_key: null

`gobby install` accepts `--embedding-url`, `--embedding-model`, and
`--embedding-dim` to override the bundled provider defaults. When
`--embedding-dim` is omitted alongside a custom URL or model, the installer
probes `/v1/embeddings` on the target endpoint to detect the dim. The setup
wizard exposes the same three knobs interactively.

The default is `nomic-embed-text-v1.5@f16` (768-dim, ~137M params) — a safe
choice for any local hardware. For users with capable local hardware,
`Qwen3-Embedding-4B` (2560-dim, 4B params) is recommended: it is significantly
stronger on MTEB and instruction-aware. Tradeoffs: roughly 3.3× the vector
storage and a slower embed step. Example install:

```bash
gobby install --embedding-url http://localhost:1234/v1 \
--embedding-model text-embedding-qwen3-embedding-4b
# --embedding-dim is auto-detected from the endpoint; pass 2560 to skip the probe.
```

memory:
enabled: true
backend: local
Expand Down Expand Up @@ -374,7 +392,7 @@ entries with policy values `auto`, `approve_once`, or `always_ask`.

## MCP Server Registry

Downstream MCP servers are stored in `~/.gobby/.mcp.json` and synchronized into
Downstream MCP servers are stored in `~/.gobby/mcp-servers.json` and synchronized into
daemon state by the MCP manager. The file has a top-level `servers` array:

```json
Expand Down Expand Up @@ -516,7 +534,7 @@ secret-like key.

### MCP Server Does Not Connect

Check `~/.gobby/.mcp.json` for the server entry, transport-specific fields, and
Check `~/.gobby/mcp-servers.json` for the server entry, transport-specific fields, and
`enabled: true`. For generated or imported servers, refresh the MCP registry
after changing server definitions.

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ curl -sS http://localhost:60887/api/admin/status
Open dashboard and traces:

```text
http://localhost:60889/#dashboard
http://localhost:60889/#traces
http://localhost:60887/#dashboard
http://localhost:60887/#traces
```

Fetch Prometheus metrics:
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/providers-and-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ curl -sS http://localhost:60887/api/providers/models
Open web chat and use the provider/model controls:

```text
http://localhost:60889/#chat
http://localhost:60887/#chat
```

Inspect configured defaults in the Gobby configuration files and through the
Expand Down
9 changes: 5 additions & 4 deletions docs/guides/system-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Use this guide to decide what has to be installed before running Gobby 0.4.0.
| Setup | Required | Good default |
|-------|----------|--------------|
| Daemon only | Python 3.13+, `uv`, 1 GB free disk, localhost ports 60887 and 60888 | 4+ CPU cores, 8 GB RAM |
| Daemon + web UI | Daemon requirements plus localhost port 60889 | 8 GB RAM |
| Daemon + web UI | Daemon requirements; installed UI is served on port 60887 | 8 GB RAM |
| Full local search stack | Docker with Compose v2, Qdrant, Neo4j, embedding endpoint | 16 GB RAM minimum, 32 GB RAM preferred, SSD/NVMe storage |
| Local embedding model | LM Studio with `lms` or Ollama with `ollama` | 16 GB RAM, GPU or unified memory when also running a chat model |

Expand Down Expand Up @@ -53,7 +53,8 @@ server, and optional UI dev server.
| Database | `~/.gobby/gobby-hub.db` by default |
| HTTP API | `localhost:60887` by default |
| WebSocket | `localhost:60888` by default |
| Web UI | `localhost:60889` by default |
| Installed Web UI | `localhost:60887` by default |
| Dev Web UI | `localhost:60889` when `gobby ui dev` is running |
| Bind host | `localhost` by default |
| Disk | 1 GB for code/dependencies plus SQLite database and transcript growth |
| RAM | Hundreds of MB idle; plan 1-2 GB when many sessions, hooks, or MCP calls are active |
Expand Down Expand Up @@ -134,9 +135,9 @@ Default ports are chosen to avoid common development-server conflicts.

| Port | Owner |
|------|-------|
| 60887 | Gobby HTTP API |
| 60887 | Gobby HTTP API and installed web UI |
| 60888 | Gobby WebSocket server |
| 60889 | Gobby web UI |
| 60889 | Gobby dev web UI |
| 6333 | Qdrant HTTP |
| 6334 | Qdrant gRPC |
| 8474 | Neo4j HTTP, mapped from container port 7474 |
Expand Down
16 changes: 8 additions & 8 deletions docs/guides/web-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ configuration pages, and the operational dashboard.

## Mental Model

Gobby runs the React app from `web/` beside the daemon HTTP and WebSocket
services. In the default local development layout:
Production installs serve the built React app from the daemon HTTP port. The
frontend development server uses a separate port when `gobby ui dev` is running.

- Web UI: `http://localhost:60889`
- HTTP API: `http://localhost:60887`
- Installed Web UI and HTTP API: `http://localhost:60887`
- Dev Web UI: `http://localhost:60889`
- WebSocket API: `ws://localhost:60888`
- Tailscale UI URL, when enabled: shown by `gobby status`

Expand Down Expand Up @@ -40,7 +40,7 @@ uv run gobby status
Open the web app:

```text
http://localhost:60889/#chat
http://localhost:60887/#chat
```

Check the backend directly when the UI appears disconnected:
Expand Down Expand Up @@ -145,9 +145,9 @@ service health.

## HTTP

The browser normally calls the web origin on `:60889`; the web server proxies
API routes to the daemon services. Direct API debugging can use the HTTP daemon
port from `gobby status`.
The installed browser app normally calls the daemon origin on `:60887`. During
frontend development, the `:60889` dev server proxies API routes to the daemon
services. Direct API debugging can use the HTTP daemon port from `gobby status`.

Useful checks:

Expand Down
Loading
Loading