Skip to content

Commit 6648e80

Browse files
committed
feat(mcp): add /review slash command for conversational feedback triage
Registers a `review` MCP prompt on the server that loads all open feedback sorted by votes and returns a structured prompt guiding Claude through a one-item-at-a-time triage loop (publish, dismiss, skip, quit). Also installs a `.claude/commands/suggestion-box/review.md` file during `init` as a local `/suggestion-box:review` slash command alternative, and cleans it up on `uninit`. Fixes: `suggestion_box_publish_to_github` was missing from ALLOWED_TOOLS, so `init` never pre-authorized it in `permissions.allow`. Closes #104
1 parent ce31bc6 commit 6648e80

File tree

2 files changed

+141
-0
lines changed

2 files changed

+141
-0
lines changed

src/cli.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const ALLOWED_TOOLS = [
3434
"mcp__suggestion-box__suggestion_box_list_feedback",
3535
"mcp__suggestion-box__suggestion_box_status",
3636
"mcp__suggestion-box__suggestion_box_dismiss_feedback",
37+
"mcp__suggestion-box__suggestion_box_publish_to_github",
3738
];
3839

3940
function getCliCommand(): { command: string; args: string[] } {
@@ -499,6 +500,36 @@ enabled = true
499500
}
500501
}
501502

503+
// .claude/commands/suggestion-box/review.md — /review slash command
504+
const commandsDir = join(claudeSettingsDir, "commands", "suggestion-box");
505+
const reviewCmdPath = join(commandsDir, "review.md");
506+
const reviewCmdContent = `---
507+
description: Triage all open suggestion-box feedback — publish, dismiss, or skip each item
508+
---
509+
Run the suggestion-box review flow: list all open feedback and triage each item one by one.
510+
511+
Use the \`suggestion_box_list_feedback\` MCP tool (status: open, sort_by: votes) to load the queue, then for each item ask the user: **publish**, **dismiss**, **skip**, or **quit**.
512+
513+
- **publish** → call \`suggestion_box_publish_to_github\` (ask for \`github_repo\` if missing)
514+
- **dismiss** → call \`suggestion_box_dismiss_feedback\`
515+
- **skip** → leave unchanged, move on
516+
- **quit** → stop and show summary
517+
518+
After finishing, show a summary: how many published, dismissed, skipped, and links to any issues created.
519+
520+
Tip: observation-category items rarely warrant a public GitHub issue — mention this when you encounter them.
521+
`;
522+
523+
if (dryRun) {
524+
console.log(`${prefix}Would create .claude/commands/suggestion-box/review.md (/review slash command)`);
525+
} else {
526+
if (!existsSync(commandsDir)) mkdirSync(commandsDir, { recursive: true });
527+
if (!existsSync(reviewCmdPath)) {
528+
writeFileSync(reviewCmdPath, reviewCmdContent);
529+
console.log(" Wrote .claude/commands/suggestion-box/review.md (/suggestion-box:review slash command)");
530+
}
531+
}
532+
502533
if (dryRun) {
503534
console.log(`\n${prefix}No files were modified.`);
504535
} else {
@@ -631,6 +662,26 @@ enabled = true
631662
} catch {}
632663
}
633664

665+
// Remove .claude/commands/suggestion-box/review.md
666+
const uninitReviewCmdPath = join(targetDir, ".claude", "commands", "suggestion-box", "review.md");
667+
if (existsSync(uninitReviewCmdPath)) {
668+
rmSync(uninitReviewCmdPath);
669+
console.log(" Removed .claude/commands/suggestion-box/review.md");
670+
// Clean up the suggestion-box commands dir if empty
671+
try {
672+
const sbCmdsDir = join(targetDir, ".claude", "commands", "suggestion-box");
673+
if (readdirSync(sbCmdsDir).length === 0) {
674+
rmSync(sbCmdsDir, { recursive: true });
675+
// Clean up commands dir if empty too
676+
const cmdsDir = join(targetDir, ".claude", "commands");
677+
if (existsSync(cmdsDir) && readdirSync(cmdsDir).length === 0) {
678+
rmSync(cmdsDir, { recursive: true });
679+
}
680+
}
681+
} catch {}
682+
removed++;
683+
}
684+
634685
// Handle .suggestion-box directory
635686
const dataDir = join(targetDir, ".suggestion-box");
636687
if (existsSync(dataDir)) {

src/mcp.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,96 @@ If similar feedback already exists, your submission becomes a vote on it instead
287287
},
288288
);
289289

290+
// -------------------------------------------------------------------------
291+
// Prompt: review (slash command /review)
292+
// -------------------------------------------------------------------------
293+
server.prompt(
294+
"review",
295+
`Walk through all open feedback items one by one and triage each one (publish, dismiss, or skip).`,
296+
{},
297+
async () => {
298+
const items = await store.listFeedback({ status: "open", sortBy: "votes" });
299+
300+
if (items.length === 0) {
301+
return {
302+
messages: [
303+
{
304+
role: "user" as const,
305+
content: {
306+
type: "text" as const,
307+
text: "No open feedback items found in suggestion-box. The queue is empty — nothing to review.",
308+
},
309+
},
310+
],
311+
};
312+
}
313+
314+
const itemSummaries = items.map((item, i) => {
315+
const impact = [
316+
item.estimatedTokensSaved ? `~${item.estimatedTokensSaved} tokens saved` : null,
317+
item.estimatedTimeSavedMinutes ? `~${item.estimatedTimeSavedMinutes}min saved` : null,
318+
].filter(Boolean).join(", ");
319+
320+
const repoHint = item.githubRepo ? ` (repo: ${item.githubRepo})` : "";
321+
const titleLine = item.title ? `Title: ${item.title}\n` : "";
322+
const impactLine = impact ? `Impact: ${impact}\n` : "";
323+
324+
return `### Item ${i + 1} of ${items.length}
325+
ID: ${item.id}
326+
Category: ${item.category} | Votes: ${item.votes} | Status: ${item.status}
327+
Target: ${item.targetType}/${item.targetName}${repoHint}
328+
${titleLine}${impactLine}Content:
329+
${item.content}`;
330+
}).join("\n\n---\n\n");
331+
332+
const promptText = `You are running the suggestion-box review flow. There are **${items.length} open feedback items** to triage.
333+
334+
Go through them one by one, in the order presented. For each item:
335+
336+
1. **Show** the item clearly (ID, category, votes, content, target, impact if available).
337+
2. **Ask** the user what to do:
338+
- **publish** — publish it as a GitHub issue (use \`suggestion_box_publish_to_github\`)
339+
- If the item has no \`github_repo\`, ask the user to provide one (format: \`owner/repo\`)
340+
- **dismiss** — mark it as dismissed (use \`suggestion_box_dismiss_feedback\`)
341+
- **skip** — leave it as-is and move on
342+
- **quit** — stop the review session early
343+
3. **Execute** the chosen action using the appropriate MCP tool.
344+
4. **Confirm** the result and move to the next item.
345+
346+
After all items are processed (or the user quits), show a **summary**:
347+
- How many were published, dismissed, skipped
348+
- Links to any GitHub issues created
349+
350+
**Important notes:**
351+
- Be conversational — one item at a time, wait for the user's decision before acting.
352+
- When publishing, if \`suggestion_box_publish_to_github\` finds an existing GitHub issue, report the deduplication result.
353+
- Observations are usually not worth publishing publicly — mention this when you encounter observation-category items (but let the user decide).
354+
- Sort preference: highest votes first (already sorted in the list below).
355+
356+
---
357+
358+
## Pending Feedback Queue (${items.length} items)
359+
360+
${itemSummaries}
361+
362+
---
363+
364+
Start now: present **Item 1** and ask the user what to do.`;
365+
366+
return {
367+
messages: [
368+
{
369+
role: "user" as const,
370+
content: {
371+
type: "text" as const,
372+
text: promptText,
373+
},
374+
},
375+
],
376+
};
377+
},
378+
);
379+
290380
const transport = new StdioServerTransport();
291381
await server.connect(transport);
292382

0 commit comments

Comments
 (0)