diff --git a/.factory-plugin/marketplace.json b/.factory-plugin/marketplace.json new file mode 100644 index 0000000..1805c2a --- /dev/null +++ b/.factory-plugin/marketplace.json @@ -0,0 +1,14 @@ +{ + "name": "agentmemory", + "owner": { + "name": "Rohit Ghumare", + "github": "rohitg00" + }, + "plugins": [ + { + "name": "agentmemory", + "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. Factory Droids integration.", + "source": "./plugin" + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39d38d6..400bd4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,7 +71,7 @@ PRs with commits lacking sign-off will not merge. | `src/health/` | Liveness + readiness + alert thresholds. | | `src/state/` | KV schema, keyed mutex, access log. | | `integrations/` | First-party plugins: `hermes/`, `openclaw/`, `filesystem-watcher/`. | -| `plugin/` | Claude Code plugin (`agentmemory@agentmemory`). | +| `plugin/` | Claude Code / Codex CLI / Factory Droids plugin (`agentmemory@agentmemory`). | | `website/` | Marketing site (Next.js 16). | | `test/` | Vitest test suite. | diff --git a/README.md b/README.md index 4f330c5..195d8dd 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Your coding agent remembers everything. No more re-explaining. Built on iii engine
- Persistent memory for Claude Code, Cursor, Gemini CLI, Codex CLI, Hermes, OpenClaw, pi, OpenCode, and any MCP client. + Persistent memory for Claude Code, Cursor, Gemini CLI, Codex CLI, Factory Droids, Hermes, OpenClaw, pi, OpenCode, and any MCP client.

@@ -152,15 +152,15 @@ agentmemory works with any agent that supports hooks, MCP, or REST API. All agen AgentSDKProvider -REST API
-Any agent
-REST API +Factory Droids
+Factory Droids
+12 hooks + MCP + skills

- Works with any agent that speaks MCP or HTTP. One server, memories shared across all of them. + Works with any agent that speaks MCP or HTTP (including REST-only agents like Aider). One server, memories shared across all of them.

--- @@ -374,6 +374,24 @@ The Codex plugin ships from the same `plugin/` directory as the Claude Code plug Codex's hook engine injects `CLAUDE_PLUGIN_ROOT` into hook subprocesses (per [`codex-rs/hooks/src/engine/discovery.rs`](https://github.com/openai/codex/blob/main/codex-rs/hooks/src/engine/discovery.rs)), so the same hook scripts work across both hosts without duplication. Subagent / SessionEnd / Notification / TaskCompleted / PostToolUseFailure events are Claude-Code-only and are not registered for Codex. +### Factory Droids (Factory plugin marketplace) + +```bash +# 1. start the memory server in a separate terminal +npx @agentmemory/agentmemory + +# 2. install the plugin via the Factory marketplace +droid plugin install agentmemory +``` + +The Factory plugin ships from the `plugin/.factory-plugin/` directory. It registers: + +- `@agentmemory/mcp` as an MCP server (all 51 tools via `plugin/.mcp.json`) +- 12 lifecycle hooks: `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PreCompact`, `SubagentStart`, `SubagentStop`, `Notification`, `TaskCompleted`, `Stop`, `SessionEnd` +- 4 skills: `/recall`, `/remember`, `/session-history`, `/forget` + +Factory's hook engine injects `FACTORY_PLUGIN_ROOT` into hook subprocesses, so the same `plugin/scripts/` hook scripts work across Claude Code, Codex, and Factory Droids without duplication. +
OpenClaw (paste this prompt) @@ -448,6 +466,7 @@ The agentmemory entry is the **same MCP server block** across every host that us | **OpenClaw** | OpenClaw MCP config | Same `mcpServers` block, or use the deeper [memory plugin](integrations/openclaw/). | | **Codex CLI (MCP only)** | `.codex/config.toml` | TOML shape: `codex mcp add agentmemory -- npx -y @agentmemory/mcp`, or add `[mcp_servers.agentmemory]` manually. | | **Codex CLI (full plugin)** | Codex plugin marketplace | `codex plugin marketplace add rohitg00/agentmemory` then `codex plugin install agentmemory`. Registers MCP + 6 lifecycle hooks (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, Stop) + 4 skills. | +| **Factory Droids** | Factory plugin marketplace | `droid plugin install agentmemory`. Registers MCP + 12 lifecycle hooks (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, PreCompact, SubagentStart, SubagentStop, Notification, TaskCompleted, Stop, SessionEnd) + 4 skills via `plugin/.factory-plugin/`. | | **OpenCode** | `opencode.json` | Different shape — top-level `mcp` key, command as array: `{"mcp": {"agentmemory": {"type": "local", "command": ["npx", "-y", "@agentmemory/mcp"], "enabled": true}}}`. | | **pi** | `~/.pi/agent/extensions/agentmemory` | Copy [`integrations/pi`](integrations/pi/) and restart pi. | | **Hermes Agent** | `~/.hermes/config.yaml` | Use the deeper [memory provider plugin](integrations/hermes/) with `memory.provider: agentmemory`. | diff --git a/SECURITY.md b/SECURITY.md index f5980ec..119977f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -42,7 +42,7 @@ In scope: - The `@agentmemory/mcp` standalone MCP server. - The `@agentmemory/fs-watcher` connector. - First-party integrations under `integrations/` (`hermes/`, `openclaw/`, `filesystem-watcher/`). -- The Claude Code plugin under `plugin/`. +- Plugins under `plugin/` (Claude Code, Codex CLI, Factory Droids). Out of scope: diff --git a/plugin/.factory-plugin/plugin.json b/plugin/.factory-plugin/plugin.json new file mode 100644 index 0000000..365d941 --- /dev/null +++ b/plugin/.factory-plugin/plugin.json @@ -0,0 +1,17 @@ +{ + "name": "agentmemory", + "version": "0.9.11", + "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 51 MCP tools, 4 skills, real-time viewer.", + "author": { + "name": "Rohit Ghumare", + "url": "https://github.com/rohitg00" + }, + "license": "Apache-2.0", + "homepage": "https://github.com/rohitg00/agentmemory", + "repository": "https://github.com/rohitg00/agentmemory", + "factoryVersion": ">=0.1.0", + "tags": ["memory", "persistence", "mcp", "knowledge-graph"], + "skills": "../skills/", + "mcpServers": "../.mcp.json", + "hooks": "../hooks/hooks.factory.json" +} diff --git a/plugin/hooks/hooks.factory.json b/plugin/hooks/hooks.factory.json new file mode 100644 index 0000000..941bd20 --- /dev/null +++ b/plugin/hooks/hooks.factory.json @@ -0,0 +1,127 @@ +{ + "hooks": [ + { + "name": "session-start", + "event": "SessionStart", + "description": "Initialize session and inject context", + "enabled": true, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/session-start.mjs\"" + } + }, + { + "name": "user-prompt-submit", + "event": "UserPromptSubmit", + "description": "Capture user prompts", + "enabled": true, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/prompt-submit.mjs\"" + } + }, + { + "name": "pre-tool-use", + "event": "PreToolUse", + "description": "Capture tool intent", + "enabled": true, + "conditions": { + "toolNames": ["Edit", "Write", "Read", "Glob", "Grep", "bash", "run_command", "edit_file", "write_file"] + }, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/pre-tool-use.mjs\"" + } + }, + { + "name": "post-tool-use", + "event": "PostToolUse", + "description": "Capture tool output", + "enabled": true, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/post-tool-use.mjs\"" + } + }, + { + "name": "post-tool-use-failure", + "event": "PostToolUseFailure", + "description": "Capture tool errors", + "enabled": true, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/post-tool-failure.mjs\"" + } + }, + { + "name": "pre-compact", + "event": "PreCompact", + "description": "Hook before memory compaction", + "enabled": true, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/pre-compact.mjs\"" + } + }, + { + "name": "subagent-start", + "event": "SubagentStart", + "description": "Capture subagent execution", + "enabled": true, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/subagent-start.mjs\"" + } + }, + { + "name": "subagent-stop", + "event": "SubagentStop", + "description": "Capture subagent completion", + "enabled": true, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/subagent-stop.mjs\"" + } + }, + { + "name": "notification", + "event": "Notification", + "description": "Capture notifications", + "enabled": true, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/notification.mjs\"" + } + }, + { + "name": "task-completed", + "event": "TaskCompleted", + "description": "Capture task completion", + "enabled": true, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/task-completed.mjs\"" + } + }, + { + "name": "stop", + "event": "Stop", + "description": "Capture stop events", + "enabled": true, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/stop.mjs\"" + } + }, + { + "name": "session-end", + "event": "SessionEnd", + "description": "Consolidate memory at end of session", + "enabled": true, + "action": { + "type": "command", + "command": "node \"${FACTORY_PLUGIN_ROOT}/scripts/session-end.mjs\"" + } + } + ] +} diff --git a/test/factory-plugin.test.ts b/test/factory-plugin.test.ts new file mode 100644 index 0000000..fb1b303 --- /dev/null +++ b/test/factory-plugin.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from "vitest"; +import { existsSync, readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; + +const repoRoot = resolve(__dirname, ".."); +const pluginRoot = join(repoRoot, "plugin"); +const factoryPluginRoot = join(pluginRoot, ".factory-plugin"); + +function readJson(path: string): T { + return JSON.parse(readFileSync(path, "utf-8")) as T; +} + +describe("Factory Droids plugin manifest", () => { + it("ships .factory-plugin/plugin.json with name + version + references", () => { + const manifestPath = join(factoryPluginRoot, "plugin.json"); + expect(existsSync(manifestPath)).toBe(true); + const manifest = readJson<{ + name: string; + version: string; + description?: string; + factoryVersion?: string; + skills?: string; + mcpServers?: string; + hooks?: string; + }>(manifestPath); + expect(manifest.name).toBe("agentmemory"); + expect(manifest.name).toMatch(/^[a-z][a-z0-9-]*$/); + expect(manifest.version).toMatch(/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/); + expect(manifest.factoryVersion).toBeDefined(); + expect(manifest.skills).toBeDefined(); + expect(manifest.mcpServers).toBeDefined(); + expect(manifest.hooks).toBeDefined(); + }); + + it("manifest version matches main package.json", () => { + const pkgVer = readJson<{ version: string }>(join(repoRoot, "package.json")).version; + const factoryVer = readJson<{ version: string }>( + join(factoryPluginRoot, "plugin.json"), + ).version; + expect(factoryVer).toBe(pkgVer); + }); + + it("all referenced manifest paths resolve to existing files / directories", () => { + const manifest = readJson<{ skills: string; mcpServers: string; hooks: string }>( + join(factoryPluginRoot, "plugin.json"), + ); + expect(existsSync(join(factoryPluginRoot, manifest.skills))).toBe(true); + expect(existsSync(join(factoryPluginRoot, manifest.mcpServers))).toBe(true); + expect(existsSync(join(factoryPluginRoot, manifest.hooks))).toBe(true); + }); + + it("hooks.factory.json registers the same first-class lifecycle events as Claude Code", () => { + const hooks = readJson<{ hooks: Array<{ event: string }> }>( + join(pluginRoot, "hooks/hooks.factory.json"), + ); + const events = hooks.hooks.map((hook) => hook.event).sort(); + expect(events).toEqual([ + "Notification", + "PostToolUse", + "PostToolUseFailure", + "PreCompact", + "PreToolUse", + "SessionEnd", + "SessionStart", + "Stop", + "SubagentStart", + "SubagentStop", + "TaskCompleted", + "UserPromptSubmit", + ]); + }); + + it("hook command scripts referenced in hooks.factory.json exist on disk", () => { + const hooks = readJson<{ + hooks: Array<{ action: { type: string; command: string } }>; + }>(join(pluginRoot, "hooks/hooks.factory.json")); + const scriptRefs = new Set(); + for (const hook of hooks.hooks) { + expect(hook.action.type).toBe("command"); + const match = hook.action.command.match( + /^node "\$\{FACTORY_PLUGIN_ROOT\}\/(scripts\/[^"\s]+\.mjs)"$/, + ); + expect(match, `invalid Factory hook command: ${hook.action.command}`).not.toBeNull(); + if (match) scriptRefs.add(match[1]); + } + expect(scriptRefs.size).toBeGreaterThan(0); + for (const rel of scriptRefs) { + expect(existsSync(join(pluginRoot, rel)), `missing hook script: ${rel}`).toBe(true); + } + }); +}); + +describe("Factory marketplace.json (.factory-plugin/marketplace.json at repo root)", () => { + it("ships a marketplace manifest pointing at the plugin/ subdirectory", () => { + const marketplacePath = join(repoRoot, ".factory-plugin/marketplace.json"); + expect(existsSync(marketplacePath)).toBe(true); + const marketplace = readJson<{ + name: string; + plugins: Array<{ + name: string; + source: string; + }>; + }>(marketplacePath); + expect(marketplace.name).toBe("agentmemory"); + expect(marketplace.plugins).toHaveLength(1); + const entry = marketplace.plugins[0]; + expect(entry.name).toBe("agentmemory"); + expect(entry.source).toBe("./plugin"); + }); +});