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
40 changes: 27 additions & 13 deletions src/agent/command-suggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ export const ACTIONABLE_COMMAND_PREFIXES = [
*/
const PLACEHOLDER_RE = /example\.(?:com|net|org)|<[^>]+>/i;

function cleanCommandCandidate(candidate: string): string {
return candidate
.trim()
.replace(/^[`*_]+/g, "")
.replace(/[`*_]+$/g, "")
.trim();
}

/**
* Scan the assistant's response text for slash commands that match
* actionable prefixes. Returns deduplicated commands in order.
Expand All @@ -35,34 +43,40 @@ export function extractSuggestedCommands(text: string): string[] {
const commands: string[] = [];
const seen = new Set<string>();

const addCommand = (candidate: string): void => {
const cmd = cleanCommandCandidate(candidate);
if (!cmd || seen.has(cmd) || PLACEHOLDER_RE.test(cmd)) return;
seen.add(cmd);
commands.push(cmd);
};

// Pattern 1: commands inside backticks — `/plugin enable fetch ...`
// This catches inline code references the LLM wraps in backticks.
const backtickRe =
/`(\/(?:plugin\s+enable|plugin\s+disable|mcp\s+enable|buffer|timeout|set)\s[^`]+)`/gi;
for (const m of text.matchAll(backtickRe)) {
const cmd = m[1].trim();
if (!seen.has(cmd) && !PLACEHOLDER_RE.test(cmd)) {
seen.add(cmd);
commands.push(cmd);
}
addCommand(m[1]);
}

// Pattern 2: commands inside markdown bold — **/mcp enable ...**
// The model often emphasises auth/setup commands this way.
const boldRe =
/\*\*(\/(?:plugin\s+enable|plugin\s+disable|mcp\s+enable|buffer|timeout|set)\s(?:(?!\*\*)[^\n])+)\*\*/gi;
for (const m of text.matchAll(boldRe)) {
addCommand(m[1]);
}

// Pattern 2: bare commands as the start of a line (possibly indented).
// Pattern 3: bare commands as the start of a line (possibly indented).
// Only matched if not already found via backtick pattern.
for (const line of text.split("\n")) {
const trimmed = line.trim();
const trimmed = cleanCommandCandidate(line);
if (
trimmed.startsWith("/") &&
ACTIONABLE_COMMAND_PREFIXES.some((p) =>
trimmed.toLowerCase().startsWith(p.toLowerCase()),
)
) {
// Strip any trailing markdown/punctuation the LLM might append
const cleaned = trimmed.replace(/[`*_]+$/g, "").trim();
if (cleaned && !seen.has(cleaned) && !PLACEHOLDER_RE.test(cleaned)) {
seen.add(cleaned);
commands.push(cleaned);
}
addCommand(trimmed);
}
}

Expand Down
33 changes: 33 additions & 0 deletions tests/command-suggestions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,39 @@ describe("extractSuggestedCommands", () => {
expect(extractSuggestedCommands(text)).toEqual(["/set heap 16"]);
});

// ── Markdown-emphasised commands ────────────────────────────

it("should extract a bold /mcp enable command", () => {
const text = [
"The Microsoft Teams MCP server requires authentication.",
"",
"**/mcp enable work-iq-teams**",
"",
"This will prompt you to authenticate in your browser.",
].join("\n");

expect(extractSuggestedCommands(text)).toEqual([
"/mcp enable work-iq-teams",
]);
});

it("should extract an inline bold /mcp enable command", () => {
const text = "Please run **/mcp enable work-iq-teams** to authenticate.";

expect(extractSuggestedCommands(text)).toEqual([
"/mcp enable work-iq-teams",
]);
});

it("should preserve wildcard arguments in bold commands", () => {
const text =
"Run **/plugin enable fetch allowedDomains=[*.bbc.co.uk,feeds.bbci.co.uk]**";

expect(extractSuggestedCommands(text)).toEqual([
"/plugin enable fetch allowedDomains=[*.bbc.co.uk,feeds.bbci.co.uk]",
]);
});

// ── Bare commands on their own line ──────────────────────────

it("should extract a bare /plugin enable on its own line", () => {
Expand Down
Loading