Skip to content
Open
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Shared tooling for coding agents. Declare skills, MCP servers, hooks, subagents,

**Shareable.** Skills are directories with a `SKILL.md`. Host them in any git repo, discover them automatically, install with one command.

**Multi-agent.** Configure Claude, Cursor, Codex, Grok, VS Code, and OpenCode from a single `agents.toml` -- skills, MCP servers, hooks, subagents, and plugins where supported. Pi reads `.agents/skills/` directly.
**Multi-agent.** Configure Claude, Cursor, Codex, GitHub Copilot CLI/Desktop, Grok, VS Code, and OpenCode from a single `agents.toml` -- skills, MCP servers, hooks, subagents, and plugins where supported. Pi reads `.agents/skills/` directly.

## Quick Start: Global by Default

Expand Down Expand Up @@ -115,7 +115,7 @@ Shorthand (`owner/repo`) resolves to GitHub by default. Set `defaultRepositorySo
The `agents` field tells dotagents which tools to configure:

```toml
agents = ["claude", "cursor", "codex", "grok", "opencode", "pi"]
agents = ["claude", "cursor", "codex", "copilot", "grok", "opencode", "pi"]
```

| Agent | Config Dir | MCP Config | Hooks | Subagents |
Expand All @@ -126,6 +126,7 @@ agents = ["claude", "cursor", "codex", "grok", "opencode", "pi"]
| `grok` | `.grok` | -- | -- | -- |
| `vscode` | `.vscode` | `.vscode/mcp.json` | `.claude/settings.json` | -- |
| `opencode` | `.opencode` | `.opencode/opencode.jsonc` | -- | `.opencode/agents/*.md` |
| `copilot` | `.copilot` | `.mcp.json` or `.github/mcp.json` | -- | -- |

Custom subagents are declared with `[[subagents]]` entries. dotagents writes generated runtime-specific files during `install` and repairs them during `sync`:

Expand Down Expand Up @@ -153,6 +154,8 @@ dotagents can also import native runtime subagent files from `.claude/agents/`,

OpenCode reuses an existing project config from `.opencode/opencode.jsonc`, `.opencode/opencode.json`, `opencode.jsonc`, or `opencode.json`, in that order. New projects use `.opencode/opencode.jsonc`.

GitHub Copilot reads project skills from `.agents/skills/` without a symlink. New project MCP configuration uses `.mcp.json`. An existing `.github/mcp.json` is reused when `.mcp.json` is absent. Global MCP configuration uses `COPILOT_HOME/mcp-config.json` when `COPILOT_HOME` is set and `~/.copilot/mcp-config.json` otherwise. On POSIX systems, dotagents creates and repairs the global file with mode `0600`, which matches Copilot CLI.

Plugins are declared with `[[plugins]]` entries. In project scope, dotagents installs canonical bundles into `.agents/plugins/<name>/` and generates runtime plugin outputs such as `.claude-plugin/marketplace.json`, `.agents/plugins/<name>/.claude-plugin/plugin.json`, `.cursor-plugin/marketplace.json`, `.agents/plugins/<name>/.cursor-plugin/plugin.json`, `.agents/plugins/marketplace.json`, `.agents/plugins/<name>/.codex-plugin/plugin.json`, `.grok/plugins/<name>/`, `.opencode/skills/<skill>/`, OpenCode MCP entries, and Pi skill links under `.agents/skills/<skill>/` where supported. During legacy migration, generalized bundles can also project Markdown agents into `.opencode/agents/`; standard extension agents are preserved but are not projected yet:

```toml
Expand Down
14 changes: 9 additions & 5 deletions docs/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> Shared tooling for coding agents

dotagents manages agent skills, MCP servers, hooks, subagents, and plugins declared in `agents.toml`, and handles symlinks and config generation so tools like Claude Code, Cursor, Codex, Grok, VS Code, and OpenCode are configured from a single source of truth.
dotagents manages agent skills, MCP servers, hooks, subagents, and plugins declared in `agents.toml`, and handles symlinks and config generation so tools like Claude Code, Cursor, Codex, GitHub Copilot CLI/Desktop, Grok, VS Code, and OpenCode are configured from a single source of truth.

Install: `npm install -g @sentry/dotagents`
Run without installing: `npx @sentry/dotagents <command>`
Expand Down Expand Up @@ -66,7 +66,7 @@ Full example with all sections:

```toml
version = 1
agents = ["claude", "cursor", "codex", "grok", "opencode", "pi"]
agents = ["claude", "cursor", "codex", "copilot", "grok", "opencode", "pi"]
minimum_release_age = 60
minimum_release_age_exclude = ["getsentry/*"]

Expand Down Expand Up @@ -158,7 +158,7 @@ targets = ["claude", "cursor", "codex", "grok", "opencode", "pi"]
|-------|------|----------|---------|-------------|
| `version` | integer | Yes | -- | Schema version. Always `1`. |
| `defaultRepositorySource` | string | No | `github` | Host used for shorthand `owner/repo` skill sources. Valid values: `github`, `gitlab`. |
| `agents` | string[] | No | `[]` | Agent tool IDs: `claude`, `cursor`, `codex`, `grok`, `vscode`, `opencode`, `pi`. Creates symlinks and config files for each where supported. `grok` and `pi` are plugin-only targets. |
| `agents` | string[] | No | `[]` | Agent tool IDs: `claude`, `cursor`, `codex`, `copilot`, `grok`, `vscode`, `opencode`, `pi`. Creates symlinks and config files for each where supported. `grok` and `pi` are plugin-only targets. |
| `subagents` | table[] | No | `[]` | Custom subagent declarations. Generates runtime-specific files for Claude, Cursor, Codex, and OpenCode. |
| `plugins` | table[] | No | `[]` | Plugin declarations. Installs canonical bundles into `.agents/plugins/` and generates runtime plugin outputs for Claude, Cursor, Codex, Grok, OpenCode, and Pi skill projection where supported. |
| `minimum_release_age` | integer | No | -- | Minimum commit age, in minutes, before a git skill, subagent, or plugin can install. |
Expand Down Expand Up @@ -211,14 +211,17 @@ Each `[[mcp]]` entry requires `name` and either `command` (stdio) or `url` (HTTP
| `headers` | table | No | HTTP headers (url servers only). Supports `${VAR}` syntax for env var interpolation. |
| `env` | string[] | No | Environment variable names to pass through |

Use `${VAR}` in header values and `url` to reference secrets from the environment. Write `${VAR}` in `agents.toml` — dotagents translates it to each agent's native syntax when generating config files. Claude keeps `${VAR}`, Cursor and VS Code use `${env:VAR}`, OpenCode uses `{env:VAR}`, and Codex splits pure refs into a separate `env_http_headers` field (mixed values like `"Bearer ${TOKEN}"` stay as literals).
Use `${VAR}` in header values and `url` to reference secrets from the environment. Write `${VAR}` in `agents.toml`. Dotagents keeps this syntax for Claude and GitHub Copilot. Cursor and VS Code use `${env:VAR}`, OpenCode uses `{env:VAR}`, and Codex moves pure references to `env_http_headers`. Mixed Codex values such as `"Bearer ${TOKEN}"` stay as literals.

Config files generated per agent:
- Claude: `.mcp.json` (JSON)
- Cursor: `.cursor/mcp.json` (JSON)
- Codex: `.codex/config.toml` (TOML, shared with other Codex config)
- VS Code: `.vscode/mcp.json` (JSON)
- OpenCode: `.opencode/opencode.jsonc` by default (JSONC, shared). Existing `.opencode/opencode.json`, `opencode.jsonc`, or `opencode.json` files are reused in precedence order.
- GitHub Copilot: `.mcp.json` by default (JSON). An existing `.github/mcp.json` is reused when `.mcp.json` is absent.

GitHub Copilot accepts the same implicit stdio shape as Claude, so both targets produce stable shared `.mcp.json` output. In user scope, GitHub Copilot uses `COPILOT_HOME/mcp-config.json` when `COPILOT_HOME` is set and `~/.copilot/mcp-config.json` otherwise. On POSIX systems, dotagents creates and repairs this file with mode `0600`.

### Hooks

Expand Down Expand Up @@ -501,8 +504,9 @@ Check selected-scope health: gitignore setup where applicable, installed skills
| `codex` | Codex | `.codex` | (reads `.agents/skills/` natively) | `.codex/config.toml` | Not supported | `.codex/agents/*.toml` |
| `vscode` | VS Code Copilot | `.vscode` | (reads `.agents/skills/` natively) | `.vscode/mcp.json` | `.claude/settings.json` | Not supported |
| `opencode` | OpenCode | `.opencode` | (reads `.agents/skills/` natively) | `.opencode/opencode.jsonc` by default | Not supported | `.opencode/agents/*.md` |
| `copilot` | GitHub Copilot CLI/Desktop | `.copilot` | (reads `.agents/skills/` natively) | `.mcp.json` or `.github/mcp.json` | Not supported | Not supported |

Claude uses `.claude/skills/`, and Cursor shares the same Claude-compatible skills symlink. Codex, VS Code, and OpenCode read `.agents/skills/` directly.
Claude uses `.claude/skills/`, and Cursor shares the same Claude-compatible skills symlink. Codex, VS Code, OpenCode, and GitHub Copilot read `.agents/skills/` directly.

[Pi](https://github.com/badlogic/pi-mono) reads `.agents/skills/` natively. Normal skills need no Pi-specific target or symlink configuration; plugin bundles can target `pi` when their `skills/` components should be exposed there.

Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ agents can discover CLI guidance. It then runs `install` best-effort.
Options:

- `--agents <list>` comma-separated agent targets (`claude`, `cursor`, `codex`,
`vscode`, `opencode`, `grok`, `pi`).
`copilot`, `vscode`, `opencode`, `grok`, `pi`).
- `--force` overwrites existing `agents.toml`.

Examples:
Expand Down Expand Up @@ -369,7 +369,7 @@ Status output:
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `version` | integer | -- | Schema version. Always `1`. |
| `agents` | string[] | `[]` | Agent targets: `claude`, `cursor`, `codex`, `grok`, `vscode`, `opencode`, `pi`. `grok` and `pi` are plugin-only targets. |
| `agents` | string[] | `[]` | Agent targets: `claude`, `cursor`, `codex`, `copilot`, `grok`, `vscode`, `opencode`, `pi`. `grok` and `pi` are plugin-only targets. |
| `subagents` | table[] | `[]` | Custom subagent declarations for Claude, Cursor, Codex, and OpenCode |
| `plugins` | table[] | `[]` | Plugin declarations for Claude, Cursor, Codex, Grok, OpenCode, and Pi. |
| `minimum_release_age` | integer | -- | Minimum commit age, in minutes, before a git skill, subagent, or plugin can install. |
Expand Down
5 changes: 4 additions & 1 deletion docs/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,20 @@ targets = ["claude", "cursor", "codex", "opencode"]`}</code></pre>
<section className="dotagents-section">
<div className="dotagents-section-heading">
<h2 id="supported-agents">Supported Agents</h2>
<p>The `agents` array tells dotagents which tools to configure. Grok and Pi are plugin-only targets; Pi also reads `.agents/skills/` directly.</p>
<p>The `agents` array tells dotagents which tools to configure. Grok and Pi are plugin-only targets. Copilot and Pi read `.agents/skills/` directly.</p>
</div>

| Agent | Config Dir | MCP Config | Hooks | Subagents |
| --- | --- | --- | --- | --- |
| `claude` | `.claude` | `.mcp.json` | `.claude/settings.json` | `.claude/agents/*.md` |
| `cursor` | `.cursor` | `.cursor/mcp.json` | `.cursor/hooks.json` | `.cursor/agents/*.md` |
| `codex` | `.codex` | `.codex/config.toml` | None | `.codex/agents/*.toml` |
| `copilot` | `.copilot` | `.mcp.json` | None | None |
| `vscode` | `.vscode` | `.vscode/mcp.json` | `.claude/settings.json` | None |
| `opencode` | `.opencode` | `.opencode/opencode.jsonc` | None | `.opencode/agents/*.md` |

GitHub Copilot reads user MCP configuration from `~/.copilot/mcp-config.json`. Projects use `.mcp.json`, or reuse `.github/mcp.json` when it exists.

OpenCode reuses existing project config from `.opencode/opencode.jsonc`, `.opencode/opencode.json`, `opencode.jsonc`, or `opencode.json`, in that order. New projects use `.opencode/opencode.jsonc`.
</section>

Expand Down
75 changes: 74 additions & 1 deletion packages/dotagents/src/cli/commands/install-user.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { mkdtemp, mkdir, readFile, readlink, rm, writeFile, lstat } from "node:fs/promises";
import { mkdtemp, mkdir, readFile, readlink, rm, writeFile, lstat, stat } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join, relative } from "node:path";
import { tmpdir } from "node:os";
Expand All @@ -17,6 +17,7 @@ describe("runInstall user scope", () => {
const previousHome = process.env["HOME"];
const previousDotagentsHome = process.env["DOTAGENTS_HOME"];
const previousStateDir = process.env["DOTAGENTS_STATE_DIR"];
const previousCopilotHome = process.env["COPILOT_HOME"];

afterEach(async () => {
if (previousHome === undefined) {
Expand All @@ -34,6 +35,11 @@ describe("runInstall user scope", () => {
} else {
process.env["DOTAGENTS_STATE_DIR"] = previousStateDir;
}
if (previousCopilotHome === undefined) {
delete process.env["COPILOT_HOME"];
} else {
process.env["COPILOT_HOME"] = previousCopilotHome;
}
vi.resetModules();

if (tmpDir) {
Expand Down Expand Up @@ -128,4 +134,71 @@ source = "path:skill-source/pdf"
const lockfile = await loadLockfile(scope.lockPath);
expect(lockfile!.skills["pdf"]).toEqual({ source: "path:skill-source/pdf" });
});

it("writes copilot MCP config without a skill symlink", async () => {
tmpDir = await mkdtemp(join(tmpdir(), "dotagents-user-copilot-"));
const homeDir = join(tmpDir, "home");
const dotagentsHome = join(tmpDir, "agents");
const stateDir = join(tmpDir, "state");
const copilotHome = join(tmpDir, "copilot");
const sourceDir = join(dotagentsHome, "skill-source", "pdf");

process.env["HOME"] = homeDir;
process.env["DOTAGENTS_HOME"] = dotagentsHome;
process.env["DOTAGENTS_STATE_DIR"] = stateDir;
process.env["COPILOT_HOME"] = copilotHome;
vi.resetModules();

const [{ runInstall }, { resolveScope }] = await Promise.all([
import("./install.js"),
import("../../scope.js"),
]);

await mkdir(sourceDir, { recursive: true });
await mkdir(copilotHome, { recursive: true });
await writeFile(join(sourceDir, "SKILL.md"), SKILL_MD);
await writeFile(
join(copilotHome, "mcp-config.json"),
JSON.stringify({
note: "keep",
mcpServers: {
manual: { command: "manual", args: [] },
fixture: { command: "old", args: [] },
},
}),
);

const scope = resolveScope("user");
await mkdir(scope.root, { recursive: true });
await writeFile(
scope.configPath,
`version = 1
agents = ["copilot"]

[[skills]]
name = "pdf"
source = "path:skill-source/pdf"

[[mcp]]
name = "fixture"
command = "node"
args = ["server.js"]
`,
);

await runInstall({ scope });

expect(existsSync(join(scope.skillsDir, "pdf", "SKILL.md"))).toBe(true);
expect(existsSync(join(copilotHome, "skills"))).toBe(false);
expect(JSON.parse(await readFile(join(copilotHome, "mcp-config.json"), "utf-8"))).toEqual({
note: "keep",
mcpServers: {
manual: { command: "manual", args: [] },
fixture: { command: "node", args: ["server.js"] },
},
});
if (process.platform !== "win32") {
expect((await stat(join(copilotHome, "mcp-config.json"))).mode & 0o777).toBe(0o600);
}
});
});
15 changes: 13 additions & 2 deletions packages/dotagents/src/cli/commands/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1087,8 +1087,14 @@ source = "path:./.agents/plugins/local-tools/source"
const userHome = join(tmpDir, "home");
process.env["DOTAGENTS_HOME"] = dotagentsHome;
process.env["HOME"] = userHome;
vi.resetModules();
try {
const scope = resolveScope("user");
const [{ runInstall: runUserInstall }, { resolveScope: resolveUserScope }] =
await Promise.all([
import("./install.js"),
import("../../scope.js"),
]);
const scope = resolveUserScope("user");
const sourceDir = join(scope.root, "plugin-source", "review-tools");
await mkdir(join(sourceDir, "skills", "review"), { recursive: true });
await writeFile(
Expand Down Expand Up @@ -1120,12 +1126,16 @@ source = "path:plugin-source/review-tools"
`,
);

const result = await runInstall({ scope });
const result = await runUserInstall({ scope });
expect(result.installedPlugins).toEqual(["review-tools"]);
expect(existsSync(join(scope.pluginsDir, "review-tools", "plugin.json"))).toBe(true);
expect(existsSync(join(scope.root, ".claude-plugin", "marketplace.json"))).toBe(true);
expect(existsSync(join(scope.root, ".agents", "plugins", "marketplace.json"))).toBe(true);
expect(await readlink(join(scope.skillsDir, "review"))).toBe("../plugins/review-tools/skills/review");
expect((await lstat(join(userHome, ".claude", "skills"))).isSymbolicLink()).toBe(true);
expect(await readlink(join(userHome, ".claude", "skills"))).toBe(
relative(join(userHome, ".claude"), scope.skillsDir),
);
expect(await readlink(join(userHome, ".config", "opencode", "skills", "review"))).toContain(
join("user-agents", "plugins", "review-tools", "skills", "review"),
);
Expand Down Expand Up @@ -1158,6 +1168,7 @@ source = "path:plugin-source/review-tools"
} else {
process.env["HOME"] = previousOsHome;
}
vi.resetModules();
}
});

Expand Down
27 changes: 27 additions & 0 deletions packages/dotagents/src/targets/definitions/copilot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { AgentDefinition } from "../types.js";
import { UnsupportedFeature } from "../errors.js";
import claude from "./claude.js";

const copilot: AgentDefinition = {
id: "copilot",
displayName: "GitHub Copilot",
configDir: ".copilot",
// reads .agents/skills/ natively at both project and user scope
skillsParentDir: undefined,
userSkillsParentDirs: undefined,
mcp: {
filePath: ".mcp.json",
fallbackFilePaths: [".github/mcp.json"],
rootKey: "mcpServers",
format: "json",
shared: false,
},
// Copilot accepts Claude's MCP shape, and both clients can share project .mcp.json.
serializeServer: claude.serializeServer,
hooks: undefined,
serializeHooks() {
throw new UnsupportedFeature("copilot", "hooks");
},
};

export default copilot;
Loading
Loading