Skip to content

Commit 653ac33

Browse files
author
alvinttang
committed
feat: npm distribution, MCP safety guardrails, one-click onboarding prompt
npm/: npx cortex-memory — downloads prebuilt binary from GitHub Releases, supports darwin/linux × arm64/x64, zero dependencies. MCP guardrails (tools.rs): - MAX_INGEST_TEXT_BYTES=100KB, MAX_BATCH_SIZE=100, MAX_SEARCH_LIMIT=100 - MAX_CONTEXT_TOKENS=8000, MAX_TAG_SCAN_PER_TIER=10000 - Clamp compress params (min_messages≥1, max_age_days≥1) - Block self-merge in person_merge QUICK_START_PROMPT.md: copy-paste prompt for Claude Code that installs Cortex, stores demo memories, and demos search/context/facts/beliefs.
1 parent b52bad2 commit 653ac33

6 files changed

Lines changed: 291 additions & 3 deletions

File tree

QUICK_START_PROMPT.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Cortex Quick Start Prompt
2+
3+
Copy the prompt below and paste it into Claude Code to automatically install Cortex and demo its memory features in one shot.
4+
5+
---
6+
7+
```
8+
Install Cortex memory engine and configure it as my MCP server, then demo the key features.
9+
10+
## Step 1: Install
11+
Run: curl -fsSL https://raw.githubusercontent.com/gambletan/cortex/main/install.sh | bash -s -- --ide claude
12+
13+
## Step 2: Verify
14+
Run: cortex-mcp-server info
15+
Run: cortex-mcp-server ~/.cortex/memory.db stats
16+
17+
## Step 3: Demo — store some memories
18+
Use the memory_ingest tool to store these:
19+
- "I'm a software engineer working at Google"
20+
- "I live in Shanghai and speak Chinese and English"
21+
- "I prefer Rust over C++ for systems programming"
22+
- "Met with Sarah from Stripe about payment integration last Tuesday"
23+
24+
## Step 4: Demo — query
25+
- Search for "Sarah" and show what comes back
26+
- Run memory_context to show the AI-ready context summary
27+
- Query facts about "User" to show extracted knowledge
28+
- List beliefs to show Bayesian inference
29+
- Show stats
30+
31+
## Step 5: Summary
32+
Print a summary of what was set up and the capabilities now available.
33+
```

cortex-mcp-server/src/tools.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ use serde_json::{json, Value};
55
use std::sync::Arc;
66
use uuid::Uuid;
77

8+
// ── Safety guardrails ───────────────────────────────────────────────────────
9+
const MAX_INGEST_TEXT_BYTES: usize = 100_000; // 100KB per memory
10+
const MAX_BATCH_SIZE: usize = 100; // memory_ingest_batch
11+
const MAX_SEARCH_LIMIT: usize = 100; // memory_search
12+
const MAX_CONTEXT_TOKENS: usize = 8_000; // memory_context
13+
const MAX_TAG_SCAN_PER_TIER: usize = 10_000; // tag_list_taxonomy
14+
815
/// Return the list of available tools (MCP tool schema format).
916
/// Includes built-in tools and any plugin-registered tools.
1017
pub fn list_tools_with_plugins(cortex: &Arc<Cortex>) -> Value {
@@ -580,6 +587,9 @@ fn get_embedding(args: &Value) -> Option<Vec<f32>> {
580587

581588
fn tool_memory_ingest(cortex: &Arc<Cortex>, args: &Value) -> Result<String, String> {
582589
let text = get_str(args, "text").ok_or("missing 'text'")?;
590+
if text.len() > MAX_INGEST_TEXT_BYTES {
591+
return Err(format!("text too large: {} bytes (max {})", text.len(), MAX_INGEST_TEXT_BYTES));
592+
}
583593
let channel = get_str(args, "channel").ok_or("missing 'channel'")?;
584594
let user_id = get_str(args, "user_id");
585595
let salience = args.get("salience").and_then(|v| v.as_f64()).map(|v| v as f32);
@@ -629,7 +639,7 @@ fn tool_memory_consolidate(cortex: &Arc<Cortex>) -> Result<String, String> {
629639

630640
fn tool_memory_search(cortex: &Arc<Cortex>, args: &Value) -> Result<String, String> {
631641
let query = get_str(args, "query").ok_or("missing 'query'")?;
632-
let limit = get_usize(args, "limit", 10);
642+
let limit = get_usize(args, "limit", 10).min(MAX_SEARCH_LIMIT);
633643
let channel = get_str(args, "channel");
634644
let person_id = get_str(args, "person_id")
635645
.and_then(|s| Uuid::parse_str(s).ok());
@@ -662,7 +672,7 @@ fn tool_memory_search(cortex: &Arc<Cortex>, args: &Value) -> Result<String, Stri
662672
}
663673

664674
fn tool_memory_context(cortex: &Arc<Cortex>, args: &Value) -> Result<String, String> {
665-
let max_tokens = get_usize(args, "max_tokens", 2000);
675+
let max_tokens = get_usize(args, "max_tokens", 2000).min(MAX_CONTEXT_TOKENS);
666676
let channel = get_str(args, "channel");
667677
let person_id = get_str(args, "person_id")
668678
.and_then(|s| Uuid::parse_str(s).ok());
@@ -852,6 +862,8 @@ fn tool_memory_compress(cortex: &Arc<Cortex>, args: &Value) -> Result<String, St
852862
.get("max_age_days")
853863
.and_then(|v| v.as_i64())
854864
.unwrap_or(7);
865+
let min_messages = min_messages.max(1); // prevent compressing everything
866+
let max_age_days = max_age_days.max(1); // prevent compressing fresh memories
855867

856868
let report = cortex
857869
.run_compression(min_messages, max_age_days)
@@ -1001,6 +1013,9 @@ fn tool_memory_ingest_batch(cortex: &Arc<Cortex>, args: &Value) -> Result<String
10011013
let items_arr = args.get("items")
10021014
.and_then(|v| v.as_array())
10031015
.ok_or("missing 'items' array")?;
1016+
if items_arr.len() > MAX_BATCH_SIZE {
1017+
return Err(format!("batch too large: {} items (max {})", items_arr.len(), MAX_BATCH_SIZE));
1018+
}
10041019

10051020
let items: Vec<cortex_core::types::BatchIngestItem> = items_arr.iter().map(|item| {
10061021
cortex_core::types::BatchIngestItem {
@@ -1035,7 +1050,7 @@ fn tool_tag_list_taxonomy(cortex: &Arc<Cortex>) -> Result<String, String> {
10351050
];
10361051

10371052
for tier in &tiers {
1038-
if let Ok(mems) = cortex.storage().list_by_tier(*tier, 100_000) {
1053+
if let Ok(mems) = cortex.storage().list_by_tier(*tier, MAX_TAG_SCAN_PER_TIER) {
10391054
for mem in mems {
10401055
for tag in &mem.tags {
10411056
*tag_counts.entry(tag.clone()).or_insert(0) += 1;
@@ -1103,6 +1118,9 @@ fn tool_person_merge(cortex: &Arc<Cortex>, args: &Value) -> Result<String, Strin
11031118
let source_str = get_str(args, "source_id").ok_or("missing 'source_id'")?;
11041119
let target_id = Uuid::parse_str(target_str).map_err(|e| format!("Invalid target UUID: {}", e))?;
11051120
let source_id = Uuid::parse_str(source_str).map_err(|e| format!("Invalid source UUID: {}", e))?;
1121+
if target_id == source_id {
1122+
return Err("cannot merge a person with themselves".to_string());
1123+
}
11061124

11071125
let merged = cortex.merge_people(target_id, source_id).map_err(|e| e.to_string())?;
11081126

npm/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# cortex-memory
2+
3+
Private local memory engine for AI agents. Zero cloud, sub-ms latency, 3.8 MB binary. Provides persistent, structured memory via the Model Context Protocol (MCP).
4+
5+
## Quick start
6+
7+
```bash
8+
npx cortex-memory
9+
```
10+
11+
## Install globally
12+
13+
```bash
14+
npm install -g cortex-memory
15+
```
16+
17+
Then run anywhere:
18+
19+
```bash
20+
cortex-memory
21+
```
22+
23+
## What is Cortex?
24+
25+
Cortex is a Rust-powered MCP server that gives AI coding agents long-term memory: facts, preferences, relationships, and beliefs -- all stored locally on your machine with no cloud dependency.
26+
27+
## More info
28+
29+
Full documentation and source: [github.com/gambletan/cortex](https://github.com/gambletan/cortex)

npm/bin/install.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
4+
const https = require("https");
5+
const fs = require("fs");
6+
const path = require("path");
7+
const os = require("os");
8+
const { execSync } = require("child_process");
9+
10+
const REPO = "gambletan/cortex";
11+
const BINARY_NAME = "cortex-mcp-server";
12+
const BIN_DIR = path.join(__dirname);
13+
const BINARY_PATH = path.join(BIN_DIR, BINARY_NAME);
14+
15+
function getPlatformSuffix() {
16+
const platform = os.platform();
17+
const arch = os.arch();
18+
19+
const platformMap = {
20+
darwin: "darwin",
21+
linux: "linux",
22+
};
23+
24+
const archMap = {
25+
arm64: "arm64",
26+
x64: "x86_64",
27+
};
28+
29+
const osSuffix = platformMap[platform];
30+
const archSuffix = archMap[arch];
31+
32+
if (!osSuffix) {
33+
throw new Error(
34+
`Unsupported platform: ${platform}. Only darwin and linux are supported.`
35+
);
36+
}
37+
if (!archSuffix) {
38+
throw new Error(
39+
`Unsupported architecture: ${arch}. Only arm64 and x64 are supported.`
40+
);
41+
}
42+
43+
return `${osSuffix}-${archSuffix}`;
44+
}
45+
46+
function httpsGet(url) {
47+
return new Promise((resolve, reject) => {
48+
const request = https.get(url, { headers: { "User-Agent": "cortex-memory-npm" } }, (res) => {
49+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
50+
httpsGet(res.headers.location).then(resolve, reject);
51+
return;
52+
}
53+
if (res.statusCode !== 200) {
54+
reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
55+
return;
56+
}
57+
const chunks = [];
58+
res.on("data", (chunk) => chunks.push(chunk));
59+
res.on("end", () => resolve(Buffer.concat(chunks)));
60+
res.on("error", reject);
61+
});
62+
request.on("error", reject);
63+
request.setTimeout(60000, () => {
64+
request.destroy();
65+
reject(new Error(`Request timed out: ${url}`));
66+
});
67+
});
68+
}
69+
70+
async function fetchLatestRelease() {
71+
const url = `https://api.github.com/repos/${REPO}/releases/latest`;
72+
const data = await httpsGet(url);
73+
return JSON.parse(data.toString());
74+
}
75+
76+
async function install() {
77+
const suffix = getPlatformSuffix();
78+
const assetName = `${BINARY_NAME}-${suffix}.tar.gz`;
79+
80+
console.log(`[cortex-memory] Detected platform: ${suffix}`);
81+
console.log(`[cortex-memory] Fetching latest release from ${REPO}...`);
82+
83+
const release = await fetchLatestRelease();
84+
const asset = release.assets.find((a) => a.name === assetName);
85+
86+
if (!asset) {
87+
// Try lite variant as fallback (linux-arm64 only has lite)
88+
const liteAssetName = `${BINARY_NAME}-lite-${suffix}.tar.gz`;
89+
const liteAsset = release.assets.find((a) => a.name === liteAssetName);
90+
91+
if (!liteAsset) {
92+
const available = release.assets.map((a) => a.name).join(", ");
93+
throw new Error(
94+
`No binary found for ${suffix}.\n` +
95+
`Looked for: ${assetName} or ${liteAssetName}\n` +
96+
`Available: ${available}\n` +
97+
`Release: ${release.tag_name}`
98+
);
99+
}
100+
101+
console.log(`[cortex-memory] Full binary not available for ${suffix}, using lite variant.`);
102+
console.log(`[cortex-memory] Downloading ${liteAssetName} (${release.tag_name})...`);
103+
await downloadAndExtract(liteAsset.browser_download_url);
104+
return;
105+
}
106+
107+
console.log(`[cortex-memory] Downloading ${assetName} (${release.tag_name})...`);
108+
await downloadAndExtract(asset.browser_download_url);
109+
}
110+
111+
async function downloadAndExtract(downloadUrl) {
112+
const tarball = await httpsGet(downloadUrl);
113+
114+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-"));
115+
const tarPath = path.join(tmpDir, "binary.tar.gz");
116+
117+
try {
118+
fs.writeFileSync(tarPath, tarball);
119+
execSync(`tar xzf "${tarPath}" -C "${tmpDir}"`, { stdio: "pipe" });
120+
121+
const extractedBinary = path.join(tmpDir, BINARY_NAME);
122+
if (!fs.existsSync(extractedBinary)) {
123+
throw new Error(
124+
`Binary not found after extraction. Expected: ${BINARY_NAME} in archive.`
125+
);
126+
}
127+
128+
fs.copyFileSync(extractedBinary, BINARY_PATH);
129+
fs.chmodSync(BINARY_PATH, 0o755);
130+
131+
console.log(`[cortex-memory] Installed ${BINARY_NAME} to ${BINARY_PATH}`);
132+
} finally {
133+
fs.rmSync(tmpDir, { recursive: true, force: true });
134+
}
135+
}
136+
137+
install().catch((err) => {
138+
console.error(`[cortex-memory] Installation failed: ${err.message}`);
139+
console.error(
140+
"[cortex-memory] You can manually download the binary from:"
141+
);
142+
console.error(` https://github.com/${REPO}/releases/latest`);
143+
process.exit(1);
144+
});

npm/bin/run.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
4+
const path = require("path");
5+
const fs = require("fs");
6+
const { spawn } = require("child_process");
7+
8+
const BINARY_NAME = "cortex-mcp-server";
9+
const BINARY_PATH = path.join(__dirname, BINARY_NAME);
10+
11+
if (!fs.existsSync(BINARY_PATH)) {
12+
console.error(
13+
`[cortex-memory] Binary not found at ${BINARY_PATH}\n` +
14+
`\n` +
15+
`The postinstall script may have failed. Try reinstalling:\n` +
16+
` npm install -g cortex-memory\n` +
17+
`\n` +
18+
`Or download the binary manually from:\n` +
19+
` https://github.com/gambletan/cortex/releases/latest`
20+
);
21+
process.exit(1);
22+
}
23+
24+
const args = process.argv.slice(2);
25+
26+
const child = spawn(BINARY_PATH, args, {
27+
stdio: "inherit",
28+
});
29+
30+
child.on("error", (err) => {
31+
console.error(`[cortex-memory] Failed to start ${BINARY_NAME}: ${err.message}`);
32+
process.exit(1);
33+
});
34+
35+
child.on("exit", (code, signal) => {
36+
if (signal) {
37+
process.kill(process.pid, signal);
38+
} else {
39+
process.exit(code ?? 1);
40+
}
41+
});

npm/package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "cortex-memory",
3+
"version": "2.0.0",
4+
"description": "Private local memory engine for AI agents — zero cloud, sub-ms latency, 3.8MB",
5+
"bin": {
6+
"cortex-memory": "./bin/run.js"
7+
},
8+
"scripts": {
9+
"postinstall": "node ./bin/install.js"
10+
},
11+
"keywords": ["ai", "memory", "mcp", "llm", "local", "privacy", "rust"],
12+
"author": "gambletan",
13+
"license": "MIT",
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/gambletan/cortex"
17+
},
18+
"os": ["darwin", "linux"],
19+
"cpu": ["x64", "arm64"],
20+
"engines": {
21+
"node": ">=16"
22+
}
23+
}

0 commit comments

Comments
 (0)