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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"name": "claude-code",
"source": "./plugins/claude-code",
"description": "Persistent semantic memory for Claude Code — user preferences, project context, prior decisions, and codebase facts that survive across sessions.",
"version": "0.1.10",
"version": "0.1.11",
"category": "productivity",
"homepage": "https://docs.atomicmemory.ai/integrations/coding-agents/claude-code",
"license": "Apache-2.0"
Expand Down
1 change: 1 addition & 0 deletions .fallowrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"packages/mcp-server/src/spawn.ts",
"packages/*/src/**/*.test.ts",
"plugins/openclaw/src/index.ts",
"plugins/*/src/**/*.test.ts",
"adapters/vercel-ai-sdk/src/index.ts",
"adapters/*/src/**/*.test.ts"
],
Expand Down
1 change: 1 addition & 0 deletions packages/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The binary loads config from environment variables:
| Variable | Required | Purpose |
|---|---|---|
| `ATOMICMEMORY_API_URL` | no** | Provider base URL. Defaults to the local AtomicMemory core (`http://127.0.0.1:3050`) when `ATOMICMEMORY_PROVIDER=atomicmemory`; required for `mem0`. |
| `ATOMICMEMORY_API_KEY` | no | Optional bearer credential forwarded to providers that require HTTP authorization. |
| `ATOMICMEMORY_PROVIDER` | no | Provider name — one of `atomicmemory` or `mem0`. Defaults to `atomicmemory`. |
| `ATOMICMEMORY_SCOPE_USER` | no | Default `user` scope. Defaults to the local machine user when omitted. |
| `ATOMICMEMORY_SCOPE_AGENT` | no* | Default `agent` scope |
Expand Down
4 changes: 4 additions & 0 deletions packages/mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
"./spawn": {
"types": "./dist/spawn.d.ts",
"import": "./dist/spawn.js"
},
"./embedded-client": {
"types": "./dist/embedded-client.d.ts",
"import": "./dist/embedded-client.js"
}
},
"bin": {
Expand Down
14 changes: 14 additions & 0 deletions packages/mcp-server/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ test('loadConfigFromEnv defaults URL, provider, and user scope', () => {
test('loadConfigFromEnv keeps explicit scope overrides', () => {
const config = loadConfigFromEnv({
USER: 'machine-user',
ATOMICMEMORY_API_URL: 'https://memory.example.com/',
ATOMICMEMORY_API_KEY: 'am-test-key',
ATOMICMEMORY_SCOPE_USER: 'configured-user',
ATOMICMEMORY_SCOPE_AGENT: 'codex',
ATOMICMEMORY_SCOPE_NAMESPACE: 'repo',
ATOMICMEMORY_SCOPE_THREAD: 'thread-1',
} as NodeJS.ProcessEnv);

assert.equal(config.apiUrl, 'https://memory.example.com');
assert.equal(config.apiKey, 'am-test-key');
assert.deepEqual(config.scope, {
user: 'configured-user',
agent: 'codex',
Expand All @@ -41,6 +45,16 @@ test('validateConfig accepts plugin config without URL, key, or scope', () => {
assert.ok(config.scope?.user);
});

test('validateConfig accepts explicit plugin api key', () => {
const config = validateConfig({
apiUrl: 'https://memory.example.com',
apiKey: 'am-plugin-key',
});

assert.equal(config.apiUrl, 'https://memory.example.com');
assert.equal(config.apiKey, 'am-plugin-key');
});

test('validateConfig requires explicit apiUrl for mem0', () => {
assert.throws(
() => validateConfig({ provider: 'mem0' }),
Expand Down
3 changes: 3 additions & 0 deletions packages/mcp-server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const ScopeSchema = z
const ConfigSchema = z
.object({
apiUrl: z.string().url().optional(),
apiKey: z.string().optional(),
provider: z.enum(['atomicmemory', 'mem0']).default(DEFAULT_PROVIDER),
scope: ScopeSchema.optional(),
})
Expand All @@ -39,6 +40,8 @@ export type Scope = z.infer<typeof ScopeSchema>;
*/
export function loadConfigFromEnv(env: NodeJS.ProcessEnv = process.env): ServerConfig {
const raw = {
apiUrl: cleanOptional(env.ATOMICMEMORY_API_URL),
apiKey: cleanOptional(env.ATOMICMEMORY_API_KEY),
provider: cleanOptional(env.ATOMICMEMORY_PROVIDER),
scope: parseScope(env),
};
Expand Down
33 changes: 33 additions & 0 deletions packages/mcp-server/src/embedded-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @file Embedded MCP client helper for host plugins that cannot speak
* MCP over stdio directly. The shared MCP server still owns tool
* semantics; this helper just wires an in-memory MCP client to it.
*/

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { spawnAtomicMemoryMcp } from './spawn.js';

export interface EmbeddedMcpToolCaller {
callTool(input: { name: string; arguments?: Record<string, unknown> }): Promise<{ content: unknown }>;
close(): Promise<void>;
}

export async function createEmbeddedMcpToolCaller(
config: unknown,
clientInfo: { name: string; version: string },
): Promise<EmbeddedMcpToolCaller> {
const { server } = await spawnAtomicMemoryMcp(config);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client(clientInfo);
await server.connect(serverTransport);
await client.connect(clientTransport);
return {
callTool(input) {
return client.callTool(input) as Promise<{ content: unknown }>;
},
close() {
return client.close();
},
};
}
1 change: 1 addition & 0 deletions packages/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export async function buildServer(config: ServerConfig): Promise<Server> {
async function initClient(config: ServerConfig): Promise<MemoryClient> {
const providerConfig = {
apiUrl: config.apiUrl,
...(config.apiKey ? { apiKey: config.apiKey } : {}),
};
const providers: MemoryClientConfig['providers'] =
config.provider === 'mem0'
Expand Down
5 changes: 4 additions & 1 deletion plugins/claude-code/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "atomicmemory",
"version": "0.1.10",
"version": "0.1.11",
"description": "Persistent semantic memory for Claude Code — user preferences, project context, prior decisions, and codebase facts that survive across sessions.",
"author": {
"name": "AtomicMemory",
Expand All @@ -14,7 +14,10 @@
"args": ["-c", "exec node \"$ATOMICMEMORY_MCP_SERVER_BIN\""],
"env": {
"ATOMICMEMORY_MCP_SERVER_BIN": "${ATOMICMEMORY_MCP_SERVER_BIN}",
"ATOMICMEMORY_API_URL": "${ATOMICMEMORY_API_URL}",
"ATOMICMEMORY_API_KEY": "${ATOMICMEMORY_API_KEY}",
"ATOMICMEMORY_PROVIDER": "${ATOMICMEMORY_PROVIDER:-atomicmemory}",
"ATOMICMEMORY_SCOPE_USER": "${ATOMICMEMORY_SCOPE_USER}",
"ATOMICMEMORY_SCOPE_AGENT": "${ATOMICMEMORY_SCOPE_AGENT:-}",
"ATOMICMEMORY_SCOPE_NAMESPACE": "${ATOMICMEMORY_SCOPE_NAMESPACE:-}",
"ATOMICMEMORY_SCOPE_THREAD": "${ATOMICMEMORY_SCOPE_THREAD:-}"
Expand Down
1 change: 1 addition & 0 deletions plugins/claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export ATOMICMEMORY_CAPTURE_LEVEL="balanced" # minimal|balanced|full
```

`ATOMICMEMORY_MCP_SERVER_BIN`, `ATOMICMEMORY_API_URL`, `ATOMICMEMORY_API_KEY`, `ATOMICMEMORY_PROVIDER`, `ATOMICMEMORY_SCOPE_USER`, and `ATOMICMEMORY_CAPTURE_LEVEL` are required for the Claude Code plugin and hooks. Optional scope vars narrow retrieval and lifecycle record metadata.
If `ATOMICMEMORY_SCOPE_USER` is empty, the MCP server derives a local user from the host OS; set it explicitly when multiple operators share a machine or when you need a stable cross-machine identity.

#### Local extraction with Claude Code auth

Expand Down
2 changes: 1 addition & 1 deletion plugins/claude-code/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atomicmemory/claude-code-plugin",
"version": "0.1.10",
"version": "0.1.11",
"description": "AtomicMemory plugin for Claude Code — persistent semantic memory across sessions.",
"private": false,
"license": "Apache-2.0",
Expand Down
2 changes: 2 additions & 0 deletions plugins/codex/.codex-mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"env_vars": [
"ATOMICMEMORY_MCP_SERVER_BIN",
"ATOMICMEMORY_API_URL",
"ATOMICMEMORY_API_KEY",
"ATOMICMEMORY_PROVIDER",
"ATOMICMEMORY_SCOPE_USER",
"ATOMICMEMORY_SCOPE_AGENT",
"ATOMICMEMORY_SCOPE_NAMESPACE",
"ATOMICMEMORY_SCOPE_THREAD"
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "atomicmemory",
"version": "0.1.10",
"version": "0.1.11",
"description": "AtomicMemory memory layer for Codex. Pluggable semantic memory — swap backends through the SDK's MemoryProvider model by config, not code change.",
"author": {
"name": "AtomicMemory",
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atomicmemory/codex-plugin",
"version": "0.1.10",
"version": "0.1.11",
"description": "AtomicMemory plugin for OpenAI Codex — plugin manifest, MCP server config, and memory protocol skill.",
"private": true,
"license": "Apache-2.0",
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/skills/atomicmemory/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ description: >
license: Apache-2.0
metadata:
author: AtomicMemory
version: "0.1.10"
version: "0.1.11"
category: ai-memory
tags: "memory, semantic-search, codex, pluggable"
---
Expand Down
2 changes: 1 addition & 1 deletion plugins/cursor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atomicmemory/cursor-plugin",
"version": "0.1.10",
"version": "0.1.11",
"description": "AtomicMemory integration for Cursor - MCP configuration and project rules for persistent semantic memory.",
"private": true,
"license": "Apache-2.0",
Expand Down
17 changes: 17 additions & 0 deletions plugins/hermes/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Changelog

## 0.1.11 - 2026-05-14

### Added

- Added the packaged Hermes provider installer exposed through the `atomicmemory-hermes` npm binary.

### Fixed

- Preserved the Python SDK import path when Hermes is installed from the packaged npm artifact.

## 0.1.10 - 2026-05-14

### Added

- Initial public npm package for the AtomicMemory Hermes plugin.
25 changes: 16 additions & 9 deletions plugins/hermes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,18 @@ hooks, tool schemas. Memory semantics flow through the published Python SDK.

## Prerequisites

- Hermes Agent installed and `HERMES_HOME` set
- Hermes Agent installed
- AtomicMemory core URL exported as `ATOMICMEMORY_API_URL`

## Install (dev)
## Install

The simplest dev install symlinks the plugin into Hermes' memory directory.
Hermes installs the published `atomicmemory` SDK from `plugin.yaml`.
Install the provider from the published npm package. The installer copies the
Python provider files into Hermes' memory-provider directory; no repository
clone is required.

```bash
cd /path/to/atomicmemory-integrations
mkdir -p "$HERMES_HOME/plugins/memory"
ln -s "$(pwd)/plugins/hermes" "$HERMES_HOME/plugins/memory/atomicmemory"
export ATOMICMEMORY_API_URL="http://localhost:3050"
npx -y @atomicmemory/hermes-plugin install
export ATOMICMEMORY_API_URL="http://127.0.0.1:3050"
```

Then select and verify the provider:
Expand All @@ -48,6 +47,14 @@ hermes memory status
# confirm "atomicmemory" is active
```

For source development, symlink the checkout instead:

```bash
cd /path/to/atomicmemory-integrations
mkdir -p "$HERMES_HOME/plugins/memory"
ln -s "$(pwd)/plugins/hermes" "$HERMES_HOME/plugins/memory/atomicmemory"
```

## Config

Hermes' setup wizard prompts for a minimal pair (`scope_user`, `memory_scope`).
Expand Down Expand Up @@ -144,7 +151,7 @@ run while AtomicMemory is temporarily unavailable.

| Symptom | Likely cause |
|---|---|
| Provider does not appear in `hermes memory setup` | Wrong install path. Memory providers must live under `$HERMES_HOME/plugins/memory/<name>/`, not `$HERMES_HOME/plugins/<name>/`. |
| Provider does not appear in `hermes memory setup` | Wrong install path. User-installed memory providers must live under `$HERMES_HOME/plugins/memory/<name>/`. |
| `is_available()` returns False | `ATOMICMEMORY_API_URL` unset, or the Hermes Python environment did not install the `atomicmemory` dependency from `plugin.yaml`. |
| Import fails at startup | The Hermes Python environment is missing the SDK dependency from `plugin.yaml`. |
| Calls fail with `PROVIDER_UNSUPPORTED` while `memory_scope=siloed` | The configured SDK provider is not the AtomicMemory core (e.g. it's `mem0`). Either switch `ATOMICMEMORY_PROVIDER=atomicmemory` or move to `memory_scope=shared`. |
Expand Down
103 changes: 103 additions & 0 deletions plugins/hermes/install.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env node
/**
* Install the AtomicMemory Hermes provider from the published npm package.
*
* Hermes memory providers are filesystem plugins under
* `$HERMES_HOME/plugins/memory/<name>`. The npm package already contains the
* Python provider files, so this installer copies only that managed provider
* surface into the active Hermes profile without requiring a Git checkout.
*/

import { copyFileSync, mkdirSync, readFileSync } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const packageDir = dirname(fileURLToPath(import.meta.url));

function main(argv) {
const options = parseArgs(argv);
if (options.help) {
printHelp();
return;
}
if (options.command !== 'install') {
throw new Error(`Unknown command '${options.command}'. Expected 'install'.`);
}
const target = options.target ?? defaultTarget();
installProvider(target);
printNextSteps(target);
}

function parseArgs(argv) {
const options = { command: 'install', target: undefined, help: false };
const args = [...argv];
if (args[0] && !args[0].startsWith('-')) {
options.command = args.shift();
}
while (args.length > 0) {
const arg = args.shift();
if (arg === '--help' || arg === '-h') {
options.help = true;
continue;
}
if (arg === '--target') {
const value = args.shift();
if (!value) throw new Error('--target requires a path');
options.target = resolve(value);
continue;
}
throw new Error(`Unknown option '${arg}'`);
}
return options;
}

function defaultTarget() {
const hermesHome = process.env.HERMES_HOME || defaultHermesHome();
return join(hermesHome, 'plugins', 'memory', 'atomicmemory');
}

function defaultHermesHome() {
const home = process.env.HOME;
if (!home) {
throw new Error('Set HERMES_HOME or HOME before installing the Hermes provider.');
}
return join(home, '.hermes');
}

function installProvider(target) {
mkdirSync(target, { recursive: true });
for (const file of providerFiles()) {
copyFileSync(join(packageDir, file), join(target, basename(file)));
}
}

function providerFiles() {
const pkg = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
return pkg.files.filter((file) => file.endsWith('.py') || file === 'plugin.yaml' || file === 'README.md');
}

function printNextSteps(target) {
console.log(`Installed AtomicMemory Hermes provider to ${target}`);
console.log('');
console.log('Next:');
console.log(' export ATOMICMEMORY_API_URL="http://127.0.0.1:3050"');
console.log(' hermes memory setup');
console.log(' hermes memory status');
}

function printHelp() {
console.log(`Usage: atomicmemory-hermes [install] [--target <dir>]

Installs the AtomicMemory Hermes memory provider into:
$HERMES_HOME/plugins/memory/atomicmemory

When HERMES_HOME is unset, defaults to:
$HOME/.hermes/plugins/memory/atomicmemory`);
}

try {
main(process.argv.slice(2));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
Loading
Loading