@londer/cortex 0.1.0 → 0.2.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 +61 -1
- package/README.md +43 -14
- package/dist/cli.js +301 -29
- package/dist/cli.js.map +1 -1
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +3 -0
- package/dist/config-store.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/extraction/extractor.d.ts +5 -2
- package/dist/extraction/extractor.d.ts.map +1 -1
- package/dist/extraction/extractor.js +38 -2
- package/dist/extraction/extractor.js.map +1 -1
- package/dist/extraction/tier25-ollama.d.ts +8 -0
- package/dist/extraction/tier25-ollama.d.ts.map +1 -0
- package/dist/extraction/tier25-ollama.js +56 -0
- package/dist/extraction/tier25-ollama.js.map +1 -0
- package/dist/index.js +164 -11
- package/dist/index.js.map +1 -1
- package/dist/llm/ollama-client.d.ts +39 -0
- package/dist/llm/ollama-client.d.ts.map +1 -0
- package/dist/llm/ollama-client.js +172 -0
- package/dist/llm/ollama-client.js.map +1 -0
- package/dist/orchestration/ranker.d.ts +2 -1
- package/dist/orchestration/ranker.d.ts.map +1 -1
- package/dist/orchestration/ranker.js +11 -1
- package/dist/orchestration/ranker.js.map +1 -1
- package/dist/orchestration/scope.d.ts +4 -3
- package/dist/orchestration/scope.d.ts.map +1 -1
- package/dist/orchestration/scope.js +16 -2
- package/dist/orchestration/scope.js.map +1 -1
- package/dist/portability/dump.d.ts +19 -0
- package/dist/portability/dump.d.ts.map +1 -0
- package/dist/portability/dump.js +225 -0
- package/dist/portability/dump.js.map +1 -0
- package/dist/portability/exporter.d.ts +5 -0
- package/dist/portability/exporter.d.ts.map +1 -0
- package/dist/portability/exporter.js +116 -0
- package/dist/portability/exporter.js.map +1 -0
- package/dist/portability/importer.d.ts +12 -0
- package/dist/portability/importer.d.ts.map +1 -0
- package/dist/portability/importer.js +164 -0
- package/dist/portability/importer.js.map +1 -0
- package/dist/sharing/cross-project.d.ts +15 -0
- package/dist/sharing/cross-project.d.ts.map +1 -0
- package/dist/sharing/cross-project.js +35 -0
- package/dist/sharing/cross-project.js.map +1 -0
- package/dist/storage/neo4j.d.ts +1 -0
- package/dist/storage/neo4j.d.ts.map +1 -1
- package/dist/storage/neo4j.js +10 -0
- package/dist/storage/neo4j.js.map +1 -1
- package/dist/storage/sqlite.d.ts +50 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +276 -3
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/templates/claude-instructions.d.ts +1 -1
- package/dist/templates/claude-instructions.d.ts.map +1 -1
- package/dist/templates/claude-instructions.js +6 -1
- package/dist/templates/claude-instructions.js.map +1 -1
- package/dist/tools/config.d.ts +2 -0
- package/dist/tools/config.d.ts.map +1 -1
- package/dist/tools/config.js +12 -1
- package/dist/tools/config.js.map +1 -1
- package/dist/tools/context.d.ts +2 -1
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +13 -4
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/export.d.ts +5 -0
- package/dist/tools/export.d.ts.map +1 -0
- package/dist/tools/export.js +24 -0
- package/dist/tools/export.js.map +1 -0
- package/dist/tools/graph-query.d.ts +2 -1
- package/dist/tools/graph-query.d.ts.map +1 -1
- package/dist/tools/graph-query.js +5 -1
- package/dist/tools/graph-query.js.map +1 -1
- package/dist/tools/import.d.ts +7 -0
- package/dist/tools/import.d.ts.map +1 -0
- package/dist/tools/import.js +25 -0
- package/dist/tools/import.js.map +1 -0
- package/dist/tools/search.d.ts +2 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +16 -3
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/share.d.ts +4 -0
- package/dist/tools/share.d.ts.map +1 -0
- package/dist/tools/share.js +51 -0
- package/dist/tools/share.js.map +1 -0
- package/dist/tools/stats.d.ts +5 -0
- package/dist/tools/stats.d.ts.map +1 -0
- package/dist/tools/stats.js +30 -0
- package/dist/tools/stats.js.map +1 -0
- package/dist/tools/store.d.ts.map +1 -1
- package/dist/tools/store.js +2 -0
- package/dist/tools/store.js.map +1 -1
- package/dist/tracking/access-tracker.d.ts +36 -0
- package/dist/tracking/access-tracker.d.ts.map +1 -0
- package/dist/tracking/access-tracker.js +68 -0
- package/dist/tracking/access-tracker.js.map +1 -0
- package/dist/types/index.d.ts +161 -4
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +73 -69
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,38 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.2.
|
|
8
|
+
## [0.2.1] - 2026-03-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Cortex JSON export** — Portable JSON format for sharing knowledge packs
|
|
13
|
+
- Filter by project, memory types, date range, min access count
|
|
14
|
+
- Includes memories, entities with graph connections, and project links
|
|
15
|
+
- `memory_export` MCP tool (limit default 100, truncated flag for large datasets)
|
|
16
|
+
- `cortex export` CLI with `--format json|dump`, `--project`, `--output`, `--types`, `--since`, `--min-access`
|
|
17
|
+
- **Cortex JSON import** — Import memories from portable JSON with dedup
|
|
18
|
+
- ID-based dedup (skip existing) + optional embedding similarity dedup (>0.92 threshold in merge mode)
|
|
19
|
+
- Re-embeds all imported content (doesn't trust imported vectors)
|
|
20
|
+
- Recreates entity graph and project links from import data
|
|
21
|
+
- `memory_import` MCP tool (dry_run defaults to true for safety)
|
|
22
|
+
- `cortex import` CLI with `--input`, `--project` override, `--merge`, `--dry-run`
|
|
23
|
+
- **Full dump export/restore** (CLI only) — Complete database backup
|
|
24
|
+
- Export: SQLite copy + Qdrant snapshot API + Neo4j graph as JSON via Cypher
|
|
25
|
+
- Restore: reverse process with metadata validation
|
|
26
|
+
- `cortex export --format dump --output <dir>` / `cortex import --input <dir>`
|
|
27
|
+
- **Import/export tests** — 18 tests covering roundtrip, dedup, dry run, filters, full dump
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `cortex init` template now documents `memory_stats`, `memory_share`, `memory_export`, `memory_import` tools
|
|
32
|
+
- Branch strategy simplified: removed `dev` branch, PRs go directly to `main`
|
|
33
|
+
- GitHub integration: CLAUDE.md now references GitHub MCP tools instead of `gh` CLI
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Neo4j dump restore skips null entity names instead of erroring
|
|
38
|
+
|
|
39
|
+
## [0.2.0] - 2026-03-16
|
|
9
40
|
|
|
10
41
|
### Added
|
|
11
42
|
|
|
@@ -37,6 +68,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
37
68
|
- **npm package** — Published as `@londer/cortex` with proper build step
|
|
38
69
|
- **Conventional commits** — commitlint + husky for commit message enforcement
|
|
39
70
|
- **Build infrastructure** — TypeScript compilation to `dist/`, declaration maps, source maps
|
|
71
|
+
- **Access tracking & staleness scoring** — Memories now track access count, last access time, and staleness score
|
|
72
|
+
- Staleness formula: base decay from days since last access, with frequency and graph-connectivity discounts
|
|
73
|
+
- Ranker applies staleness penalty: `final_score = score * (1 - staleness * 0.3)`
|
|
74
|
+
- Batch staleness recalculation for all memories
|
|
75
|
+
- `CORTEX_STALENESS_THRESHOLD_DAYS` config (default: 90)
|
|
76
|
+
- **`memory_stats` MCP tool** — Aggregate statistics: totals, by type/project, most/least accessed, staleness, graph stats
|
|
77
|
+
- **`cortex stats` CLI command** — Formatted statistics with live staleness update
|
|
78
|
+
- **Ollama support (Tier 2.5)** — Local LLM extraction via Ollama
|
|
79
|
+
- `OllamaClient` with robust JSON repair (markdown fences, trailing commas, single quotes, unquoted keys)
|
|
80
|
+
- `Tier25Ollama` extractor with explicit JSON prompts for smaller models
|
|
81
|
+
- Fallback chain: Tier 3 (Anthropic) → Tier 2.5 (Ollama) → Tier 2 (NLP) → Tier 1 (Regex)
|
|
82
|
+
- Auto-extraction and consolidation use Ollama when Anthropic unavailable
|
|
83
|
+
- `cortex ollama status` and `cortex ollama pull` CLI commands
|
|
84
|
+
- Runtime-settable: `ollama_enabled`, `ollama_url`, `ollama_model` via `memory_config`
|
|
85
|
+
- Config: `CORTEX_OLLAMA_URL`, `CORTEX_OLLAMA_MODEL`, `CORTEX_OLLAMA_ENABLED`
|
|
86
|
+
- **Cross-project memory sharing** — Share memories across linked projects
|
|
87
|
+
- Memory visibility: `project-only` (default), `cross-project`, `global`
|
|
88
|
+
- `project_links` table for bidirectional project linking
|
|
89
|
+
- `CROSS_PROJECT` scope tier (1.2x boost) for shared memories
|
|
90
|
+
- `memory_share` MCP tool: promote visibility, link/unlink projects, list links
|
|
91
|
+
- `memory_store` accepts optional `visibility` parameter
|
|
92
|
+
- `memory_search` and `memory_context` respect visibility and include shared/global memories
|
|
93
|
+
- Shared entity auto-detection (logs suggestions to stderr, never auto-shares)
|
|
94
|
+
- **`cortex setup --dev`** — Separate dev container management (ports 26333/27687)
|
|
40
95
|
|
|
41
96
|
### Changed
|
|
42
97
|
|
|
@@ -46,6 +101,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
46
101
|
- MCP server version bumped to 0.2.0
|
|
47
102
|
- Package renamed to `@londer/cortex`
|
|
48
103
|
- Added `CONTRIBUTING.md` with branching strategy, commit rules, and merge policy
|
|
104
|
+
- `memory_search`, `memory_context`, `memory_graph_query` now log access for returned results
|
|
105
|
+
- `memory_config` output includes Ollama status
|
|
106
|
+
- `cortex init` template documents Tier 2.5 (Ollama) and updated fallback chain
|
|
107
|
+
- Dev environment fix: `.env.dev` properly configured, `docker:dev` scripts use `--env-file .env.dev`
|
|
108
|
+
- `cortex setup` always copies a fresh compose file and supports `--dev` flag
|
|
49
109
|
|
|
50
110
|
## [0.1.0] - 2026-03-15
|
|
51
111
|
|
package/README.md
CHANGED
|
@@ -65,6 +65,21 @@ cortex config set anthropic_api_key sk-ant-your-key-here
|
|
|
65
65
|
export ANTHROPIC_API_KEY=sk-ant-your-key-here
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
### Ollama (Local LLM)
|
|
69
|
+
|
|
70
|
+
For privacy-first LLM extraction without an API key, install [Ollama](https://ollama.ai):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Check Ollama status
|
|
74
|
+
cortex ollama status
|
|
75
|
+
|
|
76
|
+
# Pull the default model
|
|
77
|
+
cortex ollama pull
|
|
78
|
+
|
|
79
|
+
# Or configure a different model
|
|
80
|
+
cortex config set ollama_model mistral
|
|
81
|
+
```
|
|
82
|
+
|
|
68
83
|
## Extraction Tiers
|
|
69
84
|
|
|
70
85
|
Cortex uses a tiered entity extraction system:
|
|
@@ -73,22 +88,24 @@ Cortex uses a tiered entity extraction system:
|
|
|
73
88
|
|------|--------|---------|---------|------|-------------|
|
|
74
89
|
| 1 | Regex + Heuristics | Basic | < 1ms | Free | Always available |
|
|
75
90
|
| 2 | NLP (compromise.js) | Good | < 50ms | Free | Always available |
|
|
91
|
+
| 2.5 | Ollama (local LLM) | Good+ | 1-5s | Free | Ollama running locally |
|
|
76
92
|
| 3 | LLM (Claude API) | Best | 500ms-2s | ~$0.002/call | API key required |
|
|
77
93
|
|
|
78
|
-
The system automatically selects the best available tier and falls back gracefully.
|
|
94
|
+
The system automatically selects the best available tier (3 → 2.5 → 2 → 1) and falls back gracefully.
|
|
79
95
|
|
|
80
96
|
## Capability Matrix
|
|
81
97
|
|
|
82
|
-
| Feature | No API Key | With API Key |
|
|
83
|
-
|
|
84
|
-
| Entity extraction | Tier 1-2 (regex + NLP) | Tier
|
|
85
|
-
| Relationship detection | Basic verb patterns | Full semantic understanding |
|
|
86
|
-
| Auto-extraction | Local heuristics | LLM-powered analysis |
|
|
87
|
-
| Consolidation: near-identical dedup | Yes (> 0.95 similarity) | Yes |
|
|
88
|
-
| Consolidation: smart merge | Flagged for review | Yes |
|
|
89
|
-
| Consolidation: contradiction resolution | No | Yes |
|
|
90
|
-
| memory_ingest | Tier 1-2 extraction | Tier 3 extraction |
|
|
91
|
-
|
|
|
98
|
+
| Feature | No API Key | With Ollama | With API Key |
|
|
99
|
+
|---------|-----------|-------------|-------------|
|
|
100
|
+
| Entity extraction | Tier 1-2 (regex + NLP) | Tier 2.5 (local LLM) | Tier 3 (Claude) |
|
|
101
|
+
| Relationship detection | Basic verb patterns | LLM-powered | Full semantic understanding |
|
|
102
|
+
| Auto-extraction | Local heuristics | Ollama-powered | LLM-powered analysis |
|
|
103
|
+
| Consolidation: near-identical dedup | Yes (> 0.95 similarity) | Yes | Yes |
|
|
104
|
+
| Consolidation: smart merge | Flagged for review | Yes | Yes |
|
|
105
|
+
| Consolidation: contradiction resolution | No | Yes | Yes |
|
|
106
|
+
| memory_ingest | Tier 1-2 extraction | Tier 2.5 extraction | Tier 3 extraction |
|
|
107
|
+
| Cross-project sharing | Full access | Full access | Full access |
|
|
108
|
+
| Access tracking & stats | Full access | Full access | Full access |
|
|
92
109
|
|
|
93
110
|
## Available Tools
|
|
94
111
|
|
|
@@ -103,18 +120,24 @@ The system automatically selects the best available tier and falls back graceful
|
|
|
103
120
|
| `memory_ingest` | Ingest raw text and extract memories/entities/relationships. |
|
|
104
121
|
| `memory_consolidate` | Merge redundant memories. Supports dry_run preview. |
|
|
105
122
|
| `memory_config` | View/modify runtime configuration (including API key). |
|
|
123
|
+
| `memory_stats` | Aggregate statistics: totals, breakdowns, access patterns, staleness. |
|
|
124
|
+
| `memory_share` | Cross-project sharing: promote visibility, link/unlink projects. |
|
|
106
125
|
|
|
107
126
|
## CLI Reference
|
|
108
127
|
|
|
109
128
|
```
|
|
110
129
|
cortex serve Start the MCP server (stdio transport)
|
|
111
|
-
cortex setup
|
|
112
|
-
cortex setup --
|
|
130
|
+
cortex setup Start production Qdrant + Neo4j (ports 16333/17687)
|
|
131
|
+
cortex setup --dev Start dev Qdrant + Neo4j (ports 26333/27687)
|
|
132
|
+
cortex setup --stop Stop containers
|
|
113
133
|
cortex init Generate Cortex instructions for project CLAUDE.md
|
|
114
134
|
cortex init --global Generate global instructions (~/.claude/CLAUDE.md)
|
|
115
135
|
cortex config get Show current configuration
|
|
116
136
|
cortex config set <key> <value> Set a runtime config value
|
|
117
137
|
cortex consolidate Run manual consolidation
|
|
138
|
+
cortex stats Show memory statistics and staleness info
|
|
139
|
+
cortex ollama status Check Ollama availability and installed models
|
|
140
|
+
cortex ollama pull [model] Pull an Ollama model
|
|
118
141
|
cortex version Show version
|
|
119
142
|
cortex help Show help
|
|
120
143
|
```
|
|
@@ -134,6 +157,10 @@ Key settings:
|
|
|
134
157
|
| `CORTEX_EXTRACTION_TIER` | `auto` | `auto`, `local-only`, or `llm-preferred` |
|
|
135
158
|
| `CORTEX_AUTO_EXTRACT` | `true` | Enable auto-extraction from conversations |
|
|
136
159
|
| `CORTEX_CONSOLIDATION_ENABLED` | `true` | Enable periodic memory consolidation |
|
|
160
|
+
| `CORTEX_STALENESS_THRESHOLD_DAYS` | `90` | Days before a memory is considered stale |
|
|
161
|
+
| `CORTEX_OLLAMA_ENABLED` | `true` | Enable Ollama local LLM |
|
|
162
|
+
| `CORTEX_OLLAMA_URL` | `http://localhost:11434` | Ollama server URL |
|
|
163
|
+
| `CORTEX_OLLAMA_MODEL` | `llama3.2` | Ollama model name |
|
|
137
164
|
|
|
138
165
|
Runtime overrides persist to `~/.cortex/runtime-config.json`.
|
|
139
166
|
|
|
@@ -164,7 +191,7 @@ Claude Code
|
|
|
164
191
|
│ (TypeScript) │
|
|
165
192
|
├──────────────────────────────────────┤
|
|
166
193
|
│ Extraction │ Consolidation │
|
|
167
|
-
│ Tier 1-3
|
|
194
|
+
│ Tier 1-3+ │ LLM / Ollama / Local│
|
|
168
195
|
│ Auto-extract │ Cluster + Merge │
|
|
169
196
|
├──────────────────────────────────────┤
|
|
170
197
|
│ Orchestration Layer │
|
|
@@ -176,6 +203,8 @@ Claude Code
|
|
|
176
203
|
▲ ▲
|
|
177
204
|
HuggingFace Anthropic API
|
|
178
205
|
Transformers.js (optional)
|
|
206
|
+
Ollama
|
|
207
|
+
(optional)
|
|
179
208
|
```
|
|
180
209
|
|
|
181
210
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,8 @@ import { join, dirname } from 'node:path';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
const CORTEX_DIR = join(homedir(), '.cortex');
|
|
8
|
-
const
|
|
8
|
+
const COMPOSE_PROD_DEST = join(CORTEX_DIR, 'docker-compose.yml');
|
|
9
|
+
const COMPOSE_DEV_DEST = join(CORTEX_DIR, 'docker-compose.dev.yml');
|
|
9
10
|
function getVersion() {
|
|
10
11
|
try {
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -29,13 +30,20 @@ cortex — AI memory system with MCP integration
|
|
|
29
30
|
|
|
30
31
|
Usage:
|
|
31
32
|
cortex serve Start the MCP server (stdio transport)
|
|
32
|
-
cortex setup
|
|
33
|
-
cortex setup --
|
|
33
|
+
cortex setup Start production Qdrant + Neo4j (ports 16333/17687)
|
|
34
|
+
cortex setup --dev Start dev Qdrant + Neo4j (ports 26333/27687)
|
|
35
|
+
cortex setup --stop Stop production containers
|
|
36
|
+
cortex setup --dev --stop Stop dev containers
|
|
34
37
|
cortex init Generate Cortex instructions for project CLAUDE.md
|
|
35
38
|
cortex init --global Generate global Cortex instructions (~/.claude/CLAUDE.md)
|
|
36
39
|
cortex config get Show current configuration
|
|
37
40
|
cortex config set <key> <value> Set a runtime config value
|
|
41
|
+
cortex export [options] Export memories (see cortex export --help)
|
|
42
|
+
cortex import [options] Import memories (see cortex import --help)
|
|
38
43
|
cortex consolidate Run manual consolidation
|
|
44
|
+
cortex stats Show memory statistics and staleness info
|
|
45
|
+
cortex ollama status Check Ollama availability and installed models
|
|
46
|
+
cortex ollama pull [model] Pull the configured (or specified) Ollama model
|
|
39
47
|
cortex version Show version
|
|
40
48
|
cortex help Show this help message
|
|
41
49
|
|
|
@@ -70,7 +78,7 @@ function checkDockerCompose() {
|
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
|
-
async function setup(stop) {
|
|
81
|
+
async function setup(stop, isDev) {
|
|
74
82
|
if (!checkDocker()) {
|
|
75
83
|
console.error('Error: Docker is not installed or not in PATH');
|
|
76
84
|
process.exit(1);
|
|
@@ -81,39 +89,44 @@ async function setup(stop) {
|
|
|
81
89
|
process.exit(1);
|
|
82
90
|
}
|
|
83
91
|
mkdirSync(CORTEX_DIR, { recursive: true });
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!copied) {
|
|
100
|
-
console.error('Warning: Could not find docker-compose.yml to copy');
|
|
92
|
+
// Determine which compose file to use
|
|
93
|
+
const composeSourceName = isDev ? 'docker-compose.dev.yml' : 'docker-compose.yml';
|
|
94
|
+
const composeDest = isDev ? COMPOSE_DEV_DEST : COMPOSE_PROD_DEST;
|
|
95
|
+
// Always copy fresh — ensures the compose file matches the installed version
|
|
96
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
97
|
+
const sourcePaths = [
|
|
98
|
+
join(__dirname, '..', composeSourceName),
|
|
99
|
+
join(__dirname, '..', '..', composeSourceName),
|
|
100
|
+
];
|
|
101
|
+
let copied = false;
|
|
102
|
+
for (const src of sourcePaths) {
|
|
103
|
+
if (existsSync(src)) {
|
|
104
|
+
copyFileSync(src, composeDest);
|
|
105
|
+
copied = true;
|
|
106
|
+
break;
|
|
101
107
|
}
|
|
102
108
|
}
|
|
109
|
+
if (!copied) {
|
|
110
|
+
console.error(`Warning: Could not find ${composeSourceName} to copy`);
|
|
111
|
+
}
|
|
103
112
|
const composeArgs = composeCmd === 'compose'
|
|
104
|
-
? ['docker', 'compose', '-f',
|
|
105
|
-
: ['docker-compose', '-f',
|
|
113
|
+
? ['docker', 'compose', '-f', composeDest]
|
|
114
|
+
: ['docker-compose', '-f', composeDest];
|
|
115
|
+
const mode = isDev ? 'dev' : 'production';
|
|
106
116
|
if (stop) {
|
|
107
|
-
console.log(
|
|
117
|
+
console.log(`Stopping Cortex ${mode} containers...`);
|
|
108
118
|
execSync([...composeArgs, 'down'].join(' '), { stdio: 'inherit' });
|
|
109
119
|
console.log('Containers stopped.');
|
|
110
120
|
return;
|
|
111
121
|
}
|
|
112
|
-
console.log(
|
|
122
|
+
console.log(`Starting Cortex ${mode} containers...`);
|
|
113
123
|
execSync([...composeArgs, 'up', '-d'].join(' '), { stdio: 'inherit' });
|
|
114
124
|
console.log('Waiting for services to be ready...');
|
|
115
|
-
|
|
116
|
-
const
|
|
125
|
+
// Use the correct ports for the chosen environment
|
|
126
|
+
const defaultQdrantPort = isDev ? '26333' : '16333';
|
|
127
|
+
const defaultNeo4jPort = isDev ? '27687' : '17687';
|
|
128
|
+
const qdrantUrl = process.env.QDRANT_URL ?? `http://localhost:${defaultQdrantPort}`;
|
|
129
|
+
const neo4jUri = process.env.NEO4J_URI ?? `bolt://localhost:${defaultNeo4jPort}`;
|
|
117
130
|
let qdrantReady = false;
|
|
118
131
|
for (let i = 0; i < 30; i++) {
|
|
119
132
|
try {
|
|
@@ -139,7 +152,7 @@ async function setup(stop) {
|
|
|
139
152
|
}
|
|
140
153
|
catch { /* not ready */ }
|
|
141
154
|
console.log(`Neo4j: ${neo4jReady ? 'ready' : 'not responding (check manually)'}`);
|
|
142
|
-
console.log(
|
|
155
|
+
console.log(`\n${mode.charAt(0).toUpperCase() + mode.slice(1)} setup complete. Run "cortex serve" to start the MCP server.`);
|
|
143
156
|
}
|
|
144
157
|
async function configAction(args) {
|
|
145
158
|
const subAction = args[0];
|
|
@@ -235,6 +248,253 @@ async function consolidate() {
|
|
|
235
248
|
console.log('Use the memory_consolidate MCP tool for consolidation.');
|
|
236
249
|
console.log('Example: cortex serve, then call memory_consolidate through Claude Code.');
|
|
237
250
|
}
|
|
251
|
+
async function ollamaCommand(args) {
|
|
252
|
+
const subCommand = args[0];
|
|
253
|
+
const { loadConfig } = await import('./config.js');
|
|
254
|
+
const { OllamaClient } = await import('./llm/ollama-client.js');
|
|
255
|
+
const config = loadConfig();
|
|
256
|
+
const client = new OllamaClient(config.ollamaUrl, config.ollamaModel, config.ollamaEnabled);
|
|
257
|
+
if (subCommand === 'status') {
|
|
258
|
+
console.log('Ollama Status');
|
|
259
|
+
console.log('─'.repeat(40));
|
|
260
|
+
console.log(` Enabled: ${config.ollamaEnabled}`);
|
|
261
|
+
console.log(` URL: ${config.ollamaUrl}`);
|
|
262
|
+
console.log(` Model: ${config.ollamaModel}`);
|
|
263
|
+
const available = await client.checkAvailability();
|
|
264
|
+
console.log(` Available: ${available}`);
|
|
265
|
+
const models = await client.listModels();
|
|
266
|
+
if (models.length > 0) {
|
|
267
|
+
console.log(`\nInstalled models:`);
|
|
268
|
+
for (const model of models) {
|
|
269
|
+
const marker = model === config.ollamaModel || model.startsWith(config.ollamaModel + ':') ? ' ←' : '';
|
|
270
|
+
console.log(` - ${model}${marker}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.log('\n No models found (is Ollama running?)');
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (subCommand === 'pull') {
|
|
279
|
+
const model = args[1] ?? config.ollamaModel;
|
|
280
|
+
console.log(`Pulling model: ${model}...`);
|
|
281
|
+
const success = await client.pullModel(model);
|
|
282
|
+
if (success) {
|
|
283
|
+
console.log(`Model ${model} pulled successfully.`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
console.error(`Failed to pull model ${model}. Is Ollama running?`);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
console.error('Usage: cortex ollama <status|pull> [model]');
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
async function statsCommand() {
|
|
295
|
+
const { loadConfig } = await import('./config.js');
|
|
296
|
+
const { SQLiteStore } = await import('./storage/sqlite.js');
|
|
297
|
+
const { AccessTracker } = await import('./tracking/access-tracker.js');
|
|
298
|
+
const config = loadConfig();
|
|
299
|
+
const sqlite = new SQLiteStore(config.sqlitePath);
|
|
300
|
+
sqlite.init();
|
|
301
|
+
// Update staleness scores first
|
|
302
|
+
const tracker = new AccessTracker(sqlite, config.stalenessThresholdDays);
|
|
303
|
+
const { updated, averageStaleness } = tracker.updateAllStaleness();
|
|
304
|
+
const stats = sqlite.getStats();
|
|
305
|
+
const byType = sqlite.getMemoryCountByType();
|
|
306
|
+
const byProject = sqlite.getMemoryCountByProject();
|
|
307
|
+
const { mostAccessed, leastAccessed } = sqlite.getMemoryAccessStats(5);
|
|
308
|
+
const staleCount = sqlite.getStaleMemoryCount(0.7);
|
|
309
|
+
console.log('Cortex Memory Statistics');
|
|
310
|
+
console.log('─'.repeat(40));
|
|
311
|
+
console.log(` Total memories: ${stats.totalMemories}`);
|
|
312
|
+
console.log(` Total entities: ${stats.totalEntities}`);
|
|
313
|
+
console.log(` Average staleness: ${(averageStaleness * 100).toFixed(1)}%`);
|
|
314
|
+
console.log(` Stale memories: ${staleCount} (score >= 0.7)`);
|
|
315
|
+
console.log(` Staleness updated: ${updated} memories`);
|
|
316
|
+
if (Object.keys(byType).length > 0) {
|
|
317
|
+
console.log('\nBy Type:');
|
|
318
|
+
for (const [type, count] of Object.entries(byType)) {
|
|
319
|
+
console.log(` ${type}: ${count}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (Object.keys(byProject).length > 0) {
|
|
323
|
+
console.log('\nBy Project:');
|
|
324
|
+
for (const [project, count] of Object.entries(byProject)) {
|
|
325
|
+
console.log(` ${project}: ${count}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (mostAccessed.length > 0) {
|
|
329
|
+
console.log('\nMost Accessed:');
|
|
330
|
+
for (const mem of mostAccessed) {
|
|
331
|
+
const preview = mem.content.slice(0, 60) + (mem.content.length > 60 ? '...' : '');
|
|
332
|
+
console.log(` [${mem.access_count}x] ${preview}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (leastAccessed.length > 0) {
|
|
336
|
+
console.log('\nLeast Accessed:');
|
|
337
|
+
for (const mem of leastAccessed) {
|
|
338
|
+
const preview = mem.content.slice(0, 60) + (mem.content.length > 60 ? '...' : '');
|
|
339
|
+
console.log(` [${mem.access_count}x] ${preview}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
sqlite.close();
|
|
343
|
+
}
|
|
344
|
+
async function exportCommand(args) {
|
|
345
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
346
|
+
console.log(`
|
|
347
|
+
cortex export — Export memories
|
|
348
|
+
|
|
349
|
+
Usage:
|
|
350
|
+
cortex export [options]
|
|
351
|
+
|
|
352
|
+
Options:
|
|
353
|
+
--format json|dump Export format (default: json)
|
|
354
|
+
--project <name> Filter by project
|
|
355
|
+
--output <path> Output file/directory (default: stdout for json)
|
|
356
|
+
--types <types> Comma-separated: fact,decision,insight,...
|
|
357
|
+
--since <date> Only memories after this ISO date
|
|
358
|
+
--min-access <n> Only memories accessed at least n times
|
|
359
|
+
`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const format = getArgValue(args, '--format') ?? 'json';
|
|
363
|
+
if (format === 'dump') {
|
|
364
|
+
const output = getArgValue(args, '--output');
|
|
365
|
+
if (!output) {
|
|
366
|
+
console.error('Error: --output is required for dump format');
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
const { exportDump } = await import('./portability/dump.js');
|
|
370
|
+
const result = await exportDump(output);
|
|
371
|
+
console.log(result.message);
|
|
372
|
+
if (!result.success)
|
|
373
|
+
process.exit(1);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
// JSON export
|
|
377
|
+
const { loadConfig } = await import('./config.js');
|
|
378
|
+
const { SQLiteStore } = await import('./storage/sqlite.js');
|
|
379
|
+
const { Neo4jStore } = await import('./storage/neo4j.js');
|
|
380
|
+
const { exportMemories } = await import('./portability/exporter.js');
|
|
381
|
+
const { MemoryType } = await import('./types/index.js').then(() => ({ MemoryType: null }));
|
|
382
|
+
const config = loadConfig();
|
|
383
|
+
const sqlite = new SQLiteStore(config.sqlitePath);
|
|
384
|
+
sqlite.init();
|
|
385
|
+
const neo4j = new Neo4jStore(config);
|
|
386
|
+
try {
|
|
387
|
+
await neo4j.init();
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
console.error('Warning: Neo4j unavailable — exporting without graph data');
|
|
391
|
+
}
|
|
392
|
+
const project = getArgValue(args, '--project');
|
|
393
|
+
const typesStr = getArgValue(args, '--types');
|
|
394
|
+
const types = typesStr ? typesStr.split(',').map(t => t.trim()) : undefined;
|
|
395
|
+
const since = getArgValue(args, '--since');
|
|
396
|
+
const minAccessStr = getArgValue(args, '--min-access');
|
|
397
|
+
const minAccess = minAccessStr ? parseInt(minAccessStr, 10) : undefined;
|
|
398
|
+
const output = getArgValue(args, '--output');
|
|
399
|
+
const exportData = await exportMemories(sqlite, neo4j, {
|
|
400
|
+
project,
|
|
401
|
+
types: types,
|
|
402
|
+
since,
|
|
403
|
+
min_access: minAccess,
|
|
404
|
+
});
|
|
405
|
+
const json = JSON.stringify(exportData, null, 2);
|
|
406
|
+
if (output) {
|
|
407
|
+
const { writeFileSync } = await import('node:fs');
|
|
408
|
+
writeFileSync(output, json, 'utf-8');
|
|
409
|
+
console.log(`Exported ${exportData.stats.memories} memories to ${output}`);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
console.log(json);
|
|
413
|
+
}
|
|
414
|
+
sqlite.close();
|
|
415
|
+
await neo4j.close();
|
|
416
|
+
}
|
|
417
|
+
async function importCommand(args) {
|
|
418
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
419
|
+
console.log(`
|
|
420
|
+
cortex import — Import memories
|
|
421
|
+
|
|
422
|
+
Usage:
|
|
423
|
+
cortex import [options]
|
|
424
|
+
|
|
425
|
+
Options:
|
|
426
|
+
--input <path> Input file (json) or directory (dump)
|
|
427
|
+
--project <name> Override project name on import
|
|
428
|
+
--merge Merge with existing (dedup by embedding similarity)
|
|
429
|
+
--dry-run Preview what would be imported (default)
|
|
430
|
+
`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const input = getArgValue(args, '--input');
|
|
434
|
+
if (!input) {
|
|
435
|
+
console.error('Error: --input is required');
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
const { existsSync, statSync, readFileSync } = await import('node:fs');
|
|
439
|
+
if (!existsSync(input)) {
|
|
440
|
+
console.error(`Error: ${input} does not exist`);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
const stat = statSync(input);
|
|
444
|
+
// Directory = full dump restore
|
|
445
|
+
if (stat.isDirectory()) {
|
|
446
|
+
const { restoreDump } = await import('./portability/dump.js');
|
|
447
|
+
const result = await restoreDump(input);
|
|
448
|
+
console.log(result.message);
|
|
449
|
+
if (!result.success)
|
|
450
|
+
process.exit(1);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
// File = JSON import
|
|
454
|
+
const { loadConfig } = await import('./config.js');
|
|
455
|
+
const { SQLiteStore } = await import('./storage/sqlite.js');
|
|
456
|
+
const { QdrantStore } = await import('./storage/qdrant.js');
|
|
457
|
+
const { Neo4jStore } = await import('./storage/neo4j.js');
|
|
458
|
+
const { getEmbedder } = await import('./embedding/embedder.js');
|
|
459
|
+
const { validateExportData, importMemories } = await import('./portability/importer.js');
|
|
460
|
+
const config = loadConfig();
|
|
461
|
+
const sqlite = new SQLiteStore(config.sqlitePath);
|
|
462
|
+
sqlite.init();
|
|
463
|
+
const qdrant = new QdrantStore(config);
|
|
464
|
+
await qdrant.init();
|
|
465
|
+
const neo4j = new Neo4jStore(config);
|
|
466
|
+
await neo4j.init();
|
|
467
|
+
const embedder = await getEmbedder();
|
|
468
|
+
const json = readFileSync(input, 'utf-8');
|
|
469
|
+
const parsed = JSON.parse(json);
|
|
470
|
+
const exportData = validateExportData(parsed);
|
|
471
|
+
const project = getArgValue(args, '--project');
|
|
472
|
+
const merge = args.includes('--merge');
|
|
473
|
+
const dryRun = !args.includes('--no-dry-run') && (args.includes('--dry-run') || !merge);
|
|
474
|
+
if (dryRun) {
|
|
475
|
+
console.log('Dry run mode — no changes will be made.\n');
|
|
476
|
+
}
|
|
477
|
+
const result = await importMemories(exportData, sqlite, qdrant, neo4j, embedder, {
|
|
478
|
+
merge,
|
|
479
|
+
dryRun,
|
|
480
|
+
projectOverride: project ?? undefined,
|
|
481
|
+
});
|
|
482
|
+
console.log(`Import results:`);
|
|
483
|
+
console.log(` Imported: ${result.imported}`);
|
|
484
|
+
console.log(` Skipped: ${result.skipped}`);
|
|
485
|
+
console.log(` Errors: ${result.errors}`);
|
|
486
|
+
if (dryRun && result.imported > 0) {
|
|
487
|
+
console.log(`\nRun with --no-dry-run to apply changes.`);
|
|
488
|
+
}
|
|
489
|
+
sqlite.close();
|
|
490
|
+
await neo4j.close();
|
|
491
|
+
}
|
|
492
|
+
function getArgValue(args, flag) {
|
|
493
|
+
const idx = args.indexOf(flag);
|
|
494
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
495
|
+
return undefined;
|
|
496
|
+
return args[idx + 1];
|
|
497
|
+
}
|
|
238
498
|
async function main() {
|
|
239
499
|
const args = process.argv.slice(2);
|
|
240
500
|
const command = args[0];
|
|
@@ -243,17 +503,29 @@ async function main() {
|
|
|
243
503
|
await serve();
|
|
244
504
|
break;
|
|
245
505
|
case 'setup':
|
|
246
|
-
await setup(args.includes('--stop'));
|
|
506
|
+
await setup(args.includes('--stop'), args.includes('--dev'));
|
|
247
507
|
break;
|
|
248
508
|
case 'init':
|
|
249
509
|
await initCommand(args.includes('--global'));
|
|
250
510
|
break;
|
|
511
|
+
case 'export':
|
|
512
|
+
await exportCommand(args.slice(1));
|
|
513
|
+
break;
|
|
514
|
+
case 'import':
|
|
515
|
+
await importCommand(args.slice(1));
|
|
516
|
+
break;
|
|
251
517
|
case 'config':
|
|
252
518
|
await configAction(args.slice(1));
|
|
253
519
|
break;
|
|
254
520
|
case 'consolidate':
|
|
255
521
|
await consolidate();
|
|
256
522
|
break;
|
|
523
|
+
case 'stats':
|
|
524
|
+
await statsCommand();
|
|
525
|
+
break;
|
|
526
|
+
case 'ollama':
|
|
527
|
+
await ollamaCommand(args.slice(1));
|
|
528
|
+
break;
|
|
257
529
|
case 'version':
|
|
258
530
|
case '--version':
|
|
259
531
|
case '-v':
|