diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..5c9c85a --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,33 @@ +{ + "name": "continuum", + "owner": { + "name": "00PrabalK00", + "url": "https://github.com/00PrabalK00" + }, + "metadata": { + "description": "Shared project memory for AI coding agents. Switch between Claude, Codex and Gemini without re-explaining your work.", + "version": "0.13.0" + }, + "plugins": [ + { + "name": "continuum", + "source": "./", + "description": "Records what you are working on and hands it to whichever AI you open next. Local files only.", + "version": "0.13.0", + "author": { + "name": "00PrabalK00", + "url": "https://github.com/00PrabalK00" + }, + "homepage": "https://github.com/00PrabalK00/Continuum", + "repository": "https://github.com/00PrabalK00/Continuum", + "license": "MIT", + "keywords": [ + "memory", + "context", + "handoff", + "mcp", + "multi-agent" + ] + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..55bb089 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://anthropic.com/claude-code/plugin.schema.json", + "name": "continuum", + "description": "Shared project memory for AI coding agents. Records what you are working on and hands it to whichever AI you open next.", + "version": "0.13.0", + "author": { + "name": "00PrabalK00", + "url": "https://github.com/00PrabalK00" + }, + "homepage": "https://github.com/00PrabalK00/Continuum", + "repository": "https://github.com/00PrabalK00/Continuum", + "license": "MIT", + "keywords": [ + "memory", + "context", + "handoff", + "mcp", + "multi-agent" + ] +} diff --git a/.gitignore b/.gitignore index 3ff02eb..a93219c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ build/ # Output from unrelated local tooling brag-output/ +brag-output-*/ # Projects the benchmark generates as it runs benchmarks/project/ diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..59b922a --- /dev/null +++ b/.mcp.json @@ -0,0 +1,6 @@ +{ + "continuum": { + "command": "continuum", + "args": ["mcp", "serve", "--project", "${CLAUDE_PROJECT_DIR}"] + } +} diff --git a/README.md b/README.md index 813068d..c6e843f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,18 @@ automatic session summaries. Press enter to take the defaults, or run `continuum install --yes` to skip the questions, which is also what happens when no terminal is attached so scripts and CI keep working. +In Claude Code you can add it as a plugin instead, which registers the MCP +server for you: + +``` +/plugin marketplace add 00PrabalK00/Continuum +/plugin install continuum@continuum +``` + +The plugin still needs the `continuum` command on your PATH, since that is what +it runs. `continuum install` remains the way to reach Codex, Gemini and +everything else. + Now use your AI normally: ``` diff --git a/tests/test_plugin_manifest.py b/tests/test_plugin_manifest.py new file mode 100644 index 0000000..caec831 --- /dev/null +++ b/tests/test_plugin_manifest.py @@ -0,0 +1,72 @@ +"""The Claude Code plugin manifests have to stay true. + +A manifest is only read by someone else's tool, so nothing here fails when it +drifts: the version silently disagrees with the package, or the declared MCP +command stops existing, and the first person to notice is a stranger whose +install did nothing. +""" + +import json +import unittest +from pathlib import Path + +from continuum import __version__ +from continuum.cli import parser + +ROOT = Path(__file__).resolve().parents[1] +PLUGIN = ROOT / ".claude-plugin" / "plugin.json" +MARKETPLACE = ROOT / ".claude-plugin" / "marketplace.json" +MCP = ROOT / ".mcp.json" + + +def load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +class ManifestTest(unittest.TestCase): + def test_both_manifests_are_valid_json(self): + self.assertIsInstance(load(PLUGIN), dict) + self.assertIsInstance(load(MARKETPLACE), dict) + + def test_the_plugin_version_matches_the_package(self): + self.assertEqual(load(PLUGIN)["version"], __version__) + + def test_the_marketplace_entry_matches_the_plugin(self): + entry = load(MARKETPLACE)["plugins"][0] + plugin = load(PLUGIN) + self.assertEqual(entry["name"], plugin["name"]) + self.assertEqual(entry["version"], plugin["version"]) + + def test_the_server_is_declared_where_claude_code_reads_it(self): + # Claude Code reads MCP servers from .mcp.json at the plugin root, not + # from an mcpServers key in plugin.json. Declaring it in plugin.json + # installs cleanly and registers nothing, which is what happened: the + # component inventory reported "MCP servers (0)" while the README said + # installing registers the server. + self.assertTrue(MCP.exists(), ".mcp.json is what actually registers the server") + self.assertNotIn("mcpServers", load(PLUGIN)) + + def test_the_declared_mcp_command_exists(self): + # The manifest promises `continuum mcp serve`. If that subcommand is + # renamed, the plugin installs and then does nothing. + server = load(MCP)["continuum"] + self.assertEqual(server["command"], "continuum") + self.assertEqual(server["args"][:2], ["mcp", "serve"]) + + def test_the_cli_still_has_that_subcommand(self): + root = parser() + commands = next(a for a in root._actions if getattr(a, "choices", None) and "mcp" in a.choices) + mcp = commands.choices["mcp"] + serve = next(a for a in mcp._actions if getattr(a, "choices", None) and "serve" in a.choices) + self.assertIn("serve", serve.choices) + + def test_the_project_directory_is_passed_through(self): + # Without it the server would scope itself to the process working + # directory, which is not necessarily the project being opened. + args = load(MCP)["continuum"]["args"] + self.assertIn("--project", args) + self.assertTrue(any("CLAUDE_PROJECT_DIR" in item for item in args)) + + +if __name__ == "__main__": + unittest.main()