From 1e4929597c33ac61dc4fcf45303610b9bdc62a09 Mon Sep 17 00:00:00 2001 From: plugmil Date: Sun, 4 Oct 2026 22:31:41 +0200 Subject: [PATCH 1/2] docs: refresh standalone entry and fix workspace-scoped quickstart --- README.de.md | 9 + README.md | 183 ++++-------------- docs/agent-notes/explicit-init-root.md | 5 + docs/agent-notes/notes-workspace-authority.md | 5 + docs/channels.md | 28 +++ docs/cli.md | 4 +- docs/images/knowledge-demo.png | Bin 0 -> 32286 bytes docs/images/plugbrain-wordmark.svg | 1 + docs/images/showcase.json | 17 ++ docs/images/task-demo.png | Bin 0 -> 18501 bytes docs/index.md | 19 ++ docs/quickstart.md | 19 +- src/cli.ts | 64 ++++-- test/notes-cli-workspace.test.ts | 56 ++++++ test/setup-init.test.ts | 21 ++ 15 files changed, 266 insertions(+), 165 deletions(-) create mode 100644 README.de.md create mode 100644 docs/agent-notes/explicit-init-root.md create mode 100644 docs/agent-notes/notes-workspace-authority.md create mode 100644 docs/channels.md create mode 100644 docs/images/knowledge-demo.png create mode 100644 docs/images/plugbrain-wordmark.svg create mode 100644 docs/images/showcase.json create mode 100644 docs/images/task-demo.png create mode 100644 docs/index.md create mode 100644 test/notes-cli-workspace.test.ts diff --git a/README.de.md b/README.de.md new file mode 100644 index 0000000..3b29716 --- /dev/null +++ b/README.de.md @@ -0,0 +1,9 @@ +# PlugBrain + +[English](README.md) | Deutsch + +Ein lokaler Projektindex für Code, Markdown-Wissen und die Koordination vorhandener Coding-Agenten. PlugBrain läuft ohne PlugPT, Freebuff, API-Schlüssel oder Cloud-Login. Es hostet selbst kein Sprachmodell. + +**Aktueller Einstieg:** [aus dem Quellcode starten](docs/quickstart.md), [Dokumentation](docs/index.md), [Agenten anbinden](docs/agents/README.md). Node.js 24+, npm und Git werden benötigt. Der öffentliche Source-Stand meldet 0.5.0-dev.1. Release 0.3.1 besitzt keine Installer-Assets; [PR 6](https://github.com/litbitrim/plugbrain/pull/6) ist ein offener 0.6-Kandidat. Die Produktbilder zeigen einen lokalen, unsignierten 0.7.0-rc.2-Test mit reinen Beispieldaten. Versions- und Downloadgrenzen stehen unter [Channels](docs/channels.md). + +Starte mit einem Projekt, lies Notizen und Code, verbinde danach einen bestehenden MCP-Client. Mehrere Worker teilen dieselbe Task- und Claim-Autorität. Ein Taskstatus beweist keine Ausführung; Lieferung, unabhängiges Review und Integration bleiben getrennt. Die deutsche Oberfläche kann direkt genutzt werden, auch wenn kein Agent läuft. diff --git a/README.md b/README.md index 5a4be34..50c790b 100644 --- a/README.md +++ b/README.md @@ -1,169 +1,70 @@ # PlugBrain -[![CI](https://github.com/litbitrim/plugbrain/actions/workflows/ci.yml/badge.svg)](https://github.com/litbitrim/plugbrain/actions/workflows/ci.yml) +![PlugBrain — one local project brain](docs/images/plugbrain-wordmark.svg) -**One local brain for all the coding agents working on your project.** +**Shared code knowledge, project notes and accountable agent coordination on your machine.** -Claude Code, Codex, Gemini, GLM, a local model, several of each: any agent that -can run a shell command can join the same PlugBrain. Agents take tasks from a -shared queue, claim the files they are about to change, check in at the start -and end of every turn, message each other and wait for a commit approval. -Underneath sits one index of the project's code and notes, so every agent starts -from the same knowledge instead of re-reading the repository. +PlugBrain indexes your project into one SQLite store. Use it to find definitions and callers, navigate linked Markdown notes, and coordinate coding agents through tasks, path claims, messages and delivery evidence. It runs without PlugPT, Freebuff, an API key or a cloud account. Your coding client chooses its own model; PlugBrain does not host an LLM. -The aim is simple: more agents should mean more work done, not overwritten -files, duplicated tasks or an agent silently stuck for an hour. +[Start from source](docs/quickstart.md) · [Documentation](docs/index.md) · [Agent integration](docs/agents/README.md) · [Channels and downloads](docs/channels.md) · [Deutsch](README.de.md) -Everything runs on your machine: one SQLite file, a daemon on `127.0.0.1`, no -cloud, no telemetry. MIT licensed. +![Real PlugBrain knowledge view: Checkout note with Testing backlink, isolated demonstration data](docs/images/knowledge-demo.png) -## Status +*Actual local Windows candidate 0.7.0-rc.2, using isolated example data. This screenshot is not a claim that this branch contains the candidate or that an installer is publicly available. Image provenance: [capture record](docs/images/showcase.json).* -- 0.3.1 is the first published release. It is developed on Windows; macOS and - Linux run the same tests and packaging checks in CI. -- PlugBrain's own development runs on it. On 26 September 2026 one board - coordinated Claude Code, three Codex CLI workers and four Freebuff workers on - this repository. -- The web UI and the output of `plugbrain swarm` are in German for now. The - other commands, the MCP tools and all documentation are in English. -- Not there yet: the store keeps only each agent's latest turn, so there is no - turn history to replay, and worktrees of merged branches are not removed - automatically. Both are being built. +## Choose a channel -## How agents work together +| Channel | What exists | How to use it | +| --- | --- | --- | +| Public source, main | Package version 0.5.0-dev.1; development source | Node.js 24+, npm, Git; see the candidate [source quickstart](docs/quickstart.md) | +| Published tag 0.3.1 | GitHub source archives; **zero installer assets** when checked on 4 October 2026 | Browse the [release](https://github.com/litbitrim/plugbrain/releases/tag/0.3.1); source archives are not an app installer | +| Public candidate 0.6.0-rc.1 | Open [PR 6](https://github.com/litbitrim/plugbrain/pull/6), not merged | Review its pinned source and CI; it is not the main channel | +| Local candidate 0.7.0-rc.2 | Local source and unsigned Windows QA artifact, shown above | Publication, independent review, exact CI and signed installation remain pending | -```bash -# Once per agent: who it is and which account it spends -plugbrain swarm register codex-1 --surface other --account openai --model gpt-6-luna -plugbrain swarm register claude-1 --surface claude-code --account anthropic +There is currently no verified public Windows installer or macOS/Linux app archive in Releases. Do not look for a missing download button. [Channels](docs/channels.md) separates source, local preview and installed release. -# Whoever plans, a person or an agent, queues work, optionally for one agent -plugbrain swarm enqueue "Fix the flaky login test" --body "Repro in issue #12" --to codex-1 +## Start with one project -# Every agent, at the start of every turn: read messages, take the offered task -plugbrain swarm turn codex-1 start --claim +Install Node.js 24 or newer, npm and Git. These commands select the reviewed-entry candidate branch, which includes the explicit-folder and standalone-notes fixes. It is a draft candidate, not merged main: -# Before writing: claim the paths. A path another agent holds is refused. -plugbrain swarm claim codex-1 src/auth/login.ts --task +```sh +git clone --branch codex/brain-public-entry-20261004 --single-branch https://github.com/litbitrim/plugbrain.git +cd plugbrain +npm ci +node --experimental-strip-types src/cli.ts init ../my-project --no-clients --no-agents-file +node --experimental-strip-types src/cli.ts serve +``` -# Before tests, builds or new worktrees: is there room? Exit 5 means no. -plugbrain swarm admit test +Replace `../my-project` with your own folder. Open the loopback URL printed by `serve`; the default is http://127.0.0.1:4310. The first command indexes only the chosen project and leaves client configuration and agent instruction files alone. All data stays in `~/.plugbrain` unless you explicitly choose another `PLUGBRAIN_HOME`. For a safe trial, use a separate home as described in the [quickstart](docs/quickstart.md). -# At the end of every turn: exactly one state and a short summary -plugbrain swarm turn codex-1 end --state awaiting-commit --summary "Fixed the race, login tests pass" -plugbrain swarm release codex-1 --task +### Knowledge without an agent -# The integrator looks, approves, and sends the next hint -plugbrain swarm board --git -plugbrain swarm approve codex-1 --note "Reviewed. Commit it." -plugbrain swarm send codex-1 --subject "Next" --body "Rebase on main after your commit" --from integrator -``` +Open **Notizen** for Markdown documents, tags and wiki links, or **Code** for definitions and callers. Files remain ordinary project files; the index is a local projection. Notes and code can be explored without buying tokens or logging into another product. -A turn ends in one of four states: `needs-task` (done, give me work), -`awaiting-commit` (a tested change is ready), `blocked` (a real blocker, named in -the summary) or `paused`. `plugbrain swarm board` lists every agent once with its -account, turn state, unread messages, task, claims and worktree; with `--git` it -adds branch, head and uncommitted files, and it flags two agents writing in the -same worktree. - -Agents that speak MCP find the board, turns, claims and messages as tools -(`swarm_board`, `swarm_turn`, `claim`, `release`, `message_send`, `inbox_read`, -`heartbeat`, `awareness`). Queueing, approving and admission are CLI-only today. - -The per-turn rules for agents are written down so you can paste them into your -project's `AGENTS.md` or `CLAUDE.md`: [docs/agent-protocol.md](docs/agent-protocol.md). -All `swarm` commands: [docs/cli.md](docs/cli.md#swarm). - -## The index underneath - -The index answers the questions agents ask all day: where is this defined, who -calls it, what breaks if I change it, and what did we decide about it. The goal -is to match or beat GitNexus and CodeGraph on code questions and Obsidian on -project notes, in one index every agent shares. - -**Code.** Symbols, callers and callees, impact of a change, changed files since a -revision, graph queries in Cypher, and `ask` for plain-language questions with -cited sources. - -We compared PlugBrain with CodeGraph and GitNexus on 120 questions (definitions, -callers, impact, config keys, docs) across four of the author's own projects; -PlugBrain itself is not one of them. We wrote the questions ourselves. 40 were -frozen before any tuning and serve as the holdout. Every tool was driven through -its documented command-line interface, and every raw answer is stored with the -results. - -| Tool | Holdout (40) | All questions (120) | Latency p50 | Latency p95 | -| :--- | :---: | :---: | :---: | :---: | -| PlugBrain | 38 / 40 | 106 / 120 | 265 ms (33 ms with the daemon running) | 478 ms | -| CodeGraph | 38 / 40 | 103 / 120 | 280 ms | 311 ms | -| GitNexus | 36 / 40 | 90 / 120 | 1,134 ms | 3,465 ms | - -PlugBrain ties CodeGraph on the holdout and answers three more of all 120 -questions. CodeGraph's call edges are still more complete on deeply polymorphic -class hierarchies. Method, raw answers, hand-checked misses and how to rerun it: -[bench/v2/RESULTS.md](bench/v2/RESULTS.md). - -**Notes.** A Markdown vault with wiki-links, backlinks, tags, properties and -full-text search, in the same index as the code. A note can point at a file -(`code:`), a revision (`revision:`) and an agent run (`agentenlauf:`). Compared -with Obsidian there is no canvas, no mobile app and no plugin ecosystem. - -## Quickstart - -**Windows:** download `PlugBrain--win-x64.exe` from -[Releases](https://github.com/litbitrim/plugbrain/releases) and run it. The -installer brings its own Node.js runtime. - -**macOS / Linux:** download `plugbrain---.tar.gz` from the same -page, unpack it and put `bin/plugbrain` on your `PATH`. - -Then, inside your project folder: - -```bash -plugbrain init # register this folder, index it, add PlugBrain to your MCP clients -plugbrain serve # the local daemon: web UI and API on http://127.0.0.1:4310 -``` +### Give one agent project context -`plugbrain setup --all` (or `claude`, `codex`, `cursor`, `windsurf`, `hermes`, -`agy`, `opencode`) adds PlugBrain to an MCP client later. It backs up the -client's config first; `--undo` restores it. The data directory defaults to -`~/.plugbrain`; set `PLUGBRAIN_HOME` to choose another one. One page per client, -with the file it writes, how to check it took effect and how to remove it again: -[docs/agents/](docs/agents/). +Use an existing MCP client or the CLI. Review a client setup with `node --experimental-strip-types src/cli.ts setup codex --dry-run` before applying it; setup backs up edited configuration. The [client guides](docs/agents/README.md) name each file, verification step and undo path. Ask the client to find the code for your task, inspect callers and cite the source it used. MCP supplies context and controls; it does not automatically create or execute workers. -### Claude Code plugin +### Coordinate a team -``` -/plugin marketplace add litbitrim/plugbrain -/plugin install plugbrain@plugbrain -``` +Register distinct worker identities, queue bounded work, claim paths before edits, and retain the resulting commit and delivery evidence. The [agent protocol](docs/agent-protocol.md) defines turn and message discipline; the [CLI reference](docs/cli.md#swarm) describes the commands. A board state or successful message does not prove a worker edited, tested, delivered or integrated anything. Claims coordinate cooperating agents; they are not an OS sandbox. -The plugin starts the brain when a session needs it and gives Claude a skill that -asks the brain first: context before edits, impact before renames, notes before -re-deriving a decision. +![Real PlugBrain queued review task assigned to demo-builder; zero running or delivered workers](docs/images/task-demo.png) -## Privacy +*The review task is queued and assigned to a registered demonstration agent. No provider call or coding worker was started for this capture.* -- The store is one SQLite database under `PLUGBRAIN_HOME`. Indexing runs locally - and the UI is served locally. -- No telemetry, no analytics, no phone-home. The only network listener is the - local HTTP server on `127.0.0.1`. -- The HTTP API requires a bearer token that is generated on first start and kept - in `/auth.token`. The `swarm` commands work on the local store - directly and need no token. +## Capabilities and limits -## Development +- Code intelligence: AST-derived symbols, callers, context, impact, changed files and graph queries. Accuracy depends on the parser, language and indexed revision. +- Project knowledge: Markdown notes, wiki links, backlinks, tags, attachments and cited context packs. +- Coordination: shared tasks, turn states, messages, path claims, resource admission and commit approval. Delivery, independent review and integration are separate facts. +- Local operation: loopback HTTP UI/API, SQLite data, MCP and CLI. The UI is primarily German; documentation is primarily English. +- Windows is the locally exercised host. macOS/Linux CI and packaging are separate evidence; a local Windows success is not platform parity. -```bash -git clone https://github.com/litbitrim/plugbrain -cd plugbrain -npm install -npm test # unit and integration tests (node:test) -npm run serve # start the brain against the current folder -``` +The public source and local candidate have different implementation states. Historical benchmark numbers do not establish current technical superiority. Compare pinned versions, raw answers and the actual acceptance criteria before adopting a result. [Development](CONTRIBUTING.md), [security](SECURITY.md) and [HTTP API](docs/http-api.md) explain the operating details. -See [CONTRIBUTING.md](CONTRIBUTING.md) for details. +## Privacy and licensing -## License +PlugBrain performs indexing and stores project data locally. The HTTP service binds to 127.0.0.1, uses a generated authentication token and does not offer a public cloud endpoint. Never publish the data home, private notes, tokens or provider credentials. External AI clients have their own data policies. -[MIT](LICENSE) — Copyright (c) 2026 litbitrim +[MIT](LICENSE) · [Changelog](CHANGELOG.md) · [Issues](https://github.com/litbitrim/plugbrain/issues) diff --git a/docs/agent-notes/explicit-init-root.md b/docs/agent-notes/explicit-init-root.md new file mode 100644 index 0000000..c983de9 --- /dev/null +++ b/docs/agent-notes/explicit-init-root.md @@ -0,0 +1,5 @@ +# Explicit initialization roots + +An explicit directory passed to `init` is the indexing root. Calling `init` without a directory discovers the enclosing Git root of the current directory. Both paths validate a real directory before registration. An explicit nested example or notes folder therefore cannot silently widen a scan to an unrelated parent checkout. + +The CLI integration regression creates a parent Git repository and a nested folder, then checks the registered root and indexed file list. Parent source files and agent instruction files remain outside the explicit initialization. Dry-run and client configuration behavior retain their existing tests. This fixes initialization scope; it does not add a scan quota, change the parser or grant new filesystem rights. diff --git a/docs/agent-notes/notes-workspace-authority.md b/docs/agent-notes/notes-workspace-authority.md new file mode 100644 index 0000000..122d990 --- /dev/null +++ b/docs/agent-notes/notes-workspace-authority.md @@ -0,0 +1,5 @@ +# Standalone notes workspace selection + +Every notes command resolves one registered workspace. A single registered workspace can be selected implicitly; multiple workspaces require `--workspace ` or `--workspace=`. Unknown, empty, missing and repeated selectors fail before any knowledge read or write. Selecting a workspace does not register a Planet or grant filesystem permissions. Existing note access checks and optimistic write versions continue to apply. + +The CLI removes the selector and its value before parsing note text, paths and filters. A real-process integration test initializes two separate project folders, reads indexed Markdown without a Planet, checks each knowledge command, verifies explicit search isolation, writes and reads an allowed note, and rejects ambiguous or invalid selection. The test uses an isolated store and no client credentials. diff --git a/docs/channels.md b/docs/channels.md new file mode 100644 index 0000000..fe4bedd --- /dev/null +++ b/docs/channels.md @@ -0,0 +1,28 @@ +# Channels and downloads + +Checked on **4 October 2026** against the public repository, release metadata and candidate PRs. This reference does not promote a local build or authorize installation. + +| Version / location | Status | Distribution | +| --- | --- | --- | +| main, package 0.5.0-dev.1 | Public development source | Git checkout; Node.js 24+, npm and Git | +| tag 0.3.1 | Published GitHub release | Source archives only; release assets list is empty | +| 0.6.0-rc.1, [PR 6](https://github.com/litbitrim/plugbrain/pull/6) | Open candidate, not merged | Review source; no asserted public installer | +| 0.7.0-rc.2 | Local candidate with Windows QA evidence | Not shipped by this documentation branch; unsigned and not a public release | + +## Start from source + +Follow the [quickstart](quickstart.md). The CLI uses native TypeScript support under Node.js 24+. Runtime `npm ci` installs the pinned dependencies. The repository includes its web UI output; UI development additionally uses the nested UI dependencies and `npm run build:ui`. The source path requires no PlugPT app, model credentials or hosted account. + +## Platform evidence + +| Platform | Source prerequisites | Current evidence / limit | +| --- | --- | --- | +| Windows x64 | Node.js 24+, npm, Git | Source quickstart and local candidate UI checked; no public signed installer | +| macOS | Node.js 24+, npm, Git | CI owns platform validation; not exercised in this local Windows run | +| Linux | Node.js 24+, npm, Git | CI owns platform validation; not exercised in this local Windows run | + +GitHub's generated source archives contain repository files, not a packaged executable with its own Node runtime. Only a named release asset with a verified digest and applicable signature is a binary distribution. A build script, candidate commit, green subset of tests or screenshot does not establish release acceptance. + +## Product images + +The images use a real local 0.7.0-rc.2 candidate and an isolated three-file example project. They show existing knowledge and agent coordination screens. The agent is only registered, and its review task is queued; no external worker execution is claimed. [Capture metadata](images/showcase.json) records the version and source revision without publishing a data-home path or token. diff --git a/docs/cli.md b/docs/cli.md index 4c465a5..7052c2f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -13,7 +13,7 @@ usage: plugbrain [options] ### `init` Initialize PlugBrain in the current directory or specified path with a single command. -Discovers the git repository root (or uses the directory path), registers the workspace, executes initial code indexing with a live progress report, prints the local web UI URL, detects installed AI coding assistants (Claude Code, Codex, Cursor, Windsurf, Hermes, AGY, OpenCode) to enroll PlugBrain into their MCP client configurations, and adds the PlugBrain agent-protocol block to `AGENTS.md` (plus `CLAUDE.md` when present and not already referencing `AGENTS.md`). Running `init` multiple times is idempotent. +An explicit path is used as the exact indexing root. Without a path, PlugBrain discovers the current directory's Git repository root, or uses the current directory when none exists. It registers the workspace, indexes with progress, prints the local web UI URL, enrolls detected MCP clients and adds managed agent instructions unless those actions are disabled. Repeating initialization preserves workspace identity. See the [scope decision](agent-notes/explicit-init-root.md). ```bash plugbrain init [path] [--no-clients] [--no-agents-file] [--dry-run] @@ -465,6 +465,8 @@ Manage Obsidian-compatible markdown notes and knowledge graphs. plugbrain notes [args] ``` +Every subcommand accepts `--workspace ` (also `--workspace=`). With one registered workspace the selector is optional; with several it is required. Missing, unknown or repeated selectors fail before reading or writing. Notes initialized with `init ` work directly; Planet registration is optional. The selector does not change the note access policy. See [workspace selection](agent-notes/notes-workspace-authority.md). + **Subcommands:** - `notes list [--limit ] [--json]`: List notes with title, type, and link counts. - `notes query `: Property query over frontmatter (e.g., `"typ=gate UND stand=offen"`). diff --git a/docs/images/knowledge-demo.png b/docs/images/knowledge-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..45fa88b4a27843aedabd1c378bebc796351daadd GIT binary patch literal 32286 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 literal 0 HcmV?d00001 diff --git a/docs/images/plugbrain-wordmark.svg b/docs/images/plugbrain-wordmark.svg new file mode 100644 index 0000000..53b12d7 --- /dev/null +++ b/docs/images/plugbrain-wordmark.svg @@ -0,0 +1 @@ +PlugBrain — one local project brainPlugBrainOne local project brain. diff --git a/docs/images/showcase.json b/docs/images/showcase.json new file mode 100644 index 0000000..ca79fa6 --- /dev/null +++ b/docs/images/showcase.json @@ -0,0 +1,17 @@ +{ + "captured": "2026-10-04", + "version": "0.7.0-rc.2", + "source": "86b6b2a0a82dde8305e3382ccf6fd2b22c0c9edb", + "distribution": "Local unsigned QA candidate; not delivered by this documentation branch", + "project": "Shop demo", + "files": 3, + "notes": 2, + "registeredAgents": 1, + "executedWorkers": 0, + "providerCalls": 0, + "images": [ + "knowledge-demo.png", + "task-demo.png" + ], + "privateData": "Isolated example notes and source; no owner project, credential, private path or fleet" +} diff --git a/docs/images/task-demo.png b/docs/images/task-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..e84deb3f96702bc29edbd18e8f0e70ab136a5980 GIT binary patch literal 18501 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

4eI;w^)k3lz?IFe-pWuLb^izhiHOkj< ziu&Zo;?N)1K#xvS;dJ<_Nr@(=9KLZcgkGu^{iG0YZQ3J+MHS|3fb|=#$M$3-NorvJ zX?U!VXX_?hH~O@9%*dW&S;EoV4#@G6F5|;U zip?uWtP)%2ujDyE4T5;;C?$`Va|A^7_hlqm1=t^d)4G5Ok#7HhJj=3EZQm!Cizhv{ z={&(A-XDdR<`3oqr1@wgY}vCD2~e|uVrC=ImNIi{Yk#d7A14H?A3I>>&wVK`o8Iq$ zrES7eCR>Vy#mF&0rKK6B_lKPZZd^ZQ-MZ}@7)Bx!X3vb)N{TuP1vyKt^_A$3 zs@k*oO4aSfm33!N^o8#L+wf?j>*!)Vb4$*X4vZFU5nA4JvC5z+NNlaGX+~9TrmMuG z51Drdzj7D<_y>nY!$dy>rJ5rxT{R4Fb;VfoOc)>aC(_>ElO;-L(<+GTT`{bH!Fd(O zw&6$)Gggak#0gdYeD}VY9Qb3_wWfH=$KilcMD8rW-UF87>Z*7)whM4ifVxkU<*$IZ z8#Ed4Uqm4!=71KUPu7Ny`wavQS?BK#BRQ5cby=qX84dgfu>GU(g~S|C^&gG?KaYC+ z^V^z9iWvLYjF4q8(=)hV0lu@NFsPm$(EK8ji?vCQnr1!(Xdo1Bwr%YNw#xLXRz>J0 z%PzUil4nIyHA1DMudhZ#Ut`V9B=!7U7rjqg>un@&^;G33^V^HfkY(;E!?W{@y>zrX~W*2-2>L)`x08| ztm3>SYg6Fxk0U*^4k~yz6FAtSdjMGvA_P1)duu+6OPS;HwmlVX=)RkT4U7KpQ4q{lT=4334D~WvB^k|)-7ceZufc;L9Mg-u-;fs zPftUue&PvUf*I)1)OUg}5}751%*0Xo5!G!3*Vm<_mFg-~C$WdI;Zjlrg>`1W6vFhJ zTpc7}B(ozL*iknk0|V~_Ze#pu9_bG$kDs^eY#SurRI@82$iBfohYiHP(-@sxih#Pz zn>-!zf{=CJhP<_ZF)NQI-xUEbnu_c^8LNMT4cBzWq*Payu2Fc;xejgZmP;b8K8~lT ziVlOq&riRwPpNASS6PN#8e_(`Y6@hek}K`VOMdnD(@)v0bB)#=zOuSLn$sW5t*z%SP$&eI%=Aa>D{Y)J@{H#}Xk|D6WxvdjM#RRB4Eg549 zWmlo#Dn~WOsG0SC0z=DZ`5HI&a8sJ4n74r{Q4O2L(Y5&tznD*50zFdUQ+{Dz?fA*B zp)HHl)cq+lMTsG%VfN&89`!r9((x>Ar%KKBvUjB#q}Pa6-gxq4)(PH8>XU`GBSmkp zbz7}BduJ=)9`r!@ofsik&vbB(z$np>^v-ASkl87jc%BWke1mbrYlhgeaPM?0H^seb zL;n!yC;re^_wl0S{s}{-fj;*Gu#=iJiHqV6NXPk9Q?X+b_n7m`FY)GTQl;xa>EJzXTAndbCo*3N(+xl?mXcXvS(`Zm~p(>(kT-T*302y1LH`mgdYQ1(Z%a`824)_O9*w*Gze%{j~}}bLaQ8 z51I(rLJn6Z4L z!FyGG)`Ny$ouy9Kjm2lzyp8u8da1j*?J_tSBKBe_R7O4R(S!XBwJ~b*N6$LL&qcq= zyX+9qUo}0%aU^9_YK_&N1**#Fla4E>O_~2${4k-CzVzWZ7CfE0yEc#2L`Y+4XfXExu-wBrMB@fcPQ{A=o_|6S?2S6*VLa>N;cO7tm0`!UGFaV$o&uGOurwiagrr4U zh2Jyd=@CU?FN%&K#1zvB;U%>#&l@I^&V#X0boO|V(`^Avp z-QVqvB3oy6EJ~iL!XYOMY5Z)ywAG1AI{RDZLVTL|To;V7%?z{-+nUVIuL>7s?Su5k z1UBs@+s)7?yWc7)6lccQ6&9GT5HvX|$TjF>)n){NuZ(n*zYTOe*eYqD9B`}E0M7`W zFAUu=Wm!5)}H{sn%p!i1fJnwzlCxp;+3r zxuA8dff=DTuCzOBM5T)BplyoyT+r^6oHJ9Cv4^FcVQp;6_}|)JG4n4>di2CI5`4cD z_j>PW`e-H!m`jp&+i=#omwe%7cXJ%ow3m1C%UMHXle6x0X89AUn=%Gg3?aZyNu8g| zvdVnCoFVjP^(|uupi(OMO~Rt@+n_CGBzb@MT6XP^9y?ER@+W9#kgP8yOss#DFZ!bc z(K%pJpih3UdTcdyWY-vpv=_^uz(~}6q>REN%-WA9E?9{&gHo(=y$84QI5L+Nw zptpv_-2h9PgL*qucBD!eT8uApOi@A`jPq;qQRJXmFI#&p(jxA%~1PukqK* zoaj0b>?0*#txY8x?y~kiW#A&4w$GLOhKYrIuaUNGD&YY`zt2L_-q|~$9a*<#9tE#X zMBt~4s(hT}*ZX)*f_wc9;1oarEOY8>EOX-pR0M01 z8zM$~s713y1YF$B^rIlqgUbBFNcW1*8YMjwik!u()@N^hK+8{Nv0>>VZ zv5dx)(T|G0N=J{#CmhJm)WgHbF$oTAK?>{vWU5jJHRQLbToY=fdi%rBa2N8h7u2;9 z^{^6sS9Y3svsS-p4S$7cz)E8Rq<7!m?QQP3*x@_c8|D40%#}1Pc&Ms@k z-Eu}#7xG#zbiI}}nhF3{0}!RHtY+dcvMOY9*r)!8~Sy>?yTOL9&@ozfj*;3 zS;#5eNdgH`Wge)qdoB=`tR6Es0E9nF%mEjF=28BA3f4coZG6-hAlB+EH31EFpy?EK zkE>SsXA8QT3bV%4bED{*MyZNgcP;xkaP@Mlv4+m8{S_!oY2P^oTs$hAb~pnZk9k=s zs7Cb~>)}{Uk~}I&9hx856%Apx5wERfmv{X;?MH|=k{$yOa`ZF4nTr{DmKLVGB#I!K&5#CS8a7^4neYWz!|7+^iun_1`HtT zfkxmzW9n`~;EY_q#7xEMC)TAUMEuxL{!)Wu5(WzN@b0+kxjB6=O*2|f1RA^lk~I{y z5Kb9Q%&hrNwRGzBut%WlL zBALTnI}q9NGrjmH_-lCmRXq5_=4C5WBTm&Qyzj2juY4Wt?(eFq?BmgRity-~U;O=A zn0a2_V(xTCNH=;3-yz99@&Q%(sqPx0@Bz6R1iCuGq!?=1NIPdw6Whf$9P?8b3z8?E z#ZPv7q)k_ZXI~ry`&tDVsrS8zA-0D$bg8P{&l3{WM9XicN_T*L`Cdx2ISO?NfLq*nM+krF?Xu1Q>C}P&>7uB^i*5 z6CK_y>YjsIi-W@B7RqJi()Au@@JU$zD)eZy@7ZY;wi0F?|}Ij5Jsplok_jZFgbJ~ z5ElQWO-p4RVAm3ZxjpqFJ0HOJfzIXT;1$ASgp*}1*?0a;I)nP)jrRr1kqC}f#!EIzsm`g z#tQrOwaB7@xD_adY_jcnbxJt~;=Y5mG{md@a#JaJIX36QYVgOOBd44kJbDcTFN*V@ zg|*(vee0OuRUZd=3#13x6dRZ4ICl#b2nRU05i@UhAHtot#yJfA0vkHkS{Y2r$&*<~ zQP;OD`IN5E{~aj&>rvZ@2ku0Z+Rdec@{+*^1!pdo^*&828WOUu(I5IMio42mY3A-6 zLYqpK+nley;lYukT{}FouRhyf{VD1Br+v?u@RsF<=+m~3^dmgYm*811fae(W)fvtCBrO;L& zs-6ccs~cvQ=C^Dt^``|ArmY|lNYep%(_8;+&~)YQX@H;ER?S;w1%e(~@4c}Ej{jYg z53o-WfHF!FVJRb)y0=VpIfT48TyROK<9-}FYewoOcJD`c92}$sUL3fVI(j_beTb_L ztm(GB*1P^9dqM3W_y@SP3jG?YJq~=VnyCH1o7n75a)BaIpS6{3L_Osf?SUUz!(kl{ ztVF)1iKwB5cqU37>_hvszszDI+ZNfhDRk|Z5*4yB057aa0pf5FNSnY@JdBTCOU(Ywj=j_TL5nc za4pBFXI7CHqMUnsJKlu!#kua^GcxWwzz6Xy)|1C~%+$*L_bc1nu1|V-4@FxaY`V7b_|BaXZWX(#m zW&?QJR&PcF%T@V56Tk&rN`P6E2^&!ta^%mHG~X>`Np==Cq|*;rmaR0e&TUi`?J8t| zQK~ieA=n5NRAf1231>;~*Ux9Y6Y?$2+`NUV61=szb^H3O%NeC|rainXcN-X?4OKF% z`KQ?_s7mq{o#B=Zxyyw=&hKGU%(_|Hy~VlV{-pfVkUR9%L;{5sKyLfK%C@-mzyWS7@< zSp^^ebauYP9IO4moYeo%Zvzxe++P(8%&}4!80OKBPQPi(|K#VglJbF9@W|g2>wkb7 z@ob>-uj26cqW-%JI(U-x4xd;5{TAn=EEV=2zC@6i1Iqq|wR&kF>h?cR0c=Wq{)ExP(EHvy$GFVM&Pu&RT?(Ly0iRe&zs<-(@5|Lz)*3A906nVSak-Oc%GdEn)+_*B z8T{(L)xyCH!yPTh#UwjUB11naU13!c527Ximwf)e-`>9qcX+jIyaGhZh5f_MmkE{{ zzL7s3eya*N0p0%N-Tk{8e*e5fVvg11e?Wl#7ax;|R{@V?9S3By6D(OO+4AMz0P%kw z&RPE5euPG|P{8*Yx~9P49|t%7ggIGa|1F4S%;nJ`xdt6)O=xH|gumbyi2?QCMn*-Y zUZprRMb3e#>^W=60w*`>ud`_b+TzDZ-*ZOC#U;f6j4g(Erktb)RDv}6GCvOc+|iL2 z&6bHt5itH>!_tT6yu#XyGP8}IzKtHX%xWTDT4A-l`}KsXWODB3(Woa!wXr>Q{-WhY zhB`n+c*B1(Bs;&^mBQ|7|to=}Ov;V8v$LgMl#Z`aBTj_2D~!B{v@b6fUYOUv+F@eGne13{HyRzwLP1Oo%{E+#zX?Fit zRyx;OAhT3@Zm+L(ztwJUb(={{X2xn!xtNAL5e|IX4U32l=dg`#!C`Zl*f!KRk{}+H} zZBcpf1ekypnHppzkCkTjBON)819v_$rjDp)QfbccrEC#dPie|z{Y-jOT&}?WTjU^p ze8xIzKv*-04qYM6D8*`0&^_cX7QBs7RGzzxGph+ok>Kz61Ys8>6OmKvlhj$N=s~Y) z+@bpJR*X|uw0366xb$Cr_P>B}`+=h^UXlHou%*=e>O-3$o!q~-Wfue@^3$vwG*(va zsMglUP<tLoNoTVI-ts=^mW@X@g_Y~ec2Ig{>t zeI}(JNgsmZm_WRK+LaY^N*Q8MIYsV^vT~#+?ekF!*w!^Nb1pDC5kgf1_q`WH_UX&6TC>>3lh**o5t7=z=74Vh523hmMzj~)o+aPE zxU*Pi)?#lj{Wl}#O$u|9z|L=t=6gS^LfcAL;3z+k)X_)`Pr>6O8g)6ga)o$OhRxn| zT15nD*T3Gi@|wor`?HxYh*DSRb8#0-Z83b(&ibrknSAf8_vz>s=1!L`YCeRNy;VU`qnXZFeRu1&OAN~`GE-WjpcZ#^b@x*>>> z(-eo-IQ)ll(M)2Y1=LYm8h?Ny&P}HWH+%ZS=h`E(ZJ{TfMqPq za2DDcx+5B{9)j)pNdW<5V|?4_ns&Gl)G$|b9p|M1S)-tMhIZ?peb#N#Czrlmp{C2wWbwySzInBZ2ulMRk`#%9HZ9iQEVy@KVevL7p4Y>LS2T0M}MiV z*{ZKKgi0#;GsjJ59~-yg(vjnJwXOuOz*X`E@a|r z@j_!9D!cI9A|gj%o}c)qtO+&5+D5}dANn!cDYjen7FC(VVWq}@o!nQFIq70oOWJlN z#39x9rCJ-(^dCFgn~Z)FBB^7SGir)Ab3+7(5fOd}q|!4#NJ7V~E_N0z^$a;YBLRCj zmY)$pPUH!}b@gxb)eISZCUrkpHPQ|2hs|z4QN3)Sn;Y-KvtxD8#}SD!g359_u)X>( zN8J|)c{3K7Ll*i54LkGBByB~xk1yHj7cXB}ZZ8FL12rE8#k#nJ zn9rVz;pv6Bs%}%bMh18G-nK&V#t8Tr+7izcB9+z!Zbm1jx=Ml0zt^D1NIE}Uig#Yi zr-YVoZP#p(N@ibU9taa4|2TDnp#@YinN_PV{sNpFWm)yZ ZAZxg@MkFt67$^bh+%dRa_T!_U{y*HEyp;d| literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..89ce23c --- /dev/null +++ b/docs/index.md @@ -0,0 +1,19 @@ +# Documentation + +Choose the page matching the work you want to do. PlugBrain can be used as a local knowledge browser before connecting any coding agent. + +| Goal | Start here | +| --- | --- | +| Install prerequisites, index one project, open the UI | [Quickstart](quickstart.md) | +| Understand available source, previews and downloads | [Channels and downloads](channels.md) | +| Connect Claude Code, Codex, Cursor or another MCP client | [Client guides](agents/README.md) | +| Coordinate workers without overlapping edits | [Agent protocol](agent-protocol.md), [CLI](cli.md#swarm) | +| Call the local service or inspect MCP tools | [HTTP API](http-api.md), [MCP tools](mcp-tools.md) | +| Contribute code and tests | [Contributor guide](../CONTRIBUTING.md), [security](../SECURITY.md) | +| Read the German entry | [Deutsch](../README.de.md) | + +## Troubleshooting + +Node below 24 is unsupported by the current source. If `plugbrain` is not on PATH, use the explicit Node command from the quickstart; cloning a repository does not install an executable globally. An occupied loopback port belongs to another service until its identity is verified: choose another port for a trial instead of killing it. A source ZIP is not an installer. Keep `PLUGBRAIN_HOME` separate from a checkout and back it up before a version change. + +The [channel table](channels.md) identifies unverified builds and remaining platform checks. An unavailable agent, missing credentials, historical benchmark or local candidate screenshot does not establish a working installed product. diff --git a/docs/quickstart.md b/docs/quickstart.md index 15c687d..ab394d1 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -5,8 +5,23 @@ standing in. You type **one** command inside your project folder, and from then on Claude Code, Codex, Cursor and friends know that project through PlugBrain — no workspace id, no token, no copy-pasting paths between config files. -PlugBrain needs **Node.js 22+** and nothing else: the graph lives in a single -SQLite file, no database server. +The current source requires **Node.js 24+, npm and Git**. The graph lives in a single SQLite file, with no database server. Public release 0.3.1 has no installer assets; use the source path below. [Channels](channels.md) distinguishes source and local candidates. + +## Source checkout + +The source commands select the entry candidate branch, including the initialization and standalone-notes fixes. Main remains a separate development channel until review and integration. Install the dependencies in a fresh checkout: + +```sh +git clone --branch codex/brain-public-entry-20261004 --single-branch https://github.com/litbitrim/plugbrain.git +cd plugbrain +npm ci +node --experimental-strip-types src/cli.ts init ../my-project --no-clients --no-agents-file +node --experimental-strip-types src/cli.ts serve +``` + +Replace `../my-project` with a real project folder. Open the printed loopback URL. For an isolated trial, set `PLUGBRAIN_HOME` to a new local directory before these commands: PowerShell uses `$env:PLUGBRAIN_HOME = "$env:TEMP/plugbrain-trial"`; a POSIX shell uses `export PLUGBRAIN_HOME="$HOME/.plugbrain-trial"`. Keep that choice consistent for the whole trial. + +The examples below use the short name `plugbrain` for an installed CLI. In this source checkout, replace it with `node --experimental-strip-types src/cli.ts`. Run commands from the checkout and pass the intended project path explicitly. Installing dependencies does not put a global `plugbrain` command on PATH. Client setup is optional and can be previewed before it changes config files. ## 1. One command diff --git a/src/cli.ts b/src/cli.ts index 227d6a2..e5eb456 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -508,11 +508,11 @@ const flagValue = (args: string[], name: string): string | null => { return at === -1 ? null : args[at + 1] ?? null } -function notesQuery(args: string[]): void { +function notesQuery(args: string[], workspaceId: string): void { const asJson = args.includes('--json') const text = args.filter(arg => !arg.startsWith('--') && !/^\d+$/.test(arg)).join(' ') const limit = Number(flagValue(args, '--limit') ?? 200) - const result = queryNotes(db, singlePlanetId(), text, { limit }) + const result = queryNotes(db, workspaceId, text, { limit }) if (asJson) return jsonOut(result) console.log(`${result.total} note(s) match ${result.parsed}`) for (const note of result.notes) { @@ -741,7 +741,7 @@ async function diskCommand(args: string[]): Promise { function gbText(bytes: number): string { return `${Math.round(bytes / 1024 ** 3 * 10) / 10} GB` } -function notesSearch(args: string[]): void { +function notesSearch(args: string[], workspaceId: string): void { const asJson = args.includes('--json') const text = args.filter(arg => !arg.startsWith('--') && !/^\d+$/.test(arg)).join(' ') if (text.trim() === '') { @@ -752,7 +752,7 @@ function notesSearch(args: string[]): void { // just to the file: that is the difference between a search and a search you // can act on. const withLines = args.includes('--lines') - const result = searchNotesWithLines(db, singlePlanetId(), notesAgent(), text, { + const result = searchNotesWithLines(db, workspaceId, notesAgent(), text, { limit: Number(flagValue(args, '--limit') ?? 50), lines: withLines, }) @@ -764,10 +764,10 @@ function notesSearch(args: string[]): void { } } -function notesRead(args: string[]): void { +function notesRead(args: string[], workspaceId: string): void { const relPath = args.find(arg => !arg.startsWith('--')) if (!relPath) { console.error('usage: plugbrain notes read [--json]'); process.exit(1) } - const note = readNote(db, singlePlanetId(), notesAgent(), relPath) + const note = readNote(db, workspaceId, notesAgent(), relPath) if (args.includes('--json')) return jsonOut(note) console.log(`${note.title} (${note.path})`) console.log(` version ${note.hash} ${note.bytes} bytes ${note.indexStale ? 'STALE in index' : 'indexed'}`) @@ -785,17 +785,17 @@ function notesRead(args: string[]): void { } } -function notesBacklinks(args: string[]): void { +function notesBacklinks(args: string[], workspaceId: string): void { const relPath = args.find(arg => !arg.startsWith('--')) if (!relPath) { console.error('usage: plugbrain notes backlinks '); process.exit(1) } - const rows = backlinksOf(db, singlePlanetId(), relPath) + const rows = backlinksOf(db, workspaceId, relPath) if (args.includes('--json')) return jsonOut(rows) if (rows.length === 0) { console.log('nobody links here'); return } for (const row of rows) console.log(` ${row.path}:${row.line}${row.alias ? ` (${row.alias})` : ''}`) } -function notesGraph(args: string[]): void { - const graph = noteGraph(db, singlePlanetId(), { +function notesGraph(args: string[], workspaceId: string): void { + const graph = noteGraph(db, workspaceId, { focus: flagValue(args, '--focus'), depth: Number(flagValue(args, '--depth') ?? 1), filter: flagValue(args, '--filter'), @@ -810,7 +810,7 @@ function notesGraph(args: string[]): void { if (graph.coverage.truncated) console.log(` TRUNCATED at ${graph.nodes.length}`) } -function notesWrite(args: string[]): void { +function notesWrite(args: string[], workspaceId: string): void { const relPath = args.find(arg => !arg.startsWith('--')) if (!relPath) { console.error('usage: plugbrain notes write --from |--content [--expect ] [--allow-generated]') @@ -824,7 +824,7 @@ function notesWrite(args: string[]): void { } const content = from === null ? String(inline) : readFileSync(from, 'utf8') const expected = flagValue(args, '--expect') - const result = writeNote(db, singlePlanetId(), notesAgent(), relPath, content, { + const result = writeNote(db, workspaceId, notesAgent(), relPath, content, { ...(expected === null ? {} : { expectedHash: expected }), allowGenerated: args.includes('--allow-generated'), }) @@ -1220,7 +1220,7 @@ function initCommand(args: string[]): void { console.error(`not a directory: ${dir}`) process.exit(2) } - const root = findGitRoot(dir) ?? dir + const root = given === undefined ? findGitRoot(dir) ?? dir : dir if (dryRun) console.log('dry run (nothing is written):') const record = dryRun ? planWorkspaceRoot(db, root) : registerWorkspaceRoot(db, root) const state = record.created @@ -1369,15 +1369,37 @@ switch (command) { case 'repos': reposCommand(args); break case 'disk': await diskCommand(args); break case 'notes': { - const [step, ...rest] = args - if (step === 'query') notesQuery(rest) - else if (step === 'search') notesSearch(rest) - else if (step === 'read') notesRead(rest) - else if (step === 'write') notesWrite(rest) - else if (step === 'graph') notesGraph(rest) - else if (step === 'backlinks') notesBacklinks(rest) + const [step, ...input] = args + if (!['query', 'search', 'read', 'write', 'graph', 'backlinks', 'list'].includes(step ?? '')) { + console.error('usage: plugbrain notes … [--workspace ]') + process.exit(1) + } + let requested: string | undefined + const rest: string[] = [] + for (let index = 0; index < input.length; index += 1) { + const arg = input[index]! + if (arg === '--workspace' || arg.startsWith('--workspace=')) { + const value = arg === '--workspace' ? input[++index] : arg.slice('--workspace='.length) + if (requested !== undefined || !value?.trim() || value.startsWith('--')) { + console.error('pass exactly one --workspace ') + process.exit(2) + } + requested = value + } else rest.push(arg) + } + const workspaceId = requested ?? singleWorkspaceId() + if (!db.prepare('SELECT id FROM workspaces WHERE id = ?').get(workspaceId)) { + console.error('workspace is not registered') + process.exit(2) + } + if (step === 'query') notesQuery(rest, workspaceId) + else if (step === 'search') notesSearch(rest, workspaceId) + else if (step === 'read') notesRead(rest, workspaceId) + else if (step === 'write') notesWrite(rest, workspaceId) + else if (step === 'graph') notesGraph(rest, workspaceId) + else if (step === 'backlinks') notesBacklinks(rest, workspaceId) else if (step === 'list') { - const result = listNotes(db, singlePlanetId(), { limit: Number(flagValue(rest, '--limit') ?? 200) }) + const result = listNotes(db, workspaceId, { limit: Number(flagValue(rest, '--limit') ?? 200) }) if (rest.includes('--json')) jsonOut(result) else { console.log(`${result.total} note(s)`) diff --git a/test/notes-cli-workspace.test.ts b/test/notes-cli-workspace.test.ts new file mode 100644 index 0000000..03d38f9 --- /dev/null +++ b/test/notes-cli-workspace.test.ts @@ -0,0 +1,56 @@ +/** Standalone knowledge commands use registered workspaces without requiring a Planet. */ +import { strict as assert } from 'node:assert' +import { spawnSync } from 'node:child_process' +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { test } from 'node:test' +import { workspaceIdFor } from '../src/planet.ts' + +test('notes CLI selects one workspace, rejects ambiguity, and never searches the selector as text', () => { + const root = mkdtempSync(join(tmpdir(), 'plugbrain-notes-cli-')) + const home = join(root, 'home') + const first = join(root, 'first') + const second = join(root, 'second') + for (const dir of [home, first, second]) mkdirSync(dir) + writeFileSync(join(first, 'Checkout.md'), '# Checkout\n\nCheckout calculates the total.\n') + writeFileSync(join(second, 'Private.md'), '# Other project\n\nCheckout belongs to another workspace.\n') + const run = (...args: string[]) => spawnSync(process.execPath, [ + '--experimental-strip-types', join(import.meta.dirname, '../src/cli.ts'), ...args, + ], { + cwd: root, encoding: 'utf8', windowsHide: true, + env: { ...process.env, PLUGBRAIN_HOME: home, PLUGBRAIN_CONFIG_HOME: home, HOME: home, USERPROFILE: home }, + }) + try { + assert.equal(run('init', first, '--no-clients', '--no-agents-file').status, 0) + const implicit = run('notes', 'search', 'Checkout', '--json') + assert.equal(implicit.status, 0, implicit.stderr) + assert.equal(JSON.parse(implicit.stdout).total, 1) + assert.equal(run('notes', 'read', 'Checkout.md', '--json').status, 0) + assert.equal(run('init', second, '--no-clients', '--no-agents-file').status, 0) + assert.equal(run('notes', 'list', '--json').status, 2) + const id = workspaceIdFor(first) + for (const selector of [['--workspace', id], [`--workspace=${id}`]]) { + const scoped = run('notes', 'search', 'Checkout', ...selector, '--json') + assert.equal(scoped.status, 0, scoped.stderr) + const result = JSON.parse(scoped.stdout) as { total: number; query: string; hits: Array<{ path: string }> } + assert.equal(result.total, 1) + assert.equal(result.query, 'Checkout') + assert.equal(result.hits[0]?.path, 'Checkout.md') + for (const command of ['list', 'query', 'graph', 'backlinks', 'read']) { + const positional = ['read', 'backlinks'].includes(command) ? ['Checkout.md'] : command === 'query' ? ['typ=guide'] : [] + const selected = run('notes', command, ...positional, ...selector, '--json') + assert.equal(selected.status, 0, `${command}: ${selected.stderr}`) + } + } + for (const selector of [ + ['--workspace', 'ws-not-registered'], ['--workspace'], ['--workspace='], + ['--workspace', '--json'], ['--workspace', id, '--workspace', id], + ]) assert.equal(run('notes', 'search', 'Checkout', ...selector).status, 2) + assert.equal(run('notes', 'write', 'Added.md', '--content', '# Added\n', '--workspace', id, '--json').status, 0) + const added = run('notes', 'read', 'Added.md', '--workspace', id, '--json') + assert.equal(added.status, 0, added.stderr) + } finally { + rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) + } +}) diff --git a/test/setup-init.test.ts b/test/setup-init.test.ts index 9c64d92..0a8f2c5 100644 --- a/test/setup-init.test.ts +++ b/test/setup-init.test.ts @@ -100,6 +100,27 @@ test('S2: init registers once, indexes, and enrolls a detected client', () => { } }) +test('an explicit nested folder is indexed without expanding into its parent Git repository', () => { + const f = makeFixture() + const nested = join(f.repo, 'example') + mkdirSync(nested) + writeFileSync(join(nested, 'selected.ts'), 'export function selectedOnly() { return 1 }\n') + try { + const output = runCli(['init', nested, '--no-clients', '--no-agents-file'], tempEnv(f.home), f.repo) + assert.match(output, /generation/) + const db = openStore(join(f.home, 'plugbrain.db')) + try { + const rows = db.prepare('SELECT root FROM workspaces').all() as Array<{ root: string }> + assert.deepEqual(rows.map(row => row.root), [nested]) + const files = db.prepare('SELECT path FROM files').all() as Array<{ path: string }> + assert.equal(files.length, 1) + assert.match(files[0]!.path, /selected\.ts$/) + assert.equal(existsSync(join(f.repo, 'AGENTS.md')), false) + assert.equal(existsSync(join(nested, 'AGENTS.md')), false) + } finally { db.close() } + } finally { f.cleanup() } +}) + /** A line-oriented JSON-RPC client over a child's stdio. */ function rpc(child: ChildProcess) { const pending = new Map) => void>() From 623f53ae8ff263bc1cd45e12fa37abb46db52da7 Mon Sep 17 00:00:00 2001 From: plugmil Date: Sun, 4 Oct 2026 22:43:59 +0200 Subject: [PATCH 2/2] test: compare explicit workspace roots using physical paths --- test/setup-init.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/setup-init.test.ts b/test/setup-init.test.ts index 0a8f2c5..91ab230 100644 --- a/test/setup-init.test.ts +++ b/test/setup-init.test.ts @@ -9,7 +9,7 @@ import { strict as assert } from 'node:assert' import { test } from 'node:test' import { execFileSync, spawn } from 'node:child_process' -import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import type { ChildProcess } from 'node:child_process' @@ -111,7 +111,7 @@ test('an explicit nested folder is indexed without expanding into its parent Git const db = openStore(join(f.home, 'plugbrain.db')) try { const rows = db.prepare('SELECT root FROM workspaces').all() as Array<{ root: string }> - assert.deepEqual(rows.map(row => row.root), [nested]) + assert.deepEqual(rows.map(row => row.root), [realpathSync.native(nested)]) const files = db.prepare('SELECT path FROM files').all() as Array<{ path: string }> assert.equal(files.length, 1) assert.match(files[0]!.path, /selected\.ts$/)