Files
cobble/src/utils/find-skill-mentions.ts
T
tbenandClaude Opus 5.5 fefc295d15 Let the coordinator manage skills, MCP servers and tools, behind a confirm card
- Coordinator tools in its own chat (super admin only): list_extensions,
  set_extension_access, delete_extension, and update_skill /
  update_mcp_server / update_custom_tool for what can't change where
  anything is sent (on/off, names, arguments, left-out tools)
- Adding a skill, MCP server or tool, or changing a skill's words, a
  server's URL or headers or a tool's kind or config, goes through
  propose_skill / propose_mcp_server / propose_custom_tool: stored in
  extension_proposals with the bot's background check, shown as a
  [proposal:ID] chip that opens a card with Confirm and Reject
- The card's facts are worked out by the app from what Confirm saves: the
  host, every {{env.KEY}} and whether it's saved, the config; for a skill
  its links, hosts, tool names, mentions of secrets and a diff against
  the saved runbook
- Confirming decides once and checks again; a server or tool missing a key
  is saved switched off, and the outcome is posted to the bot in chat
- Never from a bot: stdio servers, a URL whose host a template decides,
  envs in a prompt tool, a key's value

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 06:08:09 +01:00

56 lines
1.9 KiB
TypeScript

import literalUrlHost from "./literal-url-host";
type Params = {
text: string;
};
const UrlPattern = /https?:\/\/[^\s)<>\]"'`]+/gi;
const ToolPattern = /\b[a-z][a-z0-9]*(?:__?[a-z0-9]+)+\b/g;
const SecretPatterns = [
/\{\{\s*env\s*\.[^}]*\}\}/gi,
/~?\/?\.cobble\/[\w.\/-]*/gi,
/\benv\.json\b/gi,
/\$\{?[A-Z][A-Z0-9_]{2,}\}?/g,
/\b(?:process\.env|os\.environ)\b/g,
/\b(?:api[ _-]?keys?|access[ _-]?tokens?|tokens?|passwords?|secrets?|credentials?|cookies?|private[ _-]?keys?)\b/gi,
];
const MaxEach = 30;
/**
* What a skill's runbook would have bots reach for, read from its text
* by the app so the confirm card never depends on the bot's account of
* it: the links in it and their hosts, every snake_case name (what tools
* are called, e.g. `web_fetch_exa`, `exa_search__web_fetch_exa`), and
* anything that touches secrets (envs, `~/.cobble/env`, `$API_KEY`,
* words like token or password). Each list is deduplicated and capped.
*/
export default function findSkillMentions({ text }: Params) {
const urls = unique({ values: [...text.matchAll(UrlPattern)].map((m) => m[0].replace(/[.,;:!?]+$/, "")) });
const hosts = unique({
values: urls.map((url) => literalUrlHost({ url })).filter((host): host is string => Boolean(host)),
});
const tools = unique({ values: [...text.matchAll(ToolPattern)].map((m) => m[0]) });
const secrets = unique({
values: SecretPatterns.flatMap((pattern) => [...text.matchAll(pattern)].map((m) => m[0].trim())),
fold: true,
});
return { urls, hosts, tools, secrets };
}
function unique({ values, fold }: { values: string[]; fold?: boolean }) {
const seen = new Set<string>();
const kept: string[] = [];
for (const value of values) {
const id = fold ? value.toLowerCase() : value;
if (!value || seen.has(id)) continue;
seen.add(id);
kept.push(value);
}
return kept.slice(0, MaxEach);
}