diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7f8f89361..1629d8e2f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,16 +5,16 @@ "email": "open_oceanbase@oceanbase.com" }, "metadata": { - "description": "PowerMem marketplace: intelligent memory for Claude Code.", + "description": "PowerMem marketplace: intelligent memory for Claude Code and Codex CLI.", "version": "0.1.0" }, "plugins": [ { "name": "memory-powermem", - "source": "./apps/claude-code-plugin", - "description": "PowerMem intelligent memory for Claude Code: auto-recall on prompt submit and auto-save on session end via HTTP hooks.", + "source": "./apps/agent-plugin", + "description": "PowerMem intelligent memory for Claude Code and Codex CLI: Claude Code uses HTTP hooks; Codex CLI uses skills with explicit MCP wiring.", "category": "memory", - "tags": ["memory", "powermem", "rag", "hooks", "oceanbase"] + "tags": ["memory", "powermem", "rag", "hooks", "mcp", "codex", "oceanbase"] } ] } diff --git a/.github/workflows/plugins-build.yml b/.github/workflows/plugins-build.yml index 455900adf..6a6638390 100644 --- a/.github/workflows/plugins-build.yml +++ b/.github/workflows/plugins-build.yml @@ -64,8 +64,8 @@ jobs: path: apps/vscode-extension/*.vsix retention-days: 30 - package-claude-plugin: - name: Package Claude Code Plugin + package-agent-plugin: + name: Package Agent Plugin runs-on: ubuntu-latest steps: @@ -77,20 +77,20 @@ jobs: with: go-version: ${{ env.GO_VERSION }} - - name: Package Claude Code plugin (cross-compile hooks + zip) - run: bash apps/claude-code-plugin/scripts/package-plugin.sh + - name: Package agent plugin (cross-compile hooks + zip) + run: bash apps/agent-plugin/scripts/package-plugin.sh - - name: Upload Claude Code plugin (zip) + - name: Upload agent plugin (zip) uses: actions/upload-artifact@v7 with: - name: powermem-claude-code-plugin-zip - path: apps/claude-code-plugin/dist/powermem-claude-code-plugin-*.zip + name: powermem-agent-plugin-zip + path: apps/agent-plugin/dist/powermem-agent-plugin-*.zip if-no-files-found: error retention-days: 30 release-plugins: name: Release Plugin Assets runs-on: ubuntu-latest - needs: [build-vscode-extension, package-claude-plugin] + needs: [build-vscode-extension, package-agent-plugin] if: startsWith(github.ref, 'refs/tags/plugins-') || (github.event_name == 'workflow_dispatch' && github.event.inputs.create_release == 'true') permissions: contents: write @@ -102,10 +102,10 @@ jobs: name: powermem-vscode-vsix path: vsix - - name: Download Claude Code plugin + - name: Download agent plugin uses: actions/download-artifact@v8 with: - name: powermem-claude-code-plugin-zip + name: powermem-agent-plugin-zip path: zip - name: Get version from tag or default @@ -126,12 +126,12 @@ jobs: ## PowerMem IDE Plugins - **PowerMem for VS Code** (`.vsix`): Download and install from VSIX in VS Code or Cursor. - - **PowerMem for Claude Code** (`.zip`): Download, unzip, and start Claude with `claude --plugin-dir /path/to/powermem-claude-code-plugin`. + - **PowerMem agent plugin** (`.zip`): Download, unzip, and start Claude with `claude --plugin-dir /path/to/powermem-agent-plugin`; Codex CLI installs through `codex plugin`. See [apps/README.md](https://github.com/${{ github.repository }}/blob/main/apps/README.md). files: | vsix/*.vsix - zip/powermem-claude-code-plugin-*.zip + zip/powermem-agent-plugin-*.zip draft: false prerelease: ${{ contains(github.ref, 'refs/tags/') == false }} generate_release_notes: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8e89d03ad..9fa467fe9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ on: paths: - 'src/**' - 'tests/**' - - 'apps/claude-code-plugin/**' + - 'apps/agent-plugin/**' - 'docker/Dockerfile.claude-hook-regression' - 'pyproject.toml' - 'Makefile' @@ -17,7 +17,7 @@ on: paths: - 'src/**' - 'tests/**' - - 'apps/claude-code-plugin/**' + - 'apps/agent-plugin/**' - 'docker/Dockerfile.claude-hook-regression' - 'pyproject.toml' - 'Makefile' diff --git a/Makefile b/Makefile index e78e6e890..c36d7c231 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install install-dev test test-unit test-integration test-e2e test-coverage test-fast test-slow test-claude-hook-docker check-python-version lint lint-full lint-pylint format clean build build-package build-check build-mcp-package build-mcp-check build-all-python-packages build-dashboard build-claude-hook package-claude-plugin publish-pypi publish-mcp-pypi publish-all-pypi publish-testpypi install-build-tools upload docs bump-version check-package-versions server-start server-stop server-restart server-status server-logs server-dashboard-start docker-build docker-run docker-up docker-down docker-logs docker-stop docker-restart docker-clean docker-ps +.PHONY: help install install-dev test test-unit test-integration test-e2e test-coverage test-fast test-slow test-claude-hook-docker check-python-version lint lint-full lint-pylint format clean build build-package build-check build-mcp-package build-mcp-check build-all-python-packages build-dashboard build-claude-hook package-agent-plugin package-claude-plugin publish-pypi publish-mcp-pypi publish-all-pypi publish-testpypi install-build-tools upload docs bump-version check-package-versions server-start server-stop server-restart server-status server-logs server-dashboard-start docker-build docker-run docker-up docker-down docker-logs docker-stop docker-restart docker-clean docker-ps PYTHON ?= python3 @@ -167,11 +167,13 @@ build-dashboard: ## Build dashboard frontend and inject into src/server/dashboar @cp -r dashboard/dist/* src/server/dashboard/ @echo "✓ Dashboard built. Start server with: make server-start-reload (then open http://localhost:$(SERVER_PORT)/dashboard/)" -build-claude-hook: ## Build Claude Code hook binaries (Go; output: apps/claude-code-plugin/hooks/bin/) - @bash apps/claude-code-plugin/scripts/build-hook-binaries.sh +build-claude-hook: ## Build Claude Code hook binaries (Go; output: apps/agent-plugin/hooks/bin/) + @bash apps/agent-plugin/scripts/build-hook-binaries.sh -package-claude-plugin: ## Zip Claude Code plugin for sharing (apps/claude-code-plugin/dist/*.zip) - @bash apps/claude-code-plugin/scripts/package-plugin.sh +package-agent-plugin: ## Zip Agent plugin for sharing (apps/agent-plugin/dist/*.zip) + @bash apps/agent-plugin/scripts/package-plugin.sh + +package-claude-plugin: package-agent-plugin ## Compatibility alias for package-agent-plugin install-build-tools: ## Install build and upload tools @echo "Installing build tools..." diff --git a/README.md b/README.md index 77d985437..9570b7cb9 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,10 @@ cd powermem Then open Claude Code in your terminal and paste this one line: ```text -Read and follow apps/claude-code-plugin/SETUP.md to set up PowerMem memory for Claude Code. +Read and follow apps/agent-plugin/SETUP.md to set up PowerMem memory for Claude Code. ``` -Claude Code reads [`apps/claude-code-plugin/SETUP.md`](apps/claude-code-plugin/SETUP.md), asks you for the few required secrets, and wires everything up end-to-end. +Claude Code reads [`apps/agent-plugin/SETUP.md`](apps/agent-plugin/SETUP.md), asks you for the few required secrets, and wires everything up end-to-end. #### Manual setup diff --git a/README_CN.md b/README_CN.md index a1e4e0467..8a68114df 100644 --- a/README_CN.md +++ b/README_CN.md @@ -110,10 +110,10 @@ cd powermem 在终端打开 Claude Code,粘贴下面这一行: ```text -Read and follow apps/claude-code-plugin/SETUP.md to set up PowerMem memory for Claude Code. +Read and follow apps/agent-plugin/SETUP.md to set up PowerMem memory for Claude Code. ``` -Claude Code 会阅读 [`apps/claude-code-plugin/SETUP.md`](apps/claude-code-plugin/SETUP.md),向你询问少量必要密钥,并端到端完成全部配置。 +Claude Code 会阅读 [`apps/agent-plugin/SETUP.md`](apps/agent-plugin/SETUP.md),向你询问少量必要密钥,并端到端完成全部配置。 #### 手动配置 diff --git a/README_JP.md b/README_JP.md index 03ba584cb..5b2bcf22d 100644 --- a/README_JP.md +++ b/README_JP.md @@ -110,10 +110,10 @@ cd powermem ターミナルで Claude Code を開き、次の一行を貼り付けます: ```text -Read and follow apps/claude-code-plugin/SETUP.md to set up PowerMem memory for Claude Code. +Read and follow apps/agent-plugin/SETUP.md to set up PowerMem memory for Claude Code. ``` -Claude Code は [`apps/claude-code-plugin/SETUP.md`](apps/claude-code-plugin/SETUP.md) を読み、必要な秘密情報だけを尋ね、エンドツーエンドで設定を完了します。 +Claude Code は [`apps/agent-plugin/SETUP.md`](apps/agent-plugin/SETUP.md) を読み、必要な秘密情報だけを尋ね、エンドツーエンドで設定を完了します。 #### 手動セットアップ diff --git a/apps/README.md b/apps/README.md index 832a10980..35b5c9cfb 100644 --- a/apps/README.md +++ b/apps/README.md @@ -7,10 +7,14 @@ First-party setup flows that connect PowerMem to AI clients and IDEs. Every path | If you use… | Use this directory | One-line agent prompt | |-------------|-------------------|------------------------| | **Cursor**, **VS Code**, **Windsurf**, **GitHub Copilot**, **Qoder** | [`vscode-extension/`](vscode-extension/) | `Read and follow apps/vscode-extension/SETUP.md to setup PowerMem` | -| **Claude Desktop**, **Cline**, **Codex**, **OpenCode**, Roo Code, Goose, or any other MCP client | [`mcp-client/`](mcp-client/) | `Read and follow apps/mcp-client/SETUP.md to setup PowerMem` | -| **Claude Code** (hook-based plugin) | [`claude-code-plugin/`](claude-code-plugin/) | `Read and follow apps/claude-code-plugin/SETUP.md to set up PowerMem memory for Claude Code.` | +| **Claude Desktop**, **Cline**, **OpenCode**, Roo Code, Goose, or any other MCP client | [`mcp-client/`](mcp-client/) | `Read and follow apps/mcp-client/SETUP.md to setup PowerMem` | +| **Codex CLI** | [`agent-plugin/`](agent-plugin/) | Install `memory-powermem` with `codex plugin`, then ask Codex to use the init skill. | +| **Claude Code** (hook-based plugin) | [`agent-plugin/`](agent-plugin/) | `Read and follow apps/agent-plugin/SETUP.md to set up PowerMem memory for Claude Code.` | -> **Codex** and **OpenCode** belong under `mcp-client/`, not `vscode-extension/`. The VS Code extension flow is for VS Code–compatible IDEs only. +> **Codex CLI** has a native plugin path under `agent-plugin/`; generic +> MCP-only Codex setup can still use `mcp-client/`. **OpenCode** belongs under +> `mcp-client/`, not `vscode-extension/`. The VS Code extension flow is for +> VS Code–compatible IDEs only. For **OpenClaw**, use the separate [`memory-powermem`](https://github.com/ob-labs/memory-powermem) plugin — see [OpenClaw integration](../docs/integrations/openclaw.md). @@ -19,8 +23,8 @@ For **OpenClaw**, use the separate [`memory-powermem`](https://github.com/ob-lab | Directory | Description | |-----------|-------------| | **[vscode-extension](vscode-extension/)** | VS Code extension and agent-guided setup for Cursor, VS Code, Windsurf, GitHub Copilot, and Qoder. Commands: Query memories, Add selection, Quick note, Link to AI tools, Setup, Dashboard. | -| **[mcp-client](mcp-client/)** | Agent-guided setup for generic MCP clients (Claude Desktop, Cline, Codex, OpenCode, and others). Uses `powermem-mcp` directly; prefers SSE on port `8848`. | -| **[claude-code-plugin](claude-code-plugin/)** | Claude Code plugin with **HTTP mode by default** (REST hooks; empty `mcpServers`). Optional **MCP mode** via [`config/mcp-mode.mcp.json`](claude-code-plugin/config/mcp-mode.mcp.json). | +| **[mcp-client](mcp-client/)** | Agent-guided setup for generic MCP clients (Claude Desktop, Cline, OpenCode, and others). Uses `powermem-mcp` directly; prefers SSE on port `8848`. | +| **[agent-plugin](agent-plugin/)** | Claude Code and Codex CLI plugin descriptors. Claude Code uses **HTTP mode by default** (REST hooks; empty `mcpServers`). Codex CLI uses skills plus explicit `codex mcp add`. Optional Claude **MCP mode** via [`config/mcp-mode.mcp.json`](agent-plugin/config/mcp-mode.mcp.json). | ## Quick start @@ -52,7 +56,7 @@ All setup flows share the same backend priority: 3. **Fall back** to MCP-only only when HTTP is unavailable: `powermem-mcp sse 8848` (or streamable HTTP / stdio when the target client requires it) -The `mcp-client/` path uses `powermem-mcp` directly and prefers SSE on port `8848`. The `vscode-extension/` path prefers the HTTP API and links the current IDE/client first. The `claude-code-plugin/` path defaults to HTTP hooks and optionally enables in-chat MCP tools. +The `mcp-client/` path uses `powermem-mcp` directly and prefers SSE on port `8848`. The `vscode-extension/` path prefers the HTTP API and links the current IDE/client first. The `agent-plugin/` path defaults to HTTP hooks for Claude Code, and provides a Codex CLI plugin that manages init/status/stop/reset skills while MCP is added explicitly with `codex mcp add`. ## Setup & uninstall guides @@ -60,7 +64,7 @@ The `mcp-client/` path uses `powermem-mcp` directly and prefers SSE on port `884 |-----------|-------|-----------|---------| | `vscode-extension/` | [SETUP.md](vscode-extension/SETUP.md) | [UNINSTALL.md](vscode-extension/UNINSTALL.md) | [README.md](vscode-extension/README.md) | | `mcp-client/` | [SETUP.md](mcp-client/SETUP.md) | [UNINSTALL.md](mcp-client/UNINSTALL.md) | — | -| `claude-code-plugin/` | [SETUP.md](claude-code-plugin/SETUP.md) | [UNINSTALL.md](claude-code-plugin/UNINSTALL.md) | [README.md](claude-code-plugin/README.md) | +| `agent-plugin/` | [SETUP.md](agent-plugin/SETUP.md) | [UNINSTALL.md](agent-plugin/UNINSTALL.md) | [README.md](agent-plugin/README.md) | ## Per-client manual guides diff --git a/apps/claude-code-plugin/.claude-plugin/marketplace.json b/apps/agent-plugin/.claude-plugin/marketplace.json similarity index 62% rename from apps/claude-code-plugin/.claude-plugin/marketplace.json rename to apps/agent-plugin/.claude-plugin/marketplace.json index db203b4cc..edc7a2611 100644 --- a/apps/claude-code-plugin/.claude-plugin/marketplace.json +++ b/apps/agent-plugin/.claude-plugin/marketplace.json @@ -5,16 +5,16 @@ "email": "open_oceanbase@oceanbase.com" }, "metadata": { - "description": "PowerMem marketplace: intelligent memory for Claude Code (Ebbinghaus decay, multi-agent, HTTP hooks).", + "description": "PowerMem marketplace: intelligent memory for Claude Code and Codex CLI.", "version": "0.1.0" }, "plugins": [ { "name": "memory-powermem", "source": "./", - "description": "PowerMem intelligent memory for Claude Code: auto-recall on prompt submit and auto-save on session end via HTTP hooks. Ebbinghaus decay and multi-agent support.", + "description": "PowerMem intelligent memory for Claude Code and Codex CLI: Claude Code uses HTTP hooks; Codex CLI uses skills with explicit MCP wiring.", "category": "memory", - "tags": ["memory", "powermem", "rag", "hooks", "oceanbase"] + "tags": ["memory", "powermem", "rag", "hooks", "mcp", "codex", "oceanbase"] } ] } diff --git a/apps/claude-code-plugin/.claude-plugin/plugin.json b/apps/agent-plugin/.claude-plugin/plugin.json similarity index 100% rename from apps/claude-code-plugin/.claude-plugin/plugin.json rename to apps/agent-plugin/.claude-plugin/plugin.json diff --git a/apps/agent-plugin/.codex-plugin/plugin.json b/apps/agent-plugin/.codex-plugin/plugin.json new file mode 100644 index 000000000..0a72908a9 --- /dev/null +++ b/apps/agent-plugin/.codex-plugin/plugin.json @@ -0,0 +1,31 @@ +{ + "name": "memory-powermem", + "version": "0.1.0", + "description": "PowerMem intelligent memory for Codex CLI and Codex app: initialize a local backend, auto-recall through hooks, and use memory skills with optional MCP tools.", + "author": { + "name": "OceanBase / PowerMem", + "email": "open_oceanbase@oceanbase.com", + "url": "https://github.com/oceanbase" + }, + "homepage": "https://github.com/oceanbase/powermem/tree/main/apps/agent-plugin", + "repository": "https://github.com/oceanbase/powermem", + "license": "Apache-2.0", + "keywords": ["memory", "powermem", "codex", "mcp", "oceanbase"], + "skills": "./skills/", + "hooks": "./hooks/codex-hooks.json", + "interface": { + "displayName": "PowerMem", + "shortDescription": "Persistent memory for Codex.", + "longDescription": "Initialize and connect a local PowerMem backend for persistent recall, memory writes, and project decisions from Codex CLI and Codex app.", + "developerName": "OceanBase / PowerMem", + "category": "Developer Tools", + "capabilities": ["Interactive", "Read", "Write"], + "websiteURL": "https://github.com/oceanbase/powermem", + "defaultPrompt": [ + "Initialize PowerMem for Codex.", + "Search my PowerMem memories for this task.", + "Remember this project decision in PowerMem." + ], + "brandColor": "#2563EB" + } +} diff --git a/apps/claude-code-plugin/.gitignore b/apps/agent-plugin/.gitignore similarity index 100% rename from apps/claude-code-plugin/.gitignore rename to apps/agent-plugin/.gitignore diff --git a/apps/claude-code-plugin/.mcp.json b/apps/agent-plugin/.mcp.json similarity index 100% rename from apps/claude-code-plugin/.mcp.json rename to apps/agent-plugin/.mcp.json diff --git a/apps/claude-code-plugin/CHANGELOG.md b/apps/agent-plugin/CHANGELOG.md similarity index 73% rename from apps/claude-code-plugin/CHANGELOG.md rename to apps/agent-plugin/CHANGELOG.md index 70c29ee8f..f16800fb1 100644 --- a/apps/claude-code-plugin/CHANGELOG.md +++ b/apps/agent-plugin/CHANGELOG.md @@ -3,6 +3,12 @@ ## Unreleased - **UserPromptSubmit:** `POWERMEM_PROMPT_SEARCH` defaults to **on** (`POST /api/v1/memories/search` + `additionalContext` per prompt). Set `0`, `false`, `no`, or `off` to disable. +- **Codex hooks:** `.codex-plugin/plugin.json` now bundles `hooks/codex-hooks.json` + for `SessionStart`, `UserPromptSubmit`, `Stop`, and opt-in `PostToolUse` + integration with the shared `powermem-hook` runner. +- **Codex controls:** `POWERMEM_CODEX_SESSION_SEARCH=0` disables session-start + recall, `POWERMEM_CODEX_STOP_SAVE=0` disables stop-summary writes, and + `POWERMEM_CODEX_POST_TOOL_SAVE=1` opts into tool-use summary writes. ## 0.1.0 @@ -27,4 +33,4 @@ Initial release of the PowerMem plugin for Claude Code. - Optional workspace file poller: `sh hooks/run-hook.sh poll` (see `watcher/README.md`). - Windows: `hooks/hooks.windows.example.json` + PowerShell `run-hook.ps1` when `sh` is unavailable. -- Packaging: `scripts/package-plugin.sh` / `make package-claude-plugin`; hook binaries via `scripts/build-hook-binaries.sh` (Go 1.22+). +- Packaging: `scripts/package-plugin.sh` / `make package-agent-plugin`; hook binaries via `scripts/build-hook-binaries.sh` (Go 1.22+). diff --git a/apps/claude-code-plugin/README.md b/apps/agent-plugin/README.md similarity index 65% rename from apps/claude-code-plugin/README.md rename to apps/agent-plugin/README.md index 1059f978e..d08d766a0 100644 --- a/apps/claude-code-plugin/README.md +++ b/apps/agent-plugin/README.md @@ -1,19 +1,24 @@ -# PowerMem Plugin for Claude Code +# PowerMem Plugin for Claude Code and Codex The full Claude Code integration guide — the auto-setup prompt, manual steps, the two connection modes (HTTP / MCP), hooks, configuration, troubleshooting, and -uninstall — now lives in the docs and is the single source of truth: +uninstall — lives in the docs: **➡ [docs/integrations/claude_code.md](../../docs/integrations/claude_code.md)** -This directory still contains the plugin itself (`.claude-plugin/`, `hooks/`, -`skills/`, `config/`, `.mcp.json`). To load it: +The Codex guide lives here: + +**➡ [docs/integrations/codex.md](../../docs/integrations/codex.md)** + +This directory contains the plugin descriptors and shared runtime files +(`.claude-plugin/`, `.codex-plugin/`, `hooks/`, `skills/`, `config/`, `.mcp.json`). +To load it directly in Claude Code: ```bash -claude --plugin-dir /path/to/powermem/apps/claude-code-plugin +claude --plugin-dir /path/to/powermem/apps/agent-plugin ``` -## Marketplace install +## Claude Code Marketplace Install Once the PowerMem marketplace entry is available, install the Claude Code plugin with: @@ -58,21 +63,64 @@ from the same branch: /reload-plugins ``` +## Codex Install + +Install the Codex marketplace and plugin: + +```bash +codex plugin marketplace add oceanbase/powermem +codex plugin add memory-powermem@powermem +``` + +For branch testing: + +```bash +codex plugin remove memory-powermem 2>/dev/null || true +codex plugin marketplace remove powermem 2>/dev/null || true +codex plugin marketplace add https://github.com//powermem.git --ref +codex plugin add memory-powermem@powermem +``` + +Start a new Codex thread so Codex loads the bundled skills and hooks. Review and +trust the PowerMem hooks when Codex asks, or open `/hooks` and trust them there. +Then ask Codex: + +```text +Use the memory-powermem init skill to initialize PowerMem. +``` + +After init succeeds, wire MCP to the managed server: + +```bash +. "$HOME/.powermem/runtime.env" +codex mcp remove powermem 2>/dev/null || true +codex mcp add powermem --url "${POWERMEM_BASE_URL%/}/mcp" +``` + +The bundled hooks provide automatic recall on `SessionStart` and +`UserPromptSubmit`, plus concise turn-summary saves on `Stop`. `PostToolUse` +summaries are opt-in via `POWERMEM_CODEX_POST_TOOL_SAVE=1`. MCP remains an +explicit setup step so tool access points at the runtime URL written by init. + To pre-download the default local embedding model through ModelScope before starting the server: ```bash -POWERMEM_INIT_PRELOAD_MODEL=1 sh "$CLAUDE_PLUGIN_ROOT/scripts/init.sh" +PLUGIN_ROOT="${CODEX_PLUGIN_ROOT:-${POWERMEM_PLUGIN_ROOT:-}}" +[ -n "$PLUGIN_ROOT" ] || { echo "Codex plugin root not found"; exit 1; } +POWERMEM_INIT_PRELOAD_MODEL=1 sh "$PLUGIN_ROOT/scripts/init.sh" ``` Uninstall: -```text -/plugin uninstall memory-powermem@powermem -/plugin marketplace remove powermem -/reload-plugins +```bash +codex plugin remove memory-powermem 2>/dev/null || true +codex plugin marketplace remove powermem ``` +Start a new Codex thread after uninstalling or reinstalling so Codex reloads the +plugin registry. + ## Troubleshooting — Error Handling Prompt When PowerMem encounters an issue (memory writes fail, search returns nothing, diff --git a/apps/claude-code-plugin/SETUP.md b/apps/agent-plugin/SETUP.md similarity index 94% rename from apps/claude-code-plugin/SETUP.md rename to apps/agent-plugin/SETUP.md index cf13df7f9..36c59c21a 100644 --- a/apps/claude-code-plugin/SETUP.md +++ b/apps/agent-plugin/SETUP.md @@ -1,20 +1,23 @@ -# PowerMem — automated Claude Code setup +# PowerMem — automated Claude Code and Codex CLI setup This file is a **prompt for Claude Code**. Open Claude Code in your terminal and say: -> Read and follow `apps/claude-code-plugin/SETUP.md` to set up PowerMem memory for Claude Code. +> Read and follow `apps/agent-plugin/SETUP.md` to set up PowerMem memory for Claude Code. Claude Code will then run the steps below: detect whether you are in the PowerMem source tree or not, ask you for the few required secrets, and wire PowerMem up as a **globally enabled** plugin so every `claude` session (interactive AND non-interactive `claude -p`) uses it automatically — no per-session `--plugin-dir` flag. +The **Installed plugin initialization** section is shared by Claude Code and +Codex CLI. The later source/developer setup flow is Claude Code specific. + --- ## Installed plugin initialization Use this section when the `memory-powermem` plugin is already installed from a -Claude Code marketplace and the user runs: +Claude Code or Codex marketplace and the user runs the PowerMem init skill: ```text /memory-powermem:init @@ -22,9 +25,10 @@ Claude Code marketplace and the user runs: In this mode, **do not** run the source/developer install flow below: do not build hook binaries, do not stage the plugin, do not run `claude plugin marketplace add`, -do not run `claude plugin install`, and do not build the dashboard. The plugin is -already installed; this section only prepares the PowerMem backend that the plugin -connects to. +do not run `claude plugin install`, do not run `codex plugin marketplace add`, do +not run `codex plugin add`, and do not build the dashboard. The plugin is already +installed; this section only prepares the PowerMem backend that the plugin connects +to. Installed-plugin init ensures `uv` is available, then starts the backend with the uvx-style launcher `uvx --from 'powermem[server,seekdb]' powermem-server`. It does @@ -47,26 +51,47 @@ $HOME/.powermem/ Follow these steps: -**Always use a two-step invocation: discover or reuse `CLAUDE_PLUGIN_ROOT` -first, then run the script.** Never write `VAR=val sh "$VAR/..."` on one line — +**Always use a two-step invocation: discover or reuse `PLUGIN_ROOT` first, then +run the script.** Never write `VAR=val sh "$VAR/..."` on one line — the shell expands `$VAR` before the assignment, producing an empty path. ```bash -# If CLAUDE_PLUGIN_ROOT is not already set, find the plugin root: -if [ -z "${CLAUDE_PLUGIN_ROOT:-}" ]; then - export CLAUDE_PLUGIN_ROOT=$(find ~/.claude/plugins/cache/powermem/memory-powermem -maxdepth 2 -name scripts -type d 2>/dev/null | head -1 | xargs dirname) +# If PLUGIN_ROOT is not already set, reuse the host-provided root if available. +if [ -z "${PLUGIN_ROOT:-}" ]; then + PLUGIN_ROOT="${POWERMEM_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-}}}" +fi + +# If no root variable is available, find the installed plugin copy. +if [ -z "${PLUGIN_ROOT:-}" ]; then + PLUGIN_ROOT=$( + find "$HOME/.claude/plugins" "$HOME/.codex/plugins" \ + -maxdepth 8 -type d -name scripts 2>/dev/null | + while IFS= read -r scripts_dir; do + case "$scripts_dir" in + *memory-powermem*) dirname "$scripts_dir"; break ;; + esac + done + ) fi -sh "$CLAUDE_PLUGIN_ROOT/scripts/..." + +[ -n "${PLUGIN_ROOT:-}" ] || { + echo "PowerMem plugin root not found. Reinstall memory-powermem and retry." + exit 1 +} + +sh "$PLUGIN_ROOT/scripts/..." ``` 1. If the skill was just installed or updated, ask the user to run `/reload-plugins` - first, then retry `/memory-powermem:init`. -2. Run `sh "$CLAUDE_PLUGIN_ROOT/scripts/status.sh"` and inspect whether config, - uv, managed PID, Python versions, and health are present. + first in Claude Code, then retry `/memory-powermem:init`. In Codex CLI, start + a new thread after installing or updating the plugin so Codex loads the updated + skills. +2. Run `sh "$PLUGIN_ROOT/scripts/status.sh"` and inspect whether config, uv, + managed PID, Python versions, and health are present. 3. If `.env` is missing, run init with auto-detection first: ```bash - sh "$CLAUDE_PLUGIN_ROOT/scripts/init.sh" + sh "$PLUGIN_ROOT/scripts/init.sh" ``` The script reads the current process environment first and attempts to derive @@ -85,7 +110,7 @@ sh "$CLAUDE_PLUGIN_ROOT/scripts/..." POWERMEM_INIT_LLM_PROVIDER=anthropic \ POWERMEM_INIT_LLM_MODEL=anthropic/claude-sonnet-4.6 \ POWERMEM_INIT_LLM_API_KEY=... \ - sh "$CLAUDE_PLUGIN_ROOT/scripts/init.sh" + sh "$PLUGIN_ROOT/scripts/init.sh" ``` For a bearer-token gateway, use: @@ -95,7 +120,7 @@ sh "$CLAUDE_PLUGIN_ROOT/scripts/..." POWERMEM_INIT_LLM_MODEL=anthropic/claude-sonnet-4.6 \ POWERMEM_INIT_LLM_AUTH_TOKEN=... \ POWERMEM_INIT_LLM_BASE_URL=https://your-gateway.example.com \ - sh "$CLAUDE_PLUGIN_ROOT/scripts/init.sh" + sh "$PLUGIN_ROOT/scripts/init.sh" ``` Optional variables: @@ -109,10 +134,11 @@ sh "$CLAUDE_PLUGIN_ROOT/scripts/..." `all-MiniLM-L6-v2` embedding model before starting the server. 5. Never print API keys, auth tokens, or other credentials. Mask any secret in summaries. -6. After init succeeds, run `sh "$CLAUDE_PLUGIN_ROOT/scripts/status.sh"` again and +6. After init succeeds, run `sh "$PLUGIN_ROOT/scripts/status.sh"` again and report the base URL. -7. The hook launcher reads `runtime.env`, so once init writes a base URL, prompt - recall and session-save hooks use that backend automatically. +7. Claude Code hooks read `runtime.env`, so once init writes a base URL, prompt + recall and session-save hooks use that backend automatically. Codex CLI should + use the same base URL when adding the MCP server with `codex mcp add`. Installed-plugin model preload uses `uvx --from modelscope python` to download from **ModelScope**, then bridges the files into the HuggingFace hub cache layout. @@ -168,7 +194,7 @@ state and either skip, reuse, or refresh it instead of failing or duplicating wo 1. DETECT CONTEXT. The current directory is the PowerMem source tree if a pyproject.toml here has name = "powermem" (or src/powermem/ and - apps/claude-code-plugin/ both exist). Tell me which path you will take: + apps/agent-plugin/ both exist). Tell me which path you will take: - SOURCE -> build & deploy from this checkout and install the Claude Code plugin GLOBALLY in HTTP mode (hooks -> REST; needs Go 1.22+). - PYPI/MCP -> install PowerMem from PyPI with uv and connect via the @@ -424,8 +450,8 @@ writing. Never silently patch `.env`.** ~/.claude/marketplaces/powermem: DEST="$HOME/.claude/marketplaces/powermem" mkdir -p "$DEST" - rsync -a --delete "/apps/claude-code-plugin/" "$DEST/" - # no rsync? rm -rf "$DEST" && cp -a "/apps/claude-code-plugin/." "$DEST/" + rsync -a --delete "/apps/agent-plugin/" "$DEST/" + # no rsync? rm -rf "$DEST" && cp -a "/apps/agent-plugin/." "$DEST/" The binaries from `make build-claude-hook` must already be on disk before this copy. Re-copy on every re-run so the staged dir tracks your latest build. - Register the marketplace from the STAGED dir (it ships @@ -609,7 +635,7 @@ writing. Never silently patch `.env`.** This file is safe to re-run end to end. The only manual-feeling case is refreshing the cached plugin after you change the plugin or rebuild the Go hooks at the SAME version: rebuild (`make build-claude-hook`), re-copy the result into the staged -marketplace (`rsync -a --delete /apps/claude-code-plugin/ ~/.claude/marketplaces/powermem/`), +marketplace (`rsync -a --delete /apps/agent-plugin/ ~/.claude/marketplaces/powermem/`), then force-refresh the cache with `claude plugin uninstall memory-powermem@powermem` followed by `claude plugin install memory-powermem@powermem --scope user` (or bump the version in .claude-plugin/plugin.json so `claude plugin update memory-powermem` picks it up). @@ -668,13 +694,13 @@ make build-claude-hook #### [E005] Storage Backend Initialization **Problem**: 503 errors on API calls despite server health -**Fix**: the Claude Code plugin defaults to embedded OceanBase/seekdb. Stop the -managed server, remove stale seekdb data only if you accept deleting local memories, and -restart init: +**Fix**: the PowerMem plugin defaults to embedded OceanBase/seekdb. Stop the +managed server, remove stale seekdb data only if you accept deleting local memories, +and restart init: ```bash -sh "$CLAUDE_PLUGIN_ROOT/scripts/stop.sh" +sh "$PLUGIN_ROOT/scripts/stop.sh" rm -rf "$HOME/.powermem/seekdb_data" -sh "$CLAUDE_PLUGIN_ROOT/scripts/init.sh" +sh "$PLUGIN_ROOT/scripts/init.sh" ``` #### [E006] Model Download Timeout @@ -869,7 +895,7 @@ make build-claude-hook # Register marketplace DEST="$HOME/.claude/marketplaces/powermem" mkdir -p "$DEST" -rsync -a --delete "$(pwd)/apps/claude-code-plugin/" "$DEST/" +rsync -a --delete "$(pwd)/apps/agent-plugin/" "$DEST/" claude plugin marketplace add "$DEST" claude plugin install memory-powermem@powermem --scope user diff --git a/apps/claude-code-plugin/UNINSTALL.md b/apps/agent-plugin/UNINSTALL.md similarity index 91% rename from apps/claude-code-plugin/UNINSTALL.md rename to apps/agent-plugin/UNINSTALL.md index 54e1ba186..1e16620f1 100644 --- a/apps/claude-code-plugin/UNINSTALL.md +++ b/apps/agent-plugin/UNINSTALL.md @@ -2,7 +2,7 @@ This file is a **prompt for Claude Code**. Open Claude Code in your terminal and say: -> Read and follow `apps/claude-code-plugin/UNINSTALL.md` to remove PowerMem from Claude Code. +> Read and follow `apps/agent-plugin/UNINSTALL.md` to remove PowerMem from Claude Code. It reverses everything `SETUP.md` did: it unregisters the plugin/MCP server, removes the staged marketplace copy (~/.claude/marketplaces/powermem), stops the PowerMem API server, @@ -25,7 +25,7 @@ confirmation — those steps are gated below. 1. DETECT CONTEXT. The current directory is the PowerMem source tree if a pyproject.toml here has name = "powermem" (or src/powermem/ and - apps/claude-code-plugin/ both exist). Tell me which path applies: + apps/agent-plugin/ both exist). Tell me which path applies: - SOURCE -> global plugin install (HTTP hooks) was used. - PYPI/MCP -> the powermem-mcp server (MCP) was used. If unsure, check both: `claude plugin list` (look for memory-powermem@powermem) @@ -76,13 +76,12 @@ confirmation — those steps are gated below. the integration, and some destroy data: - Build artifacts (SOURCE): delete the compiled hook binaries (rm -rf never errors when the dir is already gone): - rm -rf apps/claude-code-plugin/hooks/bin + rm -rf apps/agent-plugin/hooks/bin (You may also restore the committed default if it drifted: - git checkout -- apps/claude-code-plugin/.mcp.json 2>/dev/null || true) - - Stored memories (DESTRUCTIVE — this erases all my saved memories): the default - embedded seekdb/OceanBase storage mode uses `./seekdb_data/` (or the path in - my .env). SQLite storage mode uses `./sqlite_data/` only if explicitly - configured. + git checkout -- apps/agent-plugin/.mcp.json 2>/dev/null || true) + - Stored memories (DESTRUCTIVE — this erases all my saved memories): the embedded + seekdb/OceanBase data lives in `./seekdb_data/` (or the path in my .env). + For SQLite storage mode, data lives in `./sqlite_data/` when configured. Only delete it if I explicitly say so. - Secrets: do NOT touch my .env unless I explicitly ask. If I do, redact the key in any output. diff --git a/apps/claude-code-plugin/cmd/powermem-hook/detach_unix.go b/apps/agent-plugin/cmd/powermem-hook/detach_unix.go similarity index 100% rename from apps/claude-code-plugin/cmd/powermem-hook/detach_unix.go rename to apps/agent-plugin/cmd/powermem-hook/detach_unix.go diff --git a/apps/claude-code-plugin/cmd/powermem-hook/detach_windows.go b/apps/agent-plugin/cmd/powermem-hook/detach_windows.go similarity index 100% rename from apps/claude-code-plugin/cmd/powermem-hook/detach_windows.go rename to apps/agent-plugin/cmd/powermem-hook/detach_windows.go diff --git a/apps/claude-code-plugin/cmd/powermem-hook/main.go b/apps/agent-plugin/cmd/powermem-hook/main.go similarity index 65% rename from apps/claude-code-plugin/cmd/powermem-hook/main.go rename to apps/agent-plugin/cmd/powermem-hook/main.go index 2e03bd57f..ccda23ff5 100644 --- a/apps/claude-code-plugin/cmd/powermem-hook/main.go +++ b/apps/agent-plugin/cmd/powermem-hook/main.go @@ -1,4 +1,6 @@ -// powermem-hook: Claude Code hook — stdin JSON (SessionEnd / PostCompact) → background HTTP POST to PowerMem. +// powermem-hook: Claude Code and Codex hook runner. +// Claude events push transcripts/compact summaries; Codex events retrieve context +// and can save compact turn summaries to PowerMem. // Cross-platform; zero runtime deps beyond the single binary. package main @@ -30,6 +32,12 @@ func main() { case "worker-compact": workerCompact() return + case "worker-codex-stop": + workerCodexStop() + return + case "worker-codex-tool": + workerCodexTool() + return case "worker-file": workerFile() return @@ -109,10 +117,10 @@ func scrubValue(v any) any { } } -func spawnWorker(mode string, envExtra map[string]string) { +func spawnWorker(mode string, envExtra map[string]string) bool { self, err := os.Executable() if err != nil { - return + return false } env := os.Environ() for k, v := range envExtra { @@ -124,7 +132,7 @@ func spawnWorker(mode string, envExtra map[string]string) { cmd.Stdout = nil cmd.Stderr = nil setDetachedChild(cmd) - _ = cmd.Start() + return cmd.Start() == nil } func stdinHook() { @@ -142,8 +150,14 @@ func stdinHook() { cwd, _ := payload["cwd"].(string) switch event { + case "SessionStart": + handleCodexSessionStart(payload) case "UserPromptSubmit": handleUserPromptSubmit(payload) + case "Stop": + handleCodexStop(payload) + case "PostToolUse": + handleCodexPostToolUse(payload) case "SessionEnd": tp, _ := payload["transcript_path"].(string) if tp == "" { @@ -225,6 +239,33 @@ func promptSearchEnabled() bool { } } +func codexSessionSearchEnabled() bool { + switch strings.ToLower(strings.TrimSpace(os.Getenv("POWERMEM_CODEX_SESSION_SEARCH"))) { + case "0", "false", "no", "off": + return false + default: + return true + } +} + +func codexStopSaveEnabled() bool { + switch strings.ToLower(strings.TrimSpace(os.Getenv("POWERMEM_CODEX_STOP_SAVE"))) { + case "0", "false", "no", "off": + return false + default: + return true + } +} + +func codexPostToolSaveEnabled() bool { + switch strings.ToLower(strings.TrimSpace(os.Getenv("POWERMEM_CODEX_POST_TOOL_SAVE"))) { + case "1", "true", "yes", "on": + return true + default: + return false + } +} + func searchBodyUserID() string { if u := strings.TrimSpace(os.Getenv("POWERMEM_USER_ID")); u != "" { return u @@ -268,6 +309,122 @@ func promptSearchMaxContextChars() int { return n } +func maxCodexSaveChars() int { + const defaultMax = 60000 + s := strings.TrimSpace(os.Getenv("POWERMEM_CODEX_SAVE_MAX_CHARS")) + if s == "" { + return defaultMax + } + n, err := strconv.Atoi(s) + if err != nil || n < 500 { + return defaultMax + } + return n +} + +func truncateText(s string, maxChars int) string { + if maxChars > 0 && len(s) > maxChars { + return s[:maxChars] + "\n..." + } + return s +} + +func codexRunID(sessionID, turnID string) string { + if sessionID == "" { + return turnID + } + if turnID == "" { + return sessionID + } + return sessionID + ":" + turnID +} + +type codexStopWorkerPayload struct { + Message string `json:"message"` + SessionID string `json:"session_id"` + TurnID string `json:"turn_id"` + CWD string `json:"cwd"` + RunID string `json:"run_id"` +} + +type codexToolWorkerPayload struct { + ToolName string `json:"tool_name"` + Input string `json:"input"` + Response string `json:"response"` + SessionID string `json:"session_id"` + TurnID string `json:"turn_id"` + CWD string `json:"cwd"` + RunID string `json:"run_id"` +} + +func writeWorkerPayload(prefix string, payload any) (string, error) { + f, err := os.CreateTemp("", prefix+"-*.json") + if err != nil { + return "", err + } + name := f.Name() + encErr := json.NewEncoder(f).Encode(payload) + closeErr := f.Close() + if encErr != nil { + _ = os.Remove(name) + return "", encErr + } + if closeErr != nil { + _ = os.Remove(name) + return "", closeErr + } + return name, nil +} + +func readWorkerPayload(path string, dest any) bool { + path = strings.TrimSpace(path) + if path == "" { + return false + } + defer os.Remove(path) + data, err := os.ReadFile(path) + if err != nil { + return false + } + return json.Unmarshal(data, dest) == nil +} + +func writeAdditionalContext(eventName, ctx string) { + ctx = strings.TrimSpace(ctx) + if ctx == "" { + return + } + out := map[string]any{ + "hookSpecificOutput": map[string]any{ + "hookEventName": eventName, + "additionalContext": ctx, + }, + } + b, err := json.Marshal(out) + if err != nil { + return + } + _, _ = os.Stdout.Write(b) +} + +func handleCodexSessionStart(payload map[string]any) { + if !codexSessionSearchEnabled() { + return + } + cwd, _ := payload["cwd"].(string) + source, _ := payload["source"].(string) + query := strings.TrimSpace(fmt.Sprintf("Codex session context for cwd %s source %s", cwd, source)) + if query == "" { + query = "Codex session context" + } + ctx, err := searchMemoriesForPrompt(query) + if err != nil || strings.TrimSpace(ctx) == "" { + return + } + maxC := promptSearchMaxContextChars() + writeAdditionalContext("SessionStart", truncateText(ctx, maxC)) +} + func handleUserPromptSubmit(payload map[string]any) { if !promptSearchEnabled() { return @@ -283,19 +440,68 @@ func handleUserPromptSubmit(payload map[string]any) { } maxC := promptSearchMaxContextChars() if len(ctx) > maxC { - ctx = ctx[:maxC] + "\n…" + ctx = ctx[:maxC] + "\n..." } - out := map[string]any{ - "hookSpecificOutput": map[string]any{ - "hookEventName": "UserPromptSubmit", - "additionalContext": ctx, - }, + writeAdditionalContext("UserPromptSubmit", ctx) +} + +func handleCodexStop(payload map[string]any) { + if !codexStopSaveEnabled() { + return } - b, err := json.Marshal(out) + message, _ := payload["last_assistant_message"].(string) + message = strings.TrimSpace(message) + if message == "" { + return + } + sid, _ := payload["session_id"].(string) + turnID, _ := payload["turn_id"].(string) + cwd, _ := payload["cwd"].(string) + runID := codexRunID(sid, turnID) + path, err := writeWorkerPayload("powermem-codex-stop", codexStopWorkerPayload{ + Message: truncateText(message, maxCodexSaveChars()), + SessionID: sid, + TurnID: turnID, + CWD: cwd, + RunID: runID, + }) if err != nil { return } - _, _ = os.Stdout.Write(b) + if !spawnWorker("worker-codex-stop", map[string]string{"POWERMEM_WORKER_CODEX_PAYLOAD_PATH": path}) { + _ = os.Remove(path) + } +} + +func handleCodexPostToolUse(payload map[string]any) { + if !codexPostToolSaveEnabled() { + return + } + toolName, _ := payload["tool_name"].(string) + if strings.TrimSpace(toolName) == "" { + return + } + input, _ := json.Marshal(payload["tool_input"]) + response, _ := json.Marshal(payload["tool_response"]) + sid, _ := payload["session_id"].(string) + turnID, _ := payload["turn_id"].(string) + cwd, _ := payload["cwd"].(string) + runID := codexRunID(sid, turnID) + path, err := writeWorkerPayload("powermem-codex-tool", codexToolWorkerPayload{ + ToolName: toolName, + Input: truncateText(string(input), 16000), + Response: truncateText(string(response), maxCodexSaveChars()), + SessionID: sid, + TurnID: turnID, + CWD: cwd, + RunID: runID, + }) + if err != nil { + return + } + if !spawnWorker("worker-codex-tool", map[string]string{"POWERMEM_WORKER_CODEX_PAYLOAD_PATH": path}) { + _ = os.Remove(path) + } } func searchMemoriesForPrompt(query string) (string, error) { @@ -471,6 +677,89 @@ func workerCompact() { } } +func inferCodexStop() bool { + switch strings.ToLower(strings.TrimSpace(os.Getenv("POWERMEM_INFER_CODEX_STOP"))) { + case "0", "false", "no": + return false + default: + return true + } +} + +func workerCodexStop() { + payload := codexStopWorkerPayload{ + Message: os.Getenv("POWERMEM_WORKER_CODEX_MESSAGE"), + SessionID: os.Getenv("POWERMEM_WORKER_SESSION_ID"), + TurnID: os.Getenv("POWERMEM_WORKER_TURN_ID"), + CWD: os.Getenv("POWERMEM_WORKER_CWD"), + RunID: os.Getenv("POWERMEM_WORKER_RUN_ID"), + } + if path := os.Getenv("POWERMEM_WORKER_CODEX_PAYLOAD_PATH"); path != "" { + if !readWorkerPayload(path, &payload) { + return + } + } + message := strings.TrimSpace(payload.Message) + if message == "" { + return + } + sid := payload.SessionID + turnID := payload.TurnID + cwd := payload.CWD + runID := payload.RunID + content := fmt.Sprintf("Codex turn summary (session_id=%s, turn_id=%s, cwd=%s)\n\n%s", sid, turnID, cwd, message) + if err := postMemory(content, map[string]any{ + "source": "codex-hook", + "kind": "codex-stop-summary", + "session_id": sid, + "turn_id": turnID, + "cwd": cwd, + }, &runID, inferCodexStop()); err != nil { + os.Exit(1) + } +} + +func workerCodexTool() { + payload := codexToolWorkerPayload{ + ToolName: os.Getenv("POWERMEM_WORKER_TOOL_NAME"), + Input: os.Getenv("POWERMEM_WORKER_TOOL_INPUT"), + Response: os.Getenv("POWERMEM_WORKER_TOOL_RESPONSE"), + SessionID: os.Getenv("POWERMEM_WORKER_SESSION_ID"), + TurnID: os.Getenv("POWERMEM_WORKER_TURN_ID"), + CWD: os.Getenv("POWERMEM_WORKER_CWD"), + RunID: os.Getenv("POWERMEM_WORKER_RUN_ID"), + } + if path := os.Getenv("POWERMEM_WORKER_CODEX_PAYLOAD_PATH"); path != "" { + if !readWorkerPayload(path, &payload) { + return + } + } + toolName := strings.TrimSpace(payload.ToolName) + if toolName == "" { + return + } + sid := payload.SessionID + turnID := payload.TurnID + cwd := payload.CWD + runID := payload.RunID + input := strings.TrimSpace(payload.Input) + response := strings.TrimSpace(payload.Response) + if input == "" && response == "" { + return + } + content := fmt.Sprintf("Codex tool use summary (tool=%s, session_id=%s, turn_id=%s, cwd=%s)\n\nInput:\n%s\n\nOutput:\n%s", toolName, sid, turnID, cwd, input, response) + if err := postMemory(content, map[string]any{ + "source": "codex-hook", + "kind": "codex-post-tool-use", + "session_id": sid, + "turn_id": turnID, + "cwd": cwd, + "tool_name": toolName, + }, &runID, false); err != nil { + os.Exit(1) + } +} + func maxFileChars() int { s := strings.TrimSpace(os.Getenv("POWERMEM_HOOK_MAX_CHARS")) if s == "" { diff --git a/apps/claude-code-plugin/cmd/powermem-hook/poll.go b/apps/agent-plugin/cmd/powermem-hook/poll.go similarity index 100% rename from apps/claude-code-plugin/cmd/powermem-hook/poll.go rename to apps/agent-plugin/cmd/powermem-hook/poll.go diff --git a/apps/claude-code-plugin/config/README.md b/apps/agent-plugin/config/README.md similarity index 100% rename from apps/claude-code-plugin/config/README.md rename to apps/agent-plugin/config/README.md diff --git a/apps/claude-code-plugin/config/http-mode.mcp.json b/apps/agent-plugin/config/http-mode.mcp.json similarity index 100% rename from apps/claude-code-plugin/config/http-mode.mcp.json rename to apps/agent-plugin/config/http-mode.mcp.json diff --git a/apps/claude-code-plugin/config/mcp-mode.mcp.json b/apps/agent-plugin/config/mcp-mode.mcp.json similarity index 100% rename from apps/claude-code-plugin/config/mcp-mode.mcp.json rename to apps/agent-plugin/config/mcp-mode.mcp.json diff --git a/apps/claude-code-plugin/go.mod b/apps/agent-plugin/go.mod similarity index 100% rename from apps/claude-code-plugin/go.mod rename to apps/agent-plugin/go.mod diff --git a/apps/claude-code-plugin/hooks/bin/powermem-hook-darwin-amd64 b/apps/agent-plugin/hooks/bin/powermem-hook-darwin-amd64 similarity index 58% rename from apps/claude-code-plugin/hooks/bin/powermem-hook-darwin-amd64 rename to apps/agent-plugin/hooks/bin/powermem-hook-darwin-amd64 index 2ab68f627..6a44c6acd 100755 Binary files a/apps/claude-code-plugin/hooks/bin/powermem-hook-darwin-amd64 and b/apps/agent-plugin/hooks/bin/powermem-hook-darwin-amd64 differ diff --git a/apps/claude-code-plugin/hooks/bin/powermem-hook-darwin-arm64 b/apps/agent-plugin/hooks/bin/powermem-hook-darwin-arm64 similarity index 55% rename from apps/claude-code-plugin/hooks/bin/powermem-hook-darwin-arm64 rename to apps/agent-plugin/hooks/bin/powermem-hook-darwin-arm64 index 25421f13a..94c612fae 100755 Binary files a/apps/claude-code-plugin/hooks/bin/powermem-hook-darwin-arm64 and b/apps/agent-plugin/hooks/bin/powermem-hook-darwin-arm64 differ diff --git a/apps/claude-code-plugin/hooks/bin/powermem-hook-linux-amd64 b/apps/agent-plugin/hooks/bin/powermem-hook-linux-amd64 similarity index 58% rename from apps/claude-code-plugin/hooks/bin/powermem-hook-linux-amd64 rename to apps/agent-plugin/hooks/bin/powermem-hook-linux-amd64 index 7bee9d16c..142a274a5 100755 Binary files a/apps/claude-code-plugin/hooks/bin/powermem-hook-linux-amd64 and b/apps/agent-plugin/hooks/bin/powermem-hook-linux-amd64 differ diff --git a/apps/claude-code-plugin/hooks/bin/powermem-hook-linux-arm64 b/apps/agent-plugin/hooks/bin/powermem-hook-linux-arm64 similarity index 56% rename from apps/claude-code-plugin/hooks/bin/powermem-hook-linux-arm64 rename to apps/agent-plugin/hooks/bin/powermem-hook-linux-arm64 index e4c3e995c..8ed4d9f22 100755 Binary files a/apps/claude-code-plugin/hooks/bin/powermem-hook-linux-arm64 and b/apps/agent-plugin/hooks/bin/powermem-hook-linux-arm64 differ diff --git a/apps/claude-code-plugin/hooks/bin/powermem-hook-windows-amd64.exe b/apps/agent-plugin/hooks/bin/powermem-hook-windows-amd64.exe similarity index 58% rename from apps/claude-code-plugin/hooks/bin/powermem-hook-windows-amd64.exe rename to apps/agent-plugin/hooks/bin/powermem-hook-windows-amd64.exe index 04163e0c8..cd467913d 100755 Binary files a/apps/claude-code-plugin/hooks/bin/powermem-hook-windows-amd64.exe and b/apps/agent-plugin/hooks/bin/powermem-hook-windows-amd64.exe differ diff --git a/apps/agent-plugin/hooks/codex-hooks.json b/apps/agent-plugin/hooks/codex-hooks.json new file mode 100644 index 000000000..b6ad84697 --- /dev/null +++ b/apps/agent-plugin/hooks/codex-hooks.json @@ -0,0 +1,59 @@ +{ + "description": "PowerMem lifecycle hooks for Codex CLI and Codex app. SessionStart and UserPromptSubmit retrieve relevant memories as additionalContext. Stop can save the latest assistant summary. PostToolUse writes only when POWERMEM_CODEX_POST_TOOL_SAVE=1. POWERMEM_BASE_URL defaults to http://localhost:8848.", + "hooks": { + "SessionStart": [ + { + "matcher": "startup|resume|clear|compact", + "hooks": [ + { + "type": "command", + "command": "sh \"${PLUGIN_ROOT}/hooks/run-hook.sh\"", + "commandWindows": "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"& \\\"$env:PLUGIN_ROOT\\\\hooks\\\\run-hook.ps1\\\"\"", + "timeout": 5, + "statusMessage": "Loading PowerMem context" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "sh \"${PLUGIN_ROOT}/hooks/run-hook.sh\"", + "commandWindows": "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"& \\\"$env:PLUGIN_ROOT\\\\hooks\\\\run-hook.ps1\\\"\"", + "timeout": 5, + "statusMessage": "Searching PowerMem" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "sh \"${PLUGIN_ROOT}/hooks/run-hook.sh\"", + "commandWindows": "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"& \\\"$env:PLUGIN_ROOT\\\\hooks\\\\run-hook.ps1\\\"\"", + "timeout": 10, + "statusMessage": "Saving PowerMem memory" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Bash|apply_patch|mcp__.*", + "hooks": [ + { + "type": "command", + "command": "sh \"${PLUGIN_ROOT}/hooks/run-hook.sh\"", + "commandWindows": "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"& \\\"$env:PLUGIN_ROOT\\\\hooks\\\\run-hook.ps1\\\"\"", + "timeout": 5, + "statusMessage": "Checking PowerMem tool context" + } + ] + } + ] + } +} diff --git a/apps/claude-code-plugin/hooks/hooks.json b/apps/agent-plugin/hooks/hooks.json similarity index 100% rename from apps/claude-code-plugin/hooks/hooks.json rename to apps/agent-plugin/hooks/hooks.json diff --git a/apps/claude-code-plugin/hooks/hooks.windows.example.json b/apps/agent-plugin/hooks/hooks.windows.example.json similarity index 100% rename from apps/claude-code-plugin/hooks/hooks.windows.example.json rename to apps/agent-plugin/hooks/hooks.windows.example.json diff --git a/apps/agent-plugin/hooks/run-hook.ps1 b/apps/agent-plugin/hooks/run-hook.ps1 new file mode 100644 index 000000000..83f2e6e17 --- /dev/null +++ b/apps/agent-plugin/hooks/run-hook.ps1 @@ -0,0 +1,43 @@ +# PowerShell launcher for Windows native agent hooks without Git Bash. +# Claude Code manual setup can merge commands from hooks/hooks.windows.example.json. +$Root = Split-Path -Parent $MyInvocation.MyCommand.Path +$PluginRoot = Split-Path -Parent $Root +$DataDir = if ($env:POWERMEM_DATA_DIR) { $env:POWERMEM_DATA_DIR } else { Join-Path $HOME ".powermem" } + +function Import-PowerMemEnvFile { + param([string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { + return + } + Get-Content -LiteralPath $Path | ForEach-Object { + $line = $_.Trim() + if (-not $line -or $line.StartsWith("#")) { + return + } + if ($line -match '^(?:export\s+)?([^=\s]+)=(.*)$') { + $name = $matches[1] + $value = $matches[2].Trim() + if ( + ($value.StartsWith('"') -and $value.EndsWith('"')) -or + ($value.StartsWith("'") -and $value.EndsWith("'")) + ) { + $value = $value.Substring(1, $value.Length - 2) + } + [Environment]::SetEnvironmentVariable($name, $value, "Process") + } + } +} + +Import-PowerMemEnvFile (Join-Path $DataDir "runtime.env") +Import-PowerMemEnvFile (Join-Path $PluginRoot "config\runtime.env") + +$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' } +$exe = Join-Path $Root "bin\powermem-hook-windows-$arch.exe" +if (-not (Test-Path $exe)) { + $exe = Join-Path $Root "bin\powermem-hook-windows-amd64.exe" +} +if (-not (Test-Path $exe)) { + exit 0 +} +& $exe @args +exit $LASTEXITCODE diff --git a/apps/claude-code-plugin/hooks/run-hook.sh b/apps/agent-plugin/hooks/run-hook.sh similarity index 100% rename from apps/claude-code-plugin/hooks/run-hook.sh rename to apps/agent-plugin/hooks/run-hook.sh diff --git a/apps/claude-code-plugin/init-flow.png b/apps/agent-plugin/init-flow.png similarity index 100% rename from apps/claude-code-plugin/init-flow.png rename to apps/agent-plugin/init-flow.png diff --git a/apps/claude-code-plugin/init-flow.svg b/apps/agent-plugin/init-flow.svg similarity index 100% rename from apps/claude-code-plugin/init-flow.svg rename to apps/agent-plugin/init-flow.svg diff --git a/apps/claude-code-plugin/scripts/apply-connection-mode.sh b/apps/agent-plugin/scripts/apply-connection-mode.sh similarity index 100% rename from apps/claude-code-plugin/scripts/apply-connection-mode.sh rename to apps/agent-plugin/scripts/apply-connection-mode.sh diff --git a/apps/claude-code-plugin/scripts/build-hook-binaries.sh b/apps/agent-plugin/scripts/build-hook-binaries.sh similarity index 100% rename from apps/claude-code-plugin/scripts/build-hook-binaries.sh rename to apps/agent-plugin/scripts/build-hook-binaries.sh diff --git a/apps/claude-code-plugin/scripts/common.sh b/apps/agent-plugin/scripts/common.sh similarity index 100% rename from apps/claude-code-plugin/scripts/common.sh rename to apps/agent-plugin/scripts/common.sh diff --git a/apps/claude-code-plugin/scripts/init.sh b/apps/agent-plugin/scripts/init.sh similarity index 99% rename from apps/claude-code-plugin/scripts/init.sh rename to apps/agent-plugin/scripts/init.sh index 2c0e2c0e8..e9102539e 100755 --- a/apps/claude-code-plugin/scripts/init.sh +++ b/apps/agent-plugin/scripts/init.sh @@ -5,7 +5,7 @@ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd) # shellcheck disable=SC1091 . "$SCRIPT_DIR/common.sh" -echo "PowerMem Claude Code plugin init" +echo "PowerMem agent plugin init" echo "Data dir: $DATA_DIR" base_url=$(runtime_base_url) @@ -300,7 +300,7 @@ server_log_file = env_first("POWERMEM_SERVER_LOG_FILE") or path_value("powermem- logging_level = env_first("LOGGING_LEVEL") or "INFO" lines = [ - "# Generated by the PowerMem Claude Code plugin.", + "# Generated by the PowerMem agent plugin.", "", "# Core paths", f"POWERMEM_DATA_DIR={data_dir}", diff --git a/apps/claude-code-plugin/scripts/package-plugin.sh b/apps/agent-plugin/scripts/package-plugin.sh similarity index 76% rename from apps/claude-code-plugin/scripts/package-plugin.sh rename to apps/agent-plugin/scripts/package-plugin.sh index df87169ea..9fcff382c 100755 --- a/apps/claude-code-plugin/scripts/package-plugin.sh +++ b/apps/agent-plugin/scripts/package-plugin.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Build a portable zip of the Claude Code plugin for sharing or offline install. -# Output: apps/claude-code-plugin/dist/powermem-claude-code-plugin-.zip +# Build a portable zip of the PowerMem agent plugin for sharing or offline install. +# Output: apps/agent-plugin/dist/powermem-agent-plugin-.zip set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -8,7 +8,7 @@ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" DIST="${PLUGIN_ROOT}/dist" VERSION="$(python3 -c "import json; print(json.load(open('${PLUGIN_ROOT}/.claude-plugin/plugin.json'))['version'])")" -ZIP_NAME="powermem-claude-code-plugin-${VERSION}.zip" +ZIP_NAME="powermem-agent-plugin-${VERSION}.zip" ZIP_PATH="${DIST}/${ZIP_NAME}" if ! command -v zip >/dev/null 2>&1; then @@ -23,10 +23,11 @@ TMP="$(mktemp -d)" cleanup() { rm -rf "${TMP}"; } trap cleanup EXIT -STAGE="${TMP}/powermem-claude-code-plugin" +STAGE="${TMP}/powermem-agent-plugin" mkdir -p "${STAGE}" cp -R "${PLUGIN_ROOT}/.claude-plugin" "${STAGE}/" +[[ -d "${PLUGIN_ROOT}/.codex-plugin" ]] && cp -R "${PLUGIN_ROOT}/.codex-plugin" "${STAGE}/" cp "${PLUGIN_ROOT}/.mcp.json" "${STAGE}/" [[ -d "${PLUGIN_ROOT}/config" ]] && cp -R "${PLUGIN_ROOT}/config" "${STAGE}/" cp -R "${PLUGIN_ROOT}/hooks" "${STAGE}/" @@ -39,7 +40,7 @@ cp -R "${PLUGIN_ROOT}/skills" "${STAGE}/" [[ -d "${PLUGIN_ROOT}/cmd" ]] && cp -R "${PLUGIN_ROOT}/cmd" "${STAGE}/" [[ -d "${PLUGIN_ROOT}/scripts" ]] && cp -R "${PLUGIN_ROOT}/scripts" "${STAGE}/" -( cd "${TMP}" && zip -r "${ZIP_PATH}" "powermem-claude-code-plugin" -x "*.DS_Store" -x "*__pycache__*" -x "*.pyc" ) +( cd "${TMP}" && zip -r "${ZIP_PATH}" "powermem-agent-plugin" -x "*.DS_Store" -x "*__pycache__*" -x "*.pyc" ) echo "Packaged: ${ZIP_PATH}" ls -lh "${ZIP_PATH}" diff --git a/apps/claude-code-plugin/scripts/preload-model.sh b/apps/agent-plugin/scripts/preload-model.sh similarity index 100% rename from apps/claude-code-plugin/scripts/preload-model.sh rename to apps/agent-plugin/scripts/preload-model.sh diff --git a/apps/claude-code-plugin/scripts/reset.sh b/apps/agent-plugin/scripts/reset.sh similarity index 100% rename from apps/claude-code-plugin/scripts/reset.sh rename to apps/agent-plugin/scripts/reset.sh diff --git a/apps/claude-code-plugin/scripts/status.sh b/apps/agent-plugin/scripts/status.sh similarity index 97% rename from apps/claude-code-plugin/scripts/status.sh rename to apps/agent-plugin/scripts/status.sh index c85683cc3..00a6e6bf2 100755 --- a/apps/claude-code-plugin/scripts/status.sh +++ b/apps/agent-plugin/scripts/status.sh @@ -7,7 +7,7 @@ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd) base_url=$(runtime_base_url) -echo "PowerMem Claude Code plugin status" +echo "PowerMem agent plugin status" echo "Data dir: $DATA_DIR" echo "Runtime file: $RUNTIME_FILE" echo "Env file: $ENV_FILE" diff --git a/apps/claude-code-plugin/scripts/stop.sh b/apps/agent-plugin/scripts/stop.sh similarity index 100% rename from apps/claude-code-plugin/scripts/stop.sh rename to apps/agent-plugin/scripts/stop.sh diff --git a/apps/claude-code-plugin/skills/init/SKILL.md b/apps/agent-plugin/skills/init/SKILL.md similarity index 72% rename from apps/claude-code-plugin/skills/init/SKILL.md rename to apps/agent-plugin/skills/init/SKILL.md index 179d0f9fe..f40b40ed5 100644 --- a/apps/claude-code-plugin/skills/init/SKILL.md +++ b/apps/agent-plugin/skills/init/SKILL.md @@ -1,15 +1,17 @@ --- -description: Initialize PowerMem for Claude Code after the plugin is installed. Use when the user asks to set up, initialize, or repair PowerMem. +name: init +description: Initialize PowerMem for Claude Code or Codex CLI after the plugin is installed. Use when the user asks to set up, initialize, or repair PowerMem. --- -Initialize PowerMem for Claude Code. +Initialize PowerMem for Claude Code or Codex CLI. -Read `apps/claude-code-plugin/SETUP.md`, section "Installed plugin initialization", +Read `apps/agent-plugin/SETUP.md`, section "Installed plugin initialization", and follow only that section. Do not run the source/developer setup flow from `SETUP.md`: do not build hook binaries, do not stage the plugin, do not run `claude plugin marketplace add`, do -not run `claude plugin install`, and do not build the dashboard. +not run `claude plugin install`, do not run `codex plugin marketplace add`, do +not run `codex plugin add`, and do not build the dashboard. Use the plugin scripts as directed by that section: diff --git a/apps/claude-code-plugin/skills/recall/SKILL.md b/apps/agent-plugin/skills/recall/SKILL.md similarity index 96% rename from apps/claude-code-plugin/skills/recall/SKILL.md rename to apps/agent-plugin/skills/recall/SKILL.md index 0ecdffba4..5deeece87 100644 --- a/apps/claude-code-plugin/skills/recall/SKILL.md +++ b/apps/agent-plugin/skills/recall/SKILL.md @@ -1,4 +1,5 @@ --- +name: recall description: Search PowerMem for relevant memories. Use before answering questions about the user, project, or past decisions. --- diff --git a/apps/claude-code-plugin/skills/remember/SKILL.md b/apps/agent-plugin/skills/remember/SKILL.md similarity index 96% rename from apps/claude-code-plugin/skills/remember/SKILL.md rename to apps/agent-plugin/skills/remember/SKILL.md index 35246f046..e2bcf1623 100644 --- a/apps/claude-code-plugin/skills/remember/SKILL.md +++ b/apps/agent-plugin/skills/remember/SKILL.md @@ -1,4 +1,5 @@ --- +name: remember description: Add or update a memory in PowerMem. Use when the user or conversation reveals a fact, preference, or decision that should be remembered across sessions. --- diff --git a/apps/claude-code-plugin/skills/reset/SKILL.md b/apps/agent-plugin/skills/reset/SKILL.md similarity index 57% rename from apps/claude-code-plugin/skills/reset/SKILL.md rename to apps/agent-plugin/skills/reset/SKILL.md index 612c2bc30..2df3d3b7c 100644 --- a/apps/claude-code-plugin/skills/reset/SKILL.md +++ b/apps/agent-plugin/skills/reset/SKILL.md @@ -1,9 +1,14 @@ --- +name: reset description: Reset PowerMem plugin-local data after explicit user confirmation. --- Reset is destructive. First tell the user it will stop the plugin-managed server and delete the plugin data directory containing `.env`, runtime state, logs, pid files, and seekdb data. Only after explicit confirmation, run: -`POWERMEM_RESET_CONFIRM=delete sh "${CLAUDE_PLUGIN_ROOT}/scripts/reset.sh"` +`POWERMEM_RESET_CONFIRM=delete sh "$PLUGIN_ROOT/scripts/reset.sh"` + +Resolve `PLUGIN_ROOT` from `$POWERMEM_PLUGIN_ROOT`, `$CLAUDE_PLUGIN_ROOT`, or +`$CODEX_PLUGIN_ROOT` first. If none is set, find the installed plugin root under +`~/.claude/plugins` or `~/.codex/plugins`. Do not delete project files or unrelated PowerMem servers. diff --git a/apps/agent-plugin/skills/status/SKILL.md b/apps/agent-plugin/skills/status/SKILL.md new file mode 100644 index 000000000..01a1bd192 --- /dev/null +++ b/apps/agent-plugin/skills/status/SKILL.md @@ -0,0 +1,10 @@ +--- +name: status +description: Check whether PowerMem is configured, running, and reachable from Claude Code or Codex CLI. +--- + +Resolve the PowerMem plugin root first, then run `sh "$PLUGIN_ROOT/scripts/status.sh"`. +Use `$POWERMEM_PLUGIN_ROOT`, `$CLAUDE_PLUGIN_ROOT`, or `$CODEX_PLUGIN_ROOT` when +available; otherwise find the installed plugin root under `~/.claude/plugins` or +`~/.codex/plugins`. Report the data directory, base URL, managed server PID if +any, and health state. Do not print `.env` contents. diff --git a/apps/agent-plugin/skills/stop/SKILL.md b/apps/agent-plugin/skills/stop/SKILL.md new file mode 100644 index 000000000..b6d9758e3 --- /dev/null +++ b/apps/agent-plugin/skills/stop/SKILL.md @@ -0,0 +1,8 @@ +--- +name: stop +description: Stop the PowerMem server started by the PowerMem plugin. +--- + +Resolve the PowerMem plugin root first, then run `sh "$PLUGIN_ROOT/scripts/stop.sh"`. +This stops only the server PID tracked in the plugin data directory. Do not kill +unrelated PowerMem processes unless the user explicitly asks. diff --git a/apps/claude-code-plugin/uv-init-flow.svg b/apps/agent-plugin/uv-init-flow.svg similarity index 100% rename from apps/claude-code-plugin/uv-init-flow.svg rename to apps/agent-plugin/uv-init-flow.svg diff --git a/apps/claude-code-plugin/watcher/README.md b/apps/agent-plugin/watcher/README.md similarity index 100% rename from apps/claude-code-plugin/watcher/README.md rename to apps/agent-plugin/watcher/README.md diff --git a/apps/claude-code-plugin/hooks/run-hook.ps1 b/apps/claude-code-plugin/hooks/run-hook.ps1 deleted file mode 100644 index 10bad656d..000000000 --- a/apps/claude-code-plugin/hooks/run-hook.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -# PowerShell launcher for Windows (native Claude Code without Git Bash). -# Merge hooks command into settings — see hooks/hooks.windows.example.json -$Root = Split-Path -Parent $MyInvocation.MyCommand.Path -$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' } -$exe = Join-Path $Root "bin\powermem-hook-windows-$arch.exe" -if (-not (Test-Path $exe)) { - $exe = Join-Path $Root "bin\powermem-hook-windows-amd64.exe" -} -if (-not (Test-Path $exe)) { - exit 0 -} -& $exe @args -exit $LASTEXITCODE diff --git a/apps/claude-code-plugin/skills/status/SKILL.md b/apps/claude-code-plugin/skills/status/SKILL.md deleted file mode 100644 index b6f94ea10..000000000 --- a/apps/claude-code-plugin/skills/status/SKILL.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -description: Check whether PowerMem is configured, running, and reachable from Claude Code. ---- - -Run `sh "${CLAUDE_PLUGIN_ROOT}/scripts/status.sh"` when available. Report the data directory, base URL, managed server PID if any, and health state. Do not print `.env` contents. - diff --git a/apps/claude-code-plugin/skills/stop/SKILL.md b/apps/claude-code-plugin/skills/stop/SKILL.md deleted file mode 100644 index f4ebab7e2..000000000 --- a/apps/claude-code-plugin/skills/stop/SKILL.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -description: Stop the PowerMem server started by the Claude Code plugin. ---- - -Run `sh "${CLAUDE_PLUGIN_ROOT}/scripts/stop.sh"` when available. This stops only the server PID tracked in the plugin data directory. Do not kill unrelated PowerMem processes unless the user explicitly asks. - diff --git a/docker/Dockerfile.claude-hook-regression b/docker/Dockerfile.claude-hook-regression index 2fb15584a..33ca5b85f 100644 --- a/docker/Dockerfile.claude-hook-regression +++ b/docker/Dockerfile.claude-hook-regression @@ -3,9 +3,9 @@ FROM golang:1.24-alpine AS hook-builder ARG TARGETOS=linux ARG TARGETARCH -WORKDIR /src/apps/claude-code-plugin -COPY apps/claude-code-plugin/go.* ./ -COPY apps/claude-code-plugin/cmd ./cmd +WORKDIR /src/apps/agent-plugin +COPY apps/agent-plugin/go.* ./ +COPY apps/agent-plugin/cmd ./cmd RUN CGO_ENABLED=0 GOOS="${TARGETOS:-linux}" GOARCH="${TARGETARCH:-amd64}" \ go build -trimpath -ldflags="-s -w" -o /out/powermem-hook ./cmd/powermem-hook @@ -13,14 +13,14 @@ FROM python:3.11-slim ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ - POWERMEM_HOOK_BIN=/workspace/apps/claude-code-plugin/hooks/bin/powermem-hook-linux-amd64 + POWERMEM_HOOK_BIN=/workspace/apps/agent-plugin/hooks/bin/powermem-hook-linux-amd64 WORKDIR /workspace COPY tests/regression/test_claude_hook_no_llm.py tests/regression/test_claude_hook_no_llm.py COPY tests/regression/fixtures/claude_hook tests/regression/fixtures/claude_hook -COPY --from=hook-builder /out/powermem-hook apps/claude-code-plugin/hooks/bin/powermem-hook-linux-amd64 +COPY --from=hook-builder /out/powermem-hook apps/agent-plugin/hooks/bin/powermem-hook-linux-amd64 -RUN chmod +x apps/claude-code-plugin/hooks/bin/powermem-hook-linux-amd64 +RUN chmod +x apps/agent-plugin/hooks/bin/powermem-hook-linux-amd64 CMD ["python", "tests/regression/test_claude_hook_no_llm.py"] diff --git a/docs/integrations/claude_code.md b/docs/integrations/claude_code.md index 72d888ab7..0d68506ab 100644 --- a/docs/integrations/claude_code.md +++ b/docs/integrations/claude_code.md @@ -1,18 +1,18 @@ # Claude Code -Give [Claude Code](https://code.claude.com) persistent, self-evolving memory through the first-party plugin (`memory-powermem`, under [`apps/claude-code-plugin/`](https://github.com/oceanbase/powermem/tree/main/apps/claude-code-plugin/)). +Give [Claude Code](https://code.claude.com) persistent, self-evolving memory through the first-party plugin (`memory-powermem`, under [`apps/agent-plugin/`](https://github.com/oceanbase/powermem/tree/main/apps/agent-plugin/)). -This page is the single source of truth for the Claude Code integration — the plugin's own [`README.md`](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/README.md) links here. +This page is the single source of truth for the Claude Code integration — the plugin's own [`README.md`](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/README.md) links here. ## Fastest path — let Claude Code set itself up Open Claude Code in your terminal and paste this one line: ```text -Read and follow apps/claude-code-plugin/SETUP.md to set up PowerMem memory for Claude Code. +Read and follow apps/agent-plugin/SETUP.md to set up PowerMem memory for Claude Code. ``` -Claude Code reads [`apps/claude-code-plugin/SETUP.md`](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/SETUP.md) — the canonical automated-setup prompt — which detects whether you are in the PowerMem **source tree** (developer) or anywhere else (**PyPI/MCP user**), asks you for the few required secrets, and wires everything up end-to-end. +Claude Code reads [`apps/agent-plugin/SETUP.md`](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/SETUP.md) — the canonical automated-setup prompt — which detects whether you are in the PowerMem **source tree** (developer) or anywhere else (**installed-plugin / PyPI user**), asks you for the few required secrets, and wires everything up end-to-end. Prefer to wire it by hand? The full plugin reference below covers every option. @@ -22,9 +22,9 @@ Prefer to wire it by hand? The full plugin reference below covers every option. - **Two connection modes** (aligned with the PowerMem VS Code extension). **HTTP mode is the default** (standard): REST-only via hooks, no PowerMem MCP tools in chat. **MCP mode** is optional when you want `search_memories` / `add_memory` in the conversation. See [Configuration](#configuration). - **HTTP mode (default)**: Root `.mcp.json` ships with empty `mcpServers`. Hooks use **`POST /api/v1/memories`** (`POWERMEM_BASE_URL`, default `http://localhost:8848`). -- **MCP mode (optional)**: Copy [`config/mcp-mode.mcp.json`](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/config/mcp-mode.mcp.json) to `.mcp.json` (or run `apply-connection-mode.sh mcp`). Claude gets PowerMem tools over **HTTP** `…/mcp` or **stdio**. +- **MCP mode (optional)**: Copy [`config/mcp-mode.mcp.json`](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/config/mcp-mode.mcp.json) to `.mcp.json` (or run `apply-connection-mode.sh mcp`). Claude gets PowerMem tools over **HTTP** `…/mcp` or **stdio**. - **Skills**: `/memory-powermem:remember` and `/memory-powermem:recall` — effective in **MCP mode**; in default HTTP mode they cannot drive tools. -- **Seamless REST capture**: Hooks run in **both** modes. Optional **file poller** — see [watcher/README.md](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/watcher/README.md). +- **Seamless REST capture**: Hooks run in **both** modes. Optional **file poller** — see [watcher/README.md](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/watcher/README.md). - **Auto-retrieval (no MCP required, on by default)**: The `UserPromptSubmit` hook calls **`POST /api/v1/memories/search`** with the user’s prompt and injects hits via [`additionalContext`](https://code.claude.com/docs/en/hooks#userpromptsubmit). Set **`POWERMEM_PROMPT_SEARCH=0`** (or `false` / `no` / `off`) to disable — saves a search round-trip per turn. Works in **HTTP and MCP** modes. ## Runtime requirements (end users) @@ -34,18 +34,18 @@ Prefer to wire it by hand? The full plugin reference below covers every option. | Claude Code | No | | | MCP tools | No | **Off by default** (HTTP mode). Run `apply-connection-mode.sh mcp` to enable. | | **Hooks** (transcript / compact → HTTP API) | **No** | Native binaries under `hooks/bin/` + `run-hook.sh` (macOS/Linux) or PowerShell on Windows. **`POWERMEM_BASE_URL` defaults to `http://localhost:8848`.** | -| Optional **file poller** | No | Same binary: `sh hooks/run-hook.sh poll` — see [watcher/README.md](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/watcher/README.md). | +| Optional **file poller** | No | Same binary: `sh hooks/run-hook.sh poll` — see [watcher/README.md](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/watcher/README.md). | **macOS / Linux:** default `hooks/hooks.json` runs `sh …/run-hook.sh`. POSIX `sh` is always present. -**Windows (native, no Git Bash):** if `sh` is missing, merge the commands from [`hooks/hooks.windows.example.json`](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/hooks/hooks.windows.example.json) into your Claude `settings.json` so hooks call `powershell.exe -File …/run-hook.ps1`. The zip includes `hooks/bin/powermem-hook-windows-amd64.exe` (add `windows/arm64` to the build script if you need it). +**Windows (native, no Git Bash):** if `sh` is missing, merge the commands from [`hooks/hooks.windows.example.json`](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/hooks/hooks.windows.example.json) into your Claude `settings.json` so hooks call `powershell.exe -File …/run-hook.ps1`. The zip includes `hooks/bin/powermem-hook-windows-amd64.exe` (add `windows/arm64` to the build script if you need it). -**Rebuilding binaries** (developers / CI): Go **1.22+**, then `bash scripts/build-hook-binaries.sh` or `make build-claude-hook` from the repo root. `make package-claude-plugin` builds them automatically before zipping. +**Rebuilding binaries** (developers / CI): Go **1.22+**, then `bash scripts/build-hook-binaries.sh` or `make build-claude-hook` from the repo root. `make package-agent-plugin` builds them automatically before zipping. ## Prerequisites 1. **PowerMem HTTP API** reachable from the machine running Claude (e.g. `powermem-server --port 8848`). Default hooks use **`http://localhost:8848`** — override with `POWERMEM_BASE_URL` for a remote server. -2. **MCP mode only:** additionally expose MCP (same host, usually `/mcp`) or stdio `powermem-mcp`, and switch `.mcp.json` via [`config/mcp-mode.mcp.json`](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/config/mcp-mode.mcp.json). +2. **MCP mode only:** additionally expose MCP (same host, usually `/mcp`) or stdio `powermem-mcp`, and switch `.mcp.json` via [`config/mcp-mode.mcp.json`](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/config/mcp-mode.mcp.json). 3. **Claude Code** (VS Code extension or CLI) with plugin support. ## Manual Installation @@ -103,16 +103,16 @@ uv --version ### Step 4 — Install PowerMem and build the hook binaries -`uv pip install -e '.[server,seekdb]'` provides the `powermem-server` and +`pip install -e '.[server,seekdb]'` provides the `powermem-server` and `pmem` commands plus the zero-config local seekdb path and local embedder. `make build-claude-hook` compiles the native Go hook binaries (requires **Go 1.22+**): ```bash -uv venv venv --python python3.11 -source venv/bin/activate -uv pip install --python "$VIRTUAL_ENV/bin/python" -e '.[server,seekdb]' -make build-claude-hook # outputs apps/claude-code-plugin/hooks/bin/ +python3.11 -m venv .venv +# Activate .venv, then run: +pip install -e '.[server,seekdb]' +make build-claude-hook # outputs apps/agent-plugin/hooks/bin/ ``` ### Step 5 — Start the HTTP API server @@ -126,7 +126,7 @@ powermem-server --host 0.0.0.0 --port 8848 ### Step 6 — Load the plugin into Claude Code ```bash -claude --plugin-dir "$(pwd)/apps/claude-code-plugin" +claude --plugin-dir "$(pwd)/apps/agent-plugin" ``` ### Step 7 — Verify @@ -140,7 +140,7 @@ End the session (or run `/compact`), then look for `POST /api/v1/memories` in th #### Option A: Load from directory (development) ```bash -claude --plugin-dir /path/to/powermem/apps/claude-code-plugin +claude --plugin-dir /path/to/powermem/apps/agent-plugin ``` #### Option B: Install from marketplace @@ -184,31 +184,31 @@ POWERMEM_INIT_PACKAGE='powermem[server,seekdb] @ git+https://github.com/oceanbas From the **powermem repo root**: ```bash -make package-claude-plugin +make package-agent-plugin ``` Or run the script directly: ```bash -bash apps/claude-code-plugin/scripts/package-plugin.sh +bash apps/agent-plugin/scripts/package-plugin.sh ``` -This writes **`apps/claude-code-plugin/dist/powermem-claude-code-plugin-.zip`**. Share that zip (USB, internal artifact server, etc.). +This writes **`apps/agent-plugin/dist/powermem-agent-plugin-.zip`**. Share that zip (USB, internal artifact server, etc.). **On the other computer:** -1. Unzip → you get a folder `powermem-claude-code-plugin/` containing `.claude-plugin/`, `hooks/`, `skills/`, `.mcp.json`, etc. +1. Unzip → you get a folder `powermem-agent-plugin/` containing `.claude-plugin/`, `hooks/`, `skills/`, `.mcp.json`, etc. 2. Point Claude Code at that folder (absolute path recommended): ```bash # Optional: hooks default to http://localhost:8848 if POWERMEM_BASE_URL is unset export POWERMEM_BASE_URL=https://your-team-powermem.example.com # team server only - claude --plugin-dir /path/to/powermem-claude-code-plugin + claude --plugin-dir /path/to/powermem-agent-plugin ``` 3. Requirements on that machine: **no Python**; use **macOS/Linux** `sh` or follow **Windows** PowerShell hooks above. **HTTP API** must be reachable for hooks (and `/mcp` too if you enable MCP mode). -To publish a zip **with MCP enabled by default**, replace root `.mcp.json` with `config/mcp-mode.mcp.json` before `make package-claude-plugin`, or document that users run `apply-connection-mode.sh mcp`. +To publish a zip **with MCP enabled by default**, replace root `.mcp.json` with `config/mcp-mode.mcp.json` before `make package-agent-plugin`, or document that users run `apply-connection-mode.sh mcp`. ## Uninstall and update @@ -222,7 +222,7 @@ How you remove the plugin depends on how you enabled it: | **Zip / copied folder** | Delete the unzipped directory. Stop using `--plugin-dir` pointing at it. | | **Git clone / repo path** | Stop using `--plugin-dir` for that path; remove the clone if you no longer need it. | | **Marketplace / built-in plugin UI** | Run `/plugin uninstall memory-powermem@powermem`, then `/reload-plugins`. To remove the marketplace entry as well, run `/plugin marketplace remove powermem`. | -| **You merged [`hooks/hooks.windows.example.json`](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/hooks/hooks.windows.example.json) into `settings.json`** | Edit `~/.claude/settings.json` or `.claude/settings.json` in the project and remove the `UserPromptSubmit` / `SessionEnd` / `PostCompact` hook entries that call `run-hook.ps1` (or restore a backup). Otherwise hooks keep running even after the plugin folder is deleted. | +| **You merged [`hooks/hooks.windows.example.json`](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/hooks/hooks.windows.example.json) into `settings.json`** | Edit `~/.claude/settings.json` or `.claude/settings.json` in the project and remove the `UserPromptSubmit` / `SessionEnd` / `PostCompact` hook entries that call `run-hook.ps1` (or restore a backup). Otherwise hooks keep running even after the plugin folder is deleted. | The hook binary only **writes** to your PowerMem server; it does not install a system daemon. No separate “service uninstall” is required. @@ -230,8 +230,8 @@ The hook binary only **writes** to your PowerMem server; it does not install a s | Install style | Update steps | |---------------|--------------| -| **Zip** | Download the new `.zip`, replace the old folder (delete the previous `powermem-claude-code-plugin` tree, unzip the new one to the same or a new path), then start Claude with `--plugin-dir` pointing at the new folder. | -| **Repo / `git`** | `git pull` (or fetch the release you want), run `make package-claude-plugin` or `bash scripts/package-plugin.sh` if you need a fresh zip, then restart Claude Code. | +| **Zip** | Download the new `.zip`, replace the old folder (delete the previous `powermem-agent-plugin` tree, unzip the new one to the same or a new path), then start Claude with `--plugin-dir` pointing at the new folder. | +| **Repo / `git`** | `git pull` (or fetch the release you want), run `make package-agent-plugin` or `bash scripts/package-plugin.sh` if you need a fresh zip, then restart Claude Code. | | **Marketplace** | Run `/plugin uninstall memory-powermem@powermem`, reinstall from the marketplace, then run `/reload-plugins`. If the backend package changed, re-run `/memory-powermem:init` so uvx resolves the new PyPI release. | After updating, restart the Claude Code session (or the whole app) so MCP config, skills, and hooks reload. @@ -244,8 +244,8 @@ Same **MCP / HTTP** split as elsewhere in PowerMem. **Standard shipping = HTTP m | Mode | Plugin root `.mcp.json` | Claude in-chat | Silent capture (hooks → REST) | |------|-------------------------|----------------|--------------------------------| -| **HTTP mode (default)** | Empty `mcpServers` — same as [`config/http-mode.mcp.json`](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/config/http-mode.mcp.json) | No PowerMem MCP tools | Yes (`POWERMEM_BASE_URL`, default `http://localhost:8848`) | -| **MCP mode** | Includes `powermem` — [`config/mcp-mode.mcp.json`](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/config/mcp-mode.mcp.json) | Yes — `search_memories`, `add_memory`, … | Yes | +| **HTTP mode (default)** | Empty `mcpServers` — same as [`config/http-mode.mcp.json`](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/config/http-mode.mcp.json) | No PowerMem MCP tools | Yes (`POWERMEM_BASE_URL`, default `http://localhost:8848`) | +| **MCP mode** | Includes `powermem` — [`config/mcp-mode.mcp.json`](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/config/mcp-mode.mcp.json) | Yes — `search_memories`, `add_memory`, … | Yes | **Switch mode** (from the plugin directory): @@ -254,7 +254,7 @@ bash scripts/apply-connection-mode.sh http # restore standard (default) HTTP-on bash scripts/apply-connection-mode.sh mcp # enable in-chat PowerMem tools ``` -Restart Claude Code after changing `.mcp.json`. See [`config/README.md`](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/config/README.md). +Restart Claude Code after changing `.mcp.json`. See [`config/README.md`](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/config/README.md). **Naming note:** In **MCP mode**, `transport: "http"` means “connect to the **MCP** endpoint over HTTP” (`https://host/mcp`), not “replace MCP with REST.” **HTTP mode** means “no MCP entry for PowerMem”; REST is still used by hooks. @@ -295,7 +295,7 @@ This is the **default** root `.mcp.json`. Claude has **no** PowerMem MCP tools; ### Seamless recording (hooks + HTTP API) -The plugin ships [`hooks/hooks.json`](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/hooks/hooks.json), [`hooks/run-hook.sh`](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/hooks/run-hook.sh), and **native** `hooks/bin/powermem-hook-*` (built from [`cmd/powermem-hook`](https://github.com/oceanbase/powermem/tree/main/apps/claude-code-plugin/cmd/powermem-hook/)). When the plugin is enabled, Claude Code merges these hooks: +The plugin ships [`hooks/hooks.json`](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/hooks/hooks.json), [`hooks/run-hook.sh`](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/hooks/run-hook.sh), and **native** `hooks/bin/powermem-hook-*` (built from [`cmd/powermem-hook`](https://github.com/oceanbase/powermem/tree/main/apps/agent-plugin/cmd/powermem-hook/)). When the plugin is enabled, Claude Code merges these hooks: | Hook | What happens | |------|----------------| @@ -353,7 +353,7 @@ export POWERMEM_WATCH_ROOT=/path/to/repo sh hooks/run-hook.sh poll ``` -See [watcher/README.md](https://github.com/oceanbase/powermem/blob/main/apps/claude-code-plugin/watcher/README.md) for environment variables. +See [watcher/README.md](https://github.com/oceanbase/powermem/blob/main/apps/agent-plugin/watcher/README.md) for environment variables. ## Usage diff --git a/docs/integrations/codex.md b/docs/integrations/codex.md index 0142f8248..41693b181 100644 --- a/docs/integrations/codex.md +++ b/docs/integrations/codex.md @@ -1,63 +1,159 @@ # Codex -Connect Codex to PowerMem through MCP. The recommended setup path is the generic [PowerMem MCP client setup](https://github.com/oceanbase/powermem/blob/main/apps/mcp-client/SETUP.md). +Connect Codex CLI or the Codex app to PowerMem with the native +`memory-powermem` plugin, bundled lifecycle hooks, and an explicit MCP server +entry for tools. -## Recommended setup — let your MCP client agent set it up +The Codex plugin installs PowerMem skills and reuses the same backend bootstrap +as the Claude Code plugin. It also bundles Codex hooks from +`apps/agent-plugin/hooks/codex-hooks.json` so new threads can retrieve relevant +memories and save concise turn summaries. MCP is still wired explicitly after +init writes `~/.powermem/runtime.env`. -First download the code and enter the directory: +## Recommended Setup + +Install the PowerMem marketplace and plugin: + +```bash +codex plugin marketplace add oceanbase/powermem +codex plugin add memory-powermem@powermem +``` + +For branch testing from a fork: ```bash -git clone https://github.com/oceanbase/powermem -cd powermem +codex plugin remove memory-powermem 2>/dev/null || true +codex plugin marketplace remove powermem 2>/dev/null || true +codex plugin marketplace add https://github.com//powermem.git --ref +codex plugin add memory-powermem@powermem ``` -Then open the AI agent window where you run Codex and paste this one line: +Start a new Codex thread after installing or updating the plugin so Codex loads +the new skills and hooks. Review and trust the plugin hooks when Codex asks, or +open `/hooks` and trust the PowerMem hook definitions there. Then ask Codex: ```text -Read and follow apps/mcp-client/SETUP.md to setup PowerMem +Use the memory-powermem init skill to initialize PowerMem. ``` -The agent follows [`apps/mcp-client/SETUP.md`](https://github.com/oceanbase/powermem/blob/main/apps/mcp-client/SETUP.md), runs `powermem-mcp` directly, and updates only the Codex MCP configuration. +The init skill prepares shared local state under `~/.powermem/`: -## Prerequisites +```text +~/.powermem/.env +~/.powermem/runtime.env +~/.powermem/powermem.pid +server log under the local state directory +~/.powermem/venv/ +``` + +After init succeeds, wire Codex MCP to the managed server: + +```bash +. "$HOME/.powermem/runtime.env" +codex mcp remove powermem 2>/dev/null || true +codex mcp add powermem --url "${POWERMEM_BASE_URL%/}/mcp" +``` + +## Bundled Hooks + +The plugin manifest points Codex at `hooks/codex-hooks.json`. The hook commands +run `hooks/run-hook.sh`, which executes the packaged `powermem-hook` binary and +reads the runtime endpoint from `~/.powermem/runtime.env`. + +Default behavior: + +- `SessionStart` searches PowerMem for project/session context and injects it as + Codex `additionalContext`. +- `UserPromptSubmit` searches PowerMem for memories relevant to the current + prompt and injects them as `additionalContext`. +- `Stop` saves the latest assistant turn summary to PowerMem. +- `PostToolUse` is registered but does not save tool inputs or outputs unless + explicitly enabled with `POWERMEM_CODEX_POST_TOOL_SAVE=1`. + +Hook environment controls: -- Codex installed and able to read `~/.codex/context.json`. -- A running PowerMem MCP endpoint or local `powermem-mcp` command. -- PowerMem configured with your LLM provider, API key, and model. +```bash +# Disable prompt-time recall. +export POWERMEM_PROMPT_SEARCH=0 + +# Disable SessionStart recall. +export POWERMEM_CODEX_SESSION_SEARCH=0 -## Manual setup +# Disable Stop summary writes. +export POWERMEM_CODEX_STOP_SAVE=0 -Use this section only when you want to wire Codex by hand. +# Opt in to saving selected PostToolUse summaries. +export POWERMEM_CODEX_POST_TOOL_SAVE=1 +``` -### Configure +Codex requires non-managed command hooks to be reviewed and trusted. If a hook +definition changes after an update, open `/hooks` again and trust the new +PowerMem hook hash. + +## Prerequisites -Add PowerMem to `~/.codex/context.json`: +- Codex CLI or Codex app with plugin support. CLI setup uses the + `codex plugin` and `codex mcp` commands. +- A supported Python runtime for the PowerMem backend. The init script creates + `~/.powermem/venv` and installs `powermem` there. +- Anthropic credentials available from the environment or `~/.claude/settings.json`. + `ANTHROPIC_API_KEY` works by itself. `ANTHROPIC_AUTH_TOKEN` must be paired with + `ANTHROPIC_BASE_URL`. -```json -{ - "mcpServers": { - "powermem": { - "url": "http://localhost:8848/mcp" - } - } -} +## Manual MCP Only + +If you do not want the Codex plugin skills, you can still connect Codex as a +plain MCP client: + +```bash +codex mcp add powermem --url http://localhost:8848/mcp ``` -If the PowerMem MCP endpoint requires auth, add the matching header or pass -`POWERMEM_API_KEY` to a stdio MCP command. +Use this only when a PowerMem HTTP server is already running. The native plugin +path is preferred because the `init`, `status`, `stop`, and `reset` skills manage +the local server lifecycle for you. ## Verify -1. Restart Codex so it reloads `~/.codex/context.json`. -2. Confirm the `powermem` MCP server is listed. -3. Add a probe memory with content `PowerMem Codex probe: dragonfruit-zx9`. -4. Search for `dragonfruit-zx9` and confirm Codex receives the result. +1. Confirm the managed server is healthy: + + ```bash + . "$HOME/.powermem/runtime.env" + curl -fsS "${POWERMEM_BASE_URL%/}/api/v1/system/health" + ``` + +2. Confirm Codex has the MCP entry: + + ```bash + codex mcp list + ``` + +3. Open `/hooks` and confirm the PowerMem hooks are trusted. + +4. In Codex, ask it to remember a probe such as + `PowerMem Codex probe: dragonfruit-zx9`, then search for `dragonfruit-zx9`. ## Troubleshooting -- If Codex ignores the config, validate that `~/.codex/context.json` is valid JSON. -- If MCP fails, confirm `http://localhost:8848/mcp` is reachable or switch to stdio MCP. +- If the plugin skills do not appear, start a new Codex thread after + `codex plugin add`. +- If the hooks do not run, open `/hooks`, trust the PowerMem hook definitions, + and start a new thread. +- If MCP uses the wrong port, reload `~/.powermem/runtime.env` and re-run + `codex mcp remove powermem` followed by `codex mcp add`. +- If the server fails to start, read the server log under the local state directory. +- If package installation is slow in CN networks, init detects the current + machine region and adds the Tsinghua PyPI mirror for `pip install`. ## Uninstall -Remove `mcpServers.powermem` from `~/.codex/context.json`. Leave other providers untouched. For agent-guided cleanup, follow [`apps/mcp-client/UNINSTALL.md`](https://github.com/oceanbase/powermem/blob/main/apps/mcp-client/UNINSTALL.md). +Remove the MCP entry and plugin: + +```bash +codex mcp remove powermem +codex plugin remove memory-powermem 2>/dev/null || true +``` + +If your Codex CLI does not expose `plugin remove`, use `codex plugin list` to +inspect the installed plugin and remove it through the CLI version's supported +plugin management command. diff --git a/docs/integrations/overview.md b/docs/integrations/overview.md index a2e1ba487..27949c1e5 100644 --- a/docs/integrations/overview.md +++ b/docs/integrations/overview.md @@ -9,6 +9,9 @@ the local `pmem` CLI) — there are no per-client schema rewrites. - **[Claude Code](./claude_code.md)** — Plugin (`memory-powermem`) with silent HTTP-mode capture via hooks and an optional MCP mode for in-chat `search_memories` / `add_memory` tools. +- **[Codex](./codex.md)** — Native `memory-powermem` plugin with + init/status/stop/reset skills, bundled lifecycle hooks, and explicit + `codex mcp add` wiring to the managed PowerMem server. - **[VS Code](./vs_code.md)** — First-party extension with setup UI, status bar, query/add commands, dashboard, and AI-tool config linking. - **[Cursor](./cursor.md)** — MCP setup through the VS Code extension, writing @@ -22,7 +25,7 @@ the local `pmem` CLI) — there are no per-client schema rewrites. and optional HTTP backend mode. - **[Cline](./cline.md)** — Standard MCP setup for Cline. - **[Generic MCP client](./mcp_client.md)** — Stdio, streamable HTTP, and SSE - setup for Claude Desktop, Cline, Codex, OpenCode, Roo Code, Goose, and other MCP clients. + setup for Claude Desktop, Cline, OpenCode, Roo Code, Goose, and other MCP clients. ## Frameworks & SDKs diff --git a/tests/regression/test_claude_hook_no_llm.py b/tests/regression/test_claude_hook_no_llm.py index b46972dad..3087cdeff 100644 --- a/tests/regression/test_claude_hook_no_llm.py +++ b/tests/regression/test_claude_hook_no_llm.py @@ -1,4 +1,4 @@ -"""No-LLM regression tests for the Claude Code hook binary. +"""No-LLM regression tests for the shared agent hook binary. The suite intentionally uses only the Python standard library so it can run in an isolated Docker container without installing project or test dependencies. @@ -22,7 +22,7 @@ ROOT = Path(__file__).resolve().parents[2] FIXTURES = Path(__file__).resolve().parent / "fixtures" / "claude_hook" -PLUGIN_ROOT = ROOT / "apps" / "claude-code-plugin" +PLUGIN_ROOT = ROOT / "apps" / "agent-plugin" HOOK_BIN_ENV = "POWERMEM_HOOK_BIN" _BUILT_HOOK_BIN: Path | None = None SENTINEL = "Bearer sentinelsecret1049" @@ -325,6 +325,157 @@ def test_user_prompt_submit_can_disable_search(self) -> None: self.assert_no_request("/api/v1/memories/search") self.assert_no_sentinel(result.stdout, result.stderr) + def test_codex_session_start_searches_with_event_context(self) -> None: + with tempfile.TemporaryDirectory() as raw_tmp: + payload = { + "hook_event_name": "SessionStart", + "session_id": "codex-session-1049", + "cwd": "/workspace/powermem", + "source": "startup", + } + result = self.run_hook(payload, Path(raw_tmp)) + + self.assertEqual(result.returncode, 0, result.stderr) + request = self.wait_for_request("/api/v1/memories/search") + + self.assertEqual(request.headers.get("x-api-key"), "hook-api-key") + self.assertEqual(request.body["user_id"], "hook-user") + self.assertEqual(request.body["agent_id"], "hook-agent") + self.assertIn("Codex session context", request.body["query"]) + self.assertIn("/workspace/powermem", request.body["query"]) + self.assert_no_sentinel(request.body, result.stdout, result.stderr) + + output = json.loads(result.stdout) + hook_output = output["hookSpecificOutput"] + self.assertEqual(hook_output["hookEventName"], "SessionStart") + self.assertIn("PowerMem", hook_output["additionalContext"]) + self.assertIn("isolated hook regression suite", hook_output["additionalContext"]) + self.assert_no_sentinel(hook_output["additionalContext"]) + + def test_codex_session_start_can_disable_search(self) -> None: + with tempfile.TemporaryDirectory() as raw_tmp: + payload = { + "hook_event_name": "SessionStart", + "session_id": "codex-session-1049", + "cwd": "/workspace/powermem", + "source": "resume", + } + result = self.run_hook( + payload, + Path(raw_tmp), + POWERMEM_CODEX_SESSION_SEARCH="0", + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "") + self.assert_no_request("/api/v1/memories/search") + self.assert_no_sentinel(result.stdout, result.stderr) + + def test_codex_stop_posts_last_assistant_message(self) -> None: + with tempfile.TemporaryDirectory() as raw_tmp: + payload = { + "hook_event_name": "Stop", + "session_id": "codex-session-1049", + "turn_id": "turn-7", + "cwd": "/workspace/powermem", + "last_assistant_message": "Remembered the Codex hook install path.", + } + result = self.run_hook(payload, Path(raw_tmp)) + + self.assertEqual(result.returncode, 0, result.stderr) + request = self.wait_for_request( + "/api/v1/memories", + kind="codex-stop-summary", + ) + self.wait_for_no_hook_workers() + + self.assertEqual(request.headers.get("x-api-key"), "hook-api-key") + self.assertTrue(request.body["infer"]) + self.assertEqual(request.body["user_id"], "hook-user") + self.assertEqual(request.body["agent_id"], "hook-agent") + self.assertEqual(request.body["run_id"], "codex-session-1049:turn-7") + self.assertIn("Codex turn summary", request.body["content"]) + self.assertIn("Remembered the Codex hook install path.", request.body["content"]) + self.assertEqual(request.body["metadata"]["source"], "codex-hook") + self.assertEqual(request.body["metadata"]["session_id"], "codex-session-1049") + self.assertEqual(request.body["metadata"]["turn_id"], "turn-7") + self.assertEqual(request.body["metadata"]["cwd"], "/workspace/powermem") + self.assert_no_sentinel(request.body, result.stdout, result.stderr) + + def test_codex_stop_can_disable_save(self) -> None: + with tempfile.TemporaryDirectory() as raw_tmp: + payload = { + "hook_event_name": "Stop", + "session_id": "codex-session-1049", + "turn_id": "turn-7", + "cwd": "/workspace/powermem", + "last_assistant_message": "This should not be saved.", + } + result = self.run_hook( + payload, + Path(raw_tmp), + POWERMEM_CODEX_STOP_SAVE="0", + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "") + self.assert_no_request("/api/v1/memories") + self.assert_no_sentinel(result.stdout, result.stderr) + + def test_codex_post_tool_use_opt_in_posts_summary(self) -> None: + with tempfile.TemporaryDirectory() as raw_tmp: + payload = { + "hook_event_name": "PostToolUse", + "session_id": "codex-session-1049", + "turn_id": "turn-8", + "cwd": "/workspace/powermem", + "tool_name": "Bash", + "tool_input": {"command": "python -m pytest tests/regression"}, + "tool_response": {"exit_code": 1, "output": "one assertion failed"}, + } + result = self.run_hook( + payload, + Path(raw_tmp), + POWERMEM_CODEX_POST_TOOL_SAVE="1", + ) + + self.assertEqual(result.returncode, 0, result.stderr) + request = self.wait_for_request( + "/api/v1/memories", + kind="codex-post-tool-use", + ) + self.wait_for_no_hook_workers() + + self.assertEqual(request.headers.get("x-api-key"), "hook-api-key") + self.assertFalse(request.body["infer"]) + self.assertEqual(request.body["run_id"], "codex-session-1049:turn-8") + self.assertIn("Codex tool use summary", request.body["content"]) + self.assertIn("python -m pytest tests/regression", request.body["content"]) + self.assertIn("one assertion failed", request.body["content"]) + self.assertEqual(request.body["metadata"]["source"], "codex-hook") + self.assertEqual(request.body["metadata"]["tool_name"], "Bash") + self.assertEqual(request.body["metadata"]["session_id"], "codex-session-1049") + self.assertEqual(request.body["metadata"]["turn_id"], "turn-8") + self.assert_no_sentinel(request.body, result.stdout, result.stderr) + + def test_codex_post_tool_use_default_does_not_save(self) -> None: + with tempfile.TemporaryDirectory() as raw_tmp: + payload = { + "hook_event_name": "PostToolUse", + "session_id": "codex-session-1049", + "turn_id": "turn-8", + "cwd": "/workspace/powermem", + "tool_name": "Bash", + "tool_input": {"command": "python -m pytest tests/regression"}, + "tool_response": {"exit_code": 0, "output": "ok"}, + } + result = self.run_hook(payload, Path(raw_tmp)) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "") + self.assert_no_request("/api/v1/memories") + self.assert_no_sentinel(result.stdout, result.stderr) + def test_session_end_posts_transcript_and_ignores_bad_transcripts(self) -> None: with tempfile.TemporaryDirectory() as raw_tmp: tmp_path = Path(raw_tmp) diff --git a/tests/unit/test_claude_plugin_uv_install.py b/tests/unit/test_claude_plugin_uv_install.py index c1261ff04..66641bac2 100644 --- a/tests/unit/test_claude_plugin_uv_install.py +++ b/tests/unit/test_claude_plugin_uv_install.py @@ -5,11 +5,11 @@ ROOT = Path(__file__).resolve().parents[2] -COMMON_SH = ROOT / "apps" / "claude-code-plugin" / "scripts" / "common.sh" -INIT_SH = ROOT / "apps" / "claude-code-plugin" / "scripts" / "init.sh" -STOP_SH = ROOT / "apps" / "claude-code-plugin" / "scripts" / "stop.sh" -RUN_HOOK_SH = ROOT / "apps" / "claude-code-plugin" / "hooks" / "run-hook.sh" -SCRIPT_ARG0 = ROOT / "apps" / "claude-code-plugin" / "scripts" / "init.sh" +COMMON_SH = ROOT / "apps" / "agent-plugin" / "scripts" / "common.sh" +INIT_SH = ROOT / "apps" / "agent-plugin" / "scripts" / "init.sh" +STOP_SH = ROOT / "apps" / "agent-plugin" / "scripts" / "stop.sh" +RUN_HOOK_SH = ROOT / "apps" / "agent-plugin" / "hooks" / "run-hook.sh" +SCRIPT_ARG0 = ROOT / "apps" / "agent-plugin" / "scripts" / "init.sh" def run_common(script: str, tmp_path: Path, *, bin_dir: Path | None = None) -> subprocess.CompletedProcess[str]: