compend 0.0.0 → 1.0.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.
- package/CHANGELOG.md +34 -0
- package/README.md +377 -2
- package/config.js +147 -0
- package/dashboard/api-handler.js +77 -0
- package/dashboard/public/app.js +338 -0
- package/dashboard/public/index.html +66 -0
- package/dashboard/public/logo.svg +1 -0
- package/dashboard/public/style.css +497 -0
- package/dashboard.js +203 -0
- package/db.js +569 -0
- package/embedding.js +81 -0
- package/index.js +179 -0
- package/logo.svg +1 -1
- package/package.json +20 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Compend 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
|
+
## [1.0.0] - 2026-06-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release
|
|
12
|
+
- 5 MCP tools: `compend_index`, `compend_deindex`, `compend_search`, `compend_get`, `compend_list`
|
|
13
|
+
- `compend_index` mirrors filesystem into index — accepts file path, directory path, or no args (all configured paths)
|
|
14
|
+
- `compend_deindex` removes concepts by slug or path prefix (files never touched)
|
|
15
|
+
- `compend_search` with hybrid FTS5 + vec0 weighted scoring (MurmurHash3 256-dim, alpha 0-1)
|
|
16
|
+
- `compend_get` returns full concept: frontmatter JSON, markdown body, children (references), and dependencies
|
|
17
|
+
- `compend_list` browse with type, tag, and status filters + pagination
|
|
18
|
+
- Dashboard on port 3457: read-only, dark/light theme, SSE real-time updates, type/tag/status filters, rendered markdown viewer
|
|
19
|
+
- CLI: `compend` (start dashboard), `compend stop`, `compend restart`
|
|
20
|
+
- 7 extensible concept types: skill, agent, instruction, prompt, workflow, reference, knowledge
|
|
21
|
+
- Type schemas in config.js with per-type status validation (matching Hemisphere's kind schema pattern)
|
|
22
|
+
- OKF frontmatter parsing (YAML + markdown body) with automatic type inference
|
|
23
|
+
- Auto-index on first run from opencode.json `skills.paths` + `instructions`
|
|
24
|
+
- Parent/child concept resolution via slug hierarchy (file path convention)
|
|
25
|
+
- Dependency resolution from OKF frontmatter `dependencies` field
|
|
26
|
+
- `~/.compend/config.json` config with env var overrides (`COMPEND_PORT`, `COMPEND_DB_PATH`)
|
|
27
|
+
- Index paths from opencode.json auto-discovery + `~/.compend/config.json` overrides
|
|
28
|
+
|
|
29
|
+
## [0.0.1] - 2026-06-27
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- Placeholder release to secure npm namespace
|
|
33
|
+
- Logo SVG (48x48 book icon, currentColor)
|
|
34
|
+
- GPL-3.0-only license
|
package/README.md
CHANGED
|
@@ -2,8 +2,383 @@
|
|
|
2
2
|
|
|
3
3
|
# Compend
|
|
4
4
|
|
|
5
|
+
[](https://www.gnu.org/licenses/gpl-3.0) [](https://nodejs.org)
|
|
6
|
+
|
|
7
|
+
**Author:** [Hector Jarquin](https://hectorjarquin.com)
|
|
8
|
+
|
|
5
9
|
A reference engine for AI agents. Index your Markdown skills, prompts, and knowledge bases — and serve them as just-in-time grounding with hybrid search and dynamic context. No retraining. No keys. No Python.
|
|
6
10
|
|
|
7
|
-
##
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g compend
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Once installed, run the dashboard:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
compend
|
|
21
|
+
# → http://localhost:3457
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### CLI commands
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
compend # Start the dashboard
|
|
28
|
+
compend stop # Stop a running instance
|
|
29
|
+
compend restart # Stop then restart
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### MCP server
|
|
33
|
+
|
|
34
|
+
Add to `opencode.json`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcp": {
|
|
39
|
+
"compend": {
|
|
40
|
+
"type": "local",
|
|
41
|
+
"command": ["node", "/usr/lib/node_modules/compend/index.js"],
|
|
42
|
+
"enabled": true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Find your exact path with `npm root -g` — append `/compend/index.js`. For nvm users the path is typically `~/.nvm/versions/node/vX/lib/node_modules/compend/index.js`.
|
|
49
|
+
|
|
50
|
+
### Dashboard
|
|
51
|
+
|
|
52
|
+
Browse, search and read knowledge concepts visually at `http://localhost:3457`.
|
|
53
|
+
|
|
54
|
+
Port configurable via `~/.compend/config.json` or `COMPEND_PORT`.
|
|
55
|
+
|
|
56
|
+
**Features:** dark/light theme toggle with SVG icon, 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, type, tag, and status filters, expandable detail rows with rendered markdown body.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
### Prerequisites
|
|
61
|
+
|
|
62
|
+
- Node.js 18+
|
|
63
|
+
- C++ build tools (`build-essential` on Debian/Ubuntu, Xcode CLI tools on macOS) — required to compile `better-sqlite3`
|
|
64
|
+
|
|
65
|
+
### From npm (recommended)
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install -g compend
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The `compend` CLI is now available globally. The MCP server runs via your AI client — see [Configuration](#configuration).
|
|
72
|
+
|
|
73
|
+
### From git
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
git clone https://github.com/hectorjarquin/compend.git ~/compend
|
|
77
|
+
cd ~/compend
|
|
78
|
+
npm install
|
|
79
|
+
npm link # creates global compend command
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Configuration
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcp": {
|
|
87
|
+
"compend": {
|
|
88
|
+
"type": "local",
|
|
89
|
+
"command": ["node", "/usr/lib/node_modules/compend/index.js"],
|
|
90
|
+
"enabled": true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Find your exact path with `npm root -g` — append `/compend/index.js`.
|
|
97
|
+
|
|
98
|
+
Concepts are discovered automatically from your opencode.json `skills.paths` and `instructions` directories. Add custom project knowledge bundles via `~/.compend/config.json`.
|
|
99
|
+
|
|
100
|
+
## Updating
|
|
101
|
+
|
|
102
|
+
### npm
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm install -g compend@latest
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Then restart your MCP client to pick up the new tools, and run `compend restart` to refresh the dashboard.
|
|
109
|
+
|
|
110
|
+
Database migrations run automatically on first launch — no manual steps required.
|
|
111
|
+
|
|
112
|
+
### From git
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
cd ~/compend
|
|
116
|
+
git pull
|
|
117
|
+
npm install
|
|
118
|
+
npm link
|
|
119
|
+
compend restart
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## MCP Tools (Agent-Facing)
|
|
123
|
+
|
|
124
|
+
### `compend_index`
|
|
125
|
+
|
|
126
|
+
Mirror the filesystem source of truth into the index. No args scans all configured paths. Pass `{ path }` to index a single `.md` file or a directory (recursively). Uses SHA-256 hash diffing — unchanged files are skipped. Files missing from disk are removed from the index.
|
|
127
|
+
|
|
128
|
+
| Parameter | Type | Required | Default | Description |
|
|
129
|
+
|-----------|------|----------|---------|-------------|
|
|
130
|
+
| `path` | string | no | — | Absolute path to a `.md` file or directory to index. Omit to scan all configured paths. |
|
|
131
|
+
|
|
132
|
+
Returns `{ added: [...slugs], updated: [...slugs], removed: [...slugs], total: N }`.
|
|
133
|
+
|
|
134
|
+
### `compend_deindex`
|
|
135
|
+
|
|
136
|
+
Remove concepts from the index. Pass `{ slug }` to remove one concept, or `{ path }` to remove all concepts under a directory path. Files on disk are never touched — deindex is index-only. If the file still exists, the next `compend_index` will re-index it.
|
|
137
|
+
|
|
138
|
+
| Parameter | Type | Required | Default | Description |
|
|
139
|
+
|-----------|------|----------|---------|-------------|
|
|
140
|
+
| `slug` | string | no | — | Concept slug to remove |
|
|
141
|
+
| `path` | string | no | — | Directory or file path — all concepts whose `file_path` starts with this are removed |
|
|
142
|
+
|
|
143
|
+
Returns `{ removed: [...slugs], total: N }`.
|
|
144
|
+
|
|
145
|
+
### `compend_search`
|
|
146
|
+
|
|
147
|
+
Hybrid FTS + vector search across indexed concepts. Returns metadata with snippet and relevance score.
|
|
148
|
+
|
|
149
|
+
| Parameter | Type | Required | Default | Description |
|
|
150
|
+
|-----------|------|----------|---------|-------------|
|
|
151
|
+
| `query` | string | yes | — | Search query text |
|
|
152
|
+
| `type` | string | no | — | Filter by concept type (skill, agent, instruction, etc.) |
|
|
153
|
+
| `tags` | string[] | no | — | Filter by tags (AND match) |
|
|
154
|
+
| `limit` | number | no | `10` | Max results |
|
|
155
|
+
| `alpha` | number | no | `0.3` | Vector weight. `0` = FTS-only, `1` = vector-only |
|
|
156
|
+
|
|
157
|
+
Returns concepts sorted by relevance (score 0–1) with `id`, `slug`, `type`, `title`, `description`, `tags`, `status`, `source`, `score`, and `snippet`.
|
|
158
|
+
|
|
159
|
+
### `compend_get`
|
|
160
|
+
|
|
161
|
+
Retrieve a full concept by slug. Includes frontmatter JSON, markdown body, child references (concepts whose slug starts with `{slug}/`), and dependencies (from the OKF `dependencies` frontmatter field).
|
|
162
|
+
|
|
163
|
+
| Parameter | Type | Required | Default | Description |
|
|
164
|
+
|-----------|------|----------|---------|-------------|
|
|
165
|
+
| `slug` | string | yes | — | Concept slug (e.g. `"wp-image-to-blocks"`) |
|
|
166
|
+
|
|
167
|
+
Returns `{ id, slug, type, title, description, tags, status, frontmatter, body, references: [...], dependencies: [...] }`.
|
|
168
|
+
|
|
169
|
+
### `compend_list`
|
|
170
|
+
|
|
171
|
+
List concepts with optional filters. Returns compact metadata — no body.
|
|
172
|
+
|
|
173
|
+
| Parameter | Type | Required | Default | Description |
|
|
174
|
+
|-----------|------|----------|---------|-------------|
|
|
175
|
+
| `type` | string | no | — | Filter by concept type |
|
|
176
|
+
| `tags` | string[] | no | — | Filter by tags (AND match) |
|
|
177
|
+
| `status` | string | no | — | Filter by status (stable, draft, deprecated) |
|
|
178
|
+
| `limit` | number | no | `50` | Max results |
|
|
179
|
+
| `offset` | number | no | `0` | Pagination offset |
|
|
180
|
+
|
|
181
|
+
Returns `{ concepts: [...], total, limit, offset }`.
|
|
182
|
+
|
|
183
|
+
## HTTP API (Dashboard-Facing)
|
|
184
|
+
|
|
185
|
+
The dashboard exposes REST endpoints:
|
|
186
|
+
|
|
187
|
+
| Method | Path | Description |
|
|
188
|
+
|--------|------|-------------|
|
|
189
|
+
| `GET` | `/api/stats` | Get total concept count and counts by type |
|
|
190
|
+
| `GET` | `/api/tags?type=` | List tags with concept counts, optional type filter |
|
|
191
|
+
| `GET` | `/api/concepts?type=&status=&tags=&search=&limit=&offset=` | Paginated concept list. `search=` triggers FTS+vector. |
|
|
192
|
+
| `GET` | `/api/concepts/{slug}` | Full concept including frontmatter, body, references, and dependencies |
|
|
193
|
+
| `POST` | `/api/notify` | SSE event relay from MCP server (internal — called by `notifyDash()`) |
|
|
194
|
+
| `GET` | `/api/events` | SSE (Server-Sent Events) stream for real-time dashboard updates |
|
|
195
|
+
|
|
196
|
+
## Concept Types
|
|
197
|
+
|
|
198
|
+
Seven types are built into the default schema, extensible via `~/.compend/config.json`:
|
|
199
|
+
|
|
200
|
+
| Type | Statuses | Default | Description |
|
|
201
|
+
|------|----------|---------|-------------|
|
|
202
|
+
| `skill` | `stable`, `draft`, `deprecated` | `stable` | Agent skill definition (SKILL.md) |
|
|
203
|
+
| `agent` | `stable`, `draft`, `deprecated` | `stable` | Agent/subagent definition |
|
|
204
|
+
| `instruction` | `stable`, `draft`, `deprecated` | `stable` | Instruction files injected into system prompt |
|
|
205
|
+
| `prompt` | `stable`, `draft`, `deprecated` | `stable` | Prompt templates |
|
|
206
|
+
| `workflow` | `stable`, `draft`, `deprecated` | `stable` | Multi-step pipeline definitions |
|
|
207
|
+
| `reference` | — (no validation) | — | Reference documents, examples, templates |
|
|
208
|
+
| `knowledge` | — (no validation) | — | Project domain knowledge (OKF bundles) |
|
|
209
|
+
|
|
210
|
+
Type is inferred from OKF frontmatter, file path, or directory location. Unknown types pass through without validation — existing data is never affected. Custom types can be added via `~/.compend/config.json`:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"schemas": {
|
|
215
|
+
"default": {
|
|
216
|
+
"types": {
|
|
217
|
+
"template": { "statuses": ["draft","published","retired"], "defaults": { "status": "draft" } }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Configuration
|
|
225
|
+
|
|
226
|
+
### Config File (`~/.compend/config.json`)
|
|
227
|
+
|
|
228
|
+
Create an optional JSON config file to customize operational settings. All keys are optional — missing keys use the code defaults.
|
|
229
|
+
|
|
230
|
+
**Priority chain** (highest wins): code defaults < config file < environment variables.
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"port": 3457,
|
|
235
|
+
"dbPath": "~/.compend/concepts.db",
|
|
236
|
+
"search": {
|
|
237
|
+
"limit": 10,
|
|
238
|
+
"alpha": 0.3
|
|
239
|
+
},
|
|
240
|
+
"dashboard": {
|
|
241
|
+
"paginationLimit": 50,
|
|
242
|
+
"maxLimit": 200
|
|
243
|
+
},
|
|
244
|
+
"schemas": {
|
|
245
|
+
"default": {
|
|
246
|
+
"types": {}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
"index": {
|
|
250
|
+
"paths": []
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
| Key | Type | Default | Description |
|
|
256
|
+
|-----|------|---------|-------------|
|
|
257
|
+
| `port` | number | `3457` | Dashboard HTTP server port |
|
|
258
|
+
| `dbPath` | string | `~/.compend/concepts.db` | SQLite database file path (supports `~`) |
|
|
259
|
+
| `search.limit` | number | `10` | Default search result count |
|
|
260
|
+
| `search.alpha` | number | `0.3` | Vector weight in hybrid search (0–1) |
|
|
261
|
+
| `dashboard.paginationLimit` | number | `50` | Default page size for dashboard API |
|
|
262
|
+
| `dashboard.maxLimit` | number | `200` | Hard cap on API page size |
|
|
263
|
+
| `schemas.default.types` | object | built-in set | Type definitions with `statuses` and `defaults` |
|
|
264
|
+
| `index.paths` | string[] | `[]` | Additional paths to scan for `.md` files (appended to opencode auto-discovered paths) |
|
|
265
|
+
|
|
266
|
+
### Environment Variables
|
|
267
|
+
|
|
268
|
+
| Variable | Equivalent Config Key | Default |
|
|
269
|
+
|----------|----------------------|---------|
|
|
270
|
+
| `COMPEND_PORT` | `port` | `3457` |
|
|
271
|
+
| `COMPEND_DB_PATH` | `dbPath` | `~/.compend/concepts.db` |
|
|
272
|
+
|
|
273
|
+
### Path Discovery
|
|
274
|
+
|
|
275
|
+
Compend auto-discovers filesystem paths from your opencode.json:
|
|
276
|
+
|
|
277
|
+
1. `skills.paths` — skill directories (scanned recursively for `*.md`)
|
|
278
|
+
2. `instructions` — instruction files (indexed directly)
|
|
279
|
+
|
|
280
|
+
Additional paths can be added via `index.paths` in `~/.compend/config.json`. This is where you add project OKF knowledge bundles:
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"index": {
|
|
285
|
+
"paths": ["/home/user/projects/my-project/knowledge"]
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Architecture
|
|
291
|
+
|
|
292
|
+
### Embedding
|
|
293
|
+
|
|
294
|
+
MurmurHash3 (32-bit x86) applied to word unigrams and bigrams, accumulated into a 256-bin `Float32Array`, then L2-normalized. Runs in ~2µs per text with no external dependencies. Collisions are inherent to feature hashing and don't materially affect retrieval quality at this dimension count.
|
|
295
|
+
|
|
296
|
+
### Hybrid Search
|
|
297
|
+
|
|
298
|
+
Three indexing layers work together:
|
|
299
|
+
|
|
300
|
+
1. **FTS5** — SQLite full-text search with BM25 ranking (`unicode61` tokenizer)
|
|
301
|
+
2. **Vector** — `sqlite-vec` virtual table with 256-dim float vectors, cosine distance
|
|
302
|
+
3. **Merge** — Weighted combination: `score = alpha * vec + (1 - alpha) * fts`. Both sides are normalized to 0–1 before merging.
|
|
303
|
+
|
|
304
|
+
### Database
|
|
305
|
+
|
|
306
|
+
- WAL journal mode for concurrent reads, `busy_timeout=5000ms` for write-contention safety across processes
|
|
307
|
+
- `concepts` table (slug, type, title, description, tags, status, frontmatter, body, file_path, file_hash, source, created_at, updated_at, last_synced_at)
|
|
308
|
+
- `concepts_fts` — FTS5 external content table on title, description, tags, body
|
|
309
|
+
- `concepts_vec` — `vec0` table with `float[256]`
|
|
310
|
+
|
|
311
|
+
### Index vs Source of Truth
|
|
312
|
+
|
|
313
|
+
Compend does not own content. It mirrors a source of truth — the local filesystem or a remote CMS (`compend_sync` in v2). There is no CRUD lifecycle (no create/edit/delete of concepts from the index). The source is always authoritative. The index is a mirror — always rebuildable from source.
|
|
314
|
+
|
|
315
|
+
`compend_index` uses SHA-256 hash diffing: unchanged files are skipped, changed files are updated, missing files are removed from the index. `compend_deindex` removes concepts without touching files — the next index run restores them.
|
|
316
|
+
|
|
317
|
+
### SSE Real-Time Updates
|
|
318
|
+
|
|
319
|
+
The dashboard uses Server-Sent Events for live updates — no polling. Events flow through a three-hop chain:
|
|
320
|
+
|
|
321
|
+
MCP server (index.js) → `notifyDash()` → dashboard `/api/notify` → `broadcast()` → SSE clients → `app.js` listeners
|
|
322
|
+
|
|
323
|
+
| Event | Source | Frontend Behavior |
|
|
324
|
+
|---|---|---|
|
|
325
|
+
| `index_complete` | `compend_index`, `compend_deindex` | Toast with count summary + reload list |
|
|
326
|
+
|
|
327
|
+
If the SSE connection drops, a 30-second fallback poll resumes.
|
|
328
|
+
|
|
329
|
+
## Project Structure
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
compend/
|
|
333
|
+
├── index.js MCP stdio server (5 tool handlers)
|
|
334
|
+
├── dashboard.js HTTP dashboard + SSE broadcast server
|
|
335
|
+
├── dashboard/
|
|
336
|
+
│ ├── api-handler.js Dashboard REST API routes
|
|
337
|
+
│ └── public/
|
|
338
|
+
│ ├── index.html Dashboard HTML + ARIA structure
|
|
339
|
+
│ ├── style.css Full theme (dark/light), toast, skeleton
|
|
340
|
+
│ ├── app.js Frontend SSE client, keyboard nav, WCAG 2.1 AA
|
|
341
|
+
│ └── logo.svg Book icon (48x48, currentColor)
|
|
342
|
+
├── db.js SQLite init, CRUD, hybrid FTS+vec search
|
|
343
|
+
├── embedding.js MurmurHash3 → 256-dim float vector
|
|
344
|
+
├── config.js Config loader (defaults ← config.json ← env vars)
|
|
345
|
+
├── package.json
|
|
346
|
+
├── CHANGELOG.md
|
|
347
|
+
└── README.md
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Development
|
|
351
|
+
|
|
352
|
+
### Scripts
|
|
353
|
+
|
|
354
|
+
For local development (after `git clone`):
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
npm start # Start dashboard server (same as compend)
|
|
358
|
+
npm run stop # Stop running instance (same as compend stop)
|
|
359
|
+
npm run restart # Stop then start
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
For installed users, the global `compend` CLI handles these — see [Quick Start](#quick-start).
|
|
363
|
+
|
|
364
|
+
### Testing the MCP server
|
|
365
|
+
|
|
366
|
+
Test with the MCP Inspector:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
npx @modelcontextprotocol/inspector node index.js
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Roadmap
|
|
373
|
+
|
|
374
|
+
- **Remote sync** — `compend_sync` for REST-based reconciliation with distributed teams
|
|
375
|
+
- **Import / export** — portable OKF bundle archives across Compend instances
|
|
376
|
+
- **Auth / access control** — API keys for multi-client deployments
|
|
377
|
+
|
|
378
|
+
## Contributing
|
|
379
|
+
|
|
380
|
+
Open an issue or PR at [github.com/hectorjarquin/compend](https://github.com/hectorjarquin/compend).
|
|
381
|
+
|
|
382
|
+
## License
|
|
8
383
|
|
|
9
|
-
|
|
384
|
+
GNU General Public License v3.0 or later — see [LICENSE](LICENSE) for details.
|
package/config.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
port: 3457,
|
|
7
|
+
dbPath: join(homedir(), '.compend', 'concepts.db'),
|
|
8
|
+
search: {
|
|
9
|
+
limit: 10,
|
|
10
|
+
alpha: 0.3
|
|
11
|
+
},
|
|
12
|
+
dashboard: {
|
|
13
|
+
paginationLimit: 50,
|
|
14
|
+
maxLimit: 200
|
|
15
|
+
},
|
|
16
|
+
schemas: {
|
|
17
|
+
default: {
|
|
18
|
+
types: {
|
|
19
|
+
skill: { statuses: ['stable','draft','deprecated'], defaults: { status: 'stable' } },
|
|
20
|
+
agent: { statuses: ['stable','draft','deprecated'], defaults: { status: 'stable' } },
|
|
21
|
+
instruction: { statuses: ['stable','draft','deprecated'], defaults: { status: 'stable' } },
|
|
22
|
+
prompt: { statuses: ['stable','draft','deprecated'], defaults: { status: 'stable' } },
|
|
23
|
+
workflow: { statuses: ['stable','draft','deprecated'], defaults: { status: 'stable' } },
|
|
24
|
+
reference: { statuses: [], defaults: {} },
|
|
25
|
+
knowledge: { statuses: [], defaults: {} }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
32
|
+
|
|
33
|
+
function deepMerge(target, source) {
|
|
34
|
+
for (const key of Object.keys(source)) {
|
|
35
|
+
if (!Object.hasOwn(target, key)) {
|
|
36
|
+
target[key] = JSON.parse(JSON.stringify(source[key]));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const tv = target[key];
|
|
40
|
+
const sv = source[key];
|
|
41
|
+
if (isObject(tv) && isObject(sv)) {
|
|
42
|
+
deepMerge(tv, sv);
|
|
43
|
+
} else {
|
|
44
|
+
target[key] = sv;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return target;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readConfigFile() {
|
|
51
|
+
try {
|
|
52
|
+
const filePath = join(homedir(), '.compend', 'config.json');
|
|
53
|
+
if (!existsSync(filePath)) return {};
|
|
54
|
+
const raw = readFileSync(filePath, 'utf-8').trim();
|
|
55
|
+
if (!raw) return {};
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.warn('Compend: config.json parse error — using defaults:', e.message);
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveTilde(p) {
|
|
64
|
+
if (typeof p === 'string' && p.startsWith('~')) {
|
|
65
|
+
return join(homedir(), p.slice(1));
|
|
66
|
+
}
|
|
67
|
+
return p;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function applyEnvOverrides(cfg) {
|
|
71
|
+
if (process.env.COMPEND_PORT && process.env.COMPEND_PORT.trim()) {
|
|
72
|
+
cfg.port = parseInt(process.env.COMPEND_PORT.trim(), 10) || cfg.port;
|
|
73
|
+
}
|
|
74
|
+
if (process.env.COMPEND_DB_PATH && process.env.COMPEND_DB_PATH.trim()) {
|
|
75
|
+
cfg.dbPath = resolveTilde(process.env.COMPEND_DB_PATH.trim());
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let _cfg = null;
|
|
80
|
+
|
|
81
|
+
export function getConfig() {
|
|
82
|
+
if (_cfg) return _cfg;
|
|
83
|
+
const defaults = JSON.parse(JSON.stringify(DEFAULTS));
|
|
84
|
+
_cfg = deepMerge(defaults, readConfigFile());
|
|
85
|
+
applyEnvOverrides(_cfg);
|
|
86
|
+
_cfg.dbPath = resolveTilde(_cfg.dbPath);
|
|
87
|
+
return _cfg;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getDbPath() {
|
|
91
|
+
const path = getConfig().dbPath;
|
|
92
|
+
const dir = dirname(path);
|
|
93
|
+
if (!existsSync(dir)) {
|
|
94
|
+
mkdirSync(dir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
return path;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function resolveTypeSchema(type) {
|
|
100
|
+
const schemas = getConfig().schemas || {};
|
|
101
|
+
const projectSchema = schemas.default || {};
|
|
102
|
+
const types = projectSchema.types || {};
|
|
103
|
+
return types[type] || null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getIndexPaths() {
|
|
107
|
+
const paths = [];
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const opencodePaths = [
|
|
111
|
+
join(homedir(), '.config', 'opencode', 'opencode.json'),
|
|
112
|
+
];
|
|
113
|
+
for (const p of opencodePaths) {
|
|
114
|
+
if (existsSync(p)) {
|
|
115
|
+
const raw = readFileSync(p, 'utf-8').trim();
|
|
116
|
+
if (raw) {
|
|
117
|
+
const cfg = JSON.parse(raw);
|
|
118
|
+
if (cfg.skills && Array.isArray(cfg.skills.paths)) {
|
|
119
|
+
for (const sp of cfg.skills.paths) {
|
|
120
|
+
paths.push(resolveTilde(sp));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (cfg.instructions && Array.isArray(cfg.instructions)) {
|
|
124
|
+
for (const ip of cfg.instructions) {
|
|
125
|
+
paths.push(resolveTilde(ip));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.warn('Compend: could not read opencode.json:', e.message);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const compendCfg = readConfigFile();
|
|
137
|
+
if (compendCfg.index && Array.isArray(compendCfg.index.paths)) {
|
|
138
|
+
for (const p of compendCfg.index.paths) {
|
|
139
|
+
paths.push(resolveTilde(p));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.warn('Compend: could not read compend config paths:', e.message);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return paths;
|
|
147
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { searchHybrid, getConcept, listConcepts, getTags, getChanges, getDeps } from '../db.js';
|
|
2
|
+
import { getConfig } from '../config.js';
|
|
3
|
+
|
|
4
|
+
export function json(data, status = 200) {
|
|
5
|
+
return { status, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function err(msg, status = 500) {
|
|
9
|
+
return json({ error: msg }, status);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createApiHandler(db) {
|
|
13
|
+
return function handleApi(path, method, params) {
|
|
14
|
+
if (path === '/api/stats' && method === 'GET') {
|
|
15
|
+
const total = db.prepare('SELECT COUNT(*) as c FROM concepts').get().c;
|
|
16
|
+
const types = db.prepare('SELECT type, COUNT(*) as c FROM concepts GROUP BY type ORDER BY c DESC').all().map(r => ({ type: r.type, count: r.c }));
|
|
17
|
+
return json({ total, types });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (path === '/api/tags' && method === 'GET') {
|
|
21
|
+
const type = params.get('type') || '';
|
|
22
|
+
const results = getTags(type || undefined);
|
|
23
|
+
return json({ tags: results });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (path === '/api/concepts' && method === 'GET') {
|
|
27
|
+
const type = params.get('type') || '';
|
|
28
|
+
const status = params.get('status') || '';
|
|
29
|
+
const search = params.get('search') || '';
|
|
30
|
+
const tagsRaw = params.get('tags') || '';
|
|
31
|
+
const maxLimit = getConfig().dashboard.maxLimit;
|
|
32
|
+
const limit = Math.min(parseInt(params.get('limit') || String(getConfig().dashboard.paginationLimit), 10), maxLimit);
|
|
33
|
+
const offset = Math.max(parseInt(params.get('offset') || '0', 10), 0);
|
|
34
|
+
|
|
35
|
+
if (search.trim()) {
|
|
36
|
+
try {
|
|
37
|
+
const rows = searchHybrid({ query: search, type: type || undefined, limit, alpha: getConfig().search.alpha });
|
|
38
|
+
return json({ concepts: rows, total: rows.length, limit, offset });
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return err('Search error', 400);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = listConcepts({
|
|
45
|
+
type: type || undefined,
|
|
46
|
+
status: status || undefined,
|
|
47
|
+
tags: tagsRaw ? tagsRaw.split(',').filter(Boolean) : undefined,
|
|
48
|
+
limit,
|
|
49
|
+
offset
|
|
50
|
+
});
|
|
51
|
+
return json(result);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const conceptMatch = path.match(/^\/api\/concepts\/(.+)$/);
|
|
55
|
+
if (conceptMatch && method === 'GET') {
|
|
56
|
+
const slug = decodeURIComponent(conceptMatch[1]);
|
|
57
|
+
const concept = getConcept(slug);
|
|
58
|
+
if (!concept) return err('Concept not found', 404);
|
|
59
|
+
return json(concept);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (path === '/api/changes' && method === 'GET') {
|
|
63
|
+
const since = params.get('since') || '0';
|
|
64
|
+
const results = getChanges(parseInt(since, 10) || 0);
|
|
65
|
+
return json({ changes: results });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const depsMatch = path.match(/^\/api\/deps\/(.+)$/);
|
|
69
|
+
if (depsMatch && method === 'GET') {
|
|
70
|
+
const slug = decodeURIComponent(depsMatch[1]);
|
|
71
|
+
const results = getDeps(slug);
|
|
72
|
+
return json(results);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
}
|