Skip to content

Commit 1b09b99

Browse files
njb90claude
andcommitted
docs: add implementation plan for configure-ai-tools v2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f7c9363 commit 1b09b99

1 file changed

Lines changed: 383 additions & 0 deletions

File tree

Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
# Configure AI Tools v2 Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Switch MCP server configs to remote OAuth URLs and add status annotations to every QuickPick so users see what's already installed when re-running the command.
6+
7+
**Architecture:** All changes are in `src/commands/configureAiTools.ts`. Three sequential tasks: (1) simplify `McpServerDef` and update server URLs, (2) add two status-detection helpers, (3) reorder the skills flow and wire status into both pickers.
8+
9+
**Tech Stack:** TypeScript, VS Code Extension API (`vscode.workspace.fs`, `vscode.window.showQuickPick`, `vscode.window.createQuickPick`)
10+
11+
---
12+
13+
### Task 1: Simplify McpServerDef and switch to remote OAuth URLs
14+
15+
**Files:**
16+
- Modify: `src/commands/configureAiTools.ts:7-15` (type), `src/commands/configureAiTools.ts:312-430` (MCP_SERVERS + createMcpConfig)
17+
18+
The four Cloudinary LLM MCP servers now use remote OAuth endpoints — no credentials in the file. MediaFlows keeps its headers-based config. Since both editors now use the same server entry format (only the root key `"servers"` vs `"mcpServers"` differs), the `McpServerDef` type simplifies from two config fields to one.
19+
20+
- [ ] **Step 1: Replace the `McpServerDef` type**
21+
22+
In `src/commands/configureAiTools.ts`, replace lines 7–15:
23+
24+
```typescript
25+
type McpServerDef = {
26+
label: string;
27+
description: string;
28+
key: string;
29+
config: Record<string, unknown>; // same for all editors; root key differs by editor
30+
};
31+
```
32+
33+
- [ ] **Step 2: Replace the entire `MCP_SERVERS` array**
34+
35+
Replace the `MCP_SERVERS` constant (currently lines 312–430) with:
36+
37+
```typescript
38+
const MCP_SERVERS: McpServerDef[] = [
39+
{
40+
label: "Cloudinary Asset Management",
41+
description: "Browse, upload, and manage media assets",
42+
key: "cloudinary-asset-mgmt",
43+
config: { url: "https://asset-management.mcp.cloudinary.com/mcp" },
44+
},
45+
{
46+
label: "Cloudinary Environment Config",
47+
description: "Configure upload presets, transformations, and settings",
48+
key: "cloudinary-env-config",
49+
config: { url: "https://environment-config.mcp.cloudinary.com/mcp" },
50+
},
51+
{
52+
label: "Cloudinary Structured Metadata",
53+
description: "Manage structured metadata fields and values",
54+
key: "cloudinary-smd",
55+
config: { url: "https://structured-metadata.mcp.cloudinary.com/mcp" },
56+
},
57+
{
58+
label: "Cloudinary Analysis",
59+
description: "AI-powered image and video analysis",
60+
key: "cloudinary-analysis",
61+
config: { url: "https://analysis.mcp.cloudinary.com/sse" },
62+
},
63+
{
64+
label: "MediaFlows",
65+
description: "AI-powered media workflows and automation",
66+
key: "mediaflows",
67+
config: {
68+
url: "https://mediaflows.mcp.cloudinary.com/v2/mcp",
69+
headers: {
70+
"cld-cloud-name": "your_cloud_name",
71+
"cld-api-key": "your_api_key",
72+
"cld-secret": "your_api_secret",
73+
},
74+
},
75+
},
76+
];
77+
```
78+
79+
- [ ] **Step 3: Update `createMcpConfig` to use `def.config`**
80+
81+
Inside `createMcpConfig`, find the loop that writes server entries and change it from:
82+
```typescript
83+
servers[def.key] = isVscode ? def.vscodeConfig : def.cursorConfig;
84+
```
85+
to:
86+
```typescript
87+
servers[def.key] = def.config;
88+
```
89+
90+
- [ ] **Step 4: Type-check**
91+
92+
```bash
93+
npm run check-types
94+
```
95+
96+
Expected: no errors.
97+
98+
- [ ] **Step 5: Commit**
99+
100+
```bash
101+
git add src/commands/configureAiTools.ts
102+
git commit -m "feat: switch MCP servers to remote OAuth URLs"
103+
```
104+
105+
---
106+
107+
### Task 2: Add status-detection helpers
108+
109+
**Files:**
110+
- Modify: `src/commands/configureAiTools.ts` — add two functions in the `// ── MCP Config` section, before `createMcpConfig`
111+
112+
- [ ] **Step 1: Add `readInstalledSkillDirNames`**
113+
114+
Insert this function immediately before the `// ── MCP Server definitions` comment:
115+
116+
```typescript
117+
// ── Status detection ─────────────────────────────────────────────────────────
118+
119+
/**
120+
* Returns the set of skill dirNames already installed for the given IDE target.
121+
* Errors reading individual paths are silently treated as "not installed".
122+
*/
123+
async function readInstalledSkillDirNames(
124+
rootUri: vscode.Uri,
125+
ideTargetLabel: string,
126+
skills: SkillInfo[]
127+
): Promise<Set<string>> {
128+
const installed = new Set<string>();
129+
130+
if (ideTargetLabel === "VS Code (Copilot)") {
131+
try {
132+
const uri = vscode.Uri.joinPath(rootUri, ".github/copilot-instructions.md");
133+
const bytes = await vscode.workspace.fs.readFile(uri);
134+
const content = Buffer.from(bytes).toString("utf-8");
135+
for (const skill of skills) {
136+
if (content.includes(`## ${skill.name}`)) {
137+
installed.add(skill.dirName);
138+
}
139+
}
140+
} catch {
141+
// file not found — nothing installed
142+
}
143+
return installed;
144+
}
145+
146+
await Promise.all(
147+
skills.map(async (skill) => {
148+
try {
149+
const checkPath =
150+
ideTargetLabel === "Claude Code"
151+
? `.claude/skills/${skill.dirName}/SKILL.md`
152+
: `.cursor/rules/${skill.dirName}.mdc`;
153+
await vscode.workspace.fs.stat(vscode.Uri.joinPath(rootUri, checkPath));
154+
installed.add(skill.dirName);
155+
} catch {
156+
// not installed
157+
}
158+
})
159+
);
160+
return installed;
161+
}
162+
```
163+
164+
- [ ] **Step 2: Add `readConfiguredMcpServerKeys`**
165+
166+
Insert this function immediately after `readInstalledSkillDirNames`:
167+
168+
```typescript
169+
/**
170+
* Returns the set of server keys already present in the MCP config file.
171+
* Returns an empty Set if the file doesn't exist or can't be parsed.
172+
*/
173+
async function readConfiguredMcpServerKeys(
174+
rootUri: vscode.Uri,
175+
mcpFilePath: string,
176+
rootKey: string
177+
): Promise<Set<string>> {
178+
try {
179+
const uri = vscode.Uri.joinPath(rootUri, mcpFilePath);
180+
const bytes = await vscode.workspace.fs.readFile(uri);
181+
const config = JSON.parse(Buffer.from(bytes).toString("utf-8"));
182+
const servers = config[rootKey];
183+
if (servers && typeof servers === "object") {
184+
return new Set(Object.keys(servers));
185+
}
186+
} catch {
187+
// file not found or invalid JSON
188+
}
189+
return new Set();
190+
}
191+
```
192+
193+
- [ ] **Step 3: Type-check**
194+
195+
```bash
196+
npm run check-types
197+
```
198+
199+
Expected: no errors.
200+
201+
- [ ] **Step 4: Commit**
202+
203+
```bash
204+
git add src/commands/configureAiTools.ts
205+
git commit -m "feat: add skill and MCP status-detection helpers"
206+
```
207+
208+
---
209+
210+
### Task 3: Reorder skills flow and annotate both QuickPicks
211+
212+
**Files:**
213+
- Modify: `src/commands/configureAiTools.ts` — the skills block inside `registerConfigureAiTools` (currently lines ~507–573) and `createMcpConfig`
214+
215+
**Skills flow change:** IDE picker moves before the skills picker so `readInstalledSkillDirNames` can check the right paths before presenting the list.
216+
217+
**MCP flow change:** `createMcpConfig` calls `readConfiguredMcpServerKeys` before building its picker items.
218+
219+
- [ ] **Step 1: Replace the entire skills block inside `registerConfigureAiTools`**
220+
221+
Replace the block from `// ── Step 2: skills flow` through its closing `}` with:
222+
223+
```typescript
224+
// ── Step 2: skills flow ────────────────────────────────────────────────
225+
if (options.some((o) => o.label === "Skills")) {
226+
let skills: SkillInfo[];
227+
try {
228+
skills = await fetchSkillList();
229+
} catch (err: any) {
230+
vscode.window.showErrorMessage(`Failed to fetch skills: ${err.message}`);
231+
return;
232+
}
233+
234+
// IDE target first — needed to check install status before showing skills
235+
const editor = detectEditor();
236+
const ideOptions: vscode.QuickPickItem[] = [
237+
{ label: "Claude Code", description: "Install to .claude/skills/" },
238+
{ label: "Cursor", description: "Install to .cursor/rules/" },
239+
{ label: "VS Code (Copilot)", description: "Append to .github/copilot-instructions.md" },
240+
];
241+
const defaultLabel =
242+
editor === "cursor" ? "Cursor" :
243+
editor === "vscode" ? "VS Code (Copilot)" :
244+
"Claude Code";
245+
246+
const qp = vscode.window.createQuickPick();
247+
qp.items = ideOptions;
248+
qp.activeItems = ideOptions.filter((o) => o.label === defaultLabel);
249+
qp.placeholder = "Select AI tool to install skills for";
250+
251+
const ideTarget = await new Promise<vscode.QuickPickItem | undefined>((resolve) => {
252+
qp.onDidAccept(() => { resolve(qp.activeItems[0]); qp.dispose(); });
253+
qp.onDidHide(() => { resolve(undefined); qp.dispose(); });
254+
qp.show();
255+
});
256+
if (!ideTarget) { return; }
257+
258+
const installedDirNames = await readInstalledSkillDirNames(rootUri, ideTarget.label, skills);
259+
260+
const pickedSkills = await vscode.window.showQuickPick(
261+
skills.map((s) => ({
262+
label: s.name,
263+
description: s.description,
264+
detail: installedDirNames.has(s.dirName) ? "✓ installed" : "Not installed",
265+
picked: true,
266+
})),
267+
{ canPickMany: true, placeHolder: "Select skills to install" }
268+
);
269+
if (!pickedSkills || pickedSkills.length === 0) { return; }
270+
271+
for (const item of pickedSkills) {
272+
const skill = skills.find((s) => s.name === item.label);
273+
if (!skill) { continue; }
274+
let content: string;
275+
try {
276+
content = await fetchSkillContent(skill.dirName);
277+
} catch (err: any) {
278+
errors.push(`${skill.dirName}: ${err.message}`);
279+
continue;
280+
}
281+
282+
if (ideTarget.label === "Claude Code") {
283+
await installForClaudeCode(rootUri, skill.dirName, content, createdFiles, errors);
284+
} else if (ideTarget.label === "Cursor") {
285+
try {
286+
await installForCursor(rootUri, skill.dirName, content, createdFiles);
287+
} catch (err) {
288+
errors.push(`${skill.dirName}: ${err instanceof Error ? err.message : String(err)}`);
289+
}
290+
} else {
291+
try {
292+
await installForCopilot(rootUri, skill.name, content, createdFiles);
293+
} catch (err) {
294+
errors.push(`${skill.name}: ${err instanceof Error ? err.message : String(err)}`);
295+
}
296+
}
297+
}
298+
}
299+
```
300+
301+
- [ ] **Step 2: Update `createMcpConfig` to annotate the server picker**
302+
303+
Replace the `showQuickPick` call and the `selectedDefs` derivation inside `createMcpConfig` with:
304+
305+
```typescript
306+
async function createMcpConfig(
307+
rootUri: vscode.Uri,
308+
editor: EditorType,
309+
mcpFilePath: string,
310+
createdFiles: string[]
311+
): Promise<void> {
312+
const isVscode = editor === "vscode";
313+
const rootKey = isVscode ? "servers" : "mcpServers";
314+
const configuredKeys = await readConfiguredMcpServerKeys(rootUri, mcpFilePath, rootKey);
315+
316+
const selected = await vscode.window.showQuickPick(
317+
MCP_SERVERS.map((s) => ({
318+
label: s.label,
319+
description: s.description,
320+
detail: configuredKeys.has(s.key) ? "✓ already configured" : "Not configured",
321+
picked: !configuredKeys.has(s.key),
322+
})),
323+
{ canPickMany: true, placeHolder: "Select MCP servers to configure" }
324+
);
325+
if (!selected || selected.length === 0) { return; }
326+
327+
const selectedDefs = selected
328+
.map((item) => MCP_SERVERS.find((s) => s.label === item.label))
329+
.filter((s): s is McpServerDef => s !== undefined);
330+
331+
const mcpUri = vscode.Uri.joinPath(rootUri, mcpFilePath);
332+
333+
// Read and merge into existing config if present
334+
let config: Record<string, unknown> = {};
335+
try {
336+
const bytes = await vscode.workspace.fs.readFile(mcpUri);
337+
config = JSON.parse(Buffer.from(bytes).toString("utf-8"));
338+
} catch {
339+
// new file
340+
}
341+
342+
if (!config[rootKey] || typeof config[rootKey] !== "object") {
343+
config[rootKey] = {};
344+
}
345+
const servers = config[rootKey] as Record<string, unknown>;
346+
347+
for (const def of selectedDefs) {
348+
servers[def.key] = def.config;
349+
}
350+
351+
await ensureDir(vscode.Uri.joinPath(mcpUri, ".."));
352+
await vscode.workspace.fs.writeFile(
353+
mcpUri,
354+
Buffer.from(JSON.stringify(config, null, 2), "utf-8")
355+
);
356+
if (!createdFiles.includes(mcpFilePath)) {
357+
createdFiles.push(mcpFilePath);
358+
}
359+
}
360+
```
361+
362+
- [ ] **Step 3: Type-check**
363+
364+
```bash
365+
npm run check-types
366+
```
367+
368+
Expected: no errors.
369+
370+
- [ ] **Step 4: Run test suite**
371+
372+
```bash
373+
npm run compile-tests && npm run test
374+
```
375+
376+
Expected: `1 passing`
377+
378+
- [ ] **Step 5: Commit**
379+
380+
```bash
381+
git add src/commands/configureAiTools.ts
382+
git commit -m "feat: reorder skills flow and annotate QuickPicks with install status"
383+
```

0 commit comments

Comments
 (0)