Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 17 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
* text=auto

*.js text eol=lf
*.json text eol=lf
*.py text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.toml text eol=lf
*.env text eol=lf
*.env.* text eol=lf

*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
3 changes: 2 additions & 1 deletion .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
cache: pip

- name: Install bandit
run: pip install bandit[toml]
run: pip install "bandit[toml,sarif]"

- name: Run Bandit
run: |
Expand All @@ -41,6 +41,7 @@ jobs:
continue-on-error: true

- name: Upload Bandit SARIF
if: always() && hashFiles('bandit-results.sarif') != ''
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: bandit-results.sarif
Expand Down
9 changes: 5 additions & 4 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 All @@ -65,7 +62,6 @@ rust/

# Empty root files
Makefile
CHANGELOG.md
ruff.toml

# GitHub templates (empty)
Expand Down Expand Up @@ -96,3 +92,8 @@ src/api/routes/search.py
# Misc
error.json
MIGRATION_PLAN.md
node_modules/
repos/
exports/
tmp/
.xmem/
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Changelog

## Unreleased

- Add local XMem setup through `npx create-xmem@latest` and `npm run dev`.
- Add local Docker storage, Chrome extension build patching, diagnostics, verification, and context export/import/sync commands.
50 changes: 34 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,36 +309,54 @@ 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
```

This works on Windows, macOS, and Linux. It creates a local XMem workspace, installs the backend, starts local storage, builds the Chrome extension, and launches the API at `http://localhost:8000`.

Local prerequisites:

# Install (requires Python 3.11+)
pip install -e .
- Git
- Node.js 20+
- Python 3.11+
- Docker Desktop
- Ollama, unless you add a cloud LLM key to `.env`

# 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 +370,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"
}
}
143 changes: 143 additions & 0 deletions packages/create-xmem/bin/create-xmem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/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,
};
let targetSet = false;

function readOptionValue(index, name) {
const value = argv[index + 1];
if (!value || value.startsWith("-")) {
console.error(`[create-xmem] ${name} requires a value.`);
usage(1);
}
return value;
}

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 = readOptionValue(index, arg);
index += 1;
continue;
}

if (arg === "--branch") {
options.branch = readOptionValue(index, arg);
index += 1;
continue;
}

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

if (!targetSet) {
options.target = arg;
targetSet = true;
continue;
}

console.error(`[create-xmem] Unexpected extra argument: ${arg}`);
usage(1);
}

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");
Loading
Loading