diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4fe5b91..1804b01 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug report -description: Something in bytebell-server or the bytebell CLI is broken or behaving unexpectedly. +description: Something in plumbline-server or the plumbline CLI is broken or behaving unexpectedly. title: "[bug] " labels: ["bug", "needs-triage"] body: @@ -8,7 +8,7 @@ body: value: | Thanks for taking the time to file a bug! A few quick tips: - Please redact API keys, tokens, and private repo URLs from anything you paste. - - Server and CLI logs live in `~/.bytebell/logs/`. + - Server and CLI logs live in `~/.plumbline/logs/`. - Only the first four fields are required — fill in the rest if you have it handy. - type: textarea @@ -16,7 +16,7 @@ body: attributes: label: What happened? description: A short description of the bug. What did you do, and what went wrong? - placeholder: I ran `bytebell index https://github.com/...` and the worker stalled at PROCESSING. + placeholder: I ran `plumbline index https://github.com/...` and the worker stalled at PROCESSING. validations: required: true @@ -26,8 +26,8 @@ body: label: Steps to reproduce description: The minimum sequence of commands or clicks needed to see the bug. placeholder: | - 1. `bytebell boot` - 2. `bytebell index ` + 1. `plumbline boot` + 2. `plumbline index ` 3. Wait ~2 minutes validations: required: true @@ -42,8 +42,8 @@ body: - type: input id: version attributes: - label: Bytebell version - description: Output of `bytebell --version`. + label: Plumbline version + description: Output of `plumbline --version`. placeholder: e.g. 0.4.2 validations: required: true @@ -51,16 +51,16 @@ body: - type: dropdown id: component attributes: - label: Which part of Bytebell is affected? + label: Which part of Plumbline is affected? description: Pick whatever feels closest — "Not sure" is fine. multiple: true options: - "Not sure" - - "bytebell-server (HTTP / MCP / workers)" - - "bytebell CLI / TUI" + - "plumbline-server (HTTP / MCP / workers)" + - "plumbline CLI / TUI" - "Ingestion (@bb/ingest-github)" - "MCP surface (@bb/mcp)" - - "Adapter (@bb/mongo / @bb/neo4j / @bb/redis)" + - "Adapter (@bb/sqlite / @bb/neo4j)" - "LLM layer (@bb/llm)" - "Config / first-run setup (@bb/config)" - "Docs / README" @@ -90,7 +90,7 @@ body: id: logs attributes: label: Logs or error output - description: Paste relevant lines from `~/.bytebell/logs/server-YYYY-MM-DD.log` or `cli-YYYY-MM-DD.log`. Redact secrets. + description: Paste relevant lines from `~/.plumbline/logs/server-YYYY-MM-DD.log` or `cli-YYYY-MM-DD.log`. Redact secrets. render: shell - type: textarea diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 0a8dcf4..8f9ab4a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,5 +1,5 @@ name: Feature request -description: Suggest a new capability or improvement for Bytebell. +description: Suggest a new capability or improvement for Plumbline. title: "[feat] " labels: ["enhancement", "needs-triage"] body: diff --git a/.github/banner-dark.svg b/.github/banner-dark.svg new file mode 100644 index 0000000..2a10320 --- /dev/null +++ b/.github/banner-dark.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + BYTEBELL + PLUMBLINE + + Local-first code intelligence + every file analyzed → knowledge graph → served over MCP + nothing leaves 127.0.0.1 + + + + + + + PROJECT + PLUMBLINE + GRAPH + NEO4J + MONGO + SERVES + MCP / STDIO + HTTP + BIND + 127.0.0.1 LOCAL ONLY + + + + TRUE VERTICAL + + + + + + + + + + + + + + + + + + + + + + + + + + + + + smart_search + keyword_lookup + retrieve_file + + diff --git a/.github/banner-light.svg b/.github/banner-light.svg new file mode 100644 index 0000000..b029e98 --- /dev/null +++ b/.github/banner-light.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + BYTEBELL + PLUMBLINE + + Local-first code intelligence + every file analyzed → knowledge graph → served over MCP + nothing leaves 127.0.0.1 + + + + + + + + PROJECT + PLUMBLINE + GRAPH + NEO4J + MONGO + SERVES + MCP / STDIO + HTTP + BIND + 127.0.0.1 LOCAL ONLY + + + + + TRUE VERTICAL + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + smart_search + keyword_lookup + retrieve_file + + diff --git a/.gitleaks.toml b/.gitleaks.toml index eb97eaf..ccf5ef4 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,4 +1,4 @@ -title = "Bytebell-public gitleaks config" +title = "Plumbline-public gitleaks config" [extend] useDefault = true diff --git a/CHANGELOG.md b/CHANGELOG.md index cc51696..146bc8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,50 @@ # Changelog -All notable changes to Bytebell 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). +All notable changes to Plumbline 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). ## [Unreleased] +### Changed + +- **The document store is now SQLite.** MongoDB is removed: the `mongo` container is + gone from `infra/docker/docker-compose.yml`, and `~/.plumbline/data.sqlite` holds the + knowledge documents, raw file rows, activity, usage, and the concept-graph enrichment + ledger. +- Existing installs upgrade in place: a `config.json` carrying `db_provider: "mongo"` is + migrated to `sqlite` on read, and the retired `mongo_uri` key is dropped from the file + on the next config write. **Data is not migrated** — repositories indexed against + MongoDB must be re-indexed. +- **The job queue is now SQLite too.** Redis and BullMQ are removed: the job queue is + Honker over SQLite (`~/.plumbline/queue.db`) in every configuration, so the `redis` + container is gone from `infra/docker/docker-compose.yml`. A stored + `queue_provider: "bullmq"` migrates to `honker` on read, and the retired `redis_url` + key is dropped from `config.json`. **In-flight jobs are not migrated** — drain or + re-submit anything queued before the upgrade. +- Neo4j is now the only container `plumbline boot` starts. +- `DELETE /api/v1/repos/` renames its `mongoDeleted` response field to + `dbDeleted`. + +### Removed + +- The `@bb/mongo` package, the `mongo_uri` config key, and the `mongo` value of + `db_provider`. +- The `@bb/redis` and `@bb/queue-bullmq` packages, the `redis_url` config key, the + `bullmq` value of `queue_provider`, and the `Redis*` error classes. + ## [0.1.0] — 2026-05-08 ### Added - Initial public release. -- `bytebell-server` HTTP daemon (Express 5) with ingestion routes (`/api/v1/...`) and MCP transport (`/mcp`, HTTP + SSE). -- `bytebell` CLI (Ink/React TUI + commander) with subcommands: `boot`, `index`, `ingest`, `pull`, `ls`, `delete`, `set`, `server`, `shutdown`, `stats`, `mcp`. +- `plumbline-server` HTTP daemon (Express 5) with ingestion routes (`/api/v1/...`) and MCP transport (`/mcp`, HTTP + SSE). +- `plumbline` CLI (Ink/React TUI + commander) with subcommands: `boot`, `index`, `ingest`, `pull`, `ls`, `delete`, `set`, `server`, `shutdown`, `stats`, `mcp`. - GitHub repository ingestion via `BasicFileAnalysisStrategy` (file-walk + per-file LLM analysis). - MCP retrieval tools: `smart_search`, `keyword_lookup`, `retrieve_file` . -- Token-usage telemetry persisted to MongoDB (`mcp_activity`, `usage_summary`); live USD estimate against OpenRouter pricing via `bytebell stats`. +- Token-usage telemetry persisted to MongoDB (`mcp_activity`, `usage_summary`); live USD estimate against OpenRouter pricing via `plumbline stats`. - Local-first single-tenant architecture (`orgId="local"`); BYO MongoDB + Neo4j + Redis. -- Configuration via `~/.bytebell/config.json` (no `.env`), managed through `bytebell set `. +- Configuration via `~/.plumbline/config.json` (no `.env`), managed through `plumbline set `. - BullMQ in-process workers with retryable, idempotent jobs. -- Winston structured logging to `~/.bytebell/logs/` plus stdout. +- Winston structured logging to `~/.plumbline/logs/` plus stdout. ### License diff --git a/CLAUDE.md b/CLAUDE.md index 794d2dd..cccf40a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,17 +1,17 @@ -# CLAUDE.md — Bytebell-public +# CLAUDE.md — Plumbline-public --- ## Project Summary -**Bytebell-public** is an open-source, single-tenant local knowledge engine. It ingests GitHub repo into a durable knowledge graph and serves them through an MCP retrieval surface — all from a single Bun-based process running on the user's machine. +**Plumbline-public** is an open-source, single-tenant local knowledge engine. It ingests GitHub repo into a durable knowledge graph and serves them through an MCP retrieval surface — all from a single Bun-based process running on the user's machine. It ships two binaries from a single workspace: -- **`bytebell-server`** — a single Express daemon hosting ingestion routes (`/api/v1/...`), the MCP transport (`/mcp`, HTTP + SSE), and BullMQ workers in-process. -- **`bytebell`** — an Ink/React TUI driven by commander subcommands (`boot`, `index`, `ingest`, `ls`, `delete`, `set`, `server`, `shutdown`, `stats`). Interactive only — no `-p` / headless mode. +- **`plumbline-server`** — a single Express daemon hosting ingestion routes (`/api/v1/...`), the MCP transport (`/mcp`, HTTP + SSE), and SQLite-backed queue workers in-process. +- **`plumbline`** — an Ink/React TUI driven by commander subcommands (`boot`, `index`, `ingest`, `ls`, `delete`, `set`, `server`, `shutdown`, `stats`). Interactive only — no `-p` / headless mode. -The system is **BYO-infra** (the user runs Mongo, Neo4j, Redis). Everything is single-tenant with a hardcoded `orgId="local"`. There is no auth, no users, no orgs, and the local server makes no outbound calls except to the user-selected LLM backend (OpenRouter or a user-supplied Ollama URL). +The system is **BYO-infra** (the user runs Neo4j; the document store and job queue are local SQLite files). Everything is single-tenant with a hardcoded `orgId="local"`. There is no auth, no users, no orgs, and the local server makes no outbound calls except to the user-selected LLM backend (OpenRouter or a user-supplied Ollama URL). The repository is licensed under **AGPL-3.0 with an additional non-commercial use clause** — see [LICENSE](LICENSE) at the repo root. Commercial use requires a separate license; there is no in-process license-gating. @@ -22,14 +22,14 @@ Architecturally, it is a **package-first Bun workspace** under `packages/*` with ## High-Level Flow ``` -TUI / HTTP client → Express (bytebell-server) → BullMQ (in-process) → IngestionStrategy → Graph + Storage - ↘ MCP tools → Neo4j / Mongo retrieval +TUI / HTTP client → Express (plumbline-server) → Queue (in-process) → IngestionStrategy → Graph + Storage + ↘ MCP tools → Neo4j / SQLite retrieval ``` -- The CLI never touches Mongo / Neo4j / Redis directly — it only talks HTTP to `bytebell-server`. -- Ingestion is asynchronous via BullMQ. Workers run **inside** the server process; there is no separate worker fleet. -- A worker (e.g. `handleGithubIndex`) clones the repo, runs the active `IngestionStrategy` (today: `BasicFileAnalysisStrategy` — file-walk + per-file LLM analysis), upserts file rows to Mongo + file nodes to Neo4j, and transitions `KnowledgeState`. -- MCP requests dispatch to the same Mongo + Neo4j the ingestion side wrote. +- The CLI never touches SQLite or Neo4j directly — it only talks HTTP to `plumbline-server`. +- Ingestion is asynchronous via the queue. Workers run **inside** the server process; there is no separate worker fleet. +- A worker (e.g. `handleGithubIndex`) clones the repo, runs the active `IngestionStrategy` (today: `BasicFileAnalysisStrategy` — file-walk + per-file LLM analysis), upserts file rows to SQLite + file nodes to Neo4j, and transitions `KnowledgeState`. +- MCP requests dispatch to the same SQLite + Neo4j the ingestion side wrote. --- @@ -39,13 +39,12 @@ TUI / HTTP client → Express (bytebell-server) → BullMQ (in-process) → Inge - **Language**: TypeScript (strict, all flags on — see [tsconfig.base.json](tsconfig.base.json)) - **HTTP server**: Express 5 - **TUI**: Ink (React for terminals) + commander -- **Databases**: MongoDB, Neo4j (BYO — user-supplied URIs) -- **Queue**: BullMQ (Redis-backed, in-process workers) -- **Cache + State**: Redis (BYO) -- **Local persistence**: `~/.bytebell/` (config, logs) +- **Databases**: SQLite (embedded, `~/.plumbline/data.sqlite`), Neo4j (BYO — user-supplied URI) +- **Queue**: Honker (SQLite-backed, in-process workers) +- **Local persistence**: `~/.plumbline/` (config, logs) - **LLM Provider**: OpenRouter (default) or local Ollama, selected via `Config.LlmProvider` - **Logging**: Winston (file + stdout) -- **Secret storage**: plaintext in `~/.bytebell/config.json` (mode `0600`). OS-keychain integration is not implemented. +- **Secret storage**: plaintext in `~/.plumbline/config.json` (mode `0600`). OS-keychain integration is not implemented. - **Package manager**: Bun (workspaces) --- @@ -63,7 +62,7 @@ Strategy queue ↑ Cross-cutting llm ↑ -Infrastructure config, logger, mongo, neo4j, redis +Infrastructure config, logger, db, sqlite, neo4j ↑ Kernel types, errors ``` @@ -78,7 +77,7 @@ Kernel types, errors ### 1. Local-First, Single-Tenant -There is exactly one tenant: `orgId="local"`. A single shim in `@bb/mongo` injects this on every read/write; Neo4j queries always filter on it. Do not add per-tenant logic. Do not add auth middleware. Do not introduce user/org concepts. +There is exactly one tenant: `orgId="local"`. A single shim in the document-store adapter injects this on every read/write; Neo4j queries always filter on it. Do not add per-tenant logic. Do not add auth middleware. Do not introduce user/org concepts. ### 2. One Package, One Responsibility @@ -92,9 +91,9 @@ Each package owns exactly one concern. If a package needs a second name to descr - **Routes** → HTTP shape only (parse + validate + delegate) - **Services** → Business logic + queue submission -- **Workers** → Async job execution (in-process, BullMQ); each worker dispatches to an `IngestionStrategy` -- **Strategies** → How a cloned repo is turned into Mongo rows + Neo4j nodes -- **Adapters** (`@bb/mongo`, `@bb/neo4j`, `@bb/redis`) → External system I/O +- **Workers** → Async job execution (in-process, SQLite-backed queue); each worker dispatches to an `IngestionStrategy` +- **Strategies** → How a cloned repo is turned into SQLite rows + Neo4j nodes +- **Adapters** (`@bb/sqlite`, `@bb/neo4j`) → External system I/O No layer skips another. The TUI is a special case: it is a thin HTTP client over the same routes; it does not reach into adapters. @@ -119,20 +118,20 @@ Ingestion is dispatched through `IngestionStrategy` (`@bb/ingest-github/Strategy ### 8. Observability -- Structured logging via `@bb/logger` (file + stdout, written to `~/.bytebell/logs/`) +- Structured logging via `@bb/logger` (file + stdout, written to `~/.plumbline/logs/`) - Request and job IDs propagate across pipelines -- Health checks for every external system (Mongo / Neo4j / Redis probes) -- Token usage is persisted in Mongo (`mcp_activity`, `usage_summary`) for the `bytebell stats` command +- Health checks for every external system (SQLite / Neo4j probes) +- Token usage is persisted in SQLite (`mcp_activity`, `usage_summary`) for the `plumbline stats` command There is **no outbound telemetry**. The server does not phone home; logs stay on the user's machine. ### 9. Identifiers - Public IDs are UUID v4 -- MongoDB `_id` is internal only +- Row/document primary keys are internal only - UUID fields are indexed and unique - Job IDs are globally traceable -- `install_id` (UUID, generated locally on first run, stored at `~/.bytebell/install_id`) is a stable local identifier used by the CLI dashboard. It is never transmitted off the machine. +- `install_id` (UUID, generated locally on first run, stored at `~/.plumbline/install_id`) is a stable local identifier used by the CLI dashboard. It is never transmitted off the machine. --- @@ -143,20 +142,24 @@ CREATED → QUEUED → INGESTED → PROCESSING → PROCESSED ↘ FAILED ``` -States are explicit, never inferred. Transitions are persisted before the next phase begins. Surfaced via `bytebell ls` and the dashboard's Repos pane. +States are explicit, never inferred. Transitions are persisted before the next phase begins. Surfaced via `plumbline ls` and the dashboard's Repos pane. --- ## Local Config Layout -The `~/.bytebell/` directory is the **single source of truth** for runtime configuration. There is no `.env` file (see Rule of Env Vars). +The `~/.plumbline/` directory is the **single source of truth** for runtime configuration. There is no `.env` file (see Rule of Env Vars). ``` -~/.bytebell/ - config.json server_port, mongo_uri, neo4j_uri/user/password, - redis_url, openrouter_api_key, openrouter_model, - concurrency.github, log_level, log_retention_days +~/.plumbline/ + config.json server_port, sqlite_path, queue_db_path, + neo4j_uri/user/password, openrouter_api_key, + openrouter_model, concurrency.github, log_level, + log_retention_days (mode 0600; openrouter_api_key stored in plaintext) + data.sqlite document store — knowledge docs, raw file rows, + activity, usage, enrichment ledger + queue.db Honker job queue (SQLite) install_id UUID generated on first run (local-only, never transmitted) repos// cloned source trees for every indexed repo logs/ @@ -167,7 +170,7 @@ The `~/.bytebell/` directory is the **single source of truth** for runtime confi There is no OS-keychain integration; `openrouter_api_key` lives in plaintext in `config.json` (mode `0600`). -- `bytebell set ` is the only sanctioned write path to `config.json`. Manual edits work but are not advertised. +- `plumbline set ` is the only sanctioned write path to `config.json`. Manual edits work but are not advertised. --- @@ -258,25 +261,25 @@ The codebase is pure ESM. **No `.env` file. Anywhere. Ever.** -- Every setting lives in `~/.bytebell/config.json` and is written exclusively by `bytebell set …` (or the first-run setup form) -- The server reads `config.json` directly via `@bb/config` and **must refuse to read `process.env.MONGODB_URI`** or any equivalent +- Every setting lives in `~/.plumbline/config.json` and is written exclusively by `plumbline set …` (or the first-run setup form) +- The server reads `config.json` directly via `@bb/config` and **must refuse to read `process.env.NEO4J_URI`** or any equivalent - No `.env.example`, no `dotenv` package as a dependency, no `-env-file` flag ```ts import { getConfigValue, Config } from "@bb/config"; -const url = getConfigValue(Config.MongoUri); +const uri = getConfigValue(Config.Neo4jUri); ``` -If a piece of infra is missing from `config.json`, the server prints the exact `bytebell set …` command and refuses to boot. +If a piece of infra is missing from `config.json`, the server prints the exact `plumbline set …` command and refuses to boot. --- ## Rule of LLM Provider -**OpenRouter or local Ollama. No direct vendor SDKs.** No Anthropic / OpenAI / Gemini / Bedrock keys or SDK imports. The active backend is selected by `Config.LlmProvider` (`"openrouter"` default | `"ollama"`) and switched via `bytebell set llm-provider `. Ollama mode reads `Config.OllamaUrl` (default `http://localhost:11434`) and `Config.OllamaModel` (free-form — any locally-pulled model) and reports `$0` cost. All LLM calls flow through `@bb/llm`, which: +**OpenRouter or local Ollama. No direct vendor SDKs.** No Anthropic / OpenAI / Gemini / Bedrock keys or SDK imports. The active backend is selected by `Config.LlmProvider` (`"openrouter"` default | `"ollama"`) and switched via `plumbline set llm-provider `. Ollama mode reads `Config.OllamaUrl` (default `http://localhost:11434`) and `Config.OllamaModel` (free-form — any locally-pulled model) and reports `$0` cost. All LLM calls flow through `@bb/llm`, which: - Wraps every OpenRouter / Ollama call behind a single `askLLM` surface -- Computes per-call cost via `estimateCostUsd()` against live OpenRouter pricing for the `bytebell stats` view (short-circuits to `0` when provider is Ollama) +- Computes per-call cost via `estimateCostUsd()` against live OpenRouter pricing for the `plumbline stats` view (short-circuits to `0` when provider is Ollama) LLM outputs are probabilistic. They must be: @@ -284,7 +287,7 @@ LLM outputs are probabilistic. They must be: - Normalized before persistence - Never written directly to a domain store -The user-facing model list is curated (5–10 top models). `bytebell models set` validates against OpenRouter on the fly. +The user-facing model list is curated (5–10 top models). `plumbline models set` validates against OpenRouter on the fly. --- @@ -407,7 +410,7 @@ Every package and every major subfolder MUST contain a `README.md`. ## Architecture Philosophy -Bytebell-public is **a local research instrument**, not a hosted service. +Plumbline-public is **a local research instrument**, not a hosted service. It exists so a single developer, an OSS community, or a research team can run a durable knowledge engine on their own infrastructure — turning raw repos into a queryable graph and exposing them through MCP. Everything stays on the user's machine; the engine does not phone home. @@ -418,7 +421,7 @@ Design for: - **Local-first** — no hidden cloud dependencies; the only outbound call is to the user-selected LLM backend (OpenRouter or Ollama) - **Deterministic pipelines** over heuristics - **Recoverability** over performance shortcuts -- **Auditability** — every LLM-derived fact is traceable to its source via structured logs and token-usage records in Mongo +- **Auditability** — every LLM-derived fact is traceable to its source via structured logs and token-usage records in SQLite - **Long-term maintainability** over rapid hacks Prefer: diff --git a/README.md b/README.md index 697c452..631baf5 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,58 @@ -# Bytebell [bytebell.ai] + + + + Plumbline — local-first code intelligence + + +# Plumbline + +**Local-first code intelligence.** Plumbline reads every file in your repository, extracts +what each one is _for_, and stores the result as a queryable knowledge graph your coding +agent searches over MCP — running entirely on your machine, bound to `127.0.0.1`. + +[![License](https://img.shields.io/badge/license-AGPL--3.0%20%2B%20non--commercial-A8762B)](LICENSE) +[![MCP](https://img.shields.io/badge/MCP-streamable%20http%20%2B%20sse-1A211E)](#connect-an-mcp-client) +[![Binds](https://img.shields.io/badge/binds-127.0.0.1%20only-55605B)](#who-this-is-for) + +> **On the name:** Plumbline is the project; `plumbline` is the command it installs. +> Every CLI invocation, container name, and config path below uses `plumbline` — that is +> the real binary, not a typo. + +## The problem + +Coding agents read whole files into the context window. On a real repository that is both +expensive and lossy — the agent spends its budget on files it did not need and still misses +the one that mattered, because nothing told it where to look. + +Plumbline gives it somewhere to look. Every file is analyzed once for its purpose, summary, +business context, classes, functions and keywords. That metadata becomes a Neo4j graph; the +raw content sits in a local SQLite database beside it. Retrieval fuses both — semantic meaning _and_ +structural relationships — so the agent asks a question instead of reading a directory. ## Quickstart -> Looking for the full CLI reference? Every `bytebell` subcommand, flag, and option lives in **[commands.md](commands.md)**. The Quickstart below is the minimum sequence from zero to a queryable graph. +> Looking for the full CLI reference? Every `plumbline` subcommand, flag, and option lives in **[commands.md](commands.md)**. The Quickstart below is the minimum sequence from zero to a queryable graph. ### Prerequisites - [Bun](https://bun.sh) ≥ 1.1 — runtime + workspace manager. -- [Docker](https://www.docker.com/) — for the local Mongo + Neo4j + Redis stack `bytebell boot` brings up. +- [Docker](https://www.docker.com/) — for the local Neo4j container `plumbline boot` brings up. The document store and job queue are both SQLite and need no container. - An LLM backend — either an [OpenRouter](https://openrouter.ai) API key (default) or a local [Ollama](https://ollama.com) model. Every per-file analysis call goes through the one you pick. ### Install -One command — checks prerequisites, clones the repo, installs dependencies, and links the `bytebell` binary: +One command — checks prerequisites, clones the repo, installs dependencies, and links the `plumbline` binary: ```bash -curl -fsSL https://raw.githubusercontent.com/ByteBell/open-ir/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/ByteBell/Plumbline/main/install.sh | bash ``` -Verify with `bytebell --help`. (Manual install steps are in [commands.md](commands.md).) +Verify with `plumbline --help`. (Manual install steps are in [commands.md](commands.md).) -### Fastest path: `bytebell setup` +### Fastest path: `plumbline setup` ```bash -bytebell setup +plumbline setup ``` One interactive command does everything the manual steps below automate: picks your LLM provider, auto-fills and boots the local stack, optionally indexes a repo (handling private-repo tokens and branch selection), and **auto-wires the MCP endpoint into your editor**. See [SETUP.md](SETUP.md) for the full walkthrough. @@ -32,51 +61,51 @@ The sections below are the manual, step-by-step equivalent — useful if you wan ### Configure -Two values Bytebell needs — your OpenRouter API key and model. Set them headlessly: +Two values Plumbline needs — your OpenRouter API key and model. Set them headlessly: ```bash -bytebell set openrouter-api-key sk-or-… -bytebell set openrouter-model anthropic/claude-sonnet-4.6 +plumbline set openrouter-api-key sk-or-… +plumbline set openrouter-model anthropic/claude-sonnet-4.6 ``` -Or skip this step and run `bytebell boot` straight away — on an interactive terminal it opens a setup form to collect these on first run. Running `bytebell set` with no arguments opens the same form at any time. +Or skip this step and run `plumbline boot` straight away — on an interactive terminal it opens a setup form to collect these on first run. Running `plumbline set` with no arguments opens the same form at any time. -There is no `.env` file anywhere. `~/.bytebell/config.json` (mode `0600`) is the single source of truth, and `bytebell set` is the only sanctioned way to write to it. If you already run Mongo / Neo4j / Redis and don't want the Docker stack, see [Bring your own infrastructure](#bring-your-own-infrastructure) below. +There is no `.env` file anywhere. `~/.plumbline/config.json` (mode `0600`) is the single source of truth, and `plumbline set` is the only sanctioned way to write to it. If you already run Neo4j and don't want the Docker stack, see [Bring your own infrastructure](#bring-your-own-infrastructure) below. ### Boot ```bash -bytebell boot +plumbline boot ``` What happens, in order: -1. **Pre-flight check** — verifies both OpenRouter keys are set. If either is blank and you're in an interactive terminal, Bytebell opens a setup form so you can enter them on the spot, then continues. In a non-interactive context (CI, piped input) it prints the exact `bytebell set …` commands and exits. +1. **Pre-flight check** — verifies both OpenRouter keys are set. If either is blank and you're in an interactive terminal, Plumbline opens a setup form so you can enter them on the spot, then continues. In a non-interactive context (CI, piped input) it prints the exact `plumbline set …` commands and exits. 2. **Auto-fill** — fills any missing infra config keys with local-Docker defaults; generates a Neo4j password if one isn't set. -3. **Stack up** — `docker compose up -d` brings up `bytebell-mongo`, `bytebell-neo4j`, `bytebell-redis` (named volumes — data persists across reboots). +3. **Stack up** — `docker compose up -d` brings up `plumbline-neo4j` (a named volume — data persists across reboots). SQLite needs no container; the documents live at `~/.plumbline/data.sqlite` and the queue at `~/.plumbline/queue.db`. 4. **Health gate** — polls `docker compose ps` until all three services report `healthy`. -5. **Server up** — spawns `bytebell-server` (HTTP on `127.0.0.1:8080`, MCP at `/mcp`). +5. **Server up** — spawns `plumbline-server` (HTTP on `127.0.0.1:8080`, MCP at `/mcp`). First boot pulls images and can take a couple of minutes. Subsequent boots are fast. ### Index a repo ```bash -bytebell index https://github.com/anthropics/claude-code +plumbline index https://github.com/anthropics/claude-code # private repo: add --token ; never paste the PAT positionally -bytebell ls # watch state: CREATED → QUEUED → INGESTED → PROCESSING → PROCESSED +plumbline ls # watch state: CREATED → QUEUED → INGESTED → PROCESSING → PROCESSED ``` -When the row reads `PROCESSED`, the graph is fully populated and the MCP tools will return results for that repo. Local directories work too: `bytebell ingest /path/to/source-tree`. +When the row reads `PROCESSED`, the graph is fully populated and the MCP tools will return results for that repo. Local directories work too: `plumbline ingest /path/to/source-tree`. ### Connect an MCP client -Easiest: **`bytebell mcp install`** auto-detects your installed tools — Claude Code, Cursor, Claude Desktop, Windsurf, VS Code — and writes the correct MCP entry into each one's config (the JSON shape differs per tool; the command handles that and backs up the file first). `bytebell setup` runs this for you on first boot. +Easiest: **`plumbline mcp install`** auto-detects your installed tools — Claude Code, Cursor, Claude Desktop, Windsurf, VS Code — and writes the correct MCP entry into each one's config (the JSON shape differs per tool; the command handles that and backs up the file first). `plumbline setup` runs this for you on first boot. To wire Claude Code by hand: ```bash -claude mcp add --transport http bytebell http://127.0.0.1:8080/mcp +claude mcp add --transport http plumbline http://127.0.0.1:8080/mcp ``` Or add this under the `mcpServers` key of Claude Desktop's config (or Cursor's `~/.cursor/mcp.json`): @@ -84,7 +113,7 @@ Or add this under the `mcpServers` key of Claude Desktop's config (or Cursor's ` ```json { "mcpServers": { - "bytebell": { + "plumbline": { "type": "http", "url": "http://127.0.0.1:8080/mcp" } @@ -92,29 +121,29 @@ Or add this under the `mcpServers` key of Claude Desktop's config (or Cursor's ` } ``` -The server registers `smart_search`, `keyword_lookup`, and `retrieve_file`, plus a bundled skill at `bytebell://skills/index` that the client can fetch and install once per session for the recommended workflow. +The server registers `smart_search`, `keyword_lookup`, and `retrieve_file`, plus a bundled skill at `plumbline://skills/index` that the client can fetch and install once per session for the recommended workflow. -## What Bytebell does +## What Plumbline does -You point `bytebell` at a repo. It clones the source, walks every file, and for each file calls an LLM (via OpenRouter) to extract a structured `FileAnalysis`: a one-paragraph **purpose**, a longer **summary** of what the file does and how it fits the architecture, a **business context** line tying it to the product domain, plus the file's classes, functions, keywords, and imports. +You point `plumbline` at a repo. It clones the source, walks every file, and for each file calls an LLM (via OpenRouter) to extract a structured `FileAnalysis`: a one-paragraph **purpose**, a longer **summary** of what the file does and how it fits the architecture, a **business context** line tying it to the product domain, plus the file's classes, functions, keywords, and imports. Those outputs are persisted into two stores: - **Neo4j** receives a `:File` node enriched with `purpose`, `summary`, `businessContext`, `language`, `sha`, and `sizeBytes`, linked via `:HAS_CLASS`, `:HAS_FUNCTION`, `:HAS_KEYWORD`, `:HAS_IMPORT_INTERNAL`, and `:HAS_IMPORT_EXTERNAL` to deduplicated child nodes shared across the whole graph. Fulltext indexes cover purpose+summary, business context, keyword names, and class/function signatures. -- **MongoDB** receives the raw file content, language, SHA256, and the full `FileAnalysis` JSON for cite-back and exact retrieval. +- **SQLite** receives the raw file content, language, SHA256, and the full `FileAnalysis` JSON for cite-back and exact retrieval. It is a single file at `~/.plumbline/data.sqlite` — no server, no container. LLM clients then query that graph through three MCP tools — `smart_search`, `keyword_lookup`, `retrieve_file` — which together cover fused semantic + structural search, reverse entity-to-file lookup, and targeted content reads. They let an agent answer questions like _"Which files implement our retry/backoff policy and where is it configured?"_ without reading the entire repo into context. ```mermaid flowchart LR - CLI["bytebell CLI / TUI"] -- HTTP --> Server["bytebell-server
(Express)"] + CLI["plumbline CLI / TUI"] -- HTTP --> Server["plumbline-server
(Express)"] Client["MCP-capable LLM client
Claude Code, Cursor, …"] -- MCP --> Server - Server -- enqueues --> Q["BullMQ in-process worker"] + Server -- enqueues --> Q["SQLite-backed in-process worker"] Q --> Strategy["IngestionStrategy
per-file LLM"] Strategy -- LLM call --> OR["OpenRouter"] - Strategy -- raw + analysis --> Mongo[("MongoDB")] + Strategy -- raw + analysis --> Sqlite[("SQLite")] Strategy -- enriched node --> Neo[("Neo4j")] - Server -. retrieval .-> Mongo + Server -. retrieval .-> Sqlite Server -. retrieval .-> Neo ``` @@ -130,7 +159,7 @@ It is **not** a hosted product, not a chat UI, and not a multi-tenant platform. ### Ingest -`bytebell index ` (or `bytebell ingest `) submits a job to an in-process BullMQ queue. The worker dispatches to an `IngestionStrategy` — today, `BasicFileAnalysisStrategy` ([packages/ingest-github/src/BasicFileAnalysisStrategy.ts](packages/ingest-github/src/BasicFileAnalysisStrategy.ts)). It clones the repo to `~/.bytebell/repos//`, walks every file, runs a per-file OpenRouter call, and persists raw content to Mongo + the enriched node to Neo4j. +`plumbline index ` (or `plumbline ingest `) submits a job to an in-process SQLite-backed queue. The worker dispatches to an `IngestionStrategy` — today, `BasicFileAnalysisStrategy` ([packages/ingest-github/src/BasicFileAnalysisStrategy.ts](packages/ingest-github/src/BasicFileAnalysisStrategy.ts)). It clones the repo to `~/.plumbline/repos//`, walks every file, runs a per-file OpenRouter call, and persists raw content to SQLite + the enriched node to Neo4j. The per-file LLM call returns a single JSON object with this shape: @@ -147,7 +176,7 @@ The per-file LLM call returns a single JSON object with this shape: } ``` -`classes` and `functions` carry approximate line ranges so `retrieve_file` can later pull the right slice without re-reading the whole file. **Re-indexing is diff-aware**: on `bytebell pull`, the strategy compares each file's SHA256 to the prior `:File.sha` and only re-analyses files whose hash changed. LLM cost is proportional to actual code churn, not to repo size. +`classes` and `functions` carry approximate line ranges so `retrieve_file` can later pull the right slice without re-reading the whole file. **Re-indexing is diff-aware**: on `plumbline pull`, the strategy compares each file's SHA256 to the prior `:File.sha` and only re-analyses files whose hash changed. LLM cost is proportional to actual code churn, not to repo size. ### Graph shape @@ -193,66 +222,64 @@ Most well-formed code questions resolve in 2–4 tool calls. No re-clone, no ful ## Day-to-day commands -| Command | Purpose | -| ------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| `bytebell setup` | Interactive first-run wizard: provider, boot, optional index, MCP auto-install. | -| `bytebell ls` | List indexed knowledge entries with state. | -| `bytebell stats` | Ingestion totals, per-repo breakdown, per-commit token usage. | -| `bytebell mcp install` | Auto-detect installed editors and register the MCP endpoint in their config. | -| `bytebell mcp stats` | MCP usage: input/output tokens, monthly breakdown. | -| `bytebell pull` | Re-index a previously-added GitHub repo at branch HEAD (diff-aware). | -| `bytebell delete` | Picker; cancels jobs, drops the Knowledge subgraph from Neo4j, removes Mongo rows. | -| `bytebell shutdown` | Stop the server. Docker keeps running. | -| `bytebell boot` | Warm restart. | -| `docker compose -f infra/docker/docker-compose.yml down [-v]` | Stop containers (and optionally drop volumes — destroys all indexed data). | +| Command | Purpose | +| ------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| `plumbline setup` | Interactive first-run wizard: provider, boot, optional index, MCP auto-install. | +| `plumbline ls` | List indexed knowledge entries with state. | +| `plumbline stats` | Ingestion totals, per-repo breakdown, per-commit token usage. | +| `plumbline mcp install` | Auto-detect installed editors and register the MCP endpoint in their config. | +| `plumbline mcp stats` | MCP usage: input/output tokens, monthly breakdown. | +| `plumbline pull` | Re-index a previously-added GitHub repo at branch HEAD (diff-aware). | +| `plumbline delete` | Picker; cancels jobs, drops the Knowledge subgraph from Neo4j, removes SQLite rows. | +| `plumbline shutdown` | Stop the server. Docker keeps running. | +| `plumbline boot` | Warm restart. | +| `docker compose -f infra/docker/docker-compose.yml down [-v]` | Stop containers (and optionally drop volumes — destroys all indexed data). | Full reference, including every flag and option: [commands.md](commands.md). ## Bring your own infrastructure -By default, `bytebell boot` provisions a local Docker stack (`bytebell-mongo`, `bytebell-neo4j`, `bytebell-redis`) with auto-generated credentials. If you already run Mongo, Neo4j, and Redis (or want to use a managed service), set the connection details before booting and the Docker step is skipped: +By default, `plumbline boot` provisions a local Docker stack (`plumbline-neo4j`) with auto-generated credentials. The document store and the queue are SQLite — local files, never containers. If you already run Neo4j (or want to use a managed service), set the connection details before booting and the Docker step is skipped: ```bash -bytebell set mongo-uri mongodb://user:pass@host:27017/bytebell -bytebell set neo4j-uri bolt://host:7687 -bytebell set neo4j-user neo4j -bytebell set neo4j-password -bytebell set redis-url redis://host:6379 +plumbline set neo4j-uri bolt://host:7687 +plumbline set neo4j-user neo4j +plumbline set neo4j-password ``` Docker is not required on the host in this mode. See the [Configuration reference](#configuration-reference) for the full key list. ## Architecture at a glance -A single Bun-built Express daemon, `bytebell-server`, hosts the ingestion HTTP routes, the MCP transport (Streamable HTTP + SSE), and the BullMQ workers all in-process. The CLI is a thin Ink/React TUI that only ever talks HTTP to that daemon — it never touches Mongo, Neo4j, or Redis directly. Workers run in the server's lifecycle; there is no separate worker fleet. +A single Bun-built Express daemon, `plumbline-server`, hosts the ingestion HTTP routes, the MCP transport (Streamable HTTP + SSE), and the queue workers all in-process. The CLI is a thin Ink/React TUI that only ever talks HTTP to that daemon — it never touches SQLite or Neo4j directly. Workers run in the server's lifecycle; there is no separate worker fleet. For the full PRD — package tiers, state machine, HTTP route catalogue, verification checklist, distribution strategy — see [docs/arch.md](docs/arch.md). ## Configuration reference -Settings live in `~/.bytebell/config.json` and are written exclusively by `bytebell set ` (or by first-run auto-fill on `bytebell boot`). Keys: +Settings live in `~/.plumbline/config.json` and are written exclusively by `plumbline set ` (or by first-run auto-fill on `plumbline boot`). Keys: -| Key | Purpose | Default | -| -------------------- | ---------------------------------------- | ------------------------------------ | -| `openrouter-api-key` | API key for per-file LLM analysis | _(required, blank by default)_ | -| `openrouter-model` | OpenRouter model slug used for analysis | _(required)_ | -| `mongo-uri` | MongoDB connection string | `mongodb://localhost:27017/bytebell` | -| `neo4j-uri` | Neo4j Bolt URI | `bolt://localhost:7687` | -| `neo4j-user` | Neo4j auth user | `neo4j` | -| `neo4j-password` | Neo4j auth password | _(generated on first boot)_ | -| `redis-url` | Redis URL for BullMQ | `redis://localhost:6379` | -| `server-port` | Local HTTP/MCP port | `8080` | -| `concurrency-github` | Concurrent files analysed per GitHub job | tuned per box | -| `log-level` | Winston log level | `info` | -| `log-retention-days` | Daily log retention | `14` | +| Key | Purpose | Default | +| -------------------- | ---------------------------------------- | ------------------------------ | +| `openrouter-api-key` | API key for per-file LLM analysis | _(required, blank by default)_ | +| `openrouter-model` | OpenRouter model slug used for analysis | _(required)_ | +| `sqlite-path` | Path to the SQLite document store | `~/.plumbline/data.sqlite` | +| `neo4j-uri` | Neo4j Bolt URI | `bolt://localhost:7687` | +| `neo4j-user` | Neo4j auth user | `neo4j` | +| `neo4j-password` | Neo4j auth password | _(generated on first boot)_ | +| `queue-db-path` | Path to the SQLite job queue | `~/.plumbline/queue.db` | +| `server-port` | Local HTTP/MCP port | `8080` | +| `concurrency-github` | Concurrent files analysed per GitHub job | tuned per box | +| `log-level` | Winston log level | `info` | +| `log-retention-days` | Daily log retention | `14` | -If a required setting is missing, Bytebell either opens the setup form (interactive terminal) or prints the exact `bytebell set …` command and refuses to boot (non-interactive). It never silently reads `process.env`. +If a required setting is missing, Plumbline either opens the setup form (interactive terminal) or prints the exact `plumbline set …` command and refuses to boot (non-interactive). It never silently reads `process.env`. ## Why this design — research grounding -> Comparing Bytebell to PageIndex, GitNexus, GraphRAG, Sourcegraph, or Augment Code? See **[comparison.md](comparison.md)** for a side-by-side feature table and pros / cons of each. +> Comparing Plumbline to PageIndex, GitNexus, GraphRAG, Sourcegraph, or Augment Code? See **[comparison.md](comparison.md)** for a side-by-side feature table and pros / cons of each. -Bytebell's shape — _build a code graph at ingest time, enrich every node with LLM-derived structured semantics, then serve retrieval against the joined surface_ — tracks a converging body of recent work showing that purely structural retrieval (AST / call-graph) and purely semantic retrieval (embeddings) each leave large performance on the table, and that combining them at indexing time unlocks the gains. +Plumbline's shape — _build a code graph at ingest time, enrich every node with LLM-derived structured semantics, then serve retrieval against the joined surface_ — tracks a converging body of recent work showing that purely structural retrieval (AST / call-graph) and purely semantic retrieval (embeddings) each leave large performance on the table, and that combining them at indexing time unlocks the gains. **Graphs beat flat retrieval for code.** Repository-level graphs from AST + imports + call structure consistently outperform flat embedding retrieval on real engineering tasks. @@ -266,9 +293,9 @@ Bytebell's shape — _build a code graph at ingest time, enrich every node with - Tram ([2305.11074](https://arxiv.org/abs/2305.11074), ACL 2023) — semantic enrichment beats flat sentence-level retrieval. - LLM Agents Improve Semantic Code Search ([2408.11058](https://arxiv.org/abs/2408.11058)) — LLM-injected metadata improves embedding-based retrieval. - Knowledge-Graph-Based Repo-Level Code Generation ([2505.14394](https://arxiv.org/abs/2505.14394)) — graph captures structure; LLM context fills semantic gaps. -- Sense and Sensitivity ([2505.13353](https://arxiv.org/abs/2505.13353)) — lexical and semantic recall are different capabilities; supports the `summary` (semantic) vs Mongo raw (lexical) split. +- Sense and Sensitivity ([2505.13353](https://arxiv.org/abs/2505.13353)) — lexical and semantic recall are different capabilities; supports the `summary` (semantic) vs SQLite raw (lexical) split. -**Structured summaries and hierarchy beat blob summarization.** Explicit fields — purpose, inputs, outputs, business context — aggregated bottom-up let retrieval match at the right level of abstraction. This maps directly onto Bytebell's `purpose` / `summary` / `businessContext` schema. +**Structured summaries and hierarchy beat blob summarization.** Explicit fields — purpose, inputs, outputs, business context — aggregated bottom-up let retrieval match at the right level of abstraction. This maps directly onto Plumbline's `purpose` / `summary` / `businessContext` schema. - Hierarchical Repo-Level Code Summarization for Business Applications ([2501.07857](https://arxiv.org/abs/2501.07857), ICSE LLM4Code 2025) — closest motivational match: structured per-unit summaries aggregated to file/package level, grounded in business context. - Beyond Function Level ([2502.16704](https://arxiv.org/abs/2502.16704)) — class/repo context in summaries beats function-only. @@ -283,7 +310,7 @@ The design choices follow directly: each `:File` node carries LLM-generated sema ## Enterprise -Bytebell-public is the OSS edition. ByteBell also offers a separately-licensed **Enterprise** edition for organizations that need a commercial-use grant, hardening, and direct support. Enterprise typically includes: +Plumbline — `Plumbline-public` in the [LICENSE](LICENSE) text — is the OSS edition. ByteBell also offers a separately-licensed **Enterprise** edition for organizations that need a commercial-use grant, hardening, and direct support. Enterprise typically includes: - A commercial-use grant covering use by or on behalf of for-profit entities, including SaaS deployments and revenue-generating applications. - Hardened multi-tenant deployment patterns, SSO / SCIM, audit logging, and data-isolation guarantees. @@ -299,4 +326,4 @@ Hooks, commit conventions, and pre-push gates are documented in [contributing.md ## License -Bytebell is released under **AGPL-3.0 with an additional non-commercial use clause** — see [LICENSE](LICENSE) for the authoritative text. Personal, academic, research, and non-profit use are unrestricted under AGPL-3.0 (network-copyleft applies). **Commercial use** is governed by license terms and is covered by the [Enterprise edition](#enterprise) (`team@bytebell.ai`). The running server itself does **not** verify a license; governance is by license terms, not by code. The server is meant for local single-tenant use — no remote network surface; everything binds to `127.0.0.1`. +Plumbline is released under **AGPL-3.0 with an additional non-commercial use clause** — see [LICENSE](LICENSE) for the authoritative text. Personal, academic, research, and non-profit use are unrestricted under AGPL-3.0 (network-copyleft applies). **Commercial use** is governed by license terms and is covered by the [Enterprise edition](#enterprise) (`team@bytebell.ai`). The running server itself does **not** verify a license; governance is by license terms, not by code. The server is meant for local single-tenant use — no remote network surface; everything binds to `127.0.0.1`. diff --git a/SECURITY.md b/SECURITY.md index f08da6c..45d7cd9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Reporting a Vulnerability -If you believe you have found a security issue in Bytebell, please report it privately. **Do not open a public GitHub issue for security problems.** +If you believe you have found a security issue in Plumbline, please report it privately. **Do not open a public GitHub issue for security problems.** - **Email**: `team@bytebell.ai` - **Subject line**: `[security] ` @@ -13,7 +13,7 @@ If you believe you have found a security issue in Bytebell, please report it pri - A description of the issue and its impact. - Steps to reproduce (a minimal proof of concept is ideal). - The version / commit SHA you tested against. -- Your environment (OS, Bun version, `bytebell --version`). +- Your environment (OS, Bun version, `plumbline --version`). - Whether you have already disclosed this to anyone else. ## Response timeline @@ -27,10 +27,10 @@ If you believe you have found a security issue in Bytebell, please report it pri In scope: -- The local `bytebell-server` HTTP daemon (Express routes, MCP transport, BullMQ workers). -- The `bytebell` CLI (Ink TUI + commander subcommands). +- The local `plumbline-server` HTTP daemon (Express routes, MCP transport, queue workers). +- The `plumbline` CLI (Ink TUI + commander subcommands). - The `@bb/*` workspace packages under `packages/*`. -- Credential handling (`~/.bytebell/config.json`, mode `0600`; logs under `~/.bytebell/logs/`). +- Credential handling (`~/.plumbline/config.json`, mode `0600`; logs under `~/.plumbline/logs/`). - Shell-injection in CLI argument parsing. - Prompt-injection in indexed repository content that could cause unsafe outputs from the LLM pipeline. @@ -38,11 +38,11 @@ The OSS edition binds the server to `127.0.0.1` and has no remote attack surface ## Out of scope -- Third-party services the user runs (MongoDB, Neo4j, Redis, Docker, OpenRouter). Report those upstream. +- Third-party services the user runs (Neo4j, Docker, OpenRouter). Report those upstream. - The user's own environment (their OS, their LLM provider account, their network). -- Theoretical issues without a reproducible impact on Bytebell. +- Theoretical issues without a reproducible impact on Plumbline. - DDoS or volumetric reports against `bytebell.ai` web properties (this policy covers the OSS code only). ## Recognition -Bytebell is licensed under **AGPL-3.0-only with an additional non-commercial clause**. We do not run a paid bug bounty programme. With your permission, we will credit you in [CHANGELOG.md](CHANGELOG.md) and the release notes when a reported issue is fixed. +Plumbline is licensed under **AGPL-3.0-only with an additional non-commercial clause**. We do not run a paid bug bounty programme. With your permission, we will credit you in [CHANGELOG.md](CHANGELOG.md) and the release notes when a reported issue is fixed. diff --git a/SETUP.md b/SETUP.md index 222a558..a3d247a 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,7 +1,7 @@ -# Bytebell +# Plumbline **Ask questions about any codebase — straight from Claude Code, Cursor, and other -AI assistants.** Point Bytebell at a repo, and your AI tools can suddenly answer +AI assistants.** Point Plumbline at a repo, and your AI tools can suddenly answer "where is auth handled?" or "how does caching work here?" with real, grounded answers from the actual code. @@ -12,12 +12,12 @@ you choose — no telemetry, no phone-home. ## The whole thing, in 4 steps -1. **Install** Bytebell +1. **Install** Plumbline 2. **Pick a model** (OpenRouter or local Ollama) 3. **Choose a repo** to make searchable 4. **Ask about it** in your editor -One command (`bytebell setup`) does steps 2–4 for you, including wiring itself into +One command (`plumbline setup`) does steps 2–4 for you, including wiring itself into your editor automatically. Most people are querying their code in a couple of minutes. --- @@ -32,19 +32,19 @@ You'll need a few common tools first — the installer checks for them and tells if anything's missing: - **[Bun](https://bun.sh)** — `curl -fsSL https://bun.sh/install | bash` -- **[Docker Desktop](https://www.docker.com/products/docker-desktop)**, running — Bytebell uses it to start its local store the first time. You never manage it directly. +- **[Docker Desktop](https://www.docker.com/products/docker-desktop)**, running — Plumbline uses it to start its local store the first time. You never manage it directly. - **git** > Prefer to do it by hand? `git clone https://github.com/ByteBell/open-ir && cd open-ir && bun install && cd packages/cli && bun link` -Check it's there: `bytebell --help` +Check it's there: `plumbline --help` --- ## 2. Run setup — one command handles everything ```bash -bytebell setup +plumbline setup ``` > Run it directly in a terminal (it's interactive). @@ -55,11 +55,11 @@ It asks you three quick things, then takes over: - **Which repo?** Paste a GitHub URL to index now, or skip and add one later. Private repo? It'll ask for a token. Want a specific branch? It'll let you pick. - **Confirm.** -From there it runs on its own — starting Bytebell, indexing your repo, and showing +From there it runs on its own — starting Plumbline, indexing your repo, and showing live progress for each phase. The part that makes it feel like magic: > **It detects your coding tools** — Claude Code, Cursor, Claude Desktop, Windsurf, -> VS Code — and wires Bytebell into them for you (with a backup of each config). +> VS Code — and wires Plumbline into them for you (with a backup of each config). > No copy-pasting connection strings. --- @@ -69,7 +69,7 @@ live progress for each phase. The part that makes it feel like magic: When setup finishes you'll see something like: ``` -✓ Bytebell running +✓ Plumbline running ✓ Repo indexed ✓ Connected to Cursor & Claude Code ``` @@ -82,60 +82,60 @@ When setup finishes you'll see something like: - _"What happens when a request hits the `/index` route?"_ - _"Which files would I touch to add a new CLI command?"_ -The assistant calls Bytebell's retrieval tools behind the scenes and answers from +The assistant calls Plumbline's retrieval tools behind the scenes and answers from your actual code. > If your editor wasn't auto-detected, connect it once by hand: -> `claude mcp add --transport http bytebell http://127.0.0.1:8080/mcp` +> `claude mcp add --transport http plumbline http://127.0.0.1:8080/mcp` --- ## Everyday commands -| You want to… | Run | -| ----------------------- | ---------------------------------------------- | -| Add another repo | `bytebell index https://github.com/owner/repo` | -| …a private one | `bytebell index --token ` | -| …a specific branch | `bytebell index --branch ` | -| Index a local folder | `bytebell ingest /path/to/source` | -| Check what's ready | `bytebell ls` | -| See token usage & cost | `bytebell stats` | -| Re-connect your editors | `bytebell mcp install` | -| Change a setting | `bytebell set ` | -| Start everything again | `bytebell boot` | -| Stop it | `bytebell shutdown` | - -A repo is ready to query once `bytebell ls` shows it as **PROCESSED**. +| You want to… | Run | +| ----------------------- | ----------------------------------------------- | +| Add another repo | `plumbline index https://github.com/owner/repo` | +| …a private one | `plumbline index --token ` | +| …a specific branch | `plumbline index --branch ` | +| Index a local folder | `plumbline ingest /path/to/source` | +| Check what's ready | `plumbline ls` | +| See token usage & cost | `plumbline stats` | +| Re-connect your editors | `plumbline mcp install` | +| Change a setting | `plumbline set ` | +| Start everything again | `plumbline boot` | +| Stop it | `plumbline shutdown` | + +A repo is ready to query once `plumbline ls` shows it as **PROCESSED**. ---
-Under the hood (optional — you don't need this to use Bytebell) +Under the hood (optional — you don't need this to use Plumbline) ### Local-first & private There's no `.env` file and no telemetry. All config lives in -`~/.bytebell/config.json` (mode `0600`), written only by `bytebell set`. The only +`~/.plumbline/config.json` (mode `0600`), written only by `plumbline set`. The only outbound network calls go to the LLM backend you picked (OpenRouter or your Ollama URL). ### What setup actually starts -On first boot, Bytebell brings up a small local stack via Docker (MongoDB, Neo4j, -Redis) and a server on `http://127.0.0.1:8080` (the MCP endpoint is `/mcp`). Data -lives in named volumes and `~/.bytebell/`, so it persists across reboots. First boot +On first boot, Plumbline brings up Neo4j via Docker and a server on +`http://127.0.0.1:8080` (the MCP endpoint is `/mcp`). Data +lives in named volumes and `~/.plumbline/`, so it persists across reboots. First boot pulls images and can take a couple of minutes; later boots are quick. ### Bring your own infrastructure -Already running Mongo / Neo4j / Redis and don't want the Docker stack? Point Bytebell +Already running Neo4j and don't want the Docker stack? Point Plumbline at your own instances instead — see **Bring your own infrastructure** in -[README.md](README.md). (`bytebell boot` skips any service whose config you've already set.) +[README.md](README.md). (`plumbline boot` skips any service whose config you've already set.) ### Indexing lifecycle -`bytebell ls` shows each repo moving through: +`plumbline ls` shows each repo moving through: `CREATED → QUEUED → INGESTED → PROCESSING → PROCESSED` (or `FAILED`, with a reason). -Per-file analysis runs through your chosen model; `bytebell stats` shows the token +Per-file analysis runs through your chosen model; `plumbline stats` shows the token cost. ### Full reference @@ -152,8 +152,8 @@ Every command, flag, and option: [commands.md](commands.md). Architecture and de - **"Docker is installed but not running"** — start Docker Desktop, then re-run. - **Server won't start / "infra not reachable"** — Docker isn't up yet, or a port (8080, or a DB port) is taken. Setup offers to reuse or remap a conflicting port; - otherwise free it and re-run `bytebell setup`. -- **`bytebell setup` says it needs a terminal** — don't pipe it; run it directly. + otherwise free it and re-run `plumbline setup`. +- **`plumbline setup` says it needs a terminal** — don't pipe it; run it directly. - **Private repo won't index** — your token needs `repo` scope. - **Editor returns nothing yet** — the repo is still indexing. Wait for `PROCESSED` - in `bytebell ls`. + in `plumbline ls`. diff --git a/bun.lock b/bun.lock index b018547..a2c0e87 100644 --- a/bun.lock +++ b/bun.lock @@ -24,15 +24,16 @@ "name": "@bb/cli", "version": "0.0.0", "bin": { - "bytebell": "./src/index.ts", + "plumbline": "./src/index.ts", }, "dependencies": { "@bb/config": "workspace:*", + "@bb/db": "workspace:*", "@bb/errors": "workspace:*", "@bb/ingest-github": "workspace:*", "@bb/logger": "workspace:*", - "@bb/mongo": "workspace:*", "@bb/path-migration": "workspace:*", + "@bb/sqlite": "workspace:*", "@bb/types": "workspace:*", "commander": "^14.0.3", "ink": "^7.0.1", @@ -114,7 +115,6 @@ "@bb/graph-db": "workspace:*", "@bb/llm": "workspace:*", "@bb/logger": "workspace:*", - "@bb/mongo": "workspace:*", "@bb/neo4j": "workspace:*", "@bb/sqlite": "workspace:*", "@bb/types": "workspace:*", @@ -134,7 +134,6 @@ "@bb/llm": "workspace:*", "@bb/logger": "workspace:*", "@bb/mcp": "workspace:*", - "@bb/mongo": "workspace:*", "@bb/neo4j": "workspace:*", "@bb/queue": "workspace:*", "@bb/sqlite": "workspace:*", @@ -154,7 +153,6 @@ "@bb/llm": "workspace:*", "@bb/logger": "workspace:*", "@bb/mcp": "workspace:*", - "@bb/mongo": "workspace:*", "@bb/types": "workspace:*", }, }, @@ -167,6 +165,7 @@ "@bb/graph-core": "workspace:*", "@bb/graph-db": "workspace:*", "@bb/types": "workspace:*", + "@ladybugdb/core": "^0.20.3", "parquetjs": "^0.11.2", }, "devDependencies": { @@ -213,27 +212,15 @@ "@types/express": "^5.0.6", }, }, - "packages/mongo": { - "name": "@bb/mongo", - "version": "0.0.0", - "dependencies": { - "@bb/config": "workspace:*", - "@bb/db": "workspace:*", - "@bb/db-core": "workspace:*", - "@bb/errors": "workspace:*", - "@bb/types": "workspace:*", - "mongodb": "^7.2.0", - }, - }, "packages/neo4j": { "name": "@bb/neo4j", "version": "0.0.0", "dependencies": { "@bb/config": "workspace:*", + "@bb/db-core": "workspace:*", "@bb/errors": "workspace:*", "@bb/graph-core": "workspace:*", "@bb/graph-db": "workspace:*", - "@bb/mongo": "workspace:*", "@bb/types": "workspace:*", "neo4j-driver": "^6.0.1", }, @@ -257,21 +244,6 @@ "@bb/types": "workspace:*", }, }, - "packages/queue-bullmq": { - "name": "@bb/queue-bullmq", - "version": "0.0.0", - "dependencies": { - "@bb/config": "workspace:*", - "@bb/db": "workspace:*", - "@bb/errors": "workspace:*", - "@bb/logger": "workspace:*", - "@bb/queue": "workspace:*", - "@bb/queue-core": "workspace:*", - "@bb/redis": "workspace:*", - "@bb/types": "workspace:*", - "bullmq": "^5.76.3", - }, - }, "packages/queue-core": { "name": "@bb/queue-core", "version": "0.0.0", @@ -292,21 +264,11 @@ "@russellthehippo/honker-node": "^0.3.3", }, }, - "packages/redis": { - "name": "@bb/redis", - "version": "0.0.0", - "dependencies": { - "@bb/config": "workspace:*", - "@bb/errors": "workspace:*", - "@bb/types": "workspace:*", - "ioredis": "^5.10.1", - }, - }, "packages/server": { "name": "@bb/server", "version": "0.0.0", "bin": { - "bytebell-server": "./src/index.ts", + "plumbline-server": "./src/index.ts", }, "dependencies": { "@bb/config": "workspace:*", @@ -318,11 +280,9 @@ "@bb/ingest-strategies": "workspace:*", "@bb/ladybug": "workspace:*", "@bb/mcp": "workspace:*", - "@bb/mongo": "workspace:*", "@bb/neo4j": "workspace:*", "@bb/path-migration": "workspace:*", "@bb/queue": "workspace:*", - "@bb/queue-bullmq": "workspace:*", "@bb/queue-honker": "workspace:*", "@bb/sqlite": "workspace:*", "@bb/types": "workspace:*", @@ -348,6 +308,9 @@ "version": "0.0.0", }, }, + "trustedDependencies": [ + "@ladybugdb/core", + ], "packages": { "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="], @@ -385,22 +348,16 @@ "@bb/mcp": ["@bb/mcp@workspace:packages/mcp"], - "@bb/mongo": ["@bb/mongo@workspace:packages/mongo"], - "@bb/neo4j": ["@bb/neo4j@workspace:packages/neo4j"], "@bb/path-migration": ["@bb/path-migration@workspace:packages/path-migration"], "@bb/queue": ["@bb/queue@workspace:packages/queue"], - "@bb/queue-bullmq": ["@bb/queue-bullmq@workspace:packages/queue-bullmq"], - "@bb/queue-core": ["@bb/queue-core@workspace:packages/queue-core"], "@bb/queue-honker": ["@bb/queue-honker@workspace:packages/queue-honker"], - "@bb/redis": ["@bb/redis@workspace:packages/redis"], - "@bb/server": ["@bb/server@workspace:packages/server"], "@bb/sqlite": ["@bb/sqlite@workspace:packages/sqlite"], @@ -475,23 +432,21 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@ladybugdb/core": ["@ladybugdb/core@0.20.3", "", { "dependencies": { "apache-arrow": "^21.1.0", "cmake-js": "^8.0.0", "node-addon-api": "^6.0.0" }, "optionalDependencies": { "@ladybugdb/core-darwin-arm64": "0.20.3", "@ladybugdb/core-darwin-x64": "0.20.3", "@ladybugdb/core-linux-arm64": "0.20.3", "@ladybugdb/core-linux-x64": "0.20.3", "@ladybugdb/core-win32-x64": "0.20.3" } }, "sha512-msCQt0kYDpeMJ7DgpLwAZ4russmmRWLHh8hYE3iU9YWVABFrGkCvtA+2RcHQWW3zUkWV44lW0ggZiHu+QyevEg=="], - "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.9", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-RXSxsokhAF/4nWys8An8npsqOI33Ex1Hlzqjw2pZOO+GKtMAR2noGnUdsFiGwsaO/xXI+56mtjTmDA3JXJsvmA=="], + "@ladybugdb/core-darwin-arm64": ["@ladybugdb/core-darwin-arm64@0.20.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkeuuMhqTGYsTpM5npSM2k1HwiGRoPgVaUKld66OcP90XeG4J0FN+1Xm4IC5o9jE6fKpflUv5jRnB2uiU2IptA=="], - "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], + "@ladybugdb/core-darwin-x64": ["@ladybugdb/core-darwin-x64@0.20.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ntw7qFy74WgUpbTvF8GBrr82fDgWCOTiSje8XHjlCi4wIVAOCa43yvwzo/eOj+QySoOs0Mc7w6Ex7zabd5hkyQ=="], - "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], + "@ladybugdb/core-linux-arm64": ["@ladybugdb/core-linux-arm64@0.20.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-cDvsdriDAvy1O2IQnsUHXKVZAkpvcD5z8f9J1TkGogVyWXdrrr+NrImLELb6brguI9nw+KBB1CKOMnPE0CuTdQ=="], - "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], + "@ladybugdb/core-linux-x64": ["@ladybugdb/core-linux-x64@0.20.3", "", { "os": "linux", "cpu": "x64" }, "sha512-F7jXUSaCtxAFgy9p3qxE1D5FX57ag3CsekYXrH0g1tmGZHXPmlQnLA2a2MDk6V2lSzhfWX5NJA2fFGXp0uKAog=="], - "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], + "@ladybugdb/core-win32-x64": ["@ladybugdb/core-win32-x64@0.20.3", "", { "os": "win32", "cpu": "x64" }, "sha512-qoQOvKypqlh8Nv6ych7xmOlACOLJFLxVNbpIkL3PJ9qQf63kmFAT6B+btixLU2Bmq1foNKLyiWuoHfKhXdJTCg=="], - "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], - - "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], @@ -547,10 +502,6 @@ "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], - "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], - - "@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/type-utils": "8.59.0", "@typescript-eslint/utils": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg=="], @@ -587,6 +538,8 @@ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "apache-arrow": ["apache-arrow@21.2.0", "", { "dependencies": { "@types/node": "^25.2.0", "flatbuffers": "^25.1.24", "json-with-bigint": "^3.5.3", "tslib": "^2.6.2" }, "bin": { "arrow2csv": "bin/arrow2csv.js" } }, "sha512-Hxe6Agq26gQOM954qpzYSllJBPJl+e16U5CkfuMUhLrNba+5nKkttIVlflaovN6oaTratqMGAO8H5u/aNhmHWQ=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], @@ -611,8 +564,6 @@ "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "bullmq": ["bullmq@5.76.3", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", "msgpackr": "1.11.5", "node-abort-controller": "3.1.1", "semver": "7.7.4", "tslib": "2.8.1" } }, "sha512-UBICMeWLYa+Dz7IGBNebXApQ1OIxNd4t6nX+AFPQ5gFA3sosW34PENe8Q1cvbjcbMTaU3xrKPorb6tM1czRSsw=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -625,6 +576,8 @@ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + "cli-boxes": ["cli-boxes@4.0.1", "", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="], "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], @@ -633,7 +586,7 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "cmake-js": ["cmake-js@8.0.0", "", { "dependencies": { "debug": "^4.4.3", "fs-extra": "^11.3.3", "node-api-headers": "^1.8.0", "rc": "1.2.8", "semver": "^7.7.3", "tar": "^7.5.6", "url-join": "^4.0.1", "which": "^6.0.0", "yargs": "^17.7.2" }, "bin": { "cmake-js": "bin/cmake-js" } }, "sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg=="], "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], @@ -673,22 +626,18 @@ "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.3.0", "", { "dependencies": { "jiti": "2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA=="], - "cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -777,6 +726,8 @@ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + "flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="], + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], @@ -785,6 +736,8 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -805,6 +758,8 @@ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], @@ -831,7 +786,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], "ink": ["ink@7.0.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.3", "auto-bind": "^5.0.1", "chalk": "^5.6.2", "cli-boxes": "^4.0.1", "cli-cursor": "^4.0.0", "cli-truncate": "^6.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.45.1", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^9.0.0", "stack-utils": "^2.0.6", "string-width": "^8.2.0", "terminal-size": "^4.0.1", "type-fest": "^5.5.0", "widest-line": "^6.0.0", "wrap-ansi": "^10.0.0", "ws": "^8.20.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.2.0", "react": ">=19.2.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-o6LAC268PLawlGVYrXTyaTfke4VtJftEheuwbgkQf7yvSXyWp1nRwBbAyKEkWXFZZsW/la5wrMuNbuBvZK2C1w=="], @@ -839,8 +794,6 @@ "int53": ["int53@0.2.4", "", {}, "sha512-a5jlKftS7HUOhkUyYD7j2sJ/ZnvWiNlZS1ldR+g1ifQ+/UuZXIE+YTc/lK1qGj/GwAU5F8Z0e1eVq2t1J5Ob2g=="], - "ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="], - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -883,6 +836,10 @@ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json-with-bigint": ["json-with-bigint@3.5.12", "", {}, "sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w=="], + + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], @@ -899,10 +856,6 @@ "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], - "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], - - "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], - "lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g=="], "lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="], @@ -917,16 +870,12 @@ "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], - "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], - "lzo": ["lzo@0.4.11", "", { "dependencies": { "bindings": "~1.2.1" } }, "sha512-apQHNoW2Alg72FMqaC/7pn03I7umdgSVFt2KRkCXXils4Z9u3QBh1uOtl2O5WmZIDLd9g6Lu4lIdOLmiSTFVCQ=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], - "meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], @@ -943,18 +892,14 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - "mongodb": ["mongodb@7.2.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.2.0", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw=="], + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - "mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.1", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ=="], + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], - - "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], @@ -965,9 +910,9 @@ "neo4j-driver-core": ["neo4j-driver-core@6.0.1", "", {}, "sha512-5I2KxICAvcHxnWdJyDqwu8PBAQvWVTlQH2ve3VQmtVdJScPqWhpXN1PiX5IIl+cRF3pFpz9GQF53B5n6s0QQUQ=="], - "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], + "node-addon-api": ["node-addon-api@6.1.0", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="], - "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + "node-api-headers": ["node-api-headers@1.9.0", "", {}, "sha512-2oNILP4jXwRB4ywnYKjVk1YyJ96n2D4EOVJO6S3oYZ5PtbJrw3Yt9TpAuX3nBLMuzn74rnfGQrv13pS9vC+YiA=="], "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], @@ -1033,16 +978,14 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], - - "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -1091,14 +1034,10 @@ "snappyjs": ["snappyjs@0.6.1", "", {}, "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg=="], - "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], - "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], - "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], @@ -1109,10 +1048,14 @@ "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], + "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], @@ -1127,8 +1070,6 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], - "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], @@ -1147,20 +1088,20 @@ "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "varint": ["varint@5.0.2", "", {}, "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], - - "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="], @@ -1181,6 +1122,8 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -1211,6 +1154,10 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "cmake-js/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], + + "global-directory/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "ink-text-input/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -1225,8 +1172,6 @@ "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], - "mongodb/bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="], - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1243,6 +1188,8 @@ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "cmake-js/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + "listr2/cli-truncate/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], "listr2/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], diff --git a/commands.md b/commands.md index 16d99b3..164486d 100644 --- a/commands.md +++ b/commands.md @@ -1,47 +1,47 @@ -# Bytebell CLI — Commands Reference +# Plumbline CLI — Commands Reference -The `bytebell` binary is the Ink/commander TUI front-end. It never touches Mongo / Neo4j / Redis directly — every command resolves to an HTTP call against the local `bytebell-server` daemon, auto-starting it in the background when needed. +The `plumbline` binary is the Ink/commander TUI front-end. It never touches SQLite or Neo4j directly — every command resolves to an HTTP call against the local `plumbline-server` daemon, auto-starting it in the background when needed. ``` -bytebell [command] [...args] +plumbline [command] [...args] ``` Global flags: `-V, --version`, `-h, --help`. -### `bytebell --help` +### `plumbline --help` The output below is what commander prints in the current build (registration order from [packages/cli/src/index.ts](../packages/cli/src/index.ts)). If your installed binary shows a shorter list, it predates the additions and you should rebuild the CLI. ``` -Usage: bytebell [options] [command] +Usage: plumbline [options] [command] -Bytebell — local knowledge engine TUI +Plumbline — local knowledge engine TUI Options: -V, --version output the version number -h, --help display help for command Commands: - set [key] [value] Write a value to ~/.bytebell/config.json. With no args, opens the interactive setup form. - boot Bring up Docker infra (mongo + neo4j + redis) and start the bytebell-server. - shutdown Stop the bytebell-server (docker infra is left running). - server Manage the bytebell-server daemon. + set [key] [value] Write a value to ~/.plumbline/config.json. With no args, opens the interactive setup form. + boot Bring up Docker infra (neo4j) and start the plumbline-server. + shutdown Stop the plumbline-server (docker infra is left running). + server Manage the plumbline-server daemon. index [options] Index a remote git repository. ingest [path] Ingest a local directory (defaults to the current working directory). pull [options] [knowledge-id] Re-index a previously added GitHub repo at the branch's current HEAD. ls List indexed knowledge entries. - delete Pick an indexed knowledge entry and delete it from Mongo + Neo4j. + delete Pick an indexed knowledge entry and delete it from SQLite + Neo4j. stats Show ingestion totals, per-repo breakdown, and per-commit token usage. mcp Manage and view MCP usage. help [command] display help for command ``` -Per-command help is available as `bytebell --help`, e.g.: +Per-command help is available as `plumbline --help`, e.g.: ``` -$ bytebell index --help -Usage: bytebell index [options] +$ plumbline index --help +Usage: plumbline index [options] Index a remote git repository. @@ -51,13 +51,13 @@ Arguments: Options: --branch branch to index (defaults to 'main' on the server) --token GitHub PAT for private repos - --verbose stream the server log file to the terminal during the run (set log level via `bytebell set log-level debug` for finer-grained output) + --verbose stream the server log file to the terminal during the run (set log level via `plumbline set log-level debug` for finer-grained output) -h, --help display help for command ``` ``` -$ bytebell pull --help -Usage: bytebell pull [options] [knowledge-id] +$ plumbline pull --help +Usage: plumbline pull [options] [knowledge-id] Re-index a previously added GitHub repo at the branch's current HEAD. @@ -72,8 +72,8 @@ Options: ``` ``` -$ bytebell mcp --help -Usage: bytebell mcp [options] [command] +$ plumbline mcp --help +Usage: plumbline mcp [options] [command] Manage and view MCP usage. @@ -82,42 +82,42 @@ Commands: help [command] display help for command ``` -| Command | Purpose | -| ----------- | ------------------------------------------------------------------------ | -| `set` | Write a value to `~/.bytebell/config.json` (interactive form if no args) | -| `boot` | Start Docker infra (mongo + neo4j + redis) and the bytebell-server | -| `shutdown` | Stop the bytebell-server (Docker infra is left running) | -| `server` | Manage the bytebell-server daemon | -| `index` | Index a remote git repository | -| `pull` | Re-index a previously added GitHub repo at branch HEAD (or a given SHA) | -| `ingest` | Ingest a local directory | -| `ls` | List indexed knowledge entries | -| `delete` | Pick an entry and delete it from Mongo + Neo4j | -| `stats` | Show ingestion totals, per-repo breakdown, per-commit token usage | -| `mcp` | Parent command for MCP usage subcommands | -| `mcp stats` | Show input/output token stats for MCP (global + monthly breakdown) | +| Command | Purpose | +| ----------- | ------------------------------------------------------------------------- | +| `set` | Write a value to `~/.plumbline/config.json` (interactive form if no args) | +| `boot` | Start Docker infra (neo4j) and the plumbline-server | +| `shutdown` | Stop the plumbline-server (Docker infra is left running) | +| `server` | Manage the plumbline-server daemon | +| `index` | Index a remote git repository | +| `pull` | Re-index a previously added GitHub repo at branch HEAD (or a given SHA) | +| `ingest` | Ingest a local directory | +| `ls` | List indexed knowledge entries | +| `delete` | Pick an entry and delete it from SQLite + Neo4j | +| `stats` | Show ingestion totals, per-repo breakdown, per-commit token usage | +| `mcp` | Parent command for MCP usage subcommands | +| `mcp stats` | Show input/output token stats for MCP (global + monthly breakdown) | --- -## `bytebell set [key] [value]` +## `plumbline set [key] [value]` -Writes to `~/.bytebell/config.json`. Run with no arguments to launch the interactive Ink setup form ([SetupForm.tsx](../packages/cli/src/SetupForm.tsx)). Headless form takes a key from the table below and a value; values are validated before they are persisted. +Writes to `~/.plumbline/config.json`. Run with no arguments to launch the interactive Ink setup form ([SetupForm.tsx](../packages/cli/src/SetupForm.tsx)). Headless form takes a key from the table below and a value; values are validated before they are persisted. ``` -bytebell set port 7777 -bytebell set openrouter-api-key sk-or-v1-... -bytebell set # opens the interactive form +plumbline set port 7777 +plumbline set openrouter-api-key sk-or-v1-... +plumbline set # opens the interactive form ``` Valid keys (see [keyMap.ts](../packages/cli/src/keyMap.ts)): | Key | Validation | Redacted in output | | ----------------------------- | ------------------- | ------------------ | -| `mongo` | string (URI) | no | +| `sqlite-path` | string (path) | no | | `neo4j` | string (URI) | no | | `neo4j-user` | string | no | | `neo4j-password` | string | yes | -| `redis` | string (URI) | no | +| `queue-db-path` | string (path) | no | | `port` | integer 1–65535 | no | | `log-level` | one of `LOG_LEVELS` | no | | `log-retention-days` | positive integer | no | @@ -133,58 +133,58 @@ This is the only sanctioned write path to `config.json` (manual edits work but a --- -## `bytebell boot` +## `plumbline boot` End-to-end "start everything" command ([BootCommand.ts](../packages/cli/src/BootCommand.ts)). -1. Runs preflight (refuses to boot when required config is missing — prints the exact `bytebell set …` to fix it). -2. Auto-fills local-Docker defaults for `mongo`, `neo4j`, `neo4j-user`, `neo4j-password`, `redis` if absent. -3. Brings up the docker-compose stack (mongo + neo4j + redis) and waits for healthchecks. -4. Starts `bytebell-server` in the background, prints the MCP endpoint URL. +1. Runs preflight (refuses to boot when required config is missing — prints the exact `plumbline set …` to fix it). +2. Auto-fills defaults for `sqlite-path`, `queue-db-path`, `neo4j`, `neo4j-user`, `neo4j-password` if absent. +3. Brings up the docker-compose stack (neo4j) and waits for healthchecks. +4. Starts `plumbline-server` in the background, prints the MCP endpoint URL. ``` -bytebell boot +plumbline boot ``` -Output ends with `MCP endpoint: http://127.0.0.1:/mcp` and a hint to run `bytebell index` or `bytebell ingest` next. +Output ends with `MCP endpoint: http://127.0.0.1:/mcp` and a hint to run `plumbline index` or `plumbline ingest` next. --- -## `bytebell shutdown` +## `plumbline shutdown` -Sends `SIGTERM` to the running server (PID read from `~/.bytebell/pid`) and waits up to 30 s for it to drain ([ShutdownCommand.ts](../packages/cli/src/ShutdownCommand.ts)). Does **not** stop Docker infra — prints the `docker compose -f … down` command to do so manually. +Sends `SIGTERM` to the running server (PID read from `~/.plumbline/pid`) and waits up to 30 s for it to drain ([ShutdownCommand.ts](../packages/cli/src/ShutdownCommand.ts)). Does **not** stop Docker infra — prints the `docker compose -f … down` command to do so manually. ``` -bytebell shutdown +plumbline shutdown ``` If the PID file is stale or absent, exits cleanly. --- -## `bytebell server` +## `plumbline server` Daemon management. Currently exposes one subcommand: -### `bytebell server start` +### `plumbline server start` Runs `bun --bun packages/server/src/index.ts` in the **foreground** with stdio inherited (Ctrl+C to stop). Used for development / debugging — the other commands all auto-start the server in the background instead. ``` -bytebell server start +plumbline server start ``` --- -## `bytebell index [options]` +## `plumbline index [options]` Clones a remote git repository on the server and runs the active `IngestionStrategy` against it ([IndexCommand.ts](../packages/cli/src/IndexCommand.ts)). ``` -bytebell index https://github.com/owner/repo -bytebell index https://github.com/owner/repo --branch dev -bytebell index https://github.com/owner/private --token ghp_xxx -bytebell index https://github.com/owner/repo --verbose +plumbline index https://github.com/owner/repo +plumbline index https://github.com/owner/repo --branch dev +plumbline index https://github.com/owner/private --token ghp_xxx +plumbline index https://github.com/owner/repo --verbose ``` Options: @@ -199,16 +199,16 @@ Auto-starts the server, then `POST /api/v1/github/index`, then polls `/api/v1/re --- -## `bytebell pull [knowledge-id] [options]` +## `plumbline pull [knowledge-id] [options]` Re-indexes a previously added **GitHub** repo at the branch's current HEAD ([PullCommand.ts](../packages/cli/src/PullCommand.ts)). Pull does not apply to `local:` ingests — the picker filters them out. ``` -bytebell pull # interactive multi-select picker -bytebell pull 1ee3bac7-... # pull one knowledgeId by id -bytebell pull 1ee3bac7-... --commit deadbee # anchor to a specific SHA -bytebell pull --token ghp_xxx # private repo -bytebell pull --verbose # tail server logs during the run +plumbline pull # interactive multi-select picker +plumbline pull 1ee3bac7-... # pull one knowledgeId by id +plumbline pull 1ee3bac7-... --commit deadbee # anchor to a specific SHA +plumbline pull --token ghp_xxx # private repo +plumbline pull --verbose # tail server logs during the run ``` Options: @@ -234,45 +234,45 @@ The picker is **multi-select** — toggle as many repos as you want, then `Enter Ingests a local directory — defaults to the current working directory ([IngestCommand.ts](../packages/cli/src/IngestCommand.ts)). ``` -bytebell ingest # ingest CWD -bytebell ingest /abs/path/to/repo -bytebell ingest ./relative/path +plumbline ingest # ingest CWD +plumbline ingest /abs/path/to/repo +plumbline ingest ./relative/path ``` Validates that the path exists and is a directory before calling `POST /api/v1/local/index`. Polling and progress UI are identical to `index`. --- -## `bytebell ls` +## `plumbline ls` Lists indexed knowledge entries by calling `GET /api/v1/repos` ([LsCommand.ts](../packages/cli/src/LsCommand.ts)). ``` -bytebell ls +plumbline ls ``` Renders a table of `ID | SOURCE | STATE | UPDATED | FILES`. Source is rendered as `github:[@branch]` or `local:`. State follows the lifecycle: `CREATED → QUEUED → INGESTED → PROCESSING → PROCESSED` (or `FAILED`). --- -## `bytebell delete` +## `plumbline delete` -Interactive picker (Ink) over the `ls` output that issues `DELETE /api/v1/repos/` for the chosen entry ([DeleteCommand.ts](../packages/cli/src/DeleteCommand.ts)). Removes Mongo file rows, Neo4j nodes, raw artefacts, stats rows, and any pending BullMQ jobs. +Interactive picker (Ink) over the `ls` output that issues `DELETE /api/v1/repos/` for the chosen entry ([DeleteCommand.ts](../packages/cli/src/DeleteCommand.ts)). Removes SQLite file rows, Neo4j nodes, raw artefacts, stats rows, and any pending queue jobs. ``` -bytebell delete +plumbline delete ``` Confirmation message reports counts: `removed (raw: N, stats: N, jobs: N)`. --- -## `bytebell stats` +## `plumbline stats` Hits `GET /api/v1/stats` and renders three sections ([StatsCommand.ts](../packages/cli/src/StatsCommand.ts)): ``` -bytebell stats +plumbline stats ``` - **TOTALS** — total repos, files, input tokens, output tokens, estimated cost (USD). @@ -283,11 +283,11 @@ bytebell stats --- -## `bytebell mcp` +## `plumbline mcp` Parent command for MCP-related views ([McpCommand.ts](../packages/cli/src/McpCommand.ts)). -### `bytebell mcp stats` +### `plumbline mcp stats` Hits `GET /api/v1/mcp/stats` and renders: @@ -295,7 +295,7 @@ Hits `GET /api/v1/mcp/stats` and renders: - **Monthly Usage by Identity** — per-identity / per-month rows: `Identity | Period | Reqs | In Tokens | Out Tokens | Total`. ``` -bytebell mcp stats +plumbline mcp stats ``` When no monthly rows exist, prints `No monthly usage records found.` @@ -305,38 +305,38 @@ When no monthly rows exist, prints `No monthly usage records found.` ## Lifecycle quick-start ``` -bytebell set # interactive first-run config -bytebell boot # docker + server -bytebell index https://github.com/owner/repo -bytebell ls -bytebell pull # re-index against branch HEAD -bytebell stats -bytebell mcp stats -bytebell delete # pick an entry to remove -bytebell shutdown # stop the server (docker stays up) +plumbline set # interactive first-run config +plumbline boot # docker + server +plumbline index https://github.com/owner/repo +plumbline ls +plumbline pull # re-index against branch HEAD +plumbline stats +plumbline mcp stats +plumbline delete # pick an entry to remove +plumbline shutdown # stop the server (docker stays up) ``` --- ## The `--verbose` flag -Available on `bytebell index` and `bytebell pull` ([logTailer.ts](../packages/cli/src/logTailer.ts)). +Available on `plumbline index` and `plumbline pull` ([logTailer.ts](../packages/cli/src/logTailer.ts)). -- When set, the CLI **tails the active server log file** (`~/.bytebell/logs/server-YYYY-MM-DD.log`) and streams new lines to the terminal alongside the spinner / progress bar for the duration of the job. -- The flag controls **what you see**, not **what is logged**. The server's log level is independent — set it via `bytebell set log-level ` (one of the `LOG_LEVELS` from [@bb/config](../packages/config/)). For finer-grained output, run `bytebell set log-level debug` first, then re-run with `--verbose`. +- When set, the CLI **tails the active server log file** (`~/.plumbline/logs/server-YYYY-MM-DD.log`) and streams new lines to the terminal alongside the spinner / progress bar for the duration of the job. +- The flag controls **what you see**, not **what is logged**. The server's log level is independent — set it via `plumbline set log-level ` (one of the `LOG_LEVELS` from [@bb/config](../packages/config/)). For finer-grained output, run `plumbline set log-level debug` first, then re-run with `--verbose`. - The tailer is started after the server is up and stopped automatically when the command finishes (success, failure, or Ctrl+C). - Verbose output is only the server log; client-side spinners and progress bars are unaffected. ``` -bytebell set log-level debug -bytebell index https://github.com/owner/repo --verbose -bytebell pull --verbose +plumbline set log-level debug +plumbline index https://github.com/owner/repo --verbose +plumbline pull --verbose ``` --- ## Notes -- Every command auto-starts `bytebell-server` in the background if it is not already running; logs are written to `~/.bytebell/logs/server-YYYY-MM-DD.log`. +- Every command auto-starts `plumbline-server` in the background if it is not already running; logs are written to `~/.plumbline/logs/server-YYYY-MM-DD.log`. - The `--help` text printed by `commander` may lag this document while features are added — the source files under [packages/cli/src/](../packages/cli/src/) are authoritative. - All HTTP routes referenced above are documented (OpenAPI) on the server side per the "Rule of API Logging & Documentation" in [CLAUDE.md](../CLAUDE.md). diff --git a/comparison.md b/comparison.md index 3dba4ed..a363e21 100644 --- a/comparison.md +++ b/comparison.md @@ -1,4 +1,4 @@ -# How every code-graph MCP tool works, and why Bytebell saves 80% on tokens +# How every code-graph MCP tool works, and why Plumbline saves 80% on tokens Code knowledge graphs are not a new idea. Most of the tools below build one in some form. What actually matters is what each graph stores on its nodes, and how those nodes are connected to each other. That is where the token cost gap comes from when you start running real queries against your codebase. @@ -38,7 +38,7 @@ Understand-Anything is a Claude Code plugin that runs a multi-agent pipeline to ### code-grapher -Code-grapher builds a Neo4j knowledge graph from AST analysis, with an optional AI-powered description pass through Ollama or Gemini. It supports a PRIMER.md file to inject business context, and does surgical diff-based updates via git. Of all the AST tools, this is probably closest in spirit to what Bytebell does. The difference is that business context here is global and opt-in rather than per file and automatic. +Code-grapher builds a Neo4j knowledge graph from AST analysis, with an optional AI-powered description pass through Ollama or Gemini. It supports a PRIMER.md file to inject business context, and does surgical diff-based updates via git. Of all the AST tools, this is probably closest in spirit to what Plumbline does. The difference is that business context here is global and opt-in rather than per file and automatic. ### Deep Graph MCP (CodeGPT) @@ -62,15 +62,15 @@ Skills are a different approach again. Instead of always-on context like CLAUDE. --- -## How Bytebell does it +## How Plumbline does it -Bytebell builds a Neo4j graph too, so the architecture sounds similar on the surface. The difference is in what we put on each node, and how the nodes are connected to each other across repositories. +Plumbline builds a Neo4j graph too, so the architecture sounds similar on the surface. The difference is in what we put on each node, and how the nodes are connected to each other across repositories. For every file in your codebase, an LLM generates a structured analysis at index time. You get a one-paragraph purpose explaining why the file exists, a longer summary covering what it does and how it fits into the architecture, a business context line that ties it to the product domain, plus the classes, functions, keywords, internal imports, and external imports it contains. All of that lives on the file node. The imports link to deduplicated module nodes, which means a question like "who imports parse_file" is just one Cypher hop. The semantic nodes for things like ontology concepts, business entities, contracts, and system capabilities are scoped to the entire organisation rather than to a single repository, so when two repos both reference the concept "authentication", they share the same node. That one design choice gives you a cross-repo dependency graph for free, with no special indexer required. -Every node also carries a commit hash and a SHA-256 of the file content. When you run a reindex, Bytebell compares hashes and only re-analyses the files whose content actually changed. The LLM cost ends up proportional to your actual code churn, not to the size of your repository. If you reindex a 200,000 file monorepo where 12 files changed in the last commit, that costs you 12 LLM calls, not 200,000. +Every node also carries a commit hash and a SHA-256 of the file content. When you run a reindex, Plumbline compares hashes and only re-analyses the files whose content actually changed. The LLM cost ends up proportional to your actual code churn, not to the size of your repository. If you reindex a 200,000 file monorepo where 12 files changed in the last commit, that costs you 12 LLM calls, not 200,000. -This is where the 80 percent token savings actually comes from. Your AI assistant stops re-reading the same files at the start of every session. Instead of burning through roughly 38,900 tokens and 84 tool calls trying to answer a single cross-repo question, it pulls the pre-computed purpose, summary, business context, and import edges from the graph in milliseconds. Most well-formed questions resolve in two to four MCP tool calls and a small fraction of the tokens. On a test corpus of 500,000 files spread across 100 repositories, a complex cross-repo query that costs Claude Code on its own between $6 and $10 and takes three to five minutes will cost Bytebell plus Sonnet about $0.04 and finish in 30 to 40 seconds. +This is where the 80 percent token savings actually comes from. Your AI assistant stops re-reading the same files at the start of every session. Instead of burning through roughly 38,900 tokens and 84 tool calls trying to answer a single cross-repo question, it pulls the pre-computed purpose, summary, business context, and import edges from the graph in milliseconds. Most well-formed questions resolve in two to four MCP tool calls and a small fraction of the tokens. On a test corpus of 500,000 files spread across 100 repositories, a complex cross-repo query that costs Claude Code on its own between $6 and $10 and takes three to five minutes will cost Plumbline plus Sonnet about $0.04 and finish in 30 to 40 seconds. Everything runs on 127.0.0.1. There are no vectors involved, no embedding provider, and no cloud component. The only outbound call is to OpenRouter for the per-file LLM analysis, and if you want to route that to a local model instead, you can. diff --git a/contributing.md b/contributing.md index bad34a2..1e0347a 100644 --- a/contributing.md +++ b/contributing.md @@ -1,4 +1,4 @@ -# Contributing to Bytebell-public +# Contributing to Plumbline-public Thanks for contributing. This document describes the automated checks that run on every commit and push, what each check enforces, and how to recover when one fails. @@ -144,7 +144,7 @@ Multi-line example with body and footer (note the blank lines required by `body- ``` fix(queue): dedupe retries by job-level idempotency key -Without a dedupe key, BullMQ retries were re-running the parse phase +Without a dedupe key, queue retries were re-running the parse phase and double-writing nodes whenever a worker crashed mid-job. Refs: BB-142 @@ -196,13 +196,13 @@ brew install gitleaks ## Local development (hot reload) -Two root scripts run the binaries directly from source with `BYTEBELL_DEV=1` (logs go to `./logs/` instead of `~/.bytebell/logs/`): +Two root scripts run the binaries directly from source with `PLUMBLINE_DEV=1` (logs go to `./logs/` instead of `~/.plumbline/logs/`): ```bash -bun run dev:server # BYTEBELL_DEV=1 bun --watch packages/server/src/index.ts -bun run dev:cli # BYTEBELL_DEV=1 bun packages/cli/src/index.ts +bun run dev:server # PLUMBLINE_DEV=1 bun --watch packages/server/src/index.ts +bun run dev:cli # PLUMBLINE_DEV=1 bun packages/cli/src/index.ts ``` `dev:server` uses `bun --watch`, which restarts the process automatically on any file change in the dependency graph — no `shutdown` / `boot` cycle needed between edits. -**Before running `dev:server`, run `bytebell shutdown`** to stop any server started via `bytebell boot`. Otherwise the watched process can't bind the configured server port (8080 by default) and exits with `EADDRINUSE`. `dev:server` does not manage Docker infra — bring up Mongo/Neo4j/Redis with `bytebell boot` (then `bytebell shutdown` to free the port) or `docker compose -f infra/docker/docker-compose.yml up -d`. +**Before running `dev:server`, run `plumbline shutdown`** to stop any server started via `plumbline boot`. Otherwise the watched process can't bind the configured server port (8080 by default) and exits with `EADDRINUSE`. `dev:server` does not manage Docker infra — bring up Neo4j with `plumbline boot` (then `plumbline shutdown` to free the port) or `docker compose -f infra/docker/docker-compose.yml up -d`. diff --git a/infra/README.md b/infra/README.md index 699b59c..f504fe6 100644 --- a/infra/README.md +++ b/infra/README.md @@ -4,7 +4,7 @@ Operational artefacts that are not TypeScript workspace packages. Today this dir holds the local Docker compose stack used by -`bytebell boot`. Future additions might include a Helm chart, a +`plumbline boot`. Future additions might include a Helm chart, a production Compose file, or platform-specific service files — each in its own subdirectory with its own `README.md`. @@ -15,10 +15,10 @@ boundary. ## Subdirectories -- [`docker/`](docker/README.md) — three-service `docker-compose.yml` - (Mongo + Neo4j + Redis) plus the gitignored `.env` file the CLI - generates on first boot. Versions pinned at the major level - (`mongo:7`, `neo4j:5`, `redis:7-alpine`). +- [`docker/`](docker/README.md) — single-service `docker-compose.yml` + (Neo4j) plus the gitignored `.env` file the CLI generates on first + boot. Version pinned at the major level (`neo4j:5`). The document + store and the job queue are SQLite and have no container. ## Adding a new infra artefact diff --git a/infra/docker/README.md b/infra/docker/README.md index 858af66..17fe9ac 100644 --- a/infra/docker/README.md +++ b/infra/docker/README.md @@ -2,33 +2,40 @@ ## Purpose -Three-service `docker-compose.yml` that brings up the local infrastructure -required by `bytebell-server`: MongoDB, Neo4j, and Redis. Consumed -exclusively by the `bytebell boot` CLI command in `@bb/cli`. Not a +Single-service `docker-compose.yml` that brings up the one containerised +dependency `plumbline-server` has: Neo4j. The document store and the job queue +are both SQLite files under `~/.plumbline`, so neither has a container here. +Consumed exclusively by the `plumbline boot` CLI command in `@bb/cli`. Not a workspace package — this is operational data, not TypeScript code. +The file also declares an optional `ladybug-explorer` viewer, which the CLI +does not start or health-poll; it is not part of the contract below. + ## Contract -[`docker-compose.yml`](docker-compose.yml) declares three services on a -single `bytebell` bridge network, all bound to `127.0.0.1` only -(single-tenant local OSS engine — no remote network surface). +[`docker-compose.yml`](docker-compose.yml) declares the one managed service on +a `plumbline` bridge network, bound to `127.0.0.1` only (single-tenant local +OSS engine — no remote network surface). + +The top-level `name: plumbline` key sets the Compose **project** name. It is +load-bearing: `dockerInfra.ts` shells out to `docker compose -f ` with +no `-p` flag, so without this key Compose falls back to the parent directory and +files everything under a project called `docker` — which is what `docker compose +ls` and Docker Desktop would then show. Container, network, and volume names are +all declared explicitly elsewhere in the file, so the project name is the one +identifier this key controls. -| Service | Image | Host port | Container | Volume name | -| ------- | ---------------- | ---------- | ---------------- | --------------------- | -| mongo | `mongo:7` | 27017 | `bytebell-mongo` | `bytebell_mongo_data` | -| neo4j | `neo4j:5` | 7687, 7474 | `bytebell-neo4j` | `bytebell_neo4j_data` | -| redis | `redis:7-alpine` | 6379 | `bytebell-redis` | `bytebell_redis_data` | +| Service | Image | Host port | Container | Volume name | +| ------- | --------- | ---------- | ----------------- | ---------------------- | +| neo4j | `neo4j:5` | 7687, 7474 | `plumbline-neo4j` | `plumbline_neo4j_data` | Each service has a healthcheck the CLI polls via `docker compose ps --format json`: -- mongo: `mongosh ... ping` - neo4j: `wget -qO- http://localhost:7474` (Neo4j HTTP up implies Bolt is up shortly after; the CLI gates on the `Health` field) -- redis: `redis-cli ping` -Versions are pinned at the major-version level — `mongo:7`, `neo4j:5`, -`redis:7-alpine`. Neo4j 5 is required for the fulltext-index syntax +The version is pinned at the major level — `neo4j:5`. Neo4j 5 is required for the fulltext-index syntax declared in `@bb/neo4j/src/indexes.ts` (multi-label `FOR (n:Class|Function)`). The Neo4j password is read from `${NEO4J_PASSWORD}` in `.env` (gitignored). @@ -39,14 +46,42 @@ APOC is enabled via `NEO4J_PLUGINS` and `NEO4J_dbms_security_procedures_unrestri to keep the door open for future graph algorithms; current Cypher in `@bb/neo4j` and `@bb/mcp` does not depend on it. +## Disk bounds + +Docker's own storage — container logs, image layers, named volumes — lives +inside the Docker VM disk (~31GB on a default Docker Desktop install), not on +the host filesystem. Every unbounded writer in this stack competes for that one +disk, and when it fills, writes start failing in ways that surface as unrelated +application errors — a service looks broken when the real fault is disk. Two +bounds in `docker-compose.yml` exist to prevent it, and neither may be removed +without a replacement: + +| Bound | Where | Why | +| -------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `x-logging` anchor (`max-size: 50m`, `max-file: 3`) on every service | all services | Docker's `json-file` driver is unbounded by default. A chatty ingestion run writes GB of stdout. | +| `NEO4J_db_tx__log_rotation_retention__policy: "256M size"` | `neo4j` | Neo4j keeps **2 days** of write-ahead logs by default; after a bulk ingest `/data/transactions` routinely dwarfs the store itself. Nothing replays these — no cluster, no backup window. | + +Neo4j JVM sizing (`NEO4J_HEAP_MAX`, `NEO4J_PAGECACHE`) is bounded for the same +reason — left unset, the JVM sizes itself from the host's RAM and starves the +ingestion worker running beside it. Both are overridable from `.env`. + +Note that container **logs** are capped here, while the server's own Winston +logs go to `~/.plumbline/logs/` on the host and are pruned by +`log_retention_days` in `config.json` — a different mechanism for a different +disk. + +Changing a bound requires recreating the container: `logging:` and `command:` +are baked in at create time, so an edit to this file has no effect on an +already-running container until `docker compose up -d --force-recreate `. + ## Data ownership -- `bytebell_mongo_data`, `bytebell_neo4j_data`, `bytebell_redis_data` — +- `plumbline_neo4j_data` — named Docker volumes. **Persisted across restarts**, **not** auto-deleted by `docker compose down`. Removed only on `docker compose down -v` or explicit `docker volume rm`. - `infra/docker/.env` — generated by `@bb/cli`'s `bootConfig.ts` on - first `bytebell boot`. Mode `0600`. Contains `NEO4J_PASSWORD=…`. + first `plumbline boot`. Mode `0600`. Contains `NEO4J_PASSWORD=…`. `.gitignore` here ensures it is never committed. ## How `@bb/cli` consumes this @@ -54,40 +89,39 @@ to keep the door open for future graph algorithms; current Cypher in `packages/cli/src/dockerInfra.ts` resolves this directory via `import.meta.url`, writes `.env`, then invokes `docker compose -f /docker-compose.yml up -d` and polls -`docker compose ps --format json` for `Health == "healthy"` on all -three services. +`docker compose ps --format json` for `Health == "healthy"` on the service. `packages/cli/src/bootConfig.ts` mirrors the auto-generated Neo4j -password into both `~/.bytebell/config.json` (via the `neo4j-password` +password into both `~/.plumbline/config.json` (via the `neo4j-password` key in `keyMap.ts`) and `infra/docker/.env`. The two stay in sync -because every `bytebell boot` re-reads `Config.Neo4jPassword` and +because every `plumbline boot` re-reads `Config.Neo4jPassword` and re-writes `.env` before invoking `docker compose up`. ## Lifecycle -- `bytebell boot` — `docker compose up -d` + poll until healthy + +- `plumbline boot` — `docker compose up -d` + poll until healthy + spawn server. -- `bytebell shutdown` — stops the **server only**. Docker keeps +- `plumbline shutdown` — stops the **server only**. Docker keeps running on purpose (per user direction, asymmetric lifecycle: warm re-boots are fast). - Stop the containers explicitly with `docker compose -f infra/docker/docker-compose.yml down`. Add `-v` to also drop the named volumes (destroys all indexed data). -- A future `bytebell infra` subcommand group may wrap `up | down | +- A future `plumbline infra` subcommand group may wrap `up | down | status | logs`. Out of scope for v0. ## Editing Bumping a service version: change the `image:` tag, run -`bytebell boot`, watch the healthcheck pass. The named volumes are +`plumbline boot`, watch the healthcheck pass. The named volumes are preserved, so data carries across; if a major-version bump requires a volume migration (rare) the operator runs the upgrade by hand. -If you change the password manually via `bytebell set +If you change the password manually via `plumbline set neo4j-password ` while Docker is running, the compose env stays on the old value until the container is recreated. Run `docker compose -f infra/docker/docker-compose.yml up -d ---force-recreate neo4j` afterwards. `bytebell boot` itself rewrites +--force-recreate neo4j` afterwards. `plumbline boot` itself rewrites `.env` and runs `docker compose up -d`, which compose treats as a no-op when the env hash is unchanged but as a recreate when the env differs. diff --git a/infra/docker/docker-compose.yml b/infra/docker/docker-compose.yml index b14c516..ad4f599 100644 --- a/infra/docker/docker-compose.yml +++ b/infra/docker/docker-compose.yml @@ -1,29 +1,25 @@ -services: - mongo: - image: mongo:7 - container_name: bytebell-mongo - restart: unless-stopped - ports: - - "127.0.0.1:${MONGO_HOST_PORT:-27017}:27017" - volumes: - - mongo_data:/data/db - environment: - MONGO_INITDB_DATABASE: bytebell - networks: - - bytebell - healthcheck: - test: - - CMD-SHELL - - "echo 'db.runCommand({ ping: 1 }).ok' | mongosh --quiet localhost:27017/bytebell" - interval: 5s - timeout: 5s - retries: 12 - start_period: 10s +# Compose project name. Without this, Compose derives the project from the parent +# directory and every resource lands under a project called `docker` -- the CLI +# invokes `docker compose -f up -d` with no `-p` flag +# (packages/cli/src/dockerInfra.ts), so this key is the only thing naming it. +name: plumbline + +# Every service caps its json-file log. Docker's default is UNBOUNDED, and these +# logs live on the ~31GB Docker VM disk, not the host filesystem. When that disk +# fills, writes fail in ways that surface as unrelated application errors — a +# service looks broken when the real fault is disk. +x-logging: &default-logging + driver: json-file + options: + max-size: "50m" + max-file: "3" +services: neo4j: image: neo4j:5 - container_name: bytebell-neo4j + container_name: plumbline-neo4j restart: unless-stopped + logging: *default-logging ports: - "127.0.0.1:${NEO4J_HTTP_HOST_PORT:-7474}:7474" - "127.0.0.1:${NEO4J_BOLT_HOST_PORT:-7687}:7687" @@ -33,8 +29,20 @@ services: NEO4J_AUTH: "neo4j/${NEO4J_PASSWORD:?NEO4J_PASSWORD must be set in infra/docker/.env}" NEO4J_PLUGINS: '["apoc"]' NEO4J_dbms_security_procedures_unrestricted: "apoc.*" + # Neo4j keeps 2 days of write-ahead transaction logs by default, which for + # a bulk ingestion run means /data/transactions dwarfs the actual store + # (observed: 515MB of tx logs against a 5.4MB database). Cap by size — + # ingestion is a local, restartable pipeline, so there is nothing here to + # replay into a cluster or a backup window. + NEO4J_db_tx__log_rotation_retention__policy: "256M size" + # Bound the JVM instead of letting it size itself from the container's view + # of host RAM; an unbounded page cache on a laptop starves the ingestion + # worker running alongside it. + NEO4J_server_memory_heap_initial__size: "512m" + NEO4J_server_memory_heap_max__size: "${NEO4J_HEAP_MAX:-2G}" + NEO4J_server_memory_pagecache_size: "${NEO4J_PAGECACHE:-1G}" networks: - - bytebell + - plumbline healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:7474 >/dev/null 2>&1"] interval: 5s @@ -42,46 +50,25 @@ services: retries: 24 start_period: 20s - redis: - image: redis:7-alpine - container_name: bytebell-redis - restart: unless-stopped - ports: - - "127.0.0.1:${REDIS_HOST_PORT:-6379}:6379" - volumes: - - redis_data:/data - command: ["redis-server", "--appendonly", "yes"] - networks: - - bytebell - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 5s - retries: 12 - start_period: 5s - ladybug-explorer: image: ghcr.io/ladybugdb/explorer:latest - container_name: bytebell-ladybug-explorer + container_name: plumbline-ladybug-explorer restart: unless-stopped + logging: *default-logging ports: - "127.0.0.1:8000:8000" volumes: - - ${HOME}/.bytebell:/database:ro + - ${HOME}/.plumbline:/database:ro environment: - LBUG_FILE=ladybug.lbug networks: - - bytebell + - plumbline networks: - bytebell: - name: bytebell + plumbline: + name: plumbline driver: bridge volumes: - mongo_data: - name: bytebell_mongo_data neo4j_data: - name: bytebell_neo4j_data - redis_data: - name: bytebell_redis_data + name: plumbline_neo4j_data diff --git a/install.sh b/install.sh index f859e97..8ec0cd4 100644 --- a/install.sh +++ b/install.sh @@ -6,14 +6,14 @@ set -e export PATH="$HOME/.bun/bin:$PATH" # ───────────────────────────────────────────── -# Bytebell — one-command setup (V1 / git-clone path) -# Usage: curl -fsSL https://raw.githubusercontent.com/ByteBell/open-ir/main/install.sh | bash +# Plumbline — one-command setup (V1 / git-clone path) +# Usage: curl -fsSL https://raw.githubusercontent.com/ByteBell/Plumbline/main/install.sh | bash # ───────────────────────────────────────────── -REPO_URL="https://github.com/ByteBell/open-ir" +REPO_URL="https://github.com/ByteBell/Plumbline" # Clone the same branch this installer was published from, so the installed code -# matches the config schema it writes. Override with BYTEBELL_BRANCH=... if needed. -REPO_BRANCH="${BYTEBELL_BRANCH:-merge/embedded_prerelease}" +# matches the config schema it writes. Override with PLUMBLINE_BRANCH=... if needed. +REPO_BRANCH="${PLUMBLINE_BRANCH:-main}" # ── helpers ────────────────────────────────── @@ -26,8 +26,8 @@ print_info() { echo " • $1"; } echo "" echo "This installer will:" -echo " • clone Bytebell into ./open-ir (the current directory)" -echo " • add a global 'bytebell' command" +echo " • clone Plumbline into ./plumbline (the current directory)" +echo " • add a global 'plumbline' command" echo " • install project dependencies" # ── 1. prerequisite checks ─────────────────── @@ -46,8 +46,8 @@ print_ok "Bun $(bun --version)" # Docker is OPTIONAL. The recommended default is the embedded stack # (SQLite + Ladybug + Honker) which needs no Docker at all. Docker is only -# required if you later choose "Docker" infra mode in `bytebell setup` -# (Mongo + Neo4j + Redis). So this is an informational probe, never fatal. +# required if you later choose "Docker" infra mode in `plumbline setup` +# (Neo4j). So this is an informational probe, never fatal. check_docker_running() { # `docker info` can hang if the daemon is wedged or mid-start, so cap it. # Prefer GNU `timeout`/`gtimeout`; fall back to a plain call where neither exists. @@ -61,7 +61,7 @@ check_docker_running() { } if ! command -v docker &>/dev/null; then - print_info "Docker not installed — fine for embedded mode (the default). Only needed if you pick Docker infra mode in 'bytebell setup'." + print_info "Docker not installed — fine for embedded mode (the default). Only needed if you pick Docker infra mode in 'plumbline setup'." elif ! check_docker_running; then print_info "Docker installed but not running — fine for embedded mode (the default). Start Docker Desktop only if you choose Docker infra mode." else @@ -76,14 +76,14 @@ print_ok "git $(git --version | awk '{print $3}')" # ── 2. clone ───────────────────────────────── -print_step "Cloning Bytebell" +print_step "Cloning Plumbline" -if [ -d "open-ir/.git" ]; then - print_info "existing install detected at open-ir/ — leaving it untouched (no git pull)" +if [ -d "plumbline/.git" ]; then + print_info "existing install detected at plumbline/ — leaving it untouched (no git pull)" else - git clone --branch "$REPO_BRANCH" "$REPO_URL" open-ir + git clone --branch "$REPO_BRANCH" "$REPO_URL" plumbline fi -cd open-ir +cd plumbline REPO_DIR="$(pwd)" print_ok "Repository ready" @@ -93,26 +93,26 @@ print_step "Installing dependencies" bun install --frozen-lockfile print_ok "Dependencies installed" -# ── 4. wire the bytebell binary ────────────── +# ── 4. wire the plumbline binary ────────────── # Symlinking the .ts entry directly is fragile — it relies on the shebang, # the executable bit, and bun's PATH resolution all lining up. A tiny wrapper # that execs `bun run` against the absolute entry path is reliable from any cwd. -print_step "Wiring bytebell binary" +print_step "Wiring plumbline binary" BUN_PATH="$(command -v bun)" BIN_DIR="$(dirname "$BUN_PATH")" ENTRY="$REPO_DIR/packages/cli/src/index.ts" -cat > "$BIN_DIR/bytebell" < "$BIN_DIR/plumbline" </dev/null 2>&1; then - print_ok "bytebell command available" +if command -v plumbline >/dev/null 2>&1; then + print_ok "plumbline command available" else - print_info "bytebell isn't on your PATH yet — add this line to your shell profile:" + print_info "plumbline isn't on your PATH yet — add this line to your shell profile:" echo " export PATH=\"$BIN_DIR:\$PATH\"" fi @@ -120,13 +120,13 @@ fi echo "" echo "════════════════════════════════════════" -echo " Bytebell installed!" +echo " Plumbline installed!" echo "" echo " Run the setup wizard to configure your LLM provider and boot:" echo "" -echo " bytebell setup" +echo " plumbline setup" echo "" echo " Commands reference:" -echo " https://github.com/ByteBell/open-ir/blob/main/commands.md" +echo " https://github.com/ByteBell/Plumbline/blob/main/commands.md" echo "════════════════════════════════════════" echo "" diff --git a/package.json b/package.json index 20cf76d..ee003a2 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "bytebell-public", + "name": "plumbline-public", "private": true, "version": "0.0.0", "type": "module", @@ -7,8 +7,8 @@ "packages/*" ], "scripts": { - "dev:server": "BYTEBELL_DEV=1 bun --watch packages/server/src/index.ts", - "dev:cli": "BYTEBELL_DEV=1 bun packages/cli/src/index.ts", + "dev:server": "PLUMBLINE_DEV=1 bun --watch packages/server/src/index.ts", + "dev:cli": "PLUMBLINE_DEV=1 bun packages/cli/src/index.ts", "typecheck": "tsc -b", "typecheck:clean": "tsc -b --clean", "typecheck:watch": "tsc -b --watch", diff --git a/packages/cli/README.md b/packages/cli/README.md index a76a909..437d3bf 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -5,17 +5,17 @@ Binary (deployable). Top of the import graph alongside `@bb/server`. Depends on Kernel (`@bb/types`, `@bb/errors`) and Infrastructure (`@bb/config`). Imported by no other workspace package — published as the -user-facing `bytebell` binary. +user-facing `plumbline` binary. -May **not** import `@bb/server`, `@bb/queue`, `@bb/mongo`, `@bb/redis`, +May **not** import `@bb/server`, `@bb/queue`, `@bb/llm`, `@bb/ingest-github`, or `@bb/logger`. The CLI talks HTTP to a -running `bytebell-server` (when subcommands need server state), spawns +running `plumbline-server` (when subcommands need server state), spawns the server and `docker compose` as foreign child processes, and -otherwise operates only on `~/.bytebell/` via `@bb/config`. +otherwise operates only on `~/.plumbline/` via `@bb/config`. ## Responsibility -The user-facing terminal UI for Bytebell. Arch-spec'd at +The user-facing terminal UI for Plumbline. Arch-spec'd at [docs/arch.md _TUI Spec_ §144-184](../../docs/arch.md#L144-L184) — single mode, every invocation is interactive in spirit, with subcommands for indexing, configuration, server lifecycle, and inspection. @@ -23,98 +23,98 @@ indexing, configuration, server lifecycle, and inspection. **v0 surface:** `setup`, `set`, `boot`, `shutdown`, `server start`, `index`, `ingest`, `ls`, `delete`, `stats`. -- `bytebell setup` — interactive first-run wizard. Presents an Ink multi-stage +- `plumbline setup` — interactive first-run wizard. Presents an Ink multi-stage form: (1) pick LLM provider (`openrouter` | `ollama`), (2) pick infrastructure - mode — **Docker** (non-embedded: mongo + neo4j + redis, the default selection, + mode — **Docker** (non-embedded: neo4j, the default selection, labelled "Docker needed") or **Embedded** (sqlite + ladybug + honker, no Docker, labelled "recommended"), (3) enter credentials / model, (4) optionally supply a GitHub repo URL to index after boot, (5) confirm. The infra mode is a single selector that expands to the three `db/graph/queue` provider keys via `applyInfraMode()` (see `infraMode.ts`). - On confirm: applies config via `KEY_MAP` setters (same path as `bytebell set`), + On confirm: applies config via `KEY_MAP` setters (same path as `plumbline set`), stops any running server, starts a fresh server, prints the MCP endpoint, and if a repo URL was given kicks `POST /api/v1/github/index` then polls to completion via the shared `pollIndexToCompletion()` helper. Requires an interactive TTY; exits with an error otherwise. -- `bytebell set ` — headless write to - `~/.bytebell/config.json` via `@bb/config.setConfigValue`. Type +- `plumbline set ` — headless write to + `~/.plumbline/config.json` via `@bb/config.setConfigValue`. Type coercion + Zod validation + atomic `tmp → fsync → rename`. Sole sanctioned write path per [docs/arch.md:140](../../docs/arch.md#L140). -- `bytebell set` (no args) — Ink setup form. Presents a single +- `plumbline set` (no args) — Ink setup form. Presents a single **Infrastructure** toggle (`docker|embedded`, default `docker`, embedded labelled "recommended"). In - Docker mode it walks Mongo / Neo4j / Neo4j-user / Neo4j-password / - Redis text fields (with field-level format validation) plus Port / + Docker mode it walks Neo4j / Neo4j-user / Neo4j-password / + text fields (with field-level format validation) plus Port / GitHub-concurrency / OpenRouter fields; in Embedded mode the - mongo/neo4j/redis rows are hidden and not required. On submit, the + neo4j rows are hidden and not required. On submit, the mode expands to the three provider keys via `applyInfraMode()` and every visible value is applied atomically through the same `setConfigValue` path. Esc cancels. -- `bytebell boot` — one-command bring-up. Refuses to proceed if +- `plumbline boot` — one-command bring-up. Refuses to proceed if `openrouter_api_key` or `openrouter_model` is blank (with the - matching `bytebell set …` hint). Whether Docker is started is derived + matching `plumbline set …` hint). Whether Docker is started is derived from the active provider combo (`needsDocker()` / `isEmbedded()` in `infraMode.ts`): **embedded** (sqlite + ladybug + honker) skips Docker entirely and goes straight to starting the server; **non-embedded** brings Docker up. Either way it auto-fills blank infra config keys (only for the providers in use): embedded fills the - `~/.bytebell` store paths (`sqlite-path` / `ladybug-path` / + `~/.plumbline` store paths (`sqlite-path` / `ladybug-path` / `queue-db-path` — Ladybug in particular needs a real path or it runs - in-memory), Docker fills the mongo/neo4j/redis URIs and generates a + in-memory), Docker fills the neo4j URIs and generates a random Neo4j password if one isn't already set. In Docker mode it writes `infra/docker/.env` (Neo4j password + host ports derived from the configured URIs), runs `docker compose -f infra/docker/docker-compose.yml up -d` for **only the services the - providers require** (mongo/neo4j/redis), polls + providers require** (neo4j), polls `docker compose ps --format json` until they report `healthy`, then invokes `ensureServerRunning()` (existing helper) to - spawn `bytebell-server`. Idempotent — re-running on an already-up + spawn `plumbline-server`. Idempotent — re-running on an already-up stack is a fast no-op. When a compose host port is already taken, boot drops into an Ink picker (`PortConflictSelector.tsx`) offering three choices: reuse the existing service on that port (compose starts only the unconflicted services), stop the conflicting - container and reuse the port, or change bytebell's host port for - the affected service (mongo / neo4j-bolt / redis URI gets rewritten + container and reuse the port, or change plumbline's host port for + the affected service (the neo4j-bolt URI gets rewritten via `setConfigValue`, compose env is regenerated, retry). Up to four conflict rounds before giving up. -- `bytebell shutdown` — sends SIGTERM to the server PID, polls until +- `plumbline shutdown` — sends SIGTERM to the server PID, polls until the PID file vanishes (≤ 30 s), then asks (Ink prompt `StopInfraPrompt.tsx`) whether to stop Docker infra too. Default - answer is **Yes** (Enter tears down `mongo + neo4j + redis` via + answer is **Yes** (Enter tears down `neo4j` via `docker compose down --remove-orphans`); pressing `n` / Esc keeps the containers running for fast warm re-boots and prints the manual `docker compose down` hint. The prompt is skipped when stdin isn't a TTY (CI-safe — falls back to keeping infra up). Two flags override the prompt deterministically: `--with-docker` always stops infra, `--keep-docker` always leaves it running; passing both is rejected. -- `bytebell server start` — low-level wrapper that spawns the server +- `plumbline server start` — low-level wrapper that spawns the server in the foreground (Ctrl+C to stop). Used during dev; everyday users - prefer `bytebell boot`. -- `bytebell index ` / `bytebell ingest [path]` / `bytebell ls` + prefer `plumbline boot`. +- `plumbline index ` / `plumbline ingest [path]` / `plumbline ls` — talk HTTP to a running server (lazy-spawn via `serverSpawn.ensureServerRunning` when the daemon is down). `ls` supports an interactive mode (`-i`) for hierarchical browsing of repos and commits. -- `bytebell delete` — list indexed knowledge in an Ink arrow-key picker +- `plumbline delete` — list indexed knowledge in an Ink arrow-key picker (`DeleteSelector.tsx`, plain `useInput` — no extra dep), and on confirm `DELETE /api/v1/repos/:id` against the running server. The - server cancels any pending BullMQ jobs, then `DETACH DELETE`s the - Neo4j subgraph and removes the Mongo `knowledge` / `raw` / + server cancels any pending queue jobs, then `DETACH DELETE`s the + Neo4j subgraph and removes the SQLite `knowledge` / `raw` / `processing_stats` rows for that id. -- `bytebell stats` — `GET /api/v1/stats` and render TOTALS / REPOS / +- `plumbline stats` — `GET /api/v1/stats` and render TOTALS / REPOS / COMMITS tables. Cost is per-model OpenRouter pricing computed server-side; rows with unknown pricing render as `unknown`. -- `bytebell --help` / `--version` — commander defaults. +- `plumbline --help` / `--version` — commander defaults. The package does **not** own: - Any other subcommand (index, ls, clean, models, keys, cost, server, mcp, update) — all deferred per the catalog below. -- Live infra connection probes — the CLI cannot import `@bb/mongo` / - `@bb/redis` per the tier rule. Format-only validation in v0; future - `bytebell config doctor` will probe via a running server. -- The Ink dashboard (`bytebell` no-args) — needs the server's HTTP API +- Live infra connection probes — the CLI cannot import the infra + packages per the tier rule. Format-only validation in v0; future + `plumbline config doctor` will probe via a running server. +- The Ink dashboard (`plumbline` no-args) — needs the server's HTTP API - activity feed. -- OpenRouter API key handling — own subcommand (`bytebell keys set`) +- OpenRouter API key handling — own subcommand (`plumbline keys set`) with `keytar` keychain backing. ## Public exports @@ -123,12 +123,12 @@ The package does **not** own: entry in `package.json`: ```jsonc -{ "bin": { "bytebell": "./src/index.ts" } } +{ "bin": { "plumbline": "./src/index.ts" } } ``` Publish-time builds swap to `./dist/index.js`. v0 dev workflow runs the TS file directly via Bun's `#!/usr/bin/env bun` shebang; install with -`cd packages/cli && bun link` to put `bytebell` on `PATH`. +`cd packages/cli && bun link` to put `plumbline` on `PATH`. The TypeScript module exports (`buildSetCommand`, `KEY_MAP`, etc.) are **internal** — no other workspace package imports `@bb/cli`. @@ -137,7 +137,7 @@ The TypeScript module exports (`buildSetCommand`, `KEY_MAP`, etc.) are None directly. The CLI is a thin shell over `@bb/config`'s atomic writer — it owns no module state, no caches, no on-disk artifacts of its -own. `~/.bytebell/config.json` is `@bb/config`'s data; CLI just writes +own. `~/.plumbline/config.json` is `@bb/config`'s data; CLI just writes through it. ## Invariants @@ -148,7 +148,7 @@ through it. `setConfigValue` which writes via `tmp → fsync → rename` at mode `0600` in dir `0700`. 3. **Tier discipline.** No imports from `@bb/server`, `@bb/queue`, - `@bb/mongo`, `@bb/redis`, `@bb/llm`, `@bb/ingest-github`. Blocked + `@bb/llm`, `@bb/ingest-github`. Blocked structurally (no workspace dep) and by ESLint boundary rule. Foreign processes the CLI is allowed to manage by signal: `bun … packages/server/src/index.ts` (the server daemon) and @@ -160,10 +160,10 @@ through it. on success — the raw value never appears in stdout / stderr / logs. 5. **`openrouter-api-key` and `openrouter-model` are headless-set keys** in `KEY_MAP`. The api key is `redact: true`; the model is - plain text. The pre-flight inside `bytebell boot` blocks bring-up + plain text. The pre-flight inside `plumbline boot` blocks bring-up until both are non-empty. 6. **Format-only validation in v0.** The setup form's per-field - validators check shape (`mongodb://`, `bolt://`, integer port, etc.) + validators check shape (`bolt://`, integer port, etc.) but never make network calls. 7. **TUI naming convention.** `*Command.ts` for commander handlers (plain TS), `*Form.tsx` / `*Pane.tsx` for Ink components (JSX). @@ -184,68 +184,68 @@ behind a `tty?` check (see `output.ts`). The complete arch-spec'd command surface, grouped by what each command will touch when implemented. Only the **bolded** entries ship in v0. -| Invocation | Behavior | When it lands | -| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | -| **`bytebell setup`** | **First-run wizard: pick LLM provider, configure keys/model, boot server, optionally kick index. v0.** | **Shipped** | -| **`bytebell set `** | **Headless write via `setConfigValue`. v0.** | **Shipped** | -| **`bytebell set`** | **Ink setup form (6 infra fields). v0.** | **Shipped** | -| **`bytebell boot`** | **Pre-flight + auto-fill infra keys + `docker compose up -d` + spawn server.** | **Shipped** | -| **`bytebell shutdown`** | **SIGTERM the server, leave Docker running.** | **Shipped** | -| **`bytebell server start`** | **Spawn `bytebell-server` in foreground.** | **Shipped** | -| **`bytebell index `** | **POST `/api/v1/github/index` to local server.** | **Shipped** | -| **`bytebell ingest [path]`** | **POST `/api/v1/local/index` for a directory tree.** | **Shipped** | -| **`bytebell ls`** | **Render `/api/v1/repos` as a table or interactive explorer (`-i`). v0.** | **Shipped** | -| **`bytebell delete`** | **Ink picker over `/api/v1/repos`, then DELETE `/api/v1/repos/:id` (Mongo + Neo4j + jobs).** | **Shipped** | -| **`bytebell stats`** | **Render `/api/v1/stats` (totals + per-repo + per-commit token / cost rows).** | **Shipped** | -| `bytebell` | Ink dashboard with Repos / Server / Activity / Cost panes ([docs/arch.md:172-184](../../docs/arch.md#L172-L184)) | After `@bb/server` HTTP API + activity feed | -| `bytebell` (first-run auto-launch of setup form) | If `isConfigComplete()` returns false, redirect to `bytebell set` form ([docs/arch.md:170](../../docs/arch.md#L170)) | After dashboard lands | -| `bytebell models set ` | Validate model via OpenRouter API + write `openrouter_model` | After OpenRouter helper | -| `bytebell models ls` | Curated 5-10 models, on-the-fly OpenRouter pricing | Same | -| `bytebell keys set` | Interactive masked prompt → `keytar` keychain → write key | After `keytar` integration | -| `bytebell cost` | Read `~/.bytebell/cost-ledger.sqlite` via `bun:sqlite`, render breakdowns | After cost ledger lands in `@bb/llm` | -| `bytebell server stop \| status \| logs` | Kill / inspect `bytebell-server`, tail server logs (start is shipped — see above) | After `@bb/server` health surface | -| `bytebell mcp` | Print MCP endpoint URL + sample MCP-client config | After dashboard pane | -| `bytebell infra up \| down \| status \| logs` | Thin wrapper over `docker compose` for users who want explicit infra control | If usage demands it post-v0 | -| `bytebell update` | Detect install method, run matching update, restart server | Release-engineering follow-up | -| Invocation | Behavior | When it lands | -| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | -| **`bytebell set `** | **Headless write via `setConfigValue`. v0.** | **Shipped** | -| **`bytebell set`** | **Ink setup form (6 infra fields). v0.** | **Shipped** | -| **`bytebell boot`** | **Pre-flight + auto-fill infra keys + `docker compose up -d` + spawn server.** | **Shipped** | -| **`bytebell shutdown`** | **SIGTERM the server, leave Docker running.** | **Shipped** | -| **`bytebell server start`** | **Spawn `bytebell-server` in foreground.** | **Shipped** | -| **`bytebell index `** | **POST `/api/v1/github/index` to local server.** | **Shipped** | -| **`bytebell ingest [path]`** | **POST `/api/v1/local/index` for a directory tree.** | **Shipped** | -| **`bytebell ls`** | **Render `/api/v1/repos` as a table or interactive explorer (`-i`). v0.** | **Shipped** | -| **`bytebell delete`** | **Ink picker over `/api/v1/repos`, then DELETE `/api/v1/repos/:id` (Mongo + Neo4j + jobs).** | **Shipped** | -| **`bytebell stats`** | **Render `/api/v1/stats` (totals + per-repo + per-commit token / cost rows).** | **Shipped** | -| `bytebell` | Ink dashboard with Repos / Server / Activity / Cost panes ([docs/arch.md:172-184](../../docs/arch.md#L172-L184)) | After `@bb/server` HTTP API + activity feed | -| `bytebell` (first-run auto-launch of setup form) | If `isConfigComplete()` returns false, redirect to `bytebell set` form ([docs/arch.md:170](../../docs/arch.md#L170)) | After dashboard lands | -| `bytebell models set ` | Validate model via OpenRouter API + write `openrouter_model` | After OpenRouter helper | -| `bytebell models ls` | Curated 5-10 models, on-the-fly OpenRouter pricing | Same | -| `bytebell keys set` | Interactive masked prompt → `keytar` keychain → write key | After `keytar` integration | -| `bytebell cost` | Read `~/.bytebell/cost-ledger.sqlite` via `bun:sqlite`, render breakdowns | After cost ledger lands in `@bb/llm` | -| `bytebell server stop \| status \| logs` | Kill / inspect `bytebell-server`, tail server logs (start is shipped — see above) | After `@bb/server` health surface | -| **`bytebell mcp install`** | **Detect installed coding tools (Claude Code, Cursor, Claude Desktop, Windsurf, VS Code) and merge a `bytebell` MCP server entry into each one's config, pointing at `http://127.0.0.1:/mcp`.** | **Shipped** | -| **`bytebell mcp stats`** | **Render `/api/v1/mcp/stats` (global + per-identity MCP token usage).** | **Shipped** | -| `bytebell infra up \| down \| status \| logs` | Thin wrapper over `docker compose` for users who want explicit infra control | If usage demands it post-v0 | -| `bytebell update` | Detect install method, run matching update, restart server | Release-engineering follow-up | +| Invocation | Behavior | When it lands | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- | +| **`plumbline setup`** | **First-run wizard: pick LLM provider, configure keys/model, boot server, optionally kick index. v0.** | **Shipped** | +| **`plumbline set `** | **Headless write via `setConfigValue`. v0.** | **Shipped** | +| **`plumbline set`** | **Ink setup form (6 infra fields). v0.** | **Shipped** | +| **`plumbline boot`** | **Pre-flight + auto-fill infra keys + `docker compose up -d` + spawn server.** | **Shipped** | +| **`plumbline shutdown`** | **SIGTERM the server, leave Docker running.** | **Shipped** | +| **`plumbline server start`** | **Spawn `plumbline-server` in foreground.** | **Shipped** | +| **`plumbline index `** | **POST `/api/v1/github/index` to local server.** | **Shipped** | +| **`plumbline ingest [path]`** | **POST `/api/v1/local/index` for a directory tree.** | **Shipped** | +| **`plumbline ls`** | **Render `/api/v1/repos` as a table or interactive explorer (`-i`). v0.** | **Shipped** | +| **`plumbline delete`** | **Ink picker over `/api/v1/repos`, then DELETE `/api/v1/repos/:id` (SQLite + Neo4j + jobs).** | **Shipped** | +| **`plumbline stats`** | **Render `/api/v1/stats` (totals + per-repo + per-commit token / cost rows).** | **Shipped** | +| `plumbline` | Ink dashboard with Repos / Server / Activity / Cost panes ([docs/arch.md:172-184](../../docs/arch.md#L172-L184)) | After `@bb/server` HTTP API + activity feed | +| `plumbline` (first-run auto-launch of setup form) | If `isConfigComplete()` returns false, redirect to `plumbline set` form ([docs/arch.md:170](../../docs/arch.md#L170)) | After dashboard lands | +| `plumbline models set ` | Validate model via OpenRouter API + write `openrouter_model` | After OpenRouter helper | +| `plumbline models ls` | Curated 5-10 models, on-the-fly OpenRouter pricing | Same | +| `plumbline keys set` | Interactive masked prompt → `keytar` keychain → write key | After `keytar` integration | +| `plumbline cost` | Read `~/.plumbline/cost-ledger.sqlite` via `bun:sqlite`, render breakdowns | After cost ledger lands in `@bb/llm` | +| `plumbline server stop \| status \| logs` | Kill / inspect `plumbline-server`, tail server logs (start is shipped — see above) | After `@bb/server` health surface | +| `plumbline mcp` | Print MCP endpoint URL + sample MCP-client config | After dashboard pane | +| `plumbline infra up \| down \| status \| logs` | Thin wrapper over `docker compose` for users who want explicit infra control | If usage demands it post-v0 | +| `plumbline update` | Detect install method, run matching update, restart server | Release-engineering follow-up | +| Invocation | Behavior | When it lands | +| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| **`plumbline set `** | **Headless write via `setConfigValue`. v0.** | **Shipped** | +| **`plumbline set`** | **Ink setup form (6 infra fields). v0.** | **Shipped** | +| **`plumbline boot`** | **Pre-flight + auto-fill infra keys + `docker compose up -d` + spawn server.** | **Shipped** | +| **`plumbline shutdown`** | **SIGTERM the server, leave Docker running.** | **Shipped** | +| **`plumbline server start`** | **Spawn `plumbline-server` in foreground.** | **Shipped** | +| **`plumbline index `** | **POST `/api/v1/github/index` to local server.** | **Shipped** | +| **`plumbline ingest [path]`** | **POST `/api/v1/local/index` for a directory tree.** | **Shipped** | +| **`plumbline ls`** | **Render `/api/v1/repos` as a table or interactive explorer (`-i`). v0.** | **Shipped** | +| **`plumbline delete`** | **Ink picker over `/api/v1/repos`, then DELETE `/api/v1/repos/:id` (SQLite + Neo4j + jobs).** | **Shipped** | +| **`plumbline stats`** | **Render `/api/v1/stats` (totals + per-repo + per-commit token / cost rows).** | **Shipped** | +| `plumbline` | Ink dashboard with Repos / Server / Activity / Cost panes ([docs/arch.md:172-184](../../docs/arch.md#L172-L184)) | After `@bb/server` HTTP API + activity feed | +| `plumbline` (first-run auto-launch of setup form) | If `isConfigComplete()` returns false, redirect to `plumbline set` form ([docs/arch.md:170](../../docs/arch.md#L170)) | After dashboard lands | +| `plumbline models set ` | Validate model via OpenRouter API + write `openrouter_model` | After OpenRouter helper | +| `plumbline models ls` | Curated 5-10 models, on-the-fly OpenRouter pricing | Same | +| `plumbline keys set` | Interactive masked prompt → `keytar` keychain → write key | After `keytar` integration | +| `plumbline cost` | Read `~/.plumbline/cost-ledger.sqlite` via `bun:sqlite`, render breakdowns | After cost ledger lands in `@bb/llm` | +| `plumbline server stop \| status \| logs` | Kill / inspect `plumbline-server`, tail server logs (start is shipped — see above) | After `@bb/server` health surface | +| **`plumbline mcp install`** | **Detect installed coding tools (Claude Code, Cursor, Claude Desktop, Windsurf, VS Code) and merge a `plumbline` MCP server entry into each one's config, pointing at `http://127.0.0.1:/mcp`.** | **Shipped** | +| **`plumbline mcp stats`** | **Render `/api/v1/mcp/stats` (global + per-identity MCP token usage).** | **Shipped** | +| `plumbline infra up \| down \| status \| logs` | Thin wrapper over `docker compose` for users who want explicit infra control | If usage demands it post-v0 | +| `plumbline update` | Detect install method, run matching update, restart server | Release-engineering follow-up | ## Migrations -- `bytebell migrate paths [--dry-run]` — one-shot move of the legacy - on-disk layout (`~/.bytebell/repos//` for clones, - `~/.bytebell/repos/.meta//...` for meta) into the commit-scoped tree - (`~/.bytebell/orgs///////...`). +- `plumbline migrate paths [--dry-run]` — one-shot move of the legacy + on-disk layout (`~/.plumbline/repos//` for clones, + `~/.plumbline/repos/.meta//...` for meta) into the commit-scoped tree + (`~/.plumbline/orgs///////...`). The disk work lives in `@bb/path-migration`; this command supplies the - Mongo knowledge list and renders the summary. The **same reconciliation runs + knowledge list from the document store and renders the summary. The **same reconciliation runs automatically at server boot** (see `@bb/server`), so this command is for running it ahead of time or with `--dry-run` to preview. `--dry-run` prints the plan (including would-be-deleted orphans) without touching disk. Reads - `KnowledgeDoc` from Mongo to derive each knowledge's + `KnowledgeDoc` from the document store to derive each knowledge's `(orgId, owner, repo, commitId)`; knowledges that predate commit tracking (no `source.commitId`) or have no `info.repoUrl` are skipped with a per-id - reason and need manual `bytebell delete` + re-index. Legacy dirs with **no** + reason and need manual `plumbline delete` + re-index. Legacy dirs with **no** backing `KnowledgeDoc` are unrecoverable — they are deleted and reported as `abandoned`. Local-source knowledges keep their original `source.sourcePath` untouched; only their `meta-output` tree moves. @@ -256,7 +256,7 @@ will touch when implemented. Only the **bolded** entries ship in v0. defaults - Live connection probes inside the setup form - First-run auto-launch of setup form (needs the dashboard pane first) -- OpenRouter API key in the setup form (separate `bytebell keys set`) +- OpenRouter API key in the setup form (separate `plumbline keys set`) - Tests — workspace has no test infra yet - Color theming via `kleur` / `picocolors` — manual ANSI for now - Distinct exit codes per failure mode (today: `1` = typed/handled error, @@ -266,7 +266,7 @@ will touch when implemented. Only the **bolded** entries ship in v0. Key source files added in the `setup` command: -- `src/SetupCommand.ts` — commander entry point for `bytebell setup`; orchestrates wizard → config apply → boot → optional index (private-repo PAT + branch selection via `probeRepo`) → MCP-client install. +- `src/SetupCommand.ts` — commander entry point for `plumbline setup`; orchestrates wizard → config apply → boot → optional index (private-repo PAT + branch selection via `probeRepo`) → MCP-client install. - `src/InstallWizard.tsx` — Ink multi-stage wizard component (provider picker + stage routing). - `src/InstallWizardStages.tsx` — `FieldsStage`, `RepoStage`, `ConfirmStage` sub-components (split from `InstallWizard.tsx` to honour the 300-line rule). - `src/indexPoller.ts` — shared `pollIndexToCompletion()` used by both `IndexCommand.ts` and `SetupCommand.ts`; also exports the `IndexResponse` and `RepoStatus` types so neither command duplicates them. @@ -286,7 +286,7 @@ Adding a new subcommand: 2. If interactive panes / forms are needed: add `src/Form.tsx` (or `Pane.tsx`) per [CLAUDE.md _Naming Conventions_](../../CLAUDE.md). 3. Wire into `src/index.ts`: `program.addCommand(buildCommand())`. -4. If the command speaks to `bytebell-server`: HTTP only (e.g. `fetch` +4. If the command speaks to `plumbline-server`: HTTP only (e.g. `fetch` to `http://localhost:`). Never import `@bb/server`. 5. If the command needs OS primitives (`keytar`, `bun:sqlite`, `child_process`): add the dep to `package.json`, but never import a diff --git a/packages/cli/package.json b/packages/cli/package.json index 202d91f..cb10c49 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -12,14 +12,15 @@ "#src/*": "./src/*" }, "bin": { - "bytebell": "./src/index.ts" + "plumbline": "./src/index.ts" }, "dependencies": { "@bb/config": "workspace:*", "@bb/errors": "workspace:*", "@bb/ingest-github": "workspace:*", "@bb/logger": "workspace:*", - "@bb/mongo": "workspace:*", + "@bb/db": "workspace:*", + "@bb/sqlite": "workspace:*", "@bb/path-migration": "workspace:*", "@bb/types": "workspace:*", "commander": "^14.0.3", diff --git a/packages/cli/src/BootCommand.ts b/packages/cli/src/BootCommand.ts index 7159986..53a322c 100644 --- a/packages/cli/src/BootCommand.ts +++ b/packages/cli/src/BootCommand.ts @@ -4,19 +4,19 @@ import { homedir } from "node:os"; import { Command } from "commander"; import React from "react"; import { render } from "ink"; -import { Config, DbProviderType, GraphProviderType } from "@bb/types"; -import { HINTS, getBytebellHome, getConfigValue, isDevMode } from "@bb/config"; +import { Config, GraphProviderType } from "@bb/types"; +import { HINTS, getPlumblineHome, getConfigValue, isDevMode } from "@bb/config"; import { applyInfraDefaults, checkPreflight } from "./bootConfig.ts"; import { SetupForm } from "./SetupForm.tsx"; import { error, info, success } from "./output.ts"; -import { bringInfraUp, usingHonker } from "./bootInfra.ts"; +import { bringInfraUp } from "./bootInfra.ts"; import { isEmbedded } from "./infraMode.ts"; export function buildBootCommand(): Command { const cmd = new Command("boot"); cmd .description( - "Start the bytebell-server. Non-embedded providers (mongo/neo4j/bullmq) bring up Docker infra first; embedded providers (sqlite/ladybug/honker) need no Docker.", + "Start the plumbline-server. A Neo4j graph provider brings up Docker infra first; the embedded provider (ladybug) needs no Docker.", ) .action(runBoot); return cmd; @@ -51,7 +51,6 @@ async function runBoot(): Promise { } } - const dbProvider = getConfigValue(Config.DbProvider); const graphProvider = getConfigValue(Config.GraphProvider); // Embedded mode (sqlite + ladybug + honker) needs no Docker — report the @@ -59,11 +58,11 @@ async function runBoot(): Promise { if (isEmbedded()) { info("embedded mode — no Docker required."); const queueDbPath = getConfigValue(Config.QueueDbPath); - const resolvedQueue = queueDbPath.length > 0 ? expandTilde(queueDbPath) : path.join(getBytebellHome(), "queue.db"); + const resolvedQueue = queueDbPath.length > 0 ? expandTilde(queueDbPath) : path.join(getPlumblineHome(), "queue.db"); success(`queue → honker (sqlite: ${resolvedQueue})`); success(`doc → sqlite`); success(`graph → ladybug`); - process.stdout.write("\nNext: bytebell index or bytebell ingest [path]\n"); + process.stdout.write("\nNext: plumbline index or plumbline ingest [path]\n"); return; } @@ -79,21 +78,14 @@ async function runBoot(): Promise { return; } - if (usingHonker()) { - const queueDbPath = getConfigValue(Config.QueueDbPath); - const resolved = queueDbPath.length > 0 ? expandTilde(queueDbPath) : path.join(getBytebellHome(), "queue.db"); - success(`queue → honker (sqlite: ${resolved})`); - } else { - success(`redis → ${upResult.services.redis}`); - } - if (dbProvider === DbProviderType.Mongo) { - success(`mongo → ${upResult.services.mongo}`); - } + const queueDbPath = getConfigValue(Config.QueueDbPath); + const resolvedQueueDb = queueDbPath.length > 0 ? expandTilde(queueDbPath) : path.join(getPlumblineHome(), "queue.db"); + success(`queue → honker (sqlite: ${resolvedQueueDb})`); if (graphProvider === GraphProviderType.Neo4j) { success(`neo4j → ${upResult.services.neo4j}`); } - process.stdout.write("\nNext: bytebell index or bytebell ingest [path]\n"); + process.stdout.write("\nNext: plumbline index or plumbline ingest [path]\n"); } async function ensurePreflight(): Promise { @@ -105,7 +97,7 @@ async function ensurePreflight(): Promise { reportMissing(initial.missing); return false; } - info("Bytebell needs a few settings before first boot — opening setup form…"); + info("Plumbline needs a few settings before first boot — opening setup form…"); const saved = await renderSetupForm(); if (!saved) { return false; diff --git a/packages/cli/src/DeleteCommand.ts b/packages/cli/src/DeleteCommand.ts index a65fba2..8c3cd5d 100644 --- a/packages/cli/src/DeleteCommand.ts +++ b/packages/cli/src/DeleteCommand.ts @@ -1,6 +1,4 @@ import { Command } from "commander"; -import { Config, DbProviderType } from "@bb/types"; -import { getConfigValue } from "@bb/config"; import { ensureServerRunning } from "./serverSpawn.ts"; import { ServerStartTimeoutError } from "@bb/errors"; import { deleteJson, HttpClientError } from "./httpClient.ts"; @@ -10,17 +8,14 @@ import { error, success } from "./output.ts"; interface DeleteResponse { knowledgeId: string; jobsRemoved: number; - mongoDeleted: number; + dbDeleted: number; rawDeleted: number; statsDeleted: number; } export function buildDeleteCommand(): Command { const cmd = new Command("delete"); - const dbProvider = getConfigValue(Config.DbProvider) === DbProviderType.Sqlite ? "SQLite" : "Mongo"; - cmd - .description(`Pick one or more indexed knowledge entries and delete them from ${dbProvider} + Neo4j.`) - .action(runDelete); + cmd.description("Pick one or more indexed knowledge entries and delete them from SQLite + Neo4j.").action(runDelete); return cmd; } @@ -35,7 +30,7 @@ async function runDelete(): Promise { title: "Select entries to delete", filterKind: "all", multi: true, - emptyMessage: "No indexed knowledge yet. Run `bytebell index ` or `bytebell ingest [path]` to add one.", + emptyMessage: "No indexed knowledge yet. Run `plumbline index ` or `plumbline ingest [path]` to add one.", confirm: { prompt: formatDeletePrompt }, }); if (result === null) { @@ -57,11 +52,10 @@ async function runDelete(): Promise { } function formatDeletePrompt(labels: string[]): string { - const dbProvider = getConfigValue(Config.DbProvider) === DbProviderType.Sqlite ? "SQLite" : "Mongo"; if (labels.length === 1) { - return `Delete ${labels[0]} from ${dbProvider} + Neo4j? [y/N]`; + return `Delete ${labels[0]} from SQLite + Neo4j? [y/N]`; } - return `Delete ${labels.length} entries from ${dbProvider} + Neo4j? [y/N]`; + return `Delete ${labels.length} entries from SQLite + Neo4j? [y/N]`; } function handleError(cause: unknown): void { diff --git a/packages/cli/src/IndexCommand.ts b/packages/cli/src/IndexCommand.ts index 3b7238c..8742c09 100644 --- a/packages/cli/src/IndexCommand.ts +++ b/packages/cli/src/IndexCommand.ts @@ -19,7 +19,7 @@ export function buildIndexCommand(): Command { .option("--token ", "GitHub PAT for private repos") .option( "--verbose", - "stream the server log file to the terminal during the run (set log level via `bytebell set log-level debug` for finer-grained output)", + "stream the server log file to the terminal during the run (set log level via `plumbline set log-level debug` for finer-grained output)", ) .action(runIndex); return cmd; diff --git a/packages/cli/src/LsCommand.ts b/packages/cli/src/LsCommand.ts index 0d7f948..059fdd2 100644 --- a/packages/cli/src/LsCommand.ts +++ b/packages/cli/src/LsCommand.ts @@ -39,7 +39,7 @@ async function runLs(options: { interactive?: boolean }): Promise { const { repos } = await getJson("/api/v1/repos"); if (repos.length === 0) { process.stdout.write( - "No indexed knowledge yet. Run `bytebell index ` or `bytebell ingest [path]` to add one.\n", + "No indexed knowledge yet. Run `plumbline index ` or `plumbline ingest [path]` to add one.\n", ); return; } diff --git a/packages/cli/src/McpCommand.ts b/packages/cli/src/McpCommand.ts index a7e087b..4b21b69 100644 --- a/packages/cli/src/McpCommand.ts +++ b/packages/cli/src/McpCommand.ts @@ -26,7 +26,7 @@ export function buildMcpCommand(): Command { mcp .command("install") - .description("Detect installed coding tools and register the bytebell MCP endpoint in their config.") + .description("Detect installed coding tools and register the plumbline MCP endpoint in their config.") .action(async () => { await runMcpInstall(); }); diff --git a/packages/cli/src/McpToolSelector.tsx b/packages/cli/src/McpToolSelector.tsx index ec02153..6fa9c74 100644 --- a/packages/cli/src/McpToolSelector.tsx +++ b/packages/cli/src/McpToolSelector.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import type { ReactElement } from "react"; import { Box, Text, useApp, useInput } from "ink"; -// Multi-select for `bytebell mcp install`. Mirrors RepoSelector's multi-mode +// Multi-select for `plumbline mcp install`. Mirrors RepoSelector's multi-mode // (space-toggle, enter-submit, esc-cancel) but defaults every row to selected // — the common case is "configure all detected tools" and the user deselects // the ones they don't want. `a` toggles all/none. @@ -79,7 +79,7 @@ export function McpToolSelector({ items, onDone }: McpToolSelectorProps): ReactE - Register bytebell MCP in: + Register plumbline MCP in: {` (${selected.size} selected)`} diff --git a/packages/cli/src/MigratePathsCommand.ts b/packages/cli/src/MigratePathsCommand.ts index ecb7ab9..f2abcf3 100644 --- a/packages/cli/src/MigratePathsCommand.ts +++ b/packages/cli/src/MigratePathsCommand.ts @@ -1,12 +1,13 @@ import { Command } from "commander"; import { Config } from "@bb/types"; -import { getBytebellHome, getConfigValue } from "@bb/config"; -import { connectMongo, closeMongo, listKnowledge } from "@bb/mongo"; +import { getPlumblineHome, getConfigValue } from "@bb/config"; +import { connectDb, closeDb, knowledgeDb } from "@bb/db"; +import "@bb/sqlite"; import { migrateLegacyPaths, type MigrationSummary } from "@bb/path-migration"; import { error, success } from "./output.ts"; // ───────────────────────────────────────────────────────────────────────────── -// `bytebell migrate paths` +// `plumbline migrate paths` // // One-shot reconciliation of the legacy on-disk layout // (`/repos//` for clones, `/repos/.meta//...` @@ -14,7 +15,8 @@ import { error, success } from "./output.ts"; // (`/orgs///////...`). // // The disk work lives in `@bb/path-migration` so the server boot path shares -// it. This command just supplies the knowledge list (from Mongo) and renders +// it. This command just supplies the knowledge list (from the document +// store) and renders // the summary. Knowledge with a DB record migrates; legacy dirs with no record // are abandoned (deleted). The same reconciliation runs automatically at server // boot — this command is for running it ahead of time or with `--dry-run`. @@ -45,15 +47,15 @@ export function buildMigrateCommand(): Command { } async function runPathsMigration(opts: { dryRun: boolean }): Promise { - await connectMongo(); + await connectDb(getConfigValue(Config.DbProvider)); try { - const home = getBytebellHome(); + const home = getPlumblineHome(); const orgId = getConfigValue(Config.OrgId); - const knowledgeDocs = await listKnowledge({ limit: 10_000 }); + const knowledgeDocs = await knowledgeDb.listKnowledge({ limit: 10_000 }); const summary = await migrateLegacyPaths({ home, orgId, knowledgeDocs, dryRun: opts.dryRun }); printSummary(summary, opts.dryRun); } finally { - await closeMongo(); + await closeDb(); } } diff --git a/packages/cli/src/PortConflictSelector.tsx b/packages/cli/src/PortConflictSelector.tsx index 4d22c1e..d73fcc5 100644 --- a/packages/cli/src/PortConflictSelector.tsx +++ b/packages/cli/src/PortConflictSelector.tsx @@ -38,13 +38,13 @@ export function PortConflictSelector(props: PortConflictSelectorProps): ReactEle { action: "reuse", label: `Use the service already running on ${props.port}`, - hint: "skip starting bytebell's container for this service", + hint: "skip starting plumbline's container for this service", }, killChoice(props), { action: "change", - label: `Change bytebell's host port for ${props.serviceLabel}`, - hint: "pick a new free port; bytebell config + compose env are updated", + label: `Change plumbline's host port for ${props.serviceLabel}`, + hint: "pick a new free port; plumbline config + compose env are updated", }, ]; diff --git a/packages/cli/src/PullCommand.ts b/packages/cli/src/PullCommand.ts index 65ffd95..8309ffd 100644 --- a/packages/cli/src/PullCommand.ts +++ b/packages/cli/src/PullCommand.ts @@ -95,7 +95,7 @@ async function runPull( tailer = await startLogTailer("server"); } - // Enqueue all pulls upfront — BullMQ runs workers concurrently in the + // Enqueue all pulls upfront — the queue runs workers concurrently in the // server process, so there's no benefit to serialising the HTTP submits. const enqueueResults = await Promise.all( picks.map(async ({ knowledgeId: targetId }) => { @@ -146,7 +146,7 @@ async function pickRepos(): Promise { const result = await promptRepoSelector({ title: "Select repos to pull", filterKind: "github", - emptyMessage: "No indexed GitHub repos. Run `bytebell index ` first.", + emptyMessage: "No indexed GitHub repos. Run `plumbline index ` first.", }); if (result === null) { return []; diff --git a/packages/cli/src/PullModeSelector.tsx b/packages/cli/src/PullModeSelector.tsx index f6977e8..0a43222 100644 --- a/packages/cli/src/PullModeSelector.tsx +++ b/packages/cli/src/PullModeSelector.tsx @@ -26,7 +26,7 @@ const CHOICES: readonly Choice[] = [ ]; /** - * Two-option chooser shown after a single repo is selected in `bytebell pull`. + * Two-option chooser shown after a single repo is selected in `plumbline pull`. * The user picks between pulling to the branch's HEAD (default behaviour) or * picking a specific commit from the branch history. */ diff --git a/packages/cli/src/README.md b/packages/cli/src/README.md index 10265f1..ed5459b 100644 --- a/packages/cli/src/README.md +++ b/packages/cli/src/README.md @@ -6,7 +6,7 @@ package-level contract; this file documents how the source tree is split. ## Files - **[index.ts](index.ts)** — binary entry. Shebang `#!/usr/bin/env bun`. - Constructs the commander `Command("bytebell")`, wires version, and + Constructs the commander `Command("plumbline")`, wires version, and registers every shipped subcommand: `set`, `boot`, `shutdown`, `server`, `index`, `ingest`, `ls`. Calls `parseAsync`. Top-level `try/catch` prints any uncaught error and exits `2` (the typed-error @@ -21,7 +21,7 @@ package-level contract; this file documents how the source tree is split. its own type narrowing — no `as` casts at the call site. Local helpers `parsePort` / `parsePositiveInt` / `parseLogLevel` throw `Error("Invalid value for \"\": …")` which `SetCommand` pretty- - prints with the matching `bytebell set …` hint from `@bb/config`'s + prints with the matching `plumbline set …` hint from `@bb/config`'s `HINTS`. Carries entries for every config key the user can set headlessly today, including `openrouter-api-key` (`redact: true`) and `openrouter-model` (plain text). @@ -36,13 +36,13 @@ package-level contract; this file documents how the source tree is split. and polls `/health`). Prints a final ready banner with the MCP URL. Idempotent — re-running on an already-up stack is a fast no-op. - **[ShutdownCommand.ts](ShutdownCommand.ts)** — the `shutdown` - subcommand. Reads `~/.bytebell/pid`, sends `SIGTERM`, polls until + subcommand. Reads `~/.plumbline/pid`, sends `SIGTERM`, polls until the PID file vanishes (≤ 30 s), and prints the explicit `docker compose down` hint. Docker is left running by design. Stale PID file is treated as "already stopped" and exits 0. - **[bootConfig.ts](bootConfig.ts)** — `applyInfraDefaults` writes - local-docker defaults (mongo / neo4j / neo4j-user / redis) and a - random base64url 24-byte Neo4j password into `~/.bytebell/config.json` + local-docker defaults (neo4j / neo4j-user) and a + random base64url 24-byte Neo4j password into `~/.plumbline/config.json` via `KEY_MAP[key].setter`, but only for keys that are currently blank. Returns the resolved Neo4j password (whether freshly generated or pre-existing) so `BootCommand` can pass it into @@ -72,13 +72,13 @@ package-level contract; this file documents how the source tree is split. Resolves the endpoint URL from `Config.ServerPort`, filters `MCP_TARGETS` by `detect()`, prompts (interactive multi-select on a TTY; auto-selects all detected when stdin is not a TTY), then - **merges** a `bytebell` entry into each picked tool's config and + **merges** a `plumbline` entry into each picked tool's config and prints a result table. Merge is non-destructive: reads existing JSON - (`{}` on ENOENT), backs up to `.bytebell.bak`, injects only the - `bytebell` key under the tool's top-level key, atomic-writes + (`{}` on ENOENT), backs up to `.plumbline.bak`, injects only the + `plumbline` key under the tool's top-level key, atomic-writes (tmp + rename, mode `0600`). A malformed existing file fails that one tool instead of being overwritten. Idempotent — keyed by the literal - name `bytebell`, so re-running updates (e.g. a changed port) rather + name `plumbline`, so re-running updates (e.g. a changed port) rather than duplicating. - **[mcpTargets.ts](mcpTargets.ts)** — the `MCP_TARGETS` adapter table. One `McpTarget` per supported tool: `configPath()` (platform-branched @@ -97,10 +97,10 @@ package-level contract; this file documents how the source tree is split. view shows `•••…` for masked rows. Renders an inline red error line underneath when the field's `validate` returns a non-null string. - **[SetupForm.tsx](SetupForm.tsx)** — the Ink form rendered by - `bytebell set` no-args **and** by `bytebell boot` when openrouter + `plumbline set` no-args **and** by `plumbline boot` when openrouter keys are missing on an interactive TTY. Nine rows declared in a - `ROWS` constant: Mongo URI / Neo4j URI / Neo4j user / Neo4j password - (masked) / Redis URL / Server port / GitHub Concurrency / OpenRouter + `ROWS` constant: Neo4j URI / Neo4j user / Neo4j password + (masked) / Server port / GitHub Concurrency / OpenRouter API key (masked) / OpenRouter model. Each row carries its own format-only `validate` regex (or non-empty check for the openrouter rows). State: a single `useState>` keyed by @@ -128,12 +128,12 @@ dockerInfra.ts → node:child_process, node:fs/promises, node:path, node:url BootCommand.ts → commander, react, ink (render), @bb/types (Config), @bb/config (HINTS, getConfigValue), bootConfig.ts, dockerInfra.ts, serverSpawn.ts, SetupForm.tsx (SetupForm), output.ts -ShutdownCommand.ts → commander, node:fs/promises, node:path, @bb/config (getBytebellHome), +ShutdownCommand.ts → commander, node:fs/promises, node:path, @bb/config (getPlumblineHome), dockerInfra.ts (composeFilePath), output.ts httpClient.ts → node:url serverSpawn.ts → node:child_process, node:fs/promises, node:path, node:url, - @bb/types (Config), @bb/config (getBytebellHome, getConfigValue) + @bb/types (Config), @bb/config (getPlumblineHome, getConfigValue) mcpTargets.ts → node:path, node:fs (existsSync), node:os (homedir) McpToolSelector.tsx → ink, react (type-only) @@ -179,11 +179,11 @@ context. a closure (or with a wrong-type setter) is a compile error. - **No env reads anywhere.** Repo-wide ESLint rule blocks `process.env`. - **`bootConfig.applyInfraDefaults` only writes blank keys.** Re-running - `bytebell boot` after a manual `bytebell set neo4j-password ` + `plumbline boot` after a manual `plumbline set neo4j-password ` reads the user's value back via `getConfigValue` and uses it for the compose `.env`; it does **not** overwrite the user's choice. - **`dockerInfra` resolves the compose file via `import.meta.url`.** - No env vars, no cwd dependence — `bytebell boot` works from any + No env vars, no cwd dependence — `plumbline boot` works from any directory and from `bun link`'d installs. - **`ShutdownCommand` never escalates to `SIGKILL`.** If the server doesn't drain in 30 s the command exits 1 with a warning; the diff --git a/packages/cli/src/ServerCommand.ts b/packages/cli/src/ServerCommand.ts index 2dff356..e4117f4 100644 --- a/packages/cli/src/ServerCommand.ts +++ b/packages/cli/src/ServerCommand.ts @@ -6,9 +6,9 @@ import { error } from "./output.ts"; export function buildServerCommand(): Command { const cmd = new Command("server"); - cmd.description("Manage the bytebell-server daemon."); + cmd.description("Manage the plumbline-server daemon."); const start = new Command("start"); - start.description("Start the bytebell-server in the foreground (Ctrl+C to stop)."); + start.description("Start the plumbline-server in the foreground (Ctrl+C to stop)."); start.action(runStart); cmd.addCommand(start); return cmd; diff --git a/packages/cli/src/SetupCommand.ts b/packages/cli/src/SetupCommand.ts index 8ad6942..6be48bd 100644 --- a/packages/cli/src/SetupCommand.ts +++ b/packages/cli/src/SetupCommand.ts @@ -23,7 +23,7 @@ export function buildSetupCommand(): Command { async function runSetup(): Promise { if (process.stdin.isTTY !== true) { - error("bytebell setup requires an interactive terminal. Run it directly, not piped."); + error("plumbline setup requires an interactive terminal. Run it directly, not piped."); process.exitCode = 1; return; } @@ -55,7 +55,7 @@ async function connectMcp(): Promise { return; } const port = getConfigValue(Config.ServerPort); - success(`Connect Claude Code:\n claude mcp add --transport http bytebell http://127.0.0.1:${port}/mcp`); + success(`Connect Claude Code:\n claude mcp add --transport http plumbline http://127.0.0.1:${port}/mcp`); } function runWizard(): Promise { diff --git a/packages/cli/src/SetupForm.tsx b/packages/cli/src/SetupForm.tsx index 0350856..5b57a9a 100644 --- a/packages/cli/src/SetupForm.tsx +++ b/packages/cli/src/SetupForm.tsx @@ -20,18 +20,9 @@ interface Row { validate: (raw: string) => string | null; } -const MONGO_RX = /^mongodb(\+srv)?:\/\//u; const NEO4J_RX = /^(bolt|neo4j)(\+s|\+ssc)?:\/\//u; -const REDIS_RX = /^rediss?:\/\//u; const ROWS: Row[] = [ - { - id: "mongo", - label: "Mongo URI", - cliKey: "mongo", - infra: true, - validate: (s) => (MONGO_RX.test(s) ? null : "expected mongodb:// or mongodb+srv://"), - }, { id: "neo4j", label: "Neo4j URI", @@ -54,13 +45,6 @@ const ROWS: Row[] = [ infra: true, validate: (s) => (s.length > 0 ? null : "required"), }, - { - id: "redis", - label: "Redis URL", - cliKey: "redis", - infra: true, - validate: (s) => (REDIS_RX.test(s) ? null : "expected redis:// or rediss://"), - }, { id: "port", label: "Server port", @@ -90,11 +74,9 @@ const ROWS: Row[] = [ function loadInitial(): Record { return { - mongo: getConfigValue(Config.MongoUri), neo4j: getConfigValue(Config.Neo4jUri), "neo4j-user": getConfigValue(Config.Neo4jUser), "neo4j-password": getConfigValue(Config.Neo4jPassword), - redis: getConfigValue(Config.RedisUrl), port: String(getConfigValue(Config.ServerPort)), "concurrency-github": String(getConfigValue(Config.ConcurrencyGithub)), "openrouter-api-key": getConfigValue(Config.OpenrouterApiKey), @@ -148,7 +130,7 @@ export function SetupForm({ onDone }: SetupFormProps): ReactElement { return ( - Bytebell setup + Plumbline setup runShutdown(opts)); diff --git a/packages/cli/src/StopInfraPrompt.tsx b/packages/cli/src/StopInfraPrompt.tsx index e898621..ed049a1 100644 --- a/packages/cli/src/StopInfraPrompt.tsx +++ b/packages/cli/src/StopInfraPrompt.tsx @@ -45,7 +45,7 @@ export function StopInfraPrompt(props: StopInfraPromptProps): ReactElement { return ( - Also stop Docker infra (mongo + neo4j + redis)? + Also stop Docker infra (neo4j)? {CHOICES.map((choice, i) => { const selected = i === index; return ( diff --git a/packages/cli/src/bootConfig.ts b/packages/cli/src/bootConfig.ts index df68b8e..83ead7a 100644 --- a/packages/cli/src/bootConfig.ts +++ b/packages/cli/src/bootConfig.ts @@ -2,17 +2,15 @@ import { randomBytes } from "node:crypto"; import path from "node:path"; import { Config, DbProviderType, GraphProviderType, QueueProviderType } from "@bb/types"; -import { getBytebellHome, getConfigValue, requiredKeysFor } from "@bb/config"; +import { getPlumblineHome, getConfigValue, requiredKeysFor } from "@bb/config"; import { bringInfraUp } from "./dockerBoot.ts"; import { KEY_MAP } from "./keyMap.ts"; import { success, error, info } from "./output.ts"; import { isEmbedded } from "./infraMode.ts"; import { startServer } from "./serverLifecycle.ts"; -const DEFAULT_MONGO_URI = "mongodb://127.0.0.1:27017/bytebell"; const DEFAULT_NEO4J_URI = "bolt://127.0.0.1:7687"; const DEFAULT_NEO4J_USER = "neo4j"; -const DEFAULT_REDIS_URL = "redis://127.0.0.1:6379"; interface DefaultEntry { cliKey: string; @@ -22,50 +20,39 @@ interface DefaultEntry { needed: () => boolean; } -function usingMongo(): boolean { - return getConfigValue(Config.DbProvider) === DbProviderType.Mongo; -} - function usingNeo4j(): boolean { return getConfigValue(Config.GraphProvider) === GraphProviderType.Neo4j; } const DEFAULTS: readonly DefaultEntry[] = [ - { cliKey: "mongo", configKey: Config.MongoUri, computeDefault: () => DEFAULT_MONGO_URI, needed: usingMongo }, { cliKey: "neo4j", configKey: Config.Neo4jUri, computeDefault: () => DEFAULT_NEO4J_URI, needed: usingNeo4j }, { cliKey: "neo4j-user", configKey: Config.Neo4jUser, computeDefault: () => DEFAULT_NEO4J_USER, needed: usingNeo4j }, - { - cliKey: "redis", - configKey: Config.RedisUrl, - computeDefault: () => DEFAULT_REDIS_URL, - needed: () => getConfigValue(Config.QueueProvider) === QueueProviderType.Bullmq, - }, { cliKey: "neo4j-password", configKey: Config.Neo4jPassword, computeDefault: generateNeo4jPassword, needed: usingNeo4j, }, - // Embedded stores live under ~/.bytebell. These are auto-filled (and required - // by the server preflight) because the providers don't all default to a - // persistent path on their own — notably Ladybug treats an empty path as + // File-backed stores live under ~/.plumbline. These are auto-filled (and + // required by the server preflight) because the providers don't all default + // to a persistent path on their own — notably Ladybug treats an empty path as // in-memory, which would silently lose the graph on restart. { cliKey: "sqlite-path", configKey: Config.SqlitePath, - computeDefault: () => path.join(getBytebellHome(), "data.sqlite"), + computeDefault: () => path.join(getPlumblineHome(), "data.sqlite"), needed: () => getConfigValue(Config.DbProvider) === DbProviderType.Sqlite, }, { cliKey: "ladybug-path", configKey: Config.LadybugPath, - computeDefault: () => path.join(getBytebellHome(), "ladybug.lbug"), + computeDefault: () => path.join(getPlumblineHome(), "ladybug.lbug"), needed: () => getConfigValue(Config.GraphProvider) === GraphProviderType.Ladybug, }, { cliKey: "queue-db-path", configKey: Config.QueueDbPath, - computeDefault: () => path.join(getBytebellHome(), "queue.db"), + computeDefault: () => path.join(getPlumblineHome(), "queue.db"), needed: () => getConfigValue(Config.QueueProvider) === QueueProviderType.Honker, }, ]; @@ -151,15 +138,9 @@ export async function runBootSequence(): Promise { if (upResult === null) { return false; } - if (getConfigValue(Config.DbProvider) === DbProviderType.Mongo) { - success(`mongo → ${upResult.services.mongo}`); - } if (getConfigValue(Config.GraphProvider) === GraphProviderType.Neo4j) { success(`neo4j → ${upResult.services.neo4j}`); } - if (getConfigValue(Config.QueueProvider) === QueueProviderType.Bullmq) { - success(`redis → ${upResult.services.redis}`); - } } const started = await startServer(); diff --git a/packages/cli/src/bootInfra.ts b/packages/cli/src/bootInfra.ts index 9ccabc0..be39339 100644 --- a/packages/cli/src/bootInfra.ts +++ b/packages/cli/src/bootInfra.ts @@ -1,6 +1,4 @@ // SPDX-License-Identifier: AGPL-3.0-only WITH non-commercial-clause -import { Config, QueueProviderType } from "@bb/types"; -import { getConfigValue } from "@bb/config"; import { composeServicesNeeded } from "./infraMode.ts"; import { DockerComposeError, @@ -26,12 +24,8 @@ import { removeContainer } from "./dockerPortDiagnostics.ts"; const MAX_CONFLICT_ROUNDS = 4; -export function usingHonker(): boolean { - return getConfigValue(Config.QueueProvider) === QueueProviderType.Honker; -} - export async function bringInfraUp(neo4jPassword: string): Promise { - const skipServices = new Set<"mongo" | "neo4j" | "redis">(); + const skipServices = new Set<"neo4j">(); for (let round = 0; round < MAX_CONFLICT_ROUNDS; round += 1) { const ports = readInfraPorts(); const watched = composeServicesToStart(skipServices); @@ -45,7 +39,7 @@ export async function bringInfraUp(neo4jPassword: string): Promise, + skipServices: Set<"neo4j">, ): Promise { const infraService = serviceForPort(cause.port, ports); if (infraService === null) { - error(`Port ${cause.port} conflict, but it doesn't match a known bytebell service. Aborting.`); + error(`Port ${cause.port} conflict, but it doesn't match a known plumbline service. Aborting.`); info(cause.stderr.trim()); return false; } @@ -117,7 +111,7 @@ async function handlePortConflict( return false; } setInfraPort(infraService, newPort); - success(`updated bytebell ${serviceLabel} → port ${newPort}.`); + success(`updated plumbline ${serviceLabel} → port ${newPort}.`); skipServices.delete(composeService); return true; } @@ -132,28 +126,16 @@ async function safeComposeDown(): Promise { } } -function composeServicesToStart(skip: Set<"mongo" | "neo4j" | "redis">): readonly ("mongo" | "neo4j" | "redis")[] { +function composeServicesToStart(skip: Set<"neo4j">): readonly "neo4j"[] { const needed = composeServicesNeeded(); - return (["mongo", "neo4j", "redis"] as const).filter((s) => needed.has(s) && !skip.has(s)); + return (["neo4j"] as const).filter((s) => needed.has(s) && !skip.has(s)); } -function composeServiceFor(service: InfraService): "mongo" | "neo4j" | "redis" { - if (service === "mongo") { - return "mongo"; - } - if (service === "redis") { - return "redis"; - } +function composeServiceFor(_service: InfraService): "neo4j" { return "neo4j"; } -function portFor(service: "mongo" | "neo4j" | "redis", ports: InfraPorts): number { - if (service === "mongo") { - return ports.mongo; - } - if (service === "redis") { - return ports.redis; - } +function portFor(_service: "neo4j", ports: InfraPorts): number { return ports.neo4jBolt; } diff --git a/packages/cli/src/dockerBoot.ts b/packages/cli/src/dockerBoot.ts index a230eb1..1014145 100644 --- a/packages/cli/src/dockerBoot.ts +++ b/packages/cli/src/dockerBoot.ts @@ -25,7 +25,7 @@ import { composeServicesNeeded } from "./infraMode.ts"; const MAX_CONFLICT_ROUNDS = 4; export async function bringInfraUp(neo4jPassword: string): Promise { - const skipServices = new Set<"mongo" | "neo4j" | "redis">(); + const skipServices = new Set<"neo4j">(); for (let round = 0; round < MAX_CONFLICT_ROUNDS; round += 1) { const ports = readInfraPorts(); const watched = composeServicesToStart(skipServices); @@ -39,7 +39,7 @@ export async function bringInfraUp(neo4jPassword: string): Promise, + skipServices: Set<"neo4j">, ): Promise { const infraService = serviceForPort(cause.port, ports); if (infraService === null) { - error(`Port ${cause.port} conflict, but it doesn't match a known bytebell service. Aborting.`); + error(`Port ${cause.port} conflict, but it doesn't match a known plumbline service. Aborting.`); info(cause.stderr.trim()); return false; } @@ -118,7 +118,7 @@ async function handlePortConflict( return false; } setInfraPort(infraService, newPort); - success(`updated bytebell ${serviceLabel} → port ${newPort}.`); + success(`updated plumbline ${serviceLabel} → port ${newPort}.`); skipServices.delete(composeService); return true; } @@ -133,28 +133,16 @@ async function safeComposeDown(): Promise { } } -function composeServicesToStart(skip: Set<"mongo" | "neo4j" | "redis">): readonly ("mongo" | "neo4j" | "redis")[] { +function composeServicesToStart(skip: Set<"neo4j">): readonly "neo4j"[] { const needed = composeServicesNeeded(); - return (["mongo", "neo4j", "redis"] as const).filter((s) => needed.has(s) && !skip.has(s)); + return (["neo4j"] as const).filter((s) => needed.has(s) && !skip.has(s)); } -function composeServiceFor(service: InfraService): "mongo" | "neo4j" | "redis" { - if (service === "mongo") { - return "mongo"; - } - if (service === "redis") { - return "redis"; - } +function composeServiceFor(_service: InfraService): "neo4j" { return "neo4j"; } -function portFor(service: "mongo" | "neo4j" | "redis", ports: InfraPorts): number { - if (service === "mongo") { - return ports.mongo; - } - if (service === "redis") { - return ports.redis; - } +function portFor(_service: "neo4j", ports: InfraPorts): number { return ports.neo4jBolt; } diff --git a/packages/cli/src/dockerInfra.ts b/packages/cli/src/dockerInfra.ts index b18801c..8a15a0d 100644 --- a/packages/cli/src/dockerInfra.ts +++ b/packages/cli/src/dockerInfra.ts @@ -7,7 +7,7 @@ import { parsePortFromComposeError } from "./dockerPortDiagnostics.ts"; const COMPOSE_HEALTH_POLL_MS = 2_000; const COMPOSE_HEALTH_TIMEOUT_MS = 90_000; -const SERVICES = ["mongo", "neo4j", "redis"] as const; +const SERVICES = ["neo4j"] as const; type ServiceName = (typeof SERVICES)[number]; @@ -87,9 +87,7 @@ export async function up(opts: UpOptions): Promise { return { composeFile: composeFilePath(), services: { - mongo: `127.0.0.1:${opts.ports.mongo}`, neo4j: `127.0.0.1:${opts.ports.neo4jBolt} (HTTP ${opts.ports.neo4jHttp})`, - redis: `127.0.0.1:${opts.ports.redis}`, }, }; } diff --git a/packages/cli/src/dockerPortDiagnostics.ts b/packages/cli/src/dockerPortDiagnostics.ts index 7e9a125..ee775f1 100644 --- a/packages/cli/src/dockerPortDiagnostics.ts +++ b/packages/cli/src/dockerPortDiagnostics.ts @@ -9,7 +9,7 @@ export interface ConflictingContainer { id: string; name: string; image: string; - isBytebell: boolean; + isPlumbline: boolean; } export interface ConflictingHostProcess { @@ -47,7 +47,7 @@ export async function findContainerOnPort(port: number): Promise { - const program = new Command("bytebell"); - program.version(VERSION).description("Bytebell — local knowledge engine TUI"); + const program = new Command("plumbline"); + program.version(VERSION).description("Plumbline — local knowledge engine TUI"); program.addCommand(buildSetCommand()); program.addCommand(buildSetupCommand()); program.addCommand(buildBootCommand()); diff --git a/packages/cli/src/infraMode.ts b/packages/cli/src/infraMode.ts index c360284..ebf7561 100644 --- a/packages/cli/src/infraMode.ts +++ b/packages/cli/src/infraMode.ts @@ -1,18 +1,20 @@ // SPDX-License-Identifier: AGPL-3.0-only WITH non-commercial-clause import path from "node:path"; import { Config, DbProviderType, GraphProviderType, QueueProviderType } from "@bb/types"; -import { getBytebellHome, getConfigValue, setConfigValue } from "@bb/config"; +import { getPlumblineHome, getConfigValue, setConfigValue } from "@bb/config"; /** * Infrastructure mode is not a stored flag — it's derived from the three * provider settings. There are two coherent presets: * - * • "docker" (non-embedded) — Mongo + Neo4j + BullMQ. Requires Docker. + * • "docker" (non-embedded) — SQLite + Neo4j + Honker. Requires Docker. * • "embedded" — SQLite + Ladybug + Honker. Zero Docker. * - * The providers remain the single source of truth; `mode` is a convenience the - * setup surfaces use to set all three at once and to decide whether `boot` - * should bring Docker up. + * The document store is SQLite in both: it is file-backed and needs no + * container, so only the graph and queue providers decide whether Docker is + * involved. The providers remain the single source of truth; `mode` is a + * convenience the setup surfaces use to set all three at once and to decide + * whether `boot` should bring Docker up. */ export type InfraMode = "docker" | "embedded"; @@ -31,12 +33,12 @@ export const INFRA_MODE_OPTIONS: readonly InfraModeOption[] = [ { value: "embedded", label: "Embedded (recommended)", - hint: "SQLite + Ladybug + Honker — no Docker, everything in local files under ~/.bytebell", + hint: "SQLite + Ladybug + Honker — no Docker, everything in local files under ~/.plumbline", }, { value: "docker", label: "Docker", - hint: "Mongo + Neo4j + Redis — Docker needed (Docker Desktop/engine must be running)", + hint: "Neo4j — Docker needed (Docker Desktop/engine must be running)", }, ]; @@ -57,9 +59,9 @@ interface ProviderTriple { } export const DOCKER_PROVIDERS: ProviderTriple = { - db: DbProviderType.Mongo, + db: DbProviderType.Sqlite, graph: GraphProviderType.Neo4j, - queue: QueueProviderType.Bullmq, + queue: QueueProviderType.Honker, }; export const EMBEDDED_PROVIDERS: ProviderTriple = { @@ -68,23 +70,21 @@ export const EMBEDDED_PROVIDERS: ProviderTriple = { queue: QueueProviderType.Honker, }; -export type ComposeService = "mongo" | "neo4j" | "redis"; +export type ComposeService = "neo4j"; /** * The Docker compose services the current provider combo requires. Empty when * every provider is file-based (embedded mode). + * + * The document store (SQLite) and the queue (Honker) are file-backed in both + * presets and never contribute a container, so the graph provider alone decides + * whether Docker is involved. */ export function composeServicesNeeded(): Set { const needed = new Set(); - if (getConfigValue(Config.DbProvider) === DbProviderType.Mongo) { - needed.add("mongo"); - } if (getConfigValue(Config.GraphProvider) === GraphProviderType.Neo4j) { needed.add("neo4j"); } - if (getConfigValue(Config.QueueProvider) === QueueProviderType.Bullmq) { - needed.add("redis"); - } return needed; } @@ -99,30 +99,39 @@ export function isEmbedded(): boolean { } /** - * Embedded-mode store paths, derived from the bytebell home so the user never - * has to set them by hand. Filled on entering embedded mode; an existing - * non-empty value (an explicit override) is left untouched. + * Store paths derived from the plumbline home so the user never has to set them + * by hand. An existing non-empty value (an explicit override) is left untouched. + * + * SQLite (documents) and Honker (queue) back both presets, so their paths are + * always seeded; the Ladybug graph file only exists in embedded mode. */ -const EMBEDDED_PATH_DEFAULTS: ReadonlyArray = [ +const SHARED_PATH_DEFAULTS: ReadonlyArray = [ [Config.SqlitePath, "data.sqlite"], - [Config.LadybugPath, "ladybug.lbug"], [Config.QueueDbPath, "queue.db"], ]; +const EMBEDDED_PATH_DEFAULTS: ReadonlyArray = [[Config.LadybugPath, "ladybug.lbug"]]; + +/** Fill any unset path key with `/`. */ +function seedPaths(entries: ReadonlyArray): void { + const home = getPlumblineHome(); + for (const [key, filename] of entries) { + const current = getConfigValue(key); + if (typeof current === "string" && current.length === 0) { + setConfigValue(key, path.join(home, filename)); + } + } +} + /** Apply one of the two presets to the three provider config keys. */ export function applyInfraMode(mode: InfraMode): void { const providers = mode === "embedded" ? EMBEDDED_PROVIDERS : DOCKER_PROVIDERS; setConfigValue(Config.DbProvider, providers.db); setConfigValue(Config.GraphProvider, providers.graph); setConfigValue(Config.QueueProvider, providers.queue); + seedPaths(SHARED_PATH_DEFAULTS); if (mode !== "embedded") { return; } - const home = getBytebellHome(); - for (const [key, filename] of EMBEDDED_PATH_DEFAULTS) { - const current = getConfigValue(key); - if (typeof current === "string" && current.length === 0) { - setConfigValue(key, path.join(home, filename)); - } - } + seedPaths(EMBEDDED_PATH_DEFAULTS); } diff --git a/packages/cli/src/infraPorts.ts b/packages/cli/src/infraPorts.ts index 5b6787f..940e8db 100644 --- a/packages/cli/src/infraPorts.ts +++ b/packages/cli/src/infraPorts.ts @@ -2,86 +2,62 @@ import { Config } from "@bb/types"; import { getConfigValue, setConfigValue } from "@bb/config"; -export type InfraService = "mongo" | "neo4j-bolt" | "neo4j-http" | "redis"; +export type InfraService = "neo4j-bolt" | "neo4j-http"; export const NEO4J_HTTP_BOLT_OFFSET = 213; const DEFAULTS: Record = { - mongo: 27017, "neo4j-bolt": 7687, "neo4j-http": 7474, - redis: 6379, }; export interface InfraPorts { - mongo: number; neo4jBolt: number; neo4jHttp: number; - redis: number; } export function readInfraPorts(): InfraPorts { const boltPort = portFromUri(readString(Config.Neo4jUri), DEFAULTS["neo4j-bolt"]); return { - mongo: portFromUri(readString(Config.MongoUri), DEFAULTS.mongo), neo4jBolt: boltPort, neo4jHttp: deriveHttpPort(boltPort), - redis: portFromUri(readString(Config.RedisUrl), DEFAULTS.redis), }; } export function serviceForPort(port: number, ports: InfraPorts): InfraService | null { - if (port === ports.mongo) { - return "mongo"; - } if (port === ports.neo4jBolt) { return "neo4j-bolt"; } if (port === ports.neo4jHttp) { return "neo4j-http"; } - if (port === ports.redis) { - return "redis"; - } return null; } export function setInfraPort(service: InfraService, newPort: number): void { switch (service) { - case "mongo": - setConfigValue(Config.MongoUri, replacePort(readString(Config.MongoUri), newPort)); - return; case "neo4j-bolt": case "neo4j-http": setConfigValue(Config.Neo4jUri, replacePort(readString(Config.Neo4jUri), boltPortForService(service, newPort))); return; - case "redis": - setConfigValue(Config.RedisUrl, replacePort(readString(Config.RedisUrl), newPort)); - return; } } export function envFileBody(ports: InfraPorts, neo4jPassword: string): string { return [ `NEO4J_PASSWORD=${neo4jPassword}`, - `MONGO_HOST_PORT=${ports.mongo}`, `NEO4J_BOLT_HOST_PORT=${ports.neo4jBolt}`, `NEO4J_HTTP_HOST_PORT=${ports.neo4jHttp}`, - `REDIS_HOST_PORT=${ports.redis}`, "", ].join("\n"); } export function labelForService(service: InfraService): string { switch (service) { - case "mongo": - return "mongo"; case "neo4j-bolt": return "neo4j (bolt)"; case "neo4j-http": return "neo4j (http UI)"; - case "redis": - return "redis"; } } diff --git a/packages/cli/src/keyMap.ts b/packages/cli/src/keyMap.ts index 5481c9f..a595422 100644 --- a/packages/cli/src/keyMap.ts +++ b/packages/cli/src/keyMap.ts @@ -9,7 +9,7 @@ export interface KeyEntry { setter: Setter; /** * For two-value enum keys (e.g. provider toggles): the allowed pair. When - * `bytebell set ` is run with no value, the CLI flips to the other one. + * `plumbline set ` is run with no value, the CLI flips to the other one. */ toggleValues?: readonly [string, string]; } @@ -72,11 +72,6 @@ function parseBoolean(raw: string, key: string): boolean { } export const KEY_MAP: Record = { - mongo: { - configKey: Config.MongoUri, - redact: false, - setter: (s) => setConfigValue(Config.MongoUri, s), - }, neo4j: { configKey: Config.Neo4jUri, redact: false, @@ -92,11 +87,6 @@ export const KEY_MAP: Record = { redact: true, setter: (s) => setConfigValue(Config.Neo4jPassword, s), }, - redis: { - configKey: Config.RedisUrl, - redact: false, - setter: (s) => setConfigValue(Config.RedisUrl, s), - }, port: { configKey: Config.ServerPort, redact: false, @@ -171,7 +161,6 @@ export const KEY_MAP: Record = { configKey: Config.DbProvider, redact: false, setter: (s) => setConfigValue(Config.DbProvider, parseEnum(s, "db-provider", DB_PROVIDERS)), - toggleValues: [DbProviderType.Mongo, DbProviderType.Sqlite], }, "graph-provider": { configKey: Config.GraphProvider, @@ -193,7 +182,6 @@ export const KEY_MAP: Record = { configKey: Config.QueueProvider, redact: false, setter: (s) => setConfigValue(Config.QueueProvider, parseEnum(s, "queue-provider", QUEUE_PROVIDERS)), - toggleValues: [QueueProviderType.Bullmq, QueueProviderType.Honker], }, "queue-db-path": { configKey: Config.QueueDbPath, diff --git a/packages/cli/src/mcpInstall.ts b/packages/cli/src/mcpInstall.ts index 37d7f08..dae55e4 100644 --- a/packages/cli/src/mcpInstall.ts +++ b/packages/cli/src/mcpInstall.ts @@ -48,8 +48,8 @@ export async function runMcpInstall(): Promise { }; } -// Merge the bytebell entry into one tool's config. Never clobbers: reads the -// existing JSON, backs it up, injects only the `bytebell` key under the tool's +// Merge the plumbline entry into one tool's config. Never clobbers: reads the +// existing JSON, backs it up, injects only the `plumbline` key under the tool's // top-level key, and atomic-writes. A malformed existing file fails this tool // (caught below) rather than being overwritten. function applyTarget(target: McpTarget, url: string): InstallResult { @@ -57,12 +57,12 @@ function applyTarget(target: McpTarget, url: string): InstallResult { try { const doc = readJsonObject(file); if (existsSync(file)) { - copyFileSync(file, `${file}.bytebell.bak`); + copyFileSync(file, `${file}.plumbline.bak`); } else { mkdirSync(path.dirname(file), { recursive: true }); } const servers = asObject(doc[target.topLevelKey]); - servers["bytebell"] = target.entry(url); + servers["plumbline"] = target.entry(url); doc[target.topLevelKey] = servers; atomicWriteJson(file, doc); return { label: target.label, status: "configured", detail: file }; diff --git a/packages/cli/src/mcpTargets.ts b/packages/cli/src/mcpTargets.ts index ff477c7..55f8685 100644 --- a/packages/cli/src/mcpTargets.ts +++ b/packages/cli/src/mcpTargets.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { existsSync } from "node:fs"; import { homedir } from "node:os"; -// Per-tool adapter table for `bytebell mcp install`. Each target knows how to +// Per-tool adapter table for `plumbline mcp install`. Each target knows how to // locate its config file, whether the tool looks installed, and the exact // JSON shape its MCP server entry takes (the part that differs per tool). // @@ -21,7 +21,7 @@ export interface McpTarget { configPath(): string; /** Heuristic "is this tool installed?" — config file or its app dir exists. */ detect(): boolean; - /** The `bytebell` server entry. Shape varies per tool. */ + /** The `plumbline` server entry. Shape varies per tool. */ entry(url: string): Record; } diff --git a/packages/cli/src/portConflictPrompt.ts b/packages/cli/src/portConflictPrompt.ts index 70c3285..31e8dc4 100644 --- a/packages/cli/src/portConflictPrompt.ts +++ b/packages/cli/src/portConflictPrompt.ts @@ -51,7 +51,7 @@ export async function promptPortConflict(ctx: PortConflictContext): Promise` to add one."}\n`); + process.stdout.write(`${opts.emptyMessage ?? "No matching repos. Run `plumbline index ` to add one."}\n`); return null; } diff --git a/packages/cli/src/serverLifecycle.ts b/packages/cli/src/serverLifecycle.ts index 3b5ff31..9e91d48 100644 --- a/packages/cli/src/serverLifecycle.ts +++ b/packages/cli/src/serverLifecycle.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only WITH non-commercial-clause import { readFile, stat } from "node:fs/promises"; import path from "node:path"; -import { getBytebellHome } from "@bb/config"; +import { getPlumblineHome } from "@bb/config"; import { ServerStartTimeoutError, ServerInfraDownError, @@ -62,7 +62,7 @@ export interface StopServerResult { } export async function stopServer(): Promise { - const pidFile = path.join(getBytebellHome(), "pid"); + const pidFile = path.join(getPlumblineHome(), "pid"); const pid = await readPid(pidFile); if (pid === null) { return { wasRunning: false, timedOut: false, pid: null }; diff --git a/packages/cli/src/serverSpawn.ts b/packages/cli/src/serverSpawn.ts index 32f1c2a..7798217 100644 --- a/packages/cli/src/serverSpawn.ts +++ b/packages/cli/src/serverSpawn.ts @@ -40,7 +40,7 @@ async function tcpReachable(host: string, port: number): Promise { function parseHostPort(uri: string): { host: string; port: number } | null { try { const u = new URL(uri); - const defaultPort = u.protocol === "bolt:" ? 7687 : u.protocol === "redis:" ? 6379 : 27017; + const defaultPort = u.protocol === "bolt:" ? 7687 : 0; const port = u.port !== "" ? Number.parseInt(u.port, 10) : defaultPort; return { host: u.hostname || "127.0.0.1", port }; } catch { @@ -50,14 +50,10 @@ function parseHostPort(uri: string): { host: string; port: number } | null { async function checkInfraReachable(): Promise { // Only probe the services the active provider combo actually uses. Embedded - // mode (sqlite + ladybug + honker) needs none — composeServicesNeeded() is - // empty, so we skip the probe entirely. + // mode (ladybug) needs none — composeServicesNeeded() is empty, so + // we skip the probe entirely. const needed = composeServicesNeeded(); - const checks: { name: ComposeService; uri: string }[] = [ - { name: "mongo", uri: getConfigValue(Config.MongoUri) }, - { name: "redis", uri: getConfigValue(Config.RedisUrl) }, - { name: "neo4j", uri: getConfigValue(Config.Neo4jUri) }, - ]; + const checks: { name: ComposeService; uri: string }[] = [{ name: "neo4j", uri: getConfigValue(Config.Neo4jUri) }]; const down: { name: string; uri: string }[] = []; for (const check of checks) { if (!needed.has(check.name)) { @@ -88,7 +84,7 @@ export async function ensureServerRunning(onProgress?: (line: string) => void): if (await isHealthy()) { // The running server was spawned in whatever env existed at boot. We // can't introspect its environment from here, but if the CURRENT process - // has BYTEBELL_DEV=1 set we surface a mismatch hint — without it, users + // has PLUMBLINE_DEV=1 set we surface a mismatch hint — without it, users // assume the running server picked up the toggle when it hasn't. return { alreadyRunning: true, devModeMismatch: isDevMode() }; } diff --git a/packages/config/README.md b/packages/config/README.md index 57ba333..0747030 100644 --- a/packages/config/README.md +++ b/packages/config/README.md @@ -9,9 +9,9 @@ Binaries). ## Responsibility Single source of truth for runtime settings stored in -`~/.bytebell/config.json`. Owns: +`~/.plumbline/config.json`. Owns: -- Path resolution for `~/.bytebell/` and `config.json` +- Path resolution for `~/.plumbline/` and `config.json` - Schema (Zod, strict mode) + typed `Config` enum - First-run materialization of a default `config.json` - Memoized in-process load @@ -20,27 +20,27 @@ Single source of truth for runtime settings stored in This package does **not** read from `process.env` and never will. -`setBytebellHomeResolver` registers an override function invoked on every -`getBytebellHome()` call (no caching). The resolver returns the home directory +`setPlumblineHomeResolver` registers an override function invoked on every +`getPlumblineHome()` call (no caching). The resolver returns the home directory to use for the current invocation, or `null` to fall through to the -`~/.bytebell` default. Pass `null` to clear. +`~/.plumbline` default. Pass `null` to clear. `seedConfig` injects a pre-parsed config object into the in-memory cache, validated through `configSchema.parse`. When seeded, `loadConfig()` returns -the seeded values and **does not** call `ensureBytebellHome()` or read +the seeded values and **does not** call `ensurePlumblineHome()` or read `config.json`. The cache invalidator is also no-op while seeded, so the seed survives unexpected `__notifyConfigChanged` events. `setConfigValue` throws `ConfigSeededError` when invoked against a seeded cache — writes are disabled in that mode. When `seedConfig` is never called, behaviour is bit-for-bit the -disk-backed path: `loadConfig()` materializes `~/.bytebell/config.json` on +disk-backed path: `loadConfig()` materializes `~/.plumbline/config.json` on first read and `setConfigValue` performs atomic writes. The `Config` enum lives in `@bb/types`; `ConfigIncompleteError` lives in `@bb/errors`. Both are imported from those packages directly, not from `@bb/config`. -Config keys (v0): `server_port`, `mongo_uri`, `neo4j_uri`, `neo4j_user`, -`neo4j_password`, `redis_url`, `openrouter_api_key`, `openrouter_model`, +Config keys (v0): `server_port`, `sqlite_path`, `neo4j_uri`, `neo4j_user`, +`neo4j_password`, `queue_db_path`, `openrouter_api_key`, `openrouter_model`, `openrouter_fallback_model_1..4`, `concurrency.github`, `log_level`, `log_retention_days`, `llm_cache_enabled`, `llm_provider` (`openrouter` default | `ollama`), `ollama_url` (default `http://localhost:11434`), @@ -61,24 +61,24 @@ Anything not in this list is internal — do not import from subpaths. ## Data ownership -- `~/.bytebell/` directory creation (mode `0700`) -- `~/.bytebell/config.json` content + atomic writes (mode `0600`) +- `~/.plumbline/` directory creation (mode `0700`) +- `~/.plumbline/config.json` content + atomic writes (mode `0600`) - Default values for every config key This package does **not** own: -- `~/.bytebell/install_id` — assigned to a later package -- `~/.bytebell/keys.json` — out of scope for v0 -- `~/.bytebell/logs/` — `@bb/logger` -- `~/.bytebell/cost-ledger.sqlite` — `@bb/llm` +- `~/.plumbline/install_id` — assigned to a later package +- `~/.plumbline/keys.json` — out of scope for v0 +- `~/.plumbline/logs/` — `@bb/logger` +- `~/.plumbline/cost-ledger.sqlite` — `@bb/llm` ## Invariants 1. **No env var reads.** Source files contain no `process.env` references. Enforced at lint time ([eslint.config.mjs:71-94](../../eslint.config.mjs#L71-L94)). -2. **No `.env` / `dotenv` / `BYTEBELL_HOME`.** Programmatic override seams - are `__setBytebellHomeForTests` (test-only, static) and - `setBytebellHomeResolver` (per-call function). +2. **No `.env` / `dotenv` / `PLUMBLINE_HOME`.** Programmatic override seams + are `__setPlumblineHomeForTests` (test-only, static) and + `setPlumblineHomeResolver` (per-call function). 3. **Strict schema.** Unknown keys in `config.json` cause `loadConfig()` to throw — typo defense. 4. **Defaults always present.** `loadConfig()` never returns a partial config; @@ -89,7 +89,7 @@ This package does **not** own: 6. **File mode `0600`.** `config.json` contains the OpenRouter API key in plaintext (v0 decision); the file is owner-read/write only. 7. **No public file paths besides home + config.** Other files under - `~/.bytebell/` are not addressed by this package. + `~/.plumbline/` are not addressed by this package. ## External dependencies @@ -103,7 +103,7 @@ No HTTP, no DB, no logger. This package boots before everything else. - `install_id` generation/reading (deferred ownership) - OS keychain / `keys.json` / encrypted secrets - Logger initialization -- A `bytebell set` CLI command (lives in `@bb/cli`; uses `setConfigValue` +- A `plumbline set` CLI command (lives in `@bb/cli`; uses `setConfigValue` primitive) ## How to extend diff --git a/packages/config/src/README.md b/packages/config/src/README.md index adbde66..6c50703 100644 --- a/packages/config/src/README.md +++ b/packages/config/src/README.md @@ -7,13 +7,13 @@ package-level contract; this file documents how the source tree is split. - **[index.ts](index.ts)** — public re-exports. The only entry point other packages may import. Anything not re-exported here is internal. -- **[paths.ts](paths.ts)** — `getBytebellHome`, `getConfigPath`, and the +- **[paths.ts](paths.ts)** — `getPlumblineHome`, `getConfigPath`, and the cache-invalidator registry. Holds the `testHomeOverride` slot set by - `__setBytebellHomeForTests` and the `homeResolver` slot set by - `setBytebellHomeResolver`. `getBytebellHome` consults the test override + `__setPlumblineHomeForTests` and the `homeResolver` slot set by + `setPlumblineHomeResolver`. `getPlumblineHome` consults the test override first, then the resolver (if set and returning non-null), then falls back - to `~/.bytebell`. Pure: imports nothing from the rest of the package. -- **[schema.ts](schema.ts)** — Zod `configSchema`, `BytebellConfig` type, + to `~/.plumbline`. Pure: imports nothing from the rest of the package. +- **[schema.ts](schema.ts)** — Zod `configSchema`, `PlumblineConfig` type, `ConfigValueMap`, `DEFAULT_CONFIG`, `REQUIRED_KEYS` (infra-always), `requiredKeysFor(provider)` (combines infra + provider-specific keys driven by `Config.LlmProvider`), `HINTS`, and the @@ -21,9 +21,9 @@ package-level contract; this file documents how the source tree is split. `@bb/types` and re-exports it for intra-package convenience. - **[loader.ts](loader.ts)** — `loadConfig` (memoized), `getConfigValue`, `isConfigComplete`. Subscribes to the cache invalidator on module load. -- **[writer.ts](writer.ts)** — `ensureBytebellHome`, `setConfigValue`, atomic +- **[writer.ts](writer.ts)** — `ensurePlumblineHome`, `setConfigValue`, atomic `tmp → fsync → rename` write. Notifies the invalidator after a successful - write. `ensureBytebellHome` writes `DEFAULT_CONFIG` on first run, and on + write. `ensurePlumblineHome` writes `DEFAULT_CONFIG` on first run, and on subsequent runs migrates the on-disk file by rewriting it with merged defaults whenever any top-level schema key is missing — so PRs that add defaulted fields populate existing installs at next boot, not just fresh @@ -49,14 +49,14 @@ seam so that `loader.ts` and `writer.ts` never have to import each other. - Required-field check (`isConfigComplete`) treats empty strings as missing. - Atomic write: `config.json.tmp` is `openSync` → `writeSync` → `fsyncSync` → `closeSync` → `renameSync`. -- File mode `0o600` on `config.json`; directory mode `0o700` on `~/.bytebell/`. -- `loadConfig` always calls `ensureBytebellHome` first — never reads a missing +- File mode `0o600` on `config.json`; directory mode `0o700` on `~/.plumbline/`. +- `loadConfig` always calls `ensurePlumblineHome` first — never reads a missing file. - **`org_id` is locked to `"local"` in OSS builds.** The Zod schema for `org_id` defaults to `"local"` and `.refine`s that no other value is accepted — a hand-edited `config.json` with any other `org_id` makes `loadConfig()` throw and the server refuses to boot. `writeField` throws on `Config.OrgId`, and `keyMap.ts` in `@bb/cli` deliberately - has no entry for it so `bytebell set org_id …` is rejected at the CLI. + has no entry for it so `plumbline set org_id …` is rejected at the CLI. Per-job org overrides for downstream consumers live on the payload (`GithubIndexPayload.orgId?`), not in config.json. diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index c710ff9..e4b0e83 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,16 +1,16 @@ export { LOG_LEVELS, LLM_PROVIDERS, HINTS, requiredKeysFor } from "./schema.ts"; -export type { BytebellConfig, ConfigValue, ConfigValueMap, LogLevel, LlmProvider } from "./schema.ts"; +export type { PlumblineConfig, ConfigValue, ConfigValueMap, LogLevel, LlmProvider } from "./schema.ts"; export { loadConfig, getConfigValue, isConfigComplete, seedConfig, __isSeeded, __resetSeedForTests } from "./loader.ts"; export type { ConfigCompletenessResult } from "./loader.ts"; -export { setConfigValue, ensureBytebellHome, ConfigSeededError } from "./writer.ts"; +export { setConfigValue, ensurePlumblineHome, ConfigSeededError } from "./writer.ts"; export { - getBytebellHome, + getPlumblineHome, getConfigPath, isDevMode, isForceHaltOnceEnabled, - setBytebellHomeResolver, - __setBytebellHomeForTests, + setPlumblineHomeResolver, + __setPlumblineHomeForTests, } from "./paths.ts"; diff --git a/packages/config/src/loader.ts b/packages/config/src/loader.ts index e4a4c02..f22b080 100644 --- a/packages/config/src/loader.ts +++ b/packages/config/src/loader.ts @@ -1,17 +1,17 @@ import fs from "node:fs"; import { - configSchema, + parseConfig, Config, - type BytebellConfig, + type PlumblineConfig, type ConfigValue, HINTS, readField, requiredKeysFor, } from "./schema.ts"; import { __registerCacheInvalidator, getConfigPath, resolveUnderHome } from "./paths.ts"; -import { ensureBytebellHome } from "./writer.ts"; +import { ensurePlumblineHome } from "./writer.ts"; -let cached: BytebellConfig | null = null; +let cached: PlumblineConfig | null = null; let seeded = false; __registerCacheInvalidator(() => { @@ -21,8 +21,8 @@ __registerCacheInvalidator(() => { cached = null; }); -export function seedConfig(value: unknown): BytebellConfig { - cached = configSchema.parse(value); +export function seedConfig(value: unknown): PlumblineConfig { + cached = parseConfig(value); seeded = true; return cached; } @@ -36,14 +36,14 @@ export function __resetSeedForTests(): void { seeded = false; } -export function loadConfig(): BytebellConfig { +export function loadConfig(): PlumblineConfig { if (cached !== null) { return cached; } - ensureBytebellHome(); + ensurePlumblineHome(); const raw = fs.readFileSync(getConfigPath(), "utf8"); const parsed: unknown = JSON.parse(raw); - cached = configSchema.parse(parsed); + cached = parseConfig(parsed); return cached; } diff --git a/packages/config/src/paths.ts b/packages/config/src/paths.ts index e792dc2..46e0fbf 100644 --- a/packages/config/src/paths.ts +++ b/packages/config/src/paths.ts @@ -5,7 +5,7 @@ let testHomeOverride: string | null = null; let homeResolver: (() => string | null) | null = null; const cacheInvalidators: Array<() => void> = []; -export function getBytebellHome(): string { +export function getPlumblineHome(): string { if (testHomeOverride !== null) { return testHomeOverride; } @@ -15,27 +15,27 @@ export function getBytebellHome(): string { return resolved; } } - return path.join(os.homedir(), ".bytebell"); + return path.join(os.homedir(), ".plumbline"); } /** - * Register an override resolver for `getBytebellHome()`. The resolver runs on + * Register an override resolver for `getPlumblineHome()`. The resolver runs on * every call (no caching) so it may return different values across invocations. - * Returning `null` falls through to the `~/.bytebell` default. Pass `null` to + * Returning `null` falls through to the `~/.plumbline` default. Pass `null` to * clear the resolver. */ -export function setBytebellHomeResolver(fn: (() => string | null) | null): void { +export function setPlumblineHomeResolver(fn: (() => string | null) | null): void { homeResolver = fn; __notifyConfigChanged(); } export function getConfigPath(): string { - return path.join(getBytebellHome(), "config.json"); + return path.join(getPlumblineHome(), "config.json"); } /** * Resolve a configured filesystem path to an absolute one. Expands a leading - * `~` to the OS home, resolves a relative value against the bytebell home, and + * `~` to the OS home, resolves a relative value against the plumbline home, and * returns absolute paths unchanged. An empty value stays empty so callers can * still detect "not set" — embedded-mode validation relies on this. */ @@ -53,7 +53,7 @@ export function resolveUnderHome(value: string): string { if (path.isAbsolute(v)) { return v; } - return path.join(getBytebellHome(), v); + return path.join(getPlumblineHome(), v); } export function __registerCacheInvalidator(fn: () => void): void { @@ -66,24 +66,24 @@ export function __notifyConfigChanged(): void { } } -export function __setBytebellHomeForTests(home: string | null): void { +export function __setPlumblineHomeForTests(home: string | null): void { testHomeOverride = home; __notifyConfigChanged(); } /** - * Dev-mode toggle. Enabled by `BYTEBELL_DEV=1` on the shell session. + * Dev-mode toggle. Enabled by `PLUMBLINE_DEV=1` on the shell session. * * Narrow purpose: redirect log output to the working directory so contributors - * can tail logs without cd-ing to ~/.bytebell. Does NOT bypass the Rule of Env + * can tail logs without cd-ing to ~/.plumbline. Does NOT bypass the Rule of Env * Vars — no infra URI, credential, or persisted setting is sourced here. */ export function isDevMode(): boolean { - return process.env["BYTEBELL_DEV"] === "1"; + return process.env["PLUMBLINE_DEV"] === "1"; } /** - * TEST-ONLY toggle (`BYTEBELL_FORCE_HALT_ONCE=1`). When on, the ingest pipeline + * TEST-ONLY toggle (`PLUMBLINE_FORCE_HALT_ONCE=1`). When on, the ingest pipeline * forces a one-shot transient LLM failure per knowledge so the HALTED → retry → * PROCESSED loop can be exercised end-to-end. Read here (the sanctioned env * boundary) rather than in package code, mirroring `isDevMode`. Does NOT bypass @@ -91,5 +91,5 @@ export function isDevMode(): boolean { * sourced. Remove alongside `ingest-github`'s `fault-injection.ts`. */ export function isForceHaltOnceEnabled(): boolean { - return process.env["BYTEBELL_FORCE_HALT_ONCE"] === "1"; + return process.env["PLUMBLINE_FORCE_HALT_ONCE"] === "1"; } diff --git a/packages/config/src/schema-fields.ts b/packages/config/src/schema-fields.ts index ef2b013..fac8a68 100644 --- a/packages/config/src/schema-fields.ts +++ b/packages/config/src/schema-fields.ts @@ -1,22 +1,18 @@ -import type { BytebellConfig } from "./schema.ts"; +import type { PlumblineConfig } from "./schema.ts"; import { Config } from "@bb/types"; import type { ConfigValue } from "./schema.ts"; import type { LogLevel, LlmProvider, IngestionStrategy } from "./schema.ts"; -export function readField(cfg: BytebellConfig, key: K): ConfigValue { +export function readField(cfg: PlumblineConfig, key: K): ConfigValue { switch (key) { case Config.ServerPort: return cfg.server_port as ConfigValue; - case Config.MongoUri: - return cfg.mongo_uri as ConfigValue; case Config.Neo4jUri: return cfg.neo4j_uri as ConfigValue; case Config.Neo4jUser: return cfg.neo4j_user as ConfigValue; case Config.Neo4jPassword: return cfg.neo4j_password as ConfigValue; - case Config.RedisUrl: - return cfg.redis_url as ConfigValue; case Config.OpenrouterApiKey: return cfg.openrouter_api_key as ConfigValue; case Config.OpenrouterModel: @@ -114,20 +110,16 @@ export function readField(cfg: BytebellConfig, key: K): Config } } -export function writeField(cfg: BytebellConfig, key: K, value: ConfigValue): BytebellConfig { +export function writeField(cfg: PlumblineConfig, key: K, value: ConfigValue): PlumblineConfig { switch (key) { case Config.ServerPort: return { ...cfg, server_port: value as number }; - case Config.MongoUri: - return { ...cfg, mongo_uri: value as string }; case Config.Neo4jUri: return { ...cfg, neo4j_uri: value as string }; case Config.Neo4jUser: return { ...cfg, neo4j_user: value as string }; case Config.Neo4jPassword: return { ...cfg, neo4j_password: value as string }; - case Config.RedisUrl: - return { ...cfg, redis_url: value as string }; case Config.OpenrouterApiKey: return { ...cfg, openrouter_api_key: value as string }; case Config.OpenrouterModel: diff --git a/packages/config/src/schema.ts b/packages/config/src/schema.ts index 19f1c49..694b25f 100644 --- a/packages/config/src/schema.ts +++ b/packages/config/src/schema.ts @@ -24,11 +24,9 @@ const concurrencySchema = z export const configSchema = z .object({ server_port: z.number().int().min(1).max(65535).default(8080), - mongo_uri: z.string().default(""), neo4j_uri: z.string().default(""), neo4j_user: z.string().default(""), neo4j_password: z.string().default(""), - redis_url: z.string().default(""), openrouter_api_key: z.string().default(""), openrouter_model: z.string().default("deepseek/deepseek-v4-flash"), openrouter_fallback_model_1: z.string().default("qwen/qwen3.5-flash-02-23"), @@ -59,11 +57,29 @@ export const configSchema = z "skip.decision.enabled": z.boolean().default(true), "skip.decision.max.chars.for.llm": z.number().int().positive().default(4000), "skip.decision.cache.path": z.string().default(""), - db_provider: z.string().default("mongo"), + // `mongo` was the document store before SQLite replaced it. A config.json + // written by an older build still carries it, so it is migrated on read — + // otherwise the upgrade fails at boot with "Database provider 'mongo' is + // not registered", which tells the user nothing about what to do. + db_provider: z + .string() + .default("sqlite") + .transform((value) => (value === "mongo" ? "sqlite" : value)), graph_provider: z.string().default("neo4j"), - queue_provider: z.string().default("bullmq"), - queue_db_path: z.string().default(""), - sqlite_path: z.string().default(""), + // `bullmq` was the queue before Honker-over-SQLite replaced it. Migrated on + // read for the same reason as `db_provider` above — an older config.json + // would otherwise fail at boot with "Queue provider 'bullmq' is not registered". + queue_provider: z + .string() + .default("honker") + .transform((value) => (value === "bullmq" ? "honker" : value)), + // Relative paths resolve under ~/.plumbline (see `resolveUnderHome`), so this + // default gives every install a durable queue without a `set` call. + queue_db_path: z.string().default("queue.db"), + // Relative paths resolve under ~/.plumbline (see `resolveUnderHome`), so + // this default gives every install a durable store without a `set` call — + // including one upgrading from the Mongo-era config, which has no value here. + sqlite_path: z.string().default("data.sqlite"), ladybug_path: z.string().default(""), // Free string: the OSS engine routes the public strategies it knows // (flat-folder / concept-graph) and lets any other value pass through to a @@ -83,17 +99,44 @@ export const configSchema = z }) .strict(); -export type BytebellConfig = z.infer; +export type PlumblineConfig = z.infer; -export const DEFAULT_CONFIG: BytebellConfig = configSchema.parse({}); +export const DEFAULT_CONFIG: PlumblineConfig = configSchema.parse({}); + +/** + * Keys a past release wrote into `config.json` and this build no longer + * defines. The schema is `.strict()`, so leaving one in place would turn every + * upgrade into an "Unrecognized key" crash on the first config read. They are + * dropped on parse and rewritten out of the file by `ensurePlumblineHome`. + */ +export const RETIRED_KEYS: readonly string[] = ["mongo_uri", "redis_url"]; + +/** True when a stored config still carries a key this build has retired. */ +export function hasRetiredKeys(raw: Record): boolean { + return RETIRED_KEYS.some((key) => key in raw); +} + +/** + * Parse a stored config, tolerating keys retired by a past release. Use this + * for anything read off disk or supplied by a host; `configSchema.parse` stays + * the strict form for values this build constructed itself. + */ +export function parseConfig(value: unknown): PlumblineConfig { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return configSchema.parse(value); + } + const copy: Record = { ...(value as Record) }; + for (const key of RETIRED_KEYS) { + delete copy[key]; + } + return configSchema.parse(copy); +} export type ConfigValueMap = { [Config.ServerPort]: number; - [Config.MongoUri]: string; [Config.Neo4jUri]: string; [Config.Neo4jUser]: string; [Config.Neo4jPassword]: string; - [Config.RedisUrl]: string; [Config.OpenrouterApiKey]: string; [Config.OpenrouterModel]: string; [Config.OpenrouterFallbackModel1]: string; @@ -144,13 +187,7 @@ export type ConfigValueMap = { export type ConfigValue = ConfigValueMap[K]; -export const REQUIRED_KEYS: readonly Config[] = [ - Config.MongoUri, - Config.Neo4jUri, - Config.Neo4jUser, - Config.Neo4jPassword, - Config.RedisUrl, -]; +export const REQUIRED_KEYS: readonly Config[] = [Config.Neo4jUri, Config.Neo4jUser, Config.Neo4jPassword]; const PROVIDER_REQUIRED_KEYS: Readonly> = { openrouter: [Config.OpenrouterApiKey], @@ -162,58 +199,56 @@ export function requiredKeysFor(provider: LlmProvider): readonly Config[] { } export const HINTS: Readonly> = { - [Config.ServerPort]: "bytebell set port ", - [Config.MongoUri]: "bytebell set mongo ", - [Config.Neo4jUri]: "bytebell set neo4j ", - [Config.Neo4jUser]: "bytebell set neo4j-user ", - [Config.Neo4jPassword]: "bytebell set neo4j-password ", - [Config.RedisUrl]: "bytebell set redis ", - [Config.OpenrouterApiKey]: "bytebell keys set", - [Config.OpenrouterModel]: "bytebell models set ", - [Config.OpenrouterFallbackModel1]: "bytebell set openrouter-fallback-model-1 ", - [Config.OpenrouterFallbackModel2]: "bytebell set openrouter-fallback-model-2 ", - [Config.OpenrouterFallbackModel3]: "bytebell set openrouter-fallback-model-3 ", - [Config.OpenrouterFallbackModel4]: "bytebell set openrouter-fallback-model-4 ", - [Config.ConcurrencyGithub]: "bytebell set concurrency.github ", - [Config.LogLevel]: "bytebell set log-level ", - [Config.LogRetentionDays]: "bytebell set log-retention-days ", - [Config.LlmCacheEnabled]: "bytebell set llm_cache_enabled ", - [Config.LlmProvider]: "bytebell set llm-provider ", - [Config.OllamaUrl]: "bytebell set ollama-url ", - [Config.OllamaModel]: "bytebell set ollama-model ", - [Config.ContextWindowLimit]: "bytebell set context.window.limit ", - [Config.MaxTokensPerChunk]: "bytebell set max.tokens.per.chunk ", - [Config.BigFileConcurrency]: "bytebell set big.file.concurrency ", - [Config.AbsoluteFileSizeCap]: "bytebell set absolute.file.size.cap ", - [Config.ConcurrentWorkers]: "bytebell set concurrent.workers ", - [Config.LlmConcurrency]: "bytebell set llm.concurrency ", - [Config.FolderSummaryBatchSize]: "bytebell set folder.summary.batch.size ", - [Config.FolderSummaryBatchMaxFiles]: "bytebell set folder.summary.batch.max.files ", - [Config.Neo4jBatchSize]: "bytebell set neo4j.batch.size ", - [Config.CondenseContextLimit]: "bytebell set condense.context.limit ", - [Config.CondensePromptOverhead]: "bytebell set condense.prompt.overhead ", - [Config.SmallFileDedupThreshold]: "bytebell set small.file.dedup.threshold ", - [Config.BigFileLineThreshold]: "bytebell set big.file.line.threshold ", - [Config.OrgId]: "bytebell set org_id ", - [Config.SkipDecisionEnabled]: "bytebell set skip.decision.enabled ", - [Config.SkipDecisionMaxCharsForLlm]: "bytebell set skip.decision.max.chars.for.llm ", - [Config.SkipDecisionCachePath]: "bytebell set skip.decision.cache.path ", - [Config.DbProvider]: "bytebell set db-provider ", - [Config.GraphProvider]: "bytebell set graph-provider ", - [Config.QueueProvider]: "bytebell set queue-provider ", - [Config.QueueDbPath]: "bytebell set queue-db-path ", - [Config.SqlitePath]: "bytebell set sqlite-path ", - [Config.LadybugPath]: "bytebell set ladybug-path ", - [Config.IngestionStrategy]: "bytebell set ingestion.strategy ", - [Config.UnitsModel]: "bytebell set units.model ", - [Config.EnrichmentModel]: "bytebell set enrichment.model ", - [Config.EnrichmentMaxToolCallsPerFile]: "bytebell set enrichment.max.tool.calls.per.file ", - [Config.EnrichmentMaxIterationsPerFile]: "bytebell set enrichment.max.iterations.per.file ", - [Config.EnrichmentWallTimeMsPerFile]: "bytebell set enrichment.wall.time.ms.per.file ", - [Config.EnrichmentConcurrency]: "bytebell set enrichment.concurrency ", - [Config.EnrichmentMaxToolResultChars]: "bytebell set enrichment.max.tool.result.chars ", - [Config.OpenrouterReasoningMaxTokens]: "bytebell set openrouter.reasoning.max.tokens ", - [Config.OpenrouterMaxCompletionTokens]: "bytebell set openrouter.max.completion.tokens ", + [Config.ServerPort]: "plumbline set port ", + [Config.Neo4jUri]: "plumbline set neo4j ", + [Config.Neo4jUser]: "plumbline set neo4j-user ", + [Config.Neo4jPassword]: "plumbline set neo4j-password ", + [Config.OpenrouterApiKey]: "plumbline keys set", + [Config.OpenrouterModel]: "plumbline models set ", + [Config.OpenrouterFallbackModel1]: "plumbline set openrouter-fallback-model-1 ", + [Config.OpenrouterFallbackModel2]: "plumbline set openrouter-fallback-model-2 ", + [Config.OpenrouterFallbackModel3]: "plumbline set openrouter-fallback-model-3 ", + [Config.OpenrouterFallbackModel4]: "plumbline set openrouter-fallback-model-4 ", + [Config.ConcurrencyGithub]: "plumbline set concurrency.github ", + [Config.LogLevel]: "plumbline set log-level ", + [Config.LogRetentionDays]: "plumbline set log-retention-days ", + [Config.LlmCacheEnabled]: "plumbline set llm_cache_enabled ", + [Config.LlmProvider]: "plumbline set llm-provider ", + [Config.OllamaUrl]: "plumbline set ollama-url ", + [Config.OllamaModel]: "plumbline set ollama-model ", + [Config.ContextWindowLimit]: "plumbline set context.window.limit ", + [Config.MaxTokensPerChunk]: "plumbline set max.tokens.per.chunk ", + [Config.BigFileConcurrency]: "plumbline set big.file.concurrency ", + [Config.AbsoluteFileSizeCap]: "plumbline set absolute.file.size.cap ", + [Config.ConcurrentWorkers]: "plumbline set concurrent.workers ", + [Config.LlmConcurrency]: "plumbline set llm.concurrency ", + [Config.FolderSummaryBatchSize]: "plumbline set folder.summary.batch.size ", + [Config.FolderSummaryBatchMaxFiles]: "plumbline set folder.summary.batch.max.files ", + [Config.Neo4jBatchSize]: "plumbline set neo4j.batch.size ", + [Config.CondenseContextLimit]: "plumbline set condense.context.limit ", + [Config.CondensePromptOverhead]: "plumbline set condense.prompt.overhead ", + [Config.SmallFileDedupThreshold]: "plumbline set small.file.dedup.threshold ", + [Config.BigFileLineThreshold]: "plumbline set big.file.line.threshold ", + [Config.OrgId]: "plumbline set org_id ", + [Config.SkipDecisionEnabled]: "plumbline set skip.decision.enabled ", + [Config.SkipDecisionMaxCharsForLlm]: "plumbline set skip.decision.max.chars.for.llm ", + [Config.SkipDecisionCachePath]: "plumbline set skip.decision.cache.path ", + [Config.DbProvider]: "plumbline set db-provider ", + [Config.GraphProvider]: "plumbline set graph-provider ", + [Config.QueueProvider]: "plumbline set queue-provider ", + [Config.QueueDbPath]: "plumbline set queue-db-path ", + [Config.SqlitePath]: "plumbline set sqlite-path ", + [Config.LadybugPath]: "plumbline set ladybug-path ", + [Config.IngestionStrategy]: "plumbline set ingestion.strategy ", + [Config.UnitsModel]: "plumbline set units.model ", + [Config.EnrichmentModel]: "plumbline set enrichment.model ", + [Config.EnrichmentMaxToolCallsPerFile]: "plumbline set enrichment.max.tool.calls.per.file ", + [Config.EnrichmentMaxIterationsPerFile]: "plumbline set enrichment.max.iterations.per.file ", + [Config.EnrichmentWallTimeMsPerFile]: "plumbline set enrichment.wall.time.ms.per.file ", + [Config.EnrichmentConcurrency]: "plumbline set enrichment.concurrency ", + [Config.EnrichmentMaxToolResultChars]: "plumbline set enrichment.max.tool.result.chars ", + [Config.OpenrouterReasoningMaxTokens]: "plumbline set openrouter.reasoning.max.tokens ", + [Config.OpenrouterMaxCompletionTokens]: "plumbline set openrouter.max.completion.tokens ", }; export { readField, writeField } from "./schema-fields.ts"; diff --git a/packages/config/src/writer.ts b/packages/config/src/writer.ts index c89a82c..6eeddaa 100644 --- a/packages/config/src/writer.ts +++ b/packages/config/src/writer.ts @@ -1,7 +1,16 @@ import fs from "node:fs"; -import { configSchema, Config, type BytebellConfig, type ConfigValue, DEFAULT_CONFIG, writeField } from "./schema.ts"; +import { + configSchema, + parseConfig, + hasRetiredKeys, + Config, + type PlumblineConfig, + type ConfigValue, + DEFAULT_CONFIG, + writeField, +} from "./schema.ts"; import { __isSeeded } from "./loader.ts"; -import { getBytebellHome, getConfigPath, __notifyConfigChanged } from "./paths.ts"; +import { getPlumblineHome, getConfigPath, __notifyConfigChanged } from "./paths.ts"; export class ConfigSeededError extends Error { constructor() { @@ -13,13 +22,13 @@ export class ConfigSeededError extends Error { const FILE_MODE = 0o600; const DIR_MODE = 0o700; -function readConfigFile(): BytebellConfig { +function readConfigFile(): PlumblineConfig { const raw = fs.readFileSync(getConfigPath(), "utf8"); const parsed: unknown = JSON.parse(raw); - return configSchema.parse(parsed); + return parseConfig(parsed); } -function atomicWrite(cfg: BytebellConfig): void { +function atomicWrite(cfg: PlumblineConfig): void { const target = getConfigPath(); const tmp = `${target}.tmp`; const json = `${JSON.stringify(cfg, null, 2)}\n`; @@ -33,17 +42,19 @@ function atomicWrite(cfg: BytebellConfig): void { fs.renameSync(tmp, target); } -export function ensureBytebellHome(): void { - const home = getBytebellHome(); +export function ensurePlumblineHome(): void { + const home = getPlumblineHome(); fs.mkdirSync(home, { recursive: true, mode: DIR_MODE }); if (!fs.existsSync(getConfigPath())) { atomicWrite(DEFAULT_CONFIG); return; } const raw = JSON.parse(fs.readFileSync(getConfigPath(), "utf8")) as Record; + // Rewrite when the file is missing a key this build added, or still carries + // one it retired — either way the on-disk shape is brought up to date. const expected = Object.keys(configSchema.shape); - if (expected.some((k) => !(k in raw))) { - atomicWrite(configSchema.parse(raw)); + if (expected.some((k) => !(k in raw)) || hasRetiredKeys(raw)) { + atomicWrite(parseConfig(raw)); __notifyConfigChanged(); } } @@ -52,7 +63,7 @@ export function setConfigValue(key: K, value: ConfigValue): if (__isSeeded()) { throw new ConfigSeededError(); } - ensureBytebellHome(); + ensurePlumblineHome(); const current = readConfigFile(); const next = writeField(current, key, value); configSchema.parse(next); diff --git a/packages/db-core/README.md b/packages/db-core/README.md index d118ad1..e0154f0 100644 --- a/packages/db-core/README.md +++ b/packages/db-core/README.md @@ -4,7 +4,7 @@ Provider-agnostic interfaces for the document database layer. ## Responsibilities -Defines the contract that every document database backend (Mongo, SQLite, etc.) must implement. Contains no I/O — pure TypeScript interfaces and shared types. +Defines the contract that every document database backend must implement. `@bb/sqlite` is the only implementation shipped today. Contains no I/O — pure TypeScript interfaces and shared types. ## Public Interfaces @@ -14,8 +14,9 @@ Defines the contract that every document database backend (Mongo, SQLite, etc.) - `IAggregateStatsRepository` — `aggregateStats()` - `IActivityRepository` — `recordActivity()` - `IUsageRepository` — `incrementUsage`, `getMonthlyUsage`, `getGlobalUsage` +- `IEnrichmentRepository` — per-file enrichment ledger for the concept-graph strategy: `startEnrichmentRun`, `getCompletedEnrichmentFiles`, `markFileEnriched`, `recordEnrichmentFailure`, `completeEnrichmentRun`, `failEnrichmentRun`. State lives on the knowledge document; it is the resume cursor a queue retry reads so finished files are skipped. - `DbPingResult` — health probe result shape -- `FileAnalysis`, `RawFileDoc` — shared raw-file types (previously duplicated in `@bb/mongo`) +- `FileAnalysis`, `RawFileDoc` — shared raw-file types ## Data Ownership @@ -23,4 +24,4 @@ None. This package owns no data — it only describes shapes. ## Tier -Strategy (interfaces consumed by `@bb/db` and implemented by `@bb/mongo`, `@bb/sqlite`) +Strategy (interfaces consumed by `@bb/db` and implemented by `@bb/sqlite`) diff --git a/packages/db-core/src/index.ts b/packages/db-core/src/index.ts index 04ce5fa..58b0e90 100644 --- a/packages/db-core/src/index.ts +++ b/packages/db-core/src/index.ts @@ -10,6 +10,7 @@ import type { KnowledgeListEntry, DeleteKnowledgeResult, DbPingResult, + EnrichmentFailure, } from "@bb/types"; export type { FileAnalysisSection, FileAnalysis, RawFileDoc, KnowledgeListEntry, DeleteKnowledgeResult, DbPingResult }; @@ -113,12 +114,45 @@ export interface IUsageRepository { getGlobalUsage(): Promise; } +/** + * Per-file enrichment ledger for the concept-graph strategy. The state lives on + * the existing knowledge document — no separate entity — and exists so a queue + * retry can resume by skipping files that already completed. The knowledge's + * own `KnowledgeState` stays PROCESSING throughout; this ledger is the + * finer-grained cursor underneath it. + * + * Every method throws `KnowledgeNotFoundError` when the document is missing. + */ +export interface IEnrichmentRepository { + /** + * Begins or resumes an enrichment attempt: stamps `runId`, clears recorded + * failures (failed files are re-evaluated on the retry) and moves the ledger + * to `Running`. `completedFiles` is preserved so a retry skips finished work + * — a clean re-enrichment is an explicit reset, not a retry. + */ + startEnrichmentRun(knowledgeId: string, runId: string): Promise; + /** Files already enriched, used to pre-filter the work queue on resume. */ + getCompletedEnrichmentFiles(knowledgeId: string): Promise; + /** Records `filePath` as enriched. Idempotent — a repeat does not duplicate. */ + markFileEnriched(knowledgeId: string, filePath: string): Promise; + /** + * Records or replaces the failure entry for `failure.filePath` (one entry per + * file). Diagnostic only — the strategy decides the overall outcome. + */ + recordEnrichmentFailure(knowledgeId: string, failure: EnrichmentFailure): Promise; + /** Moves the ledger to `Completed`. The caller transitions `KnowledgeState`. */ + completeEnrichmentRun(knowledgeId: string): Promise; + /** Moves the ledger to `Failed`. A fresh `startEnrichmentRun` retries it. */ + failEnrichmentRun(knowledgeId: string): Promise; +} + export interface IDocumentDatabaseProvider { knowledge: IKnowledgeRepository; raw: IRawRepository; stats: IAggregateStatsRepository; activity: IActivityRepository; usage: IUsageRepository; + enrichment: IEnrichmentRepository; connect(): Promise; close(): Promise; diff --git a/packages/db/README.md b/packages/db/README.md index c08ae38..153aa15 100644 --- a/packages/db/README.md +++ b/packages/db/README.md @@ -4,15 +4,15 @@ Provider registry and facade for the document database layer. ## Responsibilities -Maintains a map of named database providers, exposes a single `getDb()` accessor that delegates to whichever provider is active. Provides convenience facade objects (`knowledge`, `raw`, `stats`, `activity`, `usage`) that proxy to the active provider's methods. +Maintains a map of named database providers, exposes a single `getDb()` accessor that delegates to whichever provider is active. Provides convenience facade objects (`knowledgeDb`, `rawDb`, `statsDb`, `activityDb`, `usageDb`, `enrichmentDb`) that proxy to the active provider's methods. ## Public Interfaces -- `registerDbProvider(name, factory)` — register a provider (called at import time by `@bb/mongo` and `@bb/sqlite`) +- `registerDbProvider(name, factory)` — register a provider (called at import time by `@bb/sqlite`) - `connectDb(providerName)` — instantiate and connect a provider - `closeDb()` — close the active provider - `getDb()` — returns the active `IDocumentDatabaseProvider` -- `knowledge`, `raw`, `stats`, `activity`, `usage` — facade objects proxying to `getDb()` +- `knowledgeDb`, `rawDb`, `statsDb`, `activityDb`, `usageDb`, `enrichmentDb` — facade objects proxying to `getDb()` - `pingDb()` — health probe ## Data Ownership diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index d20914c..782da84 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -5,6 +5,7 @@ import type { IAggregateStatsRepository, IActivityRepository, IUsageRepository, + IEnrichmentRepository, DbPingResult, } from "@bb/db-core"; @@ -74,6 +75,15 @@ export const usageDb: IUsageRepository = { getGlobalUsage: (...args) => getDb().usage.getGlobalUsage(...args), }; +export const enrichmentDb: IEnrichmentRepository = { + startEnrichmentRun: (...args) => getDb().enrichment.startEnrichmentRun(...args), + getCompletedEnrichmentFiles: (...args) => getDb().enrichment.getCompletedEnrichmentFiles(...args), + markFileEnriched: (...args) => getDb().enrichment.markFileEnriched(...args), + recordEnrichmentFailure: (...args) => getDb().enrichment.recordEnrichmentFailure(...args), + completeEnrichmentRun: (...args) => getDb().enrichment.completeEnrichmentRun(...args), + failEnrichmentRun: (...args) => getDb().enrichment.failEnrichmentRun(...args), +}; + export async function pingDb(): Promise { return getDb().ping(); } diff --git a/packages/errors/README.md b/packages/errors/README.md index c6d1c6b..5e32451 100644 --- a/packages/errors/README.md +++ b/packages/errors/README.md @@ -16,28 +16,24 @@ re-introducing its own error namespace. Today the catalog covers: - **Config** — `ConfigIncompleteError` (missing required keys; carries the - missing `Config[]` and the corresponding `bytebell set …` hints) -- **Mongo** — `MongoConfigError` (missing URI), `MongoConnectError` (driver - connect failed; redacts credentials in URI), `MongoNotConnectedError` - (`_getDb()` called before `connectMongo()`) -- **Redis** — `RedisConfigError` (missing URL), `RedisConnectError` (ioredis - connect failed; redacts userinfo in URL), `RedisNotConnectedError` - (`_getRedis()` called before `connectRedis()`) -- **Knowledge** — `KnowledgeNotFoundError` (`@bb/mongo.setKnowledgeState` - matched zero documents; carries the offending `knowledgeId` as a typed - field). Lives alongside the Mongo errors in `mongo-errors.ts`. -- **Queue** — `QueueConnectError` (BullMQ Queue construction failed; + missing `Config[]` and the corresponding `plumbline set …` hints) +- **Knowledge** — `KnowledgeNotFoundError` (a repository operation such as + `setKnowledgeState` matched no document; carries the offending + `knowledgeId` as a typed field). Provider-agnostic — every + `IDocumentDatabaseProvider` implementation throws it. Lives in + `knowledge-errors.ts`. +- **Queue** — `QueueConnectError` (queue provider construction failed; carries `cause`), `QueueNotConnectedError` (publisher or `registerWorker` called before `connectQueue()`). - **LLM** — `LlmConfigError` (missing OpenRouter API key; carries the - `bytebell keys set` hint), `LlmError` (HTTP non-2xx, timeout, empty + `plumbline keys set` hint), `LlmError` (HTTP non-2xx, timeout, empty completion; carries `cause`). - **Ingest** — `GitCloneError` (git binary failed; redacts userinfo in the repo URL), `IngestError` (catch-all worker failure; carries `knowledgeId` and `cause`), `RepoUnavailableError` (source repo gone or inaccessible — terminal; the failure classifier maps it to the `repo_unavailable` category → knowledge marked `CORRUPTED`), - `IngestPathError` (`bytebell ingest ` + `IngestPathError` (`plumbline ingest ` pre-flight failure: missing path / not a directory), `UsageLimitExceededError` (thrown by a runtime `UsageGuard` implementation when a token quota would be exceeded mid-run; carries @@ -45,7 +41,7 @@ Today the catalog covers: `UsageLimitExceededDetail`). OSS standalone never throws this; the pipeline only catches it when an optional guard is wired in. - **Server** — `ServerConfigError` (missing required config at boot; - carries `missing[]` + matching `bytebell set …` hints), + carries `missing[]` + matching `plumbline set …` hints), `ServerStartTimeoutError` (spawned server never passed its health check; carries `logPath`), `ServerInfraDownError` (server's `/health` reports an infra dependency down; carries the `services[]` names), @@ -60,7 +56,7 @@ Today the catalog covers: before `connectNeo4j()`). - **Layout** — `LayoutMigrationRequiredError` (the legacy on-disk layout `repos/.meta//` is present; the server refuses to boot - until the operator runs `bytebell migrate paths`). Carries the + until the operator runs `plumbline migrate paths`). Carries the detected legacy path in the message and the migration hint as a typed `hint` field. @@ -70,13 +66,7 @@ New error classes land here as new packages are introduced. ```ts class ConfigIncompleteError extends Error -class MongoConfigError extends Error -class MongoConnectError extends Error -class MongoNotConnectedError extends Error class KnowledgeNotFoundError extends Error -class RedisConfigError extends Error -class RedisConnectError extends Error -class RedisNotConnectedError extends Error class QueueConnectError extends Error class QueueNotConnectedError extends Error class LlmConfigError extends Error @@ -107,7 +97,7 @@ None. Pure class declarations. 2. **Stable `name`.** Every class sets `override readonly name` to its class name. The logger keys off this string; renaming is a coordinated change. 3. **Credential redaction.** Any error message that includes a connection - URI must redact userinfo (see `redactUri` in `mongo-errors.ts`). + URI must redact userinfo (see `redactUri` in `neo4j-errors.ts`). 4. **Typed metadata over string parsing.** Errors carry structured fields (`hint`, `missing`, `hints`, `cause`) — consumers read those, never parse `message`. diff --git a/packages/errors/src/README.md b/packages/errors/src/README.md index 4edfc65..8148493 100644 --- a/packages/errors/src/README.md +++ b/packages/errors/src/README.md @@ -10,32 +10,21 @@ package-level contract; this file documents how the source tree is split. modules. Anything not re-exported here is internal. - **[config-errors.ts](config-errors.ts)** — errors thrown by callers of `@bb/config`. Today: `ConfigIncompleteError` (carries the missing - `Config[]` and the corresponding `bytebell set …` hints). Type-only + `Config[]` and the corresponding `plumbline set …` hints). Type-only imports `Config` from `@bb/types`. -- **[mongo-errors.ts](mongo-errors.ts)** — errors thrown by `@bb/mongo` - and the knowledge-document subsystem. Today: `MongoConfigError` (missing - URI; carries the `bytebell set …` hint), `MongoConnectError` (driver - connect failed; redacts userinfo in the URI via the local `redactUri` - helper), `MongoNotConnectedError` (`_getDb()` called before - `connectMongo()`), `KnowledgeNotFoundError` - (`setKnowledgeState` matched zero documents; carries the offending - `knowledgeId`). Local helpers `describe` and `redactUri` are - file-private. -- **[redis-errors.ts](redis-errors.ts)** — errors thrown by `@bb/redis`. - Today: `RedisConfigError` (missing URL; carries the `bytebell set …` - hint), `RedisConnectError` (ioredis connect failed; redacts userinfo via - the local `redactUri` helper — the regex matches `redis://user:pass@host` - identically to the mongo URI form), `RedisNotConnectedError` - (`_getRedis()` called before `connectRedis()`). Local helpers `describe` - and `redactUri` are file-private. +- **[knowledge-errors.ts](knowledge-errors.ts)** — errors thrown by the + knowledge-document subsystem. Today: `KnowledgeNotFoundError` + (`setKnowledgeState` and friends matched no document; carries the + offending `knowledgeId`). Provider-agnostic — thrown by every + `IDocumentDatabaseProvider` implementation, not by one driver. - **[queue-errors.ts](queue-errors.ts)** — errors thrown by `@bb/queue`. - Today: `QueueConnectError` (BullMQ Queue construction failed; wraps the + Today: `QueueConnectError` (queue provider construction failed; wraps the underlying cause via the local `describe` helper), `QueueNotConnectedError` (publisher or `registerWorker` called before `connectQueue()`; marker class with no extra fields). - **[llm-errors.ts](llm-errors.ts)** — errors thrown by `@bb/llm`. Today: `LlmConfigError` (missing OpenRouter API key; carries the - `bytebell keys set` hint), `LlmError` (HTTP non-2xx, timeout, empty + `plumbline keys set` hint), `LlmError` (HTTP non-2xx, timeout, empty completion; accepts an optional `cause` plus an optional `{ status?: number; detail?: string }` options bag — `status` is the provider HTTP status when the failure originated from a non-OK response, @@ -48,18 +37,18 @@ package-level contract; this file documents how the source tree is split. `GitCloneError` (git binary failed; redacts userinfo in the repo URL via the local `redactUrl` helper), `IngestError` (catch-all worker failure; carries `knowledgeId` and an optional `cause`), - `IngestPathError` (CLI pre-flight when `bytebell ingest ` is + `IngestPathError` (CLI pre-flight when `plumbline ingest ` is given a non-existent or non-directory path), `CancellationError` (cooperative cancellation signal thrown by `throwIfCancelled` between phases of the flat-folder strategy; carries `knowledgeId`. The orchestrator catches it, clears the cancellation flag, and returns - _without_ flipping Mongo state to FAILED). + _without_ flipping the knowledge state to FAILED). - **[server-errors.ts](server-errors.ts)** — errors thrown by `@bb/server` at boot. Today: `ServerConfigError` (missing required config keys; - carries `missing[]` + the corresponding `bytebell set …` hints). + carries `missing[]` + the corresponding `plumbline set …` hints). - **[neo4j-errors.ts](neo4j-errors.ts)** — errors thrown by `@bb/neo4j`. Today: `Neo4jConfigError` (missing URI / user / password; carries the - `bytebell set …` hint), `Neo4jConnectError` (`verifyConnectivity()` + `plumbline set …` hint), `Neo4jConnectError` (`verifyConnectivity()` failed; redacts userinfo in the URI via the local `redactUri` helper), `Neo4jNotConnectedError` (`_getDriver()` called before `connectNeo4j()`). Local helpers `describe` and `redactUri` are @@ -69,8 +58,7 @@ package-level contract; this file documents how the source tree is split. ``` config-errors.ts → @bb/types (type-only: Config) -mongo-errors.ts → (leaf — no imports) -redis-errors.ts → (leaf — no imports) +knowledge-errors.ts → (leaf — no imports) queue-errors.ts → (leaf — no imports) llm-errors.ts → (leaf — no imports) ingest-errors.ts → (leaf — no imports) @@ -84,7 +72,7 @@ No cross-file imports inside the package; no cycles possible. ## Invariants enforced here - **One file per source area.** New error classes for a new package land in - a new `-errors.ts` file (e.g. `neo4j-errors.ts`, `redis-errors.ts`), + a new `-errors.ts` file (e.g. `neo4j-errors.ts`, `queue-errors.ts`), never appended to an existing file. - **`override readonly name` set on every class.** The string equals the class name. `@bb/logger` discriminates on this; renames are a @@ -94,7 +82,7 @@ No cross-file imports inside the package; no cycles possible. directly instead of regexing `error.message`. - **Credential redaction at construction time.** Any constructor that accepts a connection URI must redact userinfo before composing - `super(...)`. See `redactUri` in `mongo-errors.ts` for the canonical + `super(...)`. See `redactUri` in `neo4j-errors.ts` for the canonical pattern. - **No I/O, no logging.** This package never imports `@bb/logger` or any infra package — those packages import _from_ this one. diff --git a/packages/errors/src/config-errors.ts b/packages/errors/src/config-errors.ts index a54691b..d9843a1 100644 --- a/packages/errors/src/config-errors.ts +++ b/packages/errors/src/config-errors.ts @@ -6,7 +6,7 @@ export class ConfigIncompleteError extends Error { readonly hints: readonly string[]; constructor(missing: readonly Config[], hints: readonly string[]) { - super(`Bytebell config is missing required fields: ${missing.join(", ")}.\n` + `Run:\n ${hints.join("\n ")}`); + super(`Plumbline config is missing required fields: ${missing.join(", ")}.\n` + `Run:\n ${hints.join("\n ")}`); this.missing = missing; this.hints = hints; } diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index 5c47ea0..b87497b 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -1,6 +1,5 @@ export { ConfigIncompleteError } from "./config-errors.ts"; -export { KnowledgeNotFoundError, MongoConfigError, MongoConnectError, MongoNotConnectedError } from "./mongo-errors.ts"; -export { RedisConfigError, RedisConnectError, RedisNotConnectedError } from "./redis-errors.ts"; +export { KnowledgeNotFoundError } from "./knowledge-errors.ts"; export { QueueConnectError, QueueNotConnectedError } from "./queue-errors.ts"; export { LlmConfigError, LlmError } from "./llm-errors.ts"; export { diff --git a/packages/errors/src/knowledge-errors.ts b/packages/errors/src/knowledge-errors.ts new file mode 100644 index 0000000..d1da9c3 --- /dev/null +++ b/packages/errors/src/knowledge-errors.ts @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0-only WITH non-commercial-clause + +/** + * Raised when a repository operation targets a `knowledgeId` that has no + * document in the active store. Provider-agnostic: thrown by every + * `IDocumentDatabaseProvider` implementation, not by one driver. + */ +export class KnowledgeNotFoundError extends Error { + override readonly name = "KnowledgeNotFoundError"; + readonly knowledgeId: string; + + constructor(knowledgeId: string) { + super(`No knowledge document found with knowledgeId="${knowledgeId}".`); + this.knowledgeId = knowledgeId; + } +} diff --git a/packages/errors/src/layout-errors.ts b/packages/errors/src/layout-errors.ts index 8227af9..23e3cb0 100644 --- a/packages/errors/src/layout-errors.ts +++ b/packages/errors/src/layout-errors.ts @@ -8,9 +8,9 @@ export class LayoutMigrationRequiredError extends Error { "On-disk layout is the legacy `repos/.meta//` shape;", "this build expects the commit-scoped layout under `orgs///`.", `Detected legacy path: ${detectedLegacyPath}`, - "Run: bytebell migrate paths", + "Run: plumbline migrate paths", ].join("\n "), ); - this.hint = "bytebell migrate paths"; + this.hint = "plumbline migrate paths"; } } diff --git a/packages/errors/src/mongo-errors.ts b/packages/errors/src/mongo-errors.ts deleted file mode 100644 index 8031794..0000000 --- a/packages/errors/src/mongo-errors.ts +++ /dev/null @@ -1,44 +0,0 @@ -export class MongoConfigError extends Error { - override readonly name = "MongoConfigError"; - readonly hint: string; - - constructor(hint: string) { - super(`Mongo URI is not configured. Run:\n ${hint}`); - this.hint = hint; - } -} - -export class MongoConnectError extends Error { - override readonly name = "MongoConnectError"; - - constructor(uri: string, cause: unknown) { - super(`Failed to connect to MongoDB at ${redactUri(uri)}: ${describe(cause)}`); - this.cause = cause; - } -} - -export class MongoNotConnectedError extends Error { - override readonly name = "MongoNotConnectedError"; - - constructor() { - super("MongoDB client is not connected. Call connectMongo() first."); - } -} - -export class KnowledgeNotFoundError extends Error { - override readonly name = "KnowledgeNotFoundError"; - readonly knowledgeId: string; - - constructor(knowledgeId: string) { - super(`No knowledge document found with knowledgeId="${knowledgeId}".`); - this.knowledgeId = knowledgeId; - } -} - -function describe(cause: unknown): string { - return cause instanceof Error ? cause.message : String(cause); -} - -function redactUri(uri: string): string { - return uri.replace(/\/\/([^:]+):([^@]+)@/u, "//$1:***@"); -} diff --git a/packages/errors/src/redis-errors.ts b/packages/errors/src/redis-errors.ts deleted file mode 100644 index 785f524..0000000 --- a/packages/errors/src/redis-errors.ts +++ /dev/null @@ -1,34 +0,0 @@ -export class RedisConfigError extends Error { - override readonly name = "RedisConfigError"; - readonly hint: string; - - constructor(hint: string) { - super(`Redis URL is not configured. Run:\n ${hint}`); - this.hint = hint; - } -} - -export class RedisConnectError extends Error { - override readonly name = "RedisConnectError"; - - constructor(url: string, cause: unknown) { - super(`Failed to connect to Redis at ${redactUri(url)}: ${describe(cause)}`); - this.cause = cause; - } -} - -export class RedisNotConnectedError extends Error { - override readonly name = "RedisNotConnectedError"; - - constructor() { - super("Redis client is not connected. Call connectRedis() first."); - } -} - -function describe(cause: unknown): string { - return cause instanceof Error ? cause.message : String(cause); -} - -function redactUri(uri: string): string { - return uri.replace(/\/\/([^:]+):([^@]+)@/u, "//$1:***@"); -} diff --git a/packages/ingest-business-context/README.md b/packages/ingest-business-context/README.md index 23400ba..ad98872 100644 --- a/packages/ingest-business-context/README.md +++ b/packages/ingest-business-context/README.md @@ -54,6 +54,6 @@ The package consumes `JobType.BusinessContextProcessing` jobs. For each job it: `///////meta-output/business-context/` via the `@bb/ingest-github` path helpers (`businessContextDir(knowledgeId, commitHash, slug)`) — this package never invents its own layout. The - helper is **async**: it reads `KnowledgeDoc` from Mongo to derive + helper is **async**: it reads `KnowledgeDoc` from the document store to derive `(orgId, owner, repo)` from `info.repoUrl`, then resolves the commit-scoped path. Every call site awaits. diff --git a/packages/ingest-business-context/src/worker/README.md b/packages/ingest-business-context/src/worker/README.md index d74e95f..c308110 100644 --- a/packages/ingest-business-context/src/worker/README.md +++ b/packages/ingest-business-context/src/worker/README.md @@ -1,6 +1,6 @@ # `worker/` — context -BullMQ worker registration. +Queue worker registration. | File | Responsibility | | ------------- | ---------------------------------------------------------------------- | diff --git a/packages/ingest-business-context/src/worker/handler.ts b/packages/ingest-business-context/src/worker/handler.ts index 5be25de..b6b4eda 100644 --- a/packages/ingest-business-context/src/worker/handler.ts +++ b/packages/ingest-business-context/src/worker/handler.ts @@ -32,7 +32,7 @@ function resolveOrgId(payload: BusinessContextProcessingPayload): string { } /** - * BullMQ job handler for `JobType.BusinessContextProcessing`. Runs the disk + * Queue job handler for `JobType.BusinessContextProcessing`. Runs the disk * strategy then the graph store. Re-reads the persisted analysis from disk * before the graph step so a deferred / split execution path produces the same * result as the inline path. diff --git a/packages/ingest-core/package.json b/packages/ingest-core/package.json index b74f948..1aaf62f 100644 --- a/packages/ingest-core/package.json +++ b/packages/ingest-core/package.json @@ -24,7 +24,6 @@ "@bb/graph-db": "workspace:*", "@bb/llm": "workspace:*", "@bb/logger": "workspace:*", - "@bb/mongo": "workspace:*", "@bb/neo4j": "workspace:*", "@bb/sqlite": "workspace:*", "@bb/types": "workspace:*" diff --git a/packages/ingest-core/src/backfill/README.md b/packages/ingest-core/src/backfill/README.md index f580f19..d9673c0 100644 --- a/packages/ingest-core/src/backfill/README.md +++ b/packages/ingest-core/src/backfill/README.md @@ -64,7 +64,7 @@ mutation into `FileAnalysisCache`. ## External dependencies -`@bb/llm` (`askJsonLLM`), `@bb/logger`, `@bb/mongo` (types only — +`@bb/llm` (`askJsonLLM`), `@bb/logger`, `@bb/db-core` (types only — `FileAnalysis`, `FileAnalysisSection`), the sibling `flat-folder/file-analysis-cache.ts`, and the prompts under `flat-folder/prompts/backfill.ts`. diff --git a/packages/ingest-core/src/big-file/condenser.ts b/packages/ingest-core/src/big-file/condenser.ts index d324808..c65eff5 100644 --- a/packages/ingest-core/src/big-file/condenser.ts +++ b/packages/ingest-core/src/big-file/condenser.ts @@ -3,7 +3,7 @@ import { LlmConfigError, LlmError } from "@bb/errors"; import { logger } from "@bb/logger"; import { Config } from "@bb/types"; import { getConfigValue } from "@bb/config"; -import type { FileAnalysis, FileAnalysisSection } from "@bb/mongo"; +import type { FileAnalysis, FileAnalysisSection } from "@bb/db-core"; import type { ChunkAnalysisResult } from "@bb/ingest-core"; import type { AnalyzedFileResult } from "@bb/ingest-core"; import { FALLBACK_LANGUAGE, emptyFileAnalysis } from "@bb/ingest-core"; diff --git a/packages/ingest-core/src/eligible-files.ts b/packages/ingest-core/src/eligible-files.ts index 0205889..dcfa791 100644 --- a/packages/ingest-core/src/eligible-files.ts +++ b/packages/ingest-core/src/eligible-files.ts @@ -5,7 +5,7 @@ import type { ArchiveSink, SourceReader } from "#src/types/pipeline.ts"; import { affectedFolderPaths } from "./folder-path.ts"; import type { ScanManifest } from "./scan-manifest.ts"; -export const ELIGIBLE_FILES_RELATIVE_PATH = ".bytebell/eligible_files.json"; +export const ELIGIBLE_FILES_RELATIVE_PATH = ".plumbline/eligible_files.json"; export interface EligibleFilesDocument { knowledgeId: string; @@ -25,7 +25,7 @@ export interface WriteEligibleFilesInput { /** * Persist the canonical list of files the analyzer is about to process, * BEFORE any small-file or big-file LLM call runs. The downstream - * `@bytebell/knowledge-validation` service reads this artifact via the same + * `@plumbline/knowledge-validation` service reads this artifact via the same * source layer to cross-check that every eligible file landed in Neo4j. * * Writes to whichever source layer is active: local disk when the source @@ -50,7 +50,7 @@ export async function writeEligibleFiles(input: WriteEligibleFilesInput): Promis let wrote = false; if (input.source.localRepoDir.length > 0) { - const targetDir = path.join(input.source.localRepoDir, ".bytebell"); + const targetDir = path.join(input.source.localRepoDir, ".plumbline"); const targetFile = path.join(targetDir, "eligible_files.json"); await mkdir(targetDir, { recursive: true }); await writeFile(targetFile, content, "utf8"); diff --git a/packages/ingest-core/src/phases/analyse-big-files.ts b/packages/ingest-core/src/phases/analyse-big-files.ts index 71d8a45..f4d7ee7 100644 --- a/packages/ingest-core/src/phases/analyse-big-files.ts +++ b/packages/ingest-core/src/phases/analyse-big-files.ts @@ -236,7 +236,7 @@ export async function analyseBigFiles(input: AnalyseBigFilesInput): Promise//////repository/ -// ~/.bytebell/orgs///////meta-output/... +// ~/.plumbline/orgs///////repository/ +// ~/.plumbline/orgs///////meta-output/... // // For local sources (no owner/repo) the branch collapses to: // -// ~/.bytebell/orgs//local///repository/ -// ~/.bytebell/orgs//local///meta-output/... +// ~/.plumbline/orgs//local///repository/ +// ~/.plumbline/orgs//local///meta-output/... // // Every commit gets its own self-contained snapshot — repository + analysis // + summaries together — rather than scattering meta-output across a @@ -41,7 +41,7 @@ export type RepoLocation = KernelRepoLocation; /** Root of the orgs tree. Single tenant in OSS, but the path still carries orgId. */ export function orgsRoot(): string { - return orgsRootFor(getBytebellHome()); + return orgsRootFor(getPlumblineHome()); } /** @@ -49,17 +49,17 @@ export function orgsRoot(): string { * knowledgeId, owner?, repo?, commitHash)` tuple lives under this path. */ export function commitBaseDir(loc: RepoLocation): string { - return commitBaseDirFor(getBytebellHome(), loc); + return commitBaseDirFor(getPlumblineHome(), loc); } /** Clone destination — the cloned source tree for this commit. */ export function repositoryDir(loc: RepoLocation): string { - return repositoryDirFor(getBytebellHome(), loc); + return repositoryDirFor(getPlumblineHome(), loc); } /** Meta-output root — all analysis artifacts for this commit. */ export function metaOutputRoot(loc: RepoLocation): string { - return metaOutputRootFor(getBytebellHome(), loc); + return metaOutputRootFor(getPlumblineHome(), loc); } /** @@ -75,7 +75,7 @@ export function orgRegistryDirV2(loc: RepoLocation): string { * source of truth for every per-commit meta artifact path. */ export function pathsFor(loc: RepoLocation): MetaPaths { - return bytebellPathsFor(getBytebellHome(), loc); + return plumblinePathsFor(getPlumblineHome(), loc); } /** Business-context directory for a specific titled context, under this commit's meta-output. */ @@ -93,7 +93,7 @@ export async function ensureCommitDirs(loc: RepoLocation): Promise { } // ───────────────────────────────────────────────────────────────────────────── -// knowledgeId-keyed resolvers. Now Mongo-aware: they look up `KnowledgeDoc` +// knowledgeId-keyed resolvers. Now store-aware: they look up `KnowledgeDoc` // to derive `(orgId, owner, repo, commitHash)` and delegate to the pure // `pathsFor` resolver above. Used by callers that hold only a knowledgeId // handle — primarily `@bb/ingest-business-context` and the migration @@ -107,7 +107,7 @@ export async function ensureCommitDirs(loc: RepoLocation): Promise { // ───────────────────────────────────────────────────────────────────────────── async function repoLocationFor(knowledgeId: string, commitHash?: string): Promise { - const kDoc = await getKnowledge(knowledgeId); + const kDoc = await knowledgeDb.getKnowledge(knowledgeId); if (kDoc === null) { throw new KnowledgeNotFoundError(knowledgeId); } @@ -149,7 +149,7 @@ async function repoLocationFor(knowledgeId: string, commitHash?: string): Promis } /** - * Per-knowledge meta-output root, resolved through Mongo to the + * Per-knowledge meta-output root, resolved through the document store to the * commit-scoped kube-v2 directory for the current head commit. */ export async function metaRootFor(knowledgeId: string): Promise { diff --git a/packages/ingest-core/src/pipeline/retry-llm.ts b/packages/ingest-core/src/pipeline/retry-llm.ts index 5d6e7bd..81b9dd8 100644 --- a/packages/ingest-core/src/pipeline/retry-llm.ts +++ b/packages/ingest-core/src/pipeline/retry-llm.ts @@ -18,7 +18,7 @@ import { CancellationError } from "#src/pipeline/cancellation.ts"; // Callers wrap a single LLM call (or a small LLM-bound subgraph) in this // helper, then handle the final thrown error themselves — typically by // counting the unit as "failed" and continuing with the rest of the batch, -// so the batch-level retry (BullMQ) can resume from disk on the next attempt. +// so the batch-level retry (the queue) can resume from disk on the next attempt. // ───────────────────────────────────────────────────────────────────────────── export const MAX_LLM_ATTEMPTS = 3; diff --git a/packages/ingest-core/src/pipeline/run-helpers.ts b/packages/ingest-core/src/pipeline/run-helpers.ts index 4d3a4f2..a94c5a4 100644 --- a/packages/ingest-core/src/pipeline/run-helpers.ts +++ b/packages/ingest-core/src/pipeline/run-helpers.ts @@ -8,7 +8,7 @@ import { knowledgeDb } from "@bb/db"; import { knowledgeGraph } from "@bb/graph-db"; /** - * Persists the FAILED state + structured failure reason to Mongo, then + * Persists the FAILED state + structured failure reason to the document store, then * mirrors the state into Neo4j on a best-effort basis. Errors from both * sides are swallowed so the throw path is preserved. * @@ -26,7 +26,7 @@ export async function persistFailure( } /** - * Persists the non-terminal HALTED state + structured failure reason to Mongo, + * Persists the non-terminal HALTED state + structured failure reason to the store, * then mirrors the state into Neo4j (best-effort). Used by the pipeline catch * paths for *transient* failures, where the queue retry mechanism will retry * the job and — only on exhaustion — promote HALTED → FAILED. Mirrors @@ -43,7 +43,7 @@ export async function persistHalted( } /** - * Persists the terminal CORRUPTED state + structured failure reason to Mongo, + * Persists the terminal CORRUPTED state + structured failure reason to the store, * then mirrors it into Neo4j (best-effort). Used for the `repo_unavailable` * category: the source repo is gone/inaccessible, so the row leaves PROCESSED * and the auto-pull sweep stops re-pulling it. Mirrors `persistFailure` so the @@ -61,8 +61,8 @@ export async function persistCorrupted( /** * Stamps `retryable = false` on a thrown error. Property contract read by the - * queue worker wrappers (`@bytebell/queue` BullMQManager and OSS `queue-bullmq`) - * to convert the failure into a BullMQ `UnrecoverableError` — stopping further + * queue worker wrappers to convert the failure into an unrecoverable error — + * stopping further * automatic attempts. Used for non-retryable failures the pipeline has already * moved to terminal FAILED. Duck-typed (no cross-tier import) by the queue. */ diff --git a/packages/ingest-core/src/pipeline/skip-decisions/README.md b/packages/ingest-core/src/pipeline/skip-decisions/README.md index 44621f7..a76b881 100644 --- a/packages/ingest-core/src/pipeline/skip-decisions/README.md +++ b/packages/ingest-core/src/pipeline/skip-decisions/README.md @@ -22,7 +22,7 @@ defaults). 5. Accept if the extension is in KNOWN_LANGUAGE_EXTENSIONS (fast-path, no LLM). 6. Cache lookup by `extensions:` (or `filenames:` when extensionless). 7. Cache miss → askYesNoLLM with the first N chars of the file content. -8. Persist verdict to ~/.bytebell/llmDecisions.json. LLM failure → reject + cache the rejection. +8. Persist verdict to ~/.plumbline/llmDecisions.json. LLM failure → reject + cache the rejection. ``` Steps 1-6 are pure CPU + cached lookup — they run synchronously via @@ -62,7 +62,7 @@ interface SkipDecider { `SEED_DIRECTORIES`, `SEED_FILENAMES`, `SEED_EXTENSIONS`, `SEED_GLOBS`, `KNOWN_LANGUAGE_EXTENSIONS`, and `matchesAnyGlob`. Compiles globs once and caches the resulting `RegExp` for reuse. -- `cache.ts` — load/save `~/.bytebell/llmDecisions.json` with atomic write +- `cache.ts` — load/save `~/.plumbline/llmDecisions.json` with atomic write (write-tmp + fsync + rename + mode 0600). Mirror's kube's JSON shape so users can hand-edit `ignore: true → false` to permanently un-ignore an extension. @@ -121,4 +121,4 @@ interface SkipDecider { - LLM failure defaults to reject and caches the rejection — matches kube's one-shot-rule behavior. Users can hand-edit the cache to revisit. - The decider is process-local: tests may construct one with `cachePath` - pointing at a temp file to avoid touching the real `~/.bytebell/`. + pointing at a temp file to avoid touching the real `~/.plumbline/`. diff --git a/packages/ingest-core/src/pipeline/skip-decisions/cache.ts b/packages/ingest-core/src/pipeline/skip-decisions/cache.ts index 241f985..1d77734 100644 --- a/packages/ingest-core/src/pipeline/skip-decisions/cache.ts +++ b/packages/ingest-core/src/pipeline/skip-decisions/cache.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { getBytebellHome } from "@bb/config"; +import { getPlumblineHome } from "@bb/config"; import { Config } from "@bb/types"; import { getConfigValue } from "@bb/config"; import { logger } from "@bb/logger"; @@ -33,7 +33,7 @@ export function defaultCachePath(): string { if (configured.length > 0) { return configured; } - return path.join(getBytebellHome(), "llmDecisions.json"); + return path.join(getPlumblineHome(), "llmDecisions.json"); } export function emptyCache(): DecisionsCache { diff --git a/packages/ingest-core/src/pipeline/skip-decisions/seed-data/README.md b/packages/ingest-core/src/pipeline/skip-decisions/seed-data/README.md index 31aba09..a5bd9a4 100644 --- a/packages/ingest-core/src/pipeline/skip-decisions/seed-data/README.md +++ b/packages/ingest-core/src/pipeline/skip-decisions/seed-data/README.md @@ -35,7 +35,7 @@ at module-init time by [../seed.ts](../seed.ts) via `{ directories|extensions|filenames|filename_globs: Record }`. Currently **not consumed** by the public runtime. Kept here for reference and as the seed source for a future enhancement that would copy these - entries into `~/.bytebell/llmDecisions.json` on first install (so users + entries into `~/.plumbline/llmDecisions.json` on first install (so users start with kube's curated reject list pre-loaded rather than discovering it one LLM call at a time). diff --git a/packages/ingest-core/src/prompts/file-analysis-fields.ts b/packages/ingest-core/src/prompts/file-analysis-fields.ts index 74ceef5..e4ad072 100644 --- a/packages/ingest-core/src/prompts/file-analysis-fields.ts +++ b/packages/ingest-core/src/prompts/file-analysis-fields.ts @@ -11,7 +11,7 @@ export const FILE_ANALYSIS_FIELDS_BLOCK = `- purpose : string — A - businessEntities : string[] — Domain nouns the code manipulates (e.g. "User", "Invoice", "Knowledge"). Names from the domain language, not generic CS terms. Max 8 entries. - systemCapabilities : string[] — Capabilities this file contributes to the system (e.g. "GitHub repo ingestion", "JWT signing", "rate-limited fetch"). Action-oriented phrases. Max 6 entries. - sideEffects : string[] — Observable side effects (e.g. "writes file-analysis/*.json", "sends HTTP POST to OpenRouter", "executes git clone"). Concrete and traceable. Max 8 entries. -- configDependencies : string[] — Config keys, env vars, or settings the file reads (e.g. "Config.OpenrouterApiKey", "BYTEBELL_HOME"). Exact key names. +- configDependencies : string[] — Config keys, env vars, or settings the file reads (e.g. "Config.OpenrouterApiKey", "PLUMBLINE_HOME"). Exact key names. - dataFlowDirection : string — One of: "inbound" (consumes external input), "outbound" (emits to external systems), "internal" (no external boundary crossed), "bidirectional". Empty string if ambiguous. - integrationSurface : string[] — External systems this file touches (e.g. "OpenRouter API", "Neo4j", "Redis", "filesystem"). Names of systems, not function names. - contractsProvided : string[] — Public exports / endpoints / interfaces this file exposes to other parts of the system (e.g. "registerGithubWorkers", "GET /api/v1/health"). Exact names. diff --git a/packages/ingest-core/src/types/README.md b/packages/ingest-core/src/types/README.md index f57baaa..4aa05b8 100644 --- a/packages/ingest-core/src/types/README.md +++ b/packages/ingest-core/src/types/README.md @@ -54,7 +54,7 @@ llmCallContext? }`; `llmCallContext` is the optional `AskLlmOptions` `targetCommit`, which index doesn't. - `meta-paths.ts` — `MetaPaths` shape — the per-commit `repositoryDir` + `metaOutputRoot` siblings under - `~/.bytebell/orgs///////`, + `~/.plumbline/orgs///////`, plus the named leaf paths (`fileAnalysisDir`, `folderSummariesDir`, …) computed under `metaOutputRoot`. `metaRoot` is preserved as a back-compat alias for `metaOutputRoot`. diff --git a/packages/ingest-core/src/types/meta-paths.ts b/packages/ingest-core/src/types/meta-paths.ts index f85d85e..61748e8 100644 --- a/packages/ingest-core/src/types/meta-paths.ts +++ b/packages/ingest-core/src/types/meta-paths.ts @@ -1,7 +1,7 @@ /** * Per-commit meta artifact paths. Built by `pathsFor(loc)`, where * `repositoryDir` and `metaOutputRoot` are siblings under - * `~/.bytebell/orgs///////`, + * `~/.plumbline/orgs///////`, * and every leaf path (file-analysis, folder-summaries, etc.) lives under * `metaOutputRoot`. * diff --git a/packages/ingest-core/src/types/pipeline.ts b/packages/ingest-core/src/types/pipeline.ts index c8121c4..5f72abd 100644 --- a/packages/ingest-core/src/types/pipeline.ts +++ b/packages/ingest-core/src/types/pipeline.ts @@ -157,7 +157,7 @@ export type SourceFactory = (input: SourceFactoryInput) => Promise//////repository/` -and persists per-file results to Mongo (`raw` collection via -`@bb/mongo`) **and** Neo4j (`:File` nodes + `:HAS_KEYWORD` / +`~/.plumbline/orgs///////repository/` +and persists per-file results to SQLite (`raw_files` table via +`@bb/db`) **and** Neo4j (`:File` nodes + `:HAS_KEYWORD` / `:HAS_CLASS` / `:HAS_FUNCTION` / `:HAS_IMPORT_INTERNAL` / `:HAS_IMPORT_EXTERNAL` rels via `@bb/neo4j`). @@ -37,12 +37,12 @@ The package owns: Field definitions live in `FILE_ANALYSIS_FIELDS_BLOCK` (single source of truth; wording adapted from kube-package's `fileAnalysisFieldDefs.ts`) -- Translation of LLM output JSON → `RawFileDoc` shape (Mongo) **and** +- Translation of LLM output JSON → `RawFileDoc` shape (SQLite) **and** `:File` graph node + entity relationships (Neo4j), with safe fallbacks for malformed responses - Knowledge `status.state` transitions (`Processing` on start, `Processed` on success, `Failed` on caught error) — kept in lock-step - between Mongo and Neo4j via a shared `transitionState` helper + between SQLite and Neo4j via a shared `transitionState` helper - The `IngestionStrategy` pluggable abstraction — the worker delegates the post-clone scan/analyze/persist loop to a strategy instance. v1 ships one concrete: `BasicFileAnalysisStrategy` @@ -115,7 +115,7 @@ plus a branch in `pickStrategy()` in `src/index.ts`. ## Data ownership -- `~/.bytebell/orgs//github/////repository/` +- `~/.plumbline/orgs//github/////repository/` — the cloned working tree for each indexed commit (for `github_index` / `github_pull`). Persisted across job retries (clone is idempotent: `git fetch + reset` if `.git` exists). Each commit gets its own @@ -123,13 +123,13 @@ plus a branch in `pickStrategy()` in `src/index.ts`. current head until the operator prunes. Local ingest jobs do NOT populate this dir; they read from `KnowledgeDoc.source.sourcePath` (the user's original directory) directly. Never deleted automatically — - `bytebell clean` per [docs/arch.md:157](../../docs/arch.md#L157) + `plumbline clean` per [docs/arch.md:157](../../docs/arch.md#L157) will own removal. - The Knowledge document's `status.state` field — written via - `setKnowledgeState` from `@bb/mongo` AND + `setKnowledgeState` from `@bb/db` AND `setKnowledgeStateInGraph` from `@bb/neo4j`, kept in lock-step. - Raw documents (one per scanned file) — written via `upsertRawFile` - from `@bb/mongo`. Compound key `(knowledgeId, relativePath)`. + from `@bb/db`. Compound key `(knowledgeId, relativePath)`. - `:File` graph nodes + `:HAS_FILE` / `:HAS_KEYWORD` / `:HAS_CLASS` / `:HAS_FUNCTION` / `:HAS_IMPORT_INTERNAL` / `:HAS_IMPORT_EXTERNAL` relationships — written via `upsertFileNode` from `@bb/neo4j`. @@ -166,8 +166,8 @@ plus a branch in `pickStrategy()` in `src/index.ts`. for one JSON object keyed by integer label that returns one summary per folder. Bigger folders take the individual single-folder path. Roll back to one LLM call per folder via - `bytebell set folder.summary.batch.size 1`. -3. **Clone idempotent.** Re-runs (BullMQ retries) call `git fetch` + + `plumbline set folder.summary.batch.size 1`. +3. **Clone idempotent.** Re-runs (queue retries) call `git fetch` + `git reset --hard` in the existing dir rather than re-cloning. Tokens are re-injected into the remote URL each time. 4. **Token redaction.** `GitCloneError` carries the **redacted** repo @@ -176,14 +176,14 @@ plus a branch in `pickStrategy()` in `src/index.ts`. 5. **State transition order.** `Processing` is set _before_ any clone work. `Processed` is set _only_ after the entire scan + analyze loop completes. On any thrown error, the handler best-effort sets `Failed` - then re-throws so BullMQ records the retry. + then re-throws so the queue records the retry. 6. **Fail-soft analysis, fail-hard infra.** A single file's LLM call failing falls back to an empty-analysis Raw doc and processing continues. In the big-file path, a single chunk failure contributes an empty analysis to the merge but does not stop the file; a condensation-call failure falls through to deterministic `dedupAnalyses` so the merged result is always well-formed. A clone - failure or Mongo write failure throws and propagates to BullMQ for + failure or document-store write failure throws and propagates to the queue for retry under the queue's `attempts: 3`. 7. **Hardcoded filters only.** No LLM-based ignore decisions in v0. The directory / file / extension blocklists in `scan.ts` are the only @@ -193,7 +193,7 @@ plus a branch in `pickStrategy()` in `src/index.ts`. - Node built-ins only: `node:child_process` (git), `node:fs/promises` (walk), `node:crypto` (sha-256), `node:path`, `node:util` -- Workspace deps: `@bb/config`, `@bb/errors`, `@bb/llm`, `@bb/mongo`, +- Workspace deps: `@bb/config`, `@bb/errors`, `@bb/llm`, `@bb/db`, `@bb/neo4j`, `@bb/queue`, `@bb/types` - System binary: **`git`** must be on the user's `PATH`. Documented in the project README as a runtime prerequisite. @@ -222,7 +222,7 @@ plus a branch in `pickStrategy()` in `src/index.ts`. - Model escalation - LLM-based ignore decisions - Cost ledger (the `@bb/llm` package itself doesn't have one yet) -- Auto-cleanup of the `~/.bytebell/orgs////` +- Auto-cleanup of the `~/.plumbline/orgs////` commit-scoped tree (clones + meta-output) ## How to extend @@ -250,7 +250,7 @@ Adding the `github_pull` worker: → invoke a `Strategy` (likely the same `BasicFileAnalysisStrategy`) over a smaller scoped subset of files → delete Raw + graph entries for files removed in the diff (needs `deleteRawFile` and - `deleteFileNode` helpers in `@bb/mongo` / `@bb/neo4j`). + `deleteFileNode` helpers in `@bb/db` / `@bb/neo4j`). 2. In `src/worker.ts`'s `registerGithubWorkers`, also call `registerWorker(JobType.GithubPull, handleGithubPull)`. 3. Update _Public exports_ / _Out of scope_ here. diff --git a/packages/ingest-github/package.json b/packages/ingest-github/package.json index 3675ab1..01a4f1e 100644 --- a/packages/ingest-github/package.json +++ b/packages/ingest-github/package.json @@ -23,7 +23,6 @@ "@bb/llm": "workspace:*", "@bb/logger": "workspace:*", "@bb/mcp": "workspace:*", - "@bb/mongo": "workspace:*", "@bb/sqlite": "workspace:*", "@bb/neo4j": "workspace:*", "@bb/queue": "workspace:*", diff --git a/packages/ingest-github/src/README.md b/packages/ingest-github/src/README.md index c6a8ee7..b57cc6e 100644 --- a/packages/ingest-github/src/README.md +++ b/packages/ingest-github/src/README.md @@ -6,7 +6,7 @@ under the v2 flat-folder strategy. ## Tier -Domain (composes infra: `@bb/config`, `@bb/llm`, `@bb/mongo`, `@bb/neo4j`, +Domain (composes infra: `@bb/config`, `@bb/llm`, `@bb/db`, `@bb/neo4j`, `@bb/queue`, `@bb/logger`, `@bb/types`, `@bb/errors`). ## Top-level files @@ -17,17 +17,17 @@ Domain (composes infra: `@bb/config`, `@bb/llm`, `@bb/mongo`, `@bb/neo4j`, consumers wire against their own queue/registry: - Factories: `createFlatFolderStrategy`, `createLlmFileAnalyzer`, `createDiskSourceReader`, `createPipelineRunner` (the orchestrator), - `createGithubIngestHandler` / `createLocalIngestHandler` (the BullMQ + `createGithubIngestHandler` / `createLocalIngestHandler` (the queue processor factories used internally by `registerGithubWorkers`). - Direct runner: `runPull(msg, pullFactory?, progressContextFactory?, usageGuard?)` — the pull worker downstream consumers invoke directly from their own registry. - - Helper: `reposRoot()` — resolves `~/.bytebell/repos`. + - Helper: `reposRoot()` — resolves `~/.plumbline/repos`. - Path resolvers: `pathsFor(loc)`, `orgsRoot()`, `ensureCommitDirs(loc)`, `metaRootFor(knowledgeId)`, `businessContextDir(...)`, `orgRegistryDir(...)`, plus the `RepoLocation` type. `ensureCommitDirs` is exported so downstream `SourceFactory` consumers (e.g. - `@bytebell/ingest-gitlab`) can pre-create the commit-scoped + `@plumbline/ingest-gitlab`) can pre-create the commit-scoped `repository/` + `meta-output/` tree before cloning into it. The runner also calls it inside the `sourceFactory !== undefined` branch so any factory consumer is safe even without pre-creating dirs itself @@ -89,9 +89,9 @@ Domain (composes infra: `@bb/config`, `@bb/llm`, `@bb/mongo`, `@bb/neo4j`, and commit anchoring. - **[adapters/](adapters/README.md)** — `createLlmFileAnalyzer(deps)` returns the `FileAnalyzer` port; prompts are injected by the strategy. -- **[payload/](payload/README.md)** — defensive narrowing of BullMQ +- **[payload/](payload/README.md)** — defensive narrowing of queue payloads and `isEnvelopeCoherent`. -- **[handlers/](handlers/README.md)** — BullMQ entry shells; pure +- **[handlers/](handlers/README.md)** — queue entry shells; pure validation + delegate to the runner. - **[strategies/](strategies/README.md)** — one subfolder per strategy. Currently `flat-folder/` (active) and `basic-file-analysis/` (archived). @@ -99,16 +99,16 @@ Domain (composes infra: `@bb/config`, `@bb/llm`, `@bb/mongo`, `@bb/neo4j`, ## Module dependency graph (abridged) ``` -types/ → @bb/mongo (FileAnalysis), @bb/types +types/ → @bb/db-core (FileAnalysis), @bb/types pipeline/ → types/, @bb/config, @bb/types, @bb/errors, @bb/llm (tokenizer), node:* -adapters/ → types/, @bb/llm, @bb/mongo, @bb/logger +adapters/ → types/, @bb/llm, @bb/db, @bb/logger payload/ → @bb/types, @bb/errors handlers/ → types/, payload/, @bb/types, @bb/errors strategies/flat-folder/ → types/, pipeline/, adapters/, @bb/llm, - @bb/mongo, @bb/neo4j, @bb/logger, @bb/config, + @bb/db, @bb/neo4j, @bb/logger, @bb/config, @bb/types, @bb/errors -pipeline/run.ts → types/, pipeline/*, @bb/mongo, @bb/neo4j, +pipeline/run.ts → types/, pipeline/*, @bb/db, @bb/neo4j, @bb/llm, @bb/errors, @bb/logger, @bb/types index.ts → handlers/, pipeline/run.ts, strategies/flat-folder/, adapters/, githubApi.ts, @bb/queue, @bb/types, @@ -137,13 +137,13 @@ Tier flow is strict: `types/` is the leaf; `pipeline/`, `adapters/`, `askYesNoLLM`. OSS standalone leaves these unset and falls back to `Config.OpenrouterApiKey` + `Config.LlmProvider`. - **State transitions are explicit and dual-written.** `pipeline/run.ts` - transitions Mongo state to `PROCESSING` before any work, `PROCESSED` on + transitions the knowledge state to `PROCESSING` before any work, `PROCESSED` on success, `FAILED` best-effort on uncaught errors. Each transition mirrors to Neo4j via `setKnowledgeStateInGraph`, swallowing Neo4j failures so a graph hiccup doesn't fail the job. - **`CancellationError` is not `FAILED`.** A `throwIfCancelled` thrown inside the strategy propagates past `pipeline/run.ts`, which clears the - in-process cancel flag and re-throws — Mongo state stays at `PROCESSING` + in-process cancel flag and re-throws — the knowledge state stays at `PROCESSING` (clearable by re-running). Failed state is reserved for actual errors. - **Disk is the inter-phase contract.** The flat-folder strategy writes `bigFiles.json`, `file-analysis/*.json`, `big-file-analysis/.manifest.json`, @@ -151,14 +151,14 @@ Tier flow is strict: `types/` is the leaf; `pipeline/`, `adapters/`, resumes from the next sub-phase boundary on the next run. - **Per-file fallback never throws past the file.** LLM / parse / IO failures inside a file degrade to an empty analysis + WARN log; the batch - continues. Whole-strategy errors propagate to BullMQ for retry semantics. + continues. Whole-strategy errors propagate to the queue for retry semantics. - **No env reads.** Every setting flows through `@bb/config`. Repo path - through `pipeline/paths.ts.reposRoot()` → `getBytebellHome()`. + through `pipeline/paths.ts.reposRoot()` → `getPlumblineHome()`. - **Token redaction at error boundaries.** `GitCloneError` redacts URL userinfo. The git binary is invoked via `execFile` with no shell — no injection surface. - **No outbound calls except OpenRouter.** `@bb/llm` is the single egress; - every other dependency reads from / writes to local Mongo / Neo4j / Redis. + every other dependency reads from / writes to local SQLite / Neo4j. ## Adding a strategy diff --git a/packages/ingest-github/src/bootstrap.ts b/packages/ingest-github/src/bootstrap.ts index 375e2d8..8e5fa8b 100644 --- a/packages/ingest-github/src/bootstrap.ts +++ b/packages/ingest-github/src/bootstrap.ts @@ -3,9 +3,9 @@ import { seedLoggerFactory, type LoggerFactory } from "@bb/logger"; import { Config } from "@bb/types"; import { connectDb } from "@bb/db"; import { connectGraph, indexesGraph } from "@bb/graph-db"; -import "@bb/mongo"; +import "@bb/sqlite"; import "@bb/neo4j"; -// NOTE: provider packages (`@bb/mongo`, `@bb/sqlite`, `@bb/neo4j`, `@bb/ladybug`) +// NOTE: provider packages (`@bb/sqlite`, `@bb/neo4j`, `@bb/ladybug`) // are deliberately NOT imported here. Importing them is a side-effect that // registers the provider AND loads its driver/native binding (e.g. `@bb/ladybug` // pulls in the `@ladybugdb/core` native addon). Pulling them in from this shared diff --git a/packages/ingest-github/src/handlers/README.md b/packages/ingest-github/src/handlers/README.md index 267c1ae..4127800 100644 --- a/packages/ingest-github/src/handlers/README.md +++ b/packages/ingest-github/src/handlers/README.md @@ -1,13 +1,13 @@ # `@bb/ingest-github/src/handlers` -BullMQ job entry shells. Pure boundary: narrow the payload, verify envelope +Queue job entry shells. Pure boundary: narrow the payload, verify envelope coherence, delegate to the pipeline runner. No I/O, no state transitions, no clone — those belong in `pipeline/run.ts`. ## Files - `ingest-job.ts` — `createGithubIngestHandler(deps)` and - `createLocalIngestHandler(deps)` both return BullMQ-shaped + `createLocalIngestHandler(deps)` both return queue-shaped `(msg) => Promise` callbacks (the summary carries per-commit `tokenUsage` including `costUsd` for downstream consumers that need to mirror it). `IngestJobHandlerDeps` accepts an optional @@ -16,13 +16,13 @@ no clone — those belong in `pipeline/run.ts`. forwards the returned guard to `runner.run({ ..., usageGuard })`. OSS standalone leaves the factory undefined and the pipeline runs without any quota enforcement. They throw `IngestError` on validation - failures; everything else propagates to BullMQ as the worker's failure + failures; everything else propagates to the queue as the worker's failure path. - `README.md` — this file. ## Invariants -- Handlers do not touch Mongo, Neo4j, the filesystem, or the LLM. They are +- Handlers do not touch the document store, Neo4j, the filesystem, or the LLM. They are pure validators that delegate to `IngestRunnerDeps.run`. - Handlers are factory-built so the wiring in `index.ts` injects the runner the strategy is composed against. Tests can inject a mock runner. diff --git a/packages/ingest-github/src/index.ts b/packages/ingest-github/src/index.ts index b007303..989454b 100644 --- a/packages/ingest-github/src/index.ts +++ b/packages/ingest-github/src/index.ts @@ -1,6 +1,6 @@ import path from "node:path"; import { JobType } from "@bb/types"; -import { getBytebellHome } from "@bb/config"; +import { getPlumblineHome } from "@bb/config"; import { registerWorker } from "@bb/queue"; import type { IngestStrategy, PullFactory, PullRunner, SourceFactory, ProgressContextFactory } from "@bb/ingest-core"; import { dbProgressContextFactory, orgsRoot } from "@bb/ingest-core"; @@ -66,7 +66,7 @@ export function registerLocalIngestWorker(strategy: IngestStrategy): void { } /** - * Compatibility shim — the legacy `/repos/` directory still + * Compatibility shim — the legacy `/repos/` directory still * hosts the LLM-decision cache (`repos/llmdecisions/`) and the * local-snapshots staging dir for `localIndexRoute`. Knowledge / ingest * artifacts moved to the commit-scoped `orgs/` tree, but `reposRoot()` is @@ -74,7 +74,7 @@ export function registerLocalIngestWorker(strategy: IngestStrategy): void { * the root. */ export function reposRoot(): string { - return path.join(getBytebellHome(), "repos"); + return path.join(getPlumblineHome(), "repos"); } // ── GitHub provider surface (lives in this package) ───────────────────────── diff --git a/packages/ingest-github/src/payload/README.md b/packages/ingest-github/src/payload/README.md index 1fce3a9..a48043b 100644 --- a/packages/ingest-github/src/payload/README.md +++ b/packages/ingest-github/src/payload/README.md @@ -1,14 +1,14 @@ # `@bb/ingest-github/src/payload` -Defensive payload narrowing for the BullMQ job entry. The public repo's queue +Defensive payload narrowing for the queue job entry. The public repo's queue payloads come from `@bb/types` so they are already typed, but the handler re-validates because: -1. Job documents are persisted to Redis between enqueue and execute. A schema +1. Job documents are persisted between enqueue and execute. A schema change between server restarts could leave stale payloads at the head of the queue; narrowing produces a structured `IngestError` instead of a `TypeError` from accessing `undefined`. -2. Local `bytebell ingest ` paths can construct payloads outside the +2. Local `plumbline ingest ` paths can construct payloads outside the normal `enqueueGithubIndex` factory. ## Files @@ -19,11 +19,11 @@ re-validates because: ## Invariants - Narrow functions return the canonical typed shape, not the raw `Record`. -- Failed validation throws `IngestError(knowledgeId, reason)` so the BullMQ +- Failed validation throws `IngestError(knowledgeId, reason)` so the queue worker promotes it into a terminal `JobResult.FAILED` via the existing error path. - Optional fields are passed through only when present and non-empty. In particular, `orgId` is preserved when the payload carries it (downstream consumers may set it per-job); when absent, the pipeline falls back - to `Config.OrgId` from `~/.bytebell/config.json` (locked to `"local"` in + to `Config.OrgId` from `~/.plumbline/config.json` (locked to `"local"` in OSS — see `@bb/config/src/README.md`). diff --git a/packages/ingest-github/src/payload/narrow.ts b/packages/ingest-github/src/payload/narrow.ts index 36ac843..ae4b2ce 100644 --- a/packages/ingest-github/src/payload/narrow.ts +++ b/packages/ingest-github/src/payload/narrow.ts @@ -35,7 +35,7 @@ function narrowPatch(value: unknown): IgnoreOverridePatch | undefined { } /** - * Copy per-job ignore overrides off the BullMQ payload, if present and + * Copy per-job ignore overrides off the queue payload, if present and * well-formed. The enqueue boundary (enterprise gateway) stamps this from the * org's stored ignore config; OSS standalone never sets it. */ @@ -60,7 +60,7 @@ function attachIgnoreOverrides(rec: Record, target: { ignoreOve /** * Copies optional LLM credential / model overrides from a payload record onto * a typed payload. Enterprise wrappers resolve per-org credentials at the - * enqueue boundary and stamp them on the BullMQ payload; without this passthrough + * enqueue boundary and stamp them on the queue payload; without this passthrough * the worker would always fall back to global config (and the resolver work is * wasted). OSS standalone leaves all four unset, so nothing happens here. */ diff --git a/packages/ingest-github/src/pipeline/README.md b/packages/ingest-github/src/pipeline/README.md index aa55fba..a9aea96 100644 --- a/packages/ingest-github/src/pipeline/README.md +++ b/packages/ingest-github/src/pipeline/README.md @@ -12,16 +12,16 @@ Domain (sub-folder of `@bb/ingest-github`). ## Files - `paths.ts` — commit-scoped on-disk layout resolver. `pathsFor(loc: -RepoLocation)` is the pure path builder (delegates to `bytebellPathsFor` +RepoLocation)` is the pure path builder (delegates to `plumblinePathsFor` in `@bb/types`). Every per-commit artifact lives under - `~/.bytebell/orgs///////` + `~/.plumbline/orgs///////` with the clone in `repository/` and meta in `meta-output/`. For local knowledges the owner/repo segments collapse: `orgs//local///`. The knowledgeId-keyed helpers (`metaRootFor`, `businessContextDir`, - `orgRegistryDir`) are async — they look up `KnowledgeDoc` from Mongo to + `orgRegistryDir`) are async — they look up `KnowledgeDoc` from the document store to derive the active `RepoLocation` before resolving the path. The legacy - `repos//` + `repos/.meta//` layout is gone; `bytebell migrate + `repos//` + `repos/.meta//` layout is gone; `plumbline migrate paths` walks old data into the new tree. Also exports `encodeMetaPath`/`decodeMetaPath` (slash/backslash → `__SL__`/`__BS__` so paths flatten to one file on disk). @@ -29,7 +29,7 @@ paths` walks old data into the new tree. Also exports Clone-or-fetch+reset against `origin/`, `--depth=1`. The pull plan may later relax depth; ingestion does not need full history. Wraps git failures in `GitCloneError` (from `@bb/errors`). Note this is the - GitHub-side primitive; GitLab knowledges use `@bytebell/ingest-gitlab`'s + GitHub-side primitive; GitLab knowledges use `@plumbline/ingest-gitlab`'s `cloneGitlabRepo` (oauth2 URL form, raises `GitlabCloneError`) via its `SourceFactory` and never enter this code path. - `filters.ts` — `SKIP_DIRS`, `SKIP_FILES`, `BINARY_EXTENSIONS`, `looksBinary`, @@ -95,12 +95,12 @@ deps.skipDecider.decide(input)` per file. Same semantics as before this layout themselves see no effect — but mandatory: without it, `writeScanManifest` would ENOENT on its first write because the `meta-output/` parent dir wouldn't exist. For GitLab knowledges (which - reach the runner via `@bytebell/ingest-gitlab`'s injected `SourceFactory`), + reach the runner via `@plumbline/ingest-gitlab`'s injected `SourceFactory`), `parseGithubRepo` accepts gitlab.com URLs so the `location` built from `parsed.owner` / `parsed.repo` resolves cleanly; subgroup gitlab URLs collapse to two segments at this layer and the factory itself uses the full namespace for the path it clones into. State transitions (`CREATED → QUEUED → INGESTED → …`) are - persisted to Mongo + Neo4j via `transitionState`, and `CancellationError` + persisted to SQLite + Neo4j via `transitionState`, and `CancellationError` is re-thrown without flipping to FAILED. The optional `progressContextFactory` is the runner's own `ProgressContext` source: `runGithub` emits `phaseChanged("clone")` before `syncRepository` (or before the @@ -121,7 +121,7 @@ deps.skipDecider.decide(input)` per file. Same semantics as before this - `pull-failure.ts` — `throwPullFailure(cause, deps)` maps a thrown pull cause to the correct persisted state, then re-throws (never returns). Retryable → `persistHalted` + plain `IngestError` (queue retries); terminal → `persistFailure` + failure SSE + non-retryable throw. **`deps.isAutoPull`**: set for unattended auto-pull / bulk refresh of already-`PROCESSED` knowledge (downstream multi-tenant payloads only — OSS standalone never sets it). When true, on **any** failure it restores `PROCESSED` (`transitionState`), skips both `persistHalted`/`persistFailure` and the failure SSE, and throws non-retryable — so a failed background refresh never degrades a healthy index. Because the row stays `PROCESSED`, the downstream queue finalizer's HALTED→FAILED promotion is inert. The next sweep retries. - `pull-source-resolver.ts` — `resolvePullSourceFromDisk(input)` builds a `SourceReader` + `DiffResult` + `targetCommit` triple by cloning (or fetch+resetting), reading branch HEAD, materialising the shallow clone, asserting branch ancestry, computing the diff, and checking out the target. Returns `{ noOp: true }` when the resolved target matches the previously-indexed commit so the caller can short-circuit to `PROCESSED`. Used only when `runPull`'s caller did not supply a `PullFactory`. Reads `repoUrl` and `branch` directly off `knowledge.info.*` (loaded via - `@bb/mongo.getKnowledge`). The `KnowledgeSource` discriminator (`kind`) is + `@bb/db.knowledgeDb.getKnowledge`). The `KnowledgeSource` discriminator (`kind`) is still read off `knowledge.source` along with `commitId`/`commitHashes`, but the repo coordinates themselves live on `info` — no fallback chain. When `pullFactory` is provided, it returns `{source, diff, targetCommit, @@ -152,7 +152,7 @@ archiveSink?}` and `runPull` skips `syncRepository` + `materialiseEndpoints` `persistStats` write into the `processing_stats` collection has been removed — per-commit token and cost data now lives on the knowledge document's `source.commitHashes[]` (set by `setKnowledgeCommit` from - `@bb/mongo`), with the per-call `costUsd` sourced directly from + `@bb/db`), with the per-call `costUsd` sourced directly from OpenRouter's `response.usage.cost`. - `failure-classifier.ts` — `classifyFailure(cause)` returns `{ reason, category, detail? }` for any thrown ingestion error. @@ -165,7 +165,7 @@ archiveSink?}` and `runPull` skips `syncRepository` + `materialiseEndpoints` `429` → `llm_rate_limit`, `5xx`/no-status → `llm_unreachable`. Anything else → `internal`. Each category produces a single short operator-readable `reason` sentence; the raw provider response body - lives in `detail`. Used by `run.ts`/`pull.ts` catch blocks (Mongo + lives in `detail`. Used by `run.ts`/`pull.ts` catch blocks (document-store persistence via `markKnowledgeFailed`) and `strategies/flat-folder/index.ts` (SSE event via `progressContext.failed`) so both paths share one classification. @@ -183,7 +183,7 @@ archiveSink?}` and `runPull` skips `syncRepository` + `materialiseEndpoints` `clearCancellation`, `isCancelled`, `throwIfCancelled`, `CancellationError`. Strategies call `throwIfCancelled(knowledgeId)` between sub-phases. The cancel HTTP route flips the bit; the orchestrator clears it on a - `CancellationError` re-throw and leaves Mongo state untouched (no FAILED). + `CancellationError` re-throw and leaves the knowledge state untouched (no FAILED). - `concurrency.ts` — `withConcurrency(n)` returns a `limit(task)` function in the `p-limit` style. `runInPool(n, items, task)` is a convenience over async iterables. No external `p-limit` dependency. @@ -193,7 +193,7 @@ archiveSink?}` and `runPull` skips `syncRepository` + `materialiseEndpoints` - Sibling files in this folder may import each other. - Down: `src/types/*` only (intra-package, via the `src/*` alias). - Up: `@bb/config`, `@bb/types`, `@bb/errors`, `@bb/logger`, `node:*`. -- `run.ts` and `pull.ts` additionally import `@bb/mongo` and `@bb/neo4j` +- `run.ts` and `pull.ts` additionally import `@bb/db` and `@bb/neo4j` for state transitions and graph state writes respectively. - `stats.ts` has no cross-package imports — it carries only pure helpers (`repoNameFromUrl`, `localRepoName`, `describe`). @@ -202,7 +202,7 @@ archiveSink?}` and `runPull` skips `syncRepository` + `materialiseEndpoints` ## Invariants - Every file is ≤ 300 lines. -- No graph traversal, no per-file Mongo writes happen here — those live +- No graph traversal, no per-file document-store writes happen here — those live under `strategies/`. `run.ts` only performs end-of-pipeline state transitions and stats persistence. **Exception**: `skip-decisions/` uses `@bb/llm` for the unknown-extension YES/NO gate; this is the diff --git a/packages/ingest-github/src/pipeline/fault-injection.ts b/packages/ingest-github/src/pipeline/fault-injection.ts index 07f717d..123320a 100644 --- a/packages/ingest-github/src/pipeline/fault-injection.ts +++ b/packages/ingest-github/src/pipeline/fault-injection.ts @@ -4,12 +4,12 @@ // One-shot, env-gated transient LLM failure used to exercise the // HALTED → retry → PROCESSED loop end-to-end without a real provider outage. // -// Enable: set env BYTEBELL_FORCE_HALT_ONCE=1 on the worker process +// Enable: set env PLUMBLINE_FORCE_HALT_ONCE=1 on the worker process // (knowledge-server), then ingest a repo. // // Behaviour: the FIRST job attempt for each knowledgeId throws a *retryable* // LlmError (HTTP 503 → category `llm_unreachable`), so the pipeline writes -// HALTED and BullMQ schedules a retry (~5s). The SECOND attempt is allowed to +// HALTED and the queue schedules a retry (~5s). The SECOND attempt is allowed to // proceed normally → PROCESSED. The "already injected" set is in-memory and // per-process, so it resets on restart and never affects a fresh deploy. // @@ -26,7 +26,7 @@ const alreadyInjected = new Set(); /** * Throws a one-shot retryable LlmError on the first attempt for `knowledgeId` - * when `BYTEBELL_FORCE_HALT_ONCE=1`. No-op otherwise (flag off, or this + * when `PLUMBLINE_FORCE_HALT_ONCE=1`. No-op otherwise (flag off, or this * knowledge already had its forced failure on a prior attempt). */ export function maybeInjectOneShotHalt(knowledgeId: string): void { @@ -39,5 +39,5 @@ export function maybeInjectOneShotHalt(knowledgeId: string): void { } alreadyInjected.add(knowledgeId); logger.warn(`[FAULT-INJECT] ${knowledgeId}: forcing one-shot transient LLM failure (expect HALTED → retry)`); - throw new LlmError("forced transient LLM failure (BYTEBELL_FORCE_HALT_ONCE test hook)", undefined, { status: 503 }); + throw new LlmError("forced transient LLM failure (PLUMBLINE_FORCE_HALT_ONCE test hook)", undefined, { status: 503 }); } diff --git a/packages/ingest-strategies/README.md b/packages/ingest-strategies/README.md index c2469cd..39997c2 100644 --- a/packages/ingest-strategies/README.md +++ b/packages/ingest-strategies/README.md @@ -2,7 +2,7 @@ The public, provider-agnostic ingestion **strategies**. A strategy turns a cloned source tree (exposed through a `SourceReader` from `@bb/ingest-core`) -into Mongo rows + Neo4j nodes. It never knows whether the source came from +into SQLite rows + Neo4j nodes. It never knows whether the source came from GitHub, GitLab, or anywhere else. ## Strategies @@ -19,8 +19,8 @@ GitHub, GitLab, or anywhere else. ## Dependencies Depends only on `@bb/ingest-core` (contracts, shared phases, primitives) plus -infrastructure packages (`@bb/llm`, `@bb/graph-db`, `@bb/mongo`, `@bb/mcp`, …). +infrastructure packages (`@bb/llm`, `@bb/graph-db`, `@bb/db`, `@bb/mcp`, …). It does **not** depend on any provider package (`@bb/ingest-github`, -`@bytebell/ingest-gitlab`). Providers and the composition roots depend on it. +`@plumbline/ingest-gitlab`). Providers and the composition roots depend on it. Tier: **strategy** (sits above core, below the composition roots). diff --git a/packages/ingest-strategies/package.json b/packages/ingest-strategies/package.json index 84444bb..bf5862d 100644 --- a/packages/ingest-strategies/package.json +++ b/packages/ingest-strategies/package.json @@ -24,7 +24,6 @@ "@bb/llm": "workspace:*", "@bb/logger": "workspace:*", "@bb/mcp": "workspace:*", - "@bb/mongo": "workspace:*", "@bb/types": "workspace:*" } } diff --git a/packages/ingest-strategies/src/README.md b/packages/ingest-strategies/src/README.md index 9d5398b..f21d1c6 100644 --- a/packages/ingest-strategies/src/README.md +++ b/packages/ingest-strategies/src/README.md @@ -2,7 +2,7 @@ Source root for the public, provider-agnostic ingestion strategies. A strategy turns a cloned source tree (exposed through a `SourceReader` from -`@bb/ingest-core`) into Mongo rows + Neo4j nodes. Nothing here knows whether the +`@bb/ingest-core`) into SQLite rows + Neo4j nodes. Nothing here knows whether the source came from GitHub, GitLab, or anywhere else — strategies depend only on `@bb/ingest-core` and infrastructure packages. @@ -46,8 +46,8 @@ Both factories accept `{ fileAnalyzer, progressContextFactory }` and return an ## Dependencies `@bb/ingest-core` (contracts, shared phases, primitives) plus infrastructure -packages (`@bb/llm`, `@bb/graph-db`, `@bb/mongo`, `@bb/mcp`, …). **No** provider -package (`@bb/ingest-github`, `@bytebell/ingest-gitlab`) is imported — providers +packages (`@bb/llm`, `@bb/graph-db`, `@bb/db`, `@bb/mcp`, …). **No** provider +package (`@bb/ingest-github`, `@plumbline/ingest-gitlab`) is imported — providers and the composition roots depend on this package, never the reverse. ## Invariants diff --git a/packages/ingest-strategies/src/concept-graph/README.md b/packages/ingest-strategies/src/concept-graph/README.md index b688c92..57fa060 100644 --- a/packages/ingest-strategies/src/concept-graph/README.md +++ b/packages/ingest-strategies/src/concept-graph/README.md @@ -64,10 +64,10 @@ a strategy-neutral `#src/pipeline/phases/` location because: If a third strategy ever needs the same phases, the lift is still a mechanical change at that point. -## Mongo ledger +## Enrichment ledger -ConceptGraphStrategy uses the enrichment ledger functions in -`@bb/mongo` (`startEnrichmentRun`, `markFileEnriched`, +ConceptGraphStrategy uses the enrichment ledger on `@bb/db`'s +`enrichmentDb` facade (`startEnrichmentRun`, `markFileEnriched`, `recordEnrichmentFailure`, `completeEnrichmentRun`, `failEnrichmentRun`) to track per-file resume state across retries. `KnowledgeDoc.state` stays `PROCESSING` until enrichment reports `Completed`; then the @@ -76,7 +76,7 @@ worker transitions to `PROCESSED`. ## Disk artifacts Enrichment outputs land at -`~/.bytebell/repos/{knowledgeId}/{commitId}/enrichment/{file-slug}.json` +`~/.plumbline/repos/{knowledgeId}/{commitId}/enrichment/{file-slug}.json` (one file per enriched file, written when the file's LLM call returns a schema-valid result). Disk is the audit trail; the canonical graph state is Neo4j. diff --git a/packages/ingest-strategies/src/concept-graph/VERIFY.md b/packages/ingest-strategies/src/concept-graph/VERIFY.md index 9cae218..b83dee7 100644 --- a/packages/ingest-strategies/src/concept-graph/VERIFY.md +++ b/packages/ingest-strategies/src/concept-graph/VERIFY.md @@ -1,8 +1,8 @@ # ConceptGraphStrategy — manual end-to-end verification The unit tests in `__tests__/enrichment.test.ts` cover the pure-logic surface -(schema validation, slug derivation). A full strategy run requires Mongo + -Neo4j + Redis + an OpenRouter key, so end-to-end verification is a +(schema validation, slug derivation). A full strategy run requires SQLite + +Neo4j + an OpenRouter key, so end-to-end verification is a documented manual playbook rather than a CI test. Run the playbook below after any change that touches Phase 4 (file store) or @@ -11,8 +11,8 @@ default. ## Prerequisites -- `bytebell-server` running locally with Mongo, Neo4j, and Redis reachable -- An OpenRouter API key configured (`bytebell keys set`) +- `plumbline-server` running locally with Neo4j reachable +- An OpenRouter API key configured (`plumbline keys set`) - A tool-use-capable enrichment model selected (Anthropic Claude Sonnet 4.x / Opus 4.x via OpenRouter — confirmed to support OpenAI-style `tool_calls`) - A small public repo to index (5–50 files is ideal) @@ -20,20 +20,20 @@ default. ## Step 1 — Configure the strategy ```bash -bytebell set ingestion.strategy concept-graph -bytebell set enrichment.model anthropic/claude-sonnet-4 +plumbline set ingestion.strategy concept-graph +plumbline set enrichment.model anthropic/claude-sonnet-4 # Defaults are sensible; override only if you have a reason: -# bytebell set enrichment.max.tool.calls.per.file 15 -# bytebell set enrichment.max.iterations.per.file 8 -# bytebell set enrichment.wall.time.ms.per.file 400000 -# bytebell set enrichment.concurrency 16 +# plumbline set enrichment.max.tool.calls.per.file 15 +# plumbline set enrichment.max.iterations.per.file 8 +# plumbline set enrichment.wall.time.ms.per.file 400000 +# plumbline set enrichment.concurrency 16 ``` Restart the server so the new config is picked up: ```bash -bytebell shutdown -bytebell boot +plumbline shutdown +plumbline boot ``` Check the server log for `ingest-github: active strategy = concept-graph`. @@ -41,8 +41,8 @@ Check the server log for `ingest-github: active strategy = concept-graph`. ## Step 2 — Index a small repo ```bash -bytebell index https://github.com/some/small-repo -bytebell ls # wait until state = PROCESSED +plumbline index https://github.com/some/small-repo +plumbline ls # wait until state = PROCESSED ``` Phase progression in the log should be: @@ -58,7 +58,7 @@ concept-graph: phase5 done — enriched=N runId= ## Step 3 — Inspect Neo4j -Replace `` with the `knowledgeId` from `bytebell ls`. +Replace `` with the `knowledgeId` from `plumbline ls`. ```cypher // Files are written; no :Folder, no :Repo for this knowledge. @@ -95,18 +95,19 @@ Pass criteria: - At least one `:Concept` exists with `kind` in (`role`, `pattern`, `domain`) - Each `:Concept` / `:Contract` / `:Guidepost` carries an `enrichmentRunId` -## Step 4 — Inspect Mongo - -```js -db.knowledge.findOne( - { knowledgeId: "" }, - { - enrichmentRunId: 1, - enrichmentState: 1, - completedFiles: { $slice: 5 }, - enrichmentFailures: 1, - }, -); +## Step 4 — Inspect the document store + +The knowledge document is stored as JSON in the `knowledge` table, so query +the enrichment ledger fields out of it: + +```sh +sqlite3 ~/.plumbline/data.sqlite " + SELECT json_extract(value, '\$.enrichmentRunId'), + json_extract(value, '\$.enrichmentState'), + json_array_length(json_extract(value, '\$.completedFiles')), + json_extract(value, '\$.enrichmentFailures') + FROM knowledge WHERE key = ''; +" ``` Pass criteria: @@ -120,7 +121,7 @@ Pass criteria: Re-index the same repo: ```bash -bytebell index --force # or trigger a pull-style re-run +plumbline index --force # or trigger a pull-style re-run ``` Re-run Step 3's Cypher queries — node counts should be unchanged (concepts / @@ -131,7 +132,7 @@ without duplicate nodes). Force a failure mid-enrichment: -1. Set `bytebell set enrichment.wall.time.ms.per.file 1` (1ms — every call +1. Set `plumbline set enrichment.wall.time.ms.per.file 1` (1ms — every call will exceed wall-time) 2. Re-index the repo 3. Confirm: @@ -140,28 +141,28 @@ Force a failure mid-enrichment: with `reason: "cap-exceeded"` - Knowledge state is NOT `PROCESSED` (stays `PROCESSING` since enrichment threw) -4. Reset: `bytebell set enrichment.wall.time.ms.per.file 400000` +4. Reset: `plumbline set enrichment.wall.time.ms.per.file 400000` 5. Re-index — `completedFiles` from the prior run is cleared on `startEnrichmentRun` and the run proceeds fresh ## Step 7 — Inspect disk artifacts -Under the commit-scoped layout (`bytebell migrate paths` if you're upgrading +Under the commit-scoped layout (`plumbline migrate paths` if you're upgrading from the legacy `repos/.meta/` tree first), every per-commit artifact lives -under `~/.bytebell/orgs//github/////`. For +under `~/.plumbline/orgs//github/////`. For OSS the `` segment is always `local`. The enrichment artifacts sit beside the rest of meta-output: ```bash ORG=local -ls ~/.bytebell/orgs/$ORG/github/////meta-output/enrichment/ +ls ~/.plumbline/orgs/$ORG/github/////meta-output/enrichment/ ``` Each successfully enriched file gets a JSON artifact named after its flattened path. Open one to confirm the audit trail: ```bash -cat ~/.bytebell/orgs/$ORG/github/////meta-output/enrichment/src__auth__controller.ts.json +cat ~/.plumbline/orgs/$ORG/github/////meta-output/enrichment/src__auth__controller.ts.json ``` The artifact carries `enrichment` (the validated LLM output), `llmUsage` diff --git a/packages/ingest-strategies/src/concept-graph/enrichment-artifact.ts b/packages/ingest-strategies/src/concept-graph/enrichment-artifact.ts index 2023647..6c23472 100644 --- a/packages/ingest-strategies/src/concept-graph/enrichment-artifact.ts +++ b/packages/ingest-strategies/src/concept-graph/enrichment-artifact.ts @@ -8,7 +8,7 @@ import type { PerFileEnrichment } from "./enrichment-schema.ts"; // Layout: `/enrichment/.json`. One file per // successfully enriched source file. The graph is the canonical store; disk // is the "why was this concept created" audit trail required by the LLM -// usage rule. Mongo's `KnowledgeDoc.completedFiles[]` is the resume cursor — +// usage rule. `KnowledgeDoc.completedFiles[]` is the resume cursor — // disk artifacts are write-only from the strategy's perspective, written on // success and never re-read by the strategy (a manual diff against graph // state is possible if needed). diff --git a/packages/ingest-strategies/src/concept-graph/mcp-tool-executor.ts b/packages/ingest-strategies/src/concept-graph/mcp-tool-executor.ts index 3759893..5ee0d53 100644 --- a/packages/ingest-strategies/src/concept-graph/mcp-tool-executor.ts +++ b/packages/ingest-strategies/src/concept-graph/mcp-tool-executor.ts @@ -12,7 +12,7 @@ import type { ToolDefinition } from "@bb/llm"; // Bridges the LLM tool-use loop to the same MCP tool runners the public // transport calls. Stays in-process: no HTTP loopback, no SSE, no SDK // transport — just direct function calls to the @bb/mcp runners. Justified -// because workers run inside `bytebell-server`'s process (see CLAUDE.md +// because workers run inside `plumbline-server`'s process (see CLAUDE.md // "Rule of Queue Safety"). The strategy code is the only in-process consumer // of these runners today; if a second one appears, lift this module to a // shared location. diff --git a/packages/ingest-strategies/src/concept-graph/phases/README.md b/packages/ingest-strategies/src/concept-graph/phases/README.md index db6534c..29bd2c8 100644 --- a/packages/ingest-strategies/src/concept-graph/phases/README.md +++ b/packages/ingest-strategies/src/concept-graph/phases/README.md @@ -87,7 +87,7 @@ enrichFiles ## Data ownership -- `enrichFiles` owns the Mongo enrichment-run ledger transitions +- `enrichFiles` owns the enrichment-run ledger transitions (`startEnrichmentRun`, `markFileEnriched`, `recordEnrichmentFailure`, `completeEnrichmentRun`, `failEnrichmentRun`). - `persistEnrichment` owns the Neo4j writes for `:Concept`, @@ -95,7 +95,7 @@ enrichFiles this folder writes to Neo4j. - `enrich-one-file.ts` owns the audit JSON written by `writeEnrichmentArtifact` (one file per enriched source file under - `~/.bytebell/repos/{knowledgeId}/{commitId}/enrichment/`). + `~/.plumbline/repos/{knowledgeId}/{commitId}/enrichment/`). - `EnrichmentRegistry` is in-memory only; it lives for the duration of one `enrichFiles` call and is never persisted. @@ -129,7 +129,7 @@ enrichFiles (`getConfigValue`), `@bb/logger`, `@bb/errors` (`LlmConfigError`, `LlmError`), `@bb/types` (`Config`, `ConceptKind`, `ContractKind`, `GuidepostKind`, `EnrichmentFailure`, `EnrichmentFailureReason`, -`NodeScope`, upsert input shapes, edge-kind unions), `@bb/mongo` +`NodeScope`, upsert input shapes, edge-kind unions), `@bb/db` (enrichment-run ledger), `@bb/graph-db` (`conceptsGraph`, `contractsGraph`, `guidepostsGraph`), sibling modules `enrichment-schema.ts`, `enrichment-artifact.ts`, `mcp-tool-executor.ts`, diff --git a/packages/ingest-strategies/src/concept-graph/phases/enrich-files.ts b/packages/ingest-strategies/src/concept-graph/phases/enrich-files.ts index c369d4d..744e8e3 100644 --- a/packages/ingest-strategies/src/concept-graph/phases/enrich-files.ts +++ b/packages/ingest-strategies/src/concept-graph/phases/enrich-files.ts @@ -5,14 +5,7 @@ import { getConfigValue } from "@bb/config"; import { logger } from "@bb/logger"; import { LlmConfigError, LlmError } from "@bb/errors"; import type { EnrichmentFailure, EnrichmentFailureReason, NodeScope } from "@bb/types"; -import { - startEnrichmentRun, - getCompletedEnrichmentFiles, - markFileEnriched, - recordEnrichmentFailure, - completeEnrichmentRun, - failEnrichmentRun, -} from "@bb/mongo"; +import { enrichmentDb } from "@bb/db"; import { throwIfCancelled, CancellationError } from "@bb/ingest-core"; import { withConcurrency } from "@bb/ingest-core"; import type { MetaPaths } from "@bb/ingest-core"; @@ -31,7 +24,7 @@ import { enrichOneFile, type EnrichOneFileInput } from "#src/concept-graph/phase // LLM emits a strict-Zod-validated JSON object; on success we upsert // :Concept / :Contract / :Guidepost nodes + edges in idempotent batches. // -// Resume protocol: `Mongo.KnowledgeDoc.completedFiles[]` lists files that +// Resume protocol: `KnowledgeDoc.completedFiles[]` lists files that // finished successfully in the current run. We skip those. A fresh // enrichment run (`startEnrichmentRun` with a new UUID) clears the list. // @@ -61,9 +54,9 @@ export async function enrichFiles(input: EnrichFilesInput): Promise"); + throw new LlmConfigError("plumbline set enrichment.model "); } - await startEnrichmentRun(input.scope.knowledgeId, enrichmentRunId); + await enrichmentDb.startEnrichmentRun(input.scope.knowledgeId, enrichmentRunId); const concurrency = getConfigValue(Config.EnrichmentConcurrency); const maxToolCalls = getConfigValue(Config.EnrichmentMaxToolCallsPerFile); @@ -80,24 +73,24 @@ export async function enrichFiles(input: EnrichFilesInput): Promise 0) { - await failEnrichmentRun(input.scope.knowledgeId); + await enrichmentDb.failEnrichmentRun(input.scope.knowledgeId); const head = failedPaths.slice(0, 5).join(", "); const more = failedPaths.length > 5 ? ` (+${failedPaths.length - 5} more)` : ""; throw new LlmError(`concept-graph: ${filesFailed} file(s) failed enrichment: ${head}${more}`); } - await completeEnrichmentRun(input.scope.knowledgeId); + await enrichmentDb.completeEnrichmentRun(input.scope.knowledgeId); return { enrichmentRunId, filesEnriched, filesFailed, tokenUsage: cumulativeUsage }; } diff --git a/packages/ingest-strategies/src/concept-graph/prompts/README.md b/packages/ingest-strategies/src/concept-graph/prompts/README.md index 0dd4bdb..064a372 100644 --- a/packages/ingest-strategies/src/concept-graph/prompts/README.md +++ b/packages/ingest-strategies/src/concept-graph/prompts/README.md @@ -19,7 +19,7 @@ builders — no I/O, no schemas, no LLM calls. ## Invariants -- Prompts are pure functions of typed inputs. No `await`, no Mongo, +- Prompts are pure functions of typed inputs. No `await`, no document store, no Neo4j, no `process.*`. - The output contract these prompts describe MUST match `enrichment-schema.ts` exactly. When one changes, the other diff --git a/packages/ingest-strategies/src/flat-folder/README.md b/packages/ingest-strategies/src/flat-folder/README.md index 0739a33..3718c59 100644 --- a/packages/ingest-strategies/src/flat-folder/README.md +++ b/packages/ingest-strategies/src/flat-folder/README.md @@ -22,10 +22,10 @@ this single pool. One knob bounds total in-flight LLM concurrency. parallel-deduplicated LLM resolution for unknown extensions/filenames through the shared limiter, then drain. 1b. **write-eligible-files** (`eligible-files.ts`) — between scan and the - 2a/2b parallel block, persists `.bytebell/eligible_files.json` (paths + + 2a/2b parallel block, persists `.plumbline/eligible_files.json` (paths + parent folders for every `small`/`big` entry, plus the commit hash) to - the source layer (local disk under `source.localRepoDir/.bytebell/` and/or - the `archiveSink`). Read back by `@bytebell/knowledge-validation` to + the source layer (local disk under `source.localRepoDir/.plumbline/` and/or + the `archiveSink`). Read back by `@plumbline/knowledge-validation` to verify every file the analyzer was asked to process landed in Neo4j. Hard-fails if neither write target is available — an un-validatable knowledge is not a state we want. @@ -113,7 +113,7 @@ The strategy emits progress through the `ProgressContext` port defined in see updated entries without re-reading disk. - `scan-manifest.ts` — `ScanManifest` shape, `readScanManifest`, `writeScanManifest`. The canonical handoff between phase 1 and phases 2a/2b. -- `eligible-files.ts` — `writeEligibleFiles({knowledgeId, manifest, source, archiveSink?})`. Writes `.bytebell/eligible_files.json` to the source layer between phase 1 and 2a/2b. The validation service (`@bytebell/knowledge-validation`) reads this artifact to cross-check post-indexing consistency. +- `eligible-files.ts` — `writeEligibleFiles({knowledgeId, manifest, source, archiveSink?})`. Writes `.plumbline/eligible_files.json` to the source layer between phase 1 and 2a/2b. The validation service (`@plumbline/knowledge-validation`) reads this artifact to cross-check post-indexing consistency. - `folder-path.ts` — `directFolderOf`, `affectedFolderPaths`. - `folder-summary.ts` — group + summarise (individual or batched) + persist - iterate folder summaries; shared `dispatchFolderSummaries` used by both diff --git a/packages/ingest-strategies/src/flat-folder/index.ts b/packages/ingest-strategies/src/flat-folder/index.ts index 212df27..c8f7d24 100644 --- a/packages/ingest-strategies/src/flat-folder/index.ts +++ b/packages/ingest-strategies/src/flat-folder/index.ts @@ -58,7 +58,7 @@ export function createFlatFolderStrategy(deps: FlatFolderStrategyDeps): IngestSt const { manifest } = await scanAndClassify(scanInput); // Persist the canonical eligible-files list BEFORE any small- or - // big-file LLM call runs. Read back by `@bytebell/knowledge-validation` + // big-file LLM call runs. Read back by `@plumbline/knowledge-validation` // to verify every file the analyzer was asked to process landed in // Neo4j. Must be the last step before analysis dispatch — if this // fails, the knowledge is not validatable post-hoc and we'd rather diff --git a/packages/ingest-strategies/src/flat-folder/prompts/README.md b/packages/ingest-strategies/src/flat-folder/prompts/README.md index 4309ef6..7e60861 100644 --- a/packages/ingest-strategies/src/flat-folder/prompts/README.md +++ b/packages/ingest-strategies/src/flat-folder/prompts/README.md @@ -32,6 +32,6 @@ drift apart. ## Invariants -- Prompts are pure functions of typed inputs. No I/O, no LLM calls, no Mongo. +- Prompts are pure functions of typed inputs. No I/O, no LLM calls, no document store. - The field block lives in one file. Any change to the JSON schema starts here. - Prompts never depend on `pipeline/`, `adapters/`, or `handlers/`. diff --git a/packages/ingest-strategies/src/flat-folder/store-pull.ts b/packages/ingest-strategies/src/flat-folder/store-pull.ts index 1cda437..6bd7274 100644 --- a/packages/ingest-strategies/src/flat-folder/store-pull.ts +++ b/packages/ingest-strategies/src/flat-folder/store-pull.ts @@ -39,7 +39,7 @@ export interface StorePullResult { * Pull-time graph store. Mirrors the structure of `storeFlatAnalysis` but * applies only the changes the diff specified: * - * 1. Delete `:File` + Mongo rows for deleted + renamed-from paths. + * 1. Delete `:File` + document-store rows for deleted + renamed-from paths. * 2. Upsert `:File` nodes for added + modified + renamed-to paths. * 3. Upsert affected `:Folder` nodes from the freshly-written folder * summaries on disk. diff --git a/packages/ladybug/package.json b/packages/ladybug/package.json index e11acdf..c7b7704 100644 --- a/packages/ladybug/package.json +++ b/packages/ladybug/package.json @@ -17,6 +17,7 @@ "@bb/graph-core": "workspace:*", "@bb/graph-db": "workspace:*", "@bb/types": "workspace:*", + "@ladybugdb/core": "^0.20.3", "parquetjs": "^0.11.2" }, "devDependencies": { diff --git a/packages/ladybug/src/client.ts b/packages/ladybug/src/client.ts index f035f71..0ffbd77 100644 --- a/packages/ladybug/src/client.ts +++ b/packages/ladybug/src/client.ts @@ -1,4 +1,4 @@ -import { Database, Connection, PreparedStatement, type LbugValue } from "./coreStub.ts"; +import { Database, Connection, PreparedStatement, type LbugValue } from "@ladybugdb/core"; import { getConfigValue } from "@bb/config"; import { Config } from "@bb/types"; diff --git a/packages/ladybug/src/coreStub.ts b/packages/ladybug/src/coreStub.ts deleted file mode 100644 index 8c73251..0000000 --- a/packages/ladybug/src/coreStub.ts +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only WITH non-commercial-clause -// -// Local stand-in for the native `@ladybugdb/core` addon. -// -// The native LadybugDB graph provider is NOT part of the ByteBell deployables -// (admin-server / knowledge-server): those use the Neo4j graph provider plus the -// on-disk JSONL ladybug dumps read by `@bytebell/utils`, and never load this -// native binding. `@ladybugdb/core` ships a large per-platform prebuilt native -// tarball that `bun` cannot extract under QEMU during multi-arch -// `docker buildx` on an Apple-Silicon host — which broke `make ecr-publish` -// ("Fail extracting tarball for @ladybugdb/core-linux-x64"). The external dep -// was therefore removed from `@bb/ladybug`'s manifest. -// -// These stubs preserve exactly the type surface `client.ts` / `provider.ts` -// compile against. Constructing or calling any of them throws — the native -// provider is simply unavailable in this build variant. - -export type LbugValue = unknown; - -const UNAVAILABLE = "LadybugDB native provider (@ladybugdb/core) is not available in this build."; - -export interface QueryResult { - getAll(): Promise; -} - -export class Database { - constructor(_path: string) { - throw new Error(UNAVAILABLE); - } -} - -export class PreparedStatement { - isSuccess(): boolean { - throw new Error(UNAVAILABLE); - } - - getErrorMessage(): string { - throw new Error(UNAVAILABLE); - } -} - -export class Connection { - constructor(_db: Database) { - throw new Error(UNAVAILABLE); - } - - query(_query: string): Promise { - throw new Error(UNAVAILABLE); - } - - prepare(_query: string): Promise { - throw new Error(UNAVAILABLE); - } - - execute(_prepared: PreparedStatement, _params: Record): Promise { - throw new Error(UNAVAILABLE); - } -} diff --git a/packages/ladybug/src/provider.ts b/packages/ladybug/src/provider.ts index df72667..afd359e 100644 --- a/packages/ladybug/src/provider.ts +++ b/packages/ladybug/src/provider.ts @@ -14,7 +14,7 @@ import * as guidepostsRepo from "./guideposts.ts"; import { registerGraphProvider } from "@bb/graph-db"; import type { IGraphDatabaseProvider } from "@bb/graph-core"; -import type { LbugValue } from "./coreStub.ts"; +import type { LbugValue } from "@ladybugdb/core"; class LadybugGraphProvider implements IGraphDatabaseProvider { knowledge = { diff --git a/packages/llm/README.md b/packages/llm/README.md index b1b93ae..0a71c66 100644 --- a/packages/llm/README.md +++ b/packages/llm/README.md @@ -84,8 +84,8 @@ constructs its own `fetch` request. `askLLM` consults a filesystem-backed cache before issuing a request. Implemented in `src/cache.ts`: -- **Location**: `~/.bytebell/repos/llmdecisions/.json` (one - file per cache key). Resolved via `@bb/config`'s `getBytebellHome()`. +- **Location**: `~/.plumbline/repos/llmdecisions/.json` (one + file per cache key). Resolved via `@bb/config`'s `getPlumblineHome()`. - **Key**: `sha256(JSON.stringify({ provider, prompt, systemPrompt, modelChain }))` where `provider` is `"openrouter"` or `"ollama"` and `modelChain` is the resolved chain (capped-at-3 for OpenRouter, single-element for @@ -106,18 +106,18 @@ Implemented in `src/cache.ts`: logged with `[LLM CACHE WRITE FAILED]` and the LLM call proceeds unaffected. - **Kill switch**: `Config.LlmCacheEnabled` (boolean, default `true`). - Toggle via `bytebell set llm_cache_enabled `. When + Toggle via `plumbline set llm_cache_enabled `. When `false`, both reads and writes are skipped. - **TTL / eviction**: none in v0. Manual prune is `rm` on the entry - file. A future `bytebell cache prune` lands alongside cost-ledger + file. A future `plumbline cache prune` lands alongside cost-ledger work. ## Data ownership `@bb/llm` owns the decision-cache directory at -`~/.bytebell/repos/llmdecisions/`. No other package may read or write +`~/.plumbline/repos/llmdecisions/`. No other package may read or write it. The cost ledger described in [docs/arch.md](../../docs/arch.md) is -**not** owned by v0 — it lands when `bytebell cost` ships. +**not** owned by v0 — it lands when `plumbline cost` ships. ## Invariants @@ -146,7 +146,7 @@ it. The cost ledger described in [docs/arch.md](../../docs/arch.md) is four slots ship with curated defaults so a fresh install gets fallback without any user action. OpenRouter tries the chain in order and bills only the responder; `usage.model` reflects which one. Caller still - sees a single `AskLlmResult`. BullMQ's `attempts: 3` wraps the whole + sees a single `AskLlmResult`. the queue's `attempts: 3` wraps the whole call — retries walk the chain again, useful when a transient OpenRouter outage clears between retries. 4a. **No upstream-provider fallback.** Every request carries @@ -159,7 +159,7 @@ it. The cost ledger described in [docs/arch.md](../../docs/arch.md) is actionable (specific provider, specific status) instead of a generic timeout. 5. **Errors are typed, not strings.** `LlmConfigError` carries the exact - `bytebell keys set` hint; `LlmError` carries `cause`. + `plumbline keys set` hint; `LlmError` carries `cause`. 6. **Timeout is enforced.** AbortController fires at `timeoutMs`; the resulting `AbortError` is wrapped in `LlmError` with the timeout in the message. @@ -178,7 +178,7 @@ it. The cost ledger described in [docs/arch.md](../../docs/arch.md) is ## What is intentionally out of scope (v0) -- Cost ledger (`~/.bytebell/cost-ledger.sqlite`) — lands with `bytebell cost` +- Cost ledger (`~/.plumbline/cost-ledger.sqlite`) — lands with `plumbline cost` - Streaming responses - Tool / function calling - A `askJsonLLM(prompt, schema)` JSON-mode wrapper — caller does @@ -201,10 +201,10 @@ needs strict JSON: 2. Re-export from `src/index.ts`. 3. Update _Public exports_ here. -Adding a cost ledger when `bytebell cost` lands: +Adding a cost ledger when `plumbline cost` lands: 1. New file `src/ledger.ts` writing to - `~/.bytebell/cost-ledger.sqlite` via `bun:sqlite`. + `~/.plumbline/cost-ledger.sqlite` via `bun:sqlite`. 2. Wrap `askLLM` to capture `model`, prompt-token count, completion-token count, latency. Lookup pricing via a curated table. 3. Update _Out of scope_ → _Public exports_ here. diff --git a/packages/llm/src/README.md b/packages/llm/src/README.md index 5cea49a..cc0cad0 100644 --- a/packages/llm/src/README.md +++ b/packages/llm/src/README.md @@ -92,7 +92,7 @@ pricing). `clearTimeout` call lives in a `finally` so the timer is always cleared regardless of fetch outcome. - **Errors carry typed metadata.** `LlmConfigError` carries the - `bytebell keys set` hint; `LlmError` accepts an optional `cause` and + `plumbline keys set` hint; `LlmError` accepts an optional `cause` and composes a single-line message capped at 500 chars of any HTTP error body (so the logger doesn't blow up on multi-MB error responses). - **No env reads.** Secrets come from `opts.apiKey` first, then diff --git a/packages/llm/src/cache.ts b/packages/llm/src/cache.ts index 1123aad..40ecddf 100644 --- a/packages/llm/src/cache.ts +++ b/packages/llm/src/cache.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import { getBytebellHome, getConfigValue } from "@bb/config"; +import { getPlumblineHome, getConfigValue } from "@bb/config"; import { Config } from "@bb/types"; import type { AskLlmUsage } from "./client.ts"; @@ -45,7 +45,7 @@ export function computeCacheKey(input: CacheKeyInput): string { } function cacheDir(): string { - return path.join(getBytebellHome(), CACHE_DIR_NAME); + return path.join(getPlumblineHome(), CACHE_DIR_NAME); } function entryPath(key: string): string { diff --git a/packages/llm/src/ollama.ts b/packages/llm/src/ollama.ts index 8cef2ad..9bf14bb 100644 --- a/packages/llm/src/ollama.ts +++ b/packages/llm/src/ollama.ts @@ -31,11 +31,11 @@ function joinUrl(base: string, path: string): string { export function resolveOllamaChain(opts: AskLlmOptions): string[] { const url = getConfigValue(Config.OllamaUrl); if (url.length === 0) { - throw new LlmConfigError("bytebell set ollama-url "); + throw new LlmConfigError("plumbline set ollama-url "); } const model = opts.model ?? getConfigValue(Config.OllamaModel); if (model.length === 0) { - throw new LlmConfigError("bytebell set ollama-model "); + throw new LlmConfigError("plumbline set ollama-model "); } return [model]; } diff --git a/packages/llm/src/openrouter.ts b/packages/llm/src/openrouter.ts index c1c54c9..ac88dd8 100644 --- a/packages/llm/src/openrouter.ts +++ b/packages/llm/src/openrouter.ts @@ -7,7 +7,7 @@ import { openRouterRawChat, type OpenRouterMessageInput } from "./openrouterChat export function resolveOpenRouterChain(opts: AskLlmOptions): string[] { const apiKey = opts.apiKey ?? getConfigValue(Config.OpenrouterApiKey); if (apiKey.length === 0) { - throw new LlmConfigError("bytebell keys set"); + throw new LlmConfigError("plumbline keys set"); } const model = opts.model ?? getConfigValue(Config.OpenrouterModel); const fallbackSlots = opts.fallbackModels ?? [ diff --git a/packages/logger/README.md b/packages/logger/README.md index 113af4e..9a4eed6 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -10,7 +10,7 @@ Domain, Binaries). Single logging surface for the workspace. Two sinks: -- **File** — daily-rotated `~/.bytebell/logs/-YYYY-MM-DD.log`, gzipped +- **File** — daily-rotated `~/.plumbline/logs/-YYYY-MM-DD.log`, gzipped on rotation, retained for `log_retention_days`. - **Console** — always on; verbosity from `log_level`. Colorized when stdout is a TTY. @@ -57,7 +57,7 @@ handled gracefully. ## File layout -- `src/dirs.ts` — log dir resolution (under `getBytebellHome()/logs`) +- `src/dirs.ts` — log dir resolution (under `getPlumblineHome()/logs`) - `src/caller.ts` — stack-walk `file:line` helper - `src/formats.ts` — sugar splat format + caller format + printf - `src/transports.ts` — daily-rotate file + console transport factories @@ -76,9 +76,9 @@ handled gracefully. ## Data ownership -- `~/.bytebell/logs/` directory creation (mode `0700`) -- `~/.bytebell/logs/-*.log` rotated files (mode `0600`) -- `~/.bytebell/logs/-*.log.gz` compressed rotated files +- `~/.plumbline/logs/` directory creation (mode `0700`) +- `~/.plumbline/logs/-*.log` rotated files (mode `0600`) +- `~/.plumbline/logs/-*.log.gz` compressed rotated files ## What is intentionally out of scope diff --git a/packages/logger/src/README.md b/packages/logger/src/README.md index d9a9ff8..6f9f46e 100644 --- a/packages/logger/src/README.md +++ b/packages/logger/src/README.md @@ -7,7 +7,7 @@ package-level contract. - **[index.ts](index.ts)** — public re-exports. - **[dirs.ts](dirs.ts)** — `getLogsDir`, `ensureLogsDir`. Wraps - `@bb/config.getBytebellHome()` with a `logs/` suffix; creates the dir at + `@bb/config.getPlumblineHome()` with a `logs/` suffix; creates the dir at mode `0700`. - **[caller.ts](caller.ts)** — `getCallerInfo()` and `toProjectRelative()`. Walks `Error.captureStackTrace` to find the first frame outside winston, diff --git a/packages/logger/src/dirs.ts b/packages/logger/src/dirs.ts index 8dfcab9..4ae3376 100644 --- a/packages/logger/src/dirs.ts +++ b/packages/logger/src/dirs.ts @@ -1,22 +1,22 @@ import fs from "node:fs"; import path from "node:path"; -import { getBytebellHome, isDevMode } from "@bb/config"; +import { getPlumblineHome, isDevMode } from "@bb/config"; const LOGS_DIR_NAME = "logs"; const DIR_MODE = 0o700; /** * Resolves the directory log files are written to. In dev mode - * (`BYTEBELL_DEV=1`) this is `/logs/`, so contributors can tail logs + * (`PLUMBLINE_DEV=1`) this is `/logs/`, so contributors can tail logs * from the project they're working in. Otherwise the canonical - * `~/.bytebell/logs/` is used. The CLI's server-spawn redirect honors the + * `~/.plumbline/logs/` is used. The CLI's server-spawn redirect honors the * same toggle, so both Winston output and bun stdout/stderr land together. */ export function getLogsDir(): string { if (isDevMode()) { return path.join(process.cwd(), LOGS_DIR_NAME); } - return path.join(getBytebellHome(), LOGS_DIR_NAME); + return path.join(getPlumblineHome(), LOGS_DIR_NAME); } export function ensureLogsDir(): void { diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 94babdf..272dac5 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -4,7 +4,7 @@ Domain. Imports `@bb/graph-db` (the `searchGraph` facade) and `@bb/graph-core` (read-side types like `ScoredHit`, -`KnowledgeListRow`), `@bb/config` (`getBytebellHome`), `@bb/types` for +`KnowledgeListRow`), `@bb/config` (`getPlumblineHome`), `@bb/types` for shared shapes, and `zod` + `@modelcontextprotocol/sdk`. Does not import from sibling domain packages, from binaries (`@bb/server`, `@bb/cli`), or from any concrete graph provider (`@bb/neo4j` / `@bb/ladybug`) — @@ -21,7 +21,7 @@ tools and a skill-distribution resource channel. The package owns: - A single shared `McpServer` instance (lazy, idempotent build) named - `bytebell-public`, version pulled from this `package.json`. + `plumbline-public`, version pulled from this `package.json`. - Per-session `StreamableHTTPServerTransport` instances keyed by `mcp-session-id`. New sessions are created on initialize requests; existing sessions are looked up by header. @@ -32,8 +32,8 @@ The package owns: `server.registerTool(...)` config-object API. `list_knowledge` is registered first so it sits at the top of `tools/list` output and the LLM gravitates toward calling it before anything else. -- Two resources — `bytebell://skills/index` (JSON listing of bundled - skills) and `bytebell://skills/{skillName}/{filename}` (individual +- Two resources — `plumbline://skills/index` (JSON listing of bundled + skills) and `plumbline://skills/{skillName}/{filename}` (individual markdown file content). Backed by the bundled `skills/` directory beside `package.json`. - Graceful shutdown — `closeAllMcpSessions()` closes every active @@ -44,7 +44,7 @@ The package does **not** own: - Auth gating. MCP is unauthenticated in the OSS engine — single-tenant, localhost-only. See [docs/mcp.md](../../docs/mcp.md) "Transport and mounting". -- Mongo or LLM access. The retrieval tools are pure +- Document-store or LLM access. The retrieval tools are pure graph-and-disk reads. - HTTP body parsing — relies on `@bb/server`'s top-level `express.json`. @@ -130,7 +130,7 @@ imports[], keywords[], language, sizeBytes}` from the active provider. `search` / `contextLines` / `maxTokens`. Resolves the active commit's clone via `repoFs.ts` (one `KnowledgeDoc` lookup per call to derive `(orgId, owner, repo, commitId)`), reads from - `~/.bytebell/orgs//github/////repository/{relativePath}`, + `~/.plumbline/orgs//github/////repository/{relativePath}`, slices in process, prepends line numbers, trims to the token char budget. Local knowledges read straight from `source.sourcePath`. - `bulk_search` — `paths[]` (≤ 50) + required `search` + optional @@ -139,17 +139,17 @@ imports[], keywords[], language, sizeBytes}` from the active provider. ## Resources -`bytebell://skills/index` — JSON listing of bundled skills, generated +`plumbline://skills/index` — JSON listing of bundled skills, generated on each request from disk. Each entry: `{name, description (parsed from SKILL.md frontmatter), install_path, files: [{filename, bytes}]}`. -`bytebell://skills/{skillName}/{filename}` — markdown content for a +`plumbline://skills/{skillName}/{filename}` — markdown content for a single skill file. URI templating handled by the SDK. The bundled directory is `/skills/` (relative to `package.json`). Resolution uses `import.meta.url` so the layout works in dev (`bun run`) -and from a built output. `skills/bytebell/SKILL.md` and -`skills/bytebell/bytebell-code-search.md` are the two files shipped in v1. +and from a built output. `skills/plumbline/SKILL.md` and +`skills/plumbline/plumbline-code-search.md` are the two files shipped in v1. ## Data ownership @@ -159,7 +159,7 @@ and from a built output. `skills/bytebell/SKILL.md` and - Read-only access to the graph through `@bb/graph-db`'s `searchGraph` facade (which proxies to the active `IGraphSearchRepository`) and to the local clone directory through `@bb/config`'s - `getBytebellHome()`. No writes. + `getPlumblineHome()`. No writes. ## Invariants @@ -175,7 +175,7 @@ and from a built output. `skills/bytebell/SKILL.md` and `closeAllMcpSessions` drains both maps. 5. **Disk I/O is path-traversal safe.** `repoFs.ts` rejects absolute paths, `..` components, and any resolved target outside - `/repos/{knowledgeId}/`. + `/repos/{knowledgeId}/`. 6. **No non-null assertions, no `any`, no dynamic `import()`.** Repo-wide strict-types rules apply — see CLAUDE.md. 7. **Tool input types use `field?: T | undefined`.** The Zod-inferred @@ -190,7 +190,7 @@ and from a built output. `skills/bytebell/SKILL.md` and - `@types/express` (dev) — types only; no express runtime dep - `@bb/graph-db` (workspace) — `searchGraph` facade for all read queries - `@bb/graph-core` (workspace) — read-side row/input types -- `@bb/config` (workspace) — `getBytebellHome` for the clone directory +- `@bb/config` (workspace) — `getPlumblineHome` for the clone directory - `@bb/types` (workspace) — shared shapes (no direct usage in v1, kept for upcoming tier integrations) diff --git a/packages/mcp/skills/README.md b/packages/mcp/skills/README.md index 686bbca..3838be6 100644 --- a/packages/mcp/skills/README.md +++ b/packages/mcp/skills/README.md @@ -3,7 +3,7 @@ ## Purpose Bundled skill files served over the MCP resources channel -(`bytebell://skills/...`). This is data, not code: every file under +(`plumbline://skills/...`). This is data, not code: every file under `skills//` is read verbatim from disk by [`../src/resourcesSkills.ts`](../src/resourcesSkills.ts) and streamed to the MCP client, which writes it to `~/.claude/skills//` @@ -24,7 +24,7 @@ no spaces. ## What lives here in v1 -- [`bytebell/`](bytebell) — the single skill shipped with the OSS +- [`plumbline/`](plumbline) — the single skill shipped with the OSS engine. Covers the three retrieval tools and the default code-search workflow. @@ -34,8 +34,8 @@ The package's `resourcesSkills.ts` resolves this directory via `import.meta.url` (so dev `bun run` and built outputs both find it), rebuilds the index from disk on each request, and exposes: -- `bytebell://skills/index` — JSON listing -- `bytebell://skills/{skillName}/{filename}` — markdown content +- `plumbline://skills/index` — JSON listing +- `plumbline://skills/{skillName}/{filename}` — markdown content Edits to bundled skill files take effect on the next resource read; no server restart required. @@ -55,6 +55,6 @@ server restart required. (`name`, `description`, `user-invocable`, `argument-hint`). 2. Add per-task files (`.md`) referenced from `SKILL.md`. 3. Add a `README.md` to the new `skills//` directory - following the existing `bytebell/README.md` template. + following the existing `plumbline/README.md` template. 4. Restart the MCP server is **not** required — the index rebuilds from disk per request. diff --git a/packages/mcp/skills/bytebell/README.md b/packages/mcp/skills/plumbline/README.md similarity index 76% rename from packages/mcp/skills/bytebell/README.md rename to packages/mcp/skills/plumbline/README.md index 8539f3f..c070970 100644 --- a/packages/mcp/skills/bytebell/README.md +++ b/packages/mcp/skills/plumbline/README.md @@ -1,21 +1,21 @@ -# `@bb/mcp/skills/bytebell` — context +# `@bb/mcp/skills/plumbline` — context ## Purpose -The single skill bundled with `bytebell-public`. Teaches an MCP-capable +The single skill bundled with `plumbline-public`. Teaches an MCP-capable LLM client how to use the three retrieval tools (`smart_search`, `keyword_lookup`, `retrieve_file`) against the public knowledge graph. ## Files - **[SKILL.md](SKILL.md)** — entry document. YAML frontmatter sets - `name: bytebell`, `description`, `user-invocable: true`, + `name: plumbline`, `description`, `user-invocable: true`, `argument-hint`. Body is a one-paragraph description of the public graph, a tool quick-reference, and the always-on guardrails distilled from the private repo's empirical analysis (UUID-not-name, `metadata` before `content`, max 3 consecutive `retrieve_file` calls, prefer `bulk_search` over loops, no cross-repo cap). -- **[bytebell-code-search.md](bytebell-code-search.md)** — the +- **[plumbline-code-search.md](plumbline-code-search.md)** — the default code-search workflow. Walks the LLM through the loop: `smart_search → keyword_lookup → retrieve_file metadata → retrieve_file content`. Includes common patterns (where-is-X-defined, @@ -25,8 +25,8 @@ retrieve_file content`. Includes common patterns (where-is-X-defined, These files are read from disk by [`../../src/resourcesSkills.ts`](../../src/resourcesSkills.ts) on every -`bytebell://skills/index` and `bytebell://skills/bytebell/` -fetch. The MCP client writes them to `~/.claude/skills/bytebell/` during +`plumbline://skills/index` and `plumbline://skills/plumbline/` +fetch. The MCP client writes them to `~/.claude/skills/plumbline/` during session bootstrap. ## Editing @@ -34,10 +34,10 @@ session bootstrap. - Keep each file ≤ 300 lines (CLAUDE.md _Rule of File Size_ applies to docs too). - Update both files together when behaviour changes — `SKILL.md` is - the table of contents and guardrails; `bytebell-code-search.md` is + the table of contents and guardrails; `plumbline-code-search.md` is the per-task workflow that links back to the guardrails. - The `description` field in `SKILL.md` frontmatter is what the LLM - sees as the skill's one-line summary in `bytebell://skills/index` — + sees as the skill's one-line summary in `plumbline://skills/index` — keep it punchy. ## What deliberately is NOT here diff --git a/packages/mcp/skills/bytebell/SKILL.md b/packages/mcp/skills/plumbline/SKILL.md similarity index 91% rename from packages/mcp/skills/bytebell/SKILL.md rename to packages/mcp/skills/plumbline/SKILL.md index 75fdbe1..c4ea151 100644 --- a/packages/mcp/skills/bytebell/SKILL.md +++ b/packages/mcp/skills/plumbline/SKILL.md @@ -1,15 +1,15 @@ --- -name: bytebell +name: plumbline description: > - Bytebell-public local knowledge graph. Discover, search, lookup, and - retrieve from code repositories indexed locally by bytebell-server. + Plumbline-public local knowledge graph. Discover, search, lookup, and + retrieve from code repositories indexed locally by plumbline-server. TRIGGER when using: list_knowledge, smart_search, keyword_lookup, retrieve_file. user-invocable: true argument-hint: "[search query or task description]" --- -# Bytebell-public knowledge graph +# Plumbline-public knowledge graph Single-tenant local engine. The graph is `Knowledge → File → (Keyword | Class | Function | Module)` — flat, no commit versioning, no multi-tenant scoping. Every `Knowledge` node carries a `repoName` (e.g. `anthropics/claude-code`) and a `knowledgeId` (UUID). @@ -34,7 +34,7 @@ Single-tenant local engine. The graph is `Knowledge → File → (Keyword | Clas ## Per-task workflows -For code search, file location, and reading: read `bytebell-code-search.md`. +For code search, file location, and reading: read `plumbline-code-search.md`. ## What this server does NOT have diff --git a/packages/mcp/skills/bytebell/bytebell-code-search.md b/packages/mcp/skills/plumbline/bytebell-code-search.md similarity index 98% rename from packages/mcp/skills/bytebell/bytebell-code-search.md rename to packages/mcp/skills/plumbline/bytebell-code-search.md index fe3e2ac..51d600c 100644 --- a/packages/mcp/skills/bytebell/bytebell-code-search.md +++ b/packages/mcp/skills/plumbline/bytebell-code-search.md @@ -1,6 +1,6 @@ # Code search workflow -Use this workflow whenever the user asks you to find, explain, or trace code in a repository indexed by bytebell-server. +Use this workflow whenever the user asks you to find, explain, or trace code in a repository indexed by plumbline-server. ## The default loop diff --git a/packages/mcp/src/README.md b/packages/mcp/src/README.md index 1b2834d..f72cfdf 100644 --- a/packages/mcp/src/README.md +++ b/packages/mcp/src/README.md @@ -100,7 +100,7 @@ state, fileCount, createdAt, updatedAt}` ordered by `matchOnly: true` skips the context-line rendering. - **[repoFs.ts](repoFs.ts)** — local-clone resolution helpers. `resolveCloneDir(knowledgeId)` returns - `/repos/{knowledgeId}`. `resolveFilePath` rejects + `/repos/{knowledgeId}`. `resolveFilePath` rejects absolute paths, `..` components, and any resolved target outside the clone root — a single anti-traversal guard reused by every disk- reading helper. `readFileLines` returns the splitted lines; @@ -109,8 +109,8 @@ state, fileCount, createdAt, updatedAt}` ordered by ### Resources - **[resourcesSkills.ts](resourcesSkills.ts)** — registers - `bytebell://skills/index` and the - `bytebell://skills/{skillName}/{filename}` template. The bundled + `plumbline://skills/index` and the + `plumbline://skills/{skillName}/{filename}` template. The bundled `/skills/` directory is located via `import.meta.url`. `readSkillsIndex` rebuilds the index from disk on each request so edits to bundled skill files take effect without a server restart. @@ -150,7 +150,7 @@ retrieveFileTool.ts → zod, @modelcontextprotocol/sdk/server/mcp.js, retrieveFileMetadata.ts → @bb/neo4j (runCypher) retrieveFileContent.ts → repoFs retrieveFileBulk.ts → repoFs -repoFs.ts → node:fs/promises, node:path, @bb/config (getBytebellHome) +repoFs.ts → node:fs/promises, node:path, @bb/config (getPlumblineHome) resourcesSkills.ts → node:fs, node:path, node:url, @modelcontextprotocol/sdk/server/mcp.js diff --git a/packages/mcp/src/repoFs.ts b/packages/mcp/src/repoFs.ts index 48b39ec..20bd5f7 100644 --- a/packages/mcp/src/repoFs.ts +++ b/packages/mcp/src/repoFs.ts @@ -1,6 +1,6 @@ import path from "node:path"; import { readFile } from "node:fs/promises"; -import { getBytebellHome, getConfigValue } from "@bb/config"; +import { getPlumblineHome, getConfigValue } from "@bb/config"; import { knowledgeDb } from "@bb/db"; import { Config, parseGithubOwnerRepo, repositoryDirFor, type RepoLocation } from "@bb/types"; import { IngestError, KnowledgeNotFoundError } from "@bb/errors"; @@ -8,9 +8,9 @@ import { IngestError, KnowledgeNotFoundError } from "@bb/errors"; // ───────────────────────────────────────────────────────────────────────────── // MCP file resolution. Under the commit-scoped layout, the cloned source tree // for a knowledge lives under -// `~/.bytebell/orgs//github/////repository/`, +// `~/.plumbline/orgs//github/////repository/`, // so resolving the clone dir for a `knowledgeId` is no longer a pure-string -// operation — it needs a Mongo lookup to find the active commit and the +// operation — it needs a document-store lookup to find the active commit and the // repo coordinates. We do one `KnowledgeDoc` read per `retrieve_file` call. // // For local knowledges (`source.kind === "local"`) we point straight at @@ -34,7 +34,7 @@ export class FileReadError extends Error { /** * Resolves the active clone directory for `knowledgeId`. Reads - * `KnowledgeDoc.source` from Mongo to find the active commit; for github + * `KnowledgeDoc.source` from the document store to find the active commit; for github * sources, parses `info.repoUrl` to get owner/repo; for local sources, * returns `source.sourcePath` unchanged. */ @@ -71,7 +71,7 @@ export async function resolveCloneDir(knowledgeId: string): Promise { branch: kDoc.info.branch ?? "main", commitHash: commitId, }; - return repositoryDirFor(getBytebellHome(), loc); + return repositoryDirFor(getPlumblineHome(), loc); } export async function resolveFilePath(knowledgeId: string, relativePath: string): Promise { diff --git a/packages/mcp/src/resourcesSkills.ts b/packages/mcp/src/resourcesSkills.ts index 8c6bea4..88e5404 100644 --- a/packages/mcp/src/resourcesSkills.ts +++ b/packages/mcp/src/resourcesSkills.ts @@ -3,8 +3,8 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { ResourceTemplate, type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -const SKILLS_INDEX_URI = "bytebell://skills/index"; -const SKILL_FILE_URI_TEMPLATE = "bytebell://skills/{skillName}/{filename}"; +const SKILLS_INDEX_URI = "plumbline://skills/index"; +const SKILL_FILE_URI_TEMPLATE = "plumbline://skills/{skillName}/{filename}"; interface SkillFile { filename: string; @@ -32,12 +32,12 @@ export function registerSkillResources(server: McpServer): void { } server.registerResource( - "bytebell-skills-index", + "plumbline-skills-index", SKILLS_INDEX_URI, { - title: "Bytebell Skills Index", + title: "Plumbline Skills Index", description: - "Lists every skill bundled with this server. Each entry includes name, description, target install path, and constituent files. Fetch each file via bytebell://skills/{name}/{filename} and write it to {install_path}/{filename}.", + "Lists every skill bundled with this server. Each entry includes name, description, target install path, and constituent files. Fetch each file via plumbline://skills/{name}/{filename} and write it to {install_path}/{filename}.", mimeType: "application/json", }, async (uri) => { @@ -55,7 +55,7 @@ export function registerSkillResources(server: McpServer): void { for (const skill of index.skills) { for (const file of skill.files) { resources.push({ - uri: `bytebell://skills/${skill.name}/${file.filename}`, + uri: `plumbline://skills/${skill.name}/${file.filename}`, name: `${skill.name}/${file.filename}`, mimeType: "text/markdown", description: `${skill.name} skill — ${file.filename} (${file.bytes} bytes)`, @@ -67,12 +67,12 @@ export function registerSkillResources(server: McpServer): void { }); server.registerResource( - "bytebell-skill-file", + "plumbline-skill-file", template, { - title: "Bytebell Skill File", + title: "Plumbline Skill File", description: - "Markdown content for a single bundled skill file. URI pattern: bytebell://skills/{skillName}/{filename}. Write to ~/.claude/skills/{skillName}/{filename} during bootstrap.", + "Markdown content for a single bundled skill file. URI pattern: plumbline://skills/{skillName}/{filename}. Write to ~/.claude/skills/{skillName}/{filename} during bootstrap.", mimeType: "text/markdown", }, async (uri, variables) => { diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 011f1ff..18ecbb6 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -5,18 +5,18 @@ import { registerKeywordLookupTool } from "./keywordLookupTool.ts"; import { registerRetrieveFileTool } from "./retrieveFileTool.ts"; import { registerSkillResources } from "./resourcesSkills.ts"; -const SERVER_NAME = "bytebell-public"; +const SERVER_NAME = "plumbline-public"; const SERVER_VERSION = "0.0.0"; -const INSTRUCTIONS = `Bytebell-public local knowledge graph. +const INSTRUCTIONS = `Plumbline-public local knowledge graph. Four tools are registered: list_knowledge (call first — enumerates indexed repos and their knowledgeId UUIDs), smart_search (default — fused six-channel search), keyword_lookup (reverse lookup of named entities), and retrieve_file (metadata, content, bulk_search). -Two resources are exposed: bytebell://skills/index and -bytebell://skills/{skillName}/{filename}. Fetch the index once per +Two resources are exposed: plumbline://skills/index and +plumbline://skills/{skillName}/{filename}. Fetch the index once per session, install the listed files to ~/.claude/skills/{skillName}, and follow the per-task workflow files referenced from each SKILL.md.`; diff --git a/packages/mongo/README.md b/packages/mongo/README.md deleted file mode 100644 index d83fdb2..0000000 --- a/packages/mongo/README.md +++ /dev/null @@ -1,182 +0,0 @@ -# `@bb/mongo` — context - -## Tier - -Infrastructure. Depends on Kernel (`@bb/types` for `Config` and -`KnowledgeState`, `@bb/errors` for typed error classes) and on infra -siblings explicitly listed in `package.json` (`@bb/config` for -`Config.MongoUri`). May be imported by Strategy (`@bb/queue`), Domain, -and Binaries — never by `@bb/cli` (CLI talks HTTP only). - -## Responsibility - -The package owns: - -- A single shared `MongoClient` (lazy, idempotent connect; graceful close) -- A health probe (`pingMongo`) backed by the active connection -- An internal `_getDb()` accessor that typed collection helpers in this - package compose against -- Domain CRUD helpers: - - `setKnowledgeState` — the knowledge-document state mutator. Called - by `@bb/queue` publishers on enqueue. - - Failure lifecycle (`knowledge-failure.ts`): `markKnowledgeFailed` - (terminal), `markKnowledgeHalted` (transient, auto-retry pending), - `markKnowledgeCorrupted` (terminal `CORRUPTED` — source repo - gone/inaccessible, dropped from the auto-pull sweep), and - `promoteHaltedToFailed`. All stamp the structured `failure` subdoc. - - `upsertKnowledge` / `listKnowledge` — knowledge-doc upsert and list - (with file count joined from `raw`). Used by the github / local - index routes and by `@bb/cli`'s `ls` and `delete` flows. - - `deleteKnowledge` — hard delete: removes the `knowledge` doc and - every `raw` row tagged with that `knowledgeId`. Called by the - server's `DELETE /api/v1/repos/:knowledgeId` route. - - `upsertRawFile` — per-file Raw doc writer (compound key - `{ knowledgeId, relativePath }`). Called by `@bb/ingest-github`'s - worker for every scanned file. - - `setKnowledgeCommit(knowledgeId, commitHash, inputTokens, outputTokens, costUsd)` - — appends `{ hash, inputTokens, outputTokens, costUsd }` to - `source.commitHashes[]` and sets `source.commitId`. `costUsd` is the - OpenRouter-reported USD cost (`response.usage.cost`) summed across - the pipeline phases for this commit — never computed client-side. - - `aggregateStats` — read every `knowledge` doc and assemble the - kube-shaped `StatsResponse` (totals, repos, commitStats) by summing - `source.commitHashes[].{inputTokens,outputTokens,costUsd}`. Called by - the server's `GET /api/v1/stats` route. The deprecated - `processing_stats` collection is no longer queried. -- A central registry of collection name strings (`Collections` enum): - `knowledge`, `raw`, `mcp_usage`, `mcp_activity`. The - `processing_stats` collection has been removed — per-commit token + - cost data lives on the knowledge document's `source.commitHashes[]` - instead. -- Enrichment ledger for ConceptGraphStrategy (`enrichment.ts`): - `startEnrichmentRun(knowledgeId, runId)`, - `markFileEnriched(knowledgeId, path)`, - `recordEnrichmentFailure(knowledgeId, failure)`, - `completeEnrichmentRun(knowledgeId)`, - `failEnrichmentRun(knowledgeId)`. State lives on the existing - `KnowledgeDoc` (`enrichmentRunId`, `enrichmentState`, - `completedFiles[]`, `enrichmentFailures[]`) — no new collection. - `KnowledgeDoc.status.state` itself stays `PROCESSING` until enrichment - reports `Completed`; this ledger tracks the per-file resume state so - retries skip already-enriched files. - -The package does **not** own: - -- Knowledge-document creation, full reads, or any mutation other than the - state field (deferred — see _How to extend_) -- Document schemas (live in `@bb/types`) -- Index management (deferred) -- Neo4j / graph queries (`@bb/graph`) -- Telemetry, logging, retry policies (the driver handles transport retries) - -## Public exports - -```ts -function connectMongo(): Promise; -function closeMongo(): Promise; -function pingMongo(): Promise; - -function setKnowledgeState(knowledgeId: string, state: KnowledgeState): Promise; -function upsertRawFile(doc: Omit): Promise; - -interface PingResult { - ok: boolean; - latencyMs: number; -} -interface FileAnalysis { - purpose: string; - summary: string; - businessContext: string; - classes: string[]; - functions: string[]; - importsInternal: string[]; - importsExternal: string[]; - keywords: string[]; -} -interface RawFileDoc { - knowledgeId: string; - relativePath: string; - content: string; - sha: string; - sizeBytes: number; - language: string; - analysis: FileAnalysis; - updatedAt: Date; -} -``` - -(`MongoConfigError`, `MongoConnectError`, `MongoNotConnectedError`, -`KnowledgeNotFoundError` are thrown by these functions but **defined in -`@bb/errors`** — import them from there.) - -`RawFileDoc` and `FileAnalysis` are package-local for now; they'll -graduate to `@bb/types` when a second consumer (e.g. `@bb/mcp` retrieval) -needs to read Raw docs. - -`_getDb()` and the `Collections` enum are **internal** — consumed only -by helpers inside this package. Higher tiers cannot reach a raw `Db` -handle; they go through typed domain helpers that this package will -expose as they are added. - -## Data ownership - -The single shared `MongoClient` instance. Document shapes, indexes, and -migrations are intentionally not owned here. - -## Invariants - -1. **No env reads.** The Mongo URI comes from - `getConfigValue(Config.MongoUri)`. No `process.env`, no `.env`, no fallback. - Enforced repo-wide by [eslint.config.mjs:71-94](../../eslint.config.mjs#L71-L94). -2. **`connectMongo()` is idempotent and concurrent-safe.** Repeated calls - return the existing client; concurrent calls await the same in-flight - connect promise. -3. **`closeMongo()` is graceful.** Clears the cached client before awaiting - `client.close()` so a subsequent `connectMongo()` cleanly re-establishes. -4. **Errors are typed, not strings.** `MongoConfigError` carries the exact - `bytebell set …` hint; `MongoConnectError` redacts credentials in the URI. -5. **No raw `Db` leaks.** `_getDb()` is not in `src/index.ts`. The only way - higher tiers touch Mongo is through typed helpers exported from this - package. - -## External dependencies - -- `mongodb` — official driver -- `@bb/config` — workspace dep, for `getConfigValue(Config.MongoUri)` -- `@bb/types` — workspace dep, for `Config` and `KnowledgeState` -- `@bb/errors` — workspace dep, for the typed error classes thrown here - -No logger, no telemetry, no Neo4j, no Redis. This package boots after -`@bb/config` and before everything that needs persistence. - -## What is intentionally out of scope (v0) - -- Knowledge-document creation, deletion, or full reads - (`getKnowledgeById`, `createKnowledge`, etc.) — added when the first - caller arrives -- Raw deletion / `deleteRawFile(knowledgeId, relativePath)` — - added when `github_pull` lands and needs to clean up files removed in - a diff -- `Nodes` and `Jobs` collection helpers — deferred until callers need - them -- Index creation / migrations -- Transactions helper -- Change streams, GridFS -- Application-level retry / backoff (the driver handles transport retries) -- A standalone "probe a candidate URI" helper for the setup form (added when - `@bb/cli`'s setup form lands) - -## How to extend - -Adding a new CRUD helper (e.g. `getKnowledgeById`): - -1. Pick or create the appropriate `Collections` enum entry in - `src/collections.ts` (single source of truth for collection names). -2. Create `src/.ts` (flat — repo ESLint forbids parent traversal, - so subdirectories require import gymnastics; keep `src/` flat). -3. Use `_getDb()` to obtain the `Db` handle and access the named - collection — never expose the raw `Db` to callers. -4. Return / accept domain types from `@bb/types`. Throw typed errors - from `@bb/errors`. -5. Re-export the helper from `src/index.ts`. -6. Update the _Public exports_ and _Out of scope_ sections of this file. diff --git a/packages/mongo/package.json b/packages/mongo/package.json deleted file mode 100644 index a68ed56..0000000 --- a/packages/mongo/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@bb/mongo", - "version": "0.0.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": "./src/index.ts" - }, - "imports": { - "#src/*": "./src/*" - }, - "dependencies": { - "@bb/config": "workspace:*", - "@bb/db": "workspace:*", - "@bb/errors": "workspace:*", - "@bb/db-core": "workspace:*", - "@bb/types": "workspace:*", - "mongodb": "^7.2.0" - } -} diff --git a/packages/mongo/src/README.md b/packages/mongo/src/README.md deleted file mode 100644 index dccfc53..0000000 --- a/packages/mongo/src/README.md +++ /dev/null @@ -1,117 +0,0 @@ -# `@bb/mongo/src` — context - -Implementation of `@bb/mongo`. See [../README.md](../README.md) for the -package-level contract; this file documents how the source tree is split. - -## Files - -- **[index.ts](index.ts)** — public re-exports. The only entry point other - packages may import. Exposes `connectMongo`, `closeMongo`, `pingMongo`, - `setKnowledgeState`, `markKnowledgeFailed`, `upsertRawFile`, and the - `PingResult` / `FileAnalysis` / `RawFileDoc` types. Anything not - re-exported here is internal. -- **[client.ts](client.ts)** — module-scoped `MongoClient` singleton plus - the lifecycle (`connectMongo`, `closeMongo`), the health probe - (`pingMongo`), and the **internal** `_getDb()` accessor. Reads the URI via - `getConfigValue(Config.MongoUri)` from `@bb/config` + `@bb/types`. Throws - typed errors from `@bb/errors` (`MongoConfigError`, `MongoConnectError`, - `MongoNotConnectedError`). Also exposes `__resetForTests()` — test seam - only, never imported by production code. -- **[collections.ts](collections.ts)** — the `Collections` enum: single - source of truth for collection name strings. Today: - `Collections.Knowledge = "knowledge"`, `Collections.Raw = "raw"`. - `Nodes` and `Jobs` join when their helpers land. **Internal** — not - re-exported from `index.ts`; consumed only by helpers in this folder. -- **[knowledge.ts](knowledge.ts)** — domain CRUD helpers: - - `setKnowledgeState(knowledgeId, state)` runs - `updateOne({ knowledgeId }, { $set: { "status.state": state, updatedAt }, $unset: { failure: "" } })`. - The `$unset` of `failure` makes the next successful transition out of - FAILED automatically clear any stale failure metadata. Throws - `KnowledgeNotFoundError` on `matchedCount === 0`. Called by `@bb/queue` - publishers on enqueue and by the runner on terminal success. - - `markKnowledgeFailed(knowledgeId, reason, category, detail?)` writes - the structured `failure: { reason, category, at, detail? }` subdoc - alongside `status.state = "FAILED"`. `reason` is short and - operator-readable; `detail` is the optional raw provider response. - Called by `@bb/ingest-github/src/pipeline/run.ts` (and `pull.ts`) - catch blocks via the shared `classifyFailure` helper. -- **[enrichment.ts](enrichment.ts)** — ConceptGraphStrategy ledger - helpers on `Collections.Knowledge`. State lives on the existing - `KnowledgeDoc` — no new collection. Exports: - - `startEnrichmentRun(knowledgeId, runId)` — stamps a fresh - `enrichmentRunId`, clears `completedFiles` and `enrichmentFailures`, - transitions `enrichmentState` to `Running`. Must be called before - any per-file write so stale state from a prior run can't masquerade - as already-done. - - `markFileEnriched(knowledgeId, relativePath)` — atomic - `$addToSet` to `completedFiles`. Idempotent. - - `recordEnrichmentFailure(knowledgeId, failure)` — replaces any - existing entry with `failure.filePath` (one entry per file) so the - array doesn't accumulate stale records across retries. - - `completeEnrichmentRun(knowledgeId)` / - `failEnrichmentRun(knowledgeId)` — terminal transitions of - `enrichmentState`. The parent `KnowledgeState` is the worker's - responsibility; this ledger only tracks the enrichment slice. - - All helpers throw `KnowledgeNotFoundError` on `matchedCount === 0`, - matching the existing `knowledge.ts` pattern. The Mongo collection - helper is typed via `_getDb().collection(...)` so - `$push` / `$pull` / `$addToSet` operators type-check against the - ledger arrays. - -- **[raw.ts](raw.ts)** — domain CRUD helpers for `Collections.Raw`. Defines - the `FileAnalysis` and `RawFileDoc` interfaces (package-local until - promotion to `@bb/types`). Exports: - - `upsertRawFile(doc)` — `updateOne({ knowledgeId, relativePath }, { $set: - }, { upsert: true })`. Called by `@bb/ingest-github`'s - worker for every scanned (added or modified) file. - - `listRawFileShas(knowledgeId)` — projection-only read returning a - `Map` of the previously-indexed tree. Consumed by the - pull worker to diff the new tree without needing git history. - - `deleteRawFiles(knowledgeId, relativePaths)` — `deleteMany` with `$in`; - no-op on empty input. Used by the pull worker to drop rows for files - that vanished between commits. - -## Module dependency graph - -``` -client.ts → mongodb, @bb/config (getConfigValue), @bb/types (Config), - @bb/errors (Mongo* error classes) -collections.ts → (leaf — no imports) -knowledge.ts → client.ts (_getDb), collections.ts (Collections), - @bb/types (KnowledgeState), @bb/errors (KnowledgeNotFoundError) -raw.ts → client.ts (_getDb), collections.ts (Collections) -index.ts → re-exports the public surface from client.ts + knowledge.ts + - raw.ts -``` - -No cycles. `collections.ts` is a leaf; `knowledge.ts` and `raw.ts` are -the two helpers composing `_getDb()` today. - -## Invariants enforced here - -- **Connect is idempotent and concurrent-safe.** `connectMongo()` short- - circuits if `client !== null`; concurrent callers await the same in-flight - `connecting` promise so a single connect is performed. -- **Close is graceful and re-entrant.** `closeMongo()` clears the cached - client _before_ awaiting `client.close()` so a subsequent `connectMongo()` - cleanly re-establishes; calling `closeMongo()` twice is a no-op. -- **No raw `Db` leak.** `_getDb()` is not in `index.ts`. Future typed - collection helpers will live in this folder and compose `_getDb()` - internally; consumers in higher tiers see only the typed helper signatures. -- **No env reads.** Only `getConfigValue(Config.MongoUri)` provides the URI. - Repo-wide ESLint rule blocks `process.env`. -- **Errors carry typed metadata.** Construction sites use the catalog in - `@bb/errors` — never inline `new Error(string)`. `MongoConfigError` carries - the exact `bytebell set …` hint; `MongoConnectError` redacts userinfo in - the URI before composing the message. - -## Adding a CRUD helper - -Follow the recipe in [../README.md](../README.md) under _How to extend_. -New files live as flat `src/.ts` (the repo ESLint rule forbids -parent traversal, so subdirectories require import gymnastics — keep -`src/` flat unless the package outgrows it). The helper composes -`_getDb()` to obtain the `Db` handle and `Collections.` for the -collection name; returns / accepts domain types from `@bb/types`; throws -typed errors from `@bb/errors`. diff --git a/packages/mongo/src/activity.ts b/packages/mongo/src/activity.ts deleted file mode 100644 index 5de3c0b..0000000 --- a/packages/mongo/src/activity.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { ActivityInput } from "@bb/types"; -import { _getDb } from "./client.ts"; -import { Collections } from "./collections.ts"; - -/** - * Record a detailed activity entry for an LLM interaction - * - * @param input - The activity data to record - */ -export async function recordActivity(input: ActivityInput): Promise { - const { response, ...rest } = input; - const activity = { - ...rest, - responseSnippet: response.slice(0, 500), // Only store a snippet to save space - createdAt: new Date(), - }; - - await _getDb().collection(Collections.Activity).insertOne(activity); -} diff --git a/packages/mongo/src/aggregateStats.ts b/packages/mongo/src/aggregateStats.ts deleted file mode 100644 index 95f7d59..0000000 --- a/packages/mongo/src/aggregateStats.ts +++ /dev/null @@ -1,149 +0,0 @@ -import type { KnowledgeDoc, StatsCommitEntry, StatsRepoEntry, StatsResponse, StatsTotals } from "@bb/types"; -import { _getDb } from "./client.ts"; -import { Collections } from "./collections.ts"; - -interface CommitHashRecord { - hash: string; - inputTokens: string; - outputTokens: string; - costUsd: string; -} - -/** - * Aggregates token + cost stats over the `knowledge` collection. Replaces the - * previous read against the deleted `processing_stats` collection — the - * authoritative per-commit numbers now live on the knowledge document's - * `source.commitHashes[]` (populated by `setKnowledgeCommit`). - * - * Fields that the old `processing_stats` row carried but the knowledge doc - * does not (per-commit `processingTimeMs`, `totalBatches`, `totalFolders`, - * `filesAnalyzed`, `createdAt`/`updatedAt`) are reported as 0 / empty — - * the `bytebell stats` UI tolerates that. - */ -export async function aggregateStats(): Promise { - const db = _getDb(); - const knowledgeDocs = (await db - .collection(Collections.Knowledge) - .find({}) - .sort({ updatedAt: -1 }) - .toArray()) as unknown as KnowledgeDoc[]; - - const repos: StatsRepoEntry[] = []; - const commitStats: StatsCommitEntry[] = []; - let totalInputTokens = 0; - let totalOutputTokens = 0; - let totalCost = 0; - let totalFiles = 0; - - for (const doc of knowledgeDocs) { - const commits = pickCommits(doc); - const fileCount = await db.collection(Collections.Raw).countDocuments({ knowledgeId: doc.knowledgeId }); - const repoName = deriveRepoName(doc); - const type = doc.source.kind === "github" ? ("GITHUB" as const) : ("LOCAL" as const); - - let repoIn = 0; - let repoOut = 0; - let repoCost = 0; - for (const c of commits) { - const inT = parseNumber(c.inputTokens); - const outT = parseNumber(c.outputTokens); - const cost = parseNumber(c.costUsd); - repoIn += inT; - repoOut += outT; - repoCost += cost; - commitStats.push({ - knowledgeId: doc.knowledgeId, - repoName, - commitHash: c.hash, - inputTokens: inT, - outputTokens: outT, - estimatedCost: cost, - totalBatches: 0, - processingTimeMs: 0, - totalFiles: fileCount, - totalFolders: 0, - filesAnalyzed: fileCount, - createdAt: "", - updatedAt: "", - }); - } - - repos.push({ - knowledgeId: doc.knowledgeId, - repoName, - type, - fileCount, - folderCount: 0, - inputTokens: repoIn, - outputTokens: repoOut, - estimatedCost: repoCost, - }); - - totalInputTokens += repoIn; - totalOutputTokens += repoOut; - totalCost += repoCost; - totalFiles += fileCount; - } - - const totals: StatsTotals = { - totalRepos: knowledgeDocs.length, - totalFiles, - totalFolders: 0, - totalInputTokens, - totalOutputTokens, - totalEstimatedCost: Math.round(totalCost * 1_000_000) / 1_000_000, - }; - - return { totals, repos, commitStats }; -} - -function pickCommits(doc: KnowledgeDoc): CommitHashRecord[] { - const source = (doc as unknown as { source?: { commitHashes?: unknown } }).source; - const raw = source?.commitHashes; - if (!Array.isArray(raw)) { - return []; - } - const out: CommitHashRecord[] = []; - for (const entry of raw) { - if (typeof entry !== "object" || entry === null) { - continue; - } - const rec = entry as Partial; - if (typeof rec.hash !== "string") { - continue; - } - out.push({ - hash: rec.hash, - inputTokens: typeof rec.inputTokens === "string" ? rec.inputTokens : "0", - outputTokens: typeof rec.outputTokens === "string" ? rec.outputTokens : "0", - costUsd: typeof rec.costUsd === "string" ? rec.costUsd : "0", - }); - } - return out; -} - -function parseNumber(value: string): number { - const n = Number.parseFloat(value); - return Number.isFinite(n) ? n : 0; -} - -function deriveRepoName(doc: KnowledgeDoc): string { - if (doc.source.kind === "local") { - const segments = doc.source.sourcePath.split("/").filter((s) => s.length > 0); - return segments.at(-1) ?? doc.source.sourcePath; - } - try { - const segments = new URL(doc.info.repoUrl ?? "").pathname - .split("/") - .map((s) => s.trim()) - .filter((s) => s.length > 0); - const repo = segments.at(-1)?.replace(/\.git$/u, ""); - const owner = segments.at(-2); - if (owner !== undefined && repo !== undefined) { - return `${owner}/${repo}`; - } - } catch { - // fall through - } - return doc.info.repoUrl ?? ""; -} diff --git a/packages/mongo/src/client.ts b/packages/mongo/src/client.ts deleted file mode 100644 index 05f81fe..0000000 --- a/packages/mongo/src/client.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { MongoClient, type Db } from "mongodb"; -import { getConfigValue } from "@bb/config"; -import { Config } from "@bb/types"; -import { MongoConfigError, MongoConnectError, MongoNotConnectedError } from "@bb/errors"; - -export interface PingResult { - ok: boolean; - latencyMs: number; -} - -let client: MongoClient | null = null; -let connecting: Promise | null = null; - -export async function connectMongo(): Promise { - if (client !== null) { - return; - } - if (connecting !== null) { - return connecting; - } - connecting = doConnect().finally(() => { - connecting = null; - }); - return connecting; -} - -async function doConnect(): Promise { - const uri = getConfigValue(Config.MongoUri); - if (uri.length === 0) { - throw new MongoConfigError("bytebell set mongo "); - } - const next = new MongoClient(uri); - try { - await next.connect(); - } catch (cause: unknown) { - await next.close().catch(() => undefined); - throw new MongoConnectError(uri, cause); - } - client = next; -} - -export async function closeMongo(): Promise { - if (client === null) { - return; - } - const c = client; - client = null; - await c.close(); -} - -export async function pingMongo(): Promise { - const db = _getDb(); - const start = performance.now(); - try { - await db.admin().ping(); - return { ok: true, latencyMs: Math.round(performance.now() - start) }; - } catch { - return { ok: false, latencyMs: Math.round(performance.now() - start) }; - } -} - -export function _getDb(): Db { - if (client === null) { - throw new MongoNotConnectedError(); - } - return client.db(); -} - -export function __resetForTests(): void { - client = null; - connecting = null; -} diff --git a/packages/mongo/src/collections.ts b/packages/mongo/src/collections.ts deleted file mode 100644 index 7737836..0000000 --- a/packages/mongo/src/collections.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum Collections { - Knowledge = "knowledge", - Raw = "raw", - Usage = "mcp_usage", - Activity = "mcp_activity", -} diff --git a/packages/mongo/src/enrichment.ts b/packages/mongo/src/enrichment.ts deleted file mode 100644 index e213c4d..0000000 --- a/packages/mongo/src/enrichment.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { EnrichmentFailure, KnowledgeDoc } from "@bb/types"; -import { EnrichmentState } from "@bb/types"; -import { KnowledgeNotFoundError } from "@bb/errors"; -import { _getDb } from "./client.ts"; -import { Collections } from "./collections.ts"; - -function knowledgeCollection() { - return _getDb().collection(Collections.Knowledge); -} - -// ───────────────────────────────────────────────────────────────────────────── -// Mongo enrichment ledger for ConceptGraphStrategy. State lives on the -// existing :KnowledgeDoc — no new collection. Knowledge.state itself stays -// PROCESSING throughout enrichment; this ledger tracks per-file progress so -// retries can resume by skipping `completedFiles`. -// -// All updates throw `KnowledgeNotFoundError` if the document is missing. -// ───────────────────────────────────────────────────────────────────────────── - -/** - * Begin or resume an enrichment attempt. Stamps a new `enrichmentRunId`, - * clears `enrichmentFailures` (failed files should be re-evaluated on the - * retry), transitions to `Running`. `completedFiles` is preserved so a - * BullMQ retry can skip work that already finished — the disk artifact - * tree at `meta-output/enrichment/.json` is the canonical source of - * truth, and `completedFiles` mirrors that. A clean re-enrichment requires - * an explicit reset, not a retry. - */ -export async function startEnrichmentRun(knowledgeId: string, runId: string): Promise { - const now = new Date(); - const result = await knowledgeCollection().updateOne( - { knowledgeId }, - { - $set: { - enrichmentRunId: runId, - enrichmentState: EnrichmentState.Running, - enrichmentFailures: [], - updatedAt: now, - }, - $setOnInsert: { - completedFiles: [], - }, - }, - ); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -/** - * Returns the list of files already enriched in the current/last attempt. - * Used by the strategy to pre-filter the work queue on retry. Empty array - * if the knowledge has no recorded enrichment runs. - */ -export async function getCompletedEnrichmentFiles(knowledgeId: string): Promise { - const doc = await knowledgeCollection().findOne({ knowledgeId }, { projection: { completedFiles: 1 } }); - if (doc === null) { - throw new KnowledgeNotFoundError(knowledgeId); - } - return Array.isArray(doc.completedFiles) ? doc.completedFiles : []; -} - -/** - * Records that `filePath` has been successfully enriched. Idempotent: relies - * on `$addToSet` so a re-run of the same file does not duplicate the entry. - */ -export async function markFileEnriched(knowledgeId: string, filePath: string): Promise { - const result = await knowledgeCollection().updateOne( - { knowledgeId }, - { - $addToSet: { completedFiles: filePath }, - $set: { updatedAt: new Date() }, - }, - ); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -/** - * Records or updates a per-file enrichment failure. The array is keyed by - * `filePath` (one entry per file); subsequent failures for the same file - * replace the prior entry rather than accumulating. Diagnostic, not - * load-bearing — the strategy decides whether the knowledge fails overall. - */ -export async function recordEnrichmentFailure(knowledgeId: string, failure: EnrichmentFailure): Promise { - const db = knowledgeCollection(); - // Remove any existing entry for this path, then push the fresh one. - await db.updateOne({ knowledgeId }, { $pull: { enrichmentFailures: { filePath: failure.filePath } } }); - const result = await db.updateOne( - { knowledgeId }, - { - $push: { enrichmentFailures: failure }, - $set: { updatedAt: new Date() }, - }, - ); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -/** Transitions the ledger to `Completed`. Caller is responsible for then transitioning the parent `KnowledgeState`. */ -export async function completeEnrichmentRun(knowledgeId: string): Promise { - const result = await knowledgeCollection().updateOne( - { knowledgeId }, - { $set: { enrichmentState: EnrichmentState.Completed, updatedAt: new Date() } }, - ); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -/** - * Transitions the ledger to `Failed`. Knowledge can be retried by calling - * `startEnrichmentRun` again with a fresh run id. - */ -export async function failEnrichmentRun(knowledgeId: string): Promise { - const result = await knowledgeCollection().updateOne( - { knowledgeId }, - { $set: { enrichmentState: EnrichmentState.Failed, updatedAt: new Date() } }, - ); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} diff --git a/packages/mongo/src/index.ts b/packages/mongo/src/index.ts deleted file mode 100644 index 8ceb14c..0000000 --- a/packages/mongo/src/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import "./provider.ts"; - -export { connectMongo, closeMongo, pingMongo } from "./client.ts"; -export type { PingResult } from "./client.ts"; - -export { - getKnowledge, - setKnowledgeCommit, - setKnowledgeState, - setKnowledgeBranch, - setBranchHead, - updateKnowledgeProgress, - upsertKnowledge, - listKnowledge, - deleteKnowledge, -} from "./knowledge.ts"; -export type { KnowledgeListEntry, DeleteKnowledgeResult } from "./knowledge.ts"; -export { - markKnowledgeFailed, - markKnowledgeHalted, - markKnowledgeCorrupted, - promoteHaltedToFailed, -} from "./knowledge-failure.ts"; - -export { upsertRawFile, listRawFileShas, deleteRawFiles } from "./raw.ts"; -export type { FileAnalysis, FileAnalysisSection, RawFileDoc } from "./raw.ts"; - -export { aggregateStats } from "./aggregateStats.ts"; - -export { incrementUsage, getMonthlyUsage, getGlobalUsage } from "./usage.ts"; -export { recordActivity } from "./activity.ts"; - -export { - startEnrichmentRun, - getCompletedEnrichmentFiles, - markFileEnriched, - recordEnrichmentFailure, - completeEnrichmentRun, - failEnrichmentRun, -} from "./enrichment.ts"; diff --git a/packages/mongo/src/knowledge-failure.ts b/packages/mongo/src/knowledge-failure.ts deleted file mode 100644 index 3ce5182..0000000 --- a/packages/mongo/src/knowledge-failure.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { KnowledgeFailureCategory } from "@bb/types"; -import { KnowledgeNotFoundError } from "@bb/errors"; -import { _getDb } from "./client.ts"; -import { Collections } from "./collections.ts"; - -/** - * Knowledge failure lifecycle: the terminal/transient state transitions that - * stamp a structured `failure` subdoc. Split out of `knowledge.ts` to keep each - * file single-responsibility (and under the 300-line ceiling). The next - * successful state transition clears `failure` (see `setKnowledgeState`'s - * `$unset` in `knowledge.ts`). - */ - -function buildFailureSubdoc( - reason: string, - category: KnowledgeFailureCategory, - at: Date, - detail?: string, -): { reason: string; category: KnowledgeFailureCategory; at: Date; detail?: string } { - const failure: { reason: string; category: KnowledgeFailureCategory; at: Date; detail?: string } = { - reason, - category, - at, - }; - if (detail !== undefined && detail.length > 0) { - failure.detail = detail; - } - return failure; -} - -/** - * Marks a knowledge as FAILED and records the structured failure reason on - * the top-level `failure` subdoc. The next successful transition out of - * FAILED automatically clears it (see `setKnowledgeState`'s `$unset`). - * - * `reason` is a short operator-readable sentence (UI surfaces it directly). - * `detail` is the raw provider response or structured debug payload (UI may - * hide behind a disclosure). - */ -export async function markKnowledgeFailed( - knowledgeId: string, - reason: string, - category: KnowledgeFailureCategory, - detail?: string, -): Promise { - const now = new Date(); - const result = await _getDb() - .collection(Collections.Knowledge) - .updateOne( - { knowledgeId }, - { - $set: { - "status.state": "FAILED", - failure: buildFailureSubdoc(reason, category, now, detail), - updatedAt: now, - }, - }, - ); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -/** - * Marks a knowledge as HALTED — a transient failure for which auto-retry is - * pending. Records the same structured `failure` subdoc as `markKnowledgeFailed` - * so the reason is already persisted when the queue finalizer later promotes - * HALTED → FAILED via `promoteHaltedToFailed`. HALTED is non-terminal: the next - * successful transition clears `failure` (see `setKnowledgeState`'s `$unset`). - */ -export async function markKnowledgeHalted( - knowledgeId: string, - reason: string, - category: KnowledgeFailureCategory, - detail?: string, -): Promise { - const now = new Date(); - const result = await _getDb() - .collection(Collections.Knowledge) - .updateOne( - { knowledgeId }, - { - $set: { - "status.state": "HALTED", - failure: buildFailureSubdoc(reason, category, now, detail), - updatedAt: now, - }, - }, - ); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -/** - * Marks a knowledge as terminal CORRUPTED — the source repo is gone or - * inaccessible. Records the same structured `failure` subdoc as - * `markKnowledgeFailed`. CORRUPTED removes the row from the auto-pull sweep - * (which selects only PROCESSED) while leaving the indexed data queryable. The - * next successful transition clears `failure` (see `setKnowledgeState`'s `$unset`). - */ -export async function markKnowledgeCorrupted( - knowledgeId: string, - reason: string, - category: KnowledgeFailureCategory, - detail?: string, -): Promise { - const now = new Date(); - const result = await _getDb() - .collection(Collections.Knowledge) - .updateOne( - { knowledgeId }, - { - $set: { - "status.state": "CORRUPTED", - failure: buildFailureSubdoc(reason, category, now, detail), - updatedAt: now, - }, - }, - ); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -/** - * Promotes a HALTED knowledge to terminal FAILED, preserving the `failure` - * subdoc recorded at HALT time (the original transient reason is more useful - * than a generic "retries exhausted"). Scoped to `status.state === "HALTED"` - * so it is idempotent and never clobbers a record that already moved on (e.g. - * a manual retry that flipped it back to PROCESSING). Returns whether a - * document was actually promoted. - */ -export async function promoteHaltedToFailed(knowledgeId: string): Promise { - const result = await _getDb() - .collection(Collections.Knowledge) - .updateOne( - { knowledgeId, "status.state": "HALTED" }, - { $set: { "status.state": "FAILED", updatedAt: new Date() } }, - ); - return result.modifiedCount > 0; -} diff --git a/packages/mongo/src/knowledge.ts b/packages/mongo/src/knowledge.ts deleted file mode 100644 index be73f72..0000000 --- a/packages/mongo/src/knowledge.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { branchIdFor, type KnowledgeDoc, type KnowledgeState } from "@bb/types"; -import { KnowledgeNotFoundError } from "@bb/errors"; -import { _getDb } from "./client.ts"; -import { Collections } from "./collections.ts"; - -const DEFAULT_LIST_LIMIT = 200; - -export interface KnowledgeListEntry extends KnowledgeDoc { - fileCount: number; -} - -export async function setKnowledgeState(knowledgeId: string, state: KnowledgeState): Promise { - const update: Record = { "status.state": state, updatedAt: new Date() }; - const result = await _getDb() - .collection(Collections.Knowledge) - .updateOne({ knowledgeId }, { $set: update, $unset: { failure: "" } }); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -/** - * Records that this knowledge is now indexed at `commitHash`. Sets it as the - * current head pointer (`source.commitId`) and appends to the deduped history - * array (`source.commitHashes`). Idempotent: re-recording the same commit is - * a no-op except for the `updatedAt` bump. - * - * Throws `KnowledgeNotFoundError` if the document doesn't exist. - */ -export async function setKnowledgeCommit( - knowledgeId: string, - commitHash: string, - inputTokens: string = "", - outputTokens: string = "", - costUsd: string = "0", - cachedInputTokens: string = "0", - cachedOutputTokens: string = "0", - cachedCostUsd: string = "0", -): Promise { - const result = await _getDb() - .collection(Collections.Knowledge) - .updateOne( - { knowledgeId }, - { - $set: { "source.commitId": commitHash, updatedAt: new Date() }, - $addToSet: { - "source.commitHashes": { - hash: commitHash, - inputTokens, - outputTokens, - costUsd, - cachedInputTokens, - cachedOutputTokens, - cachedCostUsd, - }, - }, - }, - ); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -/** - * Sets `source.commitId` only — no history append. Used early in the - * pipeline so MCP tools (`retrieve_file_content` etc.) called during - * enrichment can resolve the on-disk clone dir via the commit-scoped path - * layout. The history entry is written later by `setKnowledgeCommit` with - * the real token usage. - */ -export async function setKnowledgeCommitHead(knowledgeId: string, commitHash: string): Promise { - const result = await _getDb() - .collection(Collections.Knowledge) - .updateOne({ knowledgeId }, { $set: { "source.commitId": commitHash, updatedAt: new Date() } }); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -/** - * Updates the branch name of a GitHub knowledge entry. - */ -export async function setKnowledgeBranch(knowledgeId: string, branch: string): Promise { - const result = await _getDb() - .collection(Collections.Knowledge) - .updateOne({ knowledgeId }, { $set: { "source.branch": branch, updatedAt: new Date() } }); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -/** - * Records that `branch` is indexed at `commitHash` on this knowledge, under the - * branch-per-KB model (`source.branches[]`). Upserts the branch element: - * advances its `headCommit`, appends `commitHash` to the branch's own deduped - * `commitHashes` history, and (optionally) sets the branch's processing - * `state`. Seeds `source.defaultBranch` on the first branch recorded. Idempotent - * per (branch, commit). Throws `KnowledgeNotFoundError` if the doc is missing. - * - * Callers: the branch-add trigger (state = PROCESSING at enqueue) and the - * worker on completion (state = PROCESSED + token usage). - */ -export async function setBranchHead( - knowledgeId: string, - branch: string, - commitHash: string, - opts: { state?: KnowledgeState; inputTokens?: string; outputTokens?: string; costUsd?: string } = {}, -): Promise { - const db = _getDb(); - const commitEntry = { - hash: commitHash, - inputTokens: opts.inputTokens ?? "", - outputTokens: opts.outputTokens ?? "", - costUsd: opts.costUsd ?? "0", - }; - - // 1. Create the branch element if it doesn't exist yet (first commit seeds its history). - const inserted = await db.collection(Collections.Knowledge).updateOne( - { knowledgeId, "source.branches.name": { $ne: branch } }, - { - $addToSet: { - "source.branches": { - name: branch, - branchId: branchIdFor(branch), - headCommit: commitHash, - commitHashes: [commitEntry], - ...(opts.state !== undefined ? { state: opts.state } : {}), - }, - }, - $set: { updatedAt: new Date() }, - }, - ); - - // 2. Otherwise advance the existing branch's head + append its commit. - if (inserted.modifiedCount === 0) { - const set: Record = { "source.branches.$.headCommit": commitHash, updatedAt: new Date() }; - if (opts.state !== undefined) { - set["source.branches.$.state"] = opts.state; - } - const updated = await db - .collection(Collections.Knowledge) - .updateOne( - { knowledgeId, "source.branches.name": branch }, - { $set: set, $addToSet: { "source.branches.$.commitHashes": commitEntry } }, - ); - if (updated.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } - } - - // 3. Seed the default branch on the first branch recorded. - await db - .collection(Collections.Knowledge) - .updateOne( - { knowledgeId, "source.defaultBranch": { $exists: false } }, - { $set: { "source.defaultBranch": branch } }, - ); -} - -export async function updateKnowledgeProgress( - knowledgeId: string, - processedFiles: number, - totalFiles?: number, - extra?: { progressPercent?: number; currentPhase?: string }, -): Promise { - const update: Record = { - "status.processedFiles": processedFiles, - updatedAt: new Date(), - }; - if (totalFiles !== undefined) { - update["status.totalFiles"] = totalFiles; - } - if (extra?.progressPercent !== undefined) { - update["status.progressPercent"] = extra.progressPercent; - } - if (extra?.currentPhase !== undefined) { - update["status.currentPhase"] = extra.currentPhase; - } - const result = await _getDb().collection(Collections.Knowledge).updateOne({ knowledgeId }, { $set: update }); - if (result.matchedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } -} - -export async function upsertKnowledge(doc: Omit & { updatedAt?: Date }): Promise { - const now = new Date(); - await _getDb() - .collection(Collections.Knowledge) - .updateOne( - { knowledgeId: doc.knowledgeId }, - { - $set: { - source: doc.source, - // `info` carries repoUrl/branch (set at index time). It MUST be - // persisted — `GET /api/v1/repos` derives `source.repoUrl` from - // `info.repoUrl`, and the CLI groups the `ls` view by it. Omitting it - // here (as before) left Mongo entries with no repoUrl, so `ls` rendered - // the group label as "undefined". The SQLite provider never had this - // bug because it stores the whole doc as JSON. - info: doc.info, - status: doc.status, - updatedAt: doc.updatedAt ?? now, - }, - $setOnInsert: { - knowledgeId: doc.knowledgeId, - createdAt: doc.createdAt, - }, - }, - { upsert: true }, - ); -} - -export interface DeleteKnowledgeResult { - knowledgeDeleted: number; - rawDeleted: number; -} - -export async function deleteKnowledge(knowledgeId: string): Promise { - const db = _getDb(); - const knowledgeRes = await db.collection(Collections.Knowledge).deleteOne({ knowledgeId }); - if (knowledgeRes.deletedCount === 0) { - throw new KnowledgeNotFoundError(knowledgeId); - } - const rawRes = await db.collection(Collections.Raw).deleteMany({ knowledgeId }); - return { - knowledgeDeleted: knowledgeRes.deletedCount ?? 0, - rawDeleted: rawRes.deletedCount ?? 0, - }; -} - -export async function listKnowledge(opts: { limit?: number } = {}): Promise { - const db = _getDb(); - const limit = opts.limit ?? DEFAULT_LIST_LIMIT; - const docs = (await db - .collection(Collections.Knowledge) - .find({}) - .sort({ updatedAt: -1 }) - .limit(limit) - .toArray()) as unknown as KnowledgeDoc[]; - - const entries: KnowledgeListEntry[] = []; - for (const doc of docs) { - const fileCount = await db.collection(Collections.Raw).countDocuments({ knowledgeId: doc.knowledgeId }); - entries.push({ ...doc, fileCount }); - } - return entries; -} -export async function getKnowledge(knowledgeId: string): Promise { - const db = _getDb(); - const doc = (await db.collection(Collections.Knowledge).findOne({ knowledgeId })) as unknown as KnowledgeDoc | null; - if (doc === null) { - return null; - } - const fileCount = await db.collection(Collections.Raw).countDocuments({ knowledgeId }); - return { ...doc, fileCount }; -} diff --git a/packages/mongo/src/provider.ts b/packages/mongo/src/provider.ts deleted file mode 100644 index 8c47f5a..0000000 --- a/packages/mongo/src/provider.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { connectMongo, closeMongo, pingMongo } from "./client.ts"; -import * as knowledgeRepo from "./knowledge.ts"; -import * as knowledgeFailure from "./knowledge-failure.ts"; -import * as rawRepo from "./raw.ts"; -import * as statsRepo from "./aggregateStats.ts"; -import * as activityRepo from "./activity.ts"; -import * as usageRepo from "./usage.ts"; - -import { registerDbProvider } from "@bb/db"; -import type { IDocumentDatabaseProvider } from "@bb/db-core"; - -class MongoDatabaseProvider implements IDocumentDatabaseProvider { - knowledge = { - setKnowledgeState: knowledgeRepo.setKnowledgeState, - setKnowledgeCommit: knowledgeRepo.setKnowledgeCommit, - setKnowledgeCommitHead: knowledgeRepo.setKnowledgeCommitHead, - setKnowledgeBranch: knowledgeRepo.setKnowledgeBranch, - updateKnowledgeProgress: knowledgeRepo.updateKnowledgeProgress, - upsertKnowledge: knowledgeRepo.upsertKnowledge, - deleteKnowledge: knowledgeRepo.deleteKnowledge, - listKnowledge: knowledgeRepo.listKnowledge, - getKnowledge: knowledgeRepo.getKnowledge, - markKnowledgeFailed: knowledgeFailure.markKnowledgeFailed, - markKnowledgeHalted: knowledgeFailure.markKnowledgeHalted, - promoteHaltedToFailed: knowledgeFailure.promoteHaltedToFailed, - markKnowledgeCorrupted: knowledgeFailure.markKnowledgeCorrupted, - }; - - raw = { - upsertRawFile: rawRepo.upsertRawFile, - listRawFileShas: rawRepo.listRawFileShas, - deleteRawFiles: rawRepo.deleteRawFiles, - }; - - stats = { - aggregateStats: statsRepo.aggregateStats, - }; - - activity = { - recordActivity: activityRepo.recordActivity, - }; - - usage = { - incrementUsage: usageRepo.incrementUsage, - getMonthlyUsage: usageRepo.getMonthlyUsage, - getGlobalUsage: usageRepo.getGlobalUsage, - }; - - async connect(): Promise { - await connectMongo(); - } - - async close(): Promise { - await closeMongo(); - } - - async ping() { - return pingMongo(); - } -} - -registerDbProvider("mongo", () => new MongoDatabaseProvider()); diff --git a/packages/mongo/src/raw.ts b/packages/mongo/src/raw.ts deleted file mode 100644 index d554638..0000000 --- a/packages/mongo/src/raw.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { _getDb } from "./client.ts"; -import { Collections } from "./collections.ts"; - -export interface FileAnalysisSection { - name: string; - description: string; -} - -export interface FileAnalysis { - purpose: string; - summary: string; - businessContext: string; - classes: string[]; - functions: string[]; - importsInternal: string[]; - importsExternal: string[]; - keywords: string[]; - ontologyConcepts?: string[]; - businessEntities?: string[]; - systemCapabilities?: string[]; - sideEffects?: string[]; - configDependencies?: string[]; - dataFlowDirection?: string; - integrationSurface?: string[]; - contractsProvided?: string[]; - contractsConsumed?: string[]; - sectionMap?: FileAnalysisSection[]; -} - -export interface RawFileDoc { - knowledgeId: string; - relativePath: string; - content: string; - sha: string; - sizeBytes: number; - language: string; - analysis: FileAnalysis; - updatedAt: Date; -} - -export async function upsertRawFile(doc: Omit): Promise { - await _getDb() - .collection(Collections.Raw) - .updateOne( - { knowledgeId: doc.knowledgeId, relativePath: doc.relativePath }, - { $set: { ...doc, updatedAt: new Date() } }, - { upsert: true }, - ); -} - -/** - * Returns the `relativePath → sha` map for every raw file currently stored - * under `knowledgeId`. Used by the pull worker to diff the new tree against - * the previously-indexed tree without needing git history. - */ -export async function listRawFileShas(knowledgeId: string): Promise> { - const cursor = _getDb() - .collection(Collections.Raw) - .find({ knowledgeId }, { projection: { _id: 0, relativePath: 1, sha: 1 } }); - const docs = (await cursor.toArray()) as unknown as Array<{ relativePath: string; sha: string }>; - const map = new Map(); - for (const doc of docs) { - map.set(doc.relativePath, doc.sha); - } - return map; -} - -/** - * Deletes every `raw_files` row in `relativePaths` for `knowledgeId`. No-op - * when `relativePaths` is empty. Returns the count actually removed. - */ -export async function deleteRawFiles(knowledgeId: string, relativePaths: string[]): Promise { - if (relativePaths.length === 0) { - return 0; - } - const result = await _getDb() - .collection(Collections.Raw) - .deleteMany({ knowledgeId, relativePath: { $in: relativePaths } }); - return result.deletedCount ?? 0; -} diff --git a/packages/mongo/src/usage.ts b/packages/mongo/src/usage.ts deleted file mode 100644 index 9f4df6c..0000000 --- a/packages/mongo/src/usage.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { _getDb } from "./client.ts"; -import { Collections } from "./collections.ts"; - -/** - * Increment usage for an identity (user or org) for the current month - * - * @param identityId - The unique identifier for the user or organization - * @param inputTokenCount - The number of input tokens - * @param outputTokenCount - The number of output tokens - */ -export async function incrementUsage( - identityId: string, - inputTokenCount: number = 0, - outputTokenCount: number = 0, -): Promise { - const now = new Date(); - const year = now.getUTCFullYear(); - const month = now.getUTCMonth() + 1; // 1-12 - - await _getDb() - .collection(Collections.Usage) - .updateOne( - { identityId, year, month }, - { - $inc: { - requestCount: 1, - inputTokens: inputTokenCount, - outputTokens: outputTokenCount, - tokensUsed: inputTokenCount + outputTokenCount, - }, - $set: { - lastUpdated: now, - }, - $setOnInsert: { - createdAt: now, - }, - }, - { upsert: true }, - ); -} - -/** - * Get monthly usage for all identities - */ -export async function getMonthlyUsage(year: number, month: number) { - return await _getDb().collection(Collections.Usage).find({ year, month }).toArray(); -} - -/** - * Get global aggregate usage - */ -export async function getGlobalUsage() { - return await _getDb() - .collection(Collections.Usage) - .aggregate([ - { - $group: { - _id: null, - totalRequests: { $sum: "$requestCount" }, - totalInputTokens: { $sum: "$inputTokens" }, - totalOutputTokens: { $sum: "$outputTokens" }, - totalTokens: { $sum: "$tokensUsed" }, - }, - }, - ]) - .toArray(); -} diff --git a/packages/mongo/tsconfig.json b/packages/mongo/tsconfig.json deleted file mode 100644 index 4ed0786..0000000 --- a/packages/mongo/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.json"] -} diff --git a/packages/neo4j/README.md b/packages/neo4j/README.md index 116200c..be80285 100644 --- a/packages/neo4j/README.md +++ b/packages/neo4j/README.md @@ -14,7 +14,7 @@ nodes, indexes). The arch.md tier diagram reserves a separate `@bb/graph` for the typed query layer; we collapsed for v1 to keep boilerplate low. When `@bb/mcp` retrieval lands and needs to read the same graph, the helpers will graduate into a dedicated `@bb/graph` package and `@bb/neo4j` -shrinks to driver-only — mirroring the `@bb/redis` + `@bb/queue` split. +shrinks to driver-only — mirroring the `@bb/queue-core` + `@bb/queue` split. ## Responsibility @@ -271,7 +271,7 @@ Neo4jPassword`). Repo-wide ESLint rule blocks `process.env`. awaiting `driver.close()` so a subsequent `connectNeo4j()` cleanly re-establishes. 4. **Errors are typed, not strings.** `Neo4jConfigError` carries the - exact `bytebell set …` hint; `Neo4jConnectError` redacts userinfo + exact `plumbline set …` hint; `Neo4jConnectError` redacts userinfo in the URI; `Neo4jNotConnectedError` is a marker. 5. **Schema bootstrap is tolerant.** `ensureKnowledgeIndexes()` swallows "already exists" errors (Neo4j refuses constraints when a matching diff --git a/packages/neo4j/package.json b/packages/neo4j/package.json index 707f06a..22f0d1d 100644 --- a/packages/neo4j/package.json +++ b/packages/neo4j/package.json @@ -17,7 +17,7 @@ "@bb/graph-db": "workspace:*", "@bb/graph-core": "workspace:*", "@bb/types": "workspace:*", - "@bb/mongo": "workspace:*", + "@bb/db-core": "workspace:*", "neo4j-driver": "^6.0.1" } } diff --git a/packages/neo4j/src/README.md b/packages/neo4j/src/README.md index 73d6705..a05c066 100644 --- a/packages/neo4j/src/README.md +++ b/packages/neo4j/src/README.md @@ -205,10 +205,10 @@ repo.ts → client.ts, knowledge.ts (repoNameFromGithubUrl) folder.ts → client.ts, pathUtils.ts (folderLevel, parentFolderPath), repo.ts (NodeScope type) files.ts → client.ts, pathUtils.ts (basename, parentFolderPath), legacyOrgKeywordMirror.ts (mirrorFileOrgKeywords, - buildOrgKeywordMirrorSteps), @bb/mongo (FileAnalysis type) + buildOrgKeywordMirrorSteps), @bb/db-core (FileAnalysis type) fileVersions.ts → client.ts -legacyKeywordChannels.ts → @bb/mongo (FileAnalysis type) -legacyOrgKeywordMirror.ts → client.ts, legacyKeywordChannels.ts, @bb/mongo (FileAnalysis type) +legacyKeywordChannels.ts → @bb/db-core (FileAnalysis type) +legacyOrgKeywordMirror.ts → client.ts, legacyKeywordChannels.ts, @bb/db-core (FileAnalysis type) index.ts → re-exports the public surface from client.ts + indexes.ts + flatFolderIndexes.ts + knowledge.ts + files.ts + repo.ts + folder.ts + fileVersions.ts + concepts.ts + contracts.ts + diff --git a/packages/neo4j/src/client.ts b/packages/neo4j/src/client.ts index b3360c3..3f628ad 100644 --- a/packages/neo4j/src/client.ts +++ b/packages/neo4j/src/client.ts @@ -29,10 +29,10 @@ async function doConnect(): Promise { const user = getConfigValue(Config.Neo4jUser); const password = getConfigValue(Config.Neo4jPassword); if (uri.length === 0) { - throw new Neo4jConfigError("bytebell set neo4j "); + throw new Neo4jConfigError("plumbline set neo4j "); } if (user.length === 0 || password.length === 0) { - throw new Neo4jConfigError("bytebell set neo4j-user && bytebell set neo4j-password "); + throw new Neo4jConfigError("plumbline set neo4j-user && plumbline set neo4j-password "); } const next = createDriver(uri, auth.basic(user, password)); try { diff --git a/packages/neo4j/src/filesParams.ts b/packages/neo4j/src/filesParams.ts index 1895003..37cca85 100644 --- a/packages/neo4j/src/filesParams.ts +++ b/packages/neo4j/src/filesParams.ts @@ -1,4 +1,4 @@ -import type { FileAnalysis } from "@bb/mongo"; +import type { FileAnalysis } from "@bb/db-core"; import { basename } from "./pathUtils.ts"; /** Public input shape for `upsertFileNode` / `upsertFileNodesBatch`. */ diff --git a/packages/neo4j/src/knowledge.ts b/packages/neo4j/src/knowledge.ts index 4792b8a..df643ab 100644 --- a/packages/neo4j/src/knowledge.ts +++ b/packages/neo4j/src/knowledge.ts @@ -46,7 +46,7 @@ DETACH DELETE k // Defensive cleanup: wipe File nodes whose knowledgeId has no matching // :Knowledge. Orphans accumulate when a worker writes files after the Knowledge // node is deleted (interrupted runs, racing deletes, partial failures). The TUI -// delete picker reads from Mongo, so orphan-only knowledgeIds are otherwise +// delete picker reads from the document store, so orphan-only knowledgeIds are otherwise // unreachable. const DELETE_ORPHAN_FILES = ` MATCH (f:File) diff --git a/packages/neo4j/src/legacyKeywordChannels.ts b/packages/neo4j/src/legacyKeywordChannels.ts index 134e202..58ad9ec 100644 --- a/packages/neo4j/src/legacyKeywordChannels.ts +++ b/packages/neo4j/src/legacyKeywordChannels.ts @@ -1,4 +1,4 @@ -import type { FileAnalysis } from "@bb/mongo"; +import type { FileAnalysis } from "@bb/db-core"; // Maps a FileAnalysis into the 14 channels the legacy reader expects to find // materialized as (:OrgKeyword {keyword, type, org_id, content_type:'code'}) diff --git a/packages/neo4j/src/legacyOrgKeywordMirror.ts b/packages/neo4j/src/legacyOrgKeywordMirror.ts index fd9b6c1..d7e19a7 100644 --- a/packages/neo4j/src/legacyOrgKeywordMirror.ts +++ b/packages/neo4j/src/legacyOrgKeywordMirror.ts @@ -1,6 +1,6 @@ import { _runCypher, type CypherStep } from "./client.ts"; import { expandLegacyOrgKeywordEdges, type LegacyOrgKeywordEdge } from "./legacyKeywordChannels.ts"; -import type { FileAnalysis } from "@bb/mongo"; +import type { FileAnalysis } from "@bb/db-core"; // :OrgKeyword + [:APPEARS_IN_FILE]->:FileNode materialization for the // legacy snake_case search graph used by chat-mcp smart_search / diff --git a/packages/path-migration/README.md b/packages/path-migration/README.md index 72d8540..cf2af30 100644 --- a/packages/path-migration/README.md +++ b/packages/path-migration/README.md @@ -2,7 +2,7 @@ ## Tier -Utility. Depends only on Kernel (`@bb/types` for the path-layout helpers and `KnowledgeDoc`). Imported by Binaries (`@bb/cli` for `bytebell migrate paths`, `@bb/server` for boot-time reconciliation). Holds no DB connection and performs no I/O beyond the local filesystem. +Utility. Depends only on Kernel (`@bb/types` for the path-layout helpers and `KnowledgeDoc`). Imported by Binaries (`@bb/cli` for `plumbline migrate paths`, `@bb/server` for boot-time reconciliation). Holds no DB connection and performs no I/O beyond the local filesystem. ## Responsibility @@ -16,7 +16,7 @@ Given the set of knowledge known to the DB, it: 1. **Migrates** every knowledge with a derivable target (commit + repo url, or a synthetic commit for local sources) by moving its clone + meta-output under the commit-scoped tree. 2. **Abandons** legacy directories with no backing DB record — they can never be migrated (no doc to derive a target), so they are deleted and reported. This is what lets boot self-heal after a DB reset leaves orphaned dirs behind. -The package never opens a DB connection: the caller lists knowledge and passes the docs in. This keeps it usable from both the CLI (connects Mongo directly) and the server boot path (uses the active `@bb/db` provider) without either deployable importing the other. +The package never opens a DB connection: the caller lists knowledge and passes the docs in. This keeps it usable from both the CLI and the server boot path — each connects the active `@bb/db` provider itself — without either deployable importing the other. ## Invariants @@ -28,4 +28,4 @@ The package never opens a DB connection: the caller lists knowledge and passes t ## External dependencies - `node:fs/promises`, `node:path` — filesystem moves. -- `@bb/types` — `bytebellPathsFor`, `repositoryDirFor`, `parseGithubOwnerRepo`, `RepoLocation`, `KnowledgeDoc`. +- `@bb/types` — `plumblinePathsFor`, `repositoryDirFor`, `parseGithubOwnerRepo`, `RepoLocation`, `KnowledgeDoc`. diff --git a/packages/path-migration/src/migrate-legacy-paths.ts b/packages/path-migration/src/migrate-legacy-paths.ts index 549a387..471c382 100644 --- a/packages/path-migration/src/migrate-legacy-paths.ts +++ b/packages/path-migration/src/migrate-legacy-paths.ts @@ -9,7 +9,7 @@ import type { MigrateLegacyPathsInput, MigrationSummary } from "./types.ts"; * deleted and reported as `abandoned`. * * Pure disk work — the caller owns the DB connection and supplies the docs, so - * both the CLI (`bytebell migrate paths`) and the server boot path can share + * both the CLI (`plumbline migrate paths`) and the server boot path can share * this without either reaching into the other. */ export async function migrateLegacyPaths(input: MigrateLegacyPathsInput): Promise { diff --git a/packages/path-migration/src/move.ts b/packages/path-migration/src/move.ts index b7ad26e..97356bb 100644 --- a/packages/path-migration/src/move.ts +++ b/packages/path-migration/src/move.ts @@ -1,7 +1,7 @@ import path from "node:path"; import { stat, rename, mkdir, readdir, rm, cp } from "node:fs/promises"; import { - bytebellPathsFor, + plumblinePathsFor, parseGithubOwnerRepo, repositoryDirFor, type KnowledgeDoc, @@ -59,7 +59,7 @@ async function moveMetaIfPresent(ctx: MoveCtx, legacyMetaRoot: string, newLoc: R if (!(await pathExists(legacyMetaRoot))) { return; } - const newMetaOutput = bytebellPathsFor(ctx.home, newLoc).metaOutputRoot; + const newMetaOutput = plumblinePathsFor(ctx.home, newLoc).metaOutputRoot; if (await pathExists(newMetaOutput)) { ctx.summary.skippedAlready.push(`${ctx.knowledgeId} (meta-output)`); return; diff --git a/packages/path-migration/src/types.ts b/packages/path-migration/src/types.ts index 683efb4..28c685d 100644 --- a/packages/path-migration/src/types.ts +++ b/packages/path-migration/src/types.ts @@ -22,7 +22,7 @@ export interface MigrationSummary { } export interface MigrateLegacyPathsInput { - /** Bytebell home directory (e.g. `~/.bytebell`). */ + /** Plumbline home directory (e.g. `~/.plumbline`). */ home: string; /** Single-tenant org id (`local`). */ orgId: string; diff --git a/packages/queue-bullmq/README.md b/packages/queue-bullmq/README.md deleted file mode 100644 index 418b8c5..0000000 --- a/packages/queue-bullmq/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# `@bb/queue-bullmq` - -BullMQ-over-Redis implementation of `@bb/queue-core`'s `IQueueProvider`. - -## Tier - -Strategy. Registers itself with `@bb/queue` via side-effect import as the `"bullmq"` provider. - -## Responsibilities - -- Implements `IQueueProvider` by wrapping BullMQ `Queue`, `Worker`, and `QueueEvents` against the Redis connection options from `@bb/redis`. -- Owns the Redis lifecycle: `connect()` calls `connectRedis()`, `close()` calls `closeRedis()`, `ping()` calls `pingRedis()`. -- Maps `JobPriority` (Low/Normal/High) to BullMQ's smaller-number-wins ordering (1000/100/10). -- Computes the stable dedupe `jobId = ${type}-${knowledgeId}` so re-publishing is a no-op. -- Finalizes failed jobs: the worker converts a handler error carrying `retryable === false` into a BullMQ `UnrecoverableError` (skips remaining attempts, since the pipeline already wrote terminal `FAILED`), and a `worker.on("failed")` handler promotes a transiently-`HALTED` knowledge to terminal `FAILED` via `knowledgeDb.promoteHaltedToFailed` once `attemptsMade >= attempts`. This authoritative "no more retries" signal guarantees HALTED can never get stuck. (OSS has no SSE — state-only, no retry event.) - -## Public Exports - -```ts -// Registers "bullmq" provider via side effect; no runtime exports. -import "@bb/queue-bullmq"; -``` - -## Invariants (preserved from the previous `@bb/queue`) - -1. **One BullMQ `Queue` per `JobType` constructed at `connect()`.** -2. **Queue prefix `"bb"`** to keep dev Redis collision-free with the kube-package reference's `"kp"`. -3. **`removeOnComplete: true`, `removeOnFail: false`, `attempts: 3`, `backoff: fixed/5000`** — unchanged. -4. **Workers close before queues** in `close()` so in-flight jobs finish before connections drop. -5. **Dedupe key is `${type}-${knowledgeId}`** — re-publishing returns the same `jobId`. - -## External Dependencies - -- `bullmq` (queue runtime) -- `@bb/redis` (shared ioredis options + lifecycle) -- `@bb/queue-core` (interface), `@bb/queue` (registry), `@bb/types`, `@bb/config`, `@bb/errors` - -## What is intentionally out of scope - -- The publisher entry-points (`enqueueGithubIndex` etc.) live in `@bb/queue`, not here. This package only implements `IQueueProvider.enqueueRaw`. -- DLQ inspection beyond `listFailedJobs()` (no auto-retry, no purge — surfaced via `bytebell ls --failed`). diff --git a/packages/queue-bullmq/package.json b/packages/queue-bullmq/package.json deleted file mode 100644 index 269db28..0000000 --- a/packages/queue-bullmq/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@bb/queue-bullmq", - "version": "0.0.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": "./src/index.ts" - }, - "imports": { - "#src/*": "./src/*" - }, - "dependencies": { - "@bb/config": "workspace:*", - "@bb/db": "workspace:*", - "@bb/errors": "workspace:*", - "@bb/logger": "workspace:*", - "@bb/queue": "workspace:*", - "@bb/queue-core": "workspace:*", - "@bb/redis": "workspace:*", - "@bb/types": "workspace:*", - "bullmq": "^5.76.3" - } -} diff --git a/packages/queue-bullmq/src/README.md b/packages/queue-bullmq/src/README.md deleted file mode 100644 index 7138e9a..0000000 --- a/packages/queue-bullmq/src/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# `@bb/queue-bullmq/src` — context - -Implementation of the BullMQ-over-Redis provider that registers itself with `@bb/queue` as `"bullmq"`. See [../README.md](../README.md) for the package-level contract. - -## Files - -- **[index.ts](index.ts)** — single side-effect import of `./provider.ts`. Importing the package as `import "@bb/queue-bullmq"` triggers provider registration. -- **[provider.ts](provider.ts)** — `BullmqQueueProvider` class implementing `IQueueProvider`. Owns the BullMQ `Queue` map, the registered `Worker[]`, the Redis lifecycle (`connectRedis` / `closeRedis` / `pingRedis`), the queue prefix (`"bb"`), and the default job options (`attempts: 3`, fixed 5s backoff, `removeOnComplete: true`, `removeOnFail: false`). Registered as `"bullmq"` via `registerQueueProvider` at module load. -- **[priority.ts](priority.ts)** — `mapBullmqPriority(JobPriority)` returns BullMQ's smaller-number-wins numeric priority (Low=1000, Normal=100, High=10). `dedupeKey(type, knowledgeId)` returns the stable `${type}-${knowledgeId}` BullMQ jobId. - -## Module dependency graph - -``` -priority.ts → @bb/types -provider.ts → bullmq, @bb/types, @bb/errors, @bb/redis, - @bb/queue (registerQueueProvider, defaultConcurrencyFor), - @bb/queue-core (IQueueProvider + shared types), - priority.ts -index.ts → provider.ts (side effect only) -``` - -No cycles. `priority.ts` is a leaf within the package. - -## Invariants enforced here - -- **One `Queue` per `JobType` constructed at `connect()`.** Three queues (`GithubIndex`, `GithubPull`, `LocalIngest`) — `BusinessContextProcessing` is a worker-only concern in OSS and is not in this provider's `ALL_JOB_TYPES`. -- **Queue prefix is `"bb"`.** Stays distinct from the kube-package reference's `"kp"` so a shared dev Redis can host both without key collisions. -- **`removeOnComplete: true`, `removeOnFail: false`, `attempts: 3`, `backoff: fixed 5s`** — unchanged from the original `@bb/queue/src/manager.ts`. -- **Workers close before queues** in `close()` so in-flight handlers finish before BullMQ's internal Redis connections drop. -- **Redis lifecycle is owned here.** `connect()` calls `connectRedis()`; `close()` calls `closeRedis()`; `ping()` calls `pingRedis()`. The server no longer touches Redis directly. -- **Dedupe key is stable.** Re-publishing the same `(type, knowledgeId)` returns the same `jobId` — BullMQ silently dedupes. -- **No env reads.** Redis URL comes via `@bb/redis.getRedisConnection()` (which reads `Config.RedisUrl`). Repo-wide ESLint rule blocks `process.env`. - -## Side-effect import - -The provider self-registers at module load. The server's composition root does: - -```ts -import "@bb/queue-bullmq"; // registers "bullmq" -import "@bb/queue-honker"; // registers "honker" (future) -// ... -await connectQueue(getConfigValue(Config.QueueProvider)); -``` - -Nothing in this package is exported by name; consumers go through the `@bb/queue` facade. diff --git a/packages/queue-bullmq/src/index.ts b/packages/queue-bullmq/src/index.ts deleted file mode 100644 index a424074..0000000 --- a/packages/queue-bullmq/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -import "./provider.ts"; diff --git a/packages/queue-bullmq/src/priority.ts b/packages/queue-bullmq/src/priority.ts deleted file mode 100644 index a1f4cf9..0000000 --- a/packages/queue-bullmq/src/priority.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { JobPriority, type JobType } from "@bb/types"; - -const PRIORITY_TO_BULLMQ: Record = { - [JobPriority.Low]: 1000, - [JobPriority.Normal]: 100, - [JobPriority.High]: 10, -}; - -export function mapBullmqPriority(priority: JobPriority): number { - return PRIORITY_TO_BULLMQ[priority]; -} - -export function dedupeKey(type: JobType, knowledgeId: string): string { - return `${type}-${knowledgeId}`; -} diff --git a/packages/queue-bullmq/src/provider.ts b/packages/queue-bullmq/src/provider.ts deleted file mode 100644 index a6dea6b..0000000 --- a/packages/queue-bullmq/src/provider.ts +++ /dev/null @@ -1,216 +0,0 @@ -// BullMQ-over-Redis implementation of `IQueueProvider`. Registers itself -// as the "bullmq" provider at module load; the server picks it up via -// `import "@bb/queue-bullmq"` (side effect) + `connectQueue("bullmq")`. - -import { Queue, UnrecoverableError, Worker, type Job } from "bullmq"; -import { JobType, type JobMessage, type PayloadFor } from "@bb/types"; -import { QueueConnectError, QueueNotConnectedError } from "@bb/errors"; -import { connectRedis, closeRedis, pingRedis, getRedisConnection } from "@bb/redis"; -import { defaultConcurrencyFor, registerQueueProvider } from "@bb/queue"; -import { knowledgeDb } from "@bb/db"; -import { logger } from "@bb/logger"; -import type { - FailedJob, - IQueueProvider, - JobHandler, - NormalizedEnqueueOptions, - QueuePingResult, - RemoveKnowledgeJobsResult, - WorkerRegistrationOptions, -} from "@bb/queue-core"; -import { dedupeKey, mapBullmqPriority } from "./priority.ts"; - -// Distinct from the kube-package reference's `"kp"` so a shared dev Redis can -// host both without key collisions. -const QUEUE_PREFIX = "bb"; - -const ALL_JOB_TYPES: readonly JobType[] = [JobType.GithubIndex, JobType.GithubPull, JobType.LocalIngest]; - -// Retain failed jobs (`removeOnFail: false`) so `listFailedJobs()` can surface -// them; drop completed jobs to keep Redis small. BullMQ tracks attempts -// internally — workers don't need to. -const DEFAULT_JOB_OPTIONS = { - removeOnComplete: true, - removeOnFail: false, - attempts: 3, - backoff: { type: "fixed", delay: 5000 }, -} as const; - -class BullmqQueueProvider implements IQueueProvider { - private queues = new Map(); - private workers: Worker[] = []; - - async connect(): Promise { - // The provider owns its substrate lifecycle (mirroring `@bb/sqlite` - // owning `connectSqlite`). The server's `main()` no longer calls - // `connectRedis()` directly. - await connectRedis(); - try { - // One `Queue` per job type. BullMQ spawns its own internal ioredis - // client per Queue from the options blob (see §10.3 of - // `docs/redis-and-queue.md`), so the connection object is shared - // shape, not a shared connection. - const connection = getRedisConnection(); - for (const type of ALL_JOB_TYPES) { - const queue = new Queue(type, { - connection, - prefix: QUEUE_PREFIX, - defaultJobOptions: DEFAULT_JOB_OPTIONS, - }); - this.queues.set(type, queue); - } - } catch (cause: unknown) { - this.queues.clear(); - throw cause instanceof QueueConnectError ? cause : new QueueConnectError(cause); - } - } - - async close(): Promise { - // Workers first so in-flight handlers finish before BullMQ's internal - // Redis connections drop. Reversing this order would corrupt active - // jobs (BLPOP commands would error mid-claim). - const ws = this.workers.splice(0); - await Promise.all(ws.map((w) => w.close())); - const qs = Array.from(this.queues.values()); - this.queues.clear(); - await Promise.all(qs.map((q) => q.close())); - await closeRedis(); - } - - async ping(): Promise { - return pingRedis(); - } - - async enqueueRaw( - type: T, - message: JobMessage>, - opts: NormalizedEnqueueOptions, - ): Promise { - const queue = this.requireQueue(type); - // Stable `jobId` is the load-bearing dedupe primitive: BullMQ silently - // drops a second `queue.add(...)` with the same id, so re-publishing - // the same `(type, knowledgeId)` is a no-op. - const jobId = dedupeKey(type, message.knowledgeId); - await queue.add(type, message, { - jobId, - priority: mapBullmqPriority(opts.priority), - }); - return jobId; - } - - registerWorker(type: T, handler: JobHandler, opts: WorkerRegistrationOptions = {}): void { - if (this.queues.size === 0) { - throw new QueueNotConnectedError(); - } - const concurrency = opts.concurrency ?? defaultConcurrencyFor(type); - const worker = new Worker( - type, - async (job: Job>>) => { - try { - await handler(job.data); - } catch (err) { - // Non-retryable failure (pipeline already wrote terminal FAILED) → - // BullMQ UnrecoverableError so it stops retrying. Transient failures - // re-throw unchanged and BullMQ applies attempts/backoff. - if (isNonRetryable(err)) { - throw new UnrecoverableError(err instanceof Error ? err.message : String(err)); - } - throw err; - } - }, - { - connection: getRedisConnection(), - prefix: QUEUE_PREFIX, - concurrency, - }, - ); - // When BullMQ exhausts all automatic retries, promote the transiently - // HALTED knowledge to terminal FAILED. This is the authoritative "no more - // retries" signal, so HALTED can never get stuck. UnrecoverableError - // failures are already terminal (pipeline wrote FAILED); skip them. - worker.on("failed", (job, err) => { - if (job === undefined) { - return; - } - const made = job.attemptsMade ?? 0; - const max = job.opts?.attempts ?? 1; - const knowledgeId = (job.data as JobMessage | undefined)?.knowledgeId; - if (made < max || err?.name === "UnrecoverableError" || typeof knowledgeId !== "string") { - return; - } - void knowledgeDb.promoteHaltedToFailed(knowledgeId).catch((cause: unknown) => { - logger.error( - `queue-bullmq: promoteHaltedToFailed failed for ${knowledgeId}: ${ - cause instanceof Error ? cause.message : String(cause) - }`, - ); - }); - }); - this.workers.push(worker); - } - - async removeKnowledgeJobs(knowledgeId: string): Promise { - let removed = 0; - for (const type of ALL_JOB_TYPES) { - const queue = this.queues.get(type); - if (queue === undefined) { - continue; - } - // The dedupe key is deterministic — direct lookup, no scan. - const jobId = dedupeKey(type, knowledgeId); - const job = await queue.getJob(jobId); - if (job === undefined || job === null) { - continue; - } - try { - await job.remove(); - removed += 1; - } catch { - // BullMQ throws when removing an `active` job — the worker holds it. - // Leave it; the handler is idempotent and will finish naturally. - } - } - return { removed }; - } - - async listFailedJobs(): Promise { - const out: FailedJob[] = []; - for (const [type, queue] of this.queues) { - const failed = await queue.getJobs(["failed"]); - for (const job of failed) { - out.push(normalizeFailed(type, job)); - } - } - return out; - } - - private requireQueue(type: JobType): Queue { - const q = this.queues.get(type); - if (q === undefined) { - throw new QueueNotConnectedError(); - } - return q; - } -} - -/** Duck-typed contract with the pipelines: `retryable === false` ⇒ terminal. */ -function isNonRetryable(err: unknown): boolean { - return typeof err === "object" && err !== null && (err as { retryable?: boolean }).retryable === false; -} - -function normalizeFailed(type: JobType, job: Job): FailedJob { - const data = job.data as JobMessage | undefined; - const knowledgeId = typeof data?.knowledgeId === "string" ? data.knowledgeId : ""; - const failedAtMs = typeof job.finishedOn === "number" ? job.finishedOn : Date.now(); - return { - id: String(job.id ?? ""), - type, - knowledgeId, - attempts: job.attemptsMade ?? 0, - failedAt: new Date(failedAtMs).toISOString(), - reason: job.failedReason ?? "", - payload: data?.payload ?? null, - }; -} - -registerQueueProvider("bullmq", () => new BullmqQueueProvider()); diff --git a/packages/queue-bullmq/tsconfig.json b/packages/queue-bullmq/tsconfig.json deleted file mode 100644 index 4ed0786..0000000 --- a/packages/queue-bullmq/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.json"] -} diff --git a/packages/queue-core/README.md b/packages/queue-core/README.md index ef231ad..fda171c 100644 --- a/packages/queue-core/README.md +++ b/packages/queue-core/README.md @@ -4,7 +4,7 @@ Provider-agnostic interfaces for the async job queue layer. ## Responsibilities -Defines the contract that every queue backend (BullMQ over Redis, Honker over SQLite, …) must implement. Contains no I/O — pure TypeScript interfaces and shared types. +Defines the contract that every queue backend must implement. `@bb/queue-honker` (Honker over SQLite) is the only implementation shipped today. Contains no I/O — pure TypeScript interfaces and shared types. ## Public Interfaces @@ -21,11 +21,11 @@ None. This package owns no data — it only describes shapes. ## Tier -Strategy. Consumed by `@bb/queue` (facade) and implemented by `@bb/queue-bullmq`, `@bb/queue-honker`. +Strategy. Consumed by `@bb/queue` (facade) and implemented by `@bb/queue-honker`. ## Invariants -1. **`enqueueRaw` returns a stable string `jobId`.** Providers may use their own internal IDs (BullMQ dedupe strings, Honker integers stringified). The facade stores this on the knowledge doc as `queueJobId`. +1. **`enqueueRaw` returns a stable string `jobId`.** Providers may use their own internal IDs (Honker integers stringified). The facade stores this on the knowledge doc as `queueJobId`. 2. **`registerWorker` is synchronous.** It schedules workers; it does not await their first claim. Workers are torn down by `close()`. 3. **`removeKnowledgeJobs` never throws on missing jobs.** Returns `{ removed: 0 }` if no live job is associated with the knowledgeId. 4. **`listFailedJobs` is read-only.** It does not move, retry, or remove rows. diff --git a/packages/queue-core/src/README.md b/packages/queue-core/src/README.md index 174ce0d..baeacb7 100644 --- a/packages/queue-core/src/README.md +++ b/packages/queue-core/src/README.md @@ -4,4 +4,4 @@ Provider-agnostic interfaces and shared types for the async job queue layer. ## Files -- **index.ts** — public interfaces (`IQueueProvider`, `JobHandler`) and shared types (`NormalizedEnqueueOptions`, `WorkerRegistrationOptions`, `QueuePingResult`, `FailedJob`, `RemoveKnowledgeJobsResult`). No implementation — every concrete provider (`@bb/queue-bullmq`, `@bb/queue-honker`, …) implements `IQueueProvider`. +- **index.ts** — public interfaces (`IQueueProvider`, `JobHandler`) and shared types (`NormalizedEnqueueOptions`, `WorkerRegistrationOptions`, `QueuePingResult`, `FailedJob`, `RemoveKnowledgeJobsResult`). No implementation — every concrete provider (`@bb/queue-honker`, …) implements `IQueueProvider`. diff --git a/packages/queue-core/src/index.ts b/packages/queue-core/src/index.ts index d863686..a579aa9 100644 --- a/packages/queue-core/src/index.ts +++ b/packages/queue-core/src/index.ts @@ -1,6 +1,5 @@ // Pure-interface package: defines the contract every queue substrate must -// satisfy (BullMQ-over-Redis today, Honker-over-SQLite today; anything else -// tomorrow). No implementation lives here — `@bb/queue` is the facade that +// satisfy (Honker-over-SQLite today; anything else tomorrow). No implementation lives here — `@bb/queue` is the facade that // holds the registry; the actual providers are sibling packages that register // themselves at module load via `registerQueueProvider`. @@ -44,7 +43,7 @@ export interface RemoveKnowledgeJobsResult { * Lifecycle: `connect()` → … work … → `close()`. The facade owns the * single-active-provider state; providers stay stateless across reconnects. * - * Semantics every provider must preserve (per `docs/redis-and-queue.md` §9): + * Semantics every provider must preserve: * - At-least-once delivery. * - Idempotent re-publish for the same `knowledgeId` — `enqueueRaw` returns * the existing job's id if one is already live. diff --git a/packages/queue-honker/README.md b/packages/queue-honker/README.md index 051721c..90944d6 100644 --- a/packages/queue-honker/README.md +++ b/packages/queue-honker/README.md @@ -11,7 +11,7 @@ Strategy. Registers itself with `@bb/queue` via side-effect import as the `"honk - Implements `IQueueProvider` by wrapping `@russellthehippo/honker-node` (Honker SQLite extension). - Owns the queue.db lifecycle: `connect()` opens the file and constructs `Queue` handles per `JobType`; `close()` aborts worker loops and closes the database. - Maps `JobPriority` (Low / Normal / High) to Honker's higher-number-wins ordering (1 / 100 / 1000). -- Emulates BullMQ's stable-jobId dedupe by querying `_honker_live` for an existing row whose payload's `knowledgeId` matches. +- Provides stable-jobId dedupe by querying `_honker_live` for an existing row whose payload's `knowledgeId` matches. - Cancels knowledge jobs by deleting matching rows from `_honker_live` in a transaction. - Runs a 30 s sweeper that calls `queue.sweepExpired()` per `JobType` to move retry-exhausted jobs into `_honker_dead`. @@ -24,7 +24,7 @@ import "@bb/queue-honker"; ## Configuration -- `Config.QueueDbPath` — path to the SQLite file. Defaults to `path.join(getBytebellHome(), "queue.db")` at boot. Set with `bytebell set queue-db `. +- `Config.QueueDbPath` — path to the SQLite file. Defaults to `path.join(getPlumblineHome(), "queue.db")` at boot. Set with `plumbline set queue-db `. - `Config.ConcurrencyGithub` (shared with all providers) — number of parallel worker loops per `JobType`. ## Worker loop @@ -50,6 +50,6 @@ import "@bb/queue-honker"; ## What is intentionally out of scope -- Cross-provider job migration. Switching `Config.QueueProvider` between `bullmq` and `honker` requires a cold cutover. The `@bb/queue` Orphan Resumer (planned, not yet implemented) handles `state === QUEUED` knowledge docs at boot. +- Cross-provider job migration. Switching `Config.QueueProvider` requires a cold cutover; the `@bb/queue` Orphan Resumer re-enqueues `state === QUEUED` knowledge docs at boot. - Explicit `_honker_dead` purge — exposed via the facade's `listFailedJobs()` for inspection only. - AbortSignal threading through handlers — see _lease-loss policy_ above. diff --git a/packages/queue-honker/src/README.md b/packages/queue-honker/src/README.md index f514ab1..f7d2bfc 100644 --- a/packages/queue-honker/src/README.md +++ b/packages/queue-honker/src/README.md @@ -7,7 +7,7 @@ Implementation of the Honker-over-SQLite provider that registers itself with `@b - **[index.ts](index.ts)** — single side-effect import of `./provider.ts`. Importing the package as `import "@bb/queue-honker"` triggers provider registration. - **[provider.ts](provider.ts)** — `HonkerQueueProvider` class implementing `IQueueProvider`. Owns the `Database` handle, the `Map`, the per-loop `AbortController[]` for worker shutdown, and the sweeper timer. Registered as `"honker"` via `registerQueueProvider` at module load. - **[priority.ts](priority.ts)** — `mapHonkerPriority(JobPriority)` returns Honker's higher-number-wins numeric priority (Low=1, Normal=100, High=1000). -- **[paths.ts](paths.ts)** — `resolveQueueDbPath()` returns `Config.QueueDbPath` (with `~/...` expanded to the home dir) if set, else `path.join(getBytebellHome(), "queue.db")`. +- **[paths.ts](paths.ts)** — `resolveQueueDbPath()` returns `Config.QueueDbPath` (with `~/...` expanded to the home dir) if set, else `path.join(getPlumblineHome(), "queue.db")`. - **[failed.ts](failed.ts)** — `normalizeFailed(row)` converts a raw `_honker_dead` row into the cross-provider `FailedJob` shape from `@bb/queue-core` (parses the TEXT JSON payload, extracts `knowledgeId`, converts `died_at` Unix seconds → ISO). ## Module dependency graph diff --git a/packages/queue-honker/src/paths.ts b/packages/queue-honker/src/paths.ts index 1ba0490..31545eb 100644 --- a/packages/queue-honker/src/paths.ts +++ b/packages/queue-honker/src/paths.ts @@ -1,14 +1,14 @@ import path from "node:path"; import { homedir } from "node:os"; import { Config } from "@bb/types"; -import { getBytebellHome, getConfigValue } from "@bb/config"; +import { getPlumblineHome, getConfigValue } from "@bb/config"; export function resolveQueueDbPath(): string { const configured = getConfigValue(Config.QueueDbPath); if (configured.length > 0) { return expandTilde(configured); } - return path.join(getBytebellHome(), "queue.db"); + return path.join(getPlumblineHome(), "queue.db"); } function expandTilde(p: string): string { diff --git a/packages/queue-honker/src/provider.ts b/packages/queue-honker/src/provider.ts index 0659708..13e30b2 100644 --- a/packages/queue-honker/src/provider.ts +++ b/packages/queue-honker/src/provider.ts @@ -29,7 +29,7 @@ import { normalizeFailed } from "./failed.ts"; const VISIBILITY_S = 300; const HEARTBEAT_EXTEND_S = 300; const HEARTBEAT_MS = 60_000; -// Match BullMQ's fixed-5s backoff. Without an explicit retry delay, a failed +// Fixed 5s backoff. Without an explicit retry delay, a failed // job would wait the full VISIBILITY_S (300s) before being re-claimed. const RETRY_DELAY_S = 5; const MAX_ATTEMPTS = 3; @@ -119,7 +119,7 @@ class HonkerQueueProvider implements IQueueProvider { ): Promise { const db = this.requireDb(); const queue = this.requireQueue(type); - // Honker has no native stable-jobId concept (BullMQ's dedupe primitive), + // Honker has no native stable-jobId concept to dedupe on, // so we emulate it by querying `_honker_live` for an existing row with // matching knowledgeId. If found, return its id — re-publishing the // same logical job is a no-op (per the IQueueProvider contract). @@ -141,7 +141,7 @@ class HonkerQueueProvider implements IQueueProvider { const queue = this.requireQueue(type); const concurrency = opts.concurrency ?? defaultConcurrencyFor(type); // N independent loops with batch_size=1 each — prevents head-of-line - // blocking from a slow job in the same claim batch. Matches BullMQ's + // blocking from a slow job in the same claim batch. Gives the usual // `concurrency` semantics (N jobs in flight per type). for (let i = 0; i < concurrency; i++) { const workerId = `${type}-${i}-${process.pid}`; @@ -203,7 +203,7 @@ class HonkerQueueProvider implements IQueueProvider { const reason = describeError(err); logger.error(`queue-honker: job=${job.id} handler threw: ${reason}; scheduling retry`); // `retry(delayS, error)` increments `attempts` and re-queues with a - // 5s delay (matches BullMQ parity). After MAX_ATTEMPTS exhaustions, + // 5s delay. After MAX_ATTEMPTS exhaustions, // the next `sweepExpired()` tick moves the row to `_honker_dead`. if (ownsLease) { job.retry(RETRY_DELAY_S, reason); diff --git a/packages/queue/README.md b/packages/queue/README.md index 806ffdc..b08b6da 100644 --- a/packages/queue/README.md +++ b/packages/queue/README.md @@ -4,8 +4,9 @@ Strategy. May depend on Kernel (`@bb/types` for job enums + payloads, `@bb/errors` for typed error classes) and Infrastructure (`@bb/config` for -concurrency settings, `@bb/redis` for the BullMQ-compatible connection -options blob, `@bb/mongo` for the knowledge-state transition on enqueue). +concurrency settings, `@bb/db` for the knowledge-state transition on +enqueue). The concrete backend lives behind `@bb/queue-core`'s +`IQueueProvider` — today `@bb/queue-honker` (Honker over SQLite). May be imported by Domain (`@bb/ingest-github` will register workers) and Binaries (`@bb/server` will call publishers from HTTP routes). Never by `@bb/cli`. @@ -14,22 +15,22 @@ Binaries (`@bb/server` will call publishers from HTTP routes). Never by The package owns: -- BullMQ `Queue` lifecycle — one `Queue` per `JobType`, instantiated at - boot via `connectQueue()` and torn down via `closeQueue()` +- Provider lifecycle — `connectQueue(name)` selects and connects the + registered `IQueueProvider`; `closeQueue()` tears it down - The two GitHub publishers — `enqueueGithubIndex` and `enqueueGithubPull` -- Worker registration — `registerWorker(type, handler, opts?)` constructs - a typed BullMQ `Worker` from the same connection options -- The `JobMessage` envelope shape carried as BullMQ `job.data` -- Dedupe-key convention `${type}-${knowledgeId}` and BullMQ priority - mapping (`Low=1000, Normal=100, High=10`) -- The Mongo `status.state → QUEUED` write that accompanies a successful - enqueue (delegated to `@bb/mongo.setKnowledgeState`) +- Worker registration — `registerWorker(type, handler, opts?)`, delegated + to the active provider +- The `JobMessage` envelope shape carried as the job payload +- Dedupe-key convention `${type}-${knowledgeId}`; per-provider priority + mapping lives in the provider package +- The `status.state → QUEUED` write that accompanies a successful + enqueue (delegated to `@bb/db.knowledgeDb.setKnowledgeState`) The package does **not** own: - Worker handler implementations (live in `@bb/ingest-*`) - Knowledge-document creation, deletion, or any mutation other than the - state transition on enqueue (`@bb/mongo`) + state transition on enqueue (`@bb/db`) - Recovery / restart / orphan re-enqueue (deferred — see _Out of scope_) - Progress reporting, status validation, batch processing, admin pause/resume/cancel (all out of scope for OSS v0) @@ -61,47 +62,44 @@ type JobHandler = (msg: JobMessage>) => Promise ## Data ownership -The `Map` and the array of registered `Worker` instances. -No knowledge of payload semantics beyond the type contract from -`@bb/types`. No state in Mongo is owned by this package — the -`status.state → QUEUED` write is a delegated call into `@bb/mongo`. +The active provider handle and the registry of provider factories. No +knowledge of payload semantics beyond the type contract from +`@bb/types`. No document-store state is owned by this package — the +`status.state → QUEUED` write is a delegated call into `@bb/db`. ## Invariants -1. **Mongo write before BullMQ publish.** Each publisher calls - `setKnowledgeState(_, QUEUED)` first, then `queue.add(...)`. If Mongo - succeeds and BullMQ fails, both operations are idempotent under retry - (BullMQ dedupes by `jobId`, `setKnowledgeState` is a same-value set). - Reverse ordering would race the worker against a stale `CREATED` state. +1. **Document-store write before the queue publish.** Each publisher calls + `setKnowledgeState(_, QUEUED)` first, then enqueues. If the write + succeeds and the enqueue fails, both operations are idempotent under + retry (the provider dedupes by stable job id, `setKnowledgeState` is a + same-value set). Reverse ordering would race the worker against a stale + `CREATED` state. 2. **Connection is required.** Calling any publisher or `registerWorker` before `connectQueue()` throws `QueueNotConnectedError`. `closeQueue()` is graceful and re-entrant. -3. **One queue per `JobType`.** The `Map` is the single - source of truth; consumers obtain a `Queue` via the internal - `_getQueue(type)` accessor only. +3. **One provider active at a time.** `connectQueue()` is a cold cutover; + `closeQueue()` must run before re-connecting under another name. 4. **Workers close before queues.** `closeQueue()` awaits all worker - `close()` first so they finish in-flight jobs before queues drop their - redis connections. + `close()` first so they finish in-flight jobs before queues release + their handles. 5. **Dedupe key is stable.** `${type}-${knowledgeId}` — re-publishing the same logical job is a no-op; both calls return the same `jobId` string. -6. **Queue prefix is `"bb"`.** Distinct from the kube-package reference's - `"kp"` so a shared dev redis can host both without key collisions. -7. **Priority mapping is fixed.** `Low→1000`, `Normal→100`, `High→10`. - BullMQ uses smaller-number-wins; this maps the public 3-level enum to - that ordering. +6. **Priority is a fixed 3-level enum.** `Low` / `Normal` / `High`; each + provider maps it onto its own ordering (see `@bb/queue-honker`). ## External dependencies -- `bullmq` — queue runtime -- `@bb/types`, `@bb/errors`, `@bb/config`, `@bb/redis`, `@bb/mongo` — - workspace deps (all explicit in `package.json`) +- `@bb/types`, `@bb/errors`, `@bb/config`, `@bb/queue-core`, `@bb/db` — + workspace deps (all explicit in `package.json`). The queue runtime + itself is the active provider's dependency, not this package's. ## What is intentionally out of scope (v0) - Recovery / orphan re-enqueue on startup - Progress reporting and node-status state machine - Admin operations (pause / resume / cancel / inspect) -- Health monitor (covered by `pingMongo` + `pingRedis` at the server) +- Health monitor (covered by `pingDb` + `pingQueue` at the server) - Bitbucket / PDF / Website / Custom-context publishers — OSS is GitHub-only - `GITHUB_REINDEX_FILES` partial-reindex job type @@ -109,9 +107,9 @@ No knowledge of payload semantics beyond the type contract from - Pre-enqueue knowledge-state assertion (caller ensures the doc exists; unconditional state set is fine) - LLM credentials in the job payload — workers read OpenRouter key/model - from `~/.bytebell/config.json` at handler time -- `gitToken` encryption — flows through redis in plaintext; acceptable - for local single-tenant. Document at deployment time. + from `~/.plumbline/config.json` at handler time +- `gitToken` encryption — stored in the queue payload in plaintext; + acceptable for local single-tenant. Document at deployment time. ## How to extend diff --git a/packages/queue/src/README.md b/packages/queue/src/README.md index ae1f6c9..b6fa26a 100644 --- a/packages/queue/src/README.md +++ b/packages/queue/src/README.md @@ -6,66 +6,66 @@ package-level contract; this file documents how the source tree is split. ## Files - **[index.ts](index.ts)** — public re-exports. The only entry point other - packages may import. Exposes `connectQueue` / `closeQueue`, - `enqueueGithubIndex` / `enqueueGithubPull`, `registerWorker`, and the - associated types. Anything not re-exported here is internal. -- **[manager.ts](manager.ts)** — module-scoped `Map` plus - the registered `Worker[]` and the `connecting` promise. Owns the queue - lifecycle (`connectQueue`, `closeQueue`), the queue prefix (`"bb"`), the - default job options (`attempts: 3`, fixed 5s backoff, removeOnComplete), - and the internal accessors (`_getQueue`, `_registerWorker`, - `_isConnected`). Throws `QueueNotConnectedError` when accessed before - connect; `QueueConnectError` if BullMQ construction fails. -- **[envelope.ts](envelope.ts)** — pure helpers: `buildJobMessage(type, + packages may import. Anything not re-exported here is internal. +- **[registry.ts](registry.ts)** — the provider registry and facade core. + `registerQueueProvider(name, factory)` (called by a provider package at + import time), `connectQueue(name)` / `closeQueue()` / `pingQueue()`, and + the internal `getQueue()` accessor, which throws + `QueueNotConnectedError` before connect. +- **[envelope.ts](envelope.ts)** — pure helper: `buildJobMessage(type, priority, payload)` constructs the `JobMessage

` envelope (UUID v4 id, - `attempt: 0`, ISO timestamp); `mapPriority(JobPriority)` returns the - BullMQ numeric priority; `dedupeKey(type, knowledgeId)` returns the - `${type}-${knowledgeId}` BullMQ jobId. + `attempt: 0`, ISO timestamp). +- **[concurrency.ts](concurrency.ts)** — `defaultConcurrencyFor(type)`, + reading `Config.ConcurrencyGithub` for GitHub job types. - **[github-index.ts](github-index.ts)** — `enqueueGithubIndex` publisher. - Mongo write first (`setKnowledgeState(_, QUEUED)`), then BullMQ publish. - Also exports `EnqueueOptions` (shared with `github-pull.ts`). + Document-store write first (`setKnowledgeState(_, QUEUED)`), then the + provider enqueue. Also exports `EnqueueOptions` (shared with the others). - **[github-pull.ts](github-pull.ts)** — `enqueueGithubPull` publisher. Same ordering and structure as the index publisher. -- **[workers.ts](workers.ts)** — `registerWorker(type, handler, opts?)` - constructs a BullMQ `Worker` with the same connection options - (`getRedisConnection()`) and the same prefix as queues. Default - concurrency falls back to `getConfigValue(Config.ConcurrencyGithub)` for - GitHub job types. +- **[local-ingest.ts](local-ingest.ts)** — `enqueueLocalIngest` publisher. +- **[workers.ts](workers.ts)** — `registerWorker(type, handler, opts?)`, + delegated to the active provider. +- **[cancel.ts](cancel.ts)** — `removeKnowledgeJobs(knowledgeId)`. +- **[failed.ts](failed.ts)** — `listFailedJobs()` over the dead-letter set. +- **[resumer.ts](resumer.ts)** — `resumeOrphans()`, re-enqueuing knowledge + docs left in `QUEUED` at boot. ## Module dependency graph ``` -manager.ts → bullmq, @bb/types, @bb/errors, @bb/redis (getRedisConnection) -envelope.ts → @bb/types -github-index.ts → manager.ts, envelope.ts, @bb/types, @bb/mongo -github-pull.ts → manager.ts, envelope.ts, github-index.ts (EnqueueOptions), - @bb/types, @bb/mongo -workers.ts → bullmq, manager.ts, @bb/types, @bb/errors, @bb/config, @bb/redis -index.ts → re-exports the public surface +registry.ts → @bb/errors, @bb/queue-core +envelope.ts → @bb/types +concurrency.ts → @bb/types, @bb/config +github-index.ts → registry.ts, envelope.ts, @bb/types, @bb/db +github-pull.ts → registry.ts, envelope.ts, github-index.ts (EnqueueOptions), + @bb/types, @bb/db +local-ingest.ts → registry.ts, envelope.ts, github-index.ts (EnqueueOptions), @bb/types +workers.ts → registry.ts, @bb/types, @bb/queue-core +cancel.ts → registry.ts, @bb/queue-core +failed.ts → registry.ts, @bb/queue-core +resumer.ts → github-index.ts, @bb/types, @bb/db, @bb/logger +index.ts → re-exports the public surface ``` -No cycles. `manager.ts` and `envelope.ts` are leaves within the package -(no intra-package imports). Publishers depend on both. `workers.ts` -depends only on `manager.ts`. +No cycles. `registry.ts`, `envelope.ts` and `concurrency.ts` are leaves +within the package (no intra-package imports). Publishers depend on +`registry.ts` + `envelope.ts`; `workers.ts`, `cancel.ts` and `failed.ts` +depend only on `registry.ts`. ## Invariants enforced here -- **Connect is idempotent and concurrent-safe.** `connectQueue()` short- - circuits if `queues.size > 0`; concurrent callers await the same - in-flight `connecting` promise. -- **Close is graceful and ordered.** `closeQueue()` awaits worker - `close()` first (so handlers finish), then awaits queue `close()` - (which closes BullMQ's internal redis connections). -- **Mongo before BullMQ on enqueue.** Both publishers do - `setKnowledgeState(_, QUEUED)` then `queue.add(...)`. The ordering is +- **One provider active at a time.** `connectQueue()` is a cold cutover; + `closeQueue()` must run before re-connecting under another name. +- **Close is graceful and ordered.** The provider's `close()` finishes + in-flight handlers before releasing its queue handles. +- **Document-store write before the enqueue.** Every publisher does + `setKnowledgeState(_, QUEUED)` then enqueues. The ordering is load-bearing — see [../README.md](../README.md) _Invariants_. -- **No raw `Queue` leak.** `_getQueue` is not in `index.ts`. Future - publishers live in this folder and use the internal accessor; consumers - in higher tiers see only the typed publisher signatures. -- **No env reads.** The redis URL is sourced via - `@bb/redis.getRedisConnection()` (which itself reads - `getConfigValue(Config.RedisUrl)`). Repo-wide ESLint rule blocks - `process.env`. +- **No raw provider leak.** Consumers in higher tiers see only the typed + publisher signatures; new publishers live in this folder and use + `getQueue()`. +- **No env reads.** All settings come from `@bb/config`. A repo-wide + ESLint rule blocks `process.env`. - **Errors carry typed metadata.** Construction sites use the catalog in `@bb/errors` — never inline `new Error(string)`. `QueueConnectError` carries the underlying `cause`; `QueueNotConnectedError` is a marker. diff --git a/packages/queue/src/registry.ts b/packages/queue/src/registry.ts index 03dcb36..f53088f 100644 --- a/packages/queue/src/registry.ts +++ b/packages/queue/src/registry.ts @@ -1,7 +1,7 @@ // Provider registry — the facade core. Mirrors the `@bb/db` / `@bb/graph-db` -// pattern: side-effect imports of provider packages (`@bb/queue-bullmq`, -// `@bb/queue-honker`) call `registerQueueProvider(name, factory)` at module -// load. The server then picks one with `connectQueue(Config.QueueProvider)`. +// pattern: a side-effect import of a provider package (`@bb/queue-honker`) +// calls `registerQueueProvider(name, factory)` at module load. The server then +// picks one with `connectQueue(Config.QueueProvider)`. // // Switching providers is a cold cutover — only one is active at a time; // `closeQueue()` must be called before re-connecting under a different name. diff --git a/packages/queue/src/resumer.ts b/packages/queue/src/resumer.ts index 1fcf7d2..94817a1 100644 --- a/packages/queue/src/resumer.ts +++ b/packages/queue/src/resumer.ts @@ -3,7 +3,7 @@ // // If the server crashes (kill -9, OOM, SIGKILL) between those two steps, // the knowledge doc is left in `QUEUED` state with no corresponding live -// job. Workers never claim it; the user sees a stuck row in `bytebell ls`. +// job. Workers never claim it; the user sees a stuck row in `plumbline ls`. // // On boot we scan `@bb/db` for any knowledge doc in `state === QUEUED` and // re-publish it via the appropriate `enqueue*` publisher. The provider's diff --git a/packages/redis/README.md b/packages/redis/README.md deleted file mode 100644 index 25ad9ef..0000000 --- a/packages/redis/README.md +++ /dev/null @@ -1,121 +0,0 @@ -# `@bb/redis` — context - -## Tier - -Infrastructure. May depend on Kernel (`@bb/types` for the `Config` enum, -`@bb/errors` for typed error classes) and on infra siblings explicitly -listed in `package.json` (`@bb/config` for `Config.RedisUrl`). May be -imported by Strategy (`@bb/queue` — the primary consumer), Domain, and -Binaries — never by `@bb/cli` (CLI talks HTTP only). - -## Responsibility - -The package owns: - -- A single shared `ioredis` client (lazy, idempotent connect; graceful - close) -- A health probe (`pingRedis`) backed by the active connection -- An internal `_getRedis()` accessor that future consumers (chiefly - `@bb/queue`) will compose against - -The package does **not** own: - -- BullMQ Queue / Worker / QueueEvents construction (`@bb/queue`) -- Caching helpers, dedupe sets, distributed locks (deferred — see - _How to extend_) -- Pub/sub helpers (deferred — BullMQ manages its own internal pub/sub - clients from the connection options) -- Telemetry, logging, retry policies (the driver handles transport retries) - -## Public exports - -```ts -function connectRedis(): Promise; -function closeRedis(): Promise; -function pingRedis(): Promise; -function getRedisConnection(): RedisConnectionOptions; - -interface PingResult { - ok: boolean; - latencyMs: number; -} -interface RedisConnectionOptions { - host: string; - port: number; - password?: string; - username?: string; - db: number; - tls?: object; - maxRetriesPerRequest: null; - enableReadyCheck: false; -} -``` - -`getRedisConnection()` returns a BullMQ-compatible options blob (parsed -from `Config.RedisUrl`) for `@bb/queue` to construct its own `Queue` and -`Worker` instances — BullMQ instantiates separate internal redis clients -per Queue/Worker rather than reusing a single shared client. The shared -client owned by this package remains for `pingRedis()` and any future -non-queue redis use. - -`_getRedis()` is **internal** — consumed only by helpers inside this -package. Higher tiers cannot reach a raw `Redis` handle today. - -## Data ownership - -The single shared `ioredis` client. No knowledge of queue keys, cache keys, -or any application-level redis schema lives here. - -## Invariants - -1. **No env reads.** The Redis URL comes from - `getConfigValue(Config.RedisUrl)`. No `process.env`, no `.env`, no - fallback. Enforced repo-wide by - [eslint.config.mjs:71-94](../../eslint.config.mjs#L71-L94). -2. **`connectRedis()` is idempotent and concurrent-safe.** Repeated calls - return the existing client; concurrent calls await the same in-flight - connect promise. -3. **`closeRedis()` is graceful.** Clears the cached client before awaiting - `client.quit()` so a subsequent `connectRedis()` cleanly re-establishes. -4. **Errors are typed, not strings.** `RedisConfigError` carries the exact - `bytebell set …` hint; `RedisConnectError` redacts userinfo in the URL. -5. **No raw `Redis` leaks.** `_getRedis()` is not in `src/index.ts`. The - only way higher tiers touch redis is through helpers exported from this - package. -6. **BullMQ-compatible options.** The shared client is constructed with - `maxRetriesPerRequest: null` and `enableReadyCheck: false` so that - `@bb/queue`'s blocking workers (which use BLPOP-family commands) work - correctly. This is deliberate: the queue is the dominant consumer per - [docs/arch.md:74-76](../../docs/arch.md#L74-L76). - -## External dependencies - -- `ioredis` — the standard BullMQ-compatible redis client -- `@bb/config` — workspace dep, only for `getConfigValue(Config.RedisUrl)` -- `@bb/types` — workspace dep, for the `Config` enum -- `@bb/errors` — workspace dep, for `RedisConfigError`, - `RedisConnectError`, `RedisNotConnectedError` - -No logger, no telemetry, no Mongo, no Neo4j. This package boots after -`@bb/config` and before everything that needs the queue. - -## What is intentionally out of scope (v0) - -- BullMQ Queue/Worker/QueueEvents construction (`@bb/queue`) -- Caching helpers / typed key-value wrappers -- Distributed locks -- Dedicated subscriber / publisher clients (BullMQ creates its own - internally; non-queue pub/sub consumers do not exist yet) -- Application-level retry / backoff / timeout tuning (ioredis defaults - apply; revisit if the queue shows flakiness) - -## How to extend - -Adding a typed cache helper (`cacheGet` / `cacheSet`) when a non-queue -consumer arrives: - -1. Create `src/cache/.ts`. -2. Use `_getRedis()` to obtain the client; never expose the raw `Redis` - handle to callers. -3. Re-export the helper (and any new public types) from `src/index.ts`. -4. Update this `README.md`. diff --git a/packages/redis/package.json b/packages/redis/package.json deleted file mode 100644 index 3aeccf2..0000000 --- a/packages/redis/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@bb/redis", - "version": "0.0.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": "./src/index.ts" - }, - "imports": { - "#src/*": "./src/*" - }, - "dependencies": { - "@bb/config": "workspace:*", - "@bb/errors": "workspace:*", - "@bb/types": "workspace:*", - "ioredis": "^5.10.1" - } -} diff --git a/packages/redis/src/README.md b/packages/redis/src/README.md deleted file mode 100644 index 6e40f30..0000000 --- a/packages/redis/src/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# `@bb/redis/src` — context - -Implementation of `@bb/redis`. See [../README.md](../README.md) for the -package-level contract; this file documents how the source tree is split. - -## Files - -- **[index.ts](index.ts)** — public re-exports. The only entry point other - packages may import. Exposes `connectRedis`, `closeRedis`, `pingRedis`, - `getRedisConnection`, and the `PingResult` / `RedisConnectionOptions` - types. Anything not re-exported here is internal. -- **[client.ts](client.ts)** — module-scoped `Redis` singleton plus the - lifecycle (`connectRedis`, `closeRedis`), the health probe (`pingRedis`), - the BullMQ-options provider (`getRedisConnection`), and the **internal** - `_getRedis()` accessor. Reads the URL via - `getConfigValue(Config.RedisUrl)` from `@bb/config` + `@bb/types`. Throws - typed errors from `@bb/errors` (`RedisConfigError`, `RedisConnectError`, - `RedisNotConnectedError`). Also exposes `__resetForTests()` — test seam - only, never imported by production code. - -## Module dependency graph - -``` -client.ts → ioredis, @bb/config (getConfigValue), @bb/types (Config), - @bb/errors (Redis* error classes) -index.ts → re-exports the public surface from client.ts -``` - -No cycles, no intra-package leaves yet — `client.ts` is the only -implementation file. - -## Invariants enforced here - -- **Connect is idempotent and concurrent-safe.** `connectRedis()` - short-circuits if `client !== null`; concurrent callers await the same - in-flight `connecting` promise so a single connect is performed. -- **Close is graceful and re-entrant.** `closeRedis()` clears the cached - client before awaiting `client.quit()` so a subsequent `connectRedis()` - cleanly re-establishes; calling `closeRedis()` twice is a no-op. -- **Lazy connect.** The `ioredis` client is constructed with - `lazyConnect: true` and explicitly `await client.connect()`-ed inside - `doConnect()`. This makes connect failures surface as a rejected promise - from `connectRedis()` rather than as an uncaught `"error"` event on the - client. -- **BullMQ-compatible defaults.** `maxRetriesPerRequest: null` and - `enableReadyCheck: false` are baked in so that `@bb/queue`'s blocking - workers do not need to override them. -- **No raw `Redis` leak.** `_getRedis()` is not in `index.ts`. Future - helpers (cache wrappers, queue-connection accessor) will live in this - folder and compose `_getRedis()` internally; consumers in higher tiers - see only the typed helper signatures. -- **No env reads.** Only `getConfigValue(Config.RedisUrl)` provides the - URL. Repo-wide ESLint rule blocks `process.env`. -- **Errors carry typed metadata.** Construction sites use the catalog in - `@bb/errors` — never inline `new Error(string)`. `RedisConfigError` - carries the exact `bytebell set …` hint; `RedisConnectError` redacts - userinfo in the URL before composing the message. - -## Adding a helper - -Follow the recipes in [../README.md](../README.md) under _How to extend_. -Cache helpers (e.g. `cacheGet` / `cacheSet`) live as flat files in -`src/.ts` (the repo's ESLint rule forbids parent traversal, so -subdirectories require import gymnastics — keep `src/` flat unless the -package outgrows it). Compose `_getRedis()`; never expose the raw `Redis` -handle. diff --git a/packages/redis/src/client.ts b/packages/redis/src/client.ts deleted file mode 100644 index e34f4ec..0000000 --- a/packages/redis/src/client.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Redis } from "ioredis"; -import { getConfigValue } from "@bb/config"; -import { Config } from "@bb/types"; -import { RedisConfigError, RedisConnectError, RedisNotConnectedError } from "@bb/errors"; - -export interface PingResult { - ok: boolean; - latencyMs: number; -} - -export interface RedisConnectionOptions { - host: string; - port: number; - password: string | undefined; - username: string | undefined; - db: number; - tls: object | undefined; - maxRetriesPerRequest: null; - enableReadyCheck: false; -} - -let client: Redis | null = null; -let connecting: Promise | null = null; - -export async function connectRedis(): Promise { - if (client !== null) { - return; - } - if (connecting !== null) { - return connecting; - } - connecting = doConnect().finally(() => { - connecting = null; - }); - return connecting; -} - -async function doConnect(): Promise { - const url = getConfigValue(Config.RedisUrl); - if (url.length === 0) { - throw new RedisConfigError("bytebell set redis "); - } - const next = new Redis(url, { - maxRetriesPerRequest: null, - enableReadyCheck: false, - lazyConnect: true, - }); - try { - await next.connect(); - } catch (cause: unknown) { - next.disconnect(); - throw new RedisConnectError(url, cause); - } - client = next; -} - -export async function closeRedis(): Promise { - if (client === null) { - return; - } - const c = client; - client = null; - await c.quit(); -} - -export async function pingRedis(): Promise { - const c = _getRedis(); - const start = performance.now(); - try { - await c.ping(); - return { ok: true, latencyMs: Math.round(performance.now() - start) }; - } catch { - return { ok: false, latencyMs: Math.round(performance.now() - start) }; - } -} - -export function getRedisConnection(): RedisConnectionOptions { - const url = getConfigValue(Config.RedisUrl); - if (url.length === 0) { - throw new RedisConfigError("bytebell set redis "); - } - const parsed = new URL(url); - const port = parsed.port.length > 0 ? Number(parsed.port) : 6379; - const dbPath = parsed.pathname.length > 1 ? parsed.pathname.slice(1) : ""; - return { - host: parsed.hostname, - port, - password: parsed.password.length > 0 ? decodeURIComponent(parsed.password) : undefined, - username: parsed.username.length > 0 ? decodeURIComponent(parsed.username) : undefined, - db: dbPath.length > 0 ? Number(dbPath) : 0, - tls: parsed.protocol === "rediss:" ? {} : undefined, - maxRetriesPerRequest: null, - enableReadyCheck: false, - }; -} - -export function _getRedis(): Redis { - if (client === null) { - throw new RedisNotConnectedError(); - } - return client; -} - -export function __resetForTests(): void { - client = null; - connecting = null; -} diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts deleted file mode 100644 index 231c9bb..0000000 --- a/packages/redis/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { connectRedis, closeRedis, pingRedis, getRedisConnection } from "./client.ts"; -export type { PingResult, RedisConnectionOptions } from "./client.ts"; diff --git a/packages/redis/tsconfig.json b/packages/redis/tsconfig.json deleted file mode 100644 index 4ed0786..0000000 --- a/packages/redis/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.json"] -} diff --git a/packages/server/README.md b/packages/server/README.md index af0500a..00376ad 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -3,34 +3,34 @@ ## Tier Binary (deployable). Sibling to `@bb/cli`. Imports everything below the -binary tier — `@bb/types`, `@bb/errors`, `@bb/config`, `@bb/mongo`, -`@bb/redis`, `@bb/neo4j`, `@bb/queue`, `@bb/ingest-github`. Never +binary tier — `@bb/types`, `@bb/errors`, `@bb/config`, `@bb/db`, `@bb/sqlite`, +`@bb/neo4j`, `@bb/queue`, `@bb/ingest-github`. Never imports `@bb/cli`. ## Responsibility -Single-process Express daemon backing the `bytebell` TUI. Per +Single-process Express daemon backing the `plumbline` TUI. Per [docs/arch.md:53-62](../../docs/arch.md#L53-L62): boots all infra in -order, registers BullMQ workers in-process, exposes a small JSON HTTP +order, registers queue workers in-process, exposes a small JSON HTTP surface on `127.0.0.1`, single-tenant, no auth. The package owns: -- Boot sequence — `loadConfig` validation → `connectMongo` → - `connectRedis` → `connectNeo4j` → `ensureKnowledgeIndexes` → +- Boot sequence — `loadConfig` validation → `connectDb` → + `connectGraph` → `ensureKnowledgeIndexes` → `connectQueue` → `registerGithubWorkers` → `registerLocalIngestWorker` → `app.listen` → write - `~/.bytebell/pid`. -- HTTP routes: `GET /health` (pings Mongo + Redis + Neo4j), + `~/.plumbline/pid`. +- HTTP routes: `GET /health` (pings the document store + queue + graph), `POST /api/v1/github/index`, `POST /api/v1/local/index`, `GET /api/v1/repos`, plus the MCP routes (`POST|GET|DELETE /mcp`, `GET /sse`, `POST /sse/messages`) registered by `@bb/mcp`'s `mountMcp(app)` after the JSON routes. -- Knowledge-doc creation in **both** Mongo and Neo4j on each ingest +- Knowledge-doc creation in **both** SQLite and Neo4j on each ingest request — `upsertKnowledge` + `upsertKnowledgeNode` run before the publisher transitions state to `QUEUED`. - Filtered recursive copy for local ingest (`copyRepo.ts`) — files end - up at `~/.bytebell/local-snapshots//` (separate from the + up at `~/.plumbline/local-snapshots//` (separate from the commit-scoped `orgs/` tree where analysed artifacts live). The snapshot freezes the user-supplied directory at submission time so edits made during the worker run don't bleed into ingestion. MCP @@ -45,15 +45,15 @@ The package owns: dirs remain that back a live knowledge but can't be migrated (missing `commitId` / `repoUrl`) — those carry data the server won't silently destroy. - Graceful shutdown — SIGTERM/SIGINT → drain MCP sessions - (`closeAllMcpSessions`) → close queue → close redis → close neo4j → - close mongo → unlink `~/.bytebell/pid` → exit. MCP sessions drain + (`closeAllMcpSessions`) → close queue → close graph → close the + document store → unlink `~/.plumbline/pid` → exit. MCP sessions drain first so in-flight Streamable HTTP / SSE transports release before - the BullMQ worker shuts down its Redis connection. + the queue worker shuts down. The package does **not** own: - Worker handler bodies (those live in `@bb/ingest-github`) -- Knowledge / Raw schema (lives in `@bb/types` + `@bb/mongo`) +- Knowledge / Raw schema (lives in `@bb/types` + `@bb/db-core`) - Auth / rate-limit / CORS — single-tenant, localhost-only ## Public exports @@ -62,7 +62,7 @@ The package does **not** own: entry in `package.json`: ```jsonc -{ "bin": { "bytebell-server": "./src/index.ts" } } +{ "bin": { "plumbline-server": "./src/index.ts" } } ``` The TypeScript exports (route builders, shutdown installer) are @@ -71,7 +71,7 @@ The TypeScript exports (route builders, shutdown installer) are ## Routes ``` -GET /health → 200 { status: "ok", mongo, redis, neo4j } +GET /health → 200 { status: "ok", db, queue, graph } → 503 { status: "down", … } POST /api/v1/github/index body: { repoUrl, branch?, gitToken? } @@ -96,9 +96,9 @@ POST /sse/messages?sessionId=… legacy SSE messages — owned by @b ## Data ownership -- `~/.bytebell/pid` — written at boot (mode `0644`), removed on graceful +- `~/.plumbline/pid` — written at boot (mode `0644`), removed on graceful shutdown. Stale PID file is the signal that an earlier run crashed. -- `~/.bytebell/local-snapshots//` — populated by the +- `~/.plumbline/local-snapshots//` — populated by the local-ingest route's `copyRepo`. Frozen snapshot of the user's uploaded directory at submission time. Persisted across job retries; never auto-deleted. GitHub ingestion does not use this dir — it @@ -110,12 +110,12 @@ POST /sse/messages?sessionId=… legacy SSE messages — owned by @b 1. **Bind localhost only.** `app.listen(port, "127.0.0.1")`. v0 OSS is single-tenant local — no remote connections. 2. **Boot fails fast on missing config.** Required keys are - `Config.MongoUri`, `Config.RedisUrl`, `Config.Neo4jUri`, + `Config.SqlitePath`, `Config.QueueDbPath`, `Config.Neo4jUri`, `Config.Neo4jUser`, `Config.Neo4jPassword`, `Config.OpenrouterApiKey`. Missing → `ServerConfigError` with the - matching `bytebell set …` hints, exit 1. + matching `plumbline set …` hints, exit 1. 3. **Knowledge doc creation precedes enqueue and is dual-written.** Each - ingest route calls `upsertKnowledge` (Mongo) + `upsertKnowledgeNode` + ingest route calls `upsertKnowledge` (SQLite) + `upsertKnowledgeNode` (Neo4j) with `state: CREATED` before the publisher (`enqueueGithubIndex` / `enqueueLocalIngest`). The publisher's `setKnowledgeState(_, QUEUED)` then transitions the doc. @@ -124,7 +124,7 @@ POST /sse/messages?sessionId=… legacy SSE messages — owned by @b :Class / :Function / :Module`; tolerant of existing indexes. 4. **Local ingest copies into `local-snapshots/`, not in-place.** The user's `sourcePath` is read-only; the snapshot at - `~/.bytebell/local-snapshots//` freezes the tree the + `~/.plumbline/local-snapshots//` freezes the tree the worker sees. MCP retrieval for local knowledges reads from `KnowledgeDoc.source.sourcePath` (the original, unfrozen path) — the snapshot exists purely to give the worker a stable input. @@ -134,7 +134,7 @@ POST /sse/messages?sessionId=… legacy SSE messages — owned by @b commit-scoped `orgs/` tree; dirs with no DB record are deleted and logged as abandoned. Boot only throws `LayoutMigrationRequiredError` (non-zero exit) when legacy dirs remain that back a live knowledge but can't be migrated - (missing `commitId` / `repoUrl`); `bytebell migrate paths` runs the same + (missing `commitId` / `repoUrl`); `plumbline migrate paths` runs the same reconciliation ahead of time or with `--dry-run`. 5. **Filtered copy uses the same SKIP lists as `scan.ts`.** Lists are duplicated (small, stable) rather than imported across the @@ -147,8 +147,8 @@ POST /sse/messages?sessionId=… legacy SSE messages — owned by @b - `express@5` — HTTP server - `@types/express` (dev) -- Workspace deps: `@bb/config`, `@bb/errors`, `@bb/types`, `@bb/mongo`, - `@bb/redis`, `@bb/neo4j`, `@bb/queue`, `@bb/ingest-github` +- Workspace deps: `@bb/config`, `@bb/errors`, `@bb/types`, `@bb/db`, `@bb/sqlite`, + `@bb/neo4j`, `@bb/queue`, `@bb/ingest-github` ## What is intentionally out of scope (v0) @@ -156,10 +156,10 @@ POST /sse/messages?sessionId=… legacy SSE messages — owned by @b - OpenAPI schemas per [CLAUDE.md _Rule of API Logging & Documentation_](../../CLAUDE.md) — defer - Tar-streaming for `/api/v1/local/index` — JSON `{ sourcePath }` is enough for local CLI/server co-location -- Streaming progress responses — caller polls Mongo / `/api/v1/repos` -- `bytebell server stop | status | logs` — defer; user can - `kill $(cat ~/.bytebell/pid)` -- `DELETE /api/v1/repos/:knowledgeId` (powers `bytebell clean`) — defer +- Streaming progress responses — caller polls `/api/v1/repos` +- `plumbline server stop | status | logs` — defer; user can + `kill $(cat ~/.plumbline/pid)` +- `DELETE /api/v1/repos/:knowledgeId` (powers `plumbline clean`) — defer - `GET /api/v1/repos/:knowledgeId` (single-doc read with file list) — defer ## How to extend @@ -175,8 +175,8 @@ Adding a new route: Adding boot infra: 1. Add the `connect*` call to `src/index.ts`'s `main()` in correct order - (config → mongo → redis → neo4j → schema bootstrap → queue → workers + (config → db → graph → schema bootstrap → queue → workers → listen → pid). 2. Add the matching `close*` call to `src/shutdown.ts` in reverse - order, before the `unlink ~/.bytebell/pid`. + order, before the `unlink ~/.plumbline/pid`. 3. Update _Invariants_ if the new infra changes the boot contract. diff --git a/packages/server/package.json b/packages/server/package.json index 1837ce3..2c940ba 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -12,7 +12,7 @@ "#src/*": "./src/*" }, "bin": { - "bytebell-server": "./src/index.ts" + "plumbline-server": "./src/index.ts" }, "dependencies": { "@bb/config": "workspace:*", @@ -23,13 +23,11 @@ "@bb/ingest-core": "workspace:*", "@bb/ingest-strategies": "workspace:*", "@bb/mcp": "workspace:*", - "@bb/mongo": "workspace:*", "@bb/path-migration": "workspace:*", "@bb/sqlite": "workspace:*", "@bb/neo4j": "workspace:*", "@bb/ladybug": "workspace:*", "@bb/queue": "workspace:*", - "@bb/queue-bullmq": "workspace:*", "@bb/queue-honker": "workspace:*", "@bb/types": "workspace:*", "express": "^5.2.1" diff --git a/packages/server/src/README.md b/packages/server/src/README.md index e1bfc99..809a97f 100644 --- a/packages/server/src/README.md +++ b/packages/server/src/README.md @@ -7,14 +7,14 @@ package-level contract; this file documents how the source tree is split. - **[index.ts](index.ts)** — binary entry. Shebang `#!/usr/bin/env bun`. Runs `checkRequiredConfig()` (throws `ServerConfigError` with the - missing keys + matching `bytebell set …` hints); awaits the four - `connect*` calls (mongo → redis → neo4j → queue), runs + missing keys + matching `plumbline set …` hints); awaits the four + `connect*` calls (db → graph → queue), runs `reconcileLegacyLayout()` right after the db connect (see [legacyLayout.ts](legacyLayout.ts)), and runs `ensureKnowledgeIndexes()` between neo4j connect and queue connect; calls both worker registrations; installs shutdown handlers; constructs the express app with `express.json({ limit: "1mb" })`; - binds to `127.0.0.1`; writes `~/.bytebell/pid`. On any error during + binds to `127.0.0.1`; writes `~/.plumbline/pid`. On any error during boot: prints to stderr and exits 1. - **[legacyLayout.ts](legacyLayout.ts)** — `reconcileLegacyLayout()`, called after `connectDb`. Lists knowledge via `@bb/db`, delegates the disk work to @@ -27,20 +27,20 @@ package-level contract; this file documents how the source tree is split. `/mcp` plus legacy SSE at `/sse` + `/sse/messages`). Add a new line per route. - **[healthRoute.ts](healthRoute.ts)** — `GET /health`. Awaits - `pingMongo()` + `pingRedis()` + `pingNeo4j()` in parallel; 200 if all + `pingDb()` + `pingQueue()` + `pingGraph()` in parallel; 200 if all three ok, 503 otherwise. Body includes all three ping results for debugging. - **[githubIndexRoute.ts](githubIndexRoute.ts)** — `POST /api/v1/github/index`. Manual body validation (`repoUrl` non-empty string + `^https?://`). Mints `crypto.randomUUID()`, builds a - `KnowledgeDoc`, dual-writes via `upsertKnowledge` (Mongo) + + `KnowledgeDoc`, dual-writes via `upsertKnowledge` (SQLite) + `upsertKnowledgeNode` (Neo4j), then `enqueueGithubIndex` (the - publisher transitions Mongo state to `QUEUED`). Returns + publisher transitions the knowledge state to `QUEUED`). Returns `{ knowledgeId, jobId }`. - **[localIndexRoute.ts](localIndexRoute.ts)** — `POST /api/v1/local/index`. Validates `sourcePath` is non-empty / absolute / exists / is a directory. Mints `knowledgeId`, mkdirs - `~/.bytebell/repos/`, calls `copyRepo(sourcePath, destDir)`, then + `~/.plumbline/repos/`, calls `copyRepo(sourcePath, destDir)`, then dual-writes `upsertKnowledge` + `upsertKnowledgeNode`, then `enqueueLocalIngest`. Returns `{ knowledgeId, jobId }`. - **[reposRoute.ts](reposRoute.ts)** — `GET /api/v1/repos`. Calls @@ -53,8 +53,8 @@ package-level contract; this file documents how the source tree is split. muddy the tier graph). - **[shutdown.ts](shutdown.ts)** — `installShutdownHandlers()` registers SIGTERM and SIGINT handlers. The handler awaits - `closeAllMcpSessions → closeQueue → closeRedis → closeNeo4j → -closeMongo` then unlinks `~/.bytebell/pid`. MCP sessions drain first + `closeAllMcpSessions → closeQueue → closeGraph → closeDb` then unlinks + `~/.plumbline/pid`. MCP sessions drain first so in-flight Streamable HTTP / SSE transports release before any worker- or driver-level connection closes. 30-second timeout (via `setTimeout.unref()` so it doesn't keep the process alive); on @@ -64,29 +64,29 @@ closeMongo` then unlinks `~/.bytebell/pid`. MCP sessions drain first ## Module dependency graph ``` -healthRoute.ts → express, @bb/mongo (pingMongo), @bb/redis (pingRedis), - @bb/neo4j (pingNeo4j) +healthRoute.ts → express, @bb/db (pingDb), @bb/queue (pingQueue), + @bb/graph-db (pingGraph) githubIndexRoute.ts → express, @bb/types (KnowledgeState, KnowledgeDoc), - @bb/mongo (upsertKnowledge), + @bb/db (knowledgeDb.upsertKnowledge), @bb/neo4j (upsertKnowledgeNode), @bb/queue (enqueueGithubIndex) localIndexRoute.ts → express, node:fs/promises, node:path, - @bb/config (getBytebellHome), + @bb/config (getPlumblineHome), @bb/types (KnowledgeState, KnowledgeDoc), - @bb/mongo (upsertKnowledge), + @bb/db (knowledgeDb.upsertKnowledge), @bb/neo4j (upsertKnowledgeNode), @bb/queue (enqueueLocalIngest), copyRepo.ts -reposRoute.ts → express, @bb/mongo (listKnowledge) +reposRoute.ts → express, @bb/db (knowledgeDb.listKnowledge) copyRepo.ts → node:fs/promises, node:path routes.ts → express, all four route builders, @bb/mcp (mountMcp) -shutdown.ts → node:fs/promises, node:path, @bb/mongo (closeMongo), - @bb/redis (closeRedis), @bb/neo4j (closeNeo4j), +shutdown.ts → node:fs/promises, node:path, @bb/db (closeDb), + @bb/graph-db (closeGraph), @bb/queue (closeQueue), @bb/mcp (closeAllMcpSessions), - @bb/config (getBytebellHome) + @bb/config (getPlumblineHome) index.ts → express, node:fs/promises, node:path, @bb/types (Config), - @bb/config (getBytebellHome, getConfigValue, HINTS), - @bb/mongo (connectMongo), @bb/redis (connectRedis), - @bb/neo4j (connectNeo4j, ensureKnowledgeIndexes), + @bb/config (getPlumblineHome, getConfigValue, HINTS), + @bb/db (connectDb), @bb/sqlite, + @bb/graph-db (connectGraph, ensureKnowledgeIndexes), @bb/queue (connectQueue), @bb/ingest-github (registerGithubWorkers, registerLocalIngestWorker), @bb/errors (ServerConfigError), @@ -97,12 +97,12 @@ No cycles. ## Invariants enforced here -- **Boot ordering is load-bearing.** `connectMongo` must come before +- **Boot ordering is load-bearing.** `connectDb` must come before any worker registration; `connectNeo4j` before `ensureKnowledgeIndexes`; `connectQueue` before `registerWorker`; the PID file write happens last (only after `app.listen` resolves). - **`Knowledge` doc is dual-written before enqueue.** Every ingest - route runs `upsertKnowledge({ state: CREATED })` (Mongo) and + route runs `upsertKnowledge({ state: CREATED })` (SQLite) and `upsertKnowledgeNode(doc)` (Neo4j) before its publisher call. If either upsert throws, no job is ever enqueued. - **Copy precedes enqueue for local.** The local-ingest worker assumes @@ -111,8 +111,8 @@ No cycles. - **Routes are thin.** Body validation, ID minting, two upserts, one enqueue, JSON response. No business logic. Anything heavier moves into `@bb/ingest-github` or a future service package. -- **Health pings all three.** `/health` reports the status of mongo, - redis, AND neo4j; 503 if any one is down. +- **Health pings all three.** `/health` reports the status of the document + store, the queue, AND the graph; 503 if any one is down. - **No `process.env` reads.** All config via `@bb/config`. ## Adding a route diff --git a/packages/server/src/copyRepo.ts b/packages/server/src/copyRepo.ts index 9b0d2c6..881b0f7 100644 --- a/packages/server/src/copyRepo.ts +++ b/packages/server/src/copyRepo.ts @@ -10,7 +10,7 @@ const SKIP_DIRS = new Set([ ".turbo", ".cache", "coverage", - ".bytebell", + ".plumbline", ]); const SKIP_FILES = new Set([ diff --git a/packages/server/src/deleteRoute.ts b/packages/server/src/deleteRoute.ts index d32c66c..c06e3fc 100644 --- a/packages/server/src/deleteRoute.ts +++ b/packages/server/src/deleteRoute.ts @@ -23,9 +23,9 @@ export function buildDeleteRoute(): Router { return; } - let mongoResult: Awaited>; + let dbResult: Awaited>; try { - mongoResult = await knowledgeDb.deleteKnowledge(knowledgeId); + dbResult = await knowledgeDb.deleteKnowledge(knowledgeId); } catch (cause: unknown) { if (cause instanceof KnowledgeNotFoundError) { res.status(404).json({ error: cause.message }); @@ -38,8 +38,8 @@ export function buildDeleteRoute(): Router { res.status(200).json({ knowledgeId, jobsRemoved: removedJobs.removed, - mongoDeleted: mongoResult.knowledgeDeleted, - rawDeleted: mongoResult.rawDeleted, + dbDeleted: dbResult.knowledgeDeleted, + rawDeleted: dbResult.rawDeleted, }); }); return router; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 9675380..600347c 100755 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -3,20 +3,18 @@ import { writeFile } from "node:fs/promises"; import path from "node:path"; import express from "express"; import { Config, DbProviderType, GraphProviderType, QueueProviderType, type Config as ConfigEnum } from "@bb/types"; -import { getBytebellHome, getConfigValue, HINTS } from "@bb/config"; +import { getPlumblineHome, getConfigValue, HINTS } from "@bb/config"; import { connectDb } from "@bb/db"; import { connectGraph, indexesGraph } from "@bb/graph-db"; import { connectQueue, resumeOrphans } from "@bb/queue"; // Provider registration is intentional and explicit at this composition root — // the public server supports every provider (Docker + embedded), so it imports -// all of them. A different deployment that only needs a subset (e.g. a Neo4j + -// Mongo production server) would import only those packages here and would never +// all of them. A different deployment that only needs a subset (e.g. a Neo4j +// production server) would import only those packages here and would never // load the unused drivers/native bindings (e.g. the `@bb/ladybug` core addon). -import "@bb/mongo"; import "@bb/sqlite"; import "@bb/neo4j"; import "@bb/ladybug"; -import "@bb/queue-bullmq"; import "@bb/queue-honker"; import { registerGithubWorkers, registerLocalIngestWorker, resolvePullSource } from "@bb/ingest-github"; @@ -32,14 +30,7 @@ import { registerRoutes } from "./routes.ts"; import { installShutdownHandlers } from "./shutdown.ts"; import { reconcileLegacyLayout } from "./legacyLayout.ts"; -const REQUIRED: ConfigEnum[] = [ - Config.MongoUri, - Config.RedisUrl, - Config.Neo4jUri, - Config.Neo4jUser, - Config.Neo4jPassword, - Config.OpenrouterApiKey, -]; +const REQUIRED: ConfigEnum[] = [Config.Neo4jUri, Config.Neo4jUser, Config.Neo4jPassword, Config.OpenrouterApiKey]; function checkRequiredConfig(): void { const missing: string[] = []; @@ -56,22 +47,15 @@ function checkRequiredConfig(): void { } }; - if (dbProvider !== DbProviderType.Mongo) { - remove(Config.MongoUri); - } if (graphProvider !== GraphProviderType.Neo4j) { // Embedded graph (ladybug) needs no Neo4j connection details. remove(Config.Neo4jUri); remove(Config.Neo4jUser); remove(Config.Neo4jPassword); } - if (queueProvider !== QueueProviderType.Bullmq) { - remove(Config.RedisUrl); - } - // Embedded mode keeps its stores on disk — refuse to boot if any path the - // active embedded provider depends on is unset, instead of failing later - // with a cryptic file lock / IO error. + // File-backed stores refuse to boot if any path the active provider depends + // on is unset, instead of failing later with a cryptic file lock / IO error. if (dbProvider === DbProviderType.Sqlite) { required.push(Config.SqlitePath); } @@ -143,10 +127,10 @@ async function main(): Promise { const port = getConfigValue(Config.ServerPort); app.listen(port, "127.0.0.1", () => { - process.stdout.write(`Bytebell server listening on http://127.0.0.1:${port}\n`); + process.stdout.write(`Plumbline server listening on http://127.0.0.1:${port}\n`); }); - await writeFile(path.join(getBytebellHome(), "pid"), String(process.pid), { mode: 0o644 }); + await writeFile(path.join(getPlumblineHome(), "pid"), String(process.pid), { mode: 0o644 }); } main().catch((cause: unknown) => { diff --git a/packages/server/src/legacyLayout.ts b/packages/server/src/legacyLayout.ts index 4830ea6..89c7fa8 100644 --- a/packages/server/src/legacyLayout.ts +++ b/packages/server/src/legacyLayout.ts @@ -1,6 +1,6 @@ import path from "node:path"; import { Config } from "@bb/types"; -import { getBytebellHome, getConfigValue } from "@bb/config"; +import { getPlumblineHome, getConfigValue } from "@bb/config"; import { knowledgeDb } from "@bb/db"; import { LayoutMigrationRequiredError } from "@bb/errors"; import { hasLegacyLayout, migrateLegacyPaths } from "@bb/path-migration"; @@ -18,7 +18,7 @@ import { hasLegacyLayout, migrateLegacyPaths } from "@bb/path-migration"; // ───────────────────────────────────────────────────────────────────────────── export async function reconcileLegacyLayout(): Promise { - const home = getBytebellHome(); + const home = getPlumblineHome(); if (!(await hasLegacyLayout(home))) { return; } diff --git a/packages/server/src/localIndexRoute.ts b/packages/server/src/localIndexRoute.ts index 29b7f25..ebc5b59 100644 --- a/packages/server/src/localIndexRoute.ts +++ b/packages/server/src/localIndexRoute.ts @@ -3,7 +3,7 @@ import express from "express"; import { stat, mkdir } from "node:fs/promises"; import path from "node:path"; import { KnowledgeState, type KnowledgeDoc } from "@bb/types"; -import { getBytebellHome } from "@bb/config"; +import { getPlumblineHome } from "@bb/config"; import { knowledgeDb } from "@bb/db"; import { knowledgeGraph } from "@bb/graph-db"; import { enqueueLocalIngest } from "@bb/queue"; @@ -43,7 +43,7 @@ export function buildLocalIndexRoute(): Router { // analysed knowledges live. The worker reads from this snapshot rather // than the original `sourcePath` so a user moving / mutating their dir // after submission doesn't affect the in-flight ingestion. - const snapshotsRoot = path.join(getBytebellHome(), "local-snapshots"); + const snapshotsRoot = path.join(getPlumblineHome(), "local-snapshots"); await mkdir(snapshotsRoot, { recursive: true, mode: 0o700 }); const destDir = path.join(snapshotsRoot, knowledgeId); diff --git a/packages/server/src/shutdown.ts b/packages/server/src/shutdown.ts index 5bc53d9..afbd803 100644 --- a/packages/server/src/shutdown.ts +++ b/packages/server/src/shutdown.ts @@ -4,7 +4,7 @@ import { closeDb } from "@bb/db"; import { closeGraph } from "@bb/graph-db"; import { closeQueue } from "@bb/queue"; import { closeAllMcpSessions } from "@bb/mcp"; -import { getBytebellHome } from "@bb/config"; +import { getPlumblineHome } from "@bb/config"; const SHUTDOWN_TIMEOUT_MS = 30_000; @@ -29,7 +29,7 @@ async function shutdown(signal: string): Promise { await closeQueue(); await closeGraph(); await closeDb(); - await unlink(path.join(getBytebellHome(), "pid")).catch(() => undefined); + await unlink(path.join(getPlumblineHome(), "pid")).catch(() => undefined); } catch (cause: unknown) { process.stderr.write(`Shutdown error: ${cause instanceof Error ? cause.message : String(cause)}\n`); process.exit(1); diff --git a/packages/sqlite/README.md b/packages/sqlite/README.md index 684357a..6f3c994 100644 --- a/packages/sqlite/README.md +++ b/packages/sqlite/README.md @@ -1,10 +1,10 @@ # `@bb/sqlite` -SQLite implementation of the `IDocumentDatabaseProvider` interface. +SQLite implementation of the `IDocumentDatabaseProvider` interface — the document store for the engine. ## Responsibilities -Stores knowledge entries, raw file documents, activity logs, and usage records in a local SQLite database (via `bun:sqlite`). Registers itself as the `"sqlite"` provider with `@bb/db` at import time. +Stores knowledge entries, raw file documents, activity logs, usage records, and the concept-graph enrichment ledger in a local SQLite database (via `bun:sqlite`). Registers itself as the `"sqlite"` provider with `@bb/db` at import time. ## Public Interfaces @@ -14,10 +14,11 @@ Stores knowledge entries, raw file documents, activity logs, and usage records i - Stats: `aggregateStats` - Activity: `recordActivity` - Usage: `incrementUsage`, `getMonthlyUsage`, `getGlobalUsage` +- Enrichment ledger: `startEnrichmentRun`, `getCompletedEnrichmentFiles`, `markFileEnriched`, `recordEnrichmentFailure`, `completeEnrichmentRun`, `failEnrichmentRun` ## Data Ownership -Owns a single SQLite file at the path configured by `Config.SqlitePath` (defaults to `~/.bytebell/data.sqlite`). Tables: `knowledge`, `raw_files`, `activity`, `usage`. +Owns a single SQLite file at the path configured by `Config.SqlitePath` (defaults to `~/.plumbline/data.sqlite`). Tables: `knowledge`, `raw_files`, `activity`, `usage`. ## Invariants @@ -25,6 +26,8 @@ Owns a single SQLite file at the path configured by `Config.SqlitePath` (default - Raw files keyed by `knowledgeId:relativePath` with a `knowledgeId` index - WAL journal mode for concurrent read performance - Foreign keys enforced +- Enrichment state (`enrichmentRunId`, `enrichmentState`, `completedFiles`, `enrichmentFailures`) lives on the knowledge document, not a separate table +- `bun:sqlite` is synchronous, so each mutator's read-modify-write runs without an intervening `await` and cannot interleave with a concurrent enrichment worker ## Tier diff --git a/packages/sqlite/src/client.ts b/packages/sqlite/src/client.ts index ac55874..77265e2 100644 --- a/packages/sqlite/src/client.ts +++ b/packages/sqlite/src/client.ts @@ -1,7 +1,7 @@ import { Database } from "bun:sqlite"; import path from "node:path"; import fs from "node:fs"; -import { getConfigValue, getBytebellHome } from "@bb/config"; +import { getConfigValue, getPlumblineHome } from "@bb/config"; import { Config } from "@bb/types"; let db: Database | null = null; @@ -14,7 +14,7 @@ export async function connectSqlite(): Promise { let sqlitePath = getConfigValue(Config.SqlitePath); if (!sqlitePath || sqlitePath.length === 0) { - sqlitePath = path.join(getBytebellHome(), "data.sqlite"); + sqlitePath = path.join(getPlumblineHome(), "data.sqlite"); } dbPath = sqlitePath; diff --git a/packages/sqlite/src/enrichment.ts b/packages/sqlite/src/enrichment.ts new file mode 100644 index 0000000..e3aabb4 --- /dev/null +++ b/packages/sqlite/src/enrichment.ts @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: AGPL-3.0-only WITH non-commercial-clause +import type { EnrichmentFailure, KnowledgeDoc } from "@bb/types"; +import { EnrichmentState } from "@bb/types"; +import { KnowledgeNotFoundError } from "@bb/errors"; +import { getSqliteDb } from "./client.ts"; + +// ───────────────────────────────────────────────────────────────────────────── +// SQLite enrichment ledger for ConceptGraphStrategy. State lives on the +// existing knowledge row's JSON document — no new table. `status.state` itself +// stays PROCESSING throughout enrichment; this ledger tracks per-file progress +// so a queue retry can resume by skipping `completedFiles`. +// +// `bun:sqlite` is synchronous, so a read-modify-write with no `await` between +// the SELECT and the UPDATE cannot interleave with a concurrent enrichment +// worker on the JS event loop. Every mutator below keeps that property. +// +// All operations throw `KnowledgeNotFoundError` if the document is missing. +// ───────────────────────────────────────────────────────────────────────────── + +/** Loads the knowledge document, throwing when the row is absent. */ +function loadDoc(knowledgeId: string): KnowledgeDoc { + const row = getSqliteDb().query("SELECT value FROM knowledge WHERE key = ?").get(knowledgeId) as { + value: string; + } | null; + if (!row) { + throw new KnowledgeNotFoundError(knowledgeId); + } + return JSON.parse(row.value) as KnowledgeDoc; +} + +/** Writes the document back, stamping `updatedAt`. */ +function saveDoc(knowledgeId: string, doc: KnowledgeDoc): void { + doc.updatedAt = new Date(); + getSqliteDb().run("UPDATE knowledge SET value = ? WHERE key = ?", [JSON.stringify(doc), knowledgeId]); +} + +/** + * Begin or resume an enrichment attempt. Stamps a new `enrichmentRunId`, + * clears `enrichmentFailures` (failed files should be re-evaluated on the + * retry), transitions to `Running`. `completedFiles` is preserved so a + * queue retry can skip work that already finished — the disk artifact tree + * at `meta-output/enrichment/.json` is the canonical source of truth, + * and `completedFiles` mirrors that. A clean re-enrichment requires an + * explicit reset, not a retry. + */ +export async function startEnrichmentRun(knowledgeId: string, runId: string): Promise { + const doc = loadDoc(knowledgeId); + doc.enrichmentRunId = runId; + doc.enrichmentState = EnrichmentState.Running; + doc.enrichmentFailures = []; + if (!Array.isArray(doc.completedFiles)) { + doc.completedFiles = []; + } + saveDoc(knowledgeId, doc); +} + +/** + * Returns the list of files already enriched in the current/last attempt. + * Used by the strategy to pre-filter the work queue on retry. Empty array + * if the knowledge has no recorded enrichment runs. + */ +export async function getCompletedEnrichmentFiles(knowledgeId: string): Promise { + const doc = loadDoc(knowledgeId); + return Array.isArray(doc.completedFiles) ? doc.completedFiles : []; +} + +/** + * Records that `filePath` has been successfully enriched. Idempotent: the + * path is added set-wise so a re-run of the same file does not duplicate it. + */ +export async function markFileEnriched(knowledgeId: string, filePath: string): Promise { + const doc = loadDoc(knowledgeId); + const completed = Array.isArray(doc.completedFiles) ? doc.completedFiles : []; + if (completed.includes(filePath)) { + return; + } + completed.push(filePath); + doc.completedFiles = completed; + saveDoc(knowledgeId, doc); +} + +/** + * Records or updates a per-file enrichment failure. The array is keyed by + * `filePath` (one entry per file); subsequent failures for the same file + * replace the prior entry rather than accumulating. Diagnostic, not + * load-bearing — the strategy decides whether the knowledge fails overall. + */ +export async function recordEnrichmentFailure(knowledgeId: string, failure: EnrichmentFailure): Promise { + const doc = loadDoc(knowledgeId); + const failures = Array.isArray(doc.enrichmentFailures) ? doc.enrichmentFailures : []; + doc.enrichmentFailures = [...failures.filter((f) => f.filePath !== failure.filePath), failure]; + saveDoc(knowledgeId, doc); +} + +/** Transitions the ledger to `Completed`. Caller is responsible for then transitioning the parent `KnowledgeState`. */ +export async function completeEnrichmentRun(knowledgeId: string): Promise { + const doc = loadDoc(knowledgeId); + doc.enrichmentState = EnrichmentState.Completed; + saveDoc(knowledgeId, doc); +} + +/** + * Transitions the ledger to `Failed`. Knowledge can be retried by calling + * `startEnrichmentRun` again with a fresh run id. + */ +export async function failEnrichmentRun(knowledgeId: string): Promise { + const doc = loadDoc(knowledgeId); + doc.enrichmentState = EnrichmentState.Failed; + saveDoc(knowledgeId, doc); +} diff --git a/packages/sqlite/src/index.ts b/packages/sqlite/src/index.ts index 8f4ffaf..5004dc8 100644 --- a/packages/sqlite/src/index.ts +++ b/packages/sqlite/src/index.ts @@ -23,3 +23,12 @@ export { aggregateStats } from "./aggregateStats.ts"; export { incrementUsage, getMonthlyUsage, getGlobalUsage } from "./usage.ts"; export { recordActivity } from "./activity.ts"; + +export { + startEnrichmentRun, + getCompletedEnrichmentFiles, + markFileEnriched, + recordEnrichmentFailure, + completeEnrichmentRun, + failEnrichmentRun, +} from "./enrichment.ts"; diff --git a/packages/sqlite/src/provider.ts b/packages/sqlite/src/provider.ts index 860fb2f..a11959a 100644 --- a/packages/sqlite/src/provider.ts +++ b/packages/sqlite/src/provider.ts @@ -4,6 +4,7 @@ import * as rawRepo from "./raw.ts"; import * as statsRepo from "./aggregateStats.ts"; import * as activityRepo from "./activity.ts"; import * as usageRepo from "./usage.ts"; +import * as enrichmentRepo from "./enrichment.ts"; import { registerDbProvider } from "@bb/db"; import type { IDocumentDatabaseProvider } from "@bb/db-core"; @@ -45,6 +46,15 @@ class SqliteDatabaseProvider implements IDocumentDatabaseProvider { getGlobalUsage: usageRepo.getGlobalUsage, }; + enrichment = { + startEnrichmentRun: enrichmentRepo.startEnrichmentRun, + getCompletedEnrichmentFiles: enrichmentRepo.getCompletedEnrichmentFiles, + markFileEnriched: enrichmentRepo.markFileEnriched, + recordEnrichmentFailure: enrichmentRepo.recordEnrichmentFailure, + completeEnrichmentRun: enrichmentRepo.completeEnrichmentRun, + failEnrichmentRun: enrichmentRepo.failEnrichmentRun, + }; + async connect(): Promise { await connectSqlite(); } diff --git a/packages/types/README.md b/packages/types/README.md index fbc9e42..9624844 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -10,9 +10,9 @@ dependencies and no runtime dependencies — pure type / enum surface. Single home for shared types and enums that cross package boundaries: -- `Config` — the enumeration of every key under `~/.bytebell/config.json`. +- `Config` — the enumeration of every key under `~/.plumbline/config.json`. Lives here (not in `@bb/config`) because consumers in higher tiers — e.g. - `@bb/logger`, `@bb/mongo` — refer to it without wanting an implementation + `@bb/logger`, `@bb/sqlite` — refer to it without wanting an implementation dependency on `@bb/config`'s schema/loader/writer. - `JobType`, `JobPriority`, `JobMessage

`, `GithubIndexPayload`, `GithubPullPayload`, `LocalIngestPayload`, `PayloadFor`, @@ -32,10 +32,10 @@ llmModel?, llmKeyId? }` mixin that lets downstream consumers carry per-job QUEUED → INGESTED → PROCESSING → PROCESSED ↘ FAILED`, plus the terminal `CORRUPTED` for a source repo that is gone/inaccessible — indexed data stays queryable but the auto-pull sweep drops it) referenced by `@bb/queue` (writes - `QUEUED`), `@bb/mongo` (`setKnowledgeState`), and future ingest workers. + `QUEUED`), `@bb/sqlite` (`setKnowledgeState`), and future ingest workers. - `KnowledgeDoc`, `KnowledgeSource`, `GithubKnowledgeSource`, `LocalKnowledgeSource`, `KnowledgeInfo` — the cross-package shape of the - Mongo `knowledge` document. Split into two substructures with + `knowledge` document. Split into two substructures with non-overlapping responsibilities: `KnowledgeSource` discriminates the upstream type (github vs local) and carries per-kind ingestion state — for github, the current head commit and the full commit history; for diff --git a/packages/types/src/README.md b/packages/types/src/README.md index 26abf29..f3655a4 100644 --- a/packages/types/src/README.md +++ b/packages/types/src/README.md @@ -8,20 +8,20 @@ package-level contract; this file documents how the source tree is split. - **[index.ts](index.ts)** — public re-exports. The only entry point other packages may import. Anything not re-exported here is internal. - **[config.ts](config.ts)** — the `Config` enum: every key under - `~/.bytebell/config.json`. The string values match the on-disk JSON keys - (`server_port`, `mongo_uri`, …). Lives here — not in `@bb/config` — so that - consumers like `@bb/logger` and `@bb/mongo` can refer to a config key + `~/.plumbline/config.json`. The string values match the on-disk JSON keys + (`server_port`, `sqlite_path`, …). Lives here — not in `@bb/config` — so that + consumers like `@bb/logger` and `@bb/sqlite` can refer to a config key without taking a dependency on `@bb/config`'s schema/loader/writer implementation. - **[job.ts](job.ts)** — the queue vocabulary: `JobType` (today: GitHub index + pull, local ingest), `JobPriority`, the per-type payload interfaces (`GithubIndexPayload`, `GithubPullPayload`, `LocalIngestPayload`), the `PayloadLlmOverrides` mixin, the - `JobMessage

` envelope wrapping payloads as BullMQ `job.data`, and + `JobMessage

` envelope wrapping payloads as the queue's job data, and the `PayloadFor` type-level dispatcher. Shared between `@bb/queue` (publisher) and `@bb/ingest-*` packages (worker handlers). Ingest payloads carry an optional `orgId?: string` override; OSS callers omit - it and the pipeline reads `Config.OrgId` from `~/.bytebell/config.json` + it and the pipeline reads `Config.OrgId` from `~/.plumbline/config.json` (locked to `"local"` in OSS builds; downstream consumers may set `orgId` per-job). Both GitHub payloads also extend `PayloadLlmOverrides` which adds optional `llmApiKey?`, `llmProvider?: string`, `llmModel?`, @@ -34,7 +34,7 @@ package-level contract; this file documents how the source tree is split. standalone leaves all four fields unset and the pipeline falls back to `Config.OpenrouterApiKey` + `Config.LlmProvider`. `GithubPullPayload` also carries an optional `orgId?` so downstream multi-tenant workers - can scope Mongo/Neo4j lookups by org. + can scope document-store/Neo4j lookups by org. - **[knowledge.ts](knowledge.ts)** — the `KnowledgeState` enum modeling the lifecycle in [CLAUDE.md](../../../CLAUDE.md), plus the `KnowledgeDoc` document interface and its substructures: @@ -84,7 +84,7 @@ lastAttemptAt }`) plus `EnrichmentFailureReason` (`"cap-exceeded" | (implementation). - **[path-layout.ts](path-layout.ts)** — pure on-disk path resolver. Defines the `RepoLocation` union (github / local) and pure functions - (`bytebellPathsFor`, `commitBaseDirFor`, `repositoryDirFor`, + (`plumblinePathsFor`, `commitBaseDirFor`, `repositoryDirFor`, `metaOutputRootFor`, `orgsRootFor`) that take a `home` string and return the kube-style layout `/orgs///////`. @@ -102,7 +102,7 @@ lastAttemptAt }`) plus `EnrichmentFailureReason` (`"cap-exceeded" | `repo="project"`) — matching `deriveOwnerRepo` in the GitLab `SourceFactory`, so the ingest-write and business-context-read disk paths agree for nested projects. The `MetaPathsLayout` interface - documents the leaf-path shape returned by `bytebellPathsFor`. Lives + documents the leaf-path shape returned by `plumblinePathsFor`. Lives here so `@bb/ingest-github` (writer) and `@bb/mcp` (reader) can agree on the layout without one importing the other. @@ -124,8 +124,8 @@ Pure declarations, no cycles possible. - **No imports.** Source files import nothing — not from this package, not from siblings, not from Node built-ins. If an entry needs to import, it belongs in a higher tier. -- **Enum string values are the on-disk JSON keys.** `Config.MongoUri = -"mongo_uri"` is the contract `@bb/config`'s Zod schema relies on; renaming +- **Enum string values are the on-disk JSON keys.** `Config.SqlitePath = +"sqlite_path"` is the contract `@bb/config`'s Zod schema relies on; renaming a value is a breaking change for both the file format and every consumer. - **One file per logical group.** `config.ts` holds config keys, `job.ts` holds queue vocabulary, `knowledge.ts` holds knowledge-document diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 78d569b..e5ba80b 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -1,10 +1,8 @@ export enum Config { ServerPort = "server_port", - MongoUri = "mongo_uri", Neo4jUri = "neo4j_uri", Neo4jUser = "neo4j_user", Neo4jPassword = "neo4j_password", - RedisUrl = "redis_url", OpenrouterApiKey = "openrouter_api_key", OpenrouterModel = "openrouter_model", OpenrouterFallbackModel1 = "openrouter_fallback_model_1", @@ -68,7 +66,6 @@ export enum Config { export enum DbProviderType { Sqlite = "sqlite", - Mongo = "mongo", } export enum GraphProviderType { @@ -77,7 +74,6 @@ export enum GraphProviderType { } export enum QueueProviderType { - Bullmq = "bullmq", Honker = "honker", } /** diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index eba7fe1..434aa31 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -59,7 +59,7 @@ export { commitBaseDirFor, repositoryDirFor, metaOutputRootFor, - bytebellPathsFor, + plumblinePathsFor, parseGithubOwnerRepo, parseGitlabOwnerRepo, } from "./path-layout.ts"; diff --git a/packages/types/src/job.ts b/packages/types/src/job.ts index 70d1a8c..86392b5 100644 --- a/packages/types/src/job.ts +++ b/packages/types/src/job.ts @@ -43,7 +43,7 @@ export interface PayloadLlmOverrides { * A copy-on-write delta against one category of built-in ignore defaults. * `add` adds patterns to the effective ignore set; `remove` un-ignores a * built-in default (its strings are matched verbatim against the seed lists). - * Both are plain string arrays so the patch serializes cleanly into a BullMQ + * Both are plain string arrays so the patch serializes cleanly into a queue * payload. */ export interface IgnoreOverridePatch { diff --git a/packages/types/src/path-layout.ts b/packages/types/src/path-layout.ts index 2015725..6cb40d8 100644 --- a/packages/types/src/path-layout.ts +++ b/packages/types/src/path-layout.ts @@ -6,7 +6,7 @@ import path from "node:path"; // or write knowledge artifacts. No I/O, no FS calls — every helper returns // strings derived from the inputs (a sha256 of a branch name is such a // derivation; `node:crypto` is a runtime builtin, not a package dependency). -// Callers compose with their own `getBytebellHome()` (the package boundary +// Callers compose with their own `getPlumblineHome()` (the package boundary // that holds the home-dir state). // // Layout (per knowledge + provider + branch + commit): @@ -20,9 +20,9 @@ import path from "node:path"; // self-contained subtree (see `branchIdFor` for why it is hashed, not raw). // // `` is the per-tenant base directory: -// • OSS standalone: `~/.bytebell/` (single-tenant; no org segment) +// • OSS standalone: `~/.plumbline/` (single-tenant; no org segment) // • Enterprise: `/orgs//` (via the -// `setBytebellHomeResolver` override in `seed-oss-config.ts`) +// `setPlumblineHomeResolver` override in `seed-oss-config.ts`) // // The resolver deliberately stays org-agnostic. The org segment lives in // `` when the host requires per-tenant isolation — adding it again @@ -72,7 +72,7 @@ export interface MetaPathsLayout { * Deprecated. Kept as a back-compat shim for the migration tool, which * describes the legacy layout `/orgs//…`. The active layout * no longer adds an `orgs/` segment here — that responsibility moved into - * `` itself (enterprise's `getBytebellHome` resolver returns a + * `` itself (enterprise's `getPlumblineHome` resolver returns a * per-tenant `/orgs//`). */ export function orgsRootFor(home: string): string { @@ -87,7 +87,7 @@ const BRANCH_BACKSLASH_RE = /\\/gu; * branch-scoped graph nodes. A single 64-hex SHA-256 component sidesteps every * branch-name hazard at once: embedded slashes (`feat/x`), length caps, and * case-insensitive filesystems (`Feature` vs `feature` hash differently, so no - * collision). The human-readable name is kept on Mongo `knowledge.info.branch` + * collision). The human-readable name is kept on `knowledge.info.branch` * and the `:Branch` node, never on disk. Mirrors the hashing of * `@bb/ingest-core`'s `metaId`; kept here in the kernel so every tier can * derive a branch id without importing upward (same rationale as the @@ -115,7 +115,7 @@ export function metaOutputRootFor(home: string, loc: RepoLocation): string { return path.join(commitBaseDirFor(home, loc), "meta-output"); } -export function bytebellPathsFor(home: string, loc: RepoLocation): MetaPathsLayout { +export function plumblinePathsFor(home: string, loc: RepoLocation): MetaPathsLayout { const meta = metaOutputRootFor(home, loc); return { repositoryDir: repositoryDirFor(home, loc), @@ -172,7 +172,7 @@ export function parseGithubOwnerRepo(repoUrl: string): { owner: string; repo: st * hosts or paths with fewer than two segments. * * This MUST stay consistent with `deriveOwnerRepo` in - * `@bytebell/.../ingest-gitlab/src/source-factory.ts`, which is what the GitLab + * `@plumbline/.../ingest-gitlab/src/source-factory.ts`, which is what the GitLab * ingester uses to choose the on-disk `/` directory segments. The * business-context reader resolves the same path via `repoLocationFor`, so the * two derivations must agree or enrichment reads miss the directory. diff --git a/scripts/test-tool-choice.ts b/scripts/test-tool-choice.ts index 7a9db90..89abeac 100644 --- a/scripts/test-tool-choice.ts +++ b/scripts/test-tool-choice.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun /** * One-shot probe: does the configured ENRICHMENT_MODEL emit `tool_calls` - * when `tool_choice: "required"` is set? Bypasses Mongo / Neo4j entirely + * when `tool_choice: "required"` is set? Bypasses the document store / Neo4j entirely * — just exercises the OpenRouter request shape so we can confirm the * model actually honors the constraint before running a full enrichment. * @@ -28,11 +28,9 @@ if (model === undefined || model.length === 0) { // rejects unknown keys, so we provide a full-shaped object. seedConfig({ server_port: 8080, - mongo_uri: "mongodb://placeholder/local", neo4j_uri: "bolt://placeholder:7687", neo4j_user: "placeholder", neo4j_password: "placeholder", - redis_url: "redis://placeholder:6379", openrouter_api_key: apiKey, openrouter_model: model, openrouter_fallback_model_1: "", @@ -62,7 +60,7 @@ seedConfig({ "skip.decision.enabled": false, "skip.decision.max.chars.for.llm": 4000, "skip.decision.cache.path": "", - db_provider: "mongo", + db_provider: "sqlite", graph_provider: "neo4j", sqlite_path: "", concurrency: { github: 2 }, diff --git a/tsconfig.json b/tsconfig.json index 10a51bb..79c79fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,9 +9,7 @@ { "path": "packages/errors" }, { "path": "packages/config" }, { "path": "packages/logger" }, - { "path": "packages/mongo" }, { "path": "packages/sqlite" }, - { "path": "packages/redis" }, { "path": "packages/neo4j" }, { "path": "packages/ladybug" }, { "path": "packages/db-core" }, @@ -19,7 +17,6 @@ { "path": "packages/graph-core" }, { "path": "packages/graph-db" }, { "path": "packages/queue-core" }, - { "path": "packages/queue-bullmq" }, { "path": "packages/queue-honker" }, { "path": "packages/queue" }, { "path": "packages/llm" },