bikky 0.3.13 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CONTRIBUTING.md +206 -0
  2. package/README.md +64 -20
  3. package/dist/config.d.ts +49 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +125 -4
  6. package/dist/config.js.map +1 -1
  7. package/dist/daemon/loop.d.ts.map +1 -1
  8. package/dist/daemon/loop.js +15 -1
  9. package/dist/daemon/loop.js.map +1 -1
  10. package/dist/daemon/qdrant.d.ts.map +1 -1
  11. package/dist/daemon/qdrant.js +0 -1
  12. package/dist/daemon/qdrant.js.map +1 -1
  13. package/dist/lib/qdrant-pool.d.ts +57 -0
  14. package/dist/lib/qdrant-pool.d.ts.map +1 -0
  15. package/dist/lib/qdrant-pool.js +104 -0
  16. package/dist/lib/qdrant-pool.js.map +1 -0
  17. package/dist/mcp/api.d.ts +56 -19
  18. package/dist/mcp/api.d.ts.map +1 -1
  19. package/dist/mcp/api.js +133 -72
  20. package/dist/mcp/api.js.map +1 -1
  21. package/dist/mcp/helpers.d.ts +0 -1
  22. package/dist/mcp/helpers.d.ts.map +1 -1
  23. package/dist/mcp/helpers.js +2 -15
  24. package/dist/mcp/helpers.js.map +1 -1
  25. package/dist/mcp/helpers.test.js +3 -21
  26. package/dist/mcp/helpers.test.js.map +1 -1
  27. package/dist/mcp/index.d.ts.map +1 -1
  28. package/dist/mcp/index.js +29 -14
  29. package/dist/mcp/index.js.map +1 -1
  30. package/dist/mcp/tools.d.ts +0 -7
  31. package/dist/mcp/tools.d.ts.map +1 -1
  32. package/dist/mcp/tools.js +337 -219
  33. package/dist/mcp/tools.js.map +1 -1
  34. package/dist/mcp/types.d.ts +0 -3
  35. package/dist/mcp/types.d.ts.map +1 -1
  36. package/dist/routing.d.ts +53 -0
  37. package/dist/routing.d.ts.map +1 -0
  38. package/dist/routing.js +129 -0
  39. package/dist/routing.js.map +1 -0
  40. package/dist/routing.test.d.ts +2 -0
  41. package/dist/routing.test.d.ts.map +1 -0
  42. package/dist/routing.test.js +79 -0
  43. package/dist/routing.test.js.map +1 -0
  44. package/docs/config/fully-hosted.md +57 -0
  45. package/docs/config/hosted-models.md +50 -0
  46. package/docs/config/hosted-qdrant-local-models.md +39 -0
  47. package/docs/config/local.md +34 -0
  48. package/docs/configuration.md +374 -0
  49. package/docs/screenshots/dashboard.png +0 -0
  50. package/docs/screenshots/graph.png +0 -0
  51. package/docs/screenshots/memory.png +0 -0
  52. package/package.json +6 -3
  53. package/dist/mcp/api.test.d.ts +0 -6
  54. package/dist/mcp/api.test.d.ts.map +0 -1
  55. package/dist/mcp/api.test.js +0 -130
  56. package/dist/mcp/api.test.js.map +0 -1
  57. package/dist/mcp/tools.integration.itest.d.ts +0 -23
  58. package/dist/mcp/tools.integration.itest.d.ts.map +0 -1
  59. package/dist/mcp/tools.integration.itest.js +0 -171
  60. package/dist/mcp/tools.integration.itest.js.map +0 -1
  61. package/dist/mcp/tools.test.d.ts +0 -16
  62. package/dist/mcp/tools.test.d.ts.map +0 -1
  63. package/dist/mcp/tools.test.js +0 -908
  64. package/dist/mcp/tools.test.js.map +0 -1
@@ -0,0 +1,206 @@
1
+ # Contributing to bikky
2
+
3
+ Thanks for your interest in bikky! We welcome PRs of all sizes — from typo fixes to new daemon features. This document covers the practical bits: how the repo is laid out, how to run the tests, and what we look for in a contribution.
4
+
5
+ ## Repository layout
6
+
7
+ bikky is a small monorepo:
8
+
9
+ ```
10
+ .
11
+ ├── src/ # Core CLI + MCP server + daemon (published as `bikky`)
12
+ │ ├── cli/ # `bikky <subcommand>` entrypoints
13
+ │ ├── daemon/ # Background workers: extraction, consolidation, staleness, …
14
+ │ ├── mcp/ # MCP server (the surface AI agents call into)
15
+ │ ├── prompts/ # Versioned LLM prompt registry
16
+ │ └── llm/ # Provider adapters (OpenAI, Bedrock, Ollama, Portkey)
17
+ │ ├── embedding/ # embedding registry + providers
18
+ │ └── inference/ # chat-completion registry + providers
19
+ └── packages/
20
+ └── ui/ # Local web UI (`bikky-ui`) — Hono server + React frontend
21
+ ├── src/lib/ # - config, qdrant client, embeddings
22
+ ├── src/routes/ # - REST API routes
23
+ └── app/ # - React/Vite frontend
24
+ ```
25
+
26
+ ## Setup
27
+
28
+ ```bash
29
+ git clone https://github.com/bikky-dev/bikky.git
30
+ cd bikky
31
+ npm install
32
+ cd packages/ui && npm install && cd ../..
33
+ ```
34
+
35
+ ## Running the tests
36
+
37
+ We use the Node.js built-in test runner ([`node:test`](https://nodejs.org/api/test.html)) — no Jest, no Vitest, no extra dependencies.
38
+
39
+ ```bash
40
+ # Core (CLI, daemon, MCP server) — 300+ tests
41
+ npm test
42
+
43
+ # UI server + libraries — 50+ tests
44
+ cd packages/ui && npm test
45
+ ```
46
+
47
+ Both packages compile TypeScript into `dist/` first and then run the compiled `*.test.js` files. The UI test suite uses `--test-isolation=process --test-concurrency=1` because several tests share `~/.bikky/config.json` on the developer's machine; running them in isolation avoids flakiness.
48
+
49
+ > **Note on `~/.bikky/config.json`** — UI tests back up your real config in `before()` and restore it in `after()`. If a test crashes mid-run the file *should* survive, but if you ever see odd behaviour after a failed test run, just delete the file and re-run `bikky setup`.
50
+
51
+ ### What we test
52
+
53
+ We aim for **focused, fast unit tests** that lock in behaviour without being a maintenance tax:
54
+
55
+ - Pure functions (filter builders, hashers, parsers) — exhaustive cases.
56
+ - Stateful modules (config loaders, lifecycle/PID, daemons) — happy path + a couple of failure modes.
57
+ - Network clients (Qdrant, embeddings, LLM providers) — mock `globalThis.fetch` and assert on the request, never call a real backend.
58
+ - HTTP routes (Hono) — exercise via `app.fetch(new Request(...))` against the real router with mocked underlying calls.
59
+
60
+ We deliberately **do not** test:
61
+
62
+ - LLM prompt quality or extraction accuracy. Those live in the separate [`bikky-evals`](https://github.com/bikky-dev/bikky-evals) repo, which uses DeepEval for prompt-level scoring.
63
+ - Implementation details (private function internals, exact log strings) — these change often and tests that pin them slow contributors down.
64
+ - The React frontend — the testable surface there is small; we rely on type-checking and manual smoke tests.
65
+
66
+ ### Adding a new test
67
+
68
+ Tests live alongside the source as `*.test.ts`:
69
+
70
+ ```
71
+ src/foo.ts # source
72
+ src/foo.test.ts # tests
73
+ ```
74
+
75
+ Use the [`node:test`](https://nodejs.org/api/test.html) `describe/it/before/after` API and `node:assert/strict`. For mocking, prefer **dependency injection** (e.g. the `StaleDeps` pattern in `src/daemon/staleness.ts`) over module mocking — it keeps tests deterministic and the production code easier to reason about.
76
+
77
+ Good references for new tests:
78
+
79
+ | Pattern | Reference |
80
+ |----------------------------------|----------------------------------------|
81
+ | Filesystem with backup/restore | `src/lifecycle.test.ts` |
82
+ | Temp dir with `mkdtemp` | `src/logger.test.ts` |
83
+ | Env-based path override | `src/llm/telemetry.test.ts` |
84
+ | Dependency injection for daemons | `src/daemon/staleness.test.ts` |
85
+ | Mocking `globalThis.fetch` | `src/mcp/api.test.ts`, `packages/ui/src/lib/qdrant.test.ts` |
86
+ | Hono route via `app.fetch` | `packages/ui/src/routes/memory.test.ts` |
87
+
88
+ ### Integration tests (opt-in, real Qdrant)
89
+
90
+ The default `npm test` mocks every external call. We also ship one **opt-in** end-to-end smoke test that talks to a real Qdrant instance (Cloud, local Docker, or self-hosted) and a real embedding provider — it's the only thing that catches filter-shape rejections, payload-index mismatches, vector-dimension drift, and whether the dedup similarity thresholds (`THRESHOLD_DUPLICATE`, `THRESHOLD_RELATED`) actually correspond to near-duplicates against your embedding model.
91
+
92
+ ```bash
93
+ # Uses your existing ~/.bikky/config.json + QDRANT_URL (and QDRANT_API_KEY if your Qdrant requires it).
94
+ BIKKY_INTEGRATION=1 npm run test:integration
95
+ ```
96
+
97
+ What it does:
98
+
99
+ 1. Creates a throwaway collection named `bikky-it-<short-uuid>` with the real payload indexes.
100
+ 2. Exercises `memory_store` (insert, exact-dup, near-duplicate paraphrase), `memory_recall`, `memory_entity`, and `memory_forget` against live Qdrant + your real embeddings.
101
+ 3. Drops the collection in `after()` regardless of pass/fail.
102
+
103
+ Cost is negligible — a handful of small embedding calls per run (≈ $0.0001 on OpenAI's `text-embedding-3-small`, free on Ollama). Files end in `.itest.ts` so the default `*.test.js` glob never picks them up.
104
+
105
+ If the near-duplicate paraphrase doesn't reinforce on your embedding model, the test logs the actual similarity score so you can re-tune `THRESHOLD_DUPLICATE` rather than failing outright.
106
+
107
+ ## Adding an embedding or LLM provider
108
+
109
+ The most common contribution is **adding a new embedding or LLM provider**. Each provider is a single file. The registry dispatches by `provider.name`, so no central edits are required beyond adding one import line to the barrel.
110
+
111
+ ### Embedding provider
112
+
113
+ 1. Create `src/llm/embedding/providers/<name>.ts`:
114
+
115
+ ```ts
116
+ import {
117
+ registerEmbeddingProvider,
118
+ type EmbeddingProvider,
119
+ type ResolvedEmbeddingConfig,
120
+ } from "../registry.js";
121
+
122
+ export const myProvider: EmbeddingProvider = {
123
+ name: "myprovider",
124
+ label: "My Provider",
125
+ browserCompatible: true, // false if it needs a server-only SDK
126
+ defaults: {
127
+ model: "default-model",
128
+ dimensions: 1024,
129
+ baseUrl: "https://api.example.com", // omit if SDK-only
130
+ },
131
+ async embed(text, cfg) {
132
+ // cfg.{model,baseUrl,apiKey,extra} are pre-resolved
133
+ const resp = await fetch(`${cfg.baseUrl}/v1/embeddings`, { /* … */ });
134
+ // throw on programmer error; return number[] on success
135
+ return [/* embedding vector */];
136
+ },
137
+ };
138
+
139
+ registerEmbeddingProvider(myProvider);
140
+ ```
141
+
142
+ 2. Add a side-effect import in `src/llm/embedding/providers/index.ts`:
143
+
144
+ ```ts
145
+ import "./myprovider.js";
146
+ ```
147
+
148
+ 3. Add a small unit test next to your provider (`<name>.test.ts`). Mock
149
+ `globalThis.fetch` (see `ollama.test.ts` for the pattern). Cover at minimum:
150
+ - success path (verifies URL, headers, body shape)
151
+ - non-OK response handling
152
+ - any provider-specific behaviour (auth, extra headers, fallback fields)
153
+
154
+ 4. If your provider is browser-friendly, mirror it under
155
+ `packages/ui/src/lib/embedding/providers/<name>.ts` so the UI can use it.
156
+
157
+ 5. Configure it in `~/.bikky/config.json`:
158
+
159
+ ```json
160
+ {
161
+ "embedding": {
162
+ "provider": "myprovider",
163
+ "model": "my-model",
164
+ "api_key": "…",
165
+ "extra": { "any-key": "any-value" }
166
+ }
167
+ }
168
+ ```
169
+
170
+ Or via env: `BIKKY_EMBEDDING_EXTRA_<KEY>=value` flows into `extra`.
171
+
172
+ ### Inference (LLM) provider
173
+
174
+ Same pattern, under `src/llm/inference/providers/`. The interface is
175
+ `InferenceProvider` (see `src/llm/inference/types.ts`), the key method is
176
+ `chat(opts, cfg, log)`, and providers should **return `null`** on recoverable
177
+ errors (HTTP error, missing key, network failure) so the orchestrator can fall
178
+ back to `cfg.fallback` if configured. Throw only for programmer errors.
179
+
180
+ Configure a fallback chain via `llm.fallback_provider` in config (or
181
+ `LLM_FALLBACK_PROVIDER` env).
182
+
183
+ ## Submitting changes
184
+
185
+ 1. **Open an issue first** for non-trivial changes so we can align on the approach.
186
+ 2. **Branch** from `main` (`git checkout -b your-feature-name`).
187
+ 3. **Run the tests** in both packages before pushing.
188
+ 4. **Open a PR** referencing the issue (`Closes #123`). CI will re-run tests on push.
189
+ 5. We aim to review within a few business days — ping the issue if it goes quiet.
190
+
191
+ ## Style
192
+
193
+ - TypeScript strict mode is on; no `any` unless interfacing with external SDK types (use a focused local interface to constrain the surface area).
194
+ - We prefer small, pure functions and clear module boundaries to elaborate abstractions.
195
+ - Tests live next to the source they cover (`foo.ts` + `foo.test.ts`).
196
+ - Providers must not call `process.exit`, log to stdout, or modify global state beyond their own module-scope cache.
197
+ - Heavy SDKs (e.g. `@aws-sdk/*`) **must be `await import(...)`-loaded inside the provider's `embed`/`chat`** so users on lighter providers don't pay the bundle cost.
198
+ - Comments explain *why*, not *what* — the code shows the *what*.
199
+
200
+ ## License
201
+
202
+ By contributing, you agree that your contributions will be licensed under the project's [AGPL-3.0-or-later](LICENSE) license.
203
+
204
+ ## Code of conduct
205
+
206
+ Be kind. We follow the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
package/README.md CHANGED
@@ -1,25 +1,25 @@
1
1
  <h1 align="center">bikky</h1>
2
2
 
3
- <p align="center"><b>Persistent memory for AI coding agents — for teams, and for solo power users.</b></p>
3
+ <p align="center"><b>Persistent memory for AI coding agents — built for teams and multi-agent engineering workflows.</b></p>
4
4
 
5
- bikky gives AI coding agents (GitHub Copilot, Claude Code, Cursor, and other MCP clients) long-term memory that persists across sessions, across tools, and across your whole team. Whether you're a team that wants every engineer's agent to start from the same knowledge base, or a solo power dev running a dozen agentic sessions a day, bikky captures what's learned *during* sessions so future sessions start smarter.
5
+ bikky gives AI coding agents (GitHub Copilot, Claude Code, Cursor, and other MCP clients) long-term memory that persists across sessions, across tools, and across your whole team. When multiple engineers, agents, or repos need to build on the same knowledge base, bikky captures what's learned *during* sessions so future sessions start smarter.
6
6
 
7
7
  ### Who it's for
8
8
 
9
9
  - 👥 **Teams & software factories** — What one engineer's agent learns today, every agent on the team can recall tomorrow. Shared memory turns institutional knowledge into something queryable instead of tribal — onboarding accelerates, conventions stop drifting, and the same lesson never gets re-learned twice.
10
- - 🧑‍💻 **Solo AI power devs** — You run multiple Cursor / Claude Code / Copilot sessions every day and you're tired of re-explaining the codebase, the conventions, and last week's decisions to each new agent. bikky remembers across every session and every tool.
10
+ - 🤖 **Multi-agent engineering workflows** — Multiple Cursor / Claude Code / Copilot sessions can share codebase context, conventions, and recent decisions instead of re-learning them from scratch.
11
11
 
12
12
  <p align="center">
13
- <img src="https://cdn.jsdelivr.net/npm/bikky@latest/docs/diagrams/team-memory.svg" alt="Memory — facts flow from individual sessions into a self-curating knowledge store, shared across your team (or kept just for you)" width="720" />
13
+ <img src="https://cdn.jsdelivr.net/npm/bikky@latest/docs/diagrams/team-memory.svg" alt="Memory — facts flow from individual sessions into a self-curating knowledge store shared across your team" width="720" />
14
14
  </p>
15
15
 
16
- <p align="center"><i>Knowledge flows from every session into a store that curates itself over time — deduplicating, distilling, and decaying stale facts — so every future session starts smarter. Share it across a team, or keep it solo.</i></p>
16
+ <p align="center"><i>Knowledge flows from every session into a store that curates itself over time — deduplicating, distilling, and decaying stale facts — so every future session starts smarter across the team.</i></p>
17
17
 
18
18
  ---
19
19
 
20
20
  ### The problem
21
21
 
22
- The most valuable things you and your agents learn — why a config value exists, which deploy step matters, what broke last quarter, the convention you settled on yesterday — happen *during* sessions. And then they vanish when the session closes. Whether you're a team where knowledge lives in heads, chat threads, and closed PRs, and every new engineer's agent has to learn it from scratch — or a solo power dev juggling dozens of agentic sessions a day across multiple tools that don't remember each other, it's the same wall. Hand-written docs drift the moment they're published.
22
+ The most valuable things you and your agents learn — why a config value exists, which deploy step matters, what broke last quarter, the convention you settled on yesterday — happen *during* sessions. And then they vanish when the session closes. Across teams, repos, and tools, knowledge still lives in heads, chat threads, and closed PRs, and every new agent session has to learn it from scratch. Hand-written docs drift the moment they're published.
23
23
 
24
24
  ### How bikky solves it
25
25
 
@@ -30,6 +30,7 @@ bikky gives your agent memory tools and runs a small background service after `b
30
30
  - **Recall** — Every new session, yours or a teammate's, recalls from the same store via semantic search.
31
31
  - **Curate** — bikky merges duplicates, fades stale facts, resolves contradictions, distills recurring patterns, and builds an entity graph over time.
32
32
  - **Compound** — Session 50 is dramatically better than session 1 because memory accumulates.
33
+ - **Route** — Optionally keep team, client, or environment-specific memory in separate Qdrant destinations from one install. See [separate memory stores](#optional-separate-memory-stores).
33
34
 
34
35
  Subtypes keep recall precise without making setup harder:
35
36
 
@@ -42,7 +43,7 @@ Subtypes keep recall precise without making setup harder:
42
43
 
43
44
  ## Quick start
44
45
 
45
- This quick start uses **local Qdrant + hosted models**: Qdrant runs on your machine, while hosted embeddings and LLM calls provide strong extraction and recall quality without running local LLMs.
46
+ This is the fastest path to a working memory store: Qdrant runs locally, while hosted embeddings and LLM calls provide strong extraction and recall quality without running local models.
46
47
 
47
48
  ```bash
48
49
  # 1. Pull and run Qdrant (vector store)
@@ -82,9 +83,9 @@ Restart your editor. The memory tools appear automatically in supported MCP clie
82
83
  bikky status # confirms Qdrant, embeddings, daemon, and UI health
83
84
  ```
84
85
 
85
- That's it. You can keep Qdrant local forever, or move the vector store to Qdrant Cloud later.
86
+ That's it. You can keep Qdrant local forever, or move the vector store to Qdrant Cloud later for a shared team setup.
86
87
 
87
- For 100% local and account-free setup, use the [local and free config](docs/config/local.md). It is best for private testing rather than long-term team use, and extraction, embedding, and curation performance depends on the local models and hardware you run.
88
+ For other deployment shapes fully hosted, 100% local, or hosted Qdrant with local models see [Setup options](#setup-options).
88
89
 
89
90
  ---
90
91
 
@@ -104,24 +105,67 @@ bikky supports four common setup shapes. Pick based on where you want Qdrant to
104
105
 
105
106
  Both `embedding.provider` and `llm.provider` accept the same values: `ollama`, `openai`, `bedrock`, or `portkey`.
106
107
 
108
+ > ⚠️ **Qdrant Cloud free tier does not include automatic backups.** Deleted collections cannot be recovered. If your memory data is valuable, use a paid Qdrant Cloud plan (which includes daily backups), run Qdrant locally with your own backup strategy, or periodically export snapshots via the [Qdrant snapshots API](https://qdrant.tech/documentation/concepts/snapshots/).
109
+
107
110
  ### Choose a setup
108
111
 
109
112
  | Setup | Best for | Config |
110
113
  | -------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- |
111
- | **Fully hosted** | Best performance and teams; managed vector storage and models | [Fully hosted config](docs/config/fully-hosted.md) |
112
- | **Local Qdrant + hosted models** | Local vector storage with hosted extraction and embedding | [Hosted models config](docs/config/hosted-models.md) |
113
- | **Local and free** | Private/free testing; quality depends on local models | [Local config guide](docs/config/local.md) |
114
- | **Hosted Qdrant + local Ollama** | Shared vector storage while keeping model calls local | [Hosted Qdrant + local models](docs/config/hosted-qdrant-local-models.md) |
114
+ | **Fully hosted** | Best performance and teams; managed vector storage and models | [Fully hosted config][fully-hosted-config] |
115
+ | **Local Qdrant + hosted models** | Local vector storage with hosted extraction and embedding | [Hosted models config][hosted-models-config] |
116
+ | **Local and free** | Local evaluation; quality depends on local models | [Local config guide][local-config] |
117
+ | **Hosted Qdrant + local Ollama** | Shared vector storage while keeping model calls local | [Hosted Qdrant + local models][hosted-qdrant-local-models-config] |
118
+
119
+ ### Configuration basics
120
+
121
+ Pick the setup guide above for the copy-paste config. All setup shapes use the same three building blocks:
115
122
 
116
- ### Configure
123
+ - **Qdrant** — where vectors and memory payloads are stored.
124
+ - **Embeddings** — how facts become searchable vectors.
125
+ - **LLM** — how session transcripts are extracted, curated, and distilled.
117
126
 
118
- Pick the setup guide above for the copy-paste config. Config lives at `~/.bikky/config.json`, and you can also set `QDRANT_URL` and `QDRANT_API_KEY` as environment variables.
127
+ Config lives at `~/.bikky/config.json`, or at `BIKKY_HOME/config.json` when `BIKKY_HOME` is set. You can keep credentials out of the file with environment variables such as `QDRANT_URL`, `QDRANT_API_KEY`, and provider API keys.
119
128
 
120
129
  For hosted models, custom providers, multiple profiles, or advanced tuning, use the full configuration guide.
121
130
 
122
- > 📖 **Full configuration guide:** [docs/configuration.md](docs/configuration.md)
131
+ > 📖 **Full configuration guide:** [docs/configuration.md][configuration-guide]
123
132
  >
124
- > 🛠 Want to add a new embedding or LLM provider (Vertex, OpenRouter, etc.)? See **[CONTRIBUTING.md](CONTRIBUTING.md)** — it's a single-file change.
133
+ > 🛠 Want to add a new embedding or LLM provider (Vertex, OpenRouter, etc.)? See **[CONTRIBUTING.md][contributing]** — it's a single-file change.
134
+
135
+ #### Optional: separate memory stores
136
+
137
+ Most installs use one Qdrant destination. If you need clean separation later, replace the single `qdrant_url` / `collection` fields with named `destinations[]`:
138
+
139
+ ```jsonc
140
+ {
141
+ "destinations": [
142
+ {
143
+ "name": "platform",
144
+ "qdrant_url": "https://platform.cloud.qdrant.io:6333",
145
+ "qdrant_api_key": "...",
146
+ "collection": "bikky-platform",
147
+ "default": true
148
+ },
149
+ {
150
+ "name": "client-a",
151
+ "qdrant_url": "https://client-a.cloud.qdrant.io:6333",
152
+ "qdrant_api_key": "...",
153
+ "collection": "bikky-client-a"
154
+ }
155
+ ]
156
+ }
157
+ ```
158
+
159
+ That is enough for explicit selection in the UI and tools. Add routing rules only when you want automatic placement by cwd, entity, content, or metadata. Existing single-Qdrant configs continue to work.
160
+
161
+ > 📖 **Details:** [multi-destination configuration](docs/configuration.md#multi-destination-routing)
162
+
163
+ [fully-hosted-config]: https://cdn.jsdelivr.net/npm/bikky@latest/docs/config/fully-hosted.md
164
+ [hosted-models-config]: https://cdn.jsdelivr.net/npm/bikky@latest/docs/config/hosted-models.md
165
+ [local-config]: https://cdn.jsdelivr.net/npm/bikky@latest/docs/config/local.md
166
+ [hosted-qdrant-local-models-config]: https://cdn.jsdelivr.net/npm/bikky@latest/docs/config/hosted-qdrant-local-models.md
167
+ [configuration-guide]: https://cdn.jsdelivr.net/npm/bikky@latest/docs/configuration.md
168
+ [contributing]: https://cdn.jsdelivr.net/npm/bikky@latest/CONTRIBUTING.md
125
169
 
126
170
  ---
127
171
 
@@ -137,17 +181,17 @@ bikky-ui # opens http://localhost:1422
137
181
  ```
138
182
 
139
183
  <p align="center">
140
- <img src="https://cdn.jsdelivr.net/npm/bikky@latest/docs/screenshots/dashboard.png" alt="Dashboard — overview stats, category breakdown, recent facts" width="720" />
184
+ <img src="docs/screenshots/dashboard.png" alt="Dashboard — overview stats, category breakdown, recent facts" width="720" />
141
185
  </p>
142
186
  <p align="center"><i>Dashboard — memory stats, category breakdown, and recent facts at a glance</i></p>
143
187
 
144
188
  <p align="center">
145
- <img src="https://cdn.jsdelivr.net/npm/bikky@latest/docs/screenshots/memory.png" alt="Memory browser — search, filter, and browse all stored facts" width="720" />
189
+ <img src="docs/screenshots/memory.png" alt="Memory browser — search, filter, and browse all stored facts" width="720" />
146
190
  </p>
147
191
  <p align="center"><i>Memory browser — search, filter by category/kind/source, and browse all stored facts</i></p>
148
192
 
149
193
  <p align="center">
150
- <img src="https://cdn.jsdelivr.net/npm/bikky@latest/docs/screenshots/graph.png" alt="Entity graph — interactive visualization of entity relationships" width="720" />
194
+ <img src="docs/screenshots/graph.png" alt="Entity graph — interactive visualization of entity relationships" width="720" />
151
195
  </p>
152
196
  <p align="center"><i>Entity graph — interactive visualization of how concepts, people, and services relate</i></p>
153
197
 
package/dist/config.d.ts CHANGED
@@ -76,11 +76,48 @@ export interface WatcherConfig {
76
76
  path: string;
77
77
  };
78
78
  }
79
+ /**
80
+ * One Qdrant routing target. Each destination is fully self-contained: its own
81
+ * URL, API key, collection name, and match rules. All fields in `match` are
82
+ * arrays of regex strings; OR semantics within a destination's match block,
83
+ * first-match-wins across destinations.
84
+ */
85
+ export interface DestinationMatch {
86
+ /** Match against `process.cwd()`. */
87
+ cwd?: string[];
88
+ /** Match against any of the input `entities`. */
89
+ entity?: string[];
90
+ /** Match against the input `content`. */
91
+ content?: string[];
92
+ /** Per-key match against the input `metadata`. */
93
+ metadata?: Record<string, string[]>;
94
+ }
95
+ export interface Destination {
96
+ /** Stable, unique name. Used as the `destination` override on tool calls. */
97
+ name: string;
98
+ qdrant_url: string;
99
+ qdrant_api_key: string | null;
100
+ collection: string;
101
+ /** Marks this destination as the fallback when no rule matches. */
102
+ default?: boolean;
103
+ /** Routing rules. Omit for a destination that is only reachable by override. */
104
+ match?: DestinationMatch;
105
+ }
79
106
  export interface BikkyConfig {
107
+ /**
108
+ * Top-level Qdrant fields. When `destinations` is empty, a single default
109
+ * destination is synthesized from these — keeps single-Qdrant configs
110
+ * working without changes.
111
+ */
80
112
  qdrant_url: string | null;
81
113
  qdrant_api_key: string | null;
82
114
  collection: string;
83
- default_workspace: string | null;
115
+ /**
116
+ * One or more Qdrant routing targets. Memory operations resolve to a
117
+ * destination via override → cwd/entity/content/metadata regex match →
118
+ * default flag → first entry.
119
+ */
120
+ destinations: Destination[];
84
121
  aws_profile: string | null;
85
122
  embedding: EmbeddingConfig;
86
123
  llm: LLMConfig;
@@ -107,6 +144,17 @@ export declare function validateConfigObject(raw: unknown): ConfigIssue[];
107
144
  export declare function inspectConfigFile(configPath?: string): ConfigFileDiagnostics;
108
145
  export declare function getActiveConfigEnvOverrides(env?: NodeJS.ProcessEnv): string[];
109
146
  export declare function loadConfig(): BikkyConfig;
147
+ /**
148
+ * Resolve the effective list of destinations from the loaded config.
149
+ *
150
+ * - If `destinations` is non-empty, return as-is.
151
+ * - Otherwise synthesize a single fallback destination from the top-level
152
+ * `qdrant_url` / `qdrant_api_key` / `collection` so existing single-Qdrant
153
+ * configs keep working without changes.
154
+ * - If neither is configured, returns an empty array — callers should treat
155
+ * that as "Qdrant not configured" the same way they did before.
156
+ */
157
+ export declare function getEffectiveDestinations(config?: BikkyConfig): Destination[];
110
158
  /** Save config to disk (used by setup command). */
111
159
  export declare function saveConfig(config: BikkyConfig): void;
112
160
  /** Reset cached config (for testing). */
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/config.js CHANGED
@@ -28,7 +28,7 @@ const DEFAULTS = {
28
28
  qdrant_url: null,
29
29
  qdrant_api_key: null,
30
30
  collection: "bikky",
31
- default_workspace: null,
31
+ destinations: [],
32
32
  aws_profile: null,
33
33
  embedding: {
34
34
  provider: "ollama",
@@ -176,11 +176,26 @@ const identityConfigFileSchema = z.object({
176
176
  actor_id: z.string().nullable().optional(),
177
177
  actor_label: z.string().nullable().optional(),
178
178
  }).passthrough();
179
+ const regexArrayField = z.array(z.string()).optional();
180
+ const destinationMatchSchema = z.object({
181
+ cwd: regexArrayField,
182
+ entity: regexArrayField,
183
+ content: regexArrayField,
184
+ metadata: z.record(z.array(z.string())).optional(),
185
+ }).passthrough();
186
+ const destinationFileSchema = z.object({
187
+ name: z.string().min(1),
188
+ qdrant_url: z.string().min(1),
189
+ qdrant_api_key: z.string().nullable().optional(),
190
+ collection: z.string().min(1),
191
+ default: z.boolean().optional(),
192
+ match: destinationMatchSchema.optional(),
193
+ }).passthrough();
179
194
  const configFileSchema = z.object({
180
195
  qdrant_url: z.string().nullable().optional(),
181
196
  qdrant_api_key: z.string().nullable().optional(),
182
197
  collection: z.string().optional(),
183
- default_workspace: z.string().nullable().optional(),
198
+ destinations: z.array(destinationFileSchema).optional(),
184
199
  aws_profile: z.string().nullable().optional(),
185
200
  embedding: embeddingConfigFileSchema.optional(),
186
201
  llm: llmConfigFileSchema.optional(),
@@ -267,6 +282,87 @@ export function validateConfigObject(raw) {
267
282
  });
268
283
  }
269
284
  validateUrlLike(raw.qdrant_url, "qdrant_url", issues);
285
+ // Destinations validation
286
+ if (Array.isArray(raw.destinations)) {
287
+ const seenNames = new Set();
288
+ let defaultCount = 0;
289
+ raw.destinations.forEach((entry, idx) => {
290
+ const base = `destinations[${idx}]`;
291
+ if (!isObject(entry)) {
292
+ issues.push({ severity: "error", path: base, message: "must be an object" });
293
+ return;
294
+ }
295
+ const name = entry.name;
296
+ if (typeof name === "string" && name.trim() !== "") {
297
+ if (seenNames.has(name)) {
298
+ issues.push({ severity: "error", path: `${base}.name`, message: `duplicate destination name '${name}'` });
299
+ }
300
+ seenNames.add(name);
301
+ }
302
+ validateUrlLike(entry.qdrant_url, `${base}.qdrant_url`, issues);
303
+ if (typeof entry.collection === "string" && entry.collection.trim() === "") {
304
+ issues.push({ severity: "error", path: `${base}.collection`, message: "must not be empty" });
305
+ }
306
+ if (entry.default === true)
307
+ defaultCount++;
308
+ const match = childObject(entry, "match");
309
+ if (match) {
310
+ for (const field of ["cwd", "entity", "content"]) {
311
+ const value = match[field];
312
+ if (value === undefined)
313
+ continue;
314
+ if (!Array.isArray(value)) {
315
+ issues.push({ severity: "error", path: `${base}.match.${field}`, message: "must be an array of regex strings" });
316
+ continue;
317
+ }
318
+ value.forEach((pattern, pIdx) => {
319
+ if (typeof pattern !== "string") {
320
+ issues.push({ severity: "error", path: `${base}.match.${field}[${pIdx}]`, message: "must be a string" });
321
+ return;
322
+ }
323
+ try {
324
+ new RegExp(pattern);
325
+ }
326
+ catch (e) {
327
+ issues.push({
328
+ severity: "error",
329
+ path: `${base}.match.${field}[${pIdx}]`,
330
+ message: `invalid regex: ${e instanceof Error ? e.message : String(e)}`,
331
+ });
332
+ }
333
+ });
334
+ }
335
+ const metadata = childObject(match, "metadata");
336
+ if (metadata) {
337
+ for (const [key, value] of Object.entries(metadata)) {
338
+ if (!Array.isArray(value)) {
339
+ issues.push({ severity: "error", path: `${base}.match.metadata.${key}`, message: "must be an array of regex strings" });
340
+ continue;
341
+ }
342
+ value.forEach((pattern, pIdx) => {
343
+ if (typeof pattern !== "string") {
344
+ issues.push({ severity: "error", path: `${base}.match.metadata.${key}[${pIdx}]`, message: "must be a string" });
345
+ return;
346
+ }
347
+ try {
348
+ new RegExp(pattern);
349
+ }
350
+ catch (e) {
351
+ issues.push({
352
+ severity: "error",
353
+ path: `${base}.match.metadata.${key}[${pIdx}]`,
354
+ message: `invalid regex: ${e instanceof Error ? e.message : String(e)}`,
355
+ });
356
+ }
357
+ });
358
+ }
359
+ }
360
+ }
361
+ });
362
+ if (defaultCount > 1) {
363
+ issues.push({ severity: "error", path: "destinations", message: `at most one destination may set 'default: true' (found ${defaultCount})` });
364
+ }
365
+ }
270
366
  const embedding = childObject(raw, "embedding");
271
367
  if (embedding)
272
368
  validateUrlLike(embedding.base_url, "embedding.base_url", issues);
@@ -345,8 +441,6 @@ export function loadConfig() {
345
441
  config.qdrant_api_key = process.env.QDRANT_API_KEY;
346
442
  if (process.env.BIKKY_COLLECTION)
347
443
  config.collection = process.env.BIKKY_COLLECTION;
348
- if (process.env.BIKKY_DEFAULT_WORKSPACE)
349
- config.default_workspace = process.env.BIKKY_DEFAULT_WORKSPACE;
350
444
  // Embedding env overrides
351
445
  if (process.env.EMBEDDING_PROVIDER)
352
446
  config.embedding.provider = process.env.EMBEDDING_PROVIDER;
@@ -481,9 +575,36 @@ export function loadConfig() {
481
575
  config.qdrant_url = config.qdrant_url.replace(/\/+$/, "");
482
576
  config.embedding.base_url = config.embedding.base_url.replace(/\/+$/, "");
483
577
  config.llm.base_url = config.llm.base_url.replace(/\/+$/, "");
578
+ for (const dest of config.destinations) {
579
+ if (dest.qdrant_url)
580
+ dest.qdrant_url = dest.qdrant_url.replace(/\/+$/, "");
581
+ }
484
582
  _config = config;
485
583
  return config;
486
584
  }
585
+ /**
586
+ * Resolve the effective list of destinations from the loaded config.
587
+ *
588
+ * - If `destinations` is non-empty, return as-is.
589
+ * - Otherwise synthesize a single fallback destination from the top-level
590
+ * `qdrant_url` / `qdrant_api_key` / `collection` so existing single-Qdrant
591
+ * configs keep working without changes.
592
+ * - If neither is configured, returns an empty array — callers should treat
593
+ * that as "Qdrant not configured" the same way they did before.
594
+ */
595
+ export function getEffectiveDestinations(config = loadConfig()) {
596
+ if (config.destinations.length > 0)
597
+ return config.destinations;
598
+ if (!config.qdrant_url)
599
+ return [];
600
+ return [{
601
+ name: "default",
602
+ qdrant_url: config.qdrant_url,
603
+ qdrant_api_key: config.qdrant_api_key,
604
+ collection: config.collection,
605
+ default: true,
606
+ }];
607
+ }
487
608
  /** Save config to disk (used by setup command). */
488
609
  export function saveConfig(config) {
489
610
  fs.mkdirSync(BIKKY_DIR, { recursive: true });