Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ logs/

# Dev-only folders
frontend/
scripts/
!scripts/
!scripts/benchmark_v2_ingest.py
tests/
!tests/
!tests/**/*.py
Expand Down Expand Up @@ -96,3 +93,8 @@ src/api/routes/search.py
# Misc
error.json
MIGRATION_PLAN.md
node_modules/
repos/
exports/
tmp/
.xmem/
42 changes: 26 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,36 +309,46 @@ The industry standard benchmark for long-term conversational memory. Tests wheth

## Quickstart

### 1. Start the XMem Server
### Local XMem

```bash
git clone https://github.com/XortexLabs/xmem.git
npx create-xmem@latest
cd xmem
npm run dev
```

# Install (requires Python 3.11+)
pip install -e .
This creates a local XMem workspace, installs the backend, starts local storage, builds the Chrome extension, and launches the API at `http://localhost:8000`.

# Configure environment
cp .env.example .env # Add your API keys
After setup, load the extension from:

# Start
uvicorn src.api.app:create_app --factory --host 0.0.0.0 --port 8000
```text
repos/xmem-extension/dist
```

### 2. Install the Chrome Extension
Chrome path: `chrome://extensions` -> enable Developer mode -> Load unpacked.

### Local Commands

```bash
git clone https://github.com/XortexAI/xmem-extension.git
npm install && npm run build
npm run setup
npm run start
npm run verify
npm run doctor
```

Load `dist/` in Chrome via `chrome://extensions` → "Load unpacked". Point it to your server URL.
If `.env` contains a real cloud LLM key, XMem uses that provider and keeps embeddings local with FastEmbed. If no cloud key is configured, XMem falls back to local Ollama and pulls the required local models during setup.

### Context Portability

https://github.com/user-attachments/assets/605985c3-ef27-4096-a28c-b0b4cc6f8b8d
```bash
npm run context:export
npm run context:import -- --file ./exports/xmem-context.json
npm run context:sync -- --file ./exports/xmem-context.json --server https://api.xmem.in --api-key <key>
```

`context:export` writes a local context bundle that can be imported later or synced to an XMem server.

### 3. Index a Repository (Optional)
### Index a Repository

```bash
python -m src.scanner.runner \
Expand All @@ -352,8 +362,8 @@ python -m src.scanner.runner \
> For a fully local setup with no cloud dependencies:
> ```ini
> FALLBACK_ORDER='["ollama"]'
> EMBEDDING_PROVIDER=fastembed
> VECTOR_STORE_PROVIDER=chroma
> EMBEDDING_PROVIDER=ollama
> VECTOR_STORE_PROVIDER=pgvector
> ```
> Then install local extras: `pip install -e ".[local]"`

Expand Down
66 changes: 66 additions & 0 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: xmem

services:
postgres:
image: pgvector/pgvector:pg16
container_name: xmem-postgres
environment:
POSTGRES_DB: xmem
POSTGRES_USER: xmem
POSTGRES_PASSWORD: xmem
ports:
- "15432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U xmem -d xmem"]
interval: 10s
timeout: 5s
retries: 10

mongo:
image: mongo:7
container_name: xmem-mongo
ports:
- "27018:27017"
volumes:
- mongo_data:/data/db
healthcheck:
test:
[
"CMD-SHELL",
"mongosh --quiet --eval \"db.adminCommand('ping').ok\" || exit 1"
]
interval: 10s
timeout: 5s
retries: 12

neo4j:
image: neo4j:5-community
container_name: xmem-neo4j
environment:
NEO4J_AUTH: neo4j/local-password
NEO4J_server_memory_heap_initial__size: 512m
NEO4J_server_memory_heap_max__size: 2G
NEO4J_server_memory_pagecache_size: 1G
ports:
- "17474:7474"
- "17687:7687"
volumes:
- neo4j_data:/data
- neo4j_logs:/logs
healthcheck:
test:
[
"CMD-SHELL",
"cypher-shell -u neo4j -p local-password 'RETURN 1' || exit 1"
]
interval: 10s
timeout: 5s
retries: 12

volumes:
postgres_data:
mongo_data:
neo4j_data:
neo4j_logs:
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "xmem",
"version": "0.1.0",
"private": true,
"description": "XMem local-first memory layer.",
"scripts": {
"setup": "node ./scripts/xmem.js setup",
"dev": "node ./scripts/xmem.js dev",
"start": "node ./scripts/xmem.js start",
"verify": "node ./scripts/xmem.js verify",
"doctor": "node ./scripts/xmem.js doctor",
"context:export": "node ./scripts/xmem.js context:export",
"context:import": "node ./scripts/xmem.js context:import",
"context:sync": "node ./scripts/xmem.js context:sync",
"check:npm": "node ./scripts/check-npm-publish.js",
"pack:create": "npm pack ./packages/create-xmem --dry-run",
"publish:create": "npm publish ./packages/create-xmem --access public"
},
"engines": {
"node": ">=20"
}
}
126 changes: 126 additions & 0 deletions packages/create-xmem/bin/create-xmem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env node

const fs = require("node:fs");
const path = require("node:path");
const { spawnSync } = require("node:child_process");

const DEFAULT_REPO = "https://github.com/XortexAI/XMem.git";
const DEFAULT_BRANCH = "main";

function usage(exitCode = 0) {
console.log(`Create a local XMem workspace

Usage:
npx create-xmem@latest
npx create-xmem@latest my-xmem

Options:
--repo <url> XMem git repository URL
--branch <name> XMem branch to use
--help Show this message

After creation:
cd xmem
npm run dev
`);
process.exit(exitCode);
}

function parseArgs(argv) {
const options = {
target: "xmem",
repo: process.env.XMEM_REPO || DEFAULT_REPO,
branch: process.env.XMEM_BRANCH || DEFAULT_BRANCH,
};

for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];

if (arg === "--help" || arg === "-h") {
usage(0);
}

if (arg === "--repo") {
options.repo = argv[index + 1];
index += 1;
continue;
}

if (arg === "--branch") {
options.branch = argv[index + 1];
index += 1;
continue;
}

if (arg.startsWith("-")) {
console.error(`[create-xmem] Unknown option: ${arg}`);
usage(1);
}

options.target = arg;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The positional argument handling is greedy and will overwrite options.target with every non-option argument provided. This could lead to unexpected behavior if extra arguments are passed. Consider ensuring the target is only set once if it's not the default value.

    if (options.target === "xmem") {
      options.target = arg;
    }

}

if (!options.repo || !options.branch) {
console.error("[create-xmem] --repo and --branch require values.");
usage(1);
}

return options;
}

function runGit(args, cwd) {
const result = spawnSync("git", args, {
cwd,
stdio: "inherit",
shell: false,
});

if (result.error) {
console.error(`[create-xmem] Git is required: ${result.error.message}`);
console.error("[create-xmem] Install Git, reopen your terminal, and run the command again.");
process.exit(1);
}

if (result.status !== 0) {
process.exit(result.status || 1);
}
}

function assertCleanTarget(targetPath) {
if (!fs.existsSync(targetPath)) {
return;
}

const entries = fs.readdirSync(targetPath);
if (entries.length > 0) {
console.error(`[create-xmem] Target folder is not empty: ${targetPath}`);
console.error("[create-xmem] Choose a new folder name or empty the existing folder.");
process.exit(1);
}
}

function removeGitMetadata(targetPath) {
fs.rmSync(path.join(targetPath, ".git"), {
recursive: true,
force: true,
});
}

const options = parseArgs(process.argv.slice(2));
const targetPath = path.resolve(process.cwd(), options.target);

assertCleanTarget(targetPath);

console.log(`[create-xmem] Creating XMem workspace in ${targetPath}`);
runGit(["clone", "--depth", "1", "--branch", options.branch, options.repo, targetPath], process.cwd());
removeGitMetadata(targetPath);

console.log("");
console.log("[create-xmem] Created local XMem workspace.");
console.log("");
console.log("Next:");
console.log(` cd ${path.relative(process.cwd(), targetPath) || "."}`);
console.log(" npm run dev");
console.log("");
console.log("Chrome extension after setup:");
console.log(" Load unpacked: repos/xmem-extension/dist");
21 changes: 21 additions & 0 deletions packages/create-xmem/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "create-xmem",
"version": "0.1.1",
"description": "Create a local XMem workspace.",
"bin": {
"create-xmem": "bin/create-xmem.js"
},
"files": [
"bin"
],
"keywords": [
"xmem",
"xortexai",
"memory",
"local-ai"
],
"license": "MIT",
"engines": {
"node": ">=20"
}
}
68 changes: 68 additions & 0 deletions scripts/check-npm-publish.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env node

const { spawnSync } = require("node:child_process");

function runNpm(args) {
return spawnSync("npm", args, {
encoding: "utf8",
shell: process.platform === "win32",
});
}

function printOutput(result) {
if (result.error) {
console.error(result.error.message);
}
if ((result.stdout || "").trim()) {
console.log(result.stdout.trim());
}
if ((result.stderr || "").trim()) {
console.error(result.stderr.trim());
}
}

const whoami = runNpm(["whoami"]);
if (whoami.status !== 0) {
console.error("[xmem] npm is not logged in. Run: npm login");
printOutput(whoami);
process.exit(1);
}

console.log(`[xmem] npm user: ${whoami.stdout.trim()}`);

const profile = runNpm(["profile", "get", "--json"]);
if (profile.status !== 0) {
const combinedOutput = `${profile.stdout}\n${profile.stderr}`;
if (combinedOutput.includes("E403")) {
console.log("[xmem] npm profile is not readable with this token.");
console.log("[xmem] That is okay for granular publish tokens; continuing package checks.");
} else {
console.error("[xmem] Could not read npm profile.");
printOutput(profile);
process.exit(1);
}
} else {
const profileJson = JSON.parse(profile.stdout);
console.log(`[xmem] npm email verified: ${profileJson.email_verified}`);
console.log(`[xmem] npm 2FA enabled: ${profileJson.tfa}`);
if (!profileJson.tfa) {
console.log("[xmem] Enable npm 2FA or use a granular publish token before publishing.");
}
}

const packageView = runNpm(["view", "create-xmem", "name", "version", "--json"]);
if (packageView.status === 0) {
console.log("[xmem] create-xmem already exists on npm:");
printOutput(packageView);
process.exit(0);
}

const combinedOutput = `${packageView.stdout}\n${packageView.stderr}`;
if (combinedOutput.includes("E404")) {
console.log("[xmem] create-xmem is available on npm.");
process.exit(0);
}

console.error("[xmem] Could not check create-xmem package availability.");
printOutput(packageView);
process.exit(packageView.status || 1);
Loading
Loading