Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
20 changes: 20 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ build/

# Output from unrelated local tooling
brag-output/
brag-output-*/

# Projects the benchmark generates as it runs
benchmarks/project/
Expand Down
6 changes: 6 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"continuum": {
"command": "continuum",
"args": ["mcp", "serve", "--project", "${CLAUDE_PROJECT_DIR}"]
}
}
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment thread
00PrabalK00 marked this conversation as resolved.

```
/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:

```
Expand Down
72 changes: 72 additions & 0 deletions tests/test_plugin_manifest.py
Original file line number Diff line number Diff line change
@@ -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()