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
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"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.12",
"version": "0.1.13",
"category": "productivity",
"homepage": "https://docs.atomicmemory.ai/integrations/coding-agents/claude-code",
"homepage": "https://docs.atomicstrata.ai/integrations/coding-agents/claude-code",
"license": "Apache-2.0"
}
]
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
blank_issues_enabled: true
contact_links:
- name: Documentation
url: https://docs.atomicmemory.ai/integrations/
url: https://docs.atomicstrata.ai/integrations/
about: Read the AtomicMemory integration docs.
- name: Discussions
url: https://github.com/atomicstrata/atomicmemory-integrations/discussions
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,13 @@ claude plugin update claude-code@atomicmemory

Then fully restart Claude Code. Running Claude sessions can keep the previous hook registration in memory. Do not treat `~/.claude/plugins/cache/...` as the source of truth; it is only Claude's installed cache.

Required env before launching Claude Code:
Environment is **optional** for local mode — the plugin defaults to a local AtomicMemory core at `http://127.0.0.1:3050`, derives `ATOMICMEMORY_SCOPE_USER` from the host OS, and uses `ATOMICMEMORY_CAPTURE_LEVEL=balanced`. See `plugins/claude-code/README.md` for the full defaults table.

Set these before launching Claude Code only when you need to override a default (e.g. to talk to a hosted AtomicMemory service):

```bash
export ATOMICMEMORY_API_URL="https://memory.yourco.com"
export ATOMICMEMORY_API_KEY="am_live_..."
export ATOMICMEMORY_PROVIDER="atomicmemory"
export ATOMICMEMORY_SCOPE_USER="$USER"
export ATOMICMEMORY_CAPTURE_LEVEL="balanced"
```
Expand Down Expand Up @@ -278,8 +279,8 @@ For dev installs, symlink the plugin into Hermes' memory directory. Hermes
installs the published Python SDK from `plugins/hermes/plugin.yaml`:

```bash
mkdir -p "$HERMES_HOME/plugins/memory"
ln -s "$(pwd)/plugins/hermes" "$HERMES_HOME/plugins/memory/atomicmemory"
mkdir -p "$HERMES_HOME/plugins"
ln -s "$(pwd)/plugins/hermes" "$HERMES_HOME/plugins/atomicmemory"
hermes memory setup # select "atomicmemory"
hermes memory status # confirm "atomicmemory" is active
```
Expand Down
10 changes: 5 additions & 5 deletions plugins/claude-code/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
{
"name": "atomicmemory",
"version": "0.1.12",
"version": "0.1.13",
"description": "Persistent semantic memory for Claude Code — user preferences, project context, prior decisions, and codebase facts that survive across sessions.",
"author": {
"name": "AtomicMemory",
"url": "https://atomicmem.ai"
},
"homepage": "https://docs.atomicmemory.ai/integrations/coding-agents/claude-code",
"homepage": "https://docs.atomicstrata.ai/integrations/coding-agents/claude-code",
"license": "Apache-2.0",
"mcpServers": {
"atomicmemory": {
"command": "npx",
"args": ["-y", "--package=@atomicmemory/mcp-server@^0.1.1", "atomicmemory-mcp"],
"env": {
"ATOMICMEMORY_API_URL": "${ATOMICMEMORY_API_URL}",
"ATOMICMEMORY_API_KEY": "${ATOMICMEMORY_API_KEY}",
"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_USER": "${ATOMICMEMORY_SCOPE_USER:-}",
"ATOMICMEMORY_SCOPE_AGENT": "${ATOMICMEMORY_SCOPE_AGENT:-}",
"ATOMICMEMORY_SCOPE_NAMESPACE": "${ATOMICMEMORY_SCOPE_NAMESPACE:-}",
"ATOMICMEMORY_SCOPE_THREAD": "${ATOMICMEMORY_SCOPE_THREAD:-}"
Expand Down
26 changes: 17 additions & 9 deletions plugins/claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,32 @@ brew install jq
sudo apt-get install -y jq
```

### 2. Export shell env vars
### 2. Configure (optional in local mode)

The MCP server and the lifecycle hook scripts read their config from the shell environment. Export these in `~/.zshrc` / `~/.bashrc` before launching Claude Code:
The MCP server and the lifecycle hook scripts read their config from the shell environment. None of the `ATOMICMEMORY_*` variables are required to run the plugin against a local AtomicMemory core — the documented defaults are:

| Var | Local-mode default |
|---|---|
| `ATOMICMEMORY_API_URL` | `http://127.0.0.1:3050` |
| `ATOMICMEMORY_API_KEY` | not required |
| `ATOMICMEMORY_PROVIDER` | `atomicmemory` |
| `ATOMICMEMORY_SCOPE_USER` | derived from the host OS user |
| `ATOMICMEMORY_CAPTURE_LEVEL` | `balanced` |

Set them only when you need to override a default (for example, to talk to a hosted AtomicMemory service):

```bash
export ATOMICMEMORY_API_URL="https://memory.yourco.com"
export ATOMICMEMORY_API_KEY="am_live_…"
export ATOMICMEMORY_PROVIDER="atomicmemory"
export ATOMICMEMORY_SCOPE_USER="$USER"
export ATOMICMEMORY_CAPTURE_LEVEL="balanced" # minimal|balanced|full
# Optional:
# Optional scope:
# export ATOMICMEMORY_SCOPE_NAMESPACE="<repo-or-project>"
# export ATOMICMEMORY_SCOPE_AGENT="claude-code"
# export ATOMICMEMORY_SCOPE_THREAD="<thread-id>"
```

`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.
Set `ATOMICMEMORY_SCOPE_USER` explicitly when multiple operators share a machine or when you need a stable cross-machine identity; otherwise the MCP server derives one from the host OS.

#### Local extraction with Claude Code auth

Expand All @@ -54,8 +62,8 @@ for hosted/team deployments where a server would run under one operator's
Claude Code subscription. Embeddings still use core's configured embedding
provider; select a local embedding provider separately for a fully local setup.

- `_API_URL` / `_API_KEY` / `_PROVIDER` / `_SCOPE_USER` — needed by **both** the MCP server (for `memory_search` / `memory_ingest` / `memory_package` tool calls) and lifecycle hooks.
- `_CAPTURE_LEVEL` — controls lifecycle write volume. Valid values are `minimal`, `balanced`, and `full`.
- `_API_URL` / `_API_KEY` / `_PROVIDER` / `_SCOPE_USER` — read by **both** the MCP server (for `memory_search` / `memory_ingest` / `memory_package` tool calls) and lifecycle hooks. All optional in local mode (see defaults above).
- `_CAPTURE_LEVEL` — controls lifecycle write volume. Valid values are `minimal`, `balanced`, and `full`. Defaults to `balanced` when unset; invalid values still fail closed.
- `_SCOPE_NAMESPACE` — used by both, as a per-project isolation boundary.
- `_SCOPE_AGENT` / `_SCOPE_THREAD` — forwarded to the MCP server as the request scope. The direct prompt-search path uses the core fast-search endpoint's supported user/namespace scope.

Expand All @@ -73,7 +81,7 @@ Optional capture controls:
- `ATOMICMEMORY_TASK_MAX_DESCRIPTION_CHARS=600` controls the maximum cleaned task description excerpt stored by `TaskCompleted`. If set, it must be a positive integer.
- `ATOMICMEMORY_SEMANTIC_PROMPTS_ENABLED=false` disables extra Stop prompts that ask Claude to extract semantic learnings. If set, it must be `true` or `false`.

If required config is missing, helper tools are unavailable, or numeric/boolean env vars are invalid, hooks surface the error instead of running in a degraded mode.
If a helper tool is unavailable, an explicit `ATOMICMEMORY_*` value is invalid (bogus capture level, non-numeric integer var, non-boolean flag), or core is unreachable, hooks surface the error instead of running in a degraded mode.

### 3. Install the plugin

Expand Down
4 changes: 2 additions & 2 deletions 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.12",
"version": "0.1.13",
"description": "AtomicMemory plugin for Claude Code — persistent semantic memory across sessions.",
"private": false,
"license": "Apache-2.0",
Expand All @@ -17,6 +17,6 @@
"README.md"
],
"scripts": {
"test": "bash scripts/__tests__/quick-ingest-body.sh"
"test": "bash scripts/__tests__/quick-ingest-body.sh && bash scripts/__tests__/load-env-defaults.sh && bash scripts/__tests__/auth-header.sh"
}
}
140 changes: 140 additions & 0 deletions plugins/claude-code/scripts/__tests__/auth-header.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
#
# Unit gate for the Bearer auth header behavior of the hooks'
# direct-to-core HTTP calls in
# `plugins/claude-code/scripts/lib/atomicmemory.sh`.
#
# Shadows `curl` with a bash function that captures argv to a file,
# then exercises `am_post_quick_ingest` and `am_search_fast` with
# AM_API_KEY both set and unset. Asserts:
# 1. With AM_API_KEY set, both curl invocations include
# `-H Authorization: Bearer <key>`.
# 2. With AM_API_KEY empty, neither invocation includes the
# Authorization header.
#
# Matches core's `requireBearer` middleware contract
# (atomicmemory-core/src/middleware/require-bearer.ts).

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_PATH="$SCRIPT_DIR/../lib/atomicmemory.sh"

if [ ! -f "$LIB_PATH" ]; then
printf 'fixture path missing: %s\n' "$LIB_PATH" >&2
exit 1
fi

unset ATOMICMEMORY_API_KEY
unset ATOMICMEMORY_API_URL
export USER="${USER:-test-user}"

# shellcheck source=../lib/atomicmemory.sh
source "$LIB_PATH"

ARGV_LOG="$(mktemp)"
trap 'rm -f "$ARGV_LOG"' EXIT

# Shadow curl with a bash function that writes its full argv to the
# log file (one arg per line, with a record separator between calls)
# and emits a 200 OK so the caller path completes happily.
curl() {
printf '%s\n' "$@" >>"$ARGV_LOG"
printf '---END---\n' >>"$ARGV_LOG"
for arg in "$@"; do
case "$arg" in
-w|--write-out) printf '200' ;;
esac
done
}
export -f curl

PASS_COUNT=0
FAIL_COUNT=0

assert() {
local name="$1"
local condition="$2"
if [ "$condition" = "true" ]; then
printf ' ✓ %s\n' "$name"
PASS_COUNT=$((PASS_COUNT + 1))
else
printf ' ✗ %s\n' "$name" >&2
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
}

reset_log() {
: >"$ARGV_LOG"
}

argv_contains_header() {
local header_value="$1"
awk -v target="$header_value" '
/^-H$/ { capture = 1; next }
capture { if ($0 == target) { found = 1 } capture = 0 }
END { exit (found ? 0 : 1) }
' "$ARGV_LOG"
}

# ---------------------------------------------------------------------------
# Case 1: AM_API_KEY set → Authorization header present in both calls
# ---------------------------------------------------------------------------
printf '\nCase 1: AM_API_KEY set → Bearer auth header on hook curls\n'
export ATOMICMEMORY_API_URL="https://memory.example.com"
export ATOMICMEMORY_API_KEY="am_live_secret"
am_load_env || { printf 'am_load_env failed\n' >&2; exit 1; }

reset_log
body='{"user_id":"u","conversation":"c","source_site":"claude-code","source_url":"atomicmemory://test","skip_extraction":true}'
am_post_quick_ingest "$body" >/dev/null 2>&1 || true
argv_contains_header "Authorization: Bearer am_live_secret" && cond=true || cond=false
assert "ingest curl includes Authorization: Bearer <key>" "$cond"

reset_log
am_search_fast "what did we decide" 3 >/dev/null 2>&1 || true
argv_contains_header "Authorization: Bearer am_live_secret" && cond=true || cond=false
assert "search curl includes Authorization: Bearer <key>" "$cond"

unset ATOMICMEMORY_API_KEY
unset ATOMICMEMORY_API_URL

# ---------------------------------------------------------------------------
# Case 2: AM_API_KEY unset → no Authorization header on either call
# ---------------------------------------------------------------------------
printf '\nCase 2: AM_API_KEY unset → no Authorization header\n'
am_load_env || { printf 'am_load_env failed\n' >&2; exit 1; }

reset_log
am_post_quick_ingest "$body" >/dev/null 2>&1 || true
grep -q '^Authorization:' "$ARGV_LOG" && cond=false || cond=true
assert "ingest curl has no Authorization header" "$cond"

reset_log
am_search_fast "what did we decide" 3 >/dev/null 2>&1 || true
grep -q '^Authorization:' "$ARGV_LOG" && cond=false || cond=true
assert "search curl has no Authorization header" "$cond"

# ---------------------------------------------------------------------------
# Case 3: AM_API_URL override propagates to the curl call
# ---------------------------------------------------------------------------
printf '\nCase 3: AM_API_URL override propagates to the wire\n'
export ATOMICMEMORY_API_URL="https://memory.example.com"
am_load_env || { printf 'am_load_env failed\n' >&2; exit 1; }

reset_log
am_post_quick_ingest "$body" >/dev/null 2>&1 || true
grep -qx 'https://memory.example.com/v1/memories/ingest/quick' "$ARGV_LOG" && cond=true || cond=false
assert "ingest URL uses override host" "$cond"

reset_log
am_search_fast "q" 3 >/dev/null 2>&1 || true
grep -qx 'https://memory.example.com/v1/memories/search/fast' "$ARGV_LOG" && cond=true || cond=false
assert "search URL uses override host" "$cond"

unset ATOMICMEMORY_API_URL

printf '\n--- %d passed, %d failed ---\n' "$PASS_COUNT" "$FAIL_COUNT"
if [ "$FAIL_COUNT" -gt 0 ]; then
exit 1
fi
Loading
Loading