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
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,25 @@ npx universal-agent-plugins add context7
```

It reads the root `plugin.json` defined by Agent Plugins 1.0 and plans, prepares,
or installs one explicit target at a time across Codex/ChatGPT, Cursor, GitHub
or installs one explicit target at a time across Codex, ChatGPT, Cursor, GitHub
Copilot/VS Code, and Kiro. v0.1 supports user scope; client-native limits are
reported before mutation. The first catalog contains [26 portable plugins](https://github.com/777genius/universal-agent-plugins).
The portable manifest is the root `plugin.json`; optional portable components
such as `mcp.json` and skills remain package inputs. For the Codex target, the
CLI generates the official Codex package manifest at `.codex-plugin/plugin.json`.
Accepted source shapes are either a portable root `plugin.json` with optional
`mcp.json`, `.app.json`, and `skills/`, or an official `.codex-plugin/plugin.json`
with its declared root `.mcp.json`, `.app.json`, and `skills/` inputs. A portable
root manifest wins when both exist. Codex and ChatGPT retain bundled `.mcp.json`;
ChatGPT MCP additionally requires a valid `.app.json` mapping to a connection
registered in Developer Mode. Both targets receive an official generated
`.codex-plugin/plugin.json`.
Starting with agentplugins 0.1.6, direct package sources can use `--target
chatgpt`; catalog short names stay fail-closed unless their catalog v2 entry
includes pinned ChatGPT compatibility evidence. Catalog v1 remains readable
without ChatGPT binding metadata.

When GitHub Copilot CLI is detected, `agentplugins` installs, updates, and
removes the plugin automatically through a managed local marketplace. VS Code
discovers that installation automatically, so selecting either Copilot or VS
Code once is enough. Codex/ChatGPT and Kiro keep their
Code once is enough. Codex, ChatGPT, and Kiro keep their
required client confirmation and receive an exact, path-specific next step in
human-readable output.

Expand Down

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions cli/plugin-kit-ai/cmd/agentplugins/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ import (
var (
version = "0.1.0-development"
defaultCatalogURL = ""
defaultCatalogDigest = "sha256:b1e3efcdc7bd3fc5cc64c959f96964818b1cbfd815cbd125045c02757c767c30"
//go:embed catalog-v1.json
defaultCatalogDigest = "sha256:66199c87bd68c65e39d15aa2c5c6e6c7830c9b116d8ed3590123031b32357050"
//go:embed catalog-v2.json
embeddedCatalog []byte
)

Expand Down
5 changes: 4 additions & 1 deletion cli/plugin-kit-ai/cmd/agentplugins/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ func TestEmbeddedCatalogIsPinnedAndContainsAllLaunchPlugins(t *testing.T) {
if digest != defaultCatalogDigest {
t.Fatalf("embedded catalog digest = %s, want %s", digest, defaultCatalogDigest)
}
loaded, err := (catalog.Loader{CurrentCLIVersion: "0.1.0"}).Load(embeddedCatalog, defaultCatalogDigest)
loaded, err := (catalog.Loader{CurrentCLIVersion: "0.1.6"}).Load(embeddedCatalog, defaultCatalogDigest)
if err != nil {
t.Fatal(err)
}
if loaded.Catalog.SchemaVersion != 2 {
t.Fatalf("embedded catalog schema_version = %d, want 2", loaded.Catalog.SchemaVersion)
}
if len(loaded.Catalog.Plugins) != 26 {
t.Fatalf("catalog plugins = %d", len(loaded.Catalog.Plugins))
}
Expand Down
28 changes: 25 additions & 3 deletions cli/plugin-kit-ai/internal/agentpluginscli/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ func runAdd(ctx context.Context, cmd *cobra.Command, app App, opts *options, sou
if err != nil {
return err
}
if err := prepareLoadedPackageForClient(&loaded, selected.ClientID); err != nil {
return err
}
planner := clientplanner.Planner{ManagedRoot: app.ManagedRoot, Detected: detectedMap}
service := usecase.Service{
StateStore: app.StateStore,
Expand All @@ -72,6 +75,15 @@ func runAdd(ctx context.Context, cmd *cobra.Command, app App, opts *options, sou
}
planned, err := service.Add(ctx, input)
if err != nil {
if planned.Plan.Status == domain.PlanUnsupported {
if opts.format == "json" {
if renderErr := renderAddResult(cmd.OutOrStdout(), opts.format, loaded.envelope, planned, opts.dryRun); renderErr != nil {
return renderErr
}
} else if renderErr := renderHumanPlan(cmd.OutOrStdout(), loaded.envelope, planned); renderErr != nil {
return renderErr
}
}
return err
}
if opts.dryRun || planned.NoChange {
Expand Down Expand Up @@ -191,14 +203,22 @@ func selectClient(
}
target := normalizeTarget(opts.target)
if target != "" {
if strings.EqualFold(strings.TrimSpace(opts.target), "openai") {
return domain.DetectedClient{}, detectedMap, fmt.Errorf("target %q is ambiguous; use --target codex or --target chatgpt", opts.target)
}
client, ok := detectedMap[target]
if !ok || client.Status != domain.DetectionDetected {
if !ok && target == domain.ClientChatGPT {
client = domain.DetectedClient{ClientID: domain.ClientChatGPT, DisplayName: "ChatGPT", Status: domain.DetectionNotDetected}
detectedMap[target] = client
ok = true
}
if !ok || (client.Status != domain.DetectionDetected && target != domain.ClientChatGPT) {
return domain.DetectedClient{}, detectedMap, fmt.Errorf("target %q was not detected", opts.target)
}
return client, detectedMap, nil
}
if len(detected) == 0 {
return domain.DetectedClient{}, detectedMap, fmt.Errorf("no supported AI client was detected; use --target after installing a client")
return domain.DetectedClient{}, detectedMap, fmt.Errorf("no supported local AI client was detected; use --target chatgpt for ChatGPT, or install/detect another client")
}
if len(detected) == 1 {
return detected[0], detectedMap, nil
Expand All @@ -225,8 +245,10 @@ func selectClient(

func normalizeTarget(value string) domain.ClientID {
switch strings.ToLower(strings.TrimSpace(value)) {
case "openai", "codex":
case "codex":
return domain.ClientCodex
case "chatgpt":
return domain.ClientChatGPT
case "cursor":
return domain.ClientCursor
case "copilot", "github-copilot":
Expand Down
3 changes: 3 additions & 0 deletions cli/plugin-kit-ai/internal/agentpluginscli/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ func componentInventoryLabel(inventory domain.ComponentInventory) string {
} else {
parts = append(parts, "mcp=absent")
}
if inventory.AppPresent {
parts = append(parts, "apps=["+sortedList(inventory.AppBindings)+"]")
}
parts = append(parts, "skills=["+sortedList(inventory.Skills)+"]")
parts = append(parts, "extensions=["+sortedList(inventory.Extensions)+"]")
if len(inventory.InvalidMCPServer) > 0 {
Expand Down
Loading
Loading