bikky 0.4.1 → 0.4.3

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 (50) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/CODE_OF_CONDUCT.md +1 -1
  3. package/CONTRIBUTING.md +1 -1
  4. package/README.md +23 -17
  5. package/SUPPORT.md +3 -2
  6. package/dist/config.d.ts +11 -1
  7. package/dist/config.js +88 -20
  8. package/dist/daemon/capture-policy.d.ts +0 -1
  9. package/dist/daemon/capture-policy.js +0 -1
  10. package/dist/daemon/consolidation.d.ts +2 -1
  11. package/dist/daemon/consolidation.js +28 -11
  12. package/dist/daemon/entity-typing.js +10 -0
  13. package/dist/daemon/episode-summary.d.ts +4 -0
  14. package/dist/daemon/episode-summary.js +39 -8
  15. package/dist/daemon/extraction.d.ts +1 -1
  16. package/dist/daemon/extraction.js +52 -17
  17. package/dist/daemon/qdrant.d.ts +32 -10
  18. package/dist/daemon/qdrant.js +177 -60
  19. package/dist/daemon/relations.d.ts +3 -3
  20. package/dist/daemon/relations.js +27 -15
  21. package/dist/daemon/session-index.d.ts +5 -0
  22. package/dist/daemon/session-index.js +36 -9
  23. package/dist/daemon/session-summary.d.ts +3 -0
  24. package/dist/daemon/session-summary.js +48 -15
  25. package/dist/daemon/staleness.js +2 -2
  26. package/dist/daemon/transcript-sources.js +3 -2
  27. package/dist/daemon/watcher.js +2 -0
  28. package/dist/daemon/workstream-summary.d.ts +4 -0
  29. package/dist/daemon/workstream-summary.js +58 -16
  30. package/dist/install.d.ts +11 -0
  31. package/dist/install.js +38 -0
  32. package/dist/llm/embedding/index.js +2 -1
  33. package/dist/llm/embedding/providers/openai.js +8 -2
  34. package/dist/llm/embedding/providers/portkey.js +9 -2
  35. package/dist/llm/inference/index.js +2 -1
  36. package/dist/llm/util.d.ts +12 -0
  37. package/dist/llm/util.js +18 -0
  38. package/dist/mcp/helpers.d.ts +5 -0
  39. package/dist/mcp/helpers.js +27 -3
  40. package/dist/mcp/taxonomy.js +12 -1
  41. package/dist/mcp/tools.js +161 -57
  42. package/dist/mcp/types.d.ts +12 -0
  43. package/dist/package-verifier.d.ts +19 -0
  44. package/dist/package-verifier.js +83 -0
  45. package/dist/provenance/origin.d.ts +57 -0
  46. package/dist/provenance/origin.js +254 -0
  47. package/docs/config/fully-hosted.md +33 -13
  48. package/docs/config/hosted-models.md +33 -13
  49. package/docs/configuration.md +23 -5
  50. package/package.json +6 -2
package/CHANGELOG.md CHANGED
@@ -9,6 +9,12 @@ This project uses npm package versions for release tracking:
9
9
 
10
10
  ## Unreleased
11
11
 
12
+ ## 0.4.2
13
+
14
+ - Republished the core package from current `main` so the npm package README uses GitHub documentation links instead of stale jsDelivr links.
15
+
16
+ ## 0.4.1
17
+
12
18
  - Public OSS readiness cleanup: package metadata, support docs, public maintainer ownership, package tarball hygiene, and privacy/transcript-capture documentation.
13
19
  - Added package verification CI and a privacy-first quickstart for local storage/local model setups.
14
20
 
@@ -45,7 +45,7 @@ or harmful.
45
45
  ## Scope
46
46
 
47
47
  This Code of Conduct applies within all community spaces (issues, PRs,
48
- Discussions, Discord/Slack if any), and also applies when an individual is
48
+ Discord/Slack if any), and also applies when an individual is
49
49
  officially representing the community in public spaces.
50
50
 
51
51
  ## Enforcement
package/CONTRIBUTING.md CHANGED
@@ -199,7 +199,7 @@ Configure a fallback chain via `llm.fallback_provider` in config (or
199
199
 
200
200
  ## License
201
201
 
202
- By contributing, you agree that your contributions will be licensed under the project's [AGPL-3.0-or-later](LICENSE) license.
202
+ By contributing, you agree that your contributions will be licensed under the project's [AGPL-3.0-or-later](https://github.com/bikky-dev/bikky/blob/main/LICENSE) license.
203
203
 
204
204
  ## Code of conduct
205
205
 
package/README.md CHANGED
@@ -10,7 +10,7 @@ bikky gives AI coding agents (GitHub Copilot, Claude Code, Cursor, and other MCP
10
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" width="720" />
13
+ <img src="https://raw.githubusercontent.com/bikky-dev/bikky/main/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
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>
@@ -101,11 +101,11 @@ bikky supports four common setup shapes. Pick based on where you want Qdrant to
101
101
  | ----------------------- | ------------------------------ | ---------------------------------------------------------------------------------------- |
102
102
  | **Node.js** | ≥ 20 | `nvm install 20` or your package manager |
103
103
  | **Vector store** | Qdrant | Local Docker · [Qdrant Cloud](https://cloud.qdrant.io) · Self-hosted |
104
- | **Embeddings** | One provider | OpenAI · Ollama · Bedrock · Portkey |
105
- | **LLM** | One provider | OpenAI · Ollama · Bedrock · Portkey |
104
+ | **Embeddings** | One provider | Portkey · OpenAI · Ollama · Bedrock |
105
+ | **LLM** | One provider | Portkey · OpenAI · Ollama · Bedrock |
106
106
  | **Docker** *(optional)* | Only if you run Qdrant locally | Docker Desktop, OrbStack, colima, etc. |
107
107
 
108
- Both `embedding.provider` and `llm.provider` accept the same values: `ollama`, `openai`, `bedrock`, or `portkey`.
108
+ Both `embedding.provider` and `llm.provider` accept the same values: `ollama`, `openai`, `bedrock`, or `portkey`. **Portkey is the easiest cloud option** — one API key, any upstream provider, with built-in routing/fallbacks. Bikky's canonical embedding dimension is **1024**, portable across every modern provider.
109
109
 
110
110
  > ⚠️ **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/).
111
111
 
@@ -128,6 +128,8 @@ Pick the setup guide above for the copy-paste config. All setup shapes use the s
128
128
 
129
129
  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.
130
130
 
131
+ `bikky setup` also provisions `identity.user_id` / `identity.user_name` when they are missing. New memory writes store canonical `origin` metadata with the configured human user, the acting agent or daemon/UI surface, the interface, and the operation. MCP clients cannot supply or spoof `origin.user`; if config, env, Git, and shell identity detection all fail, bikky falls back to the local hostname.
132
+
131
133
  For hosted models, custom providers, multiple profiles, or advanced tuning, use the full configuration guide.
132
134
 
133
135
  > 📖 **Full configuration guide:** [docs/configuration.md][configuration-guide]
@@ -165,15 +167,15 @@ Most installs use one Qdrant destination. If you need clean separation later, re
165
167
 
166
168
  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. Search tools can also use `search_scope: "all"` or a named/listed scope when context may span stores. Existing single-Qdrant configs continue to work.
167
169
 
168
- > 📖 **Details:** [multi-destination configuration](docs/configuration.md#multi-destination-routing)
170
+ > 📖 **Details:** [multi-destination configuration](https://github.com/bikky-dev/bikky/blob/main/docs/configuration.md#multi-destination-routing)
169
171
 
170
- [fully-hosted-config]: https://cdn.jsdelivr.net/npm/bikky@latest/docs/config/fully-hosted.md
171
- [hosted-models-config]: https://cdn.jsdelivr.net/npm/bikky@latest/docs/config/hosted-models.md
172
- [local-config]: https://cdn.jsdelivr.net/npm/bikky@latest/docs/config/local.md
173
- [hosted-qdrant-local-models-config]: https://cdn.jsdelivr.net/npm/bikky@latest/docs/config/hosted-qdrant-local-models.md
174
- [configuration-guide]: https://cdn.jsdelivr.net/npm/bikky@latest/docs/configuration.md
175
- [privacy-quickstart]: https://cdn.jsdelivr.net/npm/bikky@latest/docs/privacy-first.md
176
- [contributing]: https://cdn.jsdelivr.net/npm/bikky@latest/CONTRIBUTING.md
172
+ [fully-hosted-config]: https://github.com/bikky-dev/bikky/blob/main/docs/config/fully-hosted.md
173
+ [hosted-models-config]: https://github.com/bikky-dev/bikky/blob/main/docs/config/hosted-models.md
174
+ [local-config]: https://github.com/bikky-dev/bikky/blob/main/docs/config/local.md
175
+ [hosted-qdrant-local-models-config]: https://github.com/bikky-dev/bikky/blob/main/docs/config/hosted-qdrant-local-models.md
176
+ [configuration-guide]: https://github.com/bikky-dev/bikky/blob/main/docs/configuration.md
177
+ [privacy-quickstart]: https://github.com/bikky-dev/bikky/blob/main/docs/privacy-first.md
178
+ [contributing]: https://github.com/bikky-dev/bikky/blob/main/CONTRIBUTING.md
177
179
 
178
180
  ---
179
181
 
@@ -189,17 +191,17 @@ bikky-ui # opens http://localhost:1422
189
191
  ```
190
192
 
191
193
  <p align="center">
192
- <img src="docs/screenshots/dashboard.png" alt="Dashboard — overview stats, category breakdown, recent facts" width="720" />
194
+ <img src="https://raw.githubusercontent.com/bikky-dev/bikky/main/docs/screenshots/dashboard.png" alt="Dashboard — overview stats, category breakdown, recent facts" width="720" />
193
195
  </p>
194
196
  <p align="center"><i>Dashboard — memory stats, category breakdown, and recent facts at a glance</i></p>
195
197
 
196
198
  <p align="center">
197
- <img src="docs/screenshots/memory.png" alt="Memory browser — search, filter, and browse all stored facts" width="720" />
199
+ <img src="https://raw.githubusercontent.com/bikky-dev/bikky/main/docs/screenshots/memory.png" alt="Memory browser — search, filter, and browse all stored facts" width="720" />
198
200
  </p>
199
- <p align="center"><i>Memory browser — search, filter by category/kind/source, and browse all stored facts</i></p>
201
+ <p align="center"><i>Memory browser — search, filter by category/kind/origin, and browse all stored facts</i></p>
200
202
 
201
203
  <p align="center">
202
- <img src="docs/screenshots/graph.png" alt="Entity graph — interactive visualization of entity relationships" width="720" />
204
+ <img src="https://raw.githubusercontent.com/bikky-dev/bikky/main/docs/screenshots/graph.png" alt="Entity graph — interactive visualization of entity relationships" width="720" />
203
205
  </p>
204
206
  <p align="center"><i>Entity graph — interactive visualization of how concepts, people, and services relate</i></p>
205
207
 
@@ -242,6 +244,10 @@ You can also set `daemon.extract_every_sec` to `0` to disable background extract
242
244
 
243
245
  For a local-storage, local-model setup that minimizes what leaves your machine, see the [privacy-first quickstart][privacy-quickstart].
244
246
 
247
+ ## Support and contact
248
+
249
+ For questions, bugs, and feature requests, please use [GitHub issues](https://github.com/bikky-dev/bikky/issues). For maintainer contact, reach Saber Zrelli on GitHub: [@zrelli-s](https://github.com/zrelli-s).
250
+
245
251
  ## License
246
252
 
247
- AGPL-3.0 — see [LICENSE](LICENSE).
253
+ AGPL-3.0 — see [LICENSE](https://github.com/bikky-dev/bikky/blob/main/LICENSE).
package/SUPPORT.md CHANGED
@@ -12,11 +12,12 @@ bikky status
12
12
 
13
13
  Please redact API keys, access tokens, local file contents, and any private transcript data before posting.
14
14
 
15
+ For maintainer contact, reach Saber Zrelli on GitHub: [@zrelli-s](https://github.com/zrelli-s).
16
+
15
17
  ## Bugs
16
18
 
17
19
  Use the bug report template and include a minimal reproduction when possible. If the issue involves setup, include whether you installed with `npm install -g bikky`, `npx bikky`, or a local checkout.
18
20
 
19
21
  ## Security reports
20
22
 
21
- Do not open a public issue for vulnerabilities. Follow [SECURITY.md](SECURITY.md) and use GitHub private vulnerability reporting.
22
-
23
+ Do not open a public issue for vulnerabilities. Follow [SECURITY.md](https://github.com/bikky-dev/bikky/blob/main/SECURITY.md) and use GitHub private vulnerability reporting.
package/dist/config.d.ts CHANGED
@@ -4,6 +4,12 @@
4
4
  * Resolution order: defaults → ~/.bikky/config.json → env vars.
5
5
  * Config directory: ~/.bikky/
6
6
  */
7
+ export declare function getBikkyDir(): string;
8
+ export declare function getConfigPath(): string;
9
+ export declare function getLogDir(): string;
10
+ export declare function getStateDir(): string;
11
+ export declare function getPidPath(): string;
12
+ export declare function getExtractionHealthPath(): string;
7
13
  export declare const BIKKY_DIR: string;
8
14
  export declare const CONFIG_PATH: string;
9
15
  export declare const LOG_DIR: string;
@@ -63,7 +69,11 @@ export interface QdrantClientConfig {
63
69
  retry_base_delay_ms: number;
64
70
  }
65
71
  export interface IdentityConfig {
72
+ user_id: string | null;
73
+ user_name: string | null;
74
+ /** @deprecated Use origin.user.id instead. */
66
75
  actor_id: string | null;
76
+ /** @deprecated Use origin.user.name / origin.agent.name instead. */
67
77
  actor_label: string | null;
68
78
  }
69
79
  export interface WatcherConfig {
@@ -158,7 +168,7 @@ export interface ConfigFileDiagnostics {
158
168
  issues: ConfigIssue[];
159
169
  }
160
170
  declare const DEFAULTS: BikkyConfig;
161
- export declare const CONFIG_ENV_KEYS: readonly ["QDRANT_URL", "QDRANT_API_KEY", "BIKKY_COLLECTION", "EMBEDDING_PROVIDER", "EMBEDDING_MODEL", "EMBEDDING_BASE_URL", "EMBEDDING_DIMENSIONS", "OPENAI_API_KEY", "LLM_PROVIDER", "LLM_MODEL", "LLM_BASE_URL", "LLM_FALLBACK_PROVIDER", "AWS_PROFILE", "AWS_BEDROCK_REGION", "AWS_REGION", "QDRANT_TIMEOUT_MS", "QDRANT_RETRIES", "QDRANT_RETRY_BASE_DELAY_MS", "BIKKY_EMBEDDING_TIMEOUT_MS", "BIKKY_EMBEDDING_RETRIES", "BIKKY_EMBEDDING_RETRY_BASE_DELAY_MS", "BIKKY_LLM_TIMEOUT_MS", "BIKKY_LLM_RETRIES", "BIKKY_LLM_RETRY_BASE_DELAY_MS", "BIKKY_DAEMON_RELATION_INFERENCE_ENABLED", "BIKKY_DAEMON_RELATION_INFERENCE_INTERVAL_SEC", "BIKKY_DAEMON_RELATION_INFERENCE_MAX_PAIRS_PER_RUN", "BIKKY_DAEMON_ENTITY_TYPING_ENABLED", "BIKKY_DAEMON_ENTITY_TYPING_INTERVAL_SEC", "BIKKY_DAEMON_ENTITY_TYPING_MAX_ENTITIES_PER_RUN", "BIKKY_ACTOR_ID", "BIKKY_ACTOR_LABEL"];
171
+ export declare const CONFIG_ENV_KEYS: readonly ["QDRANT_URL", "QDRANT_API_KEY", "BIKKY_COLLECTION", "EMBEDDING_PROVIDER", "EMBEDDING_MODEL", "EMBEDDING_BASE_URL", "EMBEDDING_DIMENSIONS", "OPENAI_API_KEY", "PORTKEY_API_KEY", "LLM_PROVIDER", "LLM_MODEL", "LLM_BASE_URL", "LLM_FALLBACK_PROVIDER", "AWS_PROFILE", "AWS_BEDROCK_REGION", "AWS_REGION", "QDRANT_TIMEOUT_MS", "QDRANT_RETRIES", "QDRANT_RETRY_BASE_DELAY_MS", "BIKKY_EMBEDDING_TIMEOUT_MS", "BIKKY_EMBEDDING_RETRIES", "BIKKY_EMBEDDING_RETRY_BASE_DELAY_MS", "BIKKY_LLM_TIMEOUT_MS", "BIKKY_LLM_RETRIES", "BIKKY_LLM_RETRY_BASE_DELAY_MS", "BIKKY_DAEMON_RELATION_INFERENCE_ENABLED", "BIKKY_DAEMON_RELATION_INFERENCE_INTERVAL_SEC", "BIKKY_DAEMON_RELATION_INFERENCE_MAX_PAIRS_PER_RUN", "BIKKY_DAEMON_ENTITY_TYPING_ENABLED", "BIKKY_DAEMON_ENTITY_TYPING_INTERVAL_SEC", "BIKKY_DAEMON_ENTITY_TYPING_MAX_ENTITIES_PER_RUN", "BIKKY_USER_ID", "BIKKY_USER_NAME", "BIKKY_AGENT_ID", "BIKKY_AGENT_NAME", "BIKKY_ACTOR_ID", "BIKKY_ACTOR_LABEL"];
162
172
  export declare function validateConfigObject(raw: unknown): ConfigIssue[];
163
173
  export declare function inspectConfigFile(configPath?: string): ConfigFileDiagnostics;
164
174
  export declare function getActiveConfigEnvOverrides(env?: NodeJS.ProcessEnv): string[];
package/dist/config.js CHANGED
@@ -12,15 +12,41 @@ import { z } from "zod";
12
12
  // Paths
13
13
  // ---------------------------------------------------------------------------
14
14
  // BIKKY_HOME env var lets tests (and advanced users) override the config dir
15
- // without touching the real ~/.bikky/. Tests MUST set this to an isolated
16
- // tempdir before importing this module otherwise saveConfig() will write to
17
- // the user's real config file.
18
- export const BIKKY_DIR = process.env.BIKKY_HOME ?? path.join(os.homedir(), ".bikky");
19
- export const CONFIG_PATH = path.join(BIKKY_DIR, "config.json");
20
- export const LOG_DIR = path.join(BIKKY_DIR, "logs");
21
- export const STATE_DIR = path.join(BIKKY_DIR, "state");
22
- export const PID_PATH = path.join(STATE_DIR, "daemon.pid");
23
- export const EXTRACTION_HEALTH_PATH = path.join(STATE_DIR, "extraction-health.json");
15
+ // without touching the real ~/.bikky/. The getter functions below re-read the
16
+ // env var on every call, so changing BIKKY_HOME at runtime (e.g. in a test
17
+ // setup hook) takes effect for all subsequent saveConfig()/loadConfig() calls.
18
+ //
19
+ // The legacy `BIKKY_DIR` / `CONFIG_PATH` / etc. exports are kept for
20
+ // backward-compatibility, but they capture the env state at module load
21
+ // time and should NOT be relied on for safe writes. Internal callers — and
22
+ // any test that wants sandboxing — should call the getter functions instead.
23
+ export function getBikkyDir() {
24
+ return process.env.BIKKY_HOME ?? path.join(os.homedir(), ".bikky");
25
+ }
26
+ export function getConfigPath() {
27
+ return path.join(getBikkyDir(), "config.json");
28
+ }
29
+ export function getLogDir() {
30
+ return path.join(getBikkyDir(), "logs");
31
+ }
32
+ export function getStateDir() {
33
+ return path.join(getBikkyDir(), "state");
34
+ }
35
+ export function getPidPath() {
36
+ return path.join(getStateDir(), "daemon.pid");
37
+ }
38
+ export function getExtractionHealthPath() {
39
+ return path.join(getStateDir(), "extraction-health.json");
40
+ }
41
+ // Legacy constant exports — captured at module load. Prefer the getter
42
+ // functions above when you need fresh values (e.g. inside tests, or after
43
+ // mutating BIKKY_HOME at runtime).
44
+ export const BIKKY_DIR = getBikkyDir();
45
+ export const CONFIG_PATH = getConfigPath();
46
+ export const LOG_DIR = getLogDir();
47
+ export const STATE_DIR = getStateDir();
48
+ export const PID_PATH = getPidPath();
49
+ export const EXTRACTION_HEALTH_PATH = getExtractionHealthPath();
24
50
  // ---------------------------------------------------------------------------
25
51
  // Defaults
26
52
  // ---------------------------------------------------------------------------
@@ -69,6 +95,8 @@ const DEFAULTS = {
69
95
  staleness_threshold_days: 30,
70
96
  },
71
97
  identity: {
98
+ user_id: null,
99
+ user_name: null,
72
100
  actor_id: null,
73
101
  actor_label: null,
74
102
  },
@@ -91,6 +119,7 @@ export const CONFIG_ENV_KEYS = [
91
119
  "EMBEDDING_BASE_URL",
92
120
  "EMBEDDING_DIMENSIONS",
93
121
  "OPENAI_API_KEY",
122
+ "PORTKEY_API_KEY",
94
123
  "LLM_PROVIDER",
95
124
  "LLM_MODEL",
96
125
  "LLM_BASE_URL",
@@ -113,6 +142,10 @@ export const CONFIG_ENV_KEYS = [
113
142
  "BIKKY_DAEMON_ENTITY_TYPING_ENABLED",
114
143
  "BIKKY_DAEMON_ENTITY_TYPING_INTERVAL_SEC",
115
144
  "BIKKY_DAEMON_ENTITY_TYPING_MAX_ENTITIES_PER_RUN",
145
+ "BIKKY_USER_ID",
146
+ "BIKKY_USER_NAME",
147
+ "BIKKY_AGENT_ID",
148
+ "BIKKY_AGENT_NAME",
116
149
  "BIKKY_ACTOR_ID",
117
150
  "BIKKY_ACTOR_LABEL",
118
151
  ];
@@ -122,7 +155,7 @@ const CONFIG_ENV_PREFIXES = [
122
155
  ];
123
156
  const nonNegativeInt = z.number().int().nonnegative();
124
157
  const positiveInt = z.number().int().positive();
125
- const stringRecord = z.record(z.string());
158
+ const stringRecord = z.record(z.string(), z.string());
126
159
  const embeddingConfigFileSchema = z.object({
127
160
  provider: z.string().optional(),
128
161
  model: z.string().optional(),
@@ -175,6 +208,8 @@ const qdrantClientConfigFileSchema = z.object({
175
208
  retry_base_delay_ms: nonNegativeInt.optional(),
176
209
  }).passthrough();
177
210
  const identityConfigFileSchema = z.object({
211
+ user_id: z.string().nullable().optional(),
212
+ user_name: z.string().nullable().optional(),
178
213
  actor_id: z.string().nullable().optional(),
179
214
  actor_label: z.string().nullable().optional(),
180
215
  }).passthrough();
@@ -183,7 +218,7 @@ const destinationMatchSchema = z.object({
183
218
  cwd: regexArrayField,
184
219
  entity: regexArrayField,
185
220
  content: regexArrayField,
186
- metadata: z.record(z.array(z.string())).optional(),
221
+ metadata: z.record(z.string(), z.array(z.string())).optional(),
187
222
  }).passthrough();
188
223
  const destinationFileSchema = z.object({
189
224
  name: z.string().min(1),
@@ -442,7 +477,7 @@ export function validateConfigObject(raw) {
442
477
  validateUrlLike(llm.base_url, "llm.base_url", issues);
443
478
  return issues;
444
479
  }
445
- export function inspectConfigFile(configPath = CONFIG_PATH) {
480
+ export function inspectConfigFile(configPath = getConfigPath()) {
446
481
  if (!fs.existsSync(configPath)) {
447
482
  return { path: configPath, exists: false, parse_error: null, issues: [] };
448
483
  }
@@ -490,21 +525,41 @@ export function loadConfig() {
490
525
  if (_config)
491
526
  return _config;
492
527
  // Ensure dirs exist
493
- fs.mkdirSync(BIKKY_DIR, { recursive: true });
494
- fs.mkdirSync(LOG_DIR, { recursive: true });
495
- fs.mkdirSync(STATE_DIR, { recursive: true });
528
+ fs.mkdirSync(getBikkyDir(), { recursive: true });
529
+ fs.mkdirSync(getLogDir(), { recursive: true });
530
+ fs.mkdirSync(getStateDir(), { recursive: true });
496
531
  // Start from defaults
497
532
  let config = structuredClone(DEFAULTS);
498
533
  // Merge config file
499
- if (fs.existsSync(CONFIG_PATH)) {
534
+ const configPath = getConfigPath();
535
+ let fileConfig = {};
536
+ if (fs.existsSync(configPath)) {
500
537
  try {
501
- const fileConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
538
+ fileConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
502
539
  config = deepMerge(config, fileConfig);
503
540
  }
504
541
  catch (e) {
505
- console.error(`bikky: failed to parse ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`);
542
+ console.error(`bikky: failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
506
543
  }
507
544
  }
545
+ // Provider/base_url consistency (issue #131): the DEFAULTS.embedding.base_url
546
+ // is the ollama localhost URL, baked in for the default ollama provider. If
547
+ // the user picks a different provider (portkey/openai/bedrock) but doesn't
548
+ // set an explicit base_url, drop the inherited ollama URL so initEmbedding()
549
+ // can apply the provider's own default — otherwise we'd POST every embedding
550
+ // request to localhost:11434 and Ollama would reject the foreign model name.
551
+ const fileEmbedding = (fileConfig.embedding ?? {});
552
+ if (config.embedding.provider !== DEFAULTS.embedding.provider
553
+ && typeof fileEmbedding.base_url !== "string"
554
+ && !process.env.EMBEDDING_BASE_URL) {
555
+ config.embedding.base_url = "";
556
+ }
557
+ const fileLlm = (fileConfig.llm ?? {});
558
+ if (config.llm.provider !== DEFAULTS.llm.provider
559
+ && typeof fileLlm.base_url !== "string"
560
+ && !process.env.LLM_BASE_URL) {
561
+ config.llm.base_url = "";
562
+ }
508
563
  // Env var overrides (highest priority)
509
564
  if (process.env.QDRANT_URL)
510
565
  config.qdrant_url = process.env.QDRANT_URL;
@@ -526,6 +581,12 @@ export function loadConfig() {
526
581
  }
527
582
  if (process.env.OPENAI_API_KEY)
528
583
  config.embedding.api_key = process.env.OPENAI_API_KEY;
584
+ // Portkey users can supply their gateway key via PORTKEY_API_KEY without
585
+ // needing to repurpose OPENAI_API_KEY. Only applied when the embedding
586
+ // provider is Portkey, so non-Portkey setups remain untouched.
587
+ if (process.env.PORTKEY_API_KEY && config.embedding.provider === "portkey") {
588
+ config.embedding.api_key = process.env.PORTKEY_API_KEY;
589
+ }
529
590
  // Generic provider-extras: BIKKY_EMBEDDING_EXTRA_<KEY>=value
530
591
  config.embedding.extra = config.embedding.extra ?? {};
531
592
  for (const [k, v] of Object.entries(process.env)) {
@@ -542,6 +603,9 @@ export function loadConfig() {
542
603
  config.llm.base_url = process.env.LLM_BASE_URL;
543
604
  if (process.env.OPENAI_API_KEY && !config.llm.api_key)
544
605
  config.llm.api_key = process.env.OPENAI_API_KEY;
606
+ if (process.env.PORTKEY_API_KEY && config.llm.provider === "portkey" && !config.llm.api_key) {
607
+ config.llm.api_key = process.env.PORTKEY_API_KEY;
608
+ }
545
609
  if (process.env.LLM_FALLBACK_PROVIDER)
546
610
  config.llm.fallback_provider = process.env.LLM_FALLBACK_PROVIDER;
547
611
  if (process.env.AWS_PROFILE)
@@ -632,6 +696,10 @@ export function loadConfig() {
632
696
  const entityTypingMax = positiveInt(process.env.BIKKY_DAEMON_ENTITY_TYPING_MAX_ENTITIES_PER_RUN);
633
697
  if (entityTypingMax !== null)
634
698
  config.daemon.entity_typing_max_entities_per_run = entityTypingMax;
699
+ if (process.env.BIKKY_USER_ID)
700
+ config.identity.user_id = process.env.BIKKY_USER_ID;
701
+ if (process.env.BIKKY_USER_NAME)
702
+ config.identity.user_name = process.env.BIKKY_USER_NAME;
635
703
  if (process.env.BIKKY_ACTOR_ID)
636
704
  config.identity.actor_id = process.env.BIKKY_ACTOR_ID;
637
705
  if (process.env.BIKKY_ACTOR_LABEL)
@@ -679,8 +747,8 @@ export function getEffectiveDestinations(config = loadConfig()) {
679
747
  }
680
748
  /** Save config to disk (used by setup command). */
681
749
  export function saveConfig(config) {
682
- fs.mkdirSync(BIKKY_DIR, { recursive: true });
683
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
750
+ fs.mkdirSync(getBikkyDir(), { recursive: true });
751
+ fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n");
684
752
  _config = config;
685
753
  }
686
754
  /** Reset cached config (for testing). */
@@ -86,7 +86,6 @@ export declare const CAPTURE_KIND_SUBTYPES: {
86
86
  export declare const FACT_CATEGORY_TO_SUBTYPE: Record<Category, MemorySubtype>;
87
87
  export declare const DEFAULT_CAPTURE_CONTEXT: {
88
88
  readonly domain: "software_engineering" | "product_strategy" | "business_operations" | "research" | "personal_productivity";
89
- readonly source: "system";
90
89
  readonly reviewStatus: "candidate";
91
90
  readonly volatility: "evolving";
92
91
  };
@@ -110,7 +110,6 @@ export const FACT_CATEGORY_TO_SUBTYPE = {
110
110
  };
111
111
  export const DEFAULT_CAPTURE_CONTEXT = {
112
112
  domain: DEFAULT_DOMAIN,
113
- source: "system",
114
113
  reviewStatus: "candidate",
115
114
  // Default fallback volatility when the LLM does not self-judge. Storage path
116
115
  // overrides this with the LLM's value (or the volatility verifier's
@@ -51,6 +51,7 @@ declare const detectContradiction: (fact: {
51
51
  }, _config: BikkyConfig, telemetry?: {
52
52
  sessionId?: string;
53
53
  workstreamKey?: string;
54
+ destination?: string;
54
55
  }) => Promise<ContradictionResult>;
55
56
  /**
56
57
  * Main consolidation tick — called from daemon tick loop.
@@ -58,6 +59,6 @@ declare const detectContradiction: (fact: {
58
59
  */
59
60
  declare const tick: (config: BikkyConfig, opts?: ConsolidationTickOptions) => Promise<void>;
60
61
  /** Reset state (for testing). */
61
- declare const _reset: () => void;
62
+ declare const _reset: (tickCount?: number) => void;
62
63
  export { detectContradiction, tick, setLogger, _reset, };
63
64
  //# sourceMappingURL=consolidation.d.ts.map
@@ -13,6 +13,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
13
13
  import { join, dirname } from "node:path";
14
14
  import { createHash } from "node:crypto";
15
15
  import * as qdrant from "./qdrant.js";
16
+ import { buildOperationOrigin } from "../provenance/origin.js";
16
17
  import { chatCompletion } from "../llm/index.js";
17
18
  import { categoryValues, normalizeCategory, normalizeDomain } from "../mcp/taxonomy.js";
18
19
  import { distillPrompt, DISTILL_PROMPT_DESCRIPTOR, contradictionPrompt, briefPrompt, BRIEF_PROMPT_DESCRIPTOR, safeParseJson, } from "../prompts/index.js";
@@ -33,6 +34,8 @@ const autoDistill = async (_config, { minSummaries = 5 } = {}) => {
33
34
  if (!qdrant.isReady())
34
35
  return { distilled: false };
35
36
  try {
37
+ const destination = qdrant.resolveDestination({}).name;
38
+ const collection = qdrant.collectionForDestination(destination);
36
39
  // Find undistilled session summaries (support both legacy and new taxonomy)
37
40
  const legacyFilter = {
38
41
  must: [
@@ -47,12 +50,12 @@ const autoDistill = async (_config, { minSummaries = 5 } = {}) => {
47
50
  ],
48
51
  };
49
52
  const [legacyRes, newRes] = await Promise.all([
50
- qdrant.qdrantRequest("POST", `/collections/${qdrant.collection}/points/scroll`, {
53
+ qdrant.qdrantRequest("POST", `/collections/${collection}/points/scroll`, {
51
54
  filter: legacyFilter, limit: 50, with_payload: true,
52
- }),
53
- qdrant.qdrantRequest("POST", `/collections/${qdrant.collection}/points/scroll`, {
55
+ }, destination),
56
+ qdrant.qdrantRequest("POST", `/collections/${collection}/points/scroll`, {
54
57
  filter: newFilter, limit: 50, with_payload: true,
55
- }),
58
+ }, destination),
56
59
  ]);
57
60
  // Deduplicate by ID
58
61
  const seen = new Set();
@@ -107,7 +110,6 @@ const autoDistill = async (_config, { minSummaries = 5 } = {}) => {
107
110
  domain: normalizeDomain(pattern.domain ?? "software_engineering"),
108
111
  kind: "distilled",
109
112
  entities: Array.isArray(pattern.entities) ? pattern.entities.map(e => String(e).toLowerCase()) : [],
110
- source: "system",
111
113
  confidence: 0.85,
112
114
  importance: pattern.importance || 0.7,
113
115
  content_hash: hash,
@@ -116,11 +118,25 @@ const autoDistill = async (_config, { minSummaries = 5 } = {}) => {
116
118
  distilled_at: new Date().toISOString(),
117
119
  distilled_by_prompt: promptStamp,
118
120
  },
119
- });
121
+ origin: buildOperationOrigin({
122
+ interface: "daemon",
123
+ action: "create",
124
+ subsystem: "consolidation",
125
+ metadata: {
126
+ distilled_from_count: batch.length,
127
+ prompt: promptStamp,
128
+ },
129
+ }),
130
+ }, { destination });
120
131
  }
121
132
  // Supersede the source summaries
122
133
  for (const pt of batch) {
123
- await qdrant.supersedeFact(pt.id, `distilled:${new Date().toISOString()}`);
134
+ await qdrant.supersedeFact(pt.id, `distilled:${new Date().toISOString()}`, destination, buildOperationOrigin({
135
+ interface: "daemon",
136
+ action: "supersede",
137
+ subsystem: "consolidation",
138
+ metadata: { prompt: promptStamp },
139
+ }));
124
140
  }
125
141
  logFn("INFO", `Auto-distill: consolidated ${batch.length} summaries into ${patterns.length} patterns`);
126
142
  return { distilled: true, count: patterns.length };
@@ -143,12 +159,13 @@ const detectContradiction = async (fact, _config, telemetry) => {
143
159
  try {
144
160
  const vector = await qdrant.embed(fact.content);
145
161
  // Search across all categories because contradictions can cross category lines.
146
- const results = await qdrant.qdrantRequest("POST", `/collections/${qdrant.collection}/points/search`, {
162
+ const collection = qdrant.collectionForDestination(telemetry?.destination);
163
+ const results = await qdrant.qdrantRequest("POST", `/collections/${collection}/points/search`, {
147
164
  vector,
148
165
  filter: { must: [{ is_null: { key: "superseded_by" } }] },
149
166
  limit: 5,
150
167
  with_payload: true,
151
- });
168
+ }, telemetry?.destination);
152
169
  const candidates = (results.result || [])
153
170
  .filter(r => r.score >= 0.75 && r.score < 0.92);
154
171
  if (candidates.length === 0)
@@ -448,8 +465,8 @@ const tick = async (config, opts = {}) => {
448
465
  }
449
466
  };
450
467
  /** Reset state (for testing). */
451
- const _reset = () => {
452
- consolidationTickCount = 0;
468
+ const _reset = (tickCount = 0) => {
469
+ consolidationTickCount = tickCount;
453
470
  };
454
471
  export { detectContradiction, tick, setLogger, _reset, };
455
472
  //# sourceMappingURL=consolidation.js.map
@@ -11,6 +11,7 @@ import { entityTypingPrompt, ENTITY_TYPING_PROMPT_DESCRIPTOR, safeParseJson } fr
11
11
  import * as qdrant from "./qdrant.js";
12
12
  import { isAttemptBackedOff, pruneRecentAttempts, readMaintenanceState, recordMaintenanceRun, shouldRunMaintenance, } from "./maintenance-state.js";
13
13
  import { isGenericEntity } from "./relations-vocab.js";
14
+ import { buildOperationOrigin } from "../provenance/origin.js";
14
15
  const FACTS_SCAN_LIMIT = 200;
15
16
  const FACTS_PER_ENTITY = 5;
16
17
  const DEFAULT_LOOKBACK_MS = 7 * 24 * 60 * 60 * 1000;
@@ -171,6 +172,15 @@ const upsertEntityTypePoint = async (candidate, classification, source) => {
171
172
  classified_at: now,
172
173
  updated_at: now,
173
174
  created_at: now,
175
+ origin: buildOperationOrigin({
176
+ interface: "daemon",
177
+ action: "create",
178
+ subsystem: "entity_typing",
179
+ metadata: {
180
+ entity_name: candidate.name,
181
+ classification_source: source,
182
+ },
183
+ }),
174
184
  source_fact_ids: candidate.factIds,
175
185
  ...(candidate.workstreamKeys.length > 0 ? { workstream_key: candidate.workstreamKeys[0] } : {}),
176
186
  metadata: {
@@ -2,6 +2,7 @@ import type { BikkyConfig } from "../config.js";
2
2
  import type { QdrantPayload } from "./qdrant.js";
3
3
  import { type RedactionSummary } from "../privacy/redaction.js";
4
4
  import { type WorkstreamRegistry } from "./workstream-resolver.js";
5
+ import { type OperationOrigin } from "../provenance/origin.js";
5
6
  export { buildEpisodeSummaryMessages } from "../prompts/index.js";
6
7
  export interface WorkspaceScope {
7
8
  workspaceId?: string;
@@ -33,6 +34,7 @@ export interface EpisodeSummaryWriteResult {
33
34
  action: "stored" | "updated" | "skipped";
34
35
  factId?: string;
35
36
  episodeId?: string;
37
+ destination?: string;
36
38
  workstreamKey?: string | null;
37
39
  reason?: string;
38
40
  }
@@ -57,6 +59,8 @@ export declare const buildEpisodeSummaryPayload: (input: {
57
59
  enabled: boolean;
58
60
  redactPii: boolean;
59
61
  };
62
+ config?: BikkyConfig;
63
+ origin?: OperationOrigin;
60
64
  }) => EpisodeSummaryPayloadResult;
61
65
  export declare const updateEpisodeSummary: (input: {
62
66
  segment: EpisodeSegment;