CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
consent.ts278 linesDownload Raw Back to extensions
1import type {2  ClaudeMarketplaceConfig,3  ExtensionConfig,4  ExtensionRequestOptions,5  SkillConfig,6  SubagentConfig,7} from '@qwen-code/qwen-code-core';8import type { ConfirmationRequest } from '../../ui/types.js';9import chalk from 'chalk';10import prompts from 'prompts';11import stripAnsi from 'strip-ansi';12import { t } from '../../i18n/index.js';13import { writeStdoutLine } from '../../utils/stdioHelpers.js';14 15/**16 * Requests consent from the user to perform an action, by reading a Y/n17 * character from stdin.18 *19 * This should not be called from interactive mode as it will break the CLI.20 *21 * @param consentDescription The description of the thing they will be consenting to.22 * @returns boolean, whether they consented or not.23 */24export async function requestConsentNonInteractive(25  consentDescription: string,26): Promise<boolean> {27  writeStdoutLine(consentDescription);28  const result = await promptForConsentNonInteractive(29    t('Do you want to continue? [Y/n]: '),30  );31  return result;32}33 34/**35 * Requests plugin selection from the user in non-interactive mode.36 * Displays an interactive list with arrow key navigation.37 *38 * This should not be called from interactive mode as it will break the CLI.39 *40 * @param marketplace The marketplace config containing available plugins.41 * @returns The name of the selected plugin.42 */43export async function requestChoicePluginNonInteractive(44  marketplace: ClaudeMarketplaceConfig,45): Promise<string> {46  const plugins = marketplace.plugins;47 48  if (plugins.length === 0) {49    throw new Error(t('No plugins available in this marketplace.'));50  }51 52  // Build choices for prompts select53 54  const choices = plugins.map((plugin) => ({55    title: chalk.green(chalk.bold(`[${plugin.name}]`)),56    value: plugin.name,57  }));58 59  const response = await prompts({60    type: 'select',61    name: 'plugin',62    message: t('Select a plugin to install from marketplace "{{name}}":', {63      name: marketplace.name,64    }),65    choices,66    initial: 0,67  });68 69  // Handle cancellation (Ctrl+C)70  if (response.plugin === undefined) {71    throw new Error(t('Plugin selection cancelled.'));72  }73 74  return response.plugin;75}76 77/**78 * Requests consent from the user to perform an action, in interactive mode.79 *80 * This should not be called from non-interactive mode as it will not work.81 *82 * @param consentDescription The description of the thing they will be consenting to.83 * @param addExtensionUpdateConfirmationRequest A function to actually add a prompt to the UI.84 * @returns boolean, whether they consented or not.85 */86export async function requestConsentInteractive(87  consentDescription: string,88  addExtensionUpdateConfirmationRequest: (value: ConfirmationRequest) => void,89): Promise<boolean> {90  return promptForConsentInteractive(91    consentDescription + '\n\n' + t('Do you want to continue?'),92    addExtensionUpdateConfirmationRequest,93  );94}95 96/**97 * Asks users a prompt and awaits for a y/n response on stdin.98 *99 * This should not be called from interactive mode as it will break the CLI.100 *101 * @param prompt A yes/no prompt to ask the user102 * @returns Whether or not the user answers 'y' (yes). Defaults to 'yes' on enter.103 */104async function promptForConsentNonInteractive(105  prompt: string,106): Promise<boolean> {107  const readline = await import('node:readline');108  const rl = readline.createInterface({109    input: process.stdin,110    output: process.stdout,111  });112 113  return new Promise((resolve) => {114    rl.question(prompt, (answer) => {115      rl.close();116      resolve(['y', ''].includes(answer.trim().toLowerCase()));117    });118  });119}120 121/**122 * Asks users an interactive yes/no prompt.123 *124 * This should not be called from non-interactive mode as it will break the CLI.125 *126 * @param prompt A markdown prompt to ask the user127 * @param addExtensionUpdateConfirmationRequest Function to update the UI state with the confirmation request.128 * @returns Whether or not the user answers yes.129 */130async function promptForConsentInteractive(131  prompt: string,132  addExtensionUpdateConfirmationRequest: (value: ConfirmationRequest) => void,133): Promise<boolean> {134  return new Promise<boolean>((resolve) => {135    addExtensionUpdateConfirmationRequest({136      prompt,137      onConfirm: (resolvedConfirmed) => {138        resolve(resolvedConfirmed);139      },140    });141  });142}143 144/**145 * Builds a consent string for installing an extension based on it's146 * extensionConfig.147 */148export function extensionConsentString(149  extensionConfig: ExtensionConfig,150  commands: string[] = [],151  skills: SkillConfig[] = [],152  subagents: SubagentConfig[] = [],153  originSource: string = 'QwenCode',154): string {155  const output: string[] = [];156  if (originSource !== 'QwenCode') {157    output.push(158      t(159        'You are installing an extension from {{originSource}}. Some features may not work perfectly with Qwen Code.',160        { originSource },161      ),162    );163  }164  const mcpServerEntries = Object.entries(extensionConfig.mcpServers || {});165  const displayLabel = extensionConfig.displayName ?? extensionConfig.name;166  output.push(167    t('Installing extension "{{name}}".', { name: displayLabel }),168  );169  if (170    typeof extensionConfig.description === 'string' &&171    extensionConfig.description172  ) {173    output.push(stripAnsi(extensionConfig.description));174  }175  output.push(176    t(177      '**Extensions may introduce unexpected behavior. Ensure you have investigated the extension source and trust the author.**',178    ),179  );180 181  if (mcpServerEntries.length) {182    output.push(t('This extension will run the following MCP servers:'));183    for (const [key, mcpServer] of mcpServerEntries) {184      const isLocal = !!mcpServer.command;185      const source =186        mcpServer.httpUrl ??187        `${mcpServer.command || ''}${mcpServer.args ? ' ' + mcpServer.args.join(' ') : ''}`;188      output.push(189        `  * ${key} (${isLocal ? t('local') : t('remote')}): ${source}`,190      );191    }192  }193  if (commands && commands.length > 0) {194    output.push(195      t('This extension will add the following commands: {{commands}}.', {196        commands: commands.join(', '),197      }),198    );199  }200  if (extensionConfig.contextFileName) {201    const fileName = Array.isArray(extensionConfig.contextFileName)202      ? extensionConfig.contextFileName.join(', ')203      : extensionConfig.contextFileName;204    output.push(205      t(206        'This extension will append info to your QWEN.md context using {{fileName}}',207        { fileName },208      ),209    );210  }211  if (skills.length > 0) {212    output.push(t('This extension will install the following skills:'));213    for (const skill of skills) {214      output.push(`  * ${chalk.bold(skill.name)}: ${skill.description}`);215    }216  }217  if (subagents.length > 0) {218    output.push(t('This extension will install the following subagents:'));219    for (const subagent of subagents) {220      output.push(`  * ${chalk.bold(subagent.name)}: ${subagent.description}`);221    }222  }223  return output.join('\n');224}225 226/**227 * Requests consent from the user to install an extension (extensionConfig), if228 * there is any difference between the consent string for `extensionConfig` and229 * `previousExtensionConfig`.230 *231 * Always requests consent if previousExtensionConfig is null.232 *233 * Throws if the user does not consent.234 */235export const requestConsentOrFail = async (236  requestConsent: (consent: string) => Promise<boolean>,237  options?: ExtensionRequestOptions,238) => {239  if (!options) return;240  const {241    extensionConfig,242    originSource = 'QwenCode',243    commands = [],244    skills = [],245    subagents = [],246    previousExtensionConfig,247    previousCommands = [],248    previousSkills = [],249    previousSubagents = [],250  } = options;251  const extensionConsent = extensionConsentString(252    extensionConfig,253    commands,254    skills,255    subagents,256    originSource,257  );258  if (previousExtensionConfig) {259    const previousExtensionConsent = extensionConsentString(260      previousExtensionConfig,261      previousCommands,262      previousSkills,263      previousSubagents,264      originSource,265    );266    if (previousExtensionConsent === extensionConsent) {267      return;268    }269  }270  if (!(await requestConsent(extensionConsent))) {271    throw new Error(272      t('Installation cancelled for "{{name}}".', {273        name: extensionConfig.name,274      }),275    );276  }277};278 
basant307/AI_Governance_Project · CoolFace