diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 00000000..ed9452d9 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "haiku-rag", + "interface": { + "displayName": "haiku.rag" + }, + "plugins": [ + { + "name": "haiku-rag", + "source": { + "source": "local", + "path": "./plugins/haiku-rag" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 24d16eee..93de1bc0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,8 +7,8 @@ "plugins": [ { "name": "haiku-rag", - "source": "./claude-plugin", - "description": "Search, read and question your haiku.rag knowledge base from Claude Code." + "source": "./plugins/haiku-rag", + "description": "Search, read and analyze your haiku.rag knowledge base from Claude Code." } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 81e41e43..3ace83d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,8 @@ ### Added -- Claude Code plugin under `claude-plugin/`: the server configuration and the - `haiku-rag` skill. `claude plugin marketplace add ggozad/haiku.rag`, then - `claude plugin install haiku-rag`. +- Claude Code and Codex plugin under `plugins/haiku-rag/`: two client manifests + sharing the server configuration and the `haiku-rag` Agent Skill. - MCP tool `execute_code(code, filter, sources)`: runs a program in the analysis sandbox over the selected documents and returns what it printed; one sandbox per call. diff --git a/README.md b/README.md index 24d832e3..5970c003 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ For direct agent composition, see the [capabilities documentation](https://ggoza ## MCP Server -Use with AI assistants like Claude Code and Claude Desktop: +Use with AI assistants like Claude Code, Codex, and Claude Desktop: ```bash haiku-rag mcp --stdio @@ -123,6 +123,13 @@ claude plugin marketplace add ggozad/haiku.rag claude plugin install haiku-rag ``` +In Codex, install the same plugin from its marketplace: + +```bash +codex plugin marketplace add ggozad/haiku.rag +codex plugin add haiku-rag@haiku-rag +``` + Add to your Claude Desktop configuration: ```json @@ -136,7 +143,7 @@ Add to your Claude Desktop configuration: } ``` -Provides search, document, QA, and analysis tools directly in your AI assistant. +Provides search, document reading, and analysis tools directly in your AI assistant. ## Examples diff --git a/docs/mcp.md b/docs/mcp.md index 5079ee26..4d268d23 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -57,10 +57,34 @@ plugin: claude mcp add haiku-rag -- haiku-rag mcp --stdio ``` -The skill works with that registration too: copy `claude-plugin/skills/haiku-rag` +The skill works with that registration too: copy `plugins/haiku-rag/skills/haiku-rag` into `~/.claude/skills/` and change the tool prefix in its `allowed-tools` from `mcp__plugin_haiku-rag_haiku-rag__` to `mcp__haiku-rag__`. +## Codex + +The repository's Codex plugin registers the server and installs the same Agent +Skill: + +```bash +codex plugin marketplace add ggozad/haiku.rag +codex plugin add haiku-rag@haiku-rag +``` + +The plugin runs `haiku-rag mcp --stdio`, so `haiku-rag` must be on the PATH. +Invoke the skill as `$haiku-rag`. Codex can also select it automatically from +its description. To register the server without the plugin: + +```bash +codex mcp add haiku-rag -- haiku-rag mcp --stdio +``` + +The skill works with that registration too: copy +`plugins/haiku-rag/skills/haiku-rag` into `~/.agents/skills/`. +The `allowed-tools` field supplies Claude Code's tool pre-approval and may be +ignored by other Agent Skills clients. Codex configures MCP tool approvals +separately in `config.toml`. + ## Claude Desktop Integration Add to your Claude Desktop configuration (`claude_desktop_config.json`): @@ -168,8 +192,8 @@ traceback goes to the server log. The server publishes `instructions` describing the knowledge base: what it holds, when to reach for it, the collection names when it covers several, and -`prompts.domain_preamble` when set. Claude Code shows them to the model. Claude -Desktop does not, so every tool description stands on its own. +`prompts.domain_preamble` when set. Claude Code and Codex show them to the +model. Claude Desktop does not, so every tool description stands on its own. ## Continuous ingestion diff --git a/claude-plugin/.claude-plugin/plugin.json b/plugins/haiku-rag/.claude-plugin/plugin.json similarity index 74% rename from claude-plugin/.claude-plugin/plugin.json rename to plugins/haiku-rag/.claude-plugin/plugin.json index 93282056..cf30ea82 100644 --- a/claude-plugin/.claude-plugin/plugin.json +++ b/plugins/haiku-rag/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "haiku-rag", "version": "0.82.1", - "description": "Search, read and question your haiku.rag knowledge base from Claude Code.", + "description": "Search, read and analyze your haiku.rag knowledge base from Claude Code.", "author": { "name": "Yiorgis Gozadinos", "email": "ggozadinos@gmail.com" diff --git a/plugins/haiku-rag/.codex-plugin/plugin.json b/plugins/haiku-rag/.codex-plugin/plugin.json new file mode 100644 index 00000000..f27c32ac --- /dev/null +++ b/plugins/haiku-rag/.codex-plugin/plugin.json @@ -0,0 +1,26 @@ +{ + "name": "haiku-rag", + "version": "0.82.1", + "description": "Search, read and analyze your haiku.rag knowledge base from Codex.", + "author": { + "name": "Yiorgis Gozadinos", + "email": "ggozadinos@gmail.com", + "url": "https://github.com/ggozad" + }, + "homepage": "https://ggozad.github.io/haiku.rag/mcp/", + "repository": "https://github.com/ggozad/haiku.rag", + "license": "MIT", + "keywords": ["rag", "knowledge-base", "search", "documents", "mcp"], + "skills": "./skills/", + "mcpServers": "./.mcp.json", + "interface": { + "displayName": "haiku.rag", + "shortDescription": "Search and analyze your haiku.rag knowledge base", + "longDescription": "Search, read, and compute over documents in your local haiku.rag knowledge base through MCP tools.", + "developerName": "Yiorgis Gozadinos", + "category": "Productivity", + "capabilities": ["Interactive", "Read"], + "websiteURL": "https://ggozad.github.io/haiku.rag/", + "defaultPrompt": "Search my haiku.rag knowledge base and cite the relevant documents." + } +} diff --git a/claude-plugin/.mcp.json b/plugins/haiku-rag/.mcp.json similarity index 100% rename from claude-plugin/.mcp.json rename to plugins/haiku-rag/.mcp.json diff --git a/claude-plugin/skills/haiku-rag/SKILL.md b/plugins/haiku-rag/skills/haiku-rag/SKILL.md similarity index 97% rename from claude-plugin/skills/haiku-rag/SKILL.md rename to plugins/haiku-rag/skills/haiku-rag/SKILL.md index 981baf43..c513fd2b 100644 --- a/claude-plugin/skills/haiku-rag/SKILL.md +++ b/plugins/haiku-rag/skills/haiku-rag/SKILL.md @@ -5,6 +5,7 @@ description: Search, read and compute over the user's haiku.rag knowledge base from the user's ingested documents, when asked to find, look up, check or cite something in their documents or knowledge base, or when the question is about the user's own material rather than general knowledge. +compatibility: Requires the haiku-rag MCP server to be registered in the client. allowed-tools: - mcp__plugin_haiku-rag_haiku-rag__search_documents - mcp__plugin_haiku-rag_haiku-rag__search_documents_by_image diff --git a/scripts/bump_version.py b/scripts/bump_version.py index 9593cd89..f696c2f0 100755 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -2,8 +2,8 @@ """ Version bumping script for haiku.rag workspace. -Updates version in all pyproject.toml files, the Claude Code plugin manifest -and CHANGELOG.md. +Updates version in all pyproject.toml files, both plugin manifests, and +CHANGELOG.md. """ import re @@ -56,7 +56,7 @@ def update_example_dependencies(file_path: Path, new_version: str) -> None: def update_plugin_version(file_path: Path, new_version: str) -> None: - """Update the version in the Claude Code plugin manifest.""" + """Update the version in a plugin manifest.""" content = file_path.read_text() updated = re.sub( r'^(\s*"version": )"[^"]+"', @@ -136,12 +136,15 @@ def main(): root / "app" / "backend" / "pyproject.toml", ] - plugin_file = root / "claude-plugin" / ".claude-plugin" / "plugin.json" + plugin_files = [ + root / "plugins" / "haiku-rag" / ".claude-plugin" / "plugin.json", + root / "plugins" / "haiku-rag" / ".codex-plugin" / "plugin.json", + ] changelog_file = root / "CHANGELOG.md" # Check all files exist for file in ( - pyproject_files + example_pyproject_files + [plugin_file, changelog_file] + pyproject_files + example_pyproject_files + plugin_files + [changelog_file] ): if not file.exists(): print(f"Error: {file} not found") @@ -172,7 +175,8 @@ def main(): for file in example_pyproject_files: update_example_dependencies(file, new_version) - update_plugin_version(plugin_file, new_version) + for file in plugin_files: + update_plugin_version(file, new_version) # Update CHANGELOG.md update_changelog(changelog_file, new_version) diff --git a/tests/test_bump_version.py b/tests/test_bump_version.py index 4f8fcf4f..4b6fdc27 100644 --- a/tests/test_bump_version.py +++ b/tests/test_bump_version.py @@ -27,12 +27,15 @@ def test_update_plugin_version_rewrites_only_the_version_field(tmp_path, monkeyp assert manifest.read_text().endswith("}\n") -def test_the_shipped_plugin_manifest_carries_the_package_version(): +def test_the_shipped_plugin_manifests_carry_the_package_version(): root = Path(__file__).resolve().parents[1] - plugin = json.loads( - (root / "claude-plugin" / ".claude-plugin" / "plugin.json").read_text() - ) - - assert plugin["version"] == bump_version.get_current_version( + package_version = bump_version.get_current_version( root / "haiku_rag_slim" / "pyproject.toml" ) + for client in ("claude", "codex"): + manifest = json.loads( + ( + root / "plugins" / "haiku-rag" / f".{client}-plugin" / "plugin.json" + ).read_text() + ) + assert manifest["version"] == package_version diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 9a9faee6..18d7106d 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -1158,27 +1158,39 @@ class TestMCPErrorContract: ) -class TestClaudeCodePlugin: - """The plugin under claude-plugin/ points at the server this module builds.""" +class TestAgentPlugins: + """The shared Claude Code and Codex plugin points at this MCP server.""" root = Path(__file__).resolve().parents[1] def test_the_manifests_name_the_plugin_and_its_server(self): import json - plugin = json.loads( - (self.root / "claude-plugin/.claude-plugin/plugin.json").read_text() + claude_plugin = json.loads( + (self.root / "plugins/haiku-rag/.claude-plugin/plugin.json").read_text() ) - marketplace = json.loads( + codex_plugin = json.loads( + (self.root / "plugins/haiku-rag/.codex-plugin/plugin.json").read_text() + ) + claude_marketplace = json.loads( (self.root / ".claude-plugin/marketplace.json").read_text() ) - servers = json.loads((self.root / "claude-plugin/.mcp.json").read_text()) + codex_marketplace = json.loads( + (self.root / ".agents/plugins/marketplace.json").read_text() + ) + servers = json.loads((self.root / "plugins/haiku-rag/.mcp.json").read_text()) - assert plugin["name"] == "haiku-rag" - assert plugin["description"] - [entry] = marketplace["plugins"] - assert entry["name"] == plugin["name"] - assert entry["source"] == "./claude-plugin" + assert claude_plugin["name"] == codex_plugin["name"] == "haiku-rag" + assert claude_plugin["description"] + assert codex_plugin["description"] + assert codex_plugin["skills"] == "./skills/" + assert codex_plugin["mcpServers"] == "./.mcp.json" + [claude_entry] = claude_marketplace["plugins"] + assert claude_entry["name"] == claude_plugin["name"] + assert claude_entry["source"] == "./plugins/haiku-rag" + [codex_entry] = codex_marketplace["plugins"] + assert codex_entry["name"] == codex_plugin["name"] + assert codex_entry["source"]["path"] == "./plugins/haiku-rag" assert servers["mcpServers"]["haiku-rag"]["args"] == ["mcp", "--stdio"] @pytest.mark.asyncio @@ -1187,7 +1199,7 @@ class TestClaudeCodePlugin: ): import yaml - text = (self.root / "claude-plugin/skills/haiku-rag/SKILL.md").read_text() + text = (self.root / "plugins/haiku-rag/skills/haiku-rag/SKILL.md").read_text() _, frontmatter, _ = text.split("---", 2) skill = yaml.safe_load(frontmatter) prefix = "mcp__plugin_haiku-rag_haiku-rag__"