cordenar-mcp 0.1.1

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,58 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.1] - 2026-07-11
9
+
10
+ ### Fixed
11
+ - Compend synapse parity — description, body, and preview now read from
12
+ Compend's concepts table. Content cell fallback chain shows preview,
13
+ title, description, then slug. Detail panel renders full body text.
14
+ Search includes description and body fields.
15
+
16
+ ### Changed
17
+ - Page title simplified to "Cordenar".
18
+ - Layout width matched to Compend/Hemisphere — `#app` wrapper at 960px
19
+ content area (was 920px due to padding inside body container).
20
+
21
+ ## [0.1.0] - 2026-07-10
22
+
23
+ ### Added
24
+
25
+ - Initial scaffold: MCP stdio server with 6 tool registrations (`cordenar_auth`, `cordenar_status`, `cordenar_list`, `cordenar_share`, `cordenar_pull`, `cordenar_sync`)
26
+ - Config singleton (`config.js`) — code defaults, `~/.cordenar/config.json`, environment variable overrides
27
+ - Sync database (`db.js`) — `synapse_map` table with CRUD operations for cloud-to-local ID reconciliation
28
+ - Sync logic (`sync.js`) — `cordenar_list` reads both Hemisphere and Compend databases (read-only), unified synapse view with `already_shared` flag
29
+ - Auth module (`auth.js`) — JWT token store at `~/.cordenar/auth.json`, with load, save, clear, and validation helpers
30
+ - Supabase client wrapper (`supabase.js`) — authenticated and anonymous Supabase client creation
31
+ - Embedding module (`embedding.js`) — MurmurHash3 256-dim LSH vector (copy from Hemisphere, deterministic, ~2µs per call)
32
+ - Local dashboard (port 3458) — vanilla JS SPA with dark/light theme, SSE real-time updates, skeleton loading, toast notifications, WCAG 2.1 AA accessibility
33
+ - Dashboard kebab menu per synapse — context-sensitive actions by status (Share, Unlink, Remove Local, Dismiss)
34
+ - Source and status filters with debounced search
35
+ - Confirm dialog for destructive actions
36
+ - Theme toggle with persistence (`localStorage`) and FOUC prevention via synchronous head script
37
+ - CLI commands (`cordenar`, `cordenar stop`, `cordenar restart`)
38
+ - Global bin entry and npm scripts
39
+ - Project-level OpenCode config with ponytail plugin
40
+ - `.gitignore` and `.npmignore` for database and environment files
41
+ - Real-time SSE — `notifyDash()` wired in MCP index.js with event notifications after share/pull/sync tool calls
42
+ - External event remapping in `/api/notify` handler — `memory_*`/`compend_*`/`concept_*`/`project_*` events remapped to `synapse_updated`
43
+ - Named Service Discovery — `~/.cordenar/manifest.json` written on startup, deleted on shutdown. Producers (Hemisphere/Compend) reference `"notifySubscribers": ["cordenar"]` instead of raw URLs
44
+ - Dashboard search filter in `/api/synapses` (matches preview, title, type)
45
+
46
+ ### Changed
47
+
48
+ - Replaced browser fallback polling with `synapse_updated` SSE listener
49
+ - Dynamic imports replaced with static imports in MCP index.js
50
+ - Combined Hemisphere+Compend synapse results sorted by `created_at` DESC
51
+ - `sync_complete` SSE notification gated on actual sync results
52
+ - Event name remapping: external tool events normalized to `synapse_updated` for dashboard consumption
53
+
54
+ ### Fixed
55
+
56
+ - Dashboard SSE not reaching browser — events now delivered via active HTTP request cycles (`notifyDash` → `/api/notify` → `broadcast`)
57
+ - Dashboard search box returning unfiltered results
58
+ - Removed unused `client` variable in `supabase.js`
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hector Jarquin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,330 @@
1
+ <img src="dashboard/public/logo.svg" height="48" alt="" />
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
4
+
5
+ # Cordenar MCP
6
+
7
+ Cloud orchestration MCP server — bridges local [Hemisphere](https://github.com/hectorjarquin/hemisphere) and [Compend](https://github.com/hectorjarquin/compend) knowledge bases to distributed team synapses.
8
+
9
+ Cordenar gives teams the ability to securely share, approve, and synchronize knowledge across MCP nodes. Think of it as the control plane that makes context portable, auditable, and collaborative — whether you're running a single server or a distributed mesh.
10
+
11
+ Built entirely in Node.js with zero cloud dependencies in the MCP layer. Syncing happens through Supabase on the cloud side.
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ npm install -g cordenar-mcp
17
+ ```
18
+
19
+ Once installed, run the dashboard:
20
+
21
+ ```bash
22
+ cordenar
23
+ # → http://localhost:3458
24
+ ```
25
+
26
+ ### CLI commands
27
+
28
+ ```bash
29
+ cordenar # Start the dashboard
30
+ cordenar stop # Stop a running instance
31
+ cordenar restart # Stop then restart
32
+ ```
33
+
34
+ ### MCP server
35
+
36
+ Add to `opencode.json`:
37
+
38
+ ```json
39
+ {
40
+ "mcp": {
41
+ "cordenar": {
42
+ "type": "local",
43
+ "command": ["node", "/home/user/.nvm/versions/node/v22.22.0/lib/node_modules/cordenar-mcp/index.js"],
44
+ "enabled": true
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ Find your exact path with `npm root -g` — append `/cordenar-mcp/index.js`.
51
+
52
+ ### Dashboard
53
+
54
+ Browse and manage your local synapses at `http://localhost:3458`.
55
+
56
+ Port configurable via `~/.cordenar/config.json` or `CORDENAR_PORT`.
57
+
58
+ **Features:** dark/light theme toggle with persistence, WCAG 2.1 AA accessibility (keyboard navigation, ARIA labels, focus-visible outlines), real-time SSE updates (no polling), toast notifications, skeleton loading, search with debounce, source and status filters, expandable detail rows, kebab menu per synapse with context-sensitive actions (Share, Unlink, Remove Local, Dismiss).
59
+
60
+ ## How It Works
61
+
62
+ ```
63
+ Local DBs (Hemisphere/Compend) → Cordenar MCP → Supabase (cloud) → Team nodes
64
+ (read-only) (sync) (orgs/groups) (pull + SSE revoke)
65
+ ```
66
+
67
+ ### Synapse Lifecycle
68
+
69
+ 1. **Share** — Select local synapses from Hemisphere or Compend. Push to cloud as `pending_approval`.
70
+ 2. **Approve** — Org admin reviews and approves in the cloud dashboard.
71
+ 3. **Sync** — Team members pull approved synapses into their local knowledge bases.
72
+ 4. **Revoke** — Admin can revoke access at any time. Synapse is removed from local DBs via SSE.
73
+
74
+ ### Architecture
75
+
76
+ Cordenar MCP is **standalone** — it reads Hemisphere and Compend databases directly (read-only) and does not wrap or extend either MCP. Hemisphere and Compend remain sovereign, local-first, open-source MCPs. Cordenar is the cloud bridge for distributed synapse sharing across nodes.
77
+
78
+ ### Real-Time Updates
79
+
80
+ Cordenar receives real-time notifications from Hemisphere and Compend via **Named Service Discovery**:
81
+
82
+ 1. When Cordenar starts, it writes `~/.cordenar/manifest.json`:
83
+ ```json
84
+ { "name": "cordenar", "notifyEndpoint": "http://127.0.0.1:3458/api/notify" }
85
+ ```
86
+ 2. Configure Hemisphere and Compend to subscribe by name:
87
+ ```json
88
+ // ~/.hemisphere/config.json
89
+ { "notifySubscribers": ["cordenar"] }
90
+
91
+ // ~/.compend/config.json
92
+ { "notifySubscribers": ["cordenar"] }
93
+ ```
94
+ 3. On every tool call (`memory_store`, `compend_index`, etc.), Hemisphere and Compend read the manifest and POST the event to Cordenar's notify endpoint
95
+ 4. Cordenar remaps external events (`memory_*`, `compend_*`, `concept_*`, `project_*`) to `synapse_updated` and broadcasts to the browser via SSE
96
+ 5. On shutdown, the manifest is deleted. No polling — everything is push-based and real-time.
97
+
98
+ ## Installation
99
+
100
+ ### Prerequisites
101
+
102
+ - Node.js 18+
103
+ - C++ build tools (`build-essential` on Debian/Ubuntu, Xcode CLI tools on macOS) — required to compile `better-sqlite3`
104
+
105
+ ### From npm (recommended)
106
+
107
+ ```bash
108
+ npm install -g cordenar-mcp
109
+ ```
110
+
111
+ The `cordenar` CLI is now available globally. The MCP server runs via your AI client — see [Configuration](#configuration-1).
112
+
113
+ ### From git
114
+
115
+ ```bash
116
+ git clone https://github.com/hectorjarquin/cordenar-mcp.git ~/cordenar
117
+ cd ~/cordenar
118
+ npm install
119
+ npm link # creates global `cordenar` command
120
+ ```
121
+
122
+ ### Configuration
123
+
124
+ ```json
125
+ {
126
+ "mcp": {
127
+ "cordenar": {
128
+ "type": "local",
129
+ "command": ["node", "/usr/lib/node_modules/cordenar-mcp/index.js"],
130
+ "enabled": true
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ Find your exact path with `npm root -g` — append `/cordenar-mcp/index.js`. For nvm users the path is typically `~/.nvm/versions/node/vX/lib/node_modules/cordenar-mcp/index.js`.
137
+
138
+ ## MCP Tools (Human-Facing)
139
+
140
+ These tools are available through your AI client when Cordenar MCP is configured. They provide manual control over synapse sharing — no automatic uploads or background sync.
141
+
142
+ ### `cordenar_auth`
143
+
144
+ Authenticate this node with Cordenar cloud via device code OAuth flow. Opens a browser or prints a URL + code for the user to enter in the dashboard.
145
+
146
+ No parameters. Returns authentication state and node ID.
147
+
148
+ ### `cordenar_status`
149
+
150
+ Show current auth state, node info, pending push count, un-pulled synapse count, and last sync time.
151
+
152
+ No parameters.
153
+
154
+ ### `cordenar_list`
155
+
156
+ List local synapses (Hemisphere memories + Compend concepts) available for sharing. Supports filters.
157
+
158
+ | Parameter | Type | Required | Default | Description |
159
+ |-----------|------|----------|---------|-------------|
160
+ | `source` | string | no | — | Filter by source: `'hemisphere'` or `'compend'` |
161
+ | `type` | string | no | — | Filter by type: memory kind or concept type |
162
+ | `project` | string | no | — | Filter Hemisphere memories by project namespace |
163
+ | `limit` | number | no | `50` | Max results |
164
+
165
+ Returns a unified view with `source`, `type`, `preview`, `status`, and `already_shared` flags.
166
+
167
+ ### `cordenar_share`
168
+
169
+ Push selected local synapses to the cloud. Synapses enter `pending_approval` status until an admin approves them.
170
+
171
+ | Parameter | Type | Required | Default | Description |
172
+ |-----------|------|----------|---------|-------------|
173
+ | `synapses` | array | yes | — | Array of `{ source, id }` objects to share |
174
+
175
+ ### `cordenar_pull`
176
+
177
+ Fetch approved active synapses from your groups and store them locally.
178
+
179
+ | Parameter | Type | Required | Default | Description |
180
+ |-----------|------|----------|---------|-------------|
181
+ | `group_id` | string | no | — | Limit pull to a specific group |
182
+
183
+ Pulled memories are stored in Hemisphere with `project='cordenar'`, `kind='synapse'`. Pulled concepts are stored in Compend with `source='remote'`.
184
+
185
+ ### `cordenar_sync`
186
+
187
+ Full bidirectional sync: push locally-edited shared synapses, detect and unlink locally-deleted shared synapses, pull new approved synapses from cloud.
188
+
189
+ No parameters. Returns summary of pushed, unlinked, and pulled counts.
190
+
191
+ ## HTTP API (Dashboard-Facing)
192
+
193
+ The dashboard exposes REST endpoints:
194
+
195
+ | Method | Path | Description |
196
+ |--------|------|-------------|
197
+ | `GET` | `/api/synapses` | List synapses with source, status, and search filters |
198
+ | `GET` | `/api/status` | Auth state, push/pull/dirty counts |
199
+ | `GET` | `/api/events` | SSE stream for real-time dashboard updates |
200
+ | `POST` | `/api/sync` | Trigger full bidirectional sync |
201
+ | `POST` | `/api/notify` | SSE event relay. Accepts external events from Hemisphere/Compend (via `notifySubscribers`) and MCP tools (via `notifyDash`). Remaps `memory_*`/`compend_*`/`concept_*`/`project_*` → `synapse_updated`. |
202
+ | `POST` | `/api/synapses/:id/share?source=` | Share a synapse to cloud |
203
+ | `POST` | `/api/synapses/:id/unshare?source=` | Cancel a pending share |
204
+ | `POST` | `/api/synapses/:id/unlink?source=` | Remove mapping, keep local entity |
205
+ | `POST` | `/api/synapses/:id/pull-delete?source=` | Delete local copy and unlink |
206
+ | `POST` | `/api/synapses/:id/dismiss?source=` | Dismiss a rejected synapse |
207
+
208
+ ## Configuration
209
+
210
+ ### Config File (`~/.cordenar/config.json`)
211
+
212
+ Create an optional JSON config file to customize settings. All keys are optional — missing keys use code defaults.
213
+
214
+ **Priority chain** (highest wins): code defaults < config file < environment variables.
215
+
216
+ ```json
217
+ {
218
+ "port": 3458,
219
+ "dbPath": "~/.cordenar/sync.db",
220
+ "hemisphereDb": "~/.hemisphere/memories.db",
221
+ "compendDb": "~/.compend/concepts.db",
222
+ "supabaseUrl": "",
223
+ "supabaseAnonKey": ""
224
+ }
225
+ ```
226
+
227
+ | Key | Type | Default | Description |
228
+ |-----|------|---------|-------------|
229
+ | `port` | number | `3458` | Dashboard HTTP server port |
230
+ | `dbPath` | string | `~/.cordenar/sync.db` | Sync database path (supports `~`) |
231
+ | `hemisphereDb` | string | `~/.hemisphere/memories.db` | Hemisphere database path |
232
+ | `compendDb` | string | `~/.compend/concepts.db` | Compend database path |
233
+ | `supabaseUrl` | string | — | Supabase project URL (required for cloud sync) |
234
+ | `supabaseAnonKey` | string | — | Supabase anonymous key (required for cloud sync) |
235
+ | `manifestPath` | string | `~/.cordenar/manifest.json` | Named Service Discovery manifest path |
236
+
237
+ ### Hemisphere & Compend Integration
238
+
239
+ Cordenar requires no configuration on its side to receive events. Instead, configure Hemisphere and Compend to push events to Cordenar:
240
+
241
+ ```json
242
+ // ~/.hemisphere/config.json
243
+ { "notifySubscribers": ["cordenar"] }
244
+
245
+ // ~/.compend/config.json
246
+ { "notifySubscribers": ["cordenar"] }
247
+ ```
248
+
249
+ When configured, every tool call in Hemisphere (`memory_store`, `memory_trash`, `memory_update`, etc.) and Compend (`compend_index`) will notify Cordenar in real time. The Cordenar manifest (`~/.cordenar/manifest.json`) is created automatically on startup and deleted on shutdown — no URL to maintain.
250
+
251
+ Hemisphere config key: `notifySubscribers` (array of service names). Env override: `HEMISPHERE_NOTIFY_SUBSCRIBERS` (comma-separated).
252
+ Compend config key: `notifySubscribers` (array of service names). Env override: `COMPEND_NOTIFY_SUBSCRIBERS` (comma-separated).
253
+
254
+ ### Environment Variables
255
+
256
+ Environment variables override the config file and code defaults at the highest priority.
257
+
258
+ | Variable | Equivalent Config Key | Default |
259
+ |----------|----------------------|---------|
260
+ | `CORDENAR_PORT` | `port` | `3458` |
261
+ | `CORDENAR_DB_PATH` | `dbPath` | `~/.cordenar/sync.db` |
262
+ | `CORDENAR_DIR` | `cordenarDir` | `~/.cordenar` |
263
+ | `SUPABASE_URL` | `supabaseUrl` | — |
264
+ | `SUPABASE_ANON_KEY` | `supabaseAnonKey` | — |
265
+
266
+ ## Project Structure
267
+
268
+ ```
269
+ cordenar-mcp/
270
+ ├── index.js MCP stdio server (6 tool handlers)
271
+ ├── dashboard.js HTTP dashboard + SSE broadcast server
272
+ ├── dashboard/
273
+ │ ├── api-handler.js Dashboard REST API routes
274
+ │ └── public/
275
+ │ ├── index.html Dashboard HTML + ARIA structure
276
+ │ ├── style.css Full theme (dark/light), toast, dialog, skeleton
277
+ │ ├── app.js Frontend SSE client, keyboard nav, WCAG 2.1 AA
278
+ │ └── logo.svg Network icon logo
279
+ ├── db.js Sync database init + synapse_map CRUD
280
+ ├── sync.js Push/pull/hash-diff/unlink logic
281
+ ├── auth.js Device code OAuth + JWT store
282
+ ├── supabase.js Supabase client wrapper
283
+ ├── embedding.js MurmurHash3 → 256-dim float vector
284
+ ├── config.js Config loader (defaults ← config.json ← env vars)
285
+ ├── package.json
286
+ ├── CHANGELOG.md
287
+ └── README.md
288
+ ```
289
+
290
+ On startup, Cordenar writes `~/.cordenar/manifest.json` for Named Service Discovery. On shutdown, it is deleted automatically.
291
+
292
+ ## Development
293
+
294
+ ### Scripts
295
+
296
+ For local development (after `git clone`):
297
+
298
+ ```bash
299
+ npm start # Start dashboard server (same as `cordenar`)
300
+ npm run stop # Stop running instance (same as `cordenar stop`)
301
+ npm run restart # Stop then start
302
+ ```
303
+
304
+ For installed users, the global `cordenar` CLI handles these — see [Quick Start](#quick-start).
305
+
306
+ ### Testing the MCP server
307
+
308
+ Test with the MCP Inspector:
309
+
310
+ ```bash
311
+ npx @modelcontextprotocol/inspector node index.js
312
+ ```
313
+
314
+ ## Roadmap
315
+
316
+ - Supabase integration (device auth, synapse push/pull, SSE revocation)
317
+ - Full bidirectional sync with hash diff and conflict resolution
318
+ - Cloud dashboard (React SPA — org management, approval queue, billing)
319
+ - Lemon Squeezy billing integration (annual subscriptions per node)
320
+ - Multi-node registration and seat management
321
+ - Role-based access control (Owner, Admin, Member)
322
+ - Tier enforcement (Team, Business, Enterprise)
323
+
324
+ ## Contributing
325
+
326
+ Open an issue or PR at [github.com/hectorjarquin/cordenar-mcp](https://github.com/hectorjarquin/cordenar-mcp).
327
+
328
+ ## License
329
+
330
+ MIT License — see [LICENSE](LICENSE) for details.
package/auth.js ADDED
@@ -0,0 +1,48 @@
1
+ // Cordenar MCP — Device code OAuth + JWT store
2
+ // Pattern: ~/.cordenar/auth.json holds the session
3
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
4
+ import { getConfig } from './config.js';
5
+
6
+ export function loadAuth() {
7
+ const cfg = getConfig();
8
+ try {
9
+ if (!existsSync(cfg.authFile)) return null;
10
+ const raw = readFileSync(cfg.authFile, 'utf-8');
11
+ return JSON.parse(raw);
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ export function saveAuth(data) {
18
+ const cfg = getConfig();
19
+ writeFileSync(cfg.authFile, JSON.stringify(data, null, 2));
20
+ }
21
+
22
+ export function clearAuth() {
23
+ const cfg = getConfig();
24
+ try {
25
+ if (existsSync(cfg.authFile)) {
26
+ writeFileSync(cfg.authFile, '');
27
+ }
28
+ } catch {}
29
+ }
30
+
31
+ export function isAuthenticated() {
32
+ const auth = loadAuth();
33
+ if (!auth || !auth.access_token) return false;
34
+ if (auth.expires_at && Date.now() / 1000 > auth.expires_at) return false;
35
+ return true;
36
+ }
37
+
38
+ export function getAccessToken() {
39
+ const auth = loadAuth();
40
+ if (!auth) return null;
41
+ return auth.access_token;
42
+ }
43
+
44
+ export function getNodeId() {
45
+ const auth = loadAuth();
46
+ if (!auth) return null;
47
+ return auth.node_id;
48
+ }
package/config.js ADDED
@@ -0,0 +1,87 @@
1
+ // Cordenar MCP — Configuration loader
2
+ // Pattern mirrors Hemisphere (config.js) and Compend (config.js)
3
+ // Priority: code defaults → config file → environment variables
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
7
+
8
+ const DEFAULTS = {
9
+ port: 3458,
10
+ cordenarDir: join(homedir(), '.cordenar'),
11
+ dbPath: join(homedir(), '.cordenar', 'sync.db'),
12
+ hemisphereDb: join(homedir(), '.hemisphere', 'memories.db'),
13
+ compendDb: join(homedir(), '.compend', 'concepts.db'),
14
+ authFile: join(homedir(), '.cordenar', 'auth.json'),
15
+ manifestPath: join(homedir(), '.cordenar', 'manifest.json'),
16
+ supabaseUrl: '',
17
+ supabaseAnonKey: '',
18
+ };
19
+
20
+ const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
21
+
22
+ export function deepMerge(target, source) {
23
+ for (const key of Object.keys(source)) {
24
+ if (!Object.hasOwn(target, key)) {
25
+ target[key] = JSON.parse(JSON.stringify(source[key]));
26
+ continue;
27
+ }
28
+ const tv = target[key];
29
+ const sv = source[key];
30
+ if (isObject(tv) && isObject(sv)) {
31
+ deepMerge(tv, sv);
32
+ } else {
33
+ target[key] = sv;
34
+ }
35
+ }
36
+ return target;
37
+ }
38
+
39
+ function readConfigFile() {
40
+ const path = join(homedir(), '.cordenar', 'config.json');
41
+ try {
42
+ const raw = readFileSync(path, 'utf-8');
43
+ return JSON.parse(raw);
44
+ } catch {
45
+ return {};
46
+ }
47
+ }
48
+
49
+ function resolveTilde(p) {
50
+ if (typeof p !== 'string') return p;
51
+ if (p.startsWith('~/') || p === '~') {
52
+ return join(homedir(), p.slice(1));
53
+ }
54
+ return p;
55
+ }
56
+
57
+ function applyEnvOverrides(cfg) {
58
+ if (process.env.SUPABASE_URL) cfg.supabaseUrl = process.env.SUPABASE_URL;
59
+ if (process.env.SUPABASE_ANON_KEY)
60
+ cfg.supabaseAnonKey = process.env.SUPABASE_ANON_KEY;
61
+ if (process.env.CORDENAR_PORT)
62
+ cfg.port = parseInt(process.env.CORDENAR_PORT, 10) || 3458;
63
+ if (process.env.CORDENAR_DIR)
64
+ cfg.cordenarDir = resolveTilde(process.env.CORDENAR_DIR);
65
+ if (process.env.CORDENAR_DB_PATH)
66
+ cfg.dbPath = resolveTilde(process.env.CORDENAR_DB_PATH);
67
+ return cfg;
68
+ }
69
+
70
+ let _cfg = null;
71
+
72
+ export function getConfig() {
73
+ if (_cfg) return _cfg;
74
+ const defaults = JSON.parse(JSON.stringify(DEFAULTS));
75
+ _cfg = deepMerge(defaults, readConfigFile());
76
+ applyEnvOverrides(_cfg);
77
+ return _cfg;
78
+ }
79
+
80
+ export function getDbPath() {
81
+ const cfg = getConfig();
82
+ const dir = join(cfg.dbPath, '..');
83
+ if (!existsSync(dir)) {
84
+ mkdirSync(dir, { recursive: true });
85
+ }
86
+ return cfg.dbPath;
87
+ }