basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import * as fs from 'node:fs/promises';8import type { Extension } from '@qwen-code/qwen-code-core';9import {10 getErrorMessage,11 isSubpath,12 stripTerminalControlSequences,13} from '@qwen-code/qwen-code-core';14 15export const EXTENSION_REF_PREFIX = 'ext:';16export const EXTENSION_CONTEXT_BUDGET = 200_000;17export const EXTENSION_CONTEXT_FILE_CAP = 50_000;18 19/**20 * Parses an `ext:<name>` reference string. Returns the extension name21 * portion if the input starts with the extension prefix, or `null` otherwise.22 */23export function parseExtensionRef(pathName: string): { name: string } | null {24 if (!pathName.startsWith(EXTENSION_REF_PREFIX)) return null;25 const name = pathName.slice(EXTENSION_REF_PREFIX.length);26 if (!name) return null;27 return { name };28}29 30export function buildExtensionRef(extensionName: string): string {31 return `${EXTENSION_REF_PREFIX}${extensionName}`;32}33 34export function matchExtensionByRef(35 name: string,36 extensions: Extension[],37): Extension | undefined {38 const lower = name.toLowerCase();39 return extensions.find(40 (ext) =>41 ext.name.toLowerCase() === lower ||42 ext.config.name.toLowerCase() === lower,43 );44}45 46const BIDI_CONTROL_RE = /[]/g;47 48export function sanitizeDisplayText(raw: string): string | null {49 const stripped = stripTerminalControlSequences(raw)50 .replace(BIDI_CONTROL_RE, '')51 .replace(/\s+/g, ' ')52 .trim();53 return stripped.length > 0 ? stripped : null;54}55 56export function getExtensionDisplayName(extension: Extension): string {57 return (58 sanitizeDisplayText(extension.displayName || extension.name) ||59 extension.name60 );61}62 63export function buildExtensionContextText(extension: Extension): string {64 const displayName = getExtensionDisplayName(extension);65 const lines: string[] = [];66 67 lines.push(68 `--- Extension: ${displayName} (untrusted third-party content) ---`,69 );70 if (extension.config.description) {71 const desc = sanitizeDisplayText(extension.config.description);72 if (desc) {73 lines.push(desc);74 lines.push('');75 }76 }77 78 const capabilities: string[] = [];79 80 if (extension.skills && extension.skills.length > 0) {81 const skillNames = extension.skills82 .map((s) => sanitizeDisplayText(s.name) || s.name)83 .join(', ');84 capabilities.push(`- Skills: ${skillNames} (invoke via /<skill-name>)`);85 }86 87 if (extension.mcpServers && Object.keys(extension.mcpServers).length > 0) {88 const serverNames = Object.keys(extension.mcpServers)89 .map((n) => sanitizeDisplayText(n) || n)90 .join(', ');91 capabilities.push(`- MCP Servers: ${serverNames}`);92 }93 94 if (extension.agents && extension.agents.length > 0) {95 const agentNames = extension.agents96 .map((a) => sanitizeDisplayText(a.name) || a.name)97 .join(', ');98 capabilities.push(`- Agents: ${agentNames}`);99 }100 101 if (capabilities.length > 0) {102 lines.push('Available capabilities from this extension:');103 lines.push(...capabilities);104 lines.push('');105 }106 107 lines.push(`--- End Extension: ${displayName} ---`);108 109 return lines.join('\n');110}111 112export async function buildExtensionMentionContext(113 extension: Extension,114 options: {115 remainingBudget: number;116 signal?: AbortSignal;117 onDebugMessage?: (message: string) => void;118 },119): Promise<{ text: string; remainingBudget: number }> {120 let contextText = buildExtensionContextText(extension);121 let remainingBudget = options.remainingBudget;122 123 if (extension.contextFiles.length === 0) {124 return { text: contextText, remainingBudget };125 }126 127 const fileReads = await Promise.allSettled(128 extension.contextFiles.map(async (contextFilePath) => {129 let realPath: string;130 let realExtPath: string;131 try {132 realPath = await fs.realpath(contextFilePath);133 realExtPath = await fs.realpath(extension.path);134 } catch {135 options.onDebugMessage?.(136 `Skipping unreadable context file: ${contextFilePath}`,137 );138 return null;139 }140 if (!isSubpath(realExtPath, realPath)) {141 options.onDebugMessage?.(142 `Skipping context file outside extension directory: ${contextFilePath}`,143 );144 return null;145 }146 return fs.readFile(realPath, {147 encoding: 'utf-8',148 signal: options.signal,149 });150 }),151 );152 153 for (let i = 0; i < fileReads.length; i++) {154 const outcome = fileReads[i];155 if (outcome.status === 'rejected') {156 options.onDebugMessage?.(157 `Failed to read extension context file ${extension.contextFiles[i]}: ${getErrorMessage(outcome.reason)}`,158 );159 continue;160 }161 const content = outcome.value;162 if (!content || !content.trim()) continue;163 if (remainingBudget <= 0) {164 options.onDebugMessage?.(165 'Extension context budget exhausted, skipping remaining files.',166 );167 break;168 }169 const cap = Math.min(EXTENSION_CONTEXT_FILE_CAP, remainingBudget);170 const cappedContent =171 content.length > cap172 ? content.slice(0, cap) + '\n... (truncated)'173 : content;174 contextText += `\n\n${cappedContent}`;175 remainingBudget -= cappedContent.length;176 }177 178 return { text: contextText, remainingBudget };179}180 