@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.
Files changed (103) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/README.md +43 -14
  3. package/dist/cli.js +301 -29
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config-store.d.ts.map +1 -1
  6. package/dist/config-store.js +3 -0
  7. package/dist/config-store.js.map +1 -1
  8. package/dist/config.d.ts +5 -0
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +6 -0
  11. package/dist/config.js.map +1 -1
  12. package/dist/extraction/extractor.d.ts +5 -2
  13. package/dist/extraction/extractor.d.ts.map +1 -1
  14. package/dist/extraction/extractor.js +38 -2
  15. package/dist/extraction/extractor.js.map +1 -1
  16. package/dist/extraction/tier25-ollama.d.ts +8 -0
  17. package/dist/extraction/tier25-ollama.d.ts.map +1 -0
  18. package/dist/extraction/tier25-ollama.js +56 -0
  19. package/dist/extraction/tier25-ollama.js.map +1 -0
  20. package/dist/index.js +164 -11
  21. package/dist/index.js.map +1 -1
  22. package/dist/llm/ollama-client.d.ts +39 -0
  23. package/dist/llm/ollama-client.d.ts.map +1 -0
  24. package/dist/llm/ollama-client.js +172 -0
  25. package/dist/llm/ollama-client.js.map +1 -0
  26. package/dist/orchestration/ranker.d.ts +2 -1
  27. package/dist/orchestration/ranker.d.ts.map +1 -1
  28. package/dist/orchestration/ranker.js +11 -1
  29. package/dist/orchestration/ranker.js.map +1 -1
  30. package/dist/orchestration/scope.d.ts +4 -3
  31. package/dist/orchestration/scope.d.ts.map +1 -1
  32. package/dist/orchestration/scope.js +16 -2
  33. package/dist/orchestration/scope.js.map +1 -1
  34. package/dist/portability/dump.d.ts +19 -0
  35. package/dist/portability/dump.d.ts.map +1 -0
  36. package/dist/portability/dump.js +225 -0
  37. package/dist/portability/dump.js.map +1 -0
  38. package/dist/portability/exporter.d.ts +5 -0
  39. package/dist/portability/exporter.d.ts.map +1 -0
  40. package/dist/portability/exporter.js +116 -0
  41. package/dist/portability/exporter.js.map +1 -0
  42. package/dist/portability/importer.d.ts +12 -0
  43. package/dist/portability/importer.d.ts.map +1 -0
  44. package/dist/portability/importer.js +164 -0
  45. package/dist/portability/importer.js.map +1 -0
  46. package/dist/sharing/cross-project.d.ts +15 -0
  47. package/dist/sharing/cross-project.d.ts.map +1 -0
  48. package/dist/sharing/cross-project.js +35 -0
  49. package/dist/sharing/cross-project.js.map +1 -0
  50. package/dist/storage/neo4j.d.ts +1 -0
  51. package/dist/storage/neo4j.d.ts.map +1 -1
  52. package/dist/storage/neo4j.js +10 -0
  53. package/dist/storage/neo4j.js.map +1 -1
  54. package/dist/storage/sqlite.d.ts +50 -1
  55. package/dist/storage/sqlite.d.ts.map +1 -1
  56. package/dist/storage/sqlite.js +276 -3
  57. package/dist/storage/sqlite.js.map +1 -1
  58. package/dist/templates/claude-instructions.d.ts +1 -1
  59. package/dist/templates/claude-instructions.d.ts.map +1 -1
  60. package/dist/templates/claude-instructions.js +6 -1
  61. package/dist/templates/claude-instructions.js.map +1 -1
  62. package/dist/tools/config.d.ts +2 -0
  63. package/dist/tools/config.d.ts.map +1 -1
  64. package/dist/tools/config.js +12 -1
  65. package/dist/tools/config.js.map +1 -1
  66. package/dist/tools/context.d.ts +2 -1
  67. package/dist/tools/context.d.ts.map +1 -1
  68. package/dist/tools/context.js +13 -4
  69. package/dist/tools/context.js.map +1 -1
  70. package/dist/tools/export.d.ts +5 -0
  71. package/dist/tools/export.d.ts.map +1 -0
  72. package/dist/tools/export.js +24 -0
  73. package/dist/tools/export.js.map +1 -0
  74. package/dist/tools/graph-query.d.ts +2 -1
  75. package/dist/tools/graph-query.d.ts.map +1 -1
  76. package/dist/tools/graph-query.js +5 -1
  77. package/dist/tools/graph-query.js.map +1 -1
  78. package/dist/tools/import.d.ts +7 -0
  79. package/dist/tools/import.d.ts.map +1 -0
  80. package/dist/tools/import.js +25 -0
  81. package/dist/tools/import.js.map +1 -0
  82. package/dist/tools/search.d.ts +2 -1
  83. package/dist/tools/search.d.ts.map +1 -1
  84. package/dist/tools/search.js +16 -3
  85. package/dist/tools/search.js.map +1 -1
  86. package/dist/tools/share.d.ts +4 -0
  87. package/dist/tools/share.d.ts.map +1 -0
  88. package/dist/tools/share.js +51 -0
  89. package/dist/tools/share.js.map +1 -0
  90. package/dist/tools/stats.d.ts +5 -0
  91. package/dist/tools/stats.d.ts.map +1 -0
  92. package/dist/tools/stats.js +30 -0
  93. package/dist/tools/stats.js.map +1 -0
  94. package/dist/tools/store.d.ts.map +1 -1
  95. package/dist/tools/store.js +2 -0
  96. package/dist/tools/store.js.map +1 -1
  97. package/dist/tracking/access-tracker.d.ts +36 -0
  98. package/dist/tracking/access-tracker.d.ts.map +1 -0
  99. package/dist/tracking/access-tracker.js +68 -0
  100. package/dist/tracking/access-tracker.js.map +1 -0
  101. package/dist/types/index.d.ts +161 -4
  102. package/dist/types/index.d.ts.map +1 -1
  103. 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.0] - 2026-03-15
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 3 (LLM) |
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
- | memory_config | Full access | Full access |
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 Pull Docker images, start Qdrant + Neo4j
112
- cortex setup --stop Stop Docker containers
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 │ LLM / Local
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 COMPOSE_DEST = join(CORTEX_DIR, 'docker-compose.yml');
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 Pull Docker images and start Qdrant + Neo4j
33
- cortex setup --stop Stop Docker containers
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
- if (!existsSync(COMPOSE_DEST)) {
85
- const __dirname = dirname(fileURLToPath(import.meta.url));
86
- const sourcePaths = [
87
- join(__dirname, '..', 'docker-compose.yml'),
88
- join(__dirname, '..', '..', 'docker-compose.yml'),
89
- ];
90
- let copied = false;
91
- for (const src of sourcePaths) {
92
- if (existsSync(src)) {
93
- copyFileSync(src, COMPOSE_DEST);
94
- console.log(`Copied docker-compose.yml to ${COMPOSE_DEST}`);
95
- copied = true;
96
- break;
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', COMPOSE_DEST]
105
- : ['docker-compose', '-f', COMPOSE_DEST];
113
+ ? ['docker', 'compose', '-f', composeDest]
114
+ : ['docker-compose', '-f', composeDest];
115
+ const mode = isDev ? 'dev' : 'production';
106
116
  if (stop) {
107
- console.log('Stopping Cortex containers...');
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('Starting Cortex containers...');
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
- const qdrantUrl = process.env.QDRANT_URL ?? 'http://localhost:16333';
116
- const neo4jUri = process.env.NEO4J_URI ?? 'bolt://localhost:17687';
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('\nSetup complete. Run "cortex serve" to start the MCP server.');
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':