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
5 changes: 5 additions & 0 deletions .changeset/agents-init-targets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stainless-code/codemap": patch
---

Add `codemap agents init --targets` and `--link-mode` for non-interactive IDE wiring. Combine with `--mcp` to write MCP config only for selected integrations (e.g. Cursor + Copilot without Continue/Cline). Mutually exclusive with `--interactive`.
9 changes: 7 additions & 2 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,17 @@ codemap agents init
codemap agents init --force
codemap agents init --interactive # or -i; requires a TTY
codemap agents init --mcp # project MCP config (default project-local targets; Windsurf opt-in via -i)
codemap agents init --targets cursor,copilot --mcp # non-interactive: subset of integrations + MCP
codemap agents init --targets copilot # Copilot instructions only (no MCP)
codemap agents init --targets windsurf --link-mode copy # rule mirrors as copies (sandboxes / Windows)
codemap agents init --git-hooks # opt-in background index on git events
codemap agents init --no-git-hooks # remove codemap hook blocks
```

- **`--force`** — if **`.agents/`** already exists, delete only the **same file paths** that ship in **`templates/agents`** (under **`rules/`** and **`skills/`**), then copy those files from the template. Any **other** files next to them (your custom rules, extra skill dirs, notes at **`.agents/`** root, etc.) are **not** removed. IDE mirrors (`.cursor/rules`, …) sync **only bundled template paths** (today `rules/codemap.md` and `skills/codemap/SKILL.md`) — not your whole **`.agents/`** tree. **`--force`** overwrites an existing IDE mirror **only** when it has **`<!-- codemap-init:managed -->`** or matches the **legacy mirror heuristic** (see [§ IDE mirror provenance](#ide-mirror-provenance-codemap-initmanaged)). Pointer files (`CLAUDE.md`, …): **`--force`** refreshes the `codemap-pointer` section only; your prose outside the markers is kept. Use **`--interactive`**, not a bare **`interactive`** argument (unknown tokens are rejected).
- **`--interactive`** — multiselect which tools to wire (see below); choose **symlink** vs **copy** for integrations that mirror **bundled** **`.agents/rules`** paths (and Cursor also bundled **`.agents/skills`**). Uses [**@clack/prompts**](https://github.com/bombshell-dev/clack); **non-TTY** runs exit with an error.
- **`--interactive`** — multiselect which tools to wire (see below); choose **symlink** vs **copy** for integrations that mirror **bundled** **`.agents/rules`** paths (and Cursor also bundled **`.agents/skills`**). Uses [**@clack/prompts**](https://github.com/bombshell-dev/clack); **non-TTY** runs exit with an error. Mutually exclusive with **`--targets`**.
- **`--targets`** — comma-separated integration ids (`cursor`, `copilot`, `claude-md`, `windsurf`, `continue`, `cline`, `amazon-q`, `agents-md`, `gemini-md`) or repeated `--targets` flags. Wires IDE mirrors without a TTY. With **`--mcp`**, only MCP configs for the selected integrations are written (e.g. `cursor` alone → `.cursor/mcp.json` only, not root `.mcp.json`). Default **`--link-mode`** is **symlink** when omitted. Unknown ids exit 1 with the valid list.
- **`--link-mode`** — `symlink` or `copy`; only valid when **`--targets`** includes a rule-mirror integration (`cursor`, `windsurf`, `continue`, `cline`, `amazon-q`).

## Git and `.gitignore`

Expand Down Expand Up @@ -149,7 +154,7 @@ Example: `CODEMAP_MCP_TOOLS=query,context,show codemap mcp --no-watch`
| Cline | `.cline/mcp.json` ([Cline CLI reference](https://docs.cline.bot/cli/cli-reference); global IDE settings may also use `~/.cline/data/settings/cline_mcp_settings.json`) |
| Windsurf (Cascade) | `~/.codeium/windsurf/mcp_config.json` ([Windsurf docs](https://docs.windsurf.com/windsurf/cascade/mcp) — user-global only; written when Windsurf integration is selected) |

With **`--mcp`** and no `--target` filter, all **project-local** rows above are written except **Windsurf**, which has no documented workspace MCP path.
With **`--mcp`** and no **`--targets`** filter, all **project-local** rows above are written except **Windsurf**, which has no documented workspace MCP path.

Merge is idempotent: foreign MCP servers and existing settings keys are preserved; only the `codemap` server entry and permission pattern are upserted. **`command` / spawn args are resolved from the project** (when `@stainless-code/codemap` is listed in `package.json`, the local PM runner is used — e.g. `pnpm exec codemap`, `yarn exec codemap`, `bunx codemap`; otherwise PM dlx of `@stainless-code/codemap@latest` — e.g. `npx @stainless-code/codemap@latest`, `pnpm dlx @stainless-code/codemap@latest`, `yarn dlx @stainless-code/codemap@latest`; yarn classic may fall back to `npx` per `package-manager-detector`; Bun uses **`bunx`**, not `bun x`). Init logs the chosen invocation (`MCP CLI: …`).

Expand Down
1 change: 1 addition & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Long-running MCP / HTTP sessions dominate agent workflows; one-shot CLI keeps th
- [x] **MCP session lifecycle hygiene** — stdio disconnect detection (stdin EOF, stdout EPIPE, parent-PID poll, SIGINT/SIGTERM) and refcount-gated watcher stop on MCP client exit; HTTP `serve --watch` starts/stops the watcher per client (5s release grace between stateless requests; `/health` excluded). **Explicitly no MCP idle timeout** — process stays up while the stdio pipe is open even without tool calls (IDE hosts do not respawn mid-session). See [architecture.md § Session lifecycle wiring](./architecture.md#cli-usage). Effort: S–M.
- [x] **PM-aware MCP spawn (`agents init --mcp`)** — resolve PM `execute-local` vs dlx for MCP JSON `command`/`args` when codemap is a devDependency. Shipped [#154](https://github.com/stainless-code/codemap/pull/154).
- [ ] **`--mcp-invocation global|auto` flag** — explicit override to force global `codemap` on PATH vs PM-aware auto-resolve. Effort: S.
- [x] **`agents init --targets` (non-interactive IDE wiring)** — `--targets` + `--link-mode` for CI/sandboxes; MCP subset when combined with `--mcp`. Shipped [#158](https://github.com/stainless-code/codemap/pull/158); see [agents.md](./agents.md).
- [ ] **`agents init` uninstall (teardown)** — symmetric inverse of init for failed pilots, template mistakes, or leaving a repo: remove codemap-managed MCP entries, pointer sections, and IDE symlinks only (same scoped paths as init; never delete user-authored `.agents/` siblings). `--target` filter, `--yes` non-interactive. Not the happy-path docs story — adoption stays `init --mcp --git-hooks` + committed `.agents/`. Effort: S.
- [x] **HEAD / index freshness warning** — `index_freshness.commit_drift` + `warning` on `context` / tool metadata; boot stderr on `codemap mcp` / `serve` when concerns remain after prime. Shipped [#149](https://github.com/stainless-code/codemap/pull/149).

Expand Down
30 changes: 30 additions & 0 deletions src/agents-init-targets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from "bun:test";

import {
formatAgentsInitTargetIdsForError,
parseAgentsInitTargets,
} from "./agents-init-targets";

describe("parseAgentsInitTargets", () => {
it("parses comma-separated and repeated segments", () => {
expect(parseAgentsInitTargets(["cursor,copilot", "cursor"])).toEqual([
"cursor",
"copilot",
]);
});

it("rejects unknown ids with valid list", () => {
expect(() => parseAgentsInitTargets(["nope"])).toThrow(
/unknown integration/,
);
expect(() => parseAgentsInitTargets(["nope"])).toThrow(
formatAgentsInitTargetIdsForError(),
);
});

it("rejects empty segments", () => {
expect(() => parseAgentsInitTargets([","])).toThrow(
/requires at least one integration id/,
);
});
});
55 changes: 55 additions & 0 deletions src/agents-init-targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ export type AgentsInitTarget =
| "agents-md"
| "gemini-md";

/** Same order as interactive multiselect in `agents-init-interactive.ts`. */
export const AGENTS_INIT_TARGET_IDS: readonly AgentsInitTarget[] = [
"cursor",
"claude-md",
"copilot",
"windsurf",
"continue",
"cline",
"amazon-q",
"agents-md",
"gemini-md",
] as const;

const AGENTS_INIT_TARGET_ID_SET = new Set<string>(AGENTS_INIT_TARGET_IDS);

/** Targets that mirror `.agents/rules` (and Cursor also `.agents/skills`) via per-file symlink or copy. */
export const AGENTS_INIT_SYMLINK_TARGETS: readonly AgentsInitTarget[] = [
"cursor",
Expand All @@ -25,3 +40,43 @@ export const AGENTS_INIT_SYMLINK_TARGETS: readonly AgentsInitTarget[] = [
export function targetsNeedLinkMode(targets: AgentsInitTarget[]): boolean {
return targets.some((t) => AGENTS_INIT_SYMLINK_TARGETS.includes(t));
}

export function isAgentsInitTarget(id: string): id is AgentsInitTarget {
return AGENTS_INIT_TARGET_ID_SET.has(id);
}

export function formatAgentsInitTargetIdsForError(): string {
return AGENTS_INIT_TARGET_IDS.join(", ");
}

/**
* Parse `--targets` argv segments (`cursor,copilot` or repeated flags).
* Dedupes while preserving first-seen order.
*/
export function parseAgentsInitTargets(raw: string[]): AgentsInitTarget[] {
const out: AgentsInitTarget[] = [];
const seen = new Set<AgentsInitTarget>();
for (const segment of raw) {
for (const part of segment.split(",")) {
const id = part.trim();
if (id.length === 0) {
throw new Error(
"codemap: --targets requires at least one integration id",
);
}
if (!isAgentsInitTarget(id)) {
throw new Error(
`codemap: unknown integration ${JSON.stringify(id)}. Valid ids: ${formatAgentsInitTargetIdsForError()}`,
);
}
if (!seen.has(id)) {
seen.add(id);
out.push(id);
}
}
}
if (out.length === 0) {
throw new Error("codemap: --targets requires at least one integration id");
}
return out;
}
70 changes: 70 additions & 0 deletions src/agents-init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,76 @@ describe("runAgentsInit", () => {
}
});

it("runAgentsInit --git-hooks --targets cursor on existing .agents/ composes hooks and wiring", async () => {
const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
try {
mkdirSync(join(dir, ".agents", "rules"), { recursive: true });
mkdirSync(join(dir, ".agents", "skills", "codemap"), {
recursive: true,
});
mkdirSync(join(dir, ".git", "hooks"), { recursive: true });
writeFileSync(
join(dir, ".agents", "rules", "codemap.md"),
`${CODMAP_INIT_MANAGED}\n`,
"utf-8",
);
writeFileSync(
join(dir, ".agents", "skills", "codemap", "SKILL.md"),
`${CODMAP_INIT_MANAGED}\n`,
"utf-8",
);
expect(
await runAgentsInit({
projectRoot: dir,
gitHooks: "install",
targets: ["cursor"],
}),
).toBe(true);
expect(
isCodemapHookInstalled(join(dir, ".git", "hooks", "post-commit")),
).toBe(true);
expect(existsSync(join(dir, ".cursor", "rules", "codemap.mdc"))).toBe(
true,
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("runAgentsInit --mcp --targets cursor on existing .agents/ composes MCP and wiring", async () => {
const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
try {
mkdirSync(join(dir, ".agents", "rules"), { recursive: true });
mkdirSync(join(dir, ".agents", "skills", "codemap"), {
recursive: true,
});
writeFileSync(
join(dir, ".agents", "rules", "codemap.md"),
`${CODMAP_INIT_MANAGED}\n`,
"utf-8",
);
writeFileSync(
join(dir, ".agents", "skills", "codemap", "SKILL.md"),
`${CODMAP_INIT_MANAGED}\n`,
"utf-8",
);
expect(
await runAgentsInit({
projectRoot: dir,
mcp: true,
targets: ["cursor"],
}),
).toBe(true);
expect(existsSync(join(dir, ".cursor", "mcp.json"))).toBe(true);
expect(existsSync(join(dir, ".cursor", "rules", "codemap.mdc"))).toBe(
true,
);
expect(existsSync(join(dir, ".mcp.json"))).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("runAgentsInit --no-git-hooks --mcp uninstalls hooks and writes MCP", async () => {
const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
try {
Expand Down
28 changes: 21 additions & 7 deletions src/agents-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,23 @@ async function maybeApplyAgentsInitMcp(
}
}

/** Side-effect-only path when `.agents/` exists and `--force` is off. */
function applyMaybeAgentsInitTargetsOnExisting(
options: AgentsInitOptions,
): void {
const targets = options.targets ?? [];
if (targets.length === 0) {
return;
}
applyAgentsInitTargets(
options.projectRoot,
targets,
options.linkMode ?? "symlink",
false,
);
ensureGitignoreCodemapPattern(options.projectRoot);
}

/**
* Copy bundled `rules/` and `skills/` into `<projectRoot>/.agents/`, optional integrations, `.gitignore` hint.
* **`--force`** deletes only template-backed files, then writes those files again with per-file copies — your other files under **`.agents/`**, **`rules/`**, or **`skills/`** stay.
Expand All @@ -603,6 +620,7 @@ export async function runAgentsInit(
if (options.gitHooks === "uninstall") {
uninstallGitHooks(options.projectRoot);
console.log(" Removed codemap blocks from git hooks");
applyMaybeAgentsInitTargetsOnExisting(options);
await maybeApplyAgentsInitMcp(options);
return true;
}
Expand Down Expand Up @@ -635,22 +653,18 @@ export async function runAgentsInit(
console.log(
" Installed git hooks (post-commit, post-merge, post-checkout) for background codemap sync",
);
applyMaybeAgentsInitTargetsOnExisting(options);
await maybeApplyAgentsInitMcp(options);
return true;
}
if (options.mcp === true) {
applyMaybeAgentsInitTargetsOnExisting(options);
await maybeApplyAgentsInitMcp(options);
return true;
}
const targets = options.targets ?? [];
if (targets.length > 0) {
applyAgentsInitTargets(
options.projectRoot,
targets,
options.linkMode ?? "symlink",
false,
);
ensureGitignoreCodemapPattern(options.projectRoot);
applyMaybeAgentsInitTargetsOnExisting(options);
await maybeApplyAgentsInitMcp(options);
return true;
}
Expand Down
Loading
Loading