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
6 changes: 3 additions & 3 deletions .github/workflows/publish-claude-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ jobs:
# the freshly-regenerated bundle. --delete removes files that
# no longer exist in the source (e.g. a removed skill). The
# --exclude entries protect repo-level files we don't manage
# from the gortex side: .git/, .github/ (publish target may
# carry its own readme-only CI), and a top-level README/LICENSE
# if the publish target maintains its own.
# from the gortex side: .git/ and .github/ (publish target may
# carry its own readme-only CI). The root README and LICENSE
# are generated as part of the marketplace layout.
rsync -av --delete \
--exclude='.git/' \
--exclude='.github/' \
Expand Down
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,16 @@ build-windows:
# Marketplace plugin bundle
# ---------------------------------------------------------------------------

# claude-plugin regenerates the Anthropic Plugin Marketplace bundle at
# claude-plugin/. The bundle is checked in so the marketplace's
# "git-subdir" source can pull it directly. The CI guard
# claude-plugin regenerates the Claude Code plugin marketplace at
# claude-plugin/: .claude-plugin/marketplace.json plus the plugin under
# plugins/gortex/. publish-claude-plugin.yml syncs it to
# gortexhq/claude-plugin. The CI guard
# (claude-plugin-check) asserts that re-running this target produces
# no diff against what's checked in — drift means the bundle is stale
# vs the source-of-truth content in
# internal/agents/commands_bodies.go.
claude-plugin: build
rm -rf ./claude-plugin
./$(BINARY) plugin emit --target ./claude-plugin --variant anthropic
@echo "✓ Regenerated claude-plugin/ from internal/agents commands"

Expand Down
15 changes: 9 additions & 6 deletions cmd/gortex/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ var pluginCmd = &cobra.Command{
Short: "Manage Gortex marketplace plugin bundles",
Long: `Generate plugin bundles for agent-host marketplaces.

Today this command emits the Anthropic Plugin Marketplace layout —
a directory containing plugin.json, README, LICENSE, .mcp.json,
skills, sub-agents, and hooks. The Cursor variant lands in a
follow-up.
Today this command emits a Claude Code plugin marketplace:
.claude-plugin/marketplace.json, README and LICENSE at the root, and
the gortex plugin under plugins/gortex/ (plugin.json, README, LICENSE,
.mcp.json, skills, sub-agents, and hooks). The Cursor variant lands in
a follow-up.

The marketplace bundle is generated, not hand-maintained. The single
source of truth lives in internal/agents/commands_bodies.go and
Expand All @@ -42,7 +43,9 @@ Example:
gortex plugin emit --target ./claude-plugin --variant anthropic --version 0.18.2

The target directory is created if missing; existing files are
overwritten so re-runs converge on a deterministic output.`,
overwritten so re-runs converge on a deterministic output. A target
that still holds the older flat layout (the plugin at its root) is
refused; remove it or emit into an empty directory.`,
RunE: runPluginEmit,
}

Expand All @@ -67,7 +70,7 @@ func runPluginEmit(_ *cobra.Command, _ []string) error {
if err != nil {
return fmt.Errorf("resolve target dir: %w", err)
}
written, err := claudecode.EmitPluginBundle(claudecode.PluginBundleSpec{
written, err := claudecode.EmitMarketplace(claudecode.PluginBundleSpec{
TargetDir: abs,
Version: v,
LayoutVariant: claudecode.LayoutVariant(pluginEmitVariant),
Expand Down
13 changes: 13 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ xattr -d com.apple.quarantine /usr/local/bin/gortex
gortex version
```

## Claude Code plugin

Gortex also ships as a Claude Code plugin with the MCP server, workflow skills, sub-agents and hooks. With the `gortex` binary installed, add the marketplace and install the plugin:

```bash
claude plugin marketplace add gortexhq/claude-plugin
claude plugin install gortex@gortex
```

The plugin covers Claude Code's MCP server, workflow skills, sub-agents and hooks. Running `gortex install` and `gortex init` for Claude Code as well registers Gortex twice, which duplicates them. To avoid that, pass `--agents-skip claude-code` to both. This also skips the `CLAUDE.md` blocks and the per-repo community skills under `.claude/skills/generated/`, which the plugin does not provide.

The workflows appear as `/gortex:<name>`, for example `/gortex:explore`. The marketplace at [gortexhq/claude-plugin](https://github.com/gortexhq/claude-plugin) is generated from this repository by `gortex plugin emit` on each release.

## System requirements

Every published binary is self-contained — there is no runtime dependency to
Expand Down
2 changes: 2 additions & 0 deletions docs/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Setup is split into two commands so codebase-agnostic machinery lives once per u

You can run them independently — `gortex init` doesn't require `gortex install` first; it just writes less if the user-level wiring isn't there.

Using Claude Code? Gortex also ships as a Claude Code plugin; see [Claude Code plugin](installation.md#claude-code-plugin) for how it fits with these two commands.

```bash
# One-time: machine-wide user-level setup
gortex install --start --track # install, start daemon, track current dir
Expand Down
119 changes: 119 additions & 0 deletions internal/agents/claudecode/marketplace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package claudecode

import (
"errors"
"fmt"
"os"
"path/filepath"
)

// marketplaceName is the id users type after "@" when installing:
// `claude plugin install gortex@gortex`.
const marketplaceName = "gortex"

// marketplacePluginSource is the plugin entry's source, relative to the
// marketplace root. A plugins/<name>/ layout leaves room for more
// plugins in the same marketplace later.
const marketplacePluginSource = "./plugins/" + pluginName

const (
marketplaceDescription = "Gortex plugins for Claude Code: graph-based code intelligence served by a local daemon."
marketplacePluginDescription = "Graph-based code intelligence for Claude Code: MCP tools, workflow skills, sub-agents and hooks backed by the local Gortex daemon."
)

// marketplaceReadmeBody is the README at the marketplace root, which is
// also the landing page of the published gortexhq/claude-plugin repo.
const marketplaceReadmeBody = `# Gortex plugins for Claude Code

This repository is a Claude Code plugin marketplace. It is generated by
` + "`gortex plugin emit`" + ` from https://github.com/zzet/gortex, so change the
source there rather than editing it here.

## Install

The plugin runs the ` + "`gortex`" + ` binary, so install that first
(https://github.com/zzet/gortex/blob/main/docs/installation.md). Then:

` + "```sh\nclaude plugin marketplace add gortexhq/claude-plugin\nclaude plugin install gortex@gortex\n```" + `

If you also run ` + "`gortex install`" + ` or ` + "`gortex init`" + `, pass
` + "`--agents-skip claude-code`" + ` so Gortex is not registered twice. See the
plugin README for what that skips.

## Plugins

| Plugin | What it adds |
|--------|--------------|
| [` + "`gortex`" + `](plugins/gortex/) | MCP server, workflow skills (` + "`/gortex:explore`" + `, ` + "`/gortex:impact`" + `, ...), sub-agents and hooks |
`

// EmitMarketplace writes a Claude Code plugin marketplace under
// spec.TargetDir: .claude-plugin/marketplace.json, a README and LICENSE
// at the root, and the plugin itself under plugins/gortex/. Returns the
// paths written, relative to TargetDir, in stable order.
//
// A target that still holds a bundle from before the marketplace
// layout, with the plugin at its root, is refused rather than cleaned:
// the emitter cannot tell its own old files from files someone added.
func EmitMarketplace(spec PluginBundleSpec) ([]string, error) {
if spec.TargetDir == "" {
return nil, fmt.Errorf("EmitMarketplace: TargetDir is required")
}
flatManifest := filepath.Join(spec.TargetDir, ".claude-plugin", "plugin.json")
if _, err := os.Lstat(flatManifest); err == nil {
return nil, fmt.Errorf("%s holds a plugin bundle from the older flat layout; remove it or emit into an empty directory", spec.TargetDir)
} else if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("check %s: %w", flatManifest, err)
}

pluginSpec := spec
pluginSpec.TargetDir = filepath.Join(spec.TargetDir, filepath.FromSlash(marketplacePluginSource))
pluginFiles, err := EmitPluginBundle(pluginSpec)
if err != nil {
return nil, err
}

manifest := map[string]any{
"name": marketplaceName,
"description": marketplaceDescription,
"owner": map[string]any{
"name": pluginAuthorName,
"email": pluginAuthorEmail,
},
"plugins": []any{
map[string]any{
"name": pluginName,
"source": marketplacePluginSource,
"description": marketplacePluginDescription,
},
},
}
manifestJSON, err := marshalJSONStable(manifest)
if err != nil {
return nil, fmt.Errorf("marshal marketplace.json: %w", err)
}

root := []struct {
rel string
body []byte
}{
{".claude-plugin/marketplace.json", manifestJSON},
{"README.md", []byte(marketplaceReadmeBody)},
{"LICENSE", []byte(pluginLicenseBody)},
}
written := make([]string, 0, len(root)+len(pluginFiles))
for _, f := range root {
full := filepath.Join(spec.TargetDir, f.rel)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
return nil, fmt.Errorf("mkdir %s: %w", filepath.Dir(f.rel), err)
}
if err := os.WriteFile(full, f.body, 0o644); err != nil {
return nil, fmt.Errorf("write %s: %w", f.rel, err)
}
written = append(written, f.rel)
}
for _, rel := range pluginFiles {
written = append(written, filepath.Join(filepath.FromSlash(marketplacePluginSource), rel))
}
return written, nil
}
Loading
Loading