Skip to content

Commit 491adaa

Browse files
authored
feat(init): opt-in agent selection with interactive picker (#36)
## Why `init` (shipped to canary in #34) wrote files for **all 6 agents** by default and installed the global skill unless `--no-skill`. That installs things the user never asked for. This flips it to **opt-in**: nothing is written unless chosen. Pre-release change — `init` only exists on canary, no stable release ships it, so there's no backward-compat cost. ## Behavior | Invocation | Result | |---|---| | `init` (TTY) | Interactive checkbox picker — `AGENTS.md` + `CLAUDE.md` preselected, skill toggle in-list | | `init --yes` / `--json` / no TTY | Defaults to `AGENTS.md` + `CLAUDE.md`, no skill | | `init --agents claude,cursor` | Exactly those | | `init --all` | Every agent | | `init --skill` | Also installs the global skill (**opt-in**) | Removed `--no-skill` (skill is off by default now). ## Implementation - **Zero new dependency** — interactive picker is a Node `readline` raw-mode checkbox (`src/install/prompt.ts`). The project is deliberately dep-minimal. - Selection logic is a **pure** `resolveInitPlan(flags, isTty)` + pure prompt helpers (`buildPromptItems`/`toggleItem`/`collectSelection`/`renderMenu`), all unit-tested. The raw-mode loop is a thin I/O shell over them. ## Tests / gates - 16 new tests (resolver branches + prompt helpers). Full suite **326 passed**. - lint / typecheck / build green. - Smoke-tested every non-interactive path (default, `--all`, `--agents`, `--skill`, invalid agent → exit 2). ## Docs README, `docs/cli-reference.md`, `llms-full.txt`, `CHANGELOG.md` updated; the `[Unreleased]` entry now describes the opt-in design (the never-shipped `--no-skill`/default-all behavior is gone).
1 parent 8cfac95 commit 491adaa

9 files changed

Lines changed: 416 additions & 53 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- **`init` command** — agent adoption layer. `codebase-intelligence init [path]` writes
1313
an idempotent, marked instruction block ("query CI before grep/read") into each
14-
agent's repo file (`AGENTS.md`, `CLAUDE.md`,
14+
selected agent's repo file (`AGENTS.md`, `CLAUDE.md`,
1515
`.cursor/rules/codebase-intelligence.mdc`, `.github/copilot-instructions.md`,
16-
`GEMINI.md`, `CONVENTIONS.md`) and installs a portable skill to
16+
`GEMINI.md`, `CONVENTIONS.md`) and optionally installs a portable skill to
1717
`~/.claude/skills/codebase-intelligence/SKILL.md`.
18-
- `--agents <list>` to target a subset of agents (default: all).
19-
- `--no-skill` to skip the global skill install.
20-
- `--json` for machine-readable output.
18+
- **Opt-in by design** — nothing is written unless chosen. On a TTY, an interactive
19+
picker (`AGENTS.md` + `CLAUDE.md` preselected); non-interactively, those two by
20+
default. The global skill installs only with `--skill`.
21+
- `--agents <list>` to select explicitly, `--all` for every agent, `--yes` for
22+
non-interactive defaults, `--json` for machine-readable output.
2123
- Writes are idempotent — only content between the
2224
`codebase-intelligence:start`/`:end` markers is ever touched; existing user content
2325
is preserved.

README.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,22 @@ codebase-intelligence has the data — but AI agents only benefit if they actual
125125
*query* it instead of defaulting to grep/read. `init` closes that gap.
126126

127127
```bash
128-
codebase-intelligence init # current repo, all agents + skill
129-
codebase-intelligence init ./repo --agents claude,agents
130-
codebase-intelligence init --no-skill
128+
codebase-intelligence init # interactive picker (TTY)
129+
codebase-intelligence init --agents claude,cursor
130+
codebase-intelligence init --all --skill # every agent + global skill
131+
codebase-intelligence init --yes # non-interactive defaults
131132
```
132133

133-
It writes an idempotent, marked instruction block ("query CI before grep/read") into
134-
each agent's native file, and installs a portable skill:
134+
Nothing is written unless you choose it. On a terminal, `init` shows an interactive
135+
picker (`AGENTS.md` + `CLAUDE.md` preselected); non-interactively it defaults to those
136+
two. The global skill is **opt-in** (`--skill`). It writes an idempotent, marked
137+
instruction block ("query CI before grep/read") into each selected agent's native file:
135138

136-
| Layer | Target |
137-
|---|---|
138-
| Repo instructions | `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/codebase-intelligence.mdc`, `.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md` (Aider) |
139-
| Portable skill | `~/.claude/skills/codebase-intelligence/SKILL.md` |
139+
| Layer | Target | Default |
140+
|---|---|---|
141+
| Repo instructions | `AGENTS.md`, `CLAUDE.md` | selected |
142+
| Repo instructions | `.cursor/rules/*.mdc`, `.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md` (Aider) | opt-in |
143+
| Portable skill | `~/.claude/skills/codebase-intelligence/SKILL.md` | opt-in (`--skill`) |
140144

141145
Writes are idempotent — only content between the
142146
`<!-- codebase-intelligence:start -->` / `:end` markers is ever touched, so re-running

docs/cli-reference.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,19 @@ codebase-intelligence clusters <path> [--min-files <n>] [--json] [--force]
158158

159159
### init
160160

161-
Make AI agents use codebase-intelligence: write a managed instruction block into each
162-
agent's repo file (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/codebase-intelligence.mdc`,
163-
`.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md`) and install the
164-
portable skill to `~/.claude/skills/`. Idempotent — only content between the
161+
Set up AI agents to use codebase-intelligence by writing a managed instruction block
162+
into each selected agent's repo file (`AGENTS.md`, `CLAUDE.md`,
163+
`.cursor/rules/codebase-intelligence.mdc`, `.github/copilot-instructions.md`,
164+
`GEMINI.md`, `CONVENTIONS.md`) and optionally installing the portable skill to
165+
`~/.claude/skills/`. Idempotent — only content between the
165166
`codebase-intelligence:start`/`:end` markers is touched.
166167

168+
Opt-in by design: on a TTY it shows an interactive picker (`AGENTS.md` + `CLAUDE.md`
169+
preselected). Non-interactively (or with `--yes`/`--json`) it defaults to those two.
170+
The global skill is never installed unless `--skill` is passed.
171+
167172
```bash
168-
codebase-intelligence init [path] [--agents <list>] [--no-skill] [--json]
173+
codebase-intelligence init [path] [--agents <list>] [--all] [--skill] [--yes] [--json]
169174
```
170175

171176
**Output:** per-file actions (created / updated / unchanged) and skill install status.
@@ -187,8 +192,10 @@ codebase-intelligence init [path] [--agents <list>] [--no-skill] [--json]
187192
| `--entry <name>` | processes | Filter by entry point name |
188193
| `--min-files <n>` | clusters | Min files per cluster |
189194
| `--no-dry-run` | rename | Actually perform the rename (default: dry run) |
190-
| `--agents <list>` | init | Comma-separated agents (default: all) |
191-
| `--no-skill` | init | Skip installing the global Claude skill |
195+
| `--agents <list>` | init | Comma-separated agents, non-interactive (default: agents,claude) |
196+
| `--all` | init | Target every agent (non-interactive) |
197+
| `--skill` | init | Also install the global Claude skill (opt-in) |
198+
| `-y, --yes` | init | Accept defaults without prompting |
192199

193200
## Behavior
194201

llms-full.txt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -379,15 +379,17 @@ Community-detected file clusters (Louvain algorithm).
379379

380380
### init
381381
```bash
382-
codebase-intelligence init [path] [--agents <list>] [--no-skill] [--json]
382+
codebase-intelligence init [path] [--agents <list>] [--all] [--skill] [--yes] [--json]
383383
```
384-
Make AI agents actually use codebase-intelligence. Writes an idempotent, marked
385-
instruction block ("query CI before grep/read") into each agent's repo file —
384+
Set up AI agents to use codebase-intelligence. Writes an idempotent, marked
385+
instruction block ("query CI before grep/read") into each selected agent's repo file —
386386
`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/codebase-intelligence.mdc`,
387-
`.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md` — and installs the
388-
portable skill to `~/.claude/skills/codebase-intelligence/SKILL.md`. Only content
389-
between the `codebase-intelligence:start`/`:end` markers is ever touched, so re-running
390-
is safe. `--agents` limits targets (default: all); `--no-skill` skips the skill.
387+
`.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md`. Opt-in: on a TTY it
388+
shows an interactive picker (`AGENTS.md` + `CLAUDE.md` preselected); non-interactively
389+
(or `--yes`/`--json`) it defaults to those two. `--agents` selects explicitly, `--all`
390+
targets every agent, `--skill` also installs the portable skill to
391+
`~/.claude/skills/codebase-intelligence/SKILL.md` (never installed otherwise). Only
392+
content between the `codebase-intelligence:start`/`:end` markers is ever touched.
391393

392394
## Global Behavior
393395

src/cli.ts

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ import {
4343
import {
4444
installRepoFiles,
4545
installGlobalSkill,
46-
isAgentId,
46+
resolveInitPlan,
4747
ALL_AGENT_IDS,
4848
} from "./install/index.js";
49+
import { promptSelection } from "./install/prompt.js";
4950
import type { CodebaseGraph } from "./types/index.js";
50-
import type { AgentId } from "./install/index.js";
5151

5252
const INDEX_DIR_NAME = ".code-visualizer";
5353

@@ -188,7 +188,9 @@ interface McpOptions {
188188

189189
interface InitOptions {
190190
agents?: string;
191+
all?: boolean;
191192
skill?: boolean;
193+
yes?: boolean;
192194
json?: boolean;
193195
}
194196

@@ -953,36 +955,50 @@ program
953955

954956
program
955957
.command("init")
956-
.description("Make AI agents use codebase-intelligence: write per-agent instruction files + install the skill")
958+
.description("Set up AI agents to use codebase-intelligence: write per-agent instruction files (+ optional skill)")
957959
.argument("[path]", "Repo root (default: current directory)", ".")
958-
.option("--agents <list>", `Comma-separated agents to target (default: all). Available: ${ALL_AGENT_IDS.join(", ")}`)
959-
.option("--no-skill", "Skip installing the global Claude skill")
960-
.option("--json", "Output as JSON")
961-
.action((targetPath: string, options: InitOptions) => {
960+
.option("--agents <list>", `Comma-separated agents, non-interactive. Available: ${ALL_AGENT_IDS.join(", ")}`)
961+
.option("--all", "Target every agent (non-interactive)")
962+
.option("--skill", "Also install the global Claude skill (opt-in)")
963+
.option("-y, --yes", "Accept defaults without prompting")
964+
.option("--json", "Output as JSON (implies non-interactive)")
965+
.action(async (targetPath: string, options: InitOptions) => {
962966
const resolved = path.resolve(targetPath);
963967
if (!fs.existsSync(resolved)) {
964968
process.stderr.write(`Error: Path does not exist: ${targetPath}\n`);
965969
process.exit(1);
966970
}
967971

968-
let agents: AgentId[] | undefined;
969-
if (options.agents) {
970-
const requested = options.agents
971-
.split(",")
972-
.map((a) => a.trim())
973-
.filter(Boolean);
974-
const invalid = requested.filter((a) => !isAgentId(a));
975-
if (invalid.length > 0) {
976-
process.stderr.write(
977-
`Error: Unknown agents: ${invalid.join(", ")}. Available: ${ALL_AGENT_IDS.join(", ")}\n`,
978-
);
979-
process.exit(2);
972+
const isTty = process.stdin.isTTY && process.stdout.isTTY;
973+
const plan = resolveInitPlan(options, isTty);
974+
975+
if (plan.invalidAgents.length > 0) {
976+
process.stderr.write(
977+
`Error: Unknown agents: ${plan.invalidAgents.join(", ")}. Available: ${ALL_AGENT_IDS.join(", ")}\n`,
978+
);
979+
process.exit(2);
980+
}
981+
982+
let agents = plan.agents;
983+
let installSkill = plan.installSkill;
984+
985+
if (plan.mode === "interactive") {
986+
const selection = await promptSelection(agents, installSkill);
987+
if (!selection) {
988+
output("Cancelled — nothing written.");
989+
return;
980990
}
981-
agents = requested.filter(isAgentId);
991+
agents = selection.agents;
992+
installSkill = selection.skill;
993+
}
994+
995+
if (agents.length === 0 && !installSkill) {
996+
output("Nothing selected — nothing to do.");
997+
return;
982998
}
983999

9841000
const repoResults = installRepoFiles(resolved, { agents });
985-
const skillResult = options.skill === false ? undefined : installGlobalSkill();
1001+
const skillResult = installSkill ? installGlobalSkill() : undefined;
9861002

9871003
if (options.json) {
9881004
outputJson({ repoFiles: repoResults, skill: skillResult ?? null });
@@ -991,17 +1007,19 @@ program
9911007

9921008
output(`Codebase Intelligence — agent adoption`);
9931009
output(`──────────────────────────────────────`);
994-
output(`Repo instruction files (${resolved}):`);
995-
for (const r of repoResults) {
996-
output(` ${r.action.padEnd(9)} ${r.path}`);
1010+
if (repoResults.length > 0) {
1011+
output(`Repo instruction files (${resolved}):`);
1012+
for (const r of repoResults) {
1013+
output(` ${r.action.padEnd(9)} ${r.path}`);
1014+
}
9971015
}
9981016
if (skillResult) {
9991017
output(``);
10001018
output(`Global skill:`);
10011019
output(` ${skillResult.action.padEnd(9)} ${skillResult.path}`);
10021020
}
10031021
output(``);
1004-
output(`Done. Agents in this repo will now be told to query codebase-intelligence first.`);
1022+
output(`Done. Selected agents will be told to query codebase-intelligence first.`);
10051023
output(`Re-run anytime — writes are idempotent (managed blocks only).`);
10061024
});
10071025

src/install/index.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import {
1111
installRepoFiles,
1212
installGlobalSkill,
1313
isAgentId,
14+
resolveInitPlan,
1415
AGENT_TARGETS,
1516
ALL_AGENT_IDS,
17+
DEFAULT_AGENTS,
1618
DEFAULT_MARKERS,
1719
} from "./index.js";
1820

@@ -194,3 +196,54 @@ describe("installGlobalSkill", () => {
194196
expect(second.action).toBe("unchanged");
195197
});
196198
});
199+
200+
describe("resolveInitPlan", () => {
201+
it("defaults to AGENTS.md + CLAUDE.md only", () => {
202+
expect([...DEFAULT_AGENTS]).toEqual(["agents", "claude"]);
203+
});
204+
205+
it("--all selects every agent (explicit, non-interactive)", () => {
206+
const plan = resolveInitPlan({ all: true }, true);
207+
expect(plan.mode).toBe("explicit");
208+
expect(plan.agents).toEqual([...ALL_AGENT_IDS]);
209+
expect(plan.installSkill).toBe(false);
210+
});
211+
212+
it("--agents picks the listed agents", () => {
213+
const plan = resolveInitPlan({ agents: "claude,gemini" }, true);
214+
expect(plan.mode).toBe("explicit");
215+
expect(plan.agents).toEqual(["claude", "gemini"]);
216+
expect(plan.invalidAgents).toEqual([]);
217+
});
218+
219+
it("--agents reports unknown ids and keeps valid ones", () => {
220+
const plan = resolveInitPlan({ agents: "claude,bogus" }, true);
221+
expect(plan.agents).toEqual(["claude"]);
222+
expect(plan.invalidAgents).toEqual(["bogus"]);
223+
});
224+
225+
it("no flags on a TTY → interactive, seeded with the default set", () => {
226+
const plan = resolveInitPlan({}, true);
227+
expect(plan.mode).toBe("interactive");
228+
expect(plan.agents).toEqual([...DEFAULT_AGENTS]);
229+
});
230+
231+
it("no flags without a TTY → non-interactive default", () => {
232+
const plan = resolveInitPlan({}, false);
233+
expect(plan.mode).toBe("default");
234+
expect(plan.agents).toEqual([...DEFAULT_AGENTS]);
235+
});
236+
237+
it("--json forces non-interactive even on a TTY", () => {
238+
expect(resolveInitPlan({ json: true }, true).mode).toBe("default");
239+
});
240+
241+
it("--yes forces non-interactive even on a TTY", () => {
242+
expect(resolveInitPlan({ yes: true }, true).mode).toBe("default");
243+
});
244+
245+
it("--skill is opt-in across modes", () => {
246+
expect(resolveInitPlan({ skill: true }, false).installSkill).toBe(true);
247+
expect(resolveInitPlan({}, false).installSkill).toBe(false);
248+
});
249+
});

src/install/index.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,75 @@ export function isAgentId(value: string): value is AgentId {
112112
return (ALL_AGENT_IDS as readonly string[]).includes(value);
113113
}
114114

115+
/**
116+
* Default selection when the user doesn't choose explicitly: the universal
117+
* `AGENTS.md` standard plus `CLAUDE.md`. Everything else is opt-in.
118+
*/
119+
export const DEFAULT_AGENTS: readonly AgentId[] = ["agents", "claude"];
120+
121+
// ── Init planning (pure) ────────────────────────────────────
122+
123+
export interface InitFlags {
124+
/** Comma-separated agent ids from `--agents`. */
125+
agents?: string;
126+
/** `--all`: every agent. */
127+
all?: boolean;
128+
/** `--skill`: install the global skill (opt-in). */
129+
skill?: boolean;
130+
/** `--json`: machine output, implies non-interactive. */
131+
json?: boolean;
132+
/** `--yes`: accept defaults without prompting. */
133+
yes?: boolean;
134+
}
135+
136+
export type InitMode = "explicit" | "interactive" | "default";
137+
138+
export interface InitPlan {
139+
/** Agents to write (preselection when mode is "interactive"). */
140+
agents: AgentId[];
141+
/** Whether to install the global skill (default when mode is "interactive"). */
142+
installSkill: boolean;
143+
/** How the selection was decided. "interactive" → caller should prompt. */
144+
mode: InitMode;
145+
/** Unknown ids passed to `--agents`. */
146+
invalidAgents: string[];
147+
}
148+
149+
/**
150+
* Decide what `init` should do from its flags and whether stdout is a TTY.
151+
* Pure — no prompting or filesystem. When `mode` is "interactive" the caller
152+
* presents a picker seeded with `agents`/`installSkill`; otherwise the plan is
153+
* final.
154+
*/
155+
export function resolveInitPlan(flags: InitFlags, isTty: boolean): InitPlan {
156+
const installSkill = flags.skill === true;
157+
158+
if (flags.all === true) {
159+
return { agents: [...ALL_AGENT_IDS], installSkill, mode: "explicit", invalidAgents: [] };
160+
}
161+
162+
if (flags.agents !== undefined) {
163+
const requested = flags.agents
164+
.split(",")
165+
.map((a) => a.trim())
166+
.filter(Boolean);
167+
return {
168+
agents: requested.filter(isAgentId),
169+
installSkill,
170+
mode: "explicit",
171+
invalidAgents: requested.filter((a) => !isAgentId(a)),
172+
};
173+
}
174+
175+
const interactive = isTty && flags.json !== true && flags.yes !== true;
176+
return {
177+
agents: [...DEFAULT_AGENTS],
178+
installSkill,
179+
mode: interactive ? "interactive" : "default",
180+
invalidAgents: [],
181+
};
182+
}
183+
115184
// ── Content (single source of truth) ────────────────────────
116185

117186
/**

0 commit comments

Comments
 (0)