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
9 changes: 9 additions & 0 deletions plugin/xmem-claude/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "xmem-claude",
"version": "0.1.0",
"description": "Persistent XMem memory across Claude Code sessions",
"author": {
"name": "XMem",
"email": "support@xmem.in"
}
}
49 changes: 49 additions & 0 deletions plugin/xmem-claude/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# XMem Claude Plugin

Claude Code plugin for persistent memory through XMem.

This package is inspired by `claude-supermemory`, but talks directly to XMem:

- loads relevant project memory on `SessionStart`
- stores a redacted transcript tail on `Stop`
- provides `xmem-search` and `xmem-save` skills
- includes commands for indexing, config, session checks, and logout

## Install

From Claude Code, install this plugin from the local folder once it is published or linked by the plugin marketplace flow for this repo.

## Configuration

Use environment variables. Do not commit secrets.

```bash
export XMEM_API_KEY="xmem_..."
export XMEM_API_URL="https://api.xmem.in"
export XMEM_USER_ID="your-user-id"
```

`XMEM_API_URL` defaults to `https://api.xmem.in`. `XMEM_USER_ID` falls back to the local OS username; production API keys scope requests to the authenticated key owner.

Optional project config can live at `.claude/.xmem-claude/config.json`:

```json
{
"apiUrl": "https://api.xmem.in",
"userId": "your-user-id"
}
```

Prefer environment variables for API keys.

## Commands

- `/xmem-claude:index` - explore the current repo and save a project summary
- `/xmem-claude:project-config` - show configuration options
- `/xmem-claude:session` - check whether memory is configured
- `/xmem-claude:logout` - remove project-local config

## Skills

- `xmem-search` - search prior XMem memories
- `xmem-save` - save durable project knowledge
19 changes: 19 additions & 0 deletions plugin/xmem-claude/commands/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
description: Index the current codebase into XMem for future Claude Code context
allowed-tools: ["Read", "Glob", "Grep", "Bash"]
---

# Index Codebase Into XMem

Explore the repository and save a concise architecture summary into XMem.

1. Read `README.md`, package manifests, config files, and entry points.
2. Identify the stack, runtime commands, major modules, API routes, data stores, and conventions.
3. Skip dependency folders, generated output, lock files, virtual environments, and secrets.
4. Save the final summary:

```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/save-project-memory.cjs" "SUMMARY_HERE"
```

Include important files and decisions, but do not save secrets.
16 changes: 16 additions & 0 deletions plugin/xmem-claude/commands/logout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
description: Remove local XMem Claude plugin project config
allowed-tools: ["Bash"]
---

# XMem Logout

This plugin does not store credentials by default. It reads `XMEM_API_KEY` from the environment.

To remove project-local config:

```bash
node -e "const fs=require('fs'); const p='.claude/.xmem-claude/config.json'; if(fs.existsSync(p)){fs.rmSync(p); console.log('Removed '+p)}else{console.log('No project config found')}"
```

Also unset `XMEM_API_KEY` in your shell if you want to disconnect this terminal session.
25 changes: 25 additions & 0 deletions plugin/xmem-claude/commands/project-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
description: Show XMem Claude plugin configuration options
allowed-tools: ["Read", "Write", "Bash"]
---

# XMem Claude Configuration

The plugin reads credentials from environment variables first:

```bash
export XMEM_API_KEY="xmem_..."
export XMEM_API_URL="https://api.xmem.in"
export XMEM_USER_ID="your-user-id"
```

Optional project config lives at `.claude/.xmem-claude/config.json`:

```json
{
"apiUrl": "https://api.xmem.in",
"userId": "your-user-id"
}
```

Avoid storing API keys in project config. Prefer environment variables or your shell secret manager.
14 changes: 14 additions & 0 deletions plugin/xmem-claude/commands/session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
description: Check whether XMem Claude memory is configured
allowed-tools: ["Bash"]
---

# XMem Session

Check plugin configuration without printing secrets:

```bash
node -e "console.log(process.env.XMEM_API_KEY ? 'XMEM_API_KEY is set' : 'XMEM_API_KEY is not set'); console.log('XMEM_API_URL=' + (process.env.XMEM_API_URL || 'https://api.xmem.in'))"
```

If the key is missing, set `XMEM_API_KEY` before starting Claude Code.
27 changes: 27 additions & 0 deletions plugin/xmem-claude/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"description": "XMem: Persistent memory for Claude Code",
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.cjs\"",
"timeout": 30
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.cjs\"",
"timeout": 30
}
]
}
]
}
}
17 changes: 17 additions & 0 deletions plugin/xmem-claude/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "xmem-claude",
"version": "0.1.0",
"description": "Persistent XMem memory for Claude Code sessions",
"private": true,
"type": "commonjs",
"engines": {
"node": ">=18.0.0"
},
"keywords": [
"claude-code",
"plugin",
"xmem",
"memory"
],
"license": "MIT"
}
29 changes: 29 additions & 0 deletions plugin/xmem-claude/scripts/add-memory.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env node
const { createClient } = require("./lib/xmem-client.cjs");
const { projectName, redactSecrets, truncate } = require("./lib/plugin-utils.cjs");
Comment thread
greptile-apps[bot] marked this conversation as resolved.

async function main() {
const content = process.argv.slice(2).join(" ").trim();
if (!content) {
console.log('Usage: node add-memory.cjs "content to save"');
return;
}

try {
const cwd = process.cwd();
const client = createClient(cwd);
await client.ingest(truncate(redactSecrets(content)), {
source: "claude-code",
type: "manual",
project: projectName(cwd),
});
console.log(`Saved memory to XMem for project: ${projectName(cwd)}`);
} catch (error) {
console.log(`XMem save failed: ${error.message}`);
}
}

main().catch((error) => {
console.error(`Fatal error: ${error.message}`);
process.exit(1);
});
34 changes: 34 additions & 0 deletions plugin/xmem-claude/scripts/context-hook.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env node
const { createClient, formatResults } = require("./lib/xmem-client.cjs");
const { projectName, readStdin, writeJson } = require("./lib/plugin-utils.cjs");

async function main() {
const input = await readStdin();
const cwd = input.cwd || process.cwd();
const project = projectName(cwd);

try {
const client = createClient(cwd);
const data = await client.search(`Claude Code project context, architecture, decisions, conventions for ${project}`, 6);
const formatted = formatResults(data);

writeJson({
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: `<xmem-context>\n${formatted}\n</xmem-context>`,
},
});
} catch (error) {
writeJson({
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: `<xmem-status>\nXMem memory unavailable: ${error.message}\nSet XMEM_API_KEY to enable Claude memory.\n</xmem-status>`,
},
});
}
}

main().catch((error) => {
console.error(`XMem fatal: ${error.message}`);
process.exit(1);
});
120 changes: 120 additions & 0 deletions plugin/xmem-claude/scripts/lib/plugin-utils.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");

const MAX_TEXT_LENGTH = 12000;

function readStdin() {
return new Promise((resolve) => {
let data = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
data += chunk;
});
process.stdin.on("end", () => {
if (!data.trim()) {
resolve({});
return;
}
try {
resolve(JSON.parse(data));
} catch {
resolve({});
}
});
});
}

function writeJson(value) {
process.stdout.write(`${JSON.stringify(value)}\n`);
}

function projectName(cwd = process.cwd()) {
return path.basename(path.resolve(cwd));
}

function projectConfigPath(cwd = process.cwd()) {
return path.join(path.resolve(cwd), ".claude", ".xmem-claude", "config.json");
}

function globalConfigPath() {
return path.join(os.homedir(), ".xmem-claude", "settings.json");
}

function readJsonFile(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch {
return {};
}
}

function loadConfig(cwd = process.cwd()) {
return {
...readJsonFile(globalConfigPath()),
...readJsonFile(projectConfigPath(cwd)),
};
}

function truncate(text, limit = MAX_TEXT_LENGTH) {
const value = String(text || "").trim();
if (value.length <= limit) return value;
return `${value.slice(0, limit)}\n\n[truncated]`;
}

function redactSecrets(text) {
return String(text || "")
.replace(/xmem_[A-Za-z0-9_-]{12,}/g, "[redacted-xmem-key]")
.replace(/sk-[A-Za-z0-9_-]{16,}/g, "[redacted-api-key]")
.replace(/(authorization\s*[:=]\s*bearer\s+)[^\s"'`]+/gi, "$1[redacted]")
.replace(/(bearer\s+)[^\s"'`]+/gi, "$1[redacted]")
.replace(/((?:api[_-]?key|authorization|token)\s*[:=]\s*)[^\s"'`]+/gi, "$1[redacted]");
}

function extractText(value) {
if (!value) return "";
if (typeof value === "string") return value;
if (Array.isArray(value)) return value.map(extractText).filter(Boolean).join("\n");
if (typeof value === "object") {
if (typeof value.text === "string") return value.text;
if (typeof value.content === "string") return value.content;
if (value.content) return extractText(value.content);
if (value.message) return extractText(value.message);
}
return "";
}

function transcriptTail(transcriptPath, sessionId, cwd) {
if (!transcriptPath || !fs.existsSync(transcriptPath)) return "";
const lines = fs.readFileSync(transcriptPath, "utf8").split(/\r?\n/).filter(Boolean);
const entries = [];

for (const line of lines) {
try {
const item = JSON.parse(line);
const role = item.type || item.role || item.message?.role || "entry";
const text = extractText(item.message || item.content || item);
if (text.trim()) entries.push(`${role}: ${text.trim()}`);
} catch {
if (line.trim()) entries.push(line.trim());
}
}

const body = entries.slice(-40).join("\n\n");
if (!body.trim()) return "";

return truncate(redactSecrets(`[Claude Code session]\nProject: ${projectName(cwd)}\nSession: ${sessionId || "unknown"}\n\n${body}`));
}

module.exports = {
extractText,
globalConfigPath,
loadConfig,
projectConfigPath,
projectName,
readStdin,
redactSecrets,
transcriptTail,
truncate,
writeJson,
};
Loading
Loading